Merge pull request #26 from kohana/3.3/test/migrate-travis-container-based
[kohana-image.git] / classes / Kohana / Image.php
blobb67e6a67e920f95e6f0eaa4a7f1b65d7ca060dd1
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 * @deprecated - provide an image.default_driver value in your configuration instead
27 * @var string default driver: GD, ImageMagick, etc
29 public static $default_driver = 'GD';
31 // Status of the driver check
32 protected static $_checked = FALSE;
34 /**
35 * Loads an image and prepares it for manipulation.
37 * $image = Image::factory('upload/test.jpg');
39 * @param string $file image file path
40 * @param string $driver driver type: GD, ImageMagick, etc
41 * @return Image
42 * @uses Image::$default_driver
44 public static function factory($file, $driver = NULL)
46 if ($driver === NULL)
48 // Use the driver from configuration file or default one
49 $configured_driver = Kohana::$config->load('image.default_driver');
50 $driver = ($configured_driver) ? $configured_driver : Image::$default_driver;
53 // Set the class name
54 $class = 'Image_'.$driver;
56 return new $class($file);
59 /**
60 * @var string image file path
62 public $file;
64 /**
65 * @var integer image width
67 public $width;
69 /**
70 * @var integer image height
72 public $height;
74 /**
75 * @var integer one of the IMAGETYPE_* constants
77 public $type;
79 /**
80 * @var string mime type of the image
82 public $mime;
84 /**
85 * Loads information about the image. Will throw an exception if the image
86 * does not exist or is not an image.
88 * @param string $file image file path
89 * @return void
90 * @throws Kohana_Exception
92 public function __construct($file)
94 try
96 // Get the real path to the file
97 $file = realpath($file);
99 // Get the image information
100 $info = getimagesize($file);
102 catch (Exception $e)
104 // Ignore all errors while reading the image
107 if (empty($file) OR empty($info))
109 throw new Kohana_Exception('Not an image or invalid image: :file',
110 array(':file' => Debug::path($file)));
113 // Store the image information
114 $this->file = $file;
115 $this->width = $info[0];
116 $this->height = $info[1];
117 $this->type = $info[2];
118 $this->mime = image_type_to_mime_type($this->type);
122 * Render the current image.
124 * echo $image;
126 * [!!] The output of this function is binary and must be rendered with the
127 * appropriate Content-Type header or it will not be displayed correctly!
129 * @return string
131 public function __toString()
135 // Render the current image
136 return $this->render();
138 catch (Exception $e)
140 if (is_object(Kohana::$log))
142 // Get the text of the exception
143 $error = Kohana_Exception::text($e);
145 // Add this exception to the log
146 Kohana::$log->add(Log::ERROR, $error);
149 // Showing any kind of error will be "inside" image data
150 return '';
155 * Resize the image to the given size. Either the width or the height can
156 * be omitted and the image will be resized proportionally.
158 * // Resize to 200 pixels on the shortest side
159 * $image->resize(200, 200);
161 * // Resize to 200x200 pixels, keeping aspect ratio
162 * $image->resize(200, 200, Image::INVERSE);
164 * // Resize to 500 pixel width, keeping aspect ratio
165 * $image->resize(500, NULL);
167 * // Resize to 500 pixel height, keeping aspect ratio
168 * $image->resize(NULL, 500);
170 * // Resize to 200x500 pixels, ignoring aspect ratio
171 * $image->resize(200, 500, Image::NONE);
173 * @param integer $width new width
174 * @param integer $height new height
175 * @param integer $master master dimension
176 * @return $this
177 * @uses Image::_do_resize
179 public function resize($width = NULL, $height = NULL, $master = NULL)
181 if ($master === NULL)
183 // Choose the master dimension automatically
184 $master = Image::AUTO;
186 // Image::WIDTH and Image::HEIGHT deprecated. You can use it in old projects,
187 // but in new you must pass empty value for non-master dimension
188 elseif ($master == Image::WIDTH AND ! empty($width))
190 $master = Image::AUTO;
192 // Set empty height for backward compatibility
193 $height = NULL;
195 elseif ($master == Image::HEIGHT AND ! empty($height))
197 $master = Image::AUTO;
199 // Set empty width for backward compatibility
200 $width = NULL;
203 if (empty($width))
205 if ($master === Image::NONE)
207 // Use the current width
208 $width = $this->width;
210 else
212 // If width not set, master will be height
213 $master = Image::HEIGHT;
217 if (empty($height))
219 if ($master === Image::NONE)
221 // Use the current height
222 $height = $this->height;
224 else
226 // If height not set, master will be width
227 $master = Image::WIDTH;
231 switch ($master)
233 case Image::AUTO:
234 // Choose direction with the greatest reduction ratio
235 $master = ($this->width / $width) > ($this->height / $height) ? Image::WIDTH : Image::HEIGHT;
236 break;
237 case Image::INVERSE:
238 // Choose direction with the minimum reduction ratio
239 $master = ($this->width / $width) > ($this->height / $height) ? Image::HEIGHT : Image::WIDTH;
240 break;
243 switch ($master)
245 case Image::WIDTH:
246 // Recalculate the height based on the width proportions
247 $height = $this->height * $width / $this->width;
248 break;
249 case Image::HEIGHT:
250 // Recalculate the width based on the height proportions
251 $width = $this->width * $height / $this->height;
252 break;
253 case Image::PRECISE:
254 // Resize to precise size
255 $ratio = $this->width / $this->height;
257 if ($width / $height > $ratio)
259 $height = $this->height * $width / $this->width;
261 else
263 $width = $this->width * $height / $this->height;
265 break;
268 // Convert the width and height to integers, minimum value is 1px
269 $width = max(round($width), 1);
270 $height = max(round($height), 1);
272 $this->_do_resize($width, $height);
274 return $this;
278 * Crop an image to the given size. Either the width or the height can be
279 * omitted and the current width or height will be used.
281 * If no offset is specified, the center of the axis will be used.
282 * If an offset of TRUE is specified, the bottom of the axis will be used.
284 * // Crop the image to 200x200 pixels, from the center
285 * $image->crop(200, 200);
287 * @param integer $width new width
288 * @param integer $height new height
289 * @param mixed $offset_x offset from the left
290 * @param mixed $offset_y offset from the top
291 * @return $this
292 * @uses Image::_do_crop
294 public function crop($width, $height, $offset_x = NULL, $offset_y = NULL)
296 if ($width > $this->width)
298 // Use the current width
299 $width = $this->width;
302 if ($height > $this->height)
304 // Use the current height
305 $height = $this->height;
308 if ($offset_x === NULL)
310 // Center the X offset
311 $offset_x = round(($this->width - $width) / 2);
313 elseif ($offset_x === TRUE)
315 // Bottom the X offset
316 $offset_x = $this->width - $width;
318 elseif ($offset_x < 0)
320 // Set the X offset from the right
321 $offset_x = $this->width - $width + $offset_x;
324 if ($offset_y === NULL)
326 // Center the Y offset
327 $offset_y = round(($this->height - $height) / 2);
329 elseif ($offset_y === TRUE)
331 // Bottom the Y offset
332 $offset_y = $this->height - $height;
334 elseif ($offset_y < 0)
336 // Set the Y offset from the bottom
337 $offset_y = $this->height - $height + $offset_y;
340 // Determine the maximum possible width and height
341 $max_width = $this->width - $offset_x;
342 $max_height = $this->height - $offset_y;
344 if ($width > $max_width)
346 // Use the maximum available width
347 $width = $max_width;
350 if ($height > $max_height)
352 // Use the maximum available height
353 $height = $max_height;
356 $this->_do_crop($width, $height, $offset_x, $offset_y);
358 return $this;
362 * Rotate the image by a given amount.
364 * // Rotate 45 degrees clockwise
365 * $image->rotate(45);
367 * // Rotate 90% counter-clockwise
368 * $image->rotate(-90);
370 * @param integer $degrees degrees to rotate: -360-360
371 * @return $this
372 * @uses Image::_do_rotate
374 public function rotate($degrees)
376 // Make the degrees an integer
377 $degrees = (int) $degrees;
379 if ($degrees > 180)
383 // Keep subtracting full circles until the degrees have normalized
384 $degrees -= 360;
386 while ($degrees > 180);
389 if ($degrees < -180)
393 // Keep adding full circles until the degrees have normalized
394 $degrees += 360;
396 while ($degrees < -180);
399 $this->_do_rotate($degrees);
401 return $this;
405 * Flip the image along the horizontal or vertical axis.
407 * // Flip the image from top to bottom
408 * $image->flip(Image::HORIZONTAL);
410 * // Flip the image from left to right
411 * $image->flip(Image::VERTICAL);
413 * @param integer $direction direction: Image::HORIZONTAL, Image::VERTICAL
414 * @return $this
415 * @uses Image::_do_flip
417 public function flip($direction)
419 if ($direction !== Image::HORIZONTAL)
421 // Flip vertically
422 $direction = Image::VERTICAL;
425 $this->_do_flip($direction);
427 return $this;
431 * Sharpen the image by a given amount.
433 * // Sharpen the image by 20%
434 * $image->sharpen(20);
436 * @param integer $amount amount to sharpen: 1-100
437 * @return $this
438 * @uses Image::_do_sharpen
440 public function sharpen($amount)
442 // The amount must be in the range of 1 to 100
443 $amount = min(max($amount, 1), 100);
445 $this->_do_sharpen($amount);
447 return $this;
451 * Add a reflection to an image. The most opaque part of the reflection
452 * will be equal to the opacity setting and fade out to full transparent.
453 * Alpha transparency is preserved.
455 * // Create a 50 pixel reflection that fades from 0-100% opacity
456 * $image->reflection(50);
458 * // Create a 50 pixel reflection that fades from 100-0% opacity
459 * $image->reflection(50, 100, TRUE);
461 * // Create a 50 pixel reflection that fades from 0-60% opacity
462 * $image->reflection(50, 60, TRUE);
464 * [!!] By default, the reflection will be go from transparent at the top
465 * to opaque at the bottom.
467 * @param integer $height reflection height
468 * @param integer $opacity reflection opacity: 0-100
469 * @param boolean $fade_in TRUE to fade in, FALSE to fade out
470 * @return $this
471 * @uses Image::_do_reflection
473 public function reflection($height = NULL, $opacity = 100, $fade_in = FALSE)
475 if ($height === NULL OR $height > $this->height)
477 // Use the current height
478 $height = $this->height;
481 // The opacity must be in the range of 0 to 100
482 $opacity = min(max($opacity, 0), 100);
484 $this->_do_reflection($height, $opacity, $fade_in);
486 return $this;
490 * Add a watermark to an image with a specified opacity. Alpha transparency
491 * will be preserved.
493 * If no offset is specified, the center of the axis will be used.
494 * If an offset of TRUE is specified, the bottom of the axis will be used.
496 * // Add a watermark to the bottom right of the image
497 * $mark = Image::factory('upload/watermark.png');
498 * $image->watermark($mark, TRUE, TRUE);
500 * @param Image $watermark watermark Image instance
501 * @param integer $offset_x offset from the left
502 * @param integer $offset_y offset from the top
503 * @param integer $opacity opacity of watermark: 1-100
504 * @return $this
505 * @uses Image::_do_watermark
507 public function watermark(Image $watermark, $offset_x = NULL, $offset_y = NULL, $opacity = 100)
509 if ($offset_x === NULL)
511 // Center the X offset
512 $offset_x = round(($this->width - $watermark->width) / 2);
514 elseif ($offset_x === TRUE)
516 // Bottom the X offset
517 $offset_x = $this->width - $watermark->width;
519 elseif ($offset_x < 0)
521 // Set the X offset from the right
522 $offset_x = $this->width - $watermark->width + $offset_x;
525 if ($offset_y === NULL)
527 // Center the Y offset
528 $offset_y = round(($this->height - $watermark->height) / 2);
530 elseif ($offset_y === TRUE)
532 // Bottom the Y offset
533 $offset_y = $this->height - $watermark->height;
535 elseif ($offset_y < 0)
537 // Set the Y offset from the bottom
538 $offset_y = $this->height - $watermark->height + $offset_y;
541 // The opacity must be in the range of 1 to 100
542 $opacity = min(max($opacity, 1), 100);
544 $this->_do_watermark($watermark, $offset_x, $offset_y, $opacity);
546 return $this;
550 * Set the background color of an image. This is only useful for images
551 * with alpha transparency.
553 * // Make the image background black
554 * $image->background('#000');
556 * // Make the image background black with 50% opacity
557 * $image->background('#000', 50);
559 * @param string $color hexadecimal color value
560 * @param integer $opacity background opacity: 0-100
561 * @return $this
562 * @uses Image::_do_background
564 public function background($color, $opacity = 100)
566 if ($color[0] === '#')
568 // Remove the pound
569 $color = substr($color, 1);
572 if (strlen($color) === 3)
574 // Convert shorthand into longhand hex notation
575 $color = preg_replace('/./', '$0$0', $color);
578 // Convert the hex into RGB values
579 list ($r, $g, $b) = array_map('hexdec', str_split($color, 2));
581 // The opacity must be in the range of 0 to 100
582 $opacity = min(max($opacity, 0), 100);
584 $this->_do_background($r, $g, $b, $opacity);
586 return $this;
590 * Save the image. If the filename is omitted, the original image will
591 * be overwritten.
593 * // Save the image as a PNG
594 * $image->save('saved/cool.png');
596 * // Overwrite the original image
597 * $image->save();
599 * [!!] If the file exists, but is not writable, an exception will be thrown.
601 * [!!] If the file does not exist, and the directory is not writable, an
602 * exception will be thrown.
604 * @param string $file new image path
605 * @param integer $quality quality of image: 1-100
606 * @return boolean
607 * @uses Image::_save
608 * @throws Kohana_Exception
610 public function save($file = NULL, $quality = 100)
612 if ($file === NULL)
614 // Overwrite the file
615 $file = $this->file;
618 if (is_file($file))
620 if ( ! is_writable($file))
622 throw new Kohana_Exception('File must be writable: :file',
623 array(':file' => Debug::path($file)));
626 else
628 // Get the directory of the file
629 $directory = realpath(pathinfo($file, PATHINFO_DIRNAME));
631 if ( ! is_dir($directory) OR ! is_writable($directory))
633 throw new Kohana_Exception('Directory must be writable: :directory',
634 array(':directory' => Debug::path($directory)));
638 // The quality must be in the range of 1 to 100
639 $quality = min(max($quality, 1), 100);
641 return $this->_do_save($file, $quality);
645 * Render the image and return the binary string.
647 * // Render the image at 50% quality
648 * $data = $image->render(NULL, 50);
650 * // Render the image as a PNG
651 * $data = $image->render('png');
653 * @param string $type image type to return: png, jpg, gif, etc
654 * @param integer $quality quality of image: 1-100
655 * @return string
656 * @uses Image::_do_render
658 public function render($type = NULL, $quality = 100)
660 if ($type === NULL)
662 // Use the current image type
663 $type = image_type_to_extension($this->type, FALSE);
666 return $this->_do_render($type, $quality);
670 * Execute a resize.
672 * @param integer $width new width
673 * @param integer $height new height
674 * @return void
676 abstract protected function _do_resize($width, $height);
679 * Execute a crop.
681 * @param integer $width new width
682 * @param integer $height new height
683 * @param integer $offset_x offset from the left
684 * @param integer $offset_y offset from the top
685 * @return void
687 abstract protected function _do_crop($width, $height, $offset_x, $offset_y);
690 * Execute a rotation.
692 * @param integer $degrees degrees to rotate
693 * @return void
695 abstract protected function _do_rotate($degrees);
698 * Execute a flip.
700 * @param integer $direction direction to flip
701 * @return void
703 abstract protected function _do_flip($direction);
706 * Execute a sharpen.
708 * @param integer $amount amount to sharpen
709 * @return void
711 abstract protected function _do_sharpen($amount);
714 * Execute a reflection.
716 * @param integer $height reflection height
717 * @param integer $opacity reflection opacity
718 * @param boolean $fade_in TRUE to fade out, FALSE to fade in
719 * @return void
721 abstract protected function _do_reflection($height, $opacity, $fade_in);
724 * Execute a watermarking.
726 * @param Image $image watermarking Image
727 * @param integer $offset_x offset from the left
728 * @param integer $offset_y offset from the top
729 * @param integer $opacity opacity of watermark
730 * @return void
732 abstract protected function _do_watermark(Image $image, $offset_x, $offset_y, $opacity);
735 * Execute a background.
737 * @param integer $r red
738 * @param integer $g green
739 * @param integer $b blue
740 * @param integer $opacity opacity
741 * @return void
743 abstract protected function _do_background($r, $g, $b, $opacity);
746 * Execute a save.
748 * @param string $file new image filename
749 * @param integer $quality quality
750 * @return boolean
752 abstract protected function _do_save($file, $quality);
755 * Execute a render.
757 * @param string $type image type: png, jpg, gif, etc
758 * @param integer $quality quality
759 * @return string
761 abstract protected function _do_render($type, $quality);
763 } // End Image