ElixirVault/www/system-brs-core/library/cImage/cImage.php

739 lines
24 KiB
PHP

<?php
namespace BRSystems\Core\Library\cImage;
use Exception;
/**
* Image manipulation support. Allows images to be resized, cropped, etc.
*
* @package \Utils\Image
* @copyright Copyright (c) 2011, Edgars Kohs
* @license http://www.gnu.org/licenses/gpl.html, GPL
* @author Edgars Kohs <edgars@kohs.lv>
* @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;
}
}