503 lines
13 KiB
PHP
503 lines
13 KiB
PHP
|
<?php defined('SYSPATH') OR die('No direct access allowed.');
|
||
|
/**
|
||
|
* Manipulate images using standard methods such as resize, crop, rotate, etc.
|
||
|
* This class must be re-initialized for every image you wish to manipulate.
|
||
|
*
|
||
|
* $Id: Image.php 4679 2009-11-10 01:45:52Z isaiah $
|
||
|
*
|
||
|
* @package Image
|
||
|
* @author Kohana Team
|
||
|
* @copyright (c) 2007-2009 Kohana Team
|
||
|
* @license http://kohanaphp.com/license
|
||
|
*/
|
||
|
class Image_Core {
|
||
|
|
||
|
// Master Dimension
|
||
|
const NONE = 1;
|
||
|
const AUTO = 2;
|
||
|
const HEIGHT = 3;
|
||
|
const WIDTH = 4;
|
||
|
|
||
|
// Flip Directions
|
||
|
const HORIZONTAL = 5;
|
||
|
const VERTICAL = 6;
|
||
|
|
||
|
// Orientations
|
||
|
const PORTRAIT = 7;
|
||
|
const LANDSCAPE = 8;
|
||
|
const SQUARE = 9;
|
||
|
|
||
|
// Allowed image types
|
||
|
public static $allowed_types = array
|
||
|
(
|
||
|
IMAGETYPE_GIF => 'gif',
|
||
|
IMAGETYPE_JPEG => 'jpg',
|
||
|
IMAGETYPE_PNG => 'png',
|
||
|
IMAGETYPE_TIFF_II => 'tiff',
|
||
|
IMAGETYPE_TIFF_MM => 'tiff',
|
||
|
);
|
||
|
|
||
|
// Driver instance
|
||
|
protected $driver;
|
||
|
|
||
|
// Driver actions
|
||
|
protected $actions = array();
|
||
|
|
||
|
// Reference to the current image filename
|
||
|
protected $image = '';
|
||
|
|
||
|
/**
|
||
|
* Creates a new Image instance and returns it.
|
||
|
*
|
||
|
* @param string filename of image
|
||
|
* @param array non-default configurations
|
||
|
* @return object
|
||
|
*/
|
||
|
public static function factory($image, $config = NULL)
|
||
|
{
|
||
|
return new Image($image, $config);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a new image editor instance.
|
||
|
*
|
||
|
* @throws Kohana_Exception
|
||
|
* @param string filename of image
|
||
|
* @param array non-default configurations
|
||
|
* @return void
|
||
|
*/
|
||
|
public function __construct($image, $config = NULL)
|
||
|
{
|
||
|
static $check;
|
||
|
|
||
|
// Make the check exactly once
|
||
|
($check === NULL) and $check = function_exists('getimagesize');
|
||
|
|
||
|
if ($check === FALSE)
|
||
|
throw new Kohana_Exception('The Image library requires the getimagesize() PHP function, which is not available in your installation.');
|
||
|
|
||
|
// Check to make sure the image exists
|
||
|
if ( ! is_file($image))
|
||
|
throw new Kohana_Exception('The specified image, :image:, was not found. Please verify that images exist by using file_exists() before manipulating them.', array(':image:' => $image));
|
||
|
|
||
|
// Disable error reporting, to prevent PHP warnings
|
||
|
$ER = error_reporting(0);
|
||
|
|
||
|
// Fetch the image size and mime type
|
||
|
$image_info = getimagesize($image);
|
||
|
|
||
|
// Turn on error reporting again
|
||
|
error_reporting($ER);
|
||
|
|
||
|
// Make sure that the image is readable and valid
|
||
|
if ( ! is_array($image_info) OR count($image_info) < 3)
|
||
|
throw new Kohana_Exception('The file specified, :file:, is not readable or is not an image', array(':file:' => $image));
|
||
|
|
||
|
// Check to make sure the image type is allowed
|
||
|
if ( ! isset(Image::$allowed_types[$image_info[2]]))
|
||
|
throw new Kohana_Exception('The specified image, :type:, is not an allowed image type.', array(':type:' => $image));
|
||
|
|
||
|
// Image has been validated, load it
|
||
|
$this->image = array
|
||
|
(
|
||
|
'file' => str_replace('\\', '/', realpath($image)),
|
||
|
'width' => $image_info[0],
|
||
|
'height' => $image_info[1],
|
||
|
'type' => $image_info[2],
|
||
|
'ext' => Image::$allowed_types[$image_info[2]],
|
||
|
'mime' => $image_info['mime']
|
||
|
);
|
||
|
|
||
|
$this->determine_orientation();
|
||
|
|
||
|
// Load configuration
|
||
|
$this->config = (array) $config + Kohana::config('image');
|
||
|
|
||
|
// Set driver class name
|
||
|
$driver = 'Image_'.ucfirst($this->config['driver']).'_Driver';
|
||
|
|
||
|
// Load the driver
|
||
|
if ( ! Kohana::auto_load($driver))
|
||
|
throw new Kohana_Exception('The :driver: driver for the :library: library could not be found',
|
||
|
array(':driver:' => $this->config['driver'], ':library:' => get_class($this)));
|
||
|
|
||
|
// Initialize the driver
|
||
|
$this->driver = new $driver($this->config['params']);
|
||
|
|
||
|
// Validate the driver
|
||
|
if ( ! ($this->driver instanceof Image_Driver))
|
||
|
throw new Kohana_Exception('The :driver: driver for the :library: library must implement the :interface: interface',
|
||
|
array(':driver:' => $this->config['driver'], ':library:' => get_class($this), ':interface:' => 'Image_Driver'));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Works out the correct orientation for the image
|
||
|
*
|
||
|
* @return void
|
||
|
*/
|
||
|
protected function determine_orientation()
|
||
|
{
|
||
|
switch (TRUE)
|
||
|
{
|
||
|
case $this->image['height'] > $this->image['width']:
|
||
|
$orientation = Image::PORTRAIT;
|
||
|
break;
|
||
|
|
||
|
case $this->image['height'] < $this->image['width']:
|
||
|
$orientation = Image::LANDSCAPE;
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
$orientation = Image::SQUARE;
|
||
|
}
|
||
|
|
||
|
$this->image['orientation'] = $orientation;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handles retrieval of pre-save image properties
|
||
|
*
|
||
|
* @param string property name
|
||
|
* @return mixed
|
||
|
*/
|
||
|
public function __get($property)
|
||
|
{
|
||
|
if (isset($this->image[$property]))
|
||
|
{
|
||
|
return $this->image[$property];
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
throw new Kohana_Exception('The :property: property does not exist in the :class: class.',
|
||
|
array(':property:' => $property, ':class:' => get_class($this)));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Resize an image to a specific width and height. By default, Kohana will
|
||
|
* maintain the aspect ratio using the width as the master dimension. If you
|
||
|
* wish to use height as master dim, set $image->master_dim = Image::HEIGHT
|
||
|
* This method is chainable.
|
||
|
*
|
||
|
* @throws Kohana_Exception
|
||
|
* @param integer width
|
||
|
* @param integer height
|
||
|
* @param integer one of: Image::NONE, Image::AUTO, Image::WIDTH, Image::HEIGHT
|
||
|
* @return object
|
||
|
*/
|
||
|
public function resize($width, $height, $master = NULL)
|
||
|
{
|
||
|
if ( ! $this->valid_size('width', $width))
|
||
|
throw new Kohana_Exception('The width you specified, :width:, is not valid.', array(':width:' => $width));
|
||
|
|
||
|
if ( ! $this->valid_size('height', $height))
|
||
|
throw new Kohana_Exception('The height you specified, :height:, is not valid.', array(':height:' => $height));
|
||
|
|
||
|
if (empty($width) AND empty($height))
|
||
|
throw new Kohana_Exception('The dimensions specified for :function: are not valid.', array(':function:' => __FUNCTION__));
|
||
|
|
||
|
if ($master === NULL)
|
||
|
{
|
||
|
// Maintain the aspect ratio by default
|
||
|
$master = Image::AUTO;
|
||
|
}
|
||
|
elseif ( ! $this->valid_size('master', $master))
|
||
|
throw new Kohana_Exception('The master dimension specified is not valid.');
|
||
|
|
||
|
$this->actions['resize'] = array
|
||
|
(
|
||
|
'width' => $width,
|
||
|
'height' => $height,
|
||
|
'master' => $master,
|
||
|
);
|
||
|
|
||
|
$this->determine_orientation();
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Crop an image to a specific width and height. You may also set the top
|
||
|
* and left offset.
|
||
|
* This method is chainable.
|
||
|
*
|
||
|
* @throws Kohana_Exception
|
||
|
* @param integer width
|
||
|
* @param integer height
|
||
|
* @param integer top offset, pixel value or one of: top, center, bottom
|
||
|
* @param integer left offset, pixel value or one of: left, center, right
|
||
|
* @return object
|
||
|
*/
|
||
|
public function crop($width, $height, $top = 'center', $left = 'center')
|
||
|
{
|
||
|
if ( ! $this->valid_size('width', $width))
|
||
|
throw new Kohana_Exception('The width you specified, :width:, is not valid.', array(':width:' => $width));
|
||
|
|
||
|
if ( ! $this->valid_size('height', $height))
|
||
|
throw new Kohana_Exception('The height you specified, :height:, is not valid.', array(':height:' => $height));
|
||
|
|
||
|
if ( ! $this->valid_size('top', $top))
|
||
|
throw new Kohana_Exception('The top offset you specified, :top:, is not valid.', array(':top:' => $top));
|
||
|
|
||
|
if ( ! $this->valid_size('left', $left))
|
||
|
throw new Kohana_Exception('The left offset you specified, :left:, is not valid.', array(':left:' => $left));
|
||
|
|
||
|
if (empty($width) AND empty($height))
|
||
|
throw new Kohana_Exception('The dimensions specified for :function: are not valid.', array(':function:' => __FUNCTION__));
|
||
|
|
||
|
$this->actions['crop'] = array
|
||
|
(
|
||
|
'width' => $width,
|
||
|
'height' => $height,
|
||
|
'top' => $top,
|
||
|
'left' => $left,
|
||
|
);
|
||
|
|
||
|
$this->determine_orientation();
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Allows rotation of an image by 180 degrees clockwise or counter clockwise.
|
||
|
*
|
||
|
* @param integer degrees
|
||
|
* @return object
|
||
|
*/
|
||
|
public function rotate($degrees)
|
||
|
{
|
||
|
$degrees = (int) $degrees;
|
||
|
|
||
|
if ($degrees > 180)
|
||
|
{
|
||
|
do
|
||
|
{
|
||
|
// Keep subtracting full circles until the degrees have normalized
|
||
|
$degrees -= 360;
|
||
|
}
|
||
|
while($degrees > 180);
|
||
|
}
|
||
|
|
||
|
if ($degrees < -180)
|
||
|
{
|
||
|
do
|
||
|
{
|
||
|
// Keep adding full circles until the degrees have normalized
|
||
|
$degrees += 360;
|
||
|
}
|
||
|
while($degrees < -180);
|
||
|
}
|
||
|
|
||
|
$this->actions['rotate'] = $degrees;
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Overlay a second image on top of this one.
|
||
|
*
|
||
|
* @throws Kohana_Exception
|
||
|
* @param string $overlay_file path to an image file
|
||
|
* @param integer $x x offset for the overlay
|
||
|
* @param integer $y y offset for the overlay
|
||
|
* @param integer $transparency transparency percent
|
||
|
*/
|
||
|
public function composite($overlay_file, $x, $y, $transparency)
|
||
|
{
|
||
|
$image_info = getimagesize($overlay_file);
|
||
|
|
||
|
// Check to make sure the image type is allowed
|
||
|
if ( ! isset(Image::$allowed_types[$image_info[2]]))
|
||
|
throw new Kohana_Exception('The specified image, :type:, is not an allowed image type.', array(':type:' => $overlay_file));
|
||
|
|
||
|
$this->actions['composite'] = array
|
||
|
(
|
||
|
'overlay_file' => $overlay_file,
|
||
|
'mime' => $image_info['mime'],
|
||
|
'x' => $x,
|
||
|
'y' => $y,
|
||
|
'transparency' => $transparency
|
||
|
);
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Flip an image horizontally or vertically.
|
||
|
*
|
||
|
* @throws Kohana_Exception
|
||
|
* @param integer direction
|
||
|
* @return object
|
||
|
*/
|
||
|
public function flip($direction)
|
||
|
{
|
||
|
if ($direction !== Image::HORIZONTAL AND $direction !== Image::VERTICAL)
|
||
|
throw new Kohana_Exception('The flip direction specified is not valid.');
|
||
|
|
||
|
$this->actions['flip'] = $direction;
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Change the quality of an image.
|
||
|
*
|
||
|
* @param integer quality as a percentage
|
||
|
* @return object
|
||
|
*/
|
||
|
public function quality($amount)
|
||
|
{
|
||
|
$this->actions['quality'] = max(1, min($amount, 100));
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sharpen an image.
|
||
|
*
|
||
|
* @param integer amount to sharpen, usually ~20 is ideal
|
||
|
* @return object
|
||
|
*/
|
||
|
public function sharpen($amount)
|
||
|
{
|
||
|
$this->actions['sharpen'] = max(1, min($amount, 100));
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Save the image to a new image or overwrite this image.
|
||
|
*
|
||
|
* @throws Kohana_Exception
|
||
|
* @param string new image filename
|
||
|
* @param integer permissions for new image
|
||
|
* @param boolean keep or discard image process actions
|
||
|
* @return object
|
||
|
*/
|
||
|
public function save($new_image = FALSE, $chmod = 0644, $keep_actions = FALSE, $background = NULL)
|
||
|
{
|
||
|
// If no new image is defined, use the current image
|
||
|
empty($new_image) and $new_image = $this->image['file'];
|
||
|
|
||
|
// Separate the directory and filename
|
||
|
$dir = pathinfo($new_image, PATHINFO_DIRNAME);
|
||
|
$file = pathinfo($new_image, PATHINFO_BASENAME);
|
||
|
|
||
|
// Normalize the path
|
||
|
$dir = str_replace('\\', '/', realpath($dir)).'/';
|
||
|
|
||
|
if ( ! is_writable($dir))
|
||
|
throw new Kohana_Exception('The specified directory, :dir:, is not writable.', array(':dir:' => $dir));
|
||
|
|
||
|
if ($status = $this->driver->process($this->image, $this->actions, $dir, $file, FALSE, $background))
|
||
|
{
|
||
|
if ($chmod !== FALSE)
|
||
|
{
|
||
|
// Set permissions
|
||
|
chmod($new_image, $chmod);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($keep_actions !== TRUE)
|
||
|
{
|
||
|
// Reset actions. Subsequent save() or render() will not apply previous actions.
|
||
|
$this->actions = array();
|
||
|
}
|
||
|
|
||
|
return $status;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Output the image to the browser.
|
||
|
*
|
||
|
* @param boolean keep or discard image process actions
|
||
|
* @return object
|
||
|
*/
|
||
|
public function render($keep_actions = FALSE, $background = NULL)
|
||
|
{
|
||
|
$new_image = $this->image['file'];
|
||
|
|
||
|
// Separate the directory and filename
|
||
|
$dir = pathinfo($new_image, PATHINFO_DIRNAME);
|
||
|
$file = pathinfo($new_image, PATHINFO_BASENAME);
|
||
|
|
||
|
// Normalize the path
|
||
|
$dir = str_replace('\\', '/', realpath($dir)).'/';
|
||
|
|
||
|
// Process the image with the driver
|
||
|
$status = $this->driver->process($this->image, $this->actions, $dir, $file, TRUE, $background);
|
||
|
|
||
|
if ($keep_actions !== TRUE)
|
||
|
{
|
||
|
// Reset actions. Subsequent save() or render() will not apply previous actions.
|
||
|
$this->actions = array();
|
||
|
}
|
||
|
|
||
|
return $status;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sanitize a given value type.
|
||
|
*
|
||
|
* @param string type of property
|
||
|
* @param mixed property value
|
||
|
* @return boolean
|
||
|
*/
|
||
|
protected function valid_size($type, & $value)
|
||
|
{
|
||
|
if (is_null($value))
|
||
|
return TRUE;
|
||
|
|
||
|
if ( ! is_scalar($value))
|
||
|
return FALSE;
|
||
|
|
||
|
switch ($type)
|
||
|
{
|
||
|
case 'width':
|
||
|
case 'height':
|
||
|
if (is_string($value) AND ! ctype_digit($value))
|
||
|
{
|
||
|
// Only numbers and percent signs
|
||
|
if ( ! preg_match('/^[0-9]++%$/D', $value))
|
||
|
return FALSE;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$value = (int) $value;
|
||
|
}
|
||
|
break;
|
||
|
case 'top':
|
||
|
if (is_string($value) AND ! ctype_digit($value))
|
||
|
{
|
||
|
if ( ! in_array($value, array('top', 'bottom', 'center')))
|
||
|
return FALSE;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$value = (int) $value;
|
||
|
}
|
||
|
break;
|
||
|
case 'left':
|
||
|
if (is_string($value) AND ! ctype_digit($value))
|
||
|
{
|
||
|
if ( ! in_array($value, array('left', 'right', 'center')))
|
||
|
return FALSE;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$value = (int) $value;
|
||
|
}
|
||
|
break;
|
||
|
case 'master':
|
||
|
if ($value !== Image::NONE AND
|
||
|
$value !== Image::AUTO AND
|
||
|
$value !== Image::WIDTH AND
|
||
|
$value !== Image::HEIGHT)
|
||
|
return FALSE;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
} // End Image
|