upload
[pear] / Image / Transform / Driver / GD.php
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 287351 2009-08-16 03:28:48Z clockwerx $
25  * @link       http://pear.php.net/package/Image_Transform
26  */
27
28 require_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  */
64 class 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'] = 'r';
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             imagealphablending($new_img, false);
580             imagesavealpha($new_img, true);
581         }
582         if (!$new_img) {
583             $new_img = ImageCreate($width, $height);
584             imagepalettecopy($new_img, $this->imageHandle);
585             $color = imagecolortransparent($this->imageHandle);
586             if ($color != -1) {
587                 imagecolortransparent($new_img, $color);
588                 imagefill($new_img, 0, 0, $color);
589             }
590         }
591         return $new_img;
592     }
593 }