1<?php
2
3/* vim: set expandtab tabstop=4 shiftwidth=4: */
4
5/**
6 * imagick PECL extension implementation for Image_Transform package
7 *
8 * PHP version 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 * @subpackage Image_Transform_Driver_Imagick3
19 * @author     Philippe Jausions <Philippe.Jausions@11abacus.com>
20 * @copyright  2007 The PHP Group
21 * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
22 * @version    CVS: $Id: Imagick3.php 266908 2008-10-01 21:11:07Z dufuz $
23 * @link       http://pear.php.net/package/Image_Transform
24 */
25
26require_once 'Image/Transform.php';
27
28/**
29 * imagick PECL extension implementation for Image_Transform package
30 *
31 * For use of version 2+ of the extension. For version 0.9.* use Imagick2 driver
32 * instead
33 *
34 * @category   Image
35 * @package    Image_Transform
36 * @subpackage Image_Transform_Driver_Imagick3
37 * @author     Philippe Jausions <Philippe.Jausions@11abacus.com>
38 * @copyright  2007 The PHP Group
39 * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
40 * @version    Release: @package_version@
41 * @link       http://pear.php.net/package/Image_Transform
42 * @since      0.9.2
43 * @since      PHP 5.1.3
44 * @since      PECL Imagick 2.0.0a1
45 */
46class Image_Transform_Driver_Imagick3 extends Image_Transform
47{
48    /**
49     * Instance of imagick
50     * @var Imagick
51     */
52    var $imagick = null;
53
54    /**
55     * Handler of the image resource before
56     * the last transformation
57     * @var array
58     */
59    var $oldImage;
60
61    /**
62     * @see __construct()
63     */
64    function Image_Transform_Driver_Imagick3()
65    {
66        $this->__construct();
67    }
68
69    /**
70     * @see http://www.imagemagick.org/www/formats.html
71     */
72    function __construct()
73    {
74        if (PEAR::loadExtension('imagick')) {
75            include 'Image/Transform/Driver/Imagick/ImageTypes.php';
76        } else {
77            $this->isError(PEAR::raiseError('Could not find the imagick extension.',
78                IMAGE_TRANSFORM_ERROR_UNSUPPORTED));
79        }
80    }
81
82    /**
83     * Loads an image
84     *
85     * @param string $image filename
86     *
87     * @return bool|PEAR_Error TRUE or a PEAR_Error object on error
88     * @access public
89     */
90    function load($image)
91    {
92        $this->free();
93        $this->imagick = new Imagick();
94        try {
95            $this->imagick->readImage($image);
96
97        } catch (ImagickException $e) {
98            $this->free();
99            return $this->raiseError('Could not load image:'.$e->getMessage(),
100                IMAGE_TRANSFORM_ERROR_IO);
101        }
102
103        $this->image = $image;
104        $result = $this->_get_image_details($image);
105        if (PEAR::isError($result)) {
106            return $result;
107        }
108
109        return true;
110    }
111
112    /**
113     * Resizes the image
114     *
115     * @param integer $new_x   New width
116     * @param integer $new_y   New height
117     * @param mixed $options Optional parameters
118     * <ul>
119     *  <li>'scaleMethod': "pixel" or "smooth"</li>
120     * </ul>
121     *
122     * @return bool|PEAR_Error TRUE or PEAR_Error object on error
123     * @access protected
124     */
125    function _resize($new_x, $new_y, $options = null)
126    {
127        try {
128            $scaleMethod = $this->_getOption('scaleMethod', $options, 'smooth');
129            $blur = ($scaleMethod == 'pixel') ? 0 : 1;
130            $this->imagick->resizeImage($new_x, $new_y,
131                                        imagick::FILTER_UNDEFINED, $blur);
132
133        } catch (ImagickException $e) {
134            return $this->raiseError('Could not resize image.',
135                IMAGE_TRANSFORM_ERROR_FAILED);
136        }
137
138        $this->new_x = $new_x;
139        $this->new_y = $new_y;
140        return true;
141
142    } // End resize
143
144    /**
145     * Rotates the current image
146     *
147     * @param float $angle Rotation angle in degree
148     * @param array $options Supported options:
149     * <ul>
150     *  <li>'canvasColor' : array(r ,g, b), named color or #rrggbb</li>
151     * </ul>
152     *
153     * @return bool|PEAR_Error TRUE or a PEAR_Error object on error
154     * @access public
155     */
156    function rotate($angle, $options = null)
157    {
158        if (($angle % 360) == 0) {
159            return true;
160        }
161        $color = $this->_getColor('canvasColor', $options, array(255, 255, 255));
162        if (is_array($color)) {
163            $color = $this->colorarray2colorhex($color);
164        }
165        $pixel = new ImagickPixel($color);
166        try {
167            $this->imagick->rotateImage($pixel, $angle);
168
169        } catch (ImagickException $e) {
170            return $this->raiseError('Cannot create a new imagick image for the rotation: '.$e->getMessage(),
171                IMAGE_TRANSFORM_ERROR_FAILED);
172        }
173        $info = $this->imagick->getImageGeometry();
174        $this->new_x = $info['width'];
175        $this->new_y = $info['height'];
176        return true;
177
178    } // End rotate
179
180    /**
181     * Adds text to the image
182     *
183     * @param   array   $params Array contains options:
184     * <ul>
185     *  <li>'text' (string) The string to draw</li>
186     *  <li>'x'    (integer) Horizontal position</li>
187     *  <li>'y'    (integer) Vertical Position</li>
188     *  <li>'Color' (mixed) Font color</li>
189     *  <li>'font' (string) Font to be used</li>
190     *  <li>'size' (integer) Size of the fonts in pixel</li>
191     * </ul>
192     *
193     * @return bool|PEAR_Error TRUE or a PEAR_Error object on error
194     * @access public
195     */
196    function addText($params)
197    {
198        $this->oldImage = clone $this->imagick;
199        $params = array_merge($this->_get_default_text_params(), $params);
200
201        if (is_array($params['color'])) {
202            $params['color'] = $this->colorarray2colorhex($params['color']);
203        } else {
204            $params['color'] = strtolower($params['color']);
205        }
206
207        static $cmds = array(
208            'setFillColor' => 'color',
209            'setFontSize'  => 'size',
210            'setFontFace'  => 'font'
211        );
212        $this->imagick->beginDraw();
213
214        foreach ($cmds as $cmd => $v) {
215            if (!$this->imagick->$cmd($params[$v])) {
216                return $this->raiseError("Problem with adding Text::{$v} = {$params[$v]}",
217                    IMAGE_TRANSFORM_ERROR_FAILED);
218            }
219        }
220        if (!$this->imagick->drawAnnotation($params['x'], $params['y'], $params['text'])) {
221            return $this->raiseError('Problem with adding Text',
222                IMAGE_TRANSFORM_ERROR_FAILED);
223        }
224
225        return true;
226
227    } // End addText
228
229
230    /**
231     * Saves the image to a file
232     *
233     * @param $filename string the name of the file to write to
234     *
235     * @return bool|PEAR_Error TRUE or a PEAR_Error object on error
236     * @access public
237     */
238    function save($filename, $type = '', $quality = null)
239    {
240        $options = (is_array($quality)) ? $quality : array();
241        if (is_numeric($quality)) {
242            $options['quality'] = $quality;
243        }
244        $quality = $this->_getOption('quality', $options, 75);
245        $this->imagick->setImageCompression($quality);
246
247        if ($type && strcasecmp($type, $this->type)) {
248            try {
249                $this->imagick->setImageFormat($type);
250
251            } catch (ImagickException $e) {
252                return $this->raiseError('Could not save image to file (conversion failed).',
253                IMAGE_TRANSFORM_ERROR_FAILED);
254            }
255        }
256
257        try {
258            $this->imagick->writeImage($filename);
259
260        } catch (ImagickException $e) {
261            return $this->raiseError('Could not save image to file: '.$e->getMessage(),
262                IMAGE_TRANSFORM_ERROR_IO);
263        }
264
265        if (!$this->keep_settings_on_save) {
266            $this->free();
267        }
268
269        return true;
270
271    } // End save
272
273    /**
274     * Displays image without saving and lose changes
275     *
276     * This method adds the Content-type HTTP header
277     *
278     * @param string type (JPG,PNG...);
279     * @param int quality 75
280     *
281     * @return bool|PEAR_Error TRUE or a PEAR_Error object on error
282     * @access public
283     */
284    function display($type = '', $quality = null)
285    {
286        $options = (is_array($quality)) ? $quality : array();
287        if (is_numeric($quality)) {
288            $options['quality'] = $quality;
289        }
290        $quality = $this->_getOption('quality', $options, 75);
291        $this->imagick->setImageCompression($quality);
292
293        if ($type && strcasecmp($type, $this->type)) {
294            try {
295                $this->imagick->setImageFormat($type);
296
297            } catch (ImagickException $e) {
298                return $this->raiseError('Could not save image to file (conversion failed).',
299                IMAGE_TRANSFORM_ERROR_FAILED);
300            }
301        }
302        try {
303            $image = $this->imagick->getImageBlob();
304
305        } catch (ImagickException $e) {
306            return $this->raiseError('Could not display image.',
307                IMAGE_TRANSFORM_ERROR_IO);
308        }
309        header('Content-type: ' . $this->getMimeType($type));
310        echo $image;
311        $this->free();
312        return true;
313    }
314
315    /**
316     * Adjusts the image gamma
317     *
318     * @param float $outputgamma
319     * @return bool|PEAR_Error TRUE or a PEAR_Error object on error
320     * @access public
321     */
322    function gamma($outputgamma = 1.0) {
323        if ($outputgamma != 1.0) {
324            $this->imagick->setImageGamma($outputgamma);
325        }
326        return true;
327    }
328
329    /**
330     * Crops the image
331     *
332     * @param integer $width Cropped image width
333     * @param integer $height Cropped image height
334     * @param integer $x X-coordinate to crop at
335     * @param integer $y Y-coordinate to crop at
336     *
337     * @return bool|PEAR_Error TRUE or a PEAR_Error object on error
338     * @access public
339     */
340    function crop($width, $height, $x = 0, $y = 0)
341    {
342        // Sanity check
343        if (!$this->intersects($width, $height, $x, $y)) {
344            return PEAR::raiseError('Nothing to crop', IMAGE_TRANSFORM_ERROR_OUTOFBOUND);
345        }
346        try {
347            $this->imagick->cropImage($width, $height, $x, $y);
348
349        } catch (ImagickException $e) {
350            return $this->raiseError('Could not crop image',
351                IMAGE_TRANSFORM_ERROR_FAILED);
352        }
353
354        // I think that setting img_x/y is wrong, but scaleByLength() & friends
355        // mess up the aspect after a crop otherwise.
356        $this->new_x = $width;
357        $this->new_y = $height;
358
359        return true;
360    }
361
362    /**
363     * Converts the image to greyscale
364     *
365     * @return bool|PEAR_Error TRUE or a PEAR_Error object on error
366     * @access public
367     */
368    function greyscale() {
369        $this->imagick->setImageType(Imagick::IMGTYPE_GRAYSCALE);
370        /*$this->imagick->setImageColorSpace(Imagick::COLORSPACE_GRAY);
371        $this->imagick->setImageDepth(8);
372        $this->imagick->separateImageChannel(Imagick::CHANNEL_GRAY);
373        $this->imagick->setImageChannelDepth(Imagick::CHANNEL_GRAY, 8);*/
374        return true;
375    }
376
377    /**
378     * Horizontal mirroring
379     *
380     * @return bool|PEAR_Error TRUE or a PEAR_Error object on error
381     * @access public
382     */
383    function mirror()
384    {
385        try {
386            $this->imagick->flopImage();
387        } catch (ImagickException $e) {
388            return $this->raiseError('Could not mirror the image.',
389                IMAGE_TRANSFORM_ERROR_FAILED);
390        }
391        return true;
392    }
393
394    /**
395     * Vertical mirroring
396     *
397     * @return bool|PEAR_Error TRUE or a PEAR_Error object on error
398     * @access public
399     */
400    function flip()
401    {
402        try {
403            $this->imagick->flipImage();
404
405        } catch (ImagickException $e) {
406            return $this->raiseError('Could not flip the image.',
407                IMAGE_TRANSFORM_ERROR_FAILED);
408        }
409        return true;
410    }
411
412    /**
413     * Destroy image handle
414     *
415     * @access public
416     */
417    function free()
418    {
419        if (isset($this->imagick)) {
420            $this->imagick->destroy();
421            $this->imagick = null;
422        }
423    }
424
425    /**
426     * RaiseError Method - shows imagick Raw errors.
427     *
428     * @param string $message message = prefixed message..
429     * @param int    $code error code
430     * @return PEAR error object
431     * @access protected
432     */
433    function raiseError($message, $code = 0)
434    {
435        return PEAR::raiseError($message, $code);
436    }
437}