Update composer branch alias (refs #4776)
[kohana-image.git] / classes / Kohana / Image.php
blob3fe91fcc54561a2cdc088742d82e5b2606b38a60
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;
19 const PRECISE = 0x06;
21 // Flipping directions
22 const HORIZONTAL = 0x11;
23 const VERTICAL = 0x12;
25 /**
26 * @var string default driver: GD, ImageMagick, etc
28 public static $default_driver = 'GD';
30 // Status of the driver check
31 protected static $_checked = FALSE;
33 /**
34 * Loads an image and prepares it for manipulation.
36 * $image = Image::factory('upload/test.jpg');
38 * @param string $file image file path
39 * @param string $driver driver type: GD, ImageMagick, etc
40 * @return Image
41 * @uses Image::$default_driver
43 public static function factory($file, $driver = NULL)
45 if ($driver === NULL)
47 // Use the default driver
48 $driver = Image::$default_driver;
51 // Set the class name
52 $class = 'Image_'.$driver;
54 return new $class($file);
57 /**
58 * @var string image file path
60 public $file;
62 /**
63 * @var integer image width
65 public $width;
67 /**
68 * @var integer image height
70 public $height;
72 /**
73 * @var integer one of the IMAGETYPE_* constants
75 public $type;
77 /**
78 * @var string mime type of the image
80 public $mime;
82 /**
83 * Loads information about the image. Will throw an exception if the image
84 * does not exist or is not an image.
86 * @param string $file image file path
87 * @return void
88 * @throws Kohana_Exception
90 public function __construct($file)
92 try
94 // Get the real path to the file
95 $file = realpath($file);
97 // Get the image information
98 $info = getimagesize($file);
100 catch (Exception $e)
102 // Ignore all errors while reading the image
105 if (empty($file) OR empty($info))
107 throw new Kohana_Exception('Not an image or invalid image: :file',
108 array(':file' => Debug::path($file)));
111 // Store the image information
112 $this->file = $file;
113 $this->width = $info[0];
114 $this->height = $info[1];
115 $this->type = $info[2];
116 $this->mime = image_type_to_mime_type($this->type);
120 * Render the current image.
122 * echo $image;
124 * [!!] The output of this function is binary and must be rendered with the
125 * appropriate Content-Type header or it will not be displayed correctly!
127 * @return string
129 public function __toString()
133 // Render the current image
134 return $this->render();
136 catch (Exception $e)
138 if (is_object(Kohana::$log))
140 // Get the text of the exception
141 $error = Kohana_Exception::text($e);
143 // Add this exception to the log
144 Kohana::$log->add(Log::ERROR, $error);
147 // Showing any kind of error will be "inside" image data
148 return '';
153 * Resize the image to the given size. Either the width or the height can
154 * be omitted and the image will be resized proportionally.
156 * // Resize to 200 pixels on the shortest side
157 * $image->resize(200, 200);
159 * // Resize to 200x200 pixels, keeping aspect ratio
160 * $image->resize(200, 200, Image::INVERSE);
162 * // Resize to 500 pixel width, keeping aspect ratio
163 * $image->resize(500, NULL);
165 * // Resize to 500 pixel height, keeping aspect ratio
166 * $image->resize(NULL, 500);
168 * // Resize to 200x500 pixels, ignoring aspect ratio
169 * $image->resize(200, 500, Image::NONE);
171 * @param integer $width new width
172 * @param integer $height new height
173 * @param integer $master master dimension
174 * @return $this
175 * @uses Image::_do_resize
177 public function resize($width = NULL, $height = NULL, $master = NULL)
179 if ($master === NULL)
181 // Choose the master dimension automatically
182 $master = Image::AUTO;
184 // Image::WIDTH and Image::HEIGHT deprecated. You can use it in old projects,
185 // but in new you must pass empty value for non-master dimension
186 elseif ($master == Image::WIDTH AND ! empty($width))
188 $master = Image::AUTO;
190 // Set empty height for backward compatibility
191 $height = NULL;
193 elseif ($master == Image::HEIGHT AND ! empty($height))
195 $master = Image::AUTO;
197 // Set empty width for backward compatibility
198 $width = NULL;
201 if (empty($width))
203 if ($master === Image::NONE)
205 // Use the current width
206 $width = $this->width;
208 else
210 // If width not set, master will be height
211 $master = Image::HEIGHT;
215 if (empty($height))
217 if ($master === Image::NONE)
219 // Use the current height
220 $height = $this->height;
222 else
224 // If height not set, master will be width
225 $master = Image::WIDTH;
229 switch ($master)
231 case Image::AUTO:
232 // Choose direction with the greatest reduction ratio
233 $master = ($this->width / $width) > ($this->height / $height) ? Image::WIDTH : Image::HEIGHT;
234 break;
235 case Image::INVERSE:
236 // Choose direction with the minimum reduction ratio
237 $master = ($this->width / $width) > ($this->height / $height) ? Image::HEIGHT : Image::WIDTH;
238 break;
241 switch ($master)
243 case Image::WIDTH:
244 // Recalculate the height based on the width proportions
245 $height = $this->height * $width / $this->width;
246 break;
247 case Image::HEIGHT:
248 // Recalculate the width based on the height proportions
249 $width = $this->width * $height / $this->height;
250 break;
251 case Image::PRECISE:
252 // Resize to precise size
253 $ratio = $this->width / $this->height;
255 if ($width / $height > $ratio)
257 $height = $this->height * $width / $this->width;
259 else
261 $width = $this->width * $height / $this->height;
263 break;
266 // Convert the width and height to integers, minimum value is 1px
267 $width = max(round($width), 1);
268 $height = max(round($height), 1);
270 $this->_do_resize($width, $height);
272 return $this;
276 * Crop an image to the given size. Either the width or the height can be
277 * omitted and the current width or height will be used.
279 * If no offset is specified, the center of the axis will be used.
280 * If an offset of TRUE is specified, the bottom of the axis will be used.
282 * // Crop the image to 200x200 pixels, from the center
283 * $image->crop(200, 200);
285 * @param integer $width new width
286 * @param integer $height new height
287 * @param mixed $offset_x offset from the left
288 * @param mixed $offset_y offset from the top
289 * @return $this
290 * @uses Image::_do_crop
292 public function crop($width, $height, $offset_x = NULL, $offset_y = NULL)
294 if ($width > $this->width)
296 // Use the current width
297 $width = $this->width;
300 if ($height > $this->height)
302 // Use the current height
303 $height = $this->height;
306 if ($offset_x === NULL)
308 // Center the X offset
309 $offset_x = round(($this->width - $width) / 2);
311 elseif ($offset_x === TRUE)
313 // Bottom the X offset
314 $offset_x = $this->width - $width;
316 elseif ($offset_x < 0)
318 // Set the X offset from the right
319 $offset_x = $this->width - $width + $offset_x;
322 if ($offset_y === NULL)
324 // Center the Y offset
325 $offset_y = round(($this->height - $height) / 2);
327 elseif ($offset_y === TRUE)
329 // Bottom the Y offset
330 $offset_y = $this->height - $height;
332 elseif ($offset_y < 0)
334 // Set the Y offset from the bottom
335 $offset_y = $this->height - $height + $offset_y;
338 // Determine the maximum possible width and height
339 $max_width = $this->width - $offset_x;
340 $max_height = $this->height - $offset_y;
342 if ($width > $max_width)
344 // Use the maximum available width
345 $width = $max_width;
348 if ($height > $max_height)
350 // Use the maximum available height
351 $height = $max_height;
354 $this->_do_crop($width, $height, $offset_x, $offset_y);
356 return $this;
360 * Rotate the image by a given amount.
362 * // Rotate 45 degrees clockwise
363 * $image->rotate(45);
365 * // Rotate 90% counter-clockwise
366 * $image->rotate(-90);
368 * @param integer $degrees degrees to rotate: -360-360
369 * @return $this
370 * @uses Image::_do_rotate
372 public function rotate($degrees)
374 // Make the degrees an integer
375 $degrees = (int) $degrees;
377 if ($degrees > 180)
381 // Keep subtracting full circles until the degrees have normalized
382 $degrees -= 360;
384 while ($degrees > 180);
387 if ($degrees < -180)
391 // Keep adding full circles until the degrees have normalized
392 $degrees += 360;
394 while ($degrees < -180);
397 $this->_do_rotate($degrees);
399 return $this;
403 * Flip the image along the horizontal or vertical axis.
405 * // Flip the image from top to bottom
406 * $image->flip(Image::HORIZONTAL);
408 * // Flip the image from left to right
409 * $image->flip(Image::VERTICAL);
411 * @param integer $direction direction: Image::HORIZONTAL, Image::VERTICAL
412 * @return $this
413 * @uses Image::_do_flip
415 public function flip($direction)
417 if ($direction !== Image::HORIZONTAL)
419 // Flip vertically
420 $direction = Image::VERTICAL;
423 $this->_do_flip($direction);
425 return $this;
429 * Sharpen the image by a given amount.
431 * // Sharpen the image by 20%
432 * $image->sharpen(20);
434 * @param integer $amount amount to sharpen: 1-100
435 * @return $this
436 * @uses Image::_do_sharpen
438 public function sharpen($amount)
440 // The amount must be in the range of 1 to 100
441 $amount = min(max($amount, 1), 100);
443 $this->_do_sharpen($amount);
445 return $this;
449 * Add a reflection to an image. The most opaque part of the reflection
450 * will be equal to the opacity setting and fade out to full transparent.
451 * Alpha transparency is preserved.
453 * // Create a 50 pixel reflection that fades from 0-100% opacity
454 * $image->reflection(50);
456 * // Create a 50 pixel reflection that fades from 100-0% opacity
457 * $image->reflection(50, 100, TRUE);
459 * // Create a 50 pixel reflection that fades from 0-60% opacity
460 * $image->reflection(50, 60, TRUE);
462 * [!!] By default, the reflection will be go from transparent at the top
463 * to opaque at the bottom.
465 * @param integer $height reflection height
466 * @param integer $opacity reflection opacity: 0-100
467 * @param boolean $fade_in TRUE to fade in, FALSE to fade out
468 * @return $this
469 * @uses Image::_do_reflection
471 public function reflection($height = NULL, $opacity = 100, $fade_in = FALSE)
473 if ($height === NULL OR $height > $this->height)
475 // Use the current height
476 $height = $this->height;
479 // The opacity must be in the range of 0 to 100
480 $opacity = min(max($opacity, 0), 100);
482 $this->_do_reflection($height, $opacity, $fade_in);
484 return $this;
488 * Add a watermark to an image with a specified opacity. Alpha transparency
489 * will be preserved.
491 * If no offset is specified, the center of the axis will be used.
492 * If an offset of TRUE is specified, the bottom of the axis will be used.
494 * // Add a watermark to the bottom right of the image
495 * $mark = Image::factory('upload/watermark.png');
496 * $image->watermark($mark, TRUE, TRUE);
498 * @param Image $watermark watermark Image instance
499 * @param integer $offset_x offset from the left
500 * @param integer $offset_y offset from the top
501 * @param integer $opacity opacity of watermark: 1-100
502 * @return $this
503 * @uses Image::_do_watermark
505 public function watermark(Image $watermark, $offset_x = NULL, $offset_y = NULL, $opacity = 100)
507 if ($offset_x === NULL)
509 // Center the X offset
510 $offset_x = round(($this->width - $watermark->width) / 2);
512 elseif ($offset_x === TRUE)
514 // Bottom the X offset
515 $offset_x = $this->width - $watermark->width;
517 elseif ($offset_x < 0)
519 // Set the X offset from the right
520 $offset_x = $this->width - $watermark->width + $offset_x;
523 if ($offset_y === NULL)
525 // Center the Y offset
526 $offset_y = round(($this->height - $watermark->height) / 2);
528 elseif ($offset_y === TRUE)
530 // Bottom the Y offset
531 $offset_y = $this->height - $watermark->height;
533 elseif ($offset_y < 0)
535 // Set the Y offset from the bottom
536 $offset_y = $this->height - $watermark->height + $offset_y;
539 // The opacity must be in the range of 1 to 100
540 $opacity = min(max($opacity, 1), 100);
542 $this->_do_watermark($watermark, $offset_x, $offset_y, $opacity);
544 return $this;
548 * Set the background color of an image. This is only useful for images
549 * with alpha transparency.
551 * // Make the image background black
552 * $image->background('#000');
554 * // Make the image background black with 50% opacity
555 * $image->background('#000', 50);
557 * @param string $color hexadecimal color value
558 * @param integer $opacity background opacity: 0-100
559 * @return $this
560 * @uses Image::_do_background
562 public function background($color, $opacity = 100)
564 if ($color[0] === '#')
566 // Remove the pound
567 $color = substr($color, 1);
570 if (strlen($color) === 3)
572 // Convert shorthand into longhand hex notation
573 $color = preg_replace('/./', '$0$0', $color);
576 // Convert the hex into RGB values
577 list ($r, $g, $b) = array_map('hexdec', str_split($color, 2));
579 // The opacity must be in the range of 0 to 100
580 $opacity = min(max($opacity, 0), 100);
582 $this->_do_background($r, $g, $b, $opacity);
584 return $this;
588 * Save the image. If the filename is omitted, the original image will
589 * be overwritten.
591 * // Save the image as a PNG
592 * $image->save('saved/cool.png');
594 * // Overwrite the original image
595 * $image->save();
597 * [!!] If the file exists, but is not writable, an exception will be thrown.
599 * [!!] If the file does not exist, and the directory is not writable, an
600 * exception will be thrown.
602 * @param string $file new image path
603 * @param integer $quality quality of image: 1-100
604 * @return boolean
605 * @uses Image::_save
606 * @throws Kohana_Exception
608 public function save($file = NULL, $quality = 100)
610 if ($file === NULL)
612 // Overwrite the file
613 $file = $this->file;
616 if (is_file($file))
618 if ( ! is_writable($file))
620 throw new Kohana_Exception('File must be writable: :file',
621 array(':file' => Debug::path($file)));
624 else
626 // Get the directory of the file
627 $directory = realpath(pathinfo($file, PATHINFO_DIRNAME));
629 if ( ! is_dir($directory) OR ! is_writable($directory))
631 throw new Kohana_Exception('Directory must be writable: :directory',
632 array(':directory' => Debug::path($directory)));
636 // The quality must be in the range of 1 to 100
637 $quality = min(max($quality, 1), 100);
639 return $this->_do_save($file, $quality);
643 * Render the image and return the binary string.
645 * // Render the image at 50% quality
646 * $data = $image->render(NULL, 50);
648 * // Render the image as a PNG
649 * $data = $image->render('png');
651 * @param string $type image type to return: png, jpg, gif, etc
652 * @param integer $quality quality of image: 1-100
653 * @return string
654 * @uses Image::_do_render
656 public function render($type = NULL, $quality = 100)
658 if ($type === NULL)
660 // Use the current image type
661 $type = image_type_to_extension($this->type, FALSE);
664 return $this->_do_render($type, $quality);
668 * Execute a resize.
670 * @param integer $width new width
671 * @param integer $height new height
672 * @return void
674 abstract protected function _do_resize($width, $height);
677 * Execute a crop.
679 * @param integer $width new width
680 * @param integer $height new height
681 * @param integer $offset_x offset from the left
682 * @param integer $offset_y offset from the top
683 * @return void
685 abstract protected function _do_crop($width, $height, $offset_x, $offset_y);
688 * Execute a rotation.
690 * @param integer $degrees degrees to rotate
691 * @return void
693 abstract protected function _do_rotate($degrees);
696 * Execute a flip.
698 * @param integer $direction direction to flip
699 * @return void
701 abstract protected function _do_flip($direction);
704 * Execute a sharpen.
706 * @param integer $amount amount to sharpen
707 * @return void
709 abstract protected function _do_sharpen($amount);
712 * Execute a reflection.
714 * @param integer $height reflection height
715 * @param integer $opacity reflection opacity
716 * @param boolean $fade_in TRUE to fade out, FALSE to fade in
717 * @return void
719 abstract protected function _do_reflection($height, $opacity, $fade_in);
722 * Execute a watermarking.
724 * @param Image $image watermarking Image
725 * @param integer $offset_x offset from the left
726 * @param integer $offset_y offset from the top
727 * @param integer $opacity opacity of watermark
728 * @return void
730 abstract protected function _do_watermark(Image $image, $offset_x, $offset_y, $opacity);
733 * Execute a background.
735 * @param integer $r red
736 * @param integer $g green
737 * @param integer $b blue
738 * @param integer $opacity opacity
739 * @return void
741 abstract protected function _do_background($r, $g, $b, $opacity);
744 * Execute a save.
746 * @param string $file new image filename
747 * @param integer $quality quality
748 * @return boolean
750 abstract protected function _do_save($file, $quality);
753 * Execute a render.
755 * @param string $type image type: png, jpg, gif, etc
756 * @param integer $quality quality
757 * @return string
759 abstract protected function _do_render($type, $quality);
761 } // End Image