1<?php
2
3/* vim: set expandtab tabstop=4 shiftwidth=4: */
4
5/**
6 * ImageMagick binaries implementation for Image_Transform package
7 *
8 * PHP versions 4 and 5
9 *
10 * LICENSE: This source file is subject to version 3.0 of the PHP license
11 * that is available through the world-wide-web at the following URI:
12 * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
13 * the PHP License and are unable to obtain it through the web, please
14 * send a note to license@php.net so we can mail you a copy immediately.
15 *
16 * @category   Image
17 * @package    Image_Transform
18 * @author     Peter Bowyer <peter@mapledesign.co.uk>
19 * @author     Philippe Jausions <Philippe.Jausions@11abacus.com>
20 * @copyright  2002-2005 The PHP Group
21 * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
22 * @version    CVS: $Id: IM.php 266859 2008-09-30 22:28:47Z dufuz $
23 * @link       http://pear.php.net/package/Image_Transform
24 */
25
26require_once 'Image/Transform.php';
27require_once 'System.php';
28
29/**
30 * ImageMagick binaries implementation for Image_Transform package
31 *
32 * @category   Image
33 * @package    Image_Transform
34 * @subpackage Image_Transform_Driver_IM
35 * @author     Peter Bowyer <peter@mapledesign.co.uk>
36 * @author     Philippe Jausions <Philippe.Jausions@11abacus.com>
37 * @copyright  2002-2005 The PHP Group
38 * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
39 * @version    Release: @package_version@
40 * @link       http://pear.php.net/package/Image_Transform
41 * @link http://www.imagemagick.org/
42 **/
43class Image_Transform_Driver_IM extends Image_Transform
44{
45    /**
46     * associative array commands to be executed
47     * @var array
48     * @access private
49     */
50    var $command;
51
52    /**
53     * Class constructor
54     */
55    function Image_Transform_Driver_IM()
56    {
57        $this->__construct();
58    } // End Image_IM
59
60    /**
61     * Class constructor
62     */
63    function __construct()
64    {
65        $this->_init();
66        if (!defined('IMAGE_TRANSFORM_IM_PATH')) {
67            $path = dirname(System::which('convert'))
68                    . DIRECTORY_SEPARATOR;
69            define('IMAGE_TRANSFORM_IM_PATH', $path);
70        }
71        if (System::which(IMAGE_TRANSFORM_IM_PATH . 'convert' . ((OS_WINDOWS) ? '.exe' : ''))) {
72            include 'Image/Transform/Driver/Imagick/ImageTypes.php';
73        } else {
74            $this->isError(PEAR::raiseError('Couldn\'t find "convert" binary',
75                IMAGE_TRANSFORM_ERROR_UNSUPPORTED));
76        }
77    } // End Image_IM
78
79    /**
80     * Initialize the state of the object
81     **/
82    function _init()
83    {
84        $this->command = array();
85    }
86
87    /**
88     * Load an image.
89     *
90     * This method doesn't support remote files.
91     *
92     * @param string filename
93     *
94     * @return mixed TRUE or a PEAR error object on error
95     * @see PEAR::isError()
96     */
97    function load($image)
98    {
99        $this->_init();
100        if (!file_exists($image)) {
101            return PEAR::raiseError('The image file ' . $image
102                . ' doesn\'t exist', IMAGE_TRANSFORM_ERROR_IO);
103        }
104        $this->image = $image;
105        $result = $this->_get_image_details($image);
106        if (PEAR::isError($result)) {
107            return $result;
108        }
109        return true;
110
111    } // End load
112
113    /**
114     * Image_Transform_Driver_IM::_get_image_details()
115     *
116     * @param string $image the path and name of the image file
117     * @return none
118     */
119    function _get_image_details($image)
120    {
121        $retval = Image_Transform::_get_image_details($image);
122        if (PEAR::isError($retval)) {
123            unset($retval);
124
125            if (!System::which(IMAGE_TRANSFORM_IM_PATH . 'identify' . ((OS_WINDOWS) ? '.exe' : ''))) {
126                $this->isError(PEAR::raiseError('Couldn\'t find "identify" binary', IMAGE_TRANSFORM_ERROR_UNSUPPORTED));
127            }
128            $cmd = $this->_prepare_cmd(IMAGE_TRANSFORM_IM_PATH,
129                'identify',
130                '-format %w:%h:%m ' . escapeshellarg($image));
131            exec($cmd, $res, $exit);
132
133            if ($exit != 0) {
134                return PEAR::raiseError("Cannot fetch image or images details.", true);
135            }
136
137            $data  = explode(':', $res[0]);
138            $this->img_x = $data[0];
139            $this->img_y = $data[1];
140            $this->type  = strtolower($data[2]);
141            $retval = true;
142
143        }
144
145        return $retval;
146    }
147
148    /**
149     * Resize the image.
150     *
151     * @access private
152     *
153     * @param int   $new_x   New width
154     * @param int   $new_y   New height
155     * @param mixed $options Optional parameters
156     *
157     * @return true on success or PEAR Error object on error
158     * @see PEAR::isError()
159     */
160    function _resize($new_x, $new_y, $options = null)
161    {
162        if (isset($this->command['resize'])) {
163            return PEAR::raiseError('You cannot scale or resize an image more than once without calling save() or display()', true);
164        }
165        $this->command['resize'] = '-geometry '
166            . ((int) $new_x) . 'x' . ((int) $new_y) . '!';
167
168        $this->new_x = $new_x;
169        $this->new_y = $new_y;
170
171        return true;
172    } // End resize
173
174    /**
175     * rotate
176     *
177     * @param   int     angle   rotation angle
178     * @param   array   options no option allowed
179     * @return mixed TRUE or a PEAR error object on error
180     */
181    function rotate($angle, $options = null)
182    {
183        $angle = $this->_rotation_angle($angle);
184        if ($angle % 360) {
185            $this->command['rotate'] = '-rotate ' . (float) $angle;
186        }
187        return true;
188
189    } // End rotate
190
191    /**
192     * Crop image
193     *
194     * @author Ian Eure <ieure@websprockets.com>
195     * @since 0.8
196     *
197     * @param int width Cropped image width
198     * @param int height Cropped image height
199     * @param int x X-coordinate to crop at
200     * @param int y Y-coordinate to crop at
201     *
202     * @return mixed TRUE or a PEAR error object on error
203     */
204    function crop($width, $height, $x = 0, $y = 0) {
205        // Do we want a safety check - i.e. if $width+$x > $this->img_x then we
206        // raise a warning? [and obviously same for $height+$y]
207        $this->command['crop'] = '-crop '
208            . ((int) $width)  . 'x' . ((int) $height)
209            . '+' . ((int) $x) . '+' . ((int) $y) . '!';
210
211        // I think that setting img_x/y is wrong, but scaleByLength() & friends
212        // mess up the aspect after a crop otherwise.
213        $this->new_x = $this->img_x = $width - $x;
214        $this->new_y = $this->img_y = $height - $y;
215
216        return true;
217    }
218
219    /**
220     * addText
221     *
222     * @param   array   options     Array contains options
223     *                              array(
224     *                                  'text'  The string to draw
225     *                                  'x'     Horizontal position
226     *                                  'y'     Vertical Position
227     *                                  'Color' Font color
228     *                                  'font'  Font to be used
229     *                                  'size'  Size of the fonts in pixel
230     *                                  'resize_first'  Tell if the image has to be resized
231     *                                                  before drawing the text
232     *                              )
233     *
234     * @return mixed TRUE or a PEAR error object on error
235     * @see PEAR::isError()
236     */
237    function addText($params)
238    {
239        $this->old_image = $this->imageHandle;
240         $params = array_merge($this->_get_default_text_params(), $params);
241         extract($params);
242
243         if (true === $resize_first) {
244             // Set the key so that this will be the last item in the array
245            $key = 'ztext';
246         } else {
247            $key = 'text';
248         }
249         $this->command[$key] = '-font ' . escapeshellarg($font)
250            . ' -fill ' . escapeshellarg($color)
251            . ' -draw \'text ' . escapeshellarg($x . ',' . $y)
252            . ' "' . escapeshellarg($text) . '"\'';
253         // Producing error: gs: not found gs: not found convert: Postscript delegate failed [No such file or directory].
254        return true;
255
256    } // End addText
257
258    /**
259     * Adjust the image gamma
260     *
261     * @access public
262     * @param float $outputgamma
263     * @return mixed TRUE or a PEAR error object on error
264     */
265    function gamma($outputgamma = 1.0) {
266        if ($outputgamme != 1.0) {
267            $this->command['gamma'] = '-gamma ' . (float) $outputgamma;
268        }
269        return true;
270    }
271
272    /**
273     * Convert the image to greyscale
274     *
275     * @access public
276     * @return mixed TRUE or a PEAR error object on error
277     */
278    function greyscale() {
279        $this->command['type'] = '-type Grayscale';
280        return true;
281    }
282
283    /**
284     * Horizontal mirroring
285     *
286     * @access public
287     * @return TRUE or PEAR Error object on error
288     */
289    function mirror() {
290        // We can only apply "flop" once
291        if (isset($this->command['flop'])) {
292            unset($this->command['flop']);
293        } else {
294            $this->command['flop'] = '-flop';
295        }
296        return true;
297    }
298
299    /**
300     * Vertical mirroring
301     *
302     * @access public
303     * @return TRUE or PEAR Error object on error
304     */
305    function flip() {
306        // We can only apply "flip" once
307        if (isset($this->command['flip'])) {
308            unset($this->command['flip']);
309        } else {
310            $this->command['flip'] = '-flip';
311        }
312        return true;
313    }
314
315    /**
316     * Save the image file
317     *
318     * @access public
319     *
320     * @param $filename string  the name of the file to write to
321     * @param $quality  quality image dpi, default=75
322     * @param $type     string  (JPEG, PNG...)
323     *
324     * @return mixed TRUE or a PEAR error object on error
325     */
326    function save($filename, $type = '', $quality = null)
327    {
328        $type = strtoupper(($type == '') ? $this->type : $type);
329        switch ($type) {
330            case 'JPEG':
331                $type = 'JPG';
332                break;
333        }
334        $options = array();
335        if (!is_null($quality)) {
336            $options['quality'] = $quality;
337        }
338        $quality = $this->_getOption('quality', $options, 75);
339
340        $cmd = $this->_prepare_cmd(
341            IMAGE_TRANSFORM_IM_PATH,
342            'convert',
343            implode(' ', $this->command)
344               . ' -quality ' . ((int) $quality) . ' '
345               . escapeshellarg($this->image) . ' ' . $type . ':'
346               . escapeshellarg($filename) . ' 2>&1');
347        exec($cmd, $res, $exit);
348
349        if (!$this->keep_settings_on_save) {
350            $this->free();
351        }
352
353        return ($exit == 0) ? true : PEAR::raiseError(implode('. ', $res),
354            IMAGE_TRANSFORM_ERROR_IO);
355    } // End save
356
357    /**
358     * Display image without saving and lose changes
359     *
360     * This method adds the Content-type HTTP header
361     *
362     * @access public
363     *
364     * @param string type (JPEG,PNG...);
365     * @param int quality 75
366     *
367     * @return mixed TRUE or a PEAR error object on error
368     */
369    function display($type = '', $quality = null)
370    {
371        $type    = strtoupper(($type == '') ? $this->type : $type);
372        switch ($type) {
373            case 'JPEG':
374                $type = 'JPG';
375                break;
376        }
377        $options = array();
378        if (!is_null($quality)) {
379            $options['quality'] = $quality;
380        }
381        $quality = $this->_getOption('quality', $options, 75);
382
383        $this->_send_display_headers($type);
384
385        $cmd = $this->_prepare_cmd(
386            IMAGE_TRANSFORM_IM_PATH,
387            'convert',
388            implode(' ', $this->command) . " -quality $quality "  .
389                   $this->image . ' ' . $type . ":-");
390        passthru($cmd);
391
392        if (!$this->keep_settings_on_save) {
393            $this->free();
394        }
395        return true;
396    }
397
398    /**
399     * Destroy image handle
400     *
401     * @return void
402     */
403    function free()
404    {
405        $this->command = array();
406        $this->image = '';
407        $this->type = '';
408    }
409
410} // End class ImageIM
411