1
0
mirror of https://github.com/mosbth/cimage.git synced 2025-01-17 19:18:15 +01:00
php-cimage/CImage.php

1444 lines
45 KiB
PHP
Raw Normal View History

2012-04-25 15:49:09 +02:00
<?php
/**
2014-02-12 14:45:25 +01:00
* Resize and crop images on the fly, store generated images in a cache.
2012-04-25 15:49:09 +02:00
*
* @author Mikael Roos mos@dbwebb.se
2014-02-12 14:45:25 +01:00
* @example http://dbwebb.se/opensource/cimage
2012-04-25 15:53:46 +02:00
* @link https://github.com/mosbth/cimage
2012-04-25 15:49:09 +02:00
*/
2014-02-12 14:45:25 +01:00
class CImage
{
/**
* Constants type of PNG image
*/
const PNG_GREYSCALE = 0;
const PNG_RGB = 2;
const PNG_RGB_PALETTE = 3;
const PNG_GREYSCALE_ALPHA = 4;
const PNG_RGB_ALPHA = 6;
/**
* Constant for default image quality when not set
*/
const JPEG_QUALITY_DEFAULT = 60;
/**
* Quality level for JPEG images.
*/
private $quality;
/**
* Constant for default image quality when not set
*/
const PNG_COMPRESSION_DEFAULT = -1;
/**
* Compression level for PNG images.
*/
private $compress;
/**
* Where to save the target file.
*/
private $saveFolder;
/**
* The working image object.
*/
private $image;
/**
* The root folder of images (only used in constructor to create $pathToImage?).
*/
private $imageFolder;
/**
* Image filename, may include subdirectory, relative from $imageFolder
*/
private $imageSrc;
/**
* Actual path to the image, $imageFolder . '/' . $imageSrc
*/
private $pathToImage;
/**
* Original file extension
*/
private $fileExtension;
/**
* File extension to use when saving image
*/
private $extension;
/**
* Verbose mode to print out a trace and display the created image
*/
private $verbose = false;
/**
* Keep a log/trace on what happens
*/
private $log = array();
/**
* Handle image as palette image
*/
private $palette;
/**
* Target filename, with path, to save resulting image in.
*/
private $cacheFileName;
/**
* Set a format to save image as, or null to use original format.
*/
private $saveAs;
/**
* Path to command for filter optimize, for example optipng or null.
*/
private $pngFilter;
/**
* Path to command for deflate optimize, for example pngout or null.
*/
private $pngDeflate;
/**
* Path to command to optimize jpeg images, for example jpegtran or null.
*/
private $jpegOptimize;
private $width; // Calculated from source image
private $height; // Calculated from source image
private $offset;
public $keepRatio;
public $cropToFit;
public $crop;
2012-04-25 15:49:09 +02:00
2012-04-25 15:49:09 +02:00
/**
* Properties
*/
public $newWidth;
public $newHeight;
private $cropWidth;
private $cropHeight;
public $crop_x;
public $crop_y;
public $filters;
2012-04-25 15:49:09 +02:00
private $type; // Calculated from source image
private $attr; // Calculated from source image
private $useCache; // Use the cache if true, set to false to ignore the cached file.
private $useOriginal; // Use original image if possible
2012-04-25 15:49:09 +02:00
2014-02-12 14:45:25 +01:00
/**
* Constructor, can take arguments to init the object.
*
* @param string $imageSrc filename which may contain subdirectory.
* @param string $imageFolder path to root folder for images.
* @param string $saveFolder path to folder where to save the new file or null to skip saving.
* @param string $saveName name of target file when saveing.
*/
public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null)
{
$this->setSource($imageSrc, $imageFolder);
$this->setTarget($saveFolder, $saveName);
}
2012-04-25 15:49:09 +02:00
2014-02-12 14:45:25 +01:00
/**
* Set verbose mode.
*
* @param boolean $mode true or false to enable and disable versbose mode, default is true.
*
* @return $this
*/
public function setVerbose($mode = true)
{
$this->verbose = $mode;
return $this;
}
2014-02-12 14:45:25 +01:00
/**
* Check if file extension is valid as a file extension.
*
* @param string $extension of image file.
*
* @return $this
*/
private function checkFileExtension($extension)
{
$valid = array('jpg', 'jpeg', 'png', 'gif');
in_array($extension, $valid)
or $this->raiseError('Not a valid file extension.');
return $this;
}
2014-02-12 14:45:25 +01:00
/**
* Set src file.
*
* @param string $src of image.
* @param string $dir as base directory where images are.
*
* @return $this
*/
public function setSource($src = null, $dir = null)
{
if (!(isset($src) && isset($dir))) {
return $this;
}
2014-02-12 14:45:25 +01:00
$this->imageSrc = ltrim($src, '/');
$this->imageFolder = rtrim($dir, '/');
$this->pathToImage = $this->imageFolder . '/' . $this->imageSrc;
$this->fileExtension = pathinfo($this->pathToImage, PATHINFO_EXTENSION);
$this->extension = $this->fileExtension;
$this->checkFileExtension($this->fileExtension);
2014-02-12 14:45:25 +01:00
return $this;
}
2012-04-25 15:49:09 +02:00
2014-02-12 14:45:25 +01:00
/**
* Set target file.
*
* @param string $src of target image.
* @param string $dir as base directory where images are stored.
*
* @return $this
*/
public function setTarget($src = null, $dir = null)
{
if (!(isset($src) && isset($dir))) {
return $this;
}
2014-02-12 14:45:25 +01:00
$this->saveFolder = $dir;
$this->cacheFileName = $dir . '/' . $src;
2014-02-12 14:45:25 +01:00
is_writable($this->saveFolder)
or $this->raiseError('Target directory is not writable.');
// Sanitize filename
$this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName);
$this->log("The cache file name is: " . $this->cacheFileName);
return $this;
}
2012-04-25 15:49:09 +02:00
2014-02-12 14:45:25 +01:00
/**
* Set options to use when processing image.
*
* @param array $args used when processing image.
*
* @return $this
*/
public function setOptions($args)
{
$this->log("Set new options for processing image.");
$defaults = array(
// Options for calculate dimensions
'newWidth' => null,
'newHeight' => null,
'aspectRatio' => null,
'keepRatio' => true,
'cropToFit' => false,
'crop' => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0),
'area' => null, //'0,0,0,0',
// Options for caching or using original
'useCache' => true,
'useOriginal' => true,
// Pre-processing, before resizing is done
'scale' => null,
// Post-processing, after resizing is done
'palette' => null,
'filters' => null,
'sharpen' => null,
'emboss' => null,
'blur' => null,
// Options for saving
//'quality' => null,
//'compress' => null,
//'saveAs' => null,
);
// Convert crop settins from string to array
if (isset($args['crop']) && !is_array($args['crop'])) {
$pices = explode(',', $args['crop']);
$args['crop'] = array(
'width' => $pices[0],
'height' => $pices[1],
'start_x' => $pices[2],
'start_y' => $pices[3],
);
}
2014-02-12 14:45:25 +01:00
// Convert area settins from string to array
if (isset($args['area']) && !is_array($args['area'])) {
$pices = explode(',', $args['area']);
$args['area'] = array(
'top' => $pices[0],
'right' => $pices[1],
'bottom' => $pices[2],
'left' => $pices[3],
);
}
2013-10-07 23:50:53 +02:00
2014-02-12 14:45:25 +01:00
// Convert filter settins from array of string to array of array
if (isset($args['filters']) && is_array($args['filters'])) {
foreach ($args['filters'] as $key => $filterStr) {
$parts = explode(',', $filterStr);
$filter = $this->mapFilter($parts[0]);
$filter['str'] = $filterStr;
for ($i=1;$i<=$filter['argc'];$i++) {
if (isset($parts[$i])) {
$filter["arg{$i}"] = $parts[$i];
} else {
throw new Exception('Missing arg to filter, review how many arguments are needed at http://php.net/manual/en/function.imagefilter.php');
}
}
$args['filters'][$key] = $filter;
}
}
2013-10-07 23:50:53 +02:00
2014-02-12 14:45:25 +01:00
// Merge default arguments with incoming and set properties.
//$args = array_merge_recursive($defaults, $args);
$args = array_merge($defaults, $args);
foreach ($defaults as $key=>$val) {
$this->{$key} = $args[$key];
2013-10-07 23:50:53 +02:00
}
2014-02-12 14:45:25 +01:00
return $this;
}
/**
* Map filter name to PHP filter and id.
*
* @param string $name the name of the filter.
*
* @return array with filter settings
* @throws Exception
*/
private function mapFilter($name)
{
$map = array(
'negate' => array('id'=>0, 'argc'=>0, 'type'=>IMG_FILTER_NEGATE),
'grayscale' => array('id'=>1, 'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE),
'brightness' => array('id'=>2, 'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS),
'contrast' => array('id'=>3, 'argc'=>1, 'type'=>IMG_FILTER_CONTRAST),
'colorize' => array('id'=>4, 'argc'=>4, 'type'=>IMG_FILTER_COLORIZE),
'edgedetect' => array('id'=>5, 'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT),
'emboss' => array('id'=>6, 'argc'=>0, 'type'=>IMG_FILTER_EMBOSS),
'gaussian_blur' => array('id'=>7, 'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR),
'selective_blur' => array('id'=>8, 'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR),
'mean_removal' => array('id'=>9, 'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL),
'smooth' => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH),
'pixelate' => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE),
);
if (isset($map[$name]))
return $map[$name];
2013-10-07 23:50:53 +02:00
else {
2014-02-12 14:45:25 +01:00
throw new Exception('No such filter.');
2013-10-07 23:50:53 +02:00
}
}
2014-02-12 14:45:25 +01:00
2012-04-25 15:49:09 +02:00
2014-02-12 14:45:25 +01:00
/**
* Init new width and height and do some sanity checks on constraints, before any
* processing can be done.
*
* @return $this
* @throws Exception
*/
public function initDimensions()
{
is_readable($this->pathToImage)
or $this->raiseError('Image file does not exist.');
// Get details on image
$info = list($this->width, $this->height, $this->type, $this->attr) = getimagesize($this->pathToImage);
!empty($info) or $this->raiseError("The file doesn't seem to be an image.");
if ($this->verbose) {
$this->log("Image file: {$this->pathToImage}");
$this->log("Image width x height (type): {$this->width} x {$this->height} ({$this->type}).");
$this->log("Image filesize: " . filesize($this->pathToImage) . " bytes.");
}
// width as %
if ($this->newWidth[strlen($this->newWidth)-1] == '%') {
$this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100;
$this->log("Setting new width based on % to {$this->newWidth}");
}
2014-02-12 14:45:25 +01:00
// height as %
if ($this->newHeight[strlen($this->newHeight)-1] == '%') {
$this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100;
$this->log("Setting new height based on % to {$this->newHeight}");
}
2014-02-12 14:45:25 +01:00
is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range');
2013-10-14 09:22:21 +02:00
2014-02-12 14:45:25 +01:00
// width & height from aspect ratio
if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) {
if ($this->aspectRatio >= 1) {
$this->newWidth = $this->width;
$this->newHeight = $this->width / $this->aspectRatio;
$this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}");
} else {
$this->newHeight = $this->height;
$this->newWidth = $this->height * $this->aspectRatio;
$this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}");
}
} elseif ($this->aspectRatio && is_null($this->newWidth)) {
$this->newWidth = $this->newHeight * $this->aspectRatio;
$this->log("Setting new width based on aspect ratio to {$this->newWidth}");
} elseif ($this->aspectRatio && is_null($this->newHeight)) {
$this->newHeight = $this->newWidth / $this->aspectRatio;
$this->log("Setting new height based on aspect ratio to {$this->newHeight}");
}
// Check values to be within domain
is_null($this->newWidth)
or is_numeric($this->newWidth)
or $this->raiseError('Width not numeric');
is_null($this->newHeight)
or is_numeric($this->newHeight)
or $this->raiseError('Height not numeric');
2014-02-12 14:45:25 +01:00
return $this;
2012-04-25 15:49:09 +02:00
}
2014-02-12 14:45:25 +01:00
/**
* Calculate new width and height of image, based on settings.
*
* @return $this
*/
public function calculateNewWidthAndHeight()
{
// Crop, use cropped width and height as base for calulations
$this->log("Calculate new width and height.");
$this->log("Original width x height is {$this->width} x {$this->height}.");
if (isset($this->area)) {
$this->offset['top'] = round($this->area['top'] / 100 * $this->height);
$this->offset['right'] = round($this->area['right'] / 100 * $this->width);
$this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height);
$this->offset['left'] = round($this->area['left'] / 100 * $this->width);
$this->offset['width'] = $this->width - $this->offset['left'] - $this->offset['right'];
$this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom'];
$this->width = $this->offset['width'];
$this->height = $this->offset['height'];
$this->log("The offset for the area to use is top {$this->area['top']}%, right {$this->area['right']}%, bottom {$this->area['bottom']}%, left {$this->area['left']}%.");
$this->log("The offset for the area to use is top {$this->offset['top']}px, right {$this->offset['right']}px, bottom {$this->offset['bottom']}px, left {$this->offset['left']}px, width {$this->offset['width']}px, height {$this->offset['height']}px.");
}
$width = $this->width;
$height = $this->height;
if ($this->crop) {
$width = $this->crop['width'] = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width'];
$height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height'];
if ($this->crop['start_x'] == 'left') {
$this->crop['start_x'] = 0;
} elseif ($this->crop['start_x'] == 'right') {
$this->crop['start_x'] = $this->width - $width;
} elseif ($this->crop['start_x'] == 'center') {
$this->crop['start_x'] = round($this->width / 2) - round($width / 2);
}
if ($this->crop['start_y'] == 'top') {
$this->crop['start_y'] = 0;
} elseif ($this->crop['start_y'] == 'bottom') {
$this->crop['start_y'] = $this->height - $height;
} elseif ($this->crop['start_y'] == 'center') {
$this->crop['start_y'] = round($this->height / 2) - round($height / 2);
}
$this->log("Crop area is width {$width}px, height {$height}px, start_x {$this->crop['start_x']}px, start_y {$this->crop['start_y']}px.");
}
2014-02-12 14:45:25 +01:00
// Calculate new width and height if keeping aspect-ratio.
if ($this->keepRatio) {
// Crop-to-fit and both new width and height are set.
if ($this->cropToFit && isset($this->newWidth) && isset($this->newHeight)) {
// Use newWidth and newHeigh as width/height, image should fit in box.
;
} elseif (isset($this->newWidth) && isset($this->newHeight)) {
// Both new width and height are set.
// Use newWidth and newHeigh as max width/height, image should not be larger.
$ratioWidth = $width / $this->newWidth;
$ratioHeight = $height / $this->newHeight;
$ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight;
$this->newWidth = round($width / $ratio);
$this->newHeight = round($height / $ratio);
} elseif (isset($this->newWidth)) {
// Use new width as max-width
$factor = (float)$this->newWidth / (float)$width;
$this->newHeight = round($factor * $height);
} elseif (isset($this->newHeight)) {
// Use new height as max-hight
$factor = (float)$this->newHeight / (float)$height;
$this->newWidth = round($factor * $width);
}
// Use newWidth and newHeigh as defined width/height, image should fit the area.
if ($this->cropToFit) {
$ratioWidth = $width / $this->newWidth;
$ratioHeight = $height / $this->newHeight;
$ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight;
$this->cropWidth = round($width / $ratio);
$this->cropHeight = round($height / $ratio);
}
}
// Crop, ensure to set new width and height
if ($this->crop) {
$this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
$this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
}
// No new height or width is set, use existing measures.
$this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->width);
$this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height);
$this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}.");
return $this;
}
2014-02-12 14:45:25 +01:00
/**
* Set extension for filename to save as.
*
* @param string $saveas extension to save image as
*
* @return $this
*/
public function setSaveAsExtension($saveAs = null)
{
if (isset($saveAs)) {
$this->checkFileExtension($saveAs);
$this->saveAs = $saveAs;
$this->extension = $saveAs;
}
$this->log("Prepare to save image using as: " . $this->extension);
return $this;
}
2014-02-12 14:45:25 +01:00
/**
* Set JPEG quality to use when saving image
*
* @param int $quality as the quality to set.
*
* @return $this
*/
public function setJpegQuality($quality = null)
{
$this->quality = isset($quality)
? $quality
: self::JPEG_QUALITY_DEFAULT;
(is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100)
or $this->raiseError('Quality not in range.');
$this->log("Setting JPEG quality to {$this->quality}.");
return $this;
}
2014-02-12 14:45:25 +01:00
/**
* Set PNG compressen algorithm to use when saving image
*
* @param int $compress as the algorithm to use.
*
* @return $this
*/
public function setPngCompression($compress = null)
{
$this->compress = isset($compress)
? $compress
: self::PNG_COMPRESSION_DEFAULT;
(is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9)
or $this->raiseError('Quality not in range.');
$this->log("Setting PNG compression level to {$this->compress}.");
return $this;
}
2014-02-12 14:45:25 +01:00
/**
* Use original image if possible, check options wchich affects image processing.
*
* @param boolean $useOrig default is to use original if possible, else set to false.
*
* @return $this
*/
public function useOriginalIfPossible($useOrig = true)
{
if ($useOrig
&& ($this->newWidth == $this->width)
&& ($this->newHeight == $this->height)
&& !$this->area
&& !$this->crop
&& !$this->filters
&& !$this->sharpen
&& !$this->emboss
&& !$this->blur
&& !$this->palette
&& !$this->quality
&& !$this->compress
&& !$this->saveAs
) {
$this->log("Using original image.");
$this->output($this->pathToImage);
}
2014-02-12 14:45:25 +01:00
return $this;
}
2014-02-12 14:45:25 +01:00
2014-02-12 14:45:25 +01:00
/**
* Generate filename to save file in cache.
*
* @param string $base as basepath for storing file.
*
* @return $this
*/
public function generateFilename($base)
{
$parts = pathinfo($this->pathToImage);
$cropToFit = $this->cropToFit ? '_cf' : null;
$crop_x = $this->crop_x ? "_x{$this->crop_x}" : null;
$crop_y = $this->crop_y ? "_y{$this->crop_y}" : null;
$scale = $this->scale ? "_s{$this->scale}" : null;
$quality = $this->quality ? "_q{$this->quality}" : null;
$compress = $this->compress ? "_co{$this->compress}" : null;
$offset = isset($this->offset)
? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left']
: null;
$crop = $this->crop
? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y']
: null;
$filters = null;
if (isset($this->filters)) {
foreach ($this->filters as $filter) {
if (is_array($filter)) {
$filters .= "_f{$filter['id']}";
for ($i=1;$i<=$filter['argc'];$i++) {
$filters .= ":".$filter["arg{$i}"];
}
}
}
}
2014-02-12 14:45:25 +01:00
$sharpen = $this->sharpen ? 's' : null;
$emboss = $this->emboss ? 'e' : null;
$blur = $this->blur ? 'b' : null;
$palette = $this->palette ? 'p' : null;
$this->extension = isset($this->extension)
? $this->extension
: $parts['extension'];
// Check optimizing options
$optimize = null;
if ($this->extension == 'jpeg' || $this->extension == 'jpg') {
$optimize = $this->jpegOptimize ? 'o' : null;
} elseif ($this->extension == 'png') {
$optimize .= $this->pngFilter ? 'f' : null;
$optimize .= $this->pngDeflate ? 'd' : null;
}
2014-02-12 14:45:25 +01:00
$subdir = str_replace('/', '-', dirname($this->imageSrc));
$subdir = ($subdir == '.') ? '_.' : $subdir;
$file = $subdir . '_' . $parts['filename'] . '_' . round($this->newWidth) . '_'
. round($this->newHeight) . $offset . $crop . $cropToFit . $crop_x . $crop_y
. $quality . $filters . $sharpen . $emboss . $blur . $palette . $optimize
. $scale . '.' . $this->extension;
2014-02-12 14:45:25 +01:00
return $this->setTarget($file, $base);
}
2014-02-12 14:45:25 +01:00
/**
* Use cached version of image, if possible.
*
* @param boolean $useCache is default true, set to false to avoid using cached object.
*
* @return $this
*/
public function useCacheIfPossible($useCache = true)
{
if ($useCache && is_readable($this->cacheFileName)) {
$fileTime = filemtime($this->pathToImage);
$cacheTime = filemtime($this->cacheFileName);
if ($fileTime <= $cacheTime) {
if ($this->useCache) {
if ($this->verbose) {
$this->log("Use cached file.");
$this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
}
$this->output($this->cacheFileName);
} else {
$this->log("Cache is valid but ignoring it by intention.");
}
} else {
$this->log("Original file is modified, ignoring cache.");
}
} else {
$this->log("Cachefile does not exists or ignoring it.");
}
2014-02-12 14:45:25 +01:00
return $this;
2012-10-02 22:49:43 +02:00
}
2014-02-12 14:45:25 +01:00
2012-04-25 15:49:09 +02:00
2014-02-12 14:45:25 +01:00
/**
* Load image from disk.
*
* @param string $src of image.
* @param string $dir as base directory where images are.
*
* @return $this
*
*/
public function load($src = null, $dir = null)
{
if (isset($src)) {
$this->setSource($src, $dir);
}
$this->log("Opening file as {$this->fileExtension}.");
switch ($this->fileExtension) {
case 'jpg':
case 'jpeg':
$this->image = @imagecreatefromjpeg($this->pathToImage);
break;
2012-10-02 22:49:43 +02:00
2014-02-12 14:45:25 +01:00
case 'gif':
$this->image = @imagecreatefromgif($this->pathToImage);
break;
case 'png':
$this->image = @imagecreatefrompng($this->pathToImage);
$type = $this->getPngType();
$hasFewColors = imagecolorstotal($this->image);
if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) {
if ($this->verbose) {
$this->log("Handle this image as a palette image.");
}
$this->palette = true;
}
break;
default:
$this->image = false;
throw new Exception('No support for this file extension.');
}
if ($this->verbose) {
$this->log("imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
$this->log("imagecolorstotal() : " . imagecolorstotal($this->image));
$this->log("Number of colors in image = " . $this->colorsTotal($this->image));
}
return $this;
2012-10-02 22:49:43 +02:00
}
2014-02-12 14:45:25 +01:00
/**
* Get the type of PNG image.
*
* @return int as the type of the png-image
*
*/
private function getPngType()
{
$pngType = ord (file_get_contents ($this->pathToImage, false, null, 25, 1));
switch ($pngType) {
case self::PNG_GREYSCALE:
$this->log("PNG is type 0, Greyscale.");
break;
case self::PNG_RGB:
$this->log("PNG is type 2, RGB");
break;
case self::PNG_RGB_PALETTE:
$this->log("PNG is type 3, RGB with palette");
break;
case self::PNG_GREYSCALE_ALPHA:
$this->Log("PNG is type 4, Greyscale with alpha channel");
break;
case self::PNG_RGB_ALPHA:
$this->Log("PNG is type 6, RGB with alpha channel (PNG 32-bit)");
break;
default:
$this->Log("PNG is UNKNOWN type, is it really a PNG image?");
}
return $pngType;
}
2014-02-12 14:45:25 +01:00
/**
* Calculate number of colors in an image.
*
* @param resource $im the image.
*
* @return int
*/
private function colorsTotal($im)
{
if (imageistruecolor($im)) {
$h = imagesy($im);
$w = imagesx($im);
$c = array();
for ($x=0; $x < $w; $x++) {
for ($y=0; $y < $h; $y++) {
@$c['c'.imagecolorat($im, $x, $y)]++;
}
}
return count($c);
} else {
return imagecolorstotal($im);
}
}
2014-02-12 14:45:25 +01:00
/**
* Preprocess image before rezising it.
*
* @return $this
*/
public function preResize()
{
$this->log("Pre-process before resizing");
// Scale the original image before starting
if (isset($this->scale)) {
$this->log("Scale by {$this->scale}%");
$newWidth = $this->width * $this->scale / 100;
$newHeight = $this->height * $this->scale / 100;
$img = $this->CreateImageKeepTransparency($newWidth, $newHeight);
imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
$this->image = $img;
$this->width = $newWidth;
$this->height = $newHeight;
}
return $this;
}
2014-02-12 14:45:25 +01:00
/**
* Resize and or crop the image.
*
* @return $this
*/
public function resize()
{
$this->log("Starting to Resize()");
// Only use a specified area of the image, $this->offset is defining the area to use
if (isset($this->offset)) {
$this->log("Offset for area to use, cropping it width={$this->offset['width']}, height={$this->offset['height']}, start_x={$this->offset['left']}, start_y={$this->offset['top']}");
$img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']);
imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']);
$this->image = $img;
$this->width = $this->offset['width'];
$this->height = $this->offset['height'];
}
// SaveAs need to copy image to remove transparency, if any
if ($this->saveAs) {
$this->log("Copying image before saving as another format, loosing transparency, width={$this->width}, height={$this->height}.");
$img = imagecreatetruecolor($this->width, $this->height);
$bg = imagecolorallocate($img, 255, 255, 255);
imagefill($img, 0, 0, $bg);
imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height);
$this->image = $img;
}
2014-02-12 14:45:25 +01:00
// Do as crop, take only part of image
if ($this->crop) {
2014-02-12 14:45:25 +01:00
$this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}");
$img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']);
imagecopyresampled($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height'], $this->crop['width'], $this->crop['height']);
$this->image = $img;
$this->width = $this->crop['width'];
$this->height = $this->crop['height'];
}
2014-02-12 14:45:25 +01:00
// Resize by crop to fit
if ($this->cropToFit) {
$this->log("Crop to fit");
$cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
$cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
$imgPreCrop = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight);
$imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
imagecopyresampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height);
imagecopyresampled($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight, $this->newWidth, $this->newHeight);
$this->image = $imageResized;
$this->width = $this->newWidth;
$this->height = $this->newHeight;
} else if (!($this->newWidth == $this->width && $this->newHeight == $this->height)) {
// Resize it
$this->log("Resizing, new height and/or width");
$imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
imagecopyresampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height);
//imagecopyresized($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height);
$this->image = $imageResized;
$this->width = $this->newWidth;
$this->height = $this->newHeight;
}
2014-02-12 14:45:25 +01:00
return $this;
}
2014-02-12 14:45:25 +01:00
/**
* Postprocess image after rezising image.
*
* @return $this
*/
public function postResize()
{
$this->log("Post-process after resizing");
// Apply filters
if (isset($this->filters) && is_array($this->filters)) {
foreach ($this->filters as $filter) {
$this->log("Applying filter $filter.");
switch ($filter['argc']) {
case 0:
imagefilter($this->image, $filter['type']);
break;
case 1:
imagefilter($this->image, $filter['type'], $filter['arg1']);
break;
case 2:
imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']);
break;
case 3:
imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']);
break;
case 4:
imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']);
break;
}
}
}
2014-02-12 14:45:25 +01:00
// Convert to palette image
if($this->palette) {
$this->log("Converting to palette image.");
$this->trueColorToPalette();
}
2014-02-12 14:45:25 +01:00
// Blur the image
if($this->blur) {
$this->log("Blur.");
$this->blurImage();
}
2014-02-12 14:45:25 +01:00
// Emboss the image
if($this->emboss) {
$this->log("Emboss.");
$this->embossImage();
}
2014-02-12 14:45:25 +01:00
// Sharpen the image
if($this->sharpen) {
$this->log("Sharpen.");
$this->sharpenImage();
}
2013-10-07 23:50:53 +02:00
2014-02-12 14:45:25 +01:00
return $this;
}
2012-04-25 15:49:09 +02:00
2014-02-12 14:45:25 +01:00
/**
* Convert true color image to palette image, keeping alpha.
* http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library
*
* @return void
*/
public function trueColorToPalette()
{
$img = imagecreatetruecolor($this->width, $this->height);
$bga = imagecolorallocatealpha($img, 0, 0, 0, 127);
imagecolortransparent($img, $bga);
imagefill($img, 0, 0, $bga);
imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height);
imagetruecolortopalette($img, false, 255);
imagesavealpha($img, true);
if (imageistruecolor($this->image)) {
$this->log("Matching colors with true color image.");
imagecolormatch($this->image, $img);
}
2014-02-12 14:45:25 +01:00
$this->image = $img;
}
2012-10-02 22:49:43 +02:00
2014-02-12 14:45:25 +01:00
/**
* Sharpen image as http://php.net/manual/en/ref.image.php#56144
* http://loriweb.pair.com/8udf-sharpen.html
*
* @return $this
*/
public function sharpenImage()
{
$matrix = array(
array(-1,-1,-1,),
array(-1,16,-1,),
array(-1,-1,-1,),
);
$divisor = 8;
$offset = 0;
imageconvolution($this->image, $matrix, $divisor, $offset);
return $this;
}
2012-10-02 22:49:43 +02:00
2013-10-07 23:50:53 +02:00
2014-02-12 14:45:25 +01:00
/**
* Emboss image as http://loriweb.pair.com/8udf-emboss.html
*
* @return $this
*/
public function embossImage()
{
$matrix = array(
array( 1, 1,-1,),
array( 1, 3,-1,),
array( 1,-1,-1,),
);
2013-10-07 23:50:53 +02:00
2014-02-12 14:45:25 +01:00
$divisor = 3;
$offset = 0;
2014-02-12 14:45:25 +01:00
imageconvolution($this->image, $matrix, $divisor, $offset);
return $this;
}
2012-10-02 22:49:43 +02:00
2014-02-12 14:45:25 +01:00
/**
* Blur image as http://loriweb.pair.com/8udf-basics.html
*
* @return $this
*/
public function blurImage()
{
$matrix = array(
array( 1, 1, 1,),
array( 1,15, 1,),
array( 1, 1, 1,),
);
2012-10-02 22:49:43 +02:00
2014-02-12 14:45:25 +01:00
$divisor = 23;
$offset = 0;
2012-10-02 22:49:43 +02:00
2014-02-12 14:45:25 +01:00
imageconvolution($this->image, $matrix, $divisor, $offset);
2014-02-12 14:45:25 +01:00
return $this;
}
2014-02-12 14:45:25 +01:00
/**
* Create a image and keep transparency for png and gifs.
*
* @param int $width of the new image.
* @param int $height of the new image.
* @return image resource.
*/
private function CreateImageKeepTransparency($width, $height)
{
$this->log("Creating a new working image width={$width}px, height={$height}px.");
$img = imagecreatetruecolor($width, $height);
imagealphablending($img, false);
imagesavealpha($img, true);
/*
$this->Log("Filling image with background color.");
$bg = imagecolorallocate($img, 255, 255, 255);
imagefill($img, 0, 0, $bg);
*/
return $img;
}
2014-02-12 14:45:25 +01:00
/**
* Set optimizing and post-processing options. CHANGE FROM DEFINE TO INJECT INTO CLASS, TO BE ABLE TO SET OFF POSTPROCESSING
*
* @param array $options with config for postprocessing with external tools.
*
* @return $this
*/
public function setPostProcessingOptions($options)
{
if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) {
$this->jpegOptimizeCmd = $options['jpeg_optimize_cmd'];
}
if (isset($options['png_filter']) && $options['png_filter']) {
$this->pngFilterCmd = $options['png_filter_cmd'];
}
if (isset($options['png_deflate']) && $options['png_deflate']) {
$this->pngDeflateCmd = $options['png_deflate_cmd'];
}
return $this;
}
2014-02-12 14:45:25 +01:00
/**
* Save image.
*
* @param string $src as target filename.
* @param string $base as base directory where to store images.
*
* @return $this or false if no folder is set.
*/
public function save($src = null, $base = null)
{
if (isset($src)) {
$this->setTarget($src, $base);
}
2014-02-12 14:45:25 +01:00
switch($this->extension) {
case 'jpeg':
case 'jpg':
$this->Log("Saving image as JPEG to cache using quality = {$this->quality}.");
imagejpeg($this->image, $this->cacheFileName, $this->quality);
2014-02-12 14:45:25 +01:00
// Use JPEG optimize if defined
2014-02-12 15:09:29 +01:00
if ($this->jpegOptimizeCmd) {
2014-02-12 14:45:25 +01:00
if ($this->verbose) {
clearstatcache();
$this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes.");
}
$res = array();
2014-02-12 15:09:29 +01:00
$cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName";
2014-02-12 14:45:25 +01:00
exec($cmd, $res);
$this->log($cmd);
$this->log($res);
}
break;
case 'gif':
if ($this->saveFolder) {
$this->Log("Saving image as GIF to cache.");
imagegif($this->image, $this->cacheFileName);
}
break;
case 'png':
$this->Log("Saving image as PNG to cache using compression = {$this->compress}.");
// Turn off alpha blending and set alpha flag
imagealphablending($this->image, false);
imagesavealpha($this->image, true);
imagepng($this->image, $this->cacheFileName, $this->compress);
// Use external program to filter PNG, if defined
2014-02-12 15:09:29 +01:00
if ($this->pngFilterCmd) {
2014-02-12 14:45:25 +01:00
if ($this->verbose) {
clearstatcache();
$this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes.");
}
$res = array();
2014-02-12 15:09:29 +01:00
$cmd = $this->pngFilterCmd . " $this->cacheFileName";
2014-02-12 14:45:25 +01:00
exec($cmd, $res);
$this->Log($cmd);
$this->Log($res);
}
// Use external program to deflate PNG, if defined
2014-02-12 15:09:29 +01:00
if ($this->pngDeflateCmd) {
2014-02-12 14:45:25 +01:00
if ($this->verbose) {
clearstatcache();
$this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes.");
}
$res = array();
2014-02-12 15:09:29 +01:00
$cmd = $this->pngDeflateCmd . " $this->cacheFileName";
2014-02-12 14:45:25 +01:00
exec($cmd, $res);
$this->Log($cmd);
$this->Log($res);
}
break;
default:
$this->RaiseError('No support for this file extension.');
break;
}
if ($this->verbose) {
clearstatcache();
$this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
$this->log("imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
$this->log("imagecolorstotal() : " . imagecolorstotal($this->image));
$this->log("Number of colors in image = " . $this->ColorsTotal($this->image));
}
2014-02-12 14:45:25 +01:00
return $this;
}
/**
* Output image to browser using caching.
*
* @param string $file to read and output, default is to use $this->cacheFileName
*
* @return void
*/
public function output($file = null)
{
if (is_null($file)) {
$file = $this->cacheFileName;
}
2014-02-12 14:45:25 +01:00
$this->log("Outputting image: $file");
2014-02-12 14:45:25 +01:00
// Get details on image
$info = list($width, $height, $type, $attr) = getimagesize($file);
!empty($info) or $this->raiseError("The file doesn't seem to be an image.");
$mime = $info['mime'];
$lastModified = filemtime($file);
$gmdate = gmdate("D, d M Y H:i:s", $lastModified);
if (!$this->verbose) {
header('Last-Modified: ' . $gmdate . " GMT");
}
2014-02-12 14:45:25 +01:00
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) {
if ($this->verbose) {
$this->log("304 not modified");
$this->verboseOutput();
exit;
}
header("HTTP/1.0 304 Not Modified");
} else {
if ($this->verbose) {
$this->log("Last modified: " . $gmdate . " GMT");
$this->verboseOutput();
exit;
}
header('Content-type: ' . $mime);
readfile($file);
}
exit;
}
2014-02-12 14:45:25 +01:00
/**
* Log an event if verbose mode.
*
* @param string $message to log.
*
* @return void
*/
private function log($message)
{
if ($this->verbose) {
$this->log[] = $message;
}
}
2012-04-25 15:49:09 +02:00
2014-02-12 14:45:25 +01:00
/**
* Do verbose output and print out the log and the actual images.
*
* @return void
*/
private function verboseOutput()
{
$log = null;
$this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
$this->log("Memory limit: " . ini_get('memory_limit'));
$included = get_included_files();
$this->log("Included files: " . count($included));
foreach ($this->log as $val) {
if (is_array($val)) {
foreach ($val as $val1) {
$log .= htmlentities($val1) . '<br/>';
}
} else {
$log .= htmlentities($val) . '<br/>';
}
}
echo <<<EOD
<!doctype html>
<html lang=en>
<meta charset=utf-8>
<title>CImage verbose output</title>
<style>body{background-color: #ddd}</style>
<h1>CImage Verbose Output</h1>
<pre>{$log}</pre>
EOD;
}
/**
* Raise error, enables to implement a selection of error methods.
*
* @param string $message the error message to display.
*
* @return void
* @throws Exception
*/
private function raiseError($message)
{
throw new Exception($message);
}
2012-04-25 15:49:09 +02:00
}