Updated comments, changed all @package links to "Kohana/Image" and added @category...
[kohana-image.git] / classes / kohana / image / gd.php
blob3a05b409c8d85d3ed70c685203ba94b147143002
1 <?php defined('SYSPATH') or die('No direct script access.');
2 /**
3 * Image manipulation class using {@link http://php.net/gd 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 // Is GD bundled or separate?
14 protected static $_bundled;
16 public static function check()
18 if ( ! function_exists('gd_info'))
20 throw new Kohana_Exception('GD is either not installed or not enabled, check your configuration');
23 if (defined('GD_BUNDLED'))
25 // Get the version via a constant, available in PHP 5.
26 Image_GD::$_bundled = GD_BUNDLED;
28 else
30 // Get the version information
31 $info = gd_info();
33 // Extract the bundled status
34 Image_GD::$_bundled = (bool) preg_match('/\bbundled\b/i', $info['GD Version']);
37 if (defined('GD_VERSION'))
39 // Get the version via a constant, available in PHP 5.2.4+
40 $version = GD_VERSION;
42 else
44 // Get the version information
45 $info = gd_info();
47 // Extract the version number
48 preg_match('/\d+\.\d+(?:\.\d+)?/', $info['GD Version'], $matches);
50 // Get the major version
51 $version = $matches[0];
54 if ( ! version_compare($version, '2.0.1', '>='))
56 throw new Kohana_Exception('Image_GD requires GD version :required or greater, you have :version',
57 array('required' => '2.0.1', ':version' => $version));
60 return Image_GD::$_checked = TRUE;
63 // Temporary image resource
64 protected $_image;
66 // Function name to open Image
67 protected $_create_function;
69 public function __construct($file)
71 if ( ! Image_GD::$_checked)
73 // Run the install check
74 Image_GD::check();
77 parent::__construct($file);
79 // Set the image creation function name
80 switch ($this->type)
82 case IMAGETYPE_JPEG:
83 $create = 'imagecreatefromjpeg';
84 break;
85 case IMAGETYPE_GIF:
86 $create = 'imagecreatefromgif';
87 break;
88 case IMAGETYPE_PNG:
89 $create = 'imagecreatefrompng';
90 break;
93 if ( ! isset($create) OR ! function_exists($create))
95 throw new Kohana_Exception('Installed GD does not support :type images',
96 array(':type' => image_type_to_extension($this->type, FALSE)));
99 // Save function for future use
100 $this->_create_function = $create;
102 // Save filename for lazy loading
103 $this->_image = $this->file;
106 public function __destruct()
108 if (is_resource($this->_image))
110 // Free all resources
111 imagedestroy($this->_image);
115 protected function _load_image()
117 if ( ! is_resource($this->_image))
119 // Gets create function
120 $create = $this->_create_function;
122 // Open the temporary image
123 $this->_image = $create($this->file);
125 // Preserve transparency when saving
126 imagesavealpha($this->_image, TRUE);
130 protected function _do_resize($width, $height)
132 // Presize width and height
133 $pre_width = $this->width;
134 $pre_height = $this->height;
136 // Loads image if not yet loaded
137 $this->_load_image();
139 // Test if we can do a resize without resampling to speed up the final resize
140 if ($width > ($this->width / 2) AND $height > ($this->height / 2))
142 // The maximum reduction is 10% greater than the final size
143 $reduction_width = round($width * 1.1);
144 $reduction_height = round($height * 1.1);
146 while ($pre_width / 2 > $reduction_width AND $pre_height / 2 > $reduction_height)
148 // Reduce the size using an O(2n) algorithm, until it reaches the maximum reduction
149 $pre_width /= 2;
150 $pre_height /= 2;
153 // Create the temporary image to copy to
154 $image = $this->_create($pre_width, $pre_height);
156 if (imagecopyresized($image, $this->_image, 0, 0, 0, 0, $pre_width, $pre_height, $this->width, $this->height))
158 // Swap the new image for the old one
159 imagedestroy($this->_image);
160 $this->_image = $image;
164 // Create the temporary image to copy to
165 $image = $this->_create($width, $height);
167 // Execute the resize
168 if (imagecopyresampled($image, $this->_image, 0, 0, 0, 0, $width, $height, $pre_width, $pre_height))
170 // Swap the new image for the old one
171 imagedestroy($this->_image);
172 $this->_image = $image;
174 // Reset the width and height
175 $this->width = imagesx($image);
176 $this->height = imagesy($image);
180 protected function _do_crop($width, $height, $offset_x, $offset_y)
182 // Create the temporary image to copy to
183 $image = $this->_create($width, $height);
185 // Loads image if not yet loaded
186 $this->_load_image();
188 // Execute the crop
189 if (imagecopyresampled($image, $this->_image, 0, 0, $offset_x, $offset_y, $width, $height, $width, $height))
191 // Swap the new image for the old one
192 imagedestroy($this->_image);
193 $this->_image = $image;
195 // Reset the width and height
196 $this->width = imagesx($image);
197 $this->height = imagesy($image);
201 protected function _do_rotate($degrees)
203 if ( ! Image_GD::$_bundled)
205 throw new Kohana_Exception('This method requires :function, which is only available in the bundled version of GD',
206 array(':function' => 'imagerotate'));
209 // Loads image if not yet loaded
210 $this->_load_image();
212 // Transparent black will be used as the background for the uncovered region
213 $transparent = imagecolorallocatealpha($this->_image, 0, 0, 0, 127);
215 // Rotate, setting the transparent color
216 $image = imagerotate($this->_image, 360 - $degrees, $transparent, 1);
218 // Save the alpha of the rotated image
219 imagesavealpha($image, TRUE);
221 // Get the width and height of the rotated image
222 $width = imagesx($image);
223 $height = imagesy($image);
225 if (imagecopymerge($this->_image, $image, 0, 0, 0, 0, $width, $height, 100))
227 // Swap the new image for the old one
228 imagedestroy($this->_image);
229 $this->_image = $image;
231 // Reset the width and height
232 $this->width = $width;
233 $this->height = $height;
237 protected function _do_flip($direction)
239 // Create the flipped image
240 $flipped = $this->_create($this->width, $this->height);
242 // Loads image if not yet loaded
243 $this->_load_image();
245 if ($direction === Image::HORIZONTAL)
247 for ($x = 0; $x < $this->width; $x++)
249 // Flip each row from top to bottom
250 imagecopy($flipped, $this->_image, $x, 0, $this->width - $x - 1, 0, 1, $this->height);
253 else
255 for ($y = 0; $y < $this->height; $y++)
257 // Flip each column from left to right
258 imagecopy($flipped, $this->_image, 0, $y, 0, $this->height - $y - 1, $this->width, 1);
262 // Swap the new image for the old one
263 imagedestroy($this->_image);
264 $this->_image = $flipped;
266 // Reset the width and height
267 $this->width = imagesx($flipped);
268 $this->height = imagesy($flipped);
271 protected function _do_sharpen($amount)
273 if ( ! Image_GD::$_bundled)
275 throw new Kohana_Exception('This method requires :function, which is only available in the bundled version of GD',
276 array(':function' => 'imageconvolution'));
279 // Loads image if not yet loaded
280 $this->_load_image();
282 // Amount should be in the range of 18-10
283 $amount = round(abs(-18 + ($amount * 0.08)), 2);
285 // Gaussian blur matrix
286 $matrix = array
288 array(-1, -1, -1),
289 array(-1, $amount, -1),
290 array(-1, -1, -1),
293 // Perform the sharpen
294 if (imageconvolution($this->_image, $matrix, $amount - 8, 0))
296 // Reset the width and height
297 $this->width = imagesx($this->_image);
298 $this->height = imagesy($this->_image);
302 protected function _do_reflection($height, $opacity, $fade_in)
304 if ( ! Image_GD::$_bundled)
306 throw new Kohana_Exception('This method requires :function, which is only available in the bundled version of GD',
307 array(':function' => 'imagefilter'));
310 // Loads image if not yet loaded
311 $this->_load_image();
313 // Convert an opacity range of 0-100 to 127-0
314 $opacity = round(abs(($opacity * 127 / 100) - 127));
316 if ($opacity < 127)
318 // Calculate the opacity stepping
319 $stepping = (127 - $opacity) / $height;
321 else
323 // Avoid a "divide by zero" error
324 $stepping = 127 / $height;
327 // Create the reflection image
328 $reflection = $this->_create($this->width, $this->height + $height);
330 // Copy the image to the reflection
331 imagecopy($reflection, $this->_image, 0, 0, 0, 0, $this->width, $this->height);
333 for ($offset = 0; $height >= $offset; $offset++)
335 // Read the next line down
336 $src_y = $this->height - $offset - 1;
338 // Place the line at the bottom of the reflection
339 $dst_y = $this->height + $offset;
341 if ($fade_in === TRUE)
343 // Start with the most transparent line first
344 $dst_opacity = round($opacity + ($stepping * ($height - $offset)));
346 else
348 // Start with the most opaque line first
349 $dst_opacity = round($opacity + ($stepping * $offset));
352 // Create a single line of the image
353 $line = $this->_create($this->width, 1);
355 // Copy a single line from the current image into the line
356 imagecopy($line, $this->_image, 0, 0, 0, $src_y, $this->width, 1);
358 // Colorize the line to add the correct alpha level
359 imagefilter($line, IMG_FILTER_COLORIZE, 0, 0, 0, $dst_opacity);
361 // Copy a the line into the reflection
362 imagecopy($reflection, $line, 0, $dst_y, 0, 0, $this->width, 1);
365 // Swap the new image for the old one
366 imagedestroy($this->_image);
367 $this->_image = $reflection;
369 // Reset the width and height
370 $this->width = imagesx($reflection);
371 $this->height = imagesy($reflection);
374 protected function _do_watermark(Image $watermark, $offset_x, $offset_y, $opacity)
376 if ( ! Image_GD::$_bundled)
378 throw new Kohana_Exception('This method requires :function, which is only available in the bundled version of GD',
379 array(':function' => 'imagelayereffect'));
382 // Loads image if not yet loaded
383 $this->_load_image();
385 // Create the watermark image resource
386 $overlay = imagecreatefromstring($watermark->render());
388 // Get the width and height of the watermark
389 $width = imagesx($overlay);
390 $height = imagesy($overlay);
392 if ($opacity < 100)
394 // Convert an opacity range of 0-100 to 127-0
395 $opacity = round(abs(($opacity * 127 / 100) - 127));
397 // Allocate transparent white
398 $color = imagecolorallocatealpha($overlay, 255, 255, 255, $opacity);
400 // The transparent image will overlay the watermark
401 imagelayereffect($overlay, IMG_EFFECT_OVERLAY);
403 // Fill the background with transparent white
404 imagefilledrectangle($overlay, 0, 0, $width, $height, $color);
407 // Alpha blending must be enabled on the background!
408 imagealphablending($this->_image, TRUE);
410 if (imagecopy($this->_image, $overlay, $offset_x, $offset_y, 0, 0, $width, $height))
412 // Destroy the overlay image
413 imagedestroy($overlay);
417 protected function _do_background($r, $g, $b, $opacity)
419 // Loads image if not yet loaded
420 $this->_load_image();
422 // Convert an opacity range of 0-100 to 127-0
423 $opacity = round(abs(($opacity * 127 / 100) - 127));
425 // Create a new background
426 $background = $this->_create($this->width, $this->height);
428 // Allocate the color
429 $color = imagecolorallocatealpha($background, $r, $g, $b, $opacity);
431 // Fill the image with white
432 imagefilledrectangle($background, 0, 0, $this->width, $this->height, $color);
434 // Alpha blending must be enabled on the background!
435 imagealphablending($background, TRUE);
437 // Copy the image onto a white background to remove all transparency
438 if (imagecopy($background, $this->_image, 0, 0, 0, 0, $this->width, $this->height))
440 // Swap the new image for the old one
441 imagedestroy($this->_image);
442 $this->_image = $background;
446 protected function _do_save($file, $quality)
448 // Loads image if not yet loaded
449 $this->_load_image();
451 // Get the extension of the file
452 $extension = pathinfo($file, PATHINFO_EXTENSION);
454 // Get the save function and IMAGETYPE
455 list($save, $type) = $this->_save_function($extension, $quality);
457 // Save the image to a file
458 $status = isset($quality) ? $save($this->_image, $file, $quality) : $save($this->_image, $file);
460 if ($status === TRUE AND $type !== $this->type)
462 // Reset the image type and mime type
463 $this->type = $type;
464 $this->mime = image_type_to_mime_type($type);
467 return TRUE;
470 protected function _do_render($type, $quality)
472 // Loads image if not yet loaded
473 $this->_load_image();
475 // Get the save function and IMAGETYPE
476 list($save, $type) = $this->_save_function($type, $quality);
478 // Capture the output
479 ob_start();
481 // Render the image
482 $status = isset($quality) ? $save($this->_image, NULL, $quality) : $save($this->_image, NULL);
484 if ($status === TRUE AND $type !== $this->type)
486 // Reset the image type and mime type
487 $this->type = $type;
488 $this->mime = image_type_to_mime_type($type);
491 return ob_get_clean();
495 * Get the GD saving function and image type for this extension.
496 * Also normalizes the quality setting
498 * @throws Kohana_Exception
499 * @param string image type: png, jpg, etc
500 * @param integer image quality
501 * @return array save function, IMAGETYPE_* constant
503 protected function _save_function($extension, & $quality)
505 switch (strtolower($extension))
507 case 'jpg':
508 case 'jpeg':
509 // Save a JPG file
510 $save = 'imagejpeg';
511 $type = IMAGETYPE_JPEG;
512 break;
513 case 'gif':
514 // Save a GIF file
515 $save = 'imagegif';
516 $type = IMAGETYPE_GIF;
518 // GIFs do not a quality setting
519 $quality = NULL;
520 break;
521 case 'png':
522 // Save a PNG file
523 $save = 'imagepng';
524 $type = IMAGETYPE_PNG;
526 // Use a compression level of 9 (does not affect quality!)
527 $quality = 9;
528 break;
529 default:
530 throw new Kohana_Exception('Installed GD does not support :type images',
531 array(':type' => $extension));
532 break;
535 return array($save, $type);
539 * Create an empty image with the given width and height.
541 * @param integer image width
542 * @param integer image height
543 * @return resource
545 protected function _create($width, $height)
547 // Create an empty image
548 $image = imagecreatetruecolor($width, $height);
550 // Do not apply alpha blending
551 imagealphablending($image, FALSE);
553 // Save alpha levels
554 imagesavealpha($image, TRUE);
556 return $image;
559 } // End Image_GD