* @version 2.0 2011-07-15 * * $image = new cImage('source/image.jpg')->resize(100,100)->save('target/image.jpg'); * $image = new cImage('source/image.jpg')->resize(100)->crop(80)->save('target/image.jpg'); * $image = new cImage('source/image.jpg')->resize(null,50)->rotate(40)->save('target/image.jpg'); * $image = new cImage('source/image.png')->background('#f00f00',90)->save('target/image.png'); */ class cImage { /** * @var string image file path */ public $file; /** * @var integer image width */ public $width; /** * @var integer image height */ public $height; /** * @var integer one of the IMAGETYPE_* constants */ public $type; /** * @var string mime type of the image */ public $mime; private $_image; // Temporary image resource private $_create_function; // Function name to open Image private static $_bundled = false; // Is GD bundled or separate? private static $_checked = false; // Status of the driver check // Resizing contraints const NONE = 0x01; const WIDTH = 0x02; const HEIGHT = 0x03; const AUTO = 0x04; const INVERSE = 0x05; // Flipping directions const HORIZONTAL = 0x11; const VERTICAL = 0x12; /** * Loads an image and prepares it for manipulation. * * @access public static * @param string image file path * @return cImage */ public static function factory($file){ return new self($file); } /** * Loads information about the image. Will throw an exception if the image * does not exist or is not an image. * * @access public * @param string image file path * @return void * @throws Exception */ public function __construct($file){ try{ $file = realpath($file); $info = getimagesize($file); }catch(Exception $e){ throw new Exception('Not an image or invalid image: '.$file); } $this->file = $file; $this->width = $info[0]; $this->height = $info[1]; $this->type = $info[2]; $this->mime = image_type_to_mime_type($this->type); switch ($this->type){ case IMAGETYPE_JPEG: $create = 'imagecreatefromjpeg'; break; case IMAGETYPE_GIF: $create = 'imagecreatefromgif'; break; case IMAGETYPE_PNG: $create = 'imagecreatefrompng'; break; } if(!isset($create) || !function_exists($create)) throw new Exception('Installed GD does not support '.image_type_to_extension($this->type, false).' images'); $this->_create_function = $create; $this->_image = $this->file; // Call check for class init $this->check(); return $this; } /** * Destroys the loaded image to free up resources. * * @access public * @return void */ public function __destruct(){ if(is_resource($this->_image)) imagedestroy($this->_image); } /** * Render the current image. * * @access public * @return string */ public function __toString(){ return $this->render(); } /** * Resize the image to the given size. Either the width or the height can * be omitted and the image will be resized proportionally. * * @access public * @param integer new width * @param integer new height * @param integer master dimension * @return $this * @uses cImage::_do_resize */ public function resize($width = null, $height = null, $master = null){ if(is_null($master)) $master = self::AUTO; if(empty($width)){ if($master === self::NONE){ $width = $this->width; }else{ $master = self::HEIGHT; } } if(empty($height)){ if($master === self::NONE){ $height = $this->height; }else{ $master = self::WIDTH; } } switch($master){ case self::AUTO: $master = ($this->width/$width) > ($this->height/$height) ? self::WIDTH : self::HEIGHT; break; case self::INVERSE: $master = ($this->width/$width) > ($this->height/$height) ? self::HEIGHT : self::WIDTH; break; } switch($master){ case self::WIDTH: $height = $this->height*$width/$this->width; break; case self::HEIGHT: $width = $this->width*$height/$this->height; break; } $width = max(round($width), 1); $height = max(round($height), 1); $this->_do_resize($width, $height); return $this; } /** * Crop an image to the given size. Either the width or the height can be * omitted and the current width or height will be used. * * @access public * @param integer new width * @param integer new height * @param mixed offset from the left * @param mixed offset from the top * @return $this * @uses cImage::_do_crop */ public function crop($width, $height, $offset_x = null, $offset_y = null){ if($width > $this->width) $width = $this->width; if($height > $this->height) $height = $this->height; if(is_null($offset_x)){ $offset_x = round(($this->width-$width)/2); }elseif(is_null($offset_x)){ $offset_x = $this->width-$width; }elseif($offset_x < 0){ $offset_x = $this->width-$width+$offset_x; } if(is_null($offset_y)){ $offset_y = round(($this->height-$height)/2); }elseif(is_null($offset_y)){ $offset_y = $this->height-$height; }elseif($offset_y < 0){ $offset_y = $this->height-$height+$offset_y; } $max_width = $this->width-$offset_x; $max_height = $this->height-$offset_y; if($width > $max_width) $width = $max_width; if($height > $max_height) $height = $max_height; $this->_do_crop($width, $height, $offset_x, $offset_y); return $this; } /** * Rotate the image by a given amount. * * @access public * @param integer degrees to rotate: -360-360 * @return $this * @uses cImage::_do_rotate */ public function rotate($degrees){ $degrees = (int)$degrees; if($degrees > 180){ do{ $degrees-= 360; }while($degrees > 180); } if($degrees < -180){ do{ $degrees+= 360; }while($degrees < -180); } $this->_do_rotate($degrees); return $this; } /** * Flip the image along the horizontal or vertical axis. * * @access public * @param integer direction: cImage::HORIZONTAL, cImage::VERTICAL * @return $this * @uses cImage::_do_flip */ public function flip($direction){ if($direction !== self::HORIZONTAL) $direction = self::VERTICAL; $this->_do_flip($direction); return $this; } /** * Sharpen the image by a given amount. * * @access public * @param integer amount to sharpen: 1-100 * @return $this * @uses cImage::_do_sharpen */ public function sharpen($amount){ $amount = min(max($amount, 1), 100); $this->_do_sharpen($amount); return $this; } /** * Add a reflection to an image. The most opaque part of the reflection * will be equal to the opacity setting and fade out to full transparent. * * @access public * @param integer reflection height * @param integer reflection opacity: 0-100 * @param boolean TRUE to fade in, FALSE to fade out * @return $this * @uses cImage::_do_reflection */ public function reflection($height = null, $opacity = 100, $fade_in = false){ if(is_null($height) || $height > $this->height) $height = $this->height; $opacity = min(max($opacity, 0), 100); $this->_do_reflection($height, $opacity, $fade_in); return $this; } /** * Add a watermark to an image with a specified opacity. Alpha transparency * will be preserved. * * @access public * @param cImage watermark cImage instance * @param integer offset from the left * @param integer offset from the top * @param integer opacity of watermark: 1-100 * @return $this * @uses cImage::_do_watermark */ public function watermark(cImage $watermark, $offset_x = null, $offset_y = null, $opacity = 100){ if(is_null($offset_x)){ $offset_x = round(($this->width-$watermark->width)/2); }elseif(is_null($offset_x)){ $offset_x = $this->width-$watermark->width; }elseif($offset_x < 0){ $offset_x = $this->width-$watermark->width+$offset_x; } if(is_null($offset_y)){ $offset_y = round(($this->height-$watermark->height)/2); }elseif(is_null($offset_y)){ $offset_y = $this->height-$watermark->height; }elseif($offset_y < 0){ $offset_y = $this->height-$watermark->height+$offset_y; } $opacity = min(max($opacity, 1), 100); $this->_do_watermark($watermark, $offset_x, $offset_y, $opacity); return $this; } /** * Set the background color of an image. This is only useful for images * with alpha transparency. * * @access public * @param string hexadecimal color value * @param integer background opacity: 0-100 * @return $this * @uses cImage::_do_background */ public function background($color, $opacity = 100){ if($color[0] === '#') $color = substr($color, 1); if(strlen($color) === 3) $color = preg_replace('/./', '$0$0', $color); list($r, $g, $b) = array_map('hexdec', str_split($color, 2)); $opacity = min(max($opacity, 0), 100); $this->_do_background($r, $g, $b, $opacity); return $this; } /** * Save the image. If the filename is omitted, the original image will * be overwritten. * * @access public * @param string new image path * @param integer quality of image: 1-100 * @return boolean * @uses cImage::_save * @throws Exception */ public function save($file = null, $quality = 100){ if(is_null($file)) $file = $this->file; if(is_file($file)){ if(!is_writable($file)) throw new Exception('File must be writable: '.$file); }else{ $directory = realpath(pathinfo($file, PATHINFO_DIRNAME)); if(!is_dir($directory) || !is_writable($directory)) throw new Exception('Directory must be writable: '.$directory); } $quality = min(max($quality, 1), 100); return $this->_do_save($file, $quality); } /** * Render the image and return the binary string. * * @access public * @param string image type to return: png, jpg, gif, etc * @param integer quality of image: 1-100 * @return string * @uses cImage::_do_render */ public function render($type = null, $quality = 100){ if(is_null($type)) $type = image_type_to_extension($this->type, false); return $this->_do_render($type, $quality); } /** * Checks if GD is enabled and bundled. Bundled GD is required for some * methods to work. Exceptions will be thrown from those methods when GD is * not bundled. * * @access private * @return boolean */ public static function check(){ if(!function_exists('gd_info')) throw new Exception('GD is either not installed or not enabled, check your configuration'); if(defined('GD_BUNDLED')){ self::$_bundled = GD_BUNDLED; }else{ $info = gd_info(); self::$_bundled = (bool)preg_match('/\bbundled\b/i', $info['GD Version']); } if(defined('GD_VERSION')){ $version = GD_VERSION; }else{ $info = gd_info(); preg_match('/\d+\.\d+(?:\.\d+)?/', $info['GD Version'], $matches); $version = $matches[0]; } if(!version_compare($version, '2.0.1', '>=')) throw new Exception('Image requires GD version 2.0.1 or greater, you have '.$version); return self::$_checked = true; } /** * Loads an image into GD. * * @access private * @return void */ private function _load_image(){ if(!is_resource($this->_image)){ $this->_image = call_user_func($this->_create_function, $this->file); imagesavealpha($this->_image, true); } } /** * Execute a resize. * * @access private * @param integer new width * @param integer new height * @return void */ private function _do_resize($width, $height){ $pre_width = $this->width; $pre_height = $this->height; $this->_load_image(); if($width > ($this->width/2) && $height > ($this->height/2)){ $reduction_width = round($width*1.1); $reduction_height = round($height*1.1); while($pre_width/2 > $reduction_width && $pre_height/2 > $reduction_height){ $pre_width/= 2; $pre_height/= 2; } $image = $this->_create($pre_width, $pre_height); if(imagecopyresized($image, $this->_image, 0, 0, 0, 0, $pre_width, $pre_height, $this->width, $this->height)){ imagedestroy($this->_image); $this->_image = $image; } } $image = $this->_create($width, $height); if(imagecopyresampled($image, $this->_image, 0, 0, 0, 0, $width, $height, $pre_width, $pre_height)){ imagedestroy($this->_image); $this->_image = $image; $this->width = imagesx($image); $this->height = imagesy($image); } } /** * Execute a crop. * * @access private * @param integer new width * @param integer new height * @param integer offset from the left * @param integer offset from the top * @return void */ private function _do_crop($width, $height, $offset_x, $offset_y){ $image = $this->_create($width, $height); $this->_load_image(); if(imagecopyresampled($image, $this->_image, 0, 0, $offset_x, $offset_y, $width, $height, $width, $height)){ imagedestroy($this->_image); $this->_image = $image; $this->width = imagesx($image); $this->height = imagesy($image); } } /** * Execute a rotation. * * @access private * @param integer degrees to rotate * @return void */ private function _do_rotate($degrees){ if(!self::$_bundled) throw new Exception('This method requires imagerotate, which is only available in the bundled version of GD'); $this->_load_image(); $transparent = imagecolorallocatealpha($this->_image, 0, 0, 0, 127); $image = imagerotate($this->_image, 360-$degrees, $transparent, 1); imagesavealpha($image, true); $width = imagesx($image); $height = imagesy($image); if(imagecopymerge($this->_image, $image, 0, 0, 0, 0, $width, $height, 100)){ imagedestroy($this->_image); $this->_image = $image; $this->width = $width; $this->height = $height; } } /** * Execute a flip. * * @access private * @param integer direction to flip * @return void */ private function _do_flip($direction){ $flipped = $this->_create($this->width, $this->height); $this->_load_image(); if($direction === self::HORIZONTAL){ for($x = 0; $x < $this->width; $x++) imagecopy($flipped, $this->_image, $x, 0, $this->width-$x-1, 0, 1, $this->height); }else{ for($y = 0; $y < $this->height; $y++) imagecopy($flipped, $this->_image, 0, $y, 0, $this->height-$y-1, $this->width, 1); } imagedestroy($this->_image); $this->_image = $flipped; $this->width = imagesx($flipped); $this->height = imagesy($flipped); } /** * Execute a sharpen. * * @access private * @param integer amount to sharpen * @return void */ private function _do_sharpen($amount){ if(!self::$_bundled) throw new Exception('This method requires imageconvolution, which is only available in the bundled version of GD'); $this->_load_image(); $amount = round(abs(-18+($amount*0.08)), 2); $matrix = array(array(-1, -1, -1), array(-1, $amount, -1), array(-1, -1, -1),); if(imageconvolution($this->_image, $matrix, $amount-8, 0)){ $this->width = imagesx($this->_image); $this->height = imagesy($this->_image); } } /** * Execute a reflection. * * @access private * @param integer reflection height * @param integer reflection opacity * @param boolean TRUE to fade out, FALSE to fade in * @return void */ private function _do_reflection($height, $opacity, $fade_in){ if(!self::$_bundled) throw new Exception('This method requires imagefilter, which is only available in the bundled version of GD'); $this->_load_image(); $opacity = round(abs(($opacity*127/100)-127)); if($opacity < 127){ $stepping = (127-$opacity)/$height; }else{ $stepping = 127/$height; } $reflection = $this->_create($this->width, $this->height+$height); imagecopy($reflection, $this->_image, 0, 0, 0, 0, $this->width, $this->height); for($offset = 0; $height >= $offset; $offset++){ $src_y = $this->height-$offset-1; $dst_y = $this->height+$offset; $dst_opacity = round($fade_in ? $opacity+($stepping*($height-$offset)) : $opacity + ($stepping * $offset)); $line = $this->_create($this->width, 1); imagecopy($line, $this->_image, 0, 0, 0, $src_y, $this->width, 1); imagefilter($line, IMG_FILTER_COLORIZE, 0, 0, 0, $dst_opacity); imagecopy($reflection, $line, 0, $dst_y, 0, 0, $this->width, 1); } imagedestroy($this->_image); $this->_image = $reflection; $this->width = imagesx($reflection); $this->height = imagesy($reflection); } /** * Execute a watermarking. * * @access private * @param object watermarking Image * @param integer offset from the left * @param integer offset from the top * @param integer opacity of watermark * @return void */ private function _do_watermark(cImage $watermark, $offset_x, $offset_y, $opacity){ /** @noinspection PhpUndefinedClassInspection */ //if(!cImage::$_bundled) throw new Exception('This method requires imagelayereffect, which is only available in the bundled version of GD'); // Loads image if not yet loaded $this->_load_image(); // Create the watermark image resource $overlay = imagecreatefromstring($watermark->render()); imagesavealpha($overlay, TRUE); // Get the width and height of the watermark $width = imagesx($overlay); $height = imagesy($overlay); if ($opacity < 100) { // Convert an opacity range of 0-100 to 127-0 $opacity = round(abs(($opacity * 127 / 100) - 127)); // Allocate transparent gray $color = imagecolorallocatealpha($overlay, 127, 127, 127, $opacity); // The transparent image will overlay the watermark imagelayereffect($overlay, IMG_EFFECT_OVERLAY); // Fill the background with the transparent color imagefilledrectangle($overlay, 0, 0, $width, $height, $color); } // Alpha blending must be enabled on the background! imagealphablending($this->_image, TRUE); if (imagecopy($this->_image, $overlay, $offset_x, $offset_y, 0, 0, $width, $height)) { // Destroy the overlay image imagedestroy($overlay); } } /** * Execute a background. * * @access private * @param integer red * @param integer green * @param integer blue * @param integer opacity * @return void */ private function _do_background($r, $g, $b, $opacity){ $this->_load_image(); $opacity = round(abs(($opacity*127/100)-127)); $background = $this->_create($this->width, $this->height); $color = imagecolorallocatealpha($background, $r, $g, $b, $opacity); imagefilledrectangle($background, 0, 0, $this->width, $this->height, $color); imagealphablending($background, true); if(imagecopy($background, $this->_image, 0, 0, 0, 0, $this->width, $this->height)){ imagedestroy($this->_image); $this->_image = $background; } } /** * Execute a save. * * @access private * @param string new image filename * @param integer quality * @return boolean */ private function _do_save($file, $quality){ $this->_load_image(); $extension = pathinfo($file, PATHINFO_EXTENSION); list($save, $type) = $this->_save_function($extension, $quality); $status = isset($quality) ? $save($this->_image, $file, $quality) : $save($this->_image, $file); if($status && $type !== $this->type){ $this->type = $type; $this->mime = image_type_to_mime_type($type); } return true; } /** * Execute a render. * * @access private * @param string image type: png, jpg, gif, etc * @param integer quality * @return string */ private function _do_render($type, $quality){ $this->_load_image(); list($save, $type) = $this->_save_function($type, $quality); ob_start(); $status = isset($quality) ? $save($this->_image, null, $quality) : $save($this->_image, null); if($status && $type !== $this->type){ $this->type = $type; $this->mime = image_type_to_mime_type($type); } return ob_get_clean(); } /** * Get the GD saving function and image type for this extension. * Also normalizes the quality setting * * @access private * @param string image type: png, jpg, etc * @param integer image quality * @return array save function, IMAGETYPE_* constant * @throws Exception */ private function _save_function($extension, &$quality){ switch(strtolower($extension)){ case 'jpg': case 'jpeg': $save = 'imagejpeg'; $type = IMAGETYPE_JPEG; break; case 'gif': $save = 'imagegif'; $type = IMAGETYPE_GIF; $quality = null; break; case 'png': $save = 'imagepng'; $type = IMAGETYPE_PNG; $quality = 9; break; default: throw new Exception('Installed GD does not support '.$extension.' images'); break; } return array($save, $type); } /** * Create an empty image with the given width and height. * * @access private * @param integer image width * @param integer image height * @return resource */ private function _create($width, $height){ $image = imagecreatetruecolor($width, $height); imagealphablending($image, false); imagesavealpha($image, true); return $image; } }