Update composer branch alias (refs #4776)
[kohana-image.git] / classes / Kohana / Image / GD.php
blobe9c7988956eab8fef23ea7a8377158df05a3c2d1
1 <?php defined('SYSPATH') OR die('No direct script access.');
2 /**
3 * Support for image manipulation using [GD](http://php.net/GD).
5 * @package Kohana/Image
6 * @category Drivers
7 * @author Kohana Team
8 * @copyright (c) 2008-2009 Kohana Team
9 * @license http://kohanaphp.com/license.html
11 class Kohana_Image_GD extends Image {
13 // Which GD functions are available?
14 const IMAGEROTATE = 'imagerotate';
15 const IMAGECONVOLUTION = 'imageconvolution';
16 const IMAGEFILTER = 'imagefilter';
17 const IMAGELAYEREFFECT = 'imagelayereffect';
18 protected static $_available_functions = array();
20 /**
21 * Checks if GD is enabled and verify that key methods exist, some of which require GD to
22 * be bundled with PHP. Exceptions will be thrown from those methods when GD is not
23 * bundled.
25 * @return boolean
27 public static function check()
29 if ( ! function_exists('gd_info'))
31 throw new Kohana_Exception('GD is either not installed or not enabled, check your configuration');
33 $functions = array(
34 Image_GD::IMAGEROTATE,
35 Image_GD::IMAGECONVOLUTION,
36 Image_GD::IMAGEFILTER,
37 Image_GD::IMAGELAYEREFFECT
39 foreach ($functions as $function)
41 Image_GD::$_available_functions[$function] = function_exists($function);
44 if (defined('GD_VERSION'))
46 // Get the version via a constant, available in PHP 5.2.4+
47 $version = GD_VERSION;
49 else
51 // Get the version information
52 $info = gd_info();
54 // Extract the version number
55 preg_match('/\d+\.\d+(?:\.\d+)?/', $info['GD Version'], $matches);
57 // Get the major version
58 $version = $matches[0];
61 if ( ! version_compare($version, '2.0.1', '>='))
63 throw new Kohana_Exception('Image_GD requires GD version :required or greater, you have :version',
64 array('required' => '2.0.1', ':version' => $version));
67 return Image_GD::$_checked = TRUE;
70 // Temporary image resource
71 protected $_image;
73 // Function name to open Image
74 protected $_create_function;
76 /**
77 * Runs [Image_GD::check] and loads the image.
79 * @param string $file image file path
80 * @return void
81 * @throws Kohana_Exception
83 public function __construct($file)
85 if ( ! Image_GD::$_checked)
87 // Run the install check
88 Image_GD::check();
91 parent::__construct($file);
93 // Set the image creation function name
94 switch ($this->type)
96 case IMAGETYPE_JPEG:
97 $create = 'imagecreatefromjpeg';
98 break;
99 case IMAGETYPE_GIF:
100 $create = 'imagecreatefromgif';
101 break;
102 case IMAGETYPE_PNG:
103 $create = 'imagecreatefrompng';
104 break;
107 if ( ! isset($create) OR ! function_exists($create))
109 throw new Kohana_Exception('Installed GD does not support :type images',
110 array(':type' => image_type_to_extension($this->type, FALSE)));
113 // Save function for future use
114 $this->_create_function = $create;
116 // Save filename for lazy loading
117 $this->_image = $this->file;
121 * Destroys the loaded image to free up resources.
123 * @return void
125 public function __destruct()
127 if (is_resource($this->_image))
129 // Free all resources
130 imagedestroy($this->_image);
135 * Loads an image into GD.
137 * @return void
139 protected function _load_image()
141 if ( ! is_resource($this->_image))
143 // Gets create function
144 $create = $this->_create_function;
146 // Open the temporary image
147 $this->_image = $create($this->file);
149 // Preserve transparency when saving
150 imagesavealpha($this->_image, TRUE);
155 * Execute a resize.
157 * @param integer $width new width
158 * @param integer $height new height
159 * @return void
161 protected function _do_resize($width, $height)
163 // Presize width and height
164 $pre_width = $this->width;
165 $pre_height = $this->height;
167 // Loads image if not yet loaded
168 $this->_load_image();
170 // Test if we can do a resize without resampling to speed up the final resize
171 if ($width > ($this->width / 2) AND $height > ($this->height / 2))
173 // The maximum reduction is 10% greater than the final size
174 $reduction_width = round($width * 1.1);
175 $reduction_height = round($height * 1.1);
177 while ($pre_width / 2 > $reduction_width AND $pre_height / 2 > $reduction_height)
179 // Reduce the size using an O(2n) algorithm, until it reaches the maximum reduction
180 $pre_width /= 2;
181 $pre_height /= 2;
184 // Create the temporary image to copy to
185 $image = $this->_create($pre_width, $pre_height);
187 if (imagecopyresized($image, $this->_image, 0, 0, 0, 0, $pre_width, $pre_height, $this->width, $this->height))
189 // Swap the new image for the old one
190 imagedestroy($this->_image);
191 $this->_image = $image;
195 // Create the temporary image to copy to
196 $image = $this->_create($width, $height);
198 // Execute the resize
199 if (imagecopyresampled($image, $this->_image, 0, 0, 0, 0, $width, $height, $pre_width, $pre_height))
201 // Swap the new image for the old one
202 imagedestroy($this->_image);
203 $this->_image = $image;
205 // Reset the width and height
206 $this->width = imagesx($image);
207 $this->height = imagesy($image);
212 * Execute a crop.
214 * @param integer $width new width
215 * @param integer $height new height
216 * @param integer $offset_x offset from the left
217 * @param integer $offset_y offset from the top
218 * @return void
220 protected function _do_crop($width, $height, $offset_x, $offset_y)
222 // Create the temporary image to copy to
223 $image = $this->_create($width, $height);
225 // Loads image if not yet loaded
226 $this->_load_image();
228 // Execute the crop
229 if (imagecopyresampled($image, $this->_image, 0, 0, $offset_x, $offset_y, $width, $height, $width, $height))
231 // Swap the new image for the old one
232 imagedestroy($this->_image);
233 $this->_image = $image;
235 // Reset the width and height
236 $this->width = imagesx($image);
237 $this->height = imagesy($image);
242 * Execute a rotation.
244 * @param integer $degrees degrees to rotate
245 * @return void
247 protected function _do_rotate($degrees)
249 if (empty(Image_GD::$_available_functions[Image_GD::IMAGEROTATE]))
251 throw new Kohana_Exception('This method requires :function, which is only available in the bundled version of GD',
252 array(':function' => 'imagerotate'));
255 // Loads image if not yet loaded
256 $this->_load_image();
258 // Transparent black will be used as the background for the uncovered region
259 $transparent = imagecolorallocatealpha($this->_image, 0, 0, 0, 127);
261 // Rotate, setting the transparent color
262 $image = imagerotate($this->_image, 360 - $degrees, $transparent, 1);
264 // Save the alpha of the rotated image
265 imagesavealpha($image, TRUE);
267 // Get the width and height of the rotated image
268 $width = imagesx($image);
269 $height = imagesy($image);
271 if (imagecopymerge($this->_image, $image, 0, 0, 0, 0, $width, $height, 100))
273 // Swap the new image for the old one
274 imagedestroy($this->_image);
275 $this->_image = $image;
277 // Reset the width and height
278 $this->width = $width;
279 $this->height = $height;
284 * Execute a flip.
286 * @param integer $direction direction to flip
287 * @return void
289 protected function _do_flip($direction)
291 // Create the flipped image
292 $flipped = $this->_create($this->width, $this->height);
294 // Loads image if not yet loaded
295 $this->_load_image();
297 if ($direction === Image::HORIZONTAL)
299 for ($x = 0; $x < $this->width; $x++)
301 // Flip each row from top to bottom
302 imagecopy($flipped, $this->_image, $x, 0, $this->width - $x - 1, 0, 1, $this->height);
305 else
307 for ($y = 0; $y < $this->height; $y++)
309 // Flip each column from left to right
310 imagecopy($flipped, $this->_image, 0, $y, 0, $this->height - $y - 1, $this->width, 1);
314 // Swap the new image for the old one
315 imagedestroy($this->_image);
316 $this->_image = $flipped;
318 // Reset the width and height
319 $this->width = imagesx($flipped);
320 $this->height = imagesy($flipped);
324 * Execute a sharpen.
326 * @param integer $amount amount to sharpen
327 * @return void
329 protected function _do_sharpen($amount)
331 if (empty(Image_GD::$_available_functions[Image_GD::IMAGECONVOLUTION]))
333 throw new Kohana_Exception('This method requires :function, which is only available in the bundled version of GD',
334 array(':function' => 'imageconvolution'));
337 // Loads image if not yet loaded
338 $this->_load_image();
340 // Amount should be in the range of 18-10
341 $amount = round(abs(-18 + ($amount * 0.08)), 2);
343 // Gaussian blur matrix
344 $matrix = array
346 array(-1, -1, -1),
347 array(-1, $amount, -1),
348 array(-1, -1, -1),
351 // Perform the sharpen
352 if (imageconvolution($this->_image, $matrix, $amount - 8, 0))
354 // Reset the width and height
355 $this->width = imagesx($this->_image);
356 $this->height = imagesy($this->_image);
361 * Execute a reflection.
363 * @param integer $height reflection height
364 * @param integer $opacity reflection opacity
365 * @param boolean $fade_in TRUE to fade out, FALSE to fade in
366 * @return void
368 protected function _do_reflection($height, $opacity, $fade_in)
370 if (empty(Image_GD::$_available_functions[Image_GD::IMAGEFILTER]))
372 throw new Kohana_Exception('This method requires :function, which is only available in the bundled version of GD',
373 array(':function' => 'imagefilter'));
376 // Loads image if not yet loaded
377 $this->_load_image();
379 // Convert an opacity range of 0-100 to 127-0
380 $opacity = round(abs(($opacity * 127 / 100) - 127));
382 if ($opacity < 127)
384 // Calculate the opacity stepping
385 $stepping = (127 - $opacity) / $height;
387 else
389 // Avoid a "divide by zero" error
390 $stepping = 127 / $height;
393 // Create the reflection image
394 $reflection = $this->_create($this->width, $this->height + $height);
396 // Copy the image to the reflection
397 imagecopy($reflection, $this->_image, 0, 0, 0, 0, $this->width, $this->height);
399 for ($offset = 0; $height >= $offset; $offset++)
401 // Read the next line down
402 $src_y = $this->height - $offset - 1;
404 // Place the line at the bottom of the reflection
405 $dst_y = $this->height + $offset;
407 if ($fade_in === TRUE)
409 // Start with the most transparent line first
410 $dst_opacity = round($opacity + ($stepping * ($height - $offset)));
412 else
414 // Start with the most opaque line first
415 $dst_opacity = round($opacity + ($stepping * $offset));
418 // Create a single line of the image
419 $line = $this->_create($this->width, 1);
421 // Copy a single line from the current image into the line
422 imagecopy($line, $this->_image, 0, 0, 0, $src_y, $this->width, 1);
424 // Colorize the line to add the correct alpha level
425 imagefilter($line, IMG_FILTER_COLORIZE, 0, 0, 0, $dst_opacity);
427 // Copy a the line into the reflection
428 imagecopy($reflection, $line, 0, $dst_y, 0, 0, $this->width, 1);
431 // Swap the new image for the old one
432 imagedestroy($this->_image);
433 $this->_image = $reflection;
435 // Reset the width and height
436 $this->width = imagesx($reflection);
437 $this->height = imagesy($reflection);
441 * Execute a watermarking.
443 * @param Image $image watermarking Image
444 * @param integer $offset_x offset from the left
445 * @param integer $offset_y offset from the top
446 * @param integer $opacity opacity of watermark
447 * @return void
449 protected function _do_watermark(Image $watermark, $offset_x, $offset_y, $opacity)
451 if (empty(Image_GD::$_available_functions[Image_GD::IMAGELAYEREFFECT]))
453 throw new Kohana_Exception('This method requires :function, which is only available in the bundled version of GD',
454 array(':function' => 'imagelayereffect'));
457 // Loads image if not yet loaded
458 $this->_load_image();
460 // Create the watermark image resource
461 $overlay = imagecreatefromstring($watermark->render());
463 imagesavealpha($overlay, TRUE);
465 // Get the width and height of the watermark
466 $width = imagesx($overlay);
467 $height = imagesy($overlay);
469 if ($opacity < 100)
471 // Convert an opacity range of 0-100 to 127-0
472 $opacity = round(abs(($opacity * 127 / 100) - 127));
474 // Allocate transparent gray
475 $color = imagecolorallocatealpha($overlay, 127, 127, 127, $opacity);
477 // The transparent image will overlay the watermark
478 imagelayereffect($overlay, IMG_EFFECT_OVERLAY);
480 // Fill the background with the transparent color
481 imagefilledrectangle($overlay, 0, 0, $width, $height, $color);
484 // Alpha blending must be enabled on the background!
485 imagealphablending($this->_image, TRUE);
487 if (imagecopy($this->_image, $overlay, $offset_x, $offset_y, 0, 0, $width, $height))
489 // Destroy the overlay image
490 imagedestroy($overlay);
495 * Execute a background.
497 * @param integer $r red
498 * @param integer $g green
499 * @param integer $b blue
500 * @param integer $opacity opacity
501 * @return void
503 protected function _do_background($r, $g, $b, $opacity)
505 // Loads image if not yet loaded
506 $this->_load_image();
508 // Convert an opacity range of 0-100 to 127-0
509 $opacity = round(abs(($opacity * 127 / 100) - 127));
511 // Create a new background
512 $background = $this->_create($this->width, $this->height);
514 // Allocate the color
515 $color = imagecolorallocatealpha($background, $r, $g, $b, $opacity);
517 // Fill the image with white
518 imagefilledrectangle($background, 0, 0, $this->width, $this->height, $color);
520 // Alpha blending must be enabled on the background!
521 imagealphablending($background, TRUE);
523 // Copy the image onto a white background to remove all transparency
524 if (imagecopy($background, $this->_image, 0, 0, 0, 0, $this->width, $this->height))
526 // Swap the new image for the old one
527 imagedestroy($this->_image);
528 $this->_image = $background;
533 * Execute a save.
535 * @param string $file new image filename
536 * @param integer $quality quality
537 * @return boolean
539 protected function _do_save($file, $quality)
541 // Loads image if not yet loaded
542 $this->_load_image();
544 // Get the extension of the file
545 $extension = pathinfo($file, PATHINFO_EXTENSION);
547 // Get the save function and IMAGETYPE
548 list($save, $type) = $this->_save_function($extension, $quality);
550 // Save the image to a file
551 $status = isset($quality) ? $save($this->_image, $file, $quality) : $save($this->_image, $file);
553 if ($status === TRUE AND $type !== $this->type)
555 // Reset the image type and mime type
556 $this->type = $type;
557 $this->mime = image_type_to_mime_type($type);
560 return TRUE;
564 * Execute a render.
566 * @param string $type image type: png, jpg, gif, etc
567 * @param integer $quality quality
568 * @return string
570 protected function _do_render($type, $quality)
572 // Loads image if not yet loaded
573 $this->_load_image();
575 // Get the save function and IMAGETYPE
576 list($save, $type) = $this->_save_function($type, $quality);
578 // Capture the output
579 ob_start();
581 // Render the image
582 $status = isset($quality) ? $save($this->_image, NULL, $quality) : $save($this->_image, NULL);
584 if ($status === TRUE AND $type !== $this->type)
586 // Reset the image type and mime type
587 $this->type = $type;
588 $this->mime = image_type_to_mime_type($type);
591 return ob_get_clean();
595 * Get the GD saving function and image type for this extension.
596 * Also normalizes the quality setting
598 * @param string $extension image type: png, jpg, etc
599 * @param integer $quality image quality
600 * @return array save function, IMAGETYPE_* constant
601 * @throws Kohana_Exception
603 protected function _save_function($extension, & $quality)
605 if ( ! $extension)
607 // Use the current image type
608 $extension = image_type_to_extension($this->type, FALSE);
611 switch (strtolower($extension))
613 case 'jpg':
614 case 'jpeg':
615 // Save a JPG file
616 $save = 'imagejpeg';
617 $type = IMAGETYPE_JPEG;
618 break;
619 case 'gif':
620 // Save a GIF file
621 $save = 'imagegif';
622 $type = IMAGETYPE_GIF;
624 // GIFs do not a quality setting
625 $quality = NULL;
626 break;
627 case 'png':
628 // Save a PNG file
629 $save = 'imagepng';
630 $type = IMAGETYPE_PNG;
632 // Use a compression level of 9 (does not affect quality!)
633 $quality = 9;
634 break;
635 default:
636 throw new Kohana_Exception('Installed GD does not support :type images',
637 array(':type' => $extension));
638 break;
641 return array($save, $type);
645 * Create an empty image with the given width and height.
647 * @param integer $width image width
648 * @param integer $height image height
649 * @return resource
651 protected function _create($width, $height)
653 // Create an empty image
654 $image = imagecreatetruecolor($width, $height);
656 // Do not apply alpha blending
657 imagealphablending($image, FALSE);
659 // Save alpha levels
660 imagesavealpha($image, TRUE);
662 return $image;
665 } // End Image_GD