Added tests for #3568
[kohana-image.git] / classes / kohana / image.php
blobd8b40235cbb5b9e94f606d7b40db0db1fd2b385d
1 <?php defined('SYSPATH') or die('No direct script access.');
2 /**
3 * Image manipulation support. Allows images to be resized, cropped, etc.
5 * @package Kohana/Image
6 * @category Base
7 * @author Kohana Team
8 * @copyright (c) 2008-2009 Kohana Team
9 * @license http://kohanaphp.com/license.html
11 abstract class Kohana_Image {
13 // Resizing constraints
14 const NONE = 0x01;
15 const WIDTH = 0x02;
16 const HEIGHT = 0x03;
17 const AUTO = 0x04;
18 const INVERSE = 0x05;
20 // Flipping directions
21 const HORIZONTAL = 0x11;
22 const VERTICAL = 0x12;
24 /**
25 * @var string default driver: GD, ImageMagick, etc
27 public static $default_driver = 'GD';
29 // Status of the driver check
30 protected static $_checked = FALSE;
32 /**
33 * Loads an image and prepares it for manipulation.
35 * $image = Image::factory('upload/test.jpg');
37 * @param string image file path
38 * @param string driver type: GD, ImageMagick, etc
39 * @return Image
40 * @uses Image::$default_driver
42 public static function factory($file, $driver = NULL)
44 if ($driver === NULL)
46 // Use the default driver
47 $driver = Image::$default_driver;
50 // Set the class name
51 $class = 'Image_'.$driver;
53 return new $class($file);
56 /**
57 * @var string image file path
59 public $file;
61 /**
62 * @var integer image width
64 public $width;
66 /**
67 * @var integer image height
69 public $height;
71 /**
72 * @var integer one of the IMAGETYPE_* constants
74 public $type;
76 /**
77 * @var string mime type of the image
79 public $mime;
81 /**
82 * Loads information about the image. Will throw an exception if the image
83 * does not exist or is not an image.
85 * @param string image file path
86 * @return void
87 * @throws Kohana_Exception
89 public function __construct($file)
91 try
93 // Get the real path to the file
94 $file = realpath($file);
96 // Get the image information
97 $info = getimagesize($file);
99 catch (Exception $e)
101 // Ignore all errors while reading the image
104 if (empty($file) OR empty($info))
106 throw new Kohana_Exception('Not an image or invalid image: :file',
107 array(':file' => Debug::path($file)));
110 // Store the image information
111 $this->file = $file;
112 $this->width = $info[0];
113 $this->height = $info[1];
114 $this->type = $info[2];
115 $this->mime = image_type_to_mime_type($this->type);
119 * Render the current image.
121 * echo $image;
123 * [!!] The output of this function is binary and must be rendered with the
124 * appropriate Content-Type header or it will not be displayed correctly!
126 * @return string
128 public function __toString()
132 // Render the current image
133 return $this->render();
135 catch (Exception $e)
137 if (is_object(Kohana::$log))
139 // Get the text of the exception
140 $error = Kohana_Exception::text($e);
142 // Add this exception to the log
143 Kohana::$log->add(Log::ERROR, $error);
146 // Showing any kind of error will be "inside" image data
147 return '';
152 * Resize the image to the given size. Either the width or the height can
153 * be omitted and the image will be resized proportionally.
155 * // Resize to 200 pixels on the shortest side
156 * $image->resize(200, 200);
158 * // Resize to 200x200 pixels, keeping aspect ratio
159 * $image->resize(200, 200, Image::INVERSE);
161 * // Resize to 500 pixel width, keeping aspect ratio
162 * $image->resize(500, NULL);
164 * // Resize to 500 pixel height, keeping aspect ratio
165 * $image->resize(NULL, 500);
167 * // Resize to 200x500 pixels, ignoring aspect ratio
168 * $image->resize(200, 500, Image::NONE);
170 * @param integer new width
171 * @param integer new height
172 * @param integer master dimension
173 * @return $this
174 * @uses Image::_do_resize
176 public function resize($width = NULL, $height = NULL, $master = NULL)
178 if ($master === NULL)
180 // Choose the master dimension automatically
181 $master = Image::AUTO;
183 // Image::WIDTH and Image::HEIGHT deprecated. You can use it in old projects,
184 // but in new you must pass empty value for non-master dimension
185 elseif ($master == Image::WIDTH AND ! empty($width))
187 $master = Image::AUTO;
189 // Set empty height for backward compatibility
190 $height = NULL;
192 elseif ($master == Image::HEIGHT AND ! empty($height))
194 $master = Image::AUTO;
196 // Set empty width for backward compatibility
197 $width = NULL;
200 if (empty($width))
202 if ($master === Image::NONE)
204 // Use the current width
205 $width = $this->width;
207 else
209 // If width not set, master will be height
210 $master = Image::HEIGHT;
214 if (empty($height))
216 if ($master === Image::NONE)
218 // Use the current height
219 $height = $this->height;
221 else
223 // If height not set, master will be width
224 $master = Image::WIDTH;
228 switch ($master)
230 case Image::AUTO:
231 // Choose direction with the greatest reduction ratio
232 $master = ($this->width / $width) > ($this->height / $height) ? Image::WIDTH : Image::HEIGHT;
233 break;
234 case Image::INVERSE:
235 // Choose direction with the minimum reduction ratio
236 $master = ($this->width / $width) > ($this->height / $height) ? Image::HEIGHT : Image::WIDTH;
237 break;
240 switch ($master)
242 case Image::WIDTH:
243 // Recalculate the height based on the width proportions
244 $height = $this->height * $width / $this->width;
245 break;
246 case Image::HEIGHT:
247 // Recalculate the width based on the height proportions
248 $width = $this->width * $height / $this->height;
249 break;
252 // Convert the width and height to integers, minimum value is 1px
253 $width = max(round($width), 1);
254 $height = max(round($height), 1);
256 $this->_do_resize($width, $height);
258 return $this;
262 * Crop an image to the given size. Either the width or the height can be
263 * omitted and the current width or height will be used.
265 * If no offset is specified, the center of the axis will be used.
266 * If an offset of TRUE is specified, the bottom of the axis will be used.
268 * // Crop the image to 200x200 pixels, from the center
269 * $image->crop(200, 200);
271 * @param integer new width
272 * @param integer new height
273 * @param mixed offset from the left
274 * @param mixed offset from the top
275 * @return $this
276 * @uses Image::_do_crop
278 public function crop($width, $height, $offset_x = NULL, $offset_y = NULL)
280 if ($width > $this->width)
282 // Use the current width
283 $width = $this->width;
286 if ($height > $this->height)
288 // Use the current height
289 $height = $this->height;
292 if ($offset_x === NULL)
294 // Center the X offset
295 $offset_x = round(($this->width - $width) / 2);
297 elseif ($offset_x === TRUE)
299 // Bottom the X offset
300 $offset_x = $this->width - $width;
302 elseif ($offset_x < 0)
304 // Set the X offset from the right
305 $offset_x = $this->width - $width + $offset_x;
308 if ($offset_y === NULL)
310 // Center the Y offset
311 $offset_y = round(($this->height - $height) / 2);
313 elseif ($offset_y === TRUE)
315 // Bottom the Y offset
316 $offset_y = $this->height - $height;
318 elseif ($offset_y < 0)
320 // Set the Y offset from the bottom
321 $offset_y = $this->height - $height + $offset_y;
324 // Determine the maximum possible width and height
325 $max_width = $this->width - $offset_x;
326 $max_height = $this->height - $offset_y;
328 if ($width > $max_width)
330 // Use the maximum available width
331 $width = $max_width;
334 if ($height > $max_height)
336 // Use the maximum available height
337 $height = $max_height;
340 $this->_do_crop($width, $height, $offset_x, $offset_y);
342 return $this;
346 * Rotate the image by a given amount.
348 * // Rotate 45 degrees clockwise
349 * $image->rotate(45);
351 * // Rotate 90% counter-clockwise
352 * $image->rotate(-90);
354 * @param integer degrees to rotate: -360-360
355 * @return $this
356 * @uses Image::_do_rotate
358 public function rotate($degrees)
360 // Make the degrees an integer
361 $degrees = (int) $degrees;
363 if ($degrees > 180)
367 // Keep subtracting full circles until the degrees have normalized
368 $degrees -= 360;
370 while($degrees > 180);
373 if ($degrees < -180)
377 // Keep adding full circles until the degrees have normalized
378 $degrees += 360;
380 while($degrees < -180);
383 $this->_do_rotate($degrees);
385 return $this;
389 * Flip the image along the horizontal or vertical axis.
391 * // Flip the image from top to bottom
392 * $image->flip(Image::HORIZONTAL);
394 * // Flip the image from left to right
395 * $image->flip(Image::VERTICAL);
397 * @param integer direction: Image::HORIZONTAL, Image::VERTICAL
398 * @return $this
399 * @uses Image::_do_flip
401 public function flip($direction)
403 if ($direction !== Image::HORIZONTAL)
405 // Flip vertically
406 $direction = Image::VERTICAL;
409 $this->_do_flip($direction);
411 return $this;
415 * Sharpen the image by a given amount.
417 * // Sharpen the image by 20%
418 * $image->sharpen(20);
420 * @param integer amount to sharpen: 1-100
421 * @return $this
422 * @uses Image::_do_sharpen
424 public function sharpen($amount)
426 // The amount must be in the range of 1 to 100
427 $amount = min(max($amount, 1), 100);
429 $this->_do_sharpen($amount);
431 return $this;
435 * Add a reflection to an image. The most opaque part of the reflection
436 * will be equal to the opacity setting and fade out to full transparent.
437 * Alpha transparency is preserved.
439 * // Create a 50 pixel reflection that fades from 0-100% opacity
440 * $image->reflection(50);
442 * // Create a 50 pixel reflection that fades from 100-0% opacity
443 * $image->reflection(50, 100, TRUE);
445 * // Create a 50 pixel reflection that fades from 0-60% opacity
446 * $image->reflection(50, 60, TRUE);
448 * [!!] By default, the reflection will be go from transparent at the top
449 * to opaque at the bottom.
451 * @param integer reflection height
452 * @param integer reflection opacity: 0-100
453 * @param boolean TRUE to fade in, FALSE to fade out
454 * @return $this
455 * @uses Image::_do_reflection
457 public function reflection($height = NULL, $opacity = 100, $fade_in = FALSE)
459 if ($height === NULL OR $height > $this->height)
461 // Use the current height
462 $height = $this->height;
465 // The opacity must be in the range of 0 to 100
466 $opacity = min(max($opacity, 0), 100);
468 $this->_do_reflection($height, $opacity, $fade_in);
470 return $this;
474 * Add a watermark to an image with a specified opacity. Alpha transparency
475 * will be preserved.
477 * If no offset is specified, the center of the axis will be used.
478 * If an offset of TRUE is specified, the bottom of the axis will be used.
480 * // Add a watermark to the bottom right of the image
481 * $mark = Image::factory('upload/watermark.png');
482 * $image->watermark($mark, TRUE, TRUE);
484 * @param object watermark Image instance
485 * @param integer offset from the left
486 * @param integer offset from the top
487 * @param integer opacity of watermark: 1-100
488 * @return $this
489 * @uses Image::_do_watermark
491 public function watermark(Image $watermark, $offset_x = NULL, $offset_y = NULL, $opacity = 100)
493 if ($offset_x === NULL)
495 // Center the X offset
496 $offset_x = round(($this->width - $watermark->width) / 2);
498 elseif ($offset_x === TRUE)
500 // Bottom the X offset
501 $offset_x = $this->width - $watermark->width;
503 elseif ($offset_x < 0)
505 // Set the X offset from the right
506 $offset_x = $this->width - $watermark->width + $offset_x;
509 if ($offset_y === NULL)
511 // Center the Y offset
512 $offset_y = round(($this->height - $watermark->height) / 2);
514 elseif ($offset_y === TRUE)
516 // Bottom the Y offset
517 $offset_y = $this->height - $watermark->height;
519 elseif ($offset_y < 0)
521 // Set the Y offset from the bottom
522 $offset_y = $this->height - $watermark->height + $offset_y;
525 // The opacity must be in the range of 1 to 100
526 $opacity = min(max($opacity, 1), 100);
528 $this->_do_watermark($watermark, $offset_x, $offset_y, $opacity);
530 return $this;
534 * Set the background color of an image. This is only useful for images
535 * with alpha transparency.
537 * // Make the image background black
538 * $image->background('#000');
540 * // Make the image background black with 50% opacity
541 * $image->background('#000', 50);
543 * @param string hexadecimal color value
544 * @param integer background opacity: 0-100
545 * @return $this
546 * @uses Image::_do_background
548 public function background($color, $opacity = 100)
550 if ($color[0] === '#')
552 // Remove the pound
553 $color = substr($color, 1);
556 if (strlen($color) === 3)
558 // Convert shorthand into longhand hex notation
559 $color = preg_replace('/./', '$0$0', $color);
562 // Convert the hex into RGB values
563 list ($r, $g, $b) = array_map('hexdec', str_split($color, 2));
565 // The opacity must be in the range of 0 to 100
566 $opacity = min(max($opacity, 0), 100);
568 $this->_do_background($r, $g, $b, $opacity);
570 return $this;
574 * Save the image. If the filename is omitted, the original image will
575 * be overwritten.
577 * // Save the image as a PNG
578 * $image->save('saved/cool.png');
580 * // Overwrite the original image
581 * $image->save();
583 * [!!] If the file exists, but is not writable, an exception will be thrown.
585 * [!!] If the file does not exist, and the directory is not writable, an
586 * exception will be thrown.
588 * @param string new image path
589 * @param integer quality of image: 1-100
590 * @return boolean
591 * @uses Image::_save
592 * @throws Kohana_Exception
594 public function save($file = NULL, $quality = 100)
596 if ($file === NULL)
598 // Overwrite the file
599 $file = $this->file;
602 if (is_file($file))
604 if ( ! is_writable($file))
606 throw new Kohana_Exception('File must be writable: :file',
607 array(':file' => Debug::path($file)));
610 else
612 // Get the directory of the file
613 $directory = realpath(pathinfo($file, PATHINFO_DIRNAME));
615 if ( ! is_dir($directory) OR ! is_writable($directory))
617 throw new Kohana_Exception('Directory must be writable: :directory',
618 array(':directory' => Debug::path($directory)));
622 // The quality must be in the range of 1 to 100
623 $quality = min(max($quality, 1), 100);
625 return $this->_do_save($file, $quality);
629 * Render the image and return the binary string.
631 * // Render the image at 50% quality
632 * $data = $image->render(NULL, 50);
634 * // Render the image as a PNG
635 * $data = $image->render('png');
637 * @param string image type to return: png, jpg, gif, etc
638 * @param integer quality of image: 1-100
639 * @return string
640 * @uses Image::_do_render
642 public function render($type = NULL, $quality = 100)
644 if ($type === NULL)
646 // Use the current image type
647 $type = image_type_to_extension($this->type, FALSE);
650 return $this->_do_render($type, $quality);
654 * Execute a resize.
656 * @param integer new width
657 * @param integer new height
658 * @return void
660 abstract protected function _do_resize($width, $height);
663 * Execute a crop.
665 * @param integer new width
666 * @param integer new height
667 * @param integer offset from the left
668 * @param integer offset from the top
669 * @return void
671 abstract protected function _do_crop($width, $height, $offset_x, $offset_y);
674 * Execute a rotation.
676 * @param integer degrees to rotate
677 * @return void
679 abstract protected function _do_rotate($degrees);
682 * Execute a flip.
684 * @param integer direction to flip
685 * @return void
687 abstract protected function _do_flip($direction);
690 * Execute a sharpen.
692 * @param integer amount to sharpen
693 * @return void
695 abstract protected function _do_sharpen($amount);
698 * Execute a reflection.
700 * @param integer reflection height
701 * @param integer reflection opacity
702 * @param boolean TRUE to fade out, FALSE to fade in
703 * @return void
705 abstract protected function _do_reflection($height, $opacity, $fade_in);
708 * Execute a watermarking.
710 * @param object watermarking Image
711 * @param integer offset from the left
712 * @param integer offset from the top
713 * @param integer opacity of watermark
714 * @return void
716 abstract protected function _do_watermark(Image $image, $offset_x, $offset_y, $opacity);
719 * Execute a background.
721 * @param integer red
722 * @param integer green
723 * @param integer blue
724 * @param integer opacity
725 * @return void
727 abstract protected function _do_background($r, $g, $b, $opacity);
730 * Execute a save.
732 * @param string new image filename
733 * @param integer quality
734 * @return boolean
736 abstract protected function _do_save($file, $quality);
739 * Execute a render.
741 * @param string image type: png, jpg, gif, etc
742 * @param integer quality
743 * @return string
745 abstract protected function _do_render($type, $quality);
747 } // End Image