1<?php
2
3/* vim: set expandtab tabstop=4 shiftwidth=4: */
4
5/**
6 * GD 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 * @subpackage Image_Transform_Driver_GD
19 * @author     Alan Knowles <alan@akbkhome.com>
20 * @author     Peter Bowyer <peter@mapledesign.co.uk>
21 * @author     Philippe Jausions <Philippe.Jausions@11abacus.com>
22 * @copyright  2002-2005 The PHP Group
23 * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
24 * @version    CVS: $Id: GD.php 322661 2012-01-24 12:02:59Z clockwerx $
25 * @link       http://pear.php.net/package/Image_Transform
26 */
27
28require_once 'Image/Transform.php';
29
30/**
31 * GD implementation for Image_Transform package
32 *
33 * Usage :
34 *    $img    =& Image_Transform::factory('GD');
35 *    $angle  = -78;
36 *    $img->load('magick.png');
37 *
38 *    if ($img->rotate($angle, array(
39 *               'autoresize' => true,
40 *               'color_mask' => array(255, 0, 0)))) {
41 *        $img->addText(array(
42 *               'text' => 'Rotation ' . $angle,
43 *               'x' => 0,
44 *               'y' => 100,
45 *               'font' => '/usr/share/fonts/default/TrueType/cogb____.ttf'));
46 *        $img->display();
47 *    } else {
48 *        echo "Error";
49 *    }
50 *    $img->free();
51 *
52 * @category   Image
53 * @package    Image_Transform
54 * @subpackage Image_Transform_Driver_GD
55 * @author     Alan Knowles <alan@akbkhome.com>
56 * @author     Peter Bowyer <peter@mapledesign.co.uk>
57 * @author     Philippe Jausions <Philippe.Jausions@11abacus.com>
58 * @copyright  2002-2005 The PHP Group
59 * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
60 * @version    Release: @package_version@
61 * @link       http://pear.php.net/package/Image_Transform
62 * @since      PHP 4.0
63 */
64class Image_Transform_Driver_GD extends Image_Transform
65{
66    /**
67     * Holds the image resource for manipulation
68     *
69     * @var resource $imageHandle
70     * @access protected
71     */
72    var $imageHandle = null;
73
74    /**
75     * Holds the original image file
76     *
77     * @var resource $imageHandle
78     * @access protected
79     */
80    var $oldImage = null;
81
82    /**
83     * Check settings
84     */
85    function Image_Transform_Driver_GD()
86    {
87        $this->__construct();
88    } // End function Image
89
90    /**
91     * Check settings
92     *
93     * @since PHP 5
94     */
95    function __construct()
96    {
97        if (!PEAR::loadExtension('gd')) {
98            $this->isError(PEAR::raiseError("GD library is not available.",
99                IMAGE_TRANSFORM_ERROR_UNSUPPORTED));
100        } else {
101            $types = ImageTypes();
102            if ($types & IMG_PNG) {
103                $this->_supported_image_types['png'] = 'rw';
104            }
105            if (($types & IMG_GIF)
106                || function_exists('imagegif')) {
107                $this->_supported_image_types['gif'] = 'rw';
108            } elseif (function_exists('imagecreatefromgif')) {
109                $this->_supported_image_types['gif'] = 'rw';
110            }
111            if ($types & IMG_JPG) {
112                $this->_supported_image_types['jpeg'] = 'rw';
113            }
114            if ($types & IMG_WBMP) {
115                $this->_supported_image_types['wbmp'] = 'rw';
116            }
117            if (!$this->_supported_image_types) {
118                $this->isError(PEAR::raiseError("No supported image types available", IMAGE_TRANSFORM_ERROR_UNSUPPORTED));
119            }
120        }
121
122    } // End function Image
123
124    /**
125     * Loads an image from file
126     *
127     * @param string $image filename
128     * @return bool|PEAR_Error TRUE or a PEAR_Error object on error
129     * @access public
130     */
131    function load($image)
132    {
133        $this->free();
134
135        $this->image = $image;
136        $result = $this->_get_image_details($image);
137        if (PEAR::isError($result)) {
138            return $result;
139        }
140        if (!$this->supportsType($this->type, 'r')) {
141            return PEAR::raiseError('Image type not supported for input',
142                IMAGE_TRANSFORM_ERROR_UNSUPPORTED);
143        }
144
145        $functionName = 'ImageCreateFrom' . $this->type;
146        $this->imageHandle = $functionName($this->image);
147        if (!$this->imageHandle) {
148            $this->imageHandle = null;
149            return PEAR::raiseError('Error while loading image file.',
150                IMAGE_TRANSFORM_ERROR_IO);
151        }
152        return true;
153
154    } // End load
155
156    /**
157     * Returns the GD image handle
158     *
159     * @return resource
160     *
161     * @access public
162     */
163    function getHandle()
164    {
165        return $this->imageHandle;
166    }//function getHandle()
167
168    /**
169     * Adds a border of constant width around an image
170     *
171     * @param int $border_width Width of border to add
172     * @author Peter Bowyer
173     * @return bool TRUE
174     * @access public
175     */
176    function addBorder($border_width, $color = '')
177    {
178        $this->new_x = $this->img_x + 2 * $border_width;
179        $this->new_y = $this->img_y + 2 * $border_width;
180
181        $new_img = $this->_createImage($new_x, $new_y, $this->true_color);
182
183        $options = array('pencilColor', $color);
184        $color = $this->_getColor('pencilColor', $options, array(0, 0, 0));
185        if ($color) {
186            if ($this->true_color) {
187                $c = imagecolorresolve($this->imageHandle, $color[0], $color[1], $color[2]);
188                imagefill($new_img, 0, 0, $c);
189            } else {
190                imagecolorset($new_img, imagecolorat($new_img, 0, 0), $color[0], $color[1], $color[2]);
191            }
192        }
193        ImageCopy($new_img, $this->imageHandle, $border_width, $border_width, 0, 0, $this->img_x, $this->img_y);
194        $this->imageHandle = $new_img;
195        $this->resized = true;
196
197        return true;
198    }
199
200    /**
201     * addText
202     *
203     * @param   array   $params     Array contains options
204     *                              array(
205     *                                  'text'  The string to draw
206     *                                  'x'     Horizontal position
207     *                                  'y'     Vertical Position
208     *                                  'color' Font color
209     *                                  'font'  Font to be used
210     *                                  'size'  Size of the fonts in pixel
211     *                                  'resize_first'  Tell if the image has to be resized
212     *                                                  before drawing the text
213     *                              )
214     *
215     * @return bool|PEAR_Error TRUE or a PEAR_Error object on error
216     */
217    function addText($params)
218    {
219        $this->oldImage = $this->imageHandle;
220        $params = array_merge($this->_get_default_text_params(), $params);
221        extract($params);
222
223        $options = array('fontColor' => $color);
224        $color = $this->_getColor('fontColor', $options, array(0, 0, 0));
225
226        $c = imagecolorresolve ($this->imageHandle, $color[0], $color[1], $color[2]);
227
228        if ('ttf' == substr($font, -3)) {
229            ImageTTFText($this->imageHandle, $size, $angle, $x, $y, $c, $font, $text);
230        } else {
231            ImagePSText($this->imageHandle, $size, $angle, $x, $y, $c, $font, $text);
232        }
233
234        return true;
235    } // End addText
236
237    /**
238     * Rotates image by the given angle
239     *
240     * Uses a fast rotation algorythm for custom angles
241     * or lines copy for multiple of 90 degrees
242     *
243     * @param int   $angle   Rotation angle
244     * @param array $options array(
245     *                             'canvasColor' => array(r ,g, b), named color or #rrggbb
246     *                            )
247     * @author Pierre-Alain Joye
248     * @return bool|PEAR_Error TRUE or a PEAR_Error object on error
249     * @access public
250     */
251    function rotate($angle, $options = null)
252    {
253        if (($angle % 360) == 0) {
254            return true;
255        }
256
257        $color_mask = $this->_getColor('canvasColor', $options,
258                                        array(255, 255, 255));
259
260        $mask = imagecolorresolve($this->imageHandle, $color_mask[0], $color_mask[1], $color_mask[2]);
261
262        $this->oldImage = $this->imageHandle;
263
264        // Multiply by -1 to change the sign, so the image is rotated clockwise
265        $this->imageHandle = ImageRotate($this->imageHandle, $angle * -1, $mask);
266        return true;
267    }
268
269    /**
270     * Horizontal mirroring
271     *
272     * @return mixed TRUE or PEAR_Error object on error
273     * @access public
274     * @see flip()
275     **/
276    function mirror()
277    {
278        $new_img = $this->_createImage();
279        for ($x = 0; $x < $this->new_x; ++$x) {
280            imagecopy($new_img, $this->imageHandle, $x, 0,
281                $this->new_x - $x - 1, 0, 1, $this->new_y);
282        }
283        imagedestroy($this->imageHandle);
284        $this->imageHandle = $new_img;
285        return true;
286    }
287
288    /**
289     * Vertical mirroring
290     *
291     * @return TRUE or PEAR Error object on error
292     * @access public
293     * @see mirror()
294     **/
295    function flip()
296    {
297        $new_img = $this->_createImage();
298        for ($y = 0; $y < $this->new_y; ++$y) {
299            imagecopy($new_img, $this->imageHandle, 0, $y,
300                0, $this->new_y - $y - 1, $this->new_x, 1);
301        }
302        imagedestroy($this->imageHandle);
303        $this->imageHandle = $new_img;
304
305        /* for very large images we may want to use the following
306           Needs to find out what is the threshhold
307        for ($x = 0; $x < $this->new_x; ++$x) {
308            for ($y1 = 0; $y1 < $this->new_y / 2; ++$y1) {
309                $y2 = $this->new_y - 1 - $y1;
310                $color1 = imagecolorat($this->imageHandle, $x, $y1);
311                $color2 = imagecolorat($this->imageHandle, $x, $y2);
312                imagesetpixel($this->imageHandle, $x, $y1, $color2);
313                imagesetpixel($this->imageHandle, $x, $y2, $color1);
314            }
315        } */
316        return true;
317    }
318
319    /**
320     * Crops image by size and start coordinates
321     *
322     * @param int width Cropped image width
323     * @param int height Cropped image height
324     * @param int x X-coordinate to crop at
325     * @param int y Y-coordinate to crop at
326     * @return bool|PEAR_Error TRUE or a PEAR_Error object on error
327     * @access public
328     */
329    function crop($width, $height, $x = 0, $y = 0)
330    {
331        // Sanity check
332        if (!$this->intersects($width, $height, $x, $y)) {
333            return PEAR::raiseError('Nothing to crop', IMAGE_TRANSFORM_ERROR_OUTOFBOUND);
334        }
335        $x = min($this->new_x, max(0, $x));
336        $y = min($this->new_y, max(0, $y));
337        $width   = min($width,  $this->new_x - $x);
338        $height  = min($height, $this->new_y - $y);
339        $new_img = $this->_createImage($width, $height);
340
341        if (!imagecopy($new_img, $this->imageHandle, 0, 0, $x, $y, $width, $height)) {
342            imagedestroy($new_img);
343            return PEAR::raiseError('Failed transformation: crop()',
344                IMAGE_TRANSFORM_ERROR_FAILED);
345        }
346
347        $this->oldImage = $this->imageHandle;
348        $this->imageHandle = $new_img;
349        $this->resized = true;
350
351        $this->new_x = $width;
352        $this->new_y = $height;
353        return true;
354    }
355
356    /**
357     * Converts the image to greyscale
358     *
359     * @return bool|PEAR_Error TRUE or a PEAR_Error object on error
360     * @access public
361     */
362    function greyscale() {
363        imagecopymergegray($this->imageHandle, $this->imageHandle, 0, 0, 0, 0, $this->new_x, $this->new_y, 0);
364        return true;
365    }
366
367    /**
368     * Resize Action
369     *
370     * For GD 2.01+ the new copyresampled function is used
371     * It uses a bicubic interpolation algorithm to get far
372     * better result.
373     *
374     * @param int   $new_x   New width
375     * @param int   $new_y   New height
376     * @param array $options Optional parameters
377     * <ul>
378     *  <li>'scaleMethod': "pixel" or "smooth"</li>
379     * </ul>
380     *
381     * @return bool|PEAR_Error TRUE on success or PEAR_Error object on error
382     * @access protected
383     */
384    function _resize($new_x, $new_y, $options = null)
385    {
386        if ($this->resized === true) {
387            return PEAR::raiseError('You have already resized the image without saving it.  Your previous resizing will be overwritten', null, PEAR_ERROR_TRIGGER, E_USER_NOTICE);
388        }
389
390        if ($this->new_x == $new_x && $this->new_y == $new_y) {
391            return true;
392        }
393
394        $scaleMethod = $this->_getOption('scaleMethod', $options, 'smooth');
395
396        // Make sure to get a true color image if doing resampled resizing
397        // otherwise get the same type of image
398        $trueColor = ($scaleMethod == 'pixel') ? null : true;
399        $new_img = $this->_createImage($new_x, $new_y, $trueColor);
400
401        $icr_res = null;
402        if ($scaleMethod != 'pixel' && function_exists('ImageCopyResampled')) {
403            $icr_res = ImageCopyResampled($new_img, $this->imageHandle, 0, 0, 0, 0, $new_x, $new_y, $this->img_x, $this->img_y);
404        }
405        if (!$icr_res) {
406            ImageCopyResized($new_img, $this->imageHandle, 0, 0, 0, 0, $new_x, $new_y, $this->img_x, $this->img_y);
407        }
408        $this->oldImage = $this->imageHandle;
409        $this->imageHandle = $new_img;
410        $this->resized = true;
411
412        $this->new_x = $new_x;
413        $this->new_y = $new_y;
414        return true;
415    }
416
417    /**
418     * Adjusts the image gamma
419     *
420     * @param float $outputgamma
421     *
422     * @return bool|PEAR_Error TRUE or a PEAR_Error object on error
423     * @access public
424     */
425    function gamma($outputgamma = 1.0)
426    {
427        if ($outputgamma != 1.0) {
428            ImageGammaCorrect($this->imageHandle, 1.0, $outputgamma);
429        }
430        return true;
431    }
432
433    /**
434     * Helper method to save to a file or output the image
435     *
436     * @param string $filename the name of the file to write to (blank to output)
437     * @param string $types    define the output format, default
438     *                          is the current used format
439     * @param int    $quality  output DPI, default is 75
440     *
441     * @return bool|PEAR_Error TRUE on success or PEAR_Error object on error
442     * @access protected
443     */
444    function _generate($filename, $type = '', $quality = null)
445    {
446        $type = strtolower(($type == '') ? $this->type : $type);
447        $options = (is_array($quality)) ? $quality : array();
448        switch ($type) {
449            case 'jpg':
450                $type = 'jpeg';
451            case 'jpeg':
452                if (is_numeric($quality)) {
453                    $options['quality'] = $quality;
454                }
455                $quality = $this->_getOption('quality', $options, 75);
456                break;
457        }
458        if (!$this->supportsType($type, 'w')) {
459            return PEAR::raiseError('Image type not supported for output',
460                IMAGE_TRANSFORM_ERROR_UNSUPPORTED);
461        }
462
463        if ($filename == '') {
464            header('Content-type: ' . $this->getMimeType($type));
465            $action = 'output image';
466        } else {
467            $action = 'save image to file';
468        }
469
470        $functionName = 'image' . $type;
471        switch ($type) {
472            case 'jpeg':
473                $result = $functionName($this->imageHandle, $filename, $quality);
474                break;
475            default:
476                if ($filename == '') {
477                    $result = $functionName($this->imageHandle);
478                } else {
479                    $result = $functionName($this->imageHandle, $filename);
480                }
481        }
482        if (!$result) {
483            return PEAR::raiseError('Couldn\'t ' . $action,
484                IMAGE_TRANSFORM_ERROR_IO);
485        }
486        $this->imageHandle = $this->oldImage;
487        if (!$this->keep_settings_on_save) {
488            $this->free();
489        }
490        return true;
491
492    } // End save
493
494    /**
495     * Displays image without saving and lose changes.
496     *
497     * This method adds the Content-type HTTP header
498     *
499     * @param string $type (JPEG, PNG...);
500     * @param int    $quality 75
501     *
502     * @return bool|PEAR_Error TRUE or PEAR_Error object on error
503     * @access public
504     */
505    function display($type = '', $quality = null)
506    {
507        return $this->_generate('', $type, $quality);
508    }
509
510    /**
511     * Saves the image to a file
512     *
513     * @param string $filename the name of the file to write to
514     * @param string $type     the output format, default
515     *                          is the current used format
516     * @param int    $quality  default is 75
517     *
518     * @return bool|PEAR_Error TRUE on success or PEAR_Error object on error
519     * @access public
520     */
521    function save($filename, $type = '', $quality = null)
522    {
523        if (!trim($filename)) {
524            return PEAR::raiseError('Filename missing',
525                IMAGE_TRANSFORM_ERROR_ARGUMENT);
526        }
527        return $this->_generate($filename, $type, $quality);
528    }
529
530    /**
531     * Destroys image handle
532     *
533     * @access public
534     */
535    function free()
536    {
537        $this->resized = false;
538        if (is_resource($this->imageHandle)) {
539            ImageDestroy($this->imageHandle);
540        }
541        $this->imageHandle = null;
542        if (is_resource($this->oldImage)){
543            ImageDestroy($this->oldImage);
544        }
545        $this->oldImage = null;
546    }
547
548    /**
549     * Returns a new image for temporary processing
550     *
551     * @param int $width width of the new image
552     * @param int $height height of the new image
553     * @param bool $trueColor force which type of image to create
554     * @return resource a GD image resource
555     * @access protected
556     */
557    function _createImage($width = -1, $height = -1, $trueColor = null)
558    {
559        if ($width == -1) {
560            $width = $this->new_x;
561        }
562        if ($height == -1) {
563            $height = $this->new_y;
564        }
565
566        $new_img = null;
567        if (is_null($trueColor)) {
568            if (function_exists('imageistruecolor')) {
569                $createtruecolor = imageistruecolor($this->imageHandle);
570            } else {
571                $createtruecolor = true;
572            }
573        } else {
574            $createtruecolor = $trueColor;
575        }
576        if ($createtruecolor
577            && function_exists('ImageCreateTrueColor')) {
578            $new_img = @ImageCreateTrueColor($width, $height);
579            //GIF Transparent Patch
580            if ($this->type!='gif') {
581                imagealphablending($new_img, false);
582                imagesavealpha($new_img, true);
583            }
584            //End GIF Transparent Patch
585        }
586        if (!$new_img) {
587            $new_img = ImageCreate($width, $height);
588            imagepalettecopy($new_img, $this->imageHandle);
589            $color = imagecolortransparent($this->imageHandle);
590            if ($color != -1) {
591                imagecolortransparent($new_img, $color);
592                imagefill($new_img, 0, 0, $color);
593            }
594        }
595
596        //GIF Transparent Patch
597        if ($this->type=='gif') {
598            $transparencyIndex = imagecolortransparent($this->imageHandle);
599            $transparencyColor = array('red' => 255, 'green' => 255, 'blue' => 255);
600
601            if ($transparencyIndex >= 0) {
602                $transparencyColor = imagecolorsforindex($this->imageHandle, $transparencyIndex);
603            }
604
605            $transparencyIndex = imagecolorallocate($new_img, $transparencyColor['red'], $transparencyColor['green'], $transparencyColor['blue']);
606            imagefill($new_img, 0, 0, $transparencyIndex);
607            imagecolortransparent($new_img, $transparencyIndex);
608        }
609        //End GIF Transparent Patch
610
611
612        return $new_img;
613    }
614}