diff --git a/CImage.php b/CImage.php index 41b5028..3fa5a7e 100644 --- a/CImage.php +++ b/CImage.php @@ -8,6 +8,18 @@ */ class CImage { + /** + * Constants + */ + const PNG_GREYSCALE = 0; + const PNG_RGB = 2; + const PNG_RGB_PALETTE = 3; + const PNG_GREYSCALE_ALPHA = 4; + const PNG_RGB_ALPHA = 6; + const PNG_QUALITY_DEFAULT = -1; + const JPEG_QUALITY_DEFAULT = 60; + + /** * Properties */ @@ -29,13 +41,28 @@ class CImage { public $filters; public $saveFolder; public $newName; - private $newFileName; + //private $newFileName; // OBSOLETE, using cacheFileName instead. private $mime; // Calculated from source image private $width; // Calculated from source image private $height; // Calculated from source image private $type; // Calculated from source image private $attr; // Calculated from source image private $validExtensions = array('jpg', 'jpeg', 'png', 'gif'); + private $verbose; // Print out a trace together with original and created image + private $log; // Keep a log/trace on what happens. + private $cacheFileName; // Filename of the new image in the cache. + private $useCache; // Use the cache if true, set to false to ignore the cached file. + private $useOriginal; // Use original image if possible + private $saveAs; // Define a format to save image as, or null to use original format. + private $extension; // Extension to save image as. + + // Specific for PNG + private $pngType; // Find out which type of PNG image it is. + private $pngFilter; // Path to command for filter optimize, for example optipng or null. + private $pngDeflate; // Path to command for deflate optimize, for example pngout or null. + + // Specific for JPEG + private $jpegOptimize; // Path to command to optimize jpeg images, for example jpegtran or null. /** @@ -47,15 +74,64 @@ class CImage { * @param string $newName new filename or leave to null to autogenerate filename. */ public function __construct($imageName=null, $imageFolder=null, $saveFolder=null, $newName=null) { - $this->imageName = ltrim($imageName, '/'); - $this->imageFolder = rtrim($imageFolder, '/'); - $this->pathToImage = $this->imageFolder . '/' . $this->imageName; - $this->fileExtension = pathinfo($this->pathToImage, PATHINFO_EXTENSION); - $this->saveFolder = $saveFolder; - $this->newName = $newName; + $this->imageName = ltrim($imageName, '/'); + $this->imageFolder = rtrim($imageFolder, '/'); + $this->pathToImage = $this->imageFolder . '/' . $this->imageName; + $this->fileExtension = pathinfo($this->pathToImage, PATHINFO_EXTENSION); + $this->extension = $this->fileExtension; + $this->saveFolder = $saveFolder; + $this->newName = $newName; } + + /** + * Log an event. + * + * @param string $message to log. + */ + public function Log($message) { + if($this->verbose) { + $this->log[] = $message; + } + } + + + + /** + * Do verbose output and print out the log and the actual images. + * + */ + public function VerboseOutput() { + $log = null; + $this->Log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M"); + $this->Log("Memory limit: " . ini_get('memory_limit')); + foreach($this->log as $val) { + if(is_array($val)) { + foreach($val as $val1) { + $log .= $val1 . '
'; + } + } + else { + $log .= $val . '
'; + } + } + + $object = null; //print_r($this, 1); + + echo << + + +CImage verbose output + +

CImage Verbose Output

+
{$log}
+
{$object}
+EOD; + } + + /** * Raise error, enables to implement a selection of error methods. * @@ -64,9 +140,10 @@ class CImage { public function RaiseError($message) { throw new Exception($message); } + + - - /** + /* * Create filename to save file in cache. */ public function CreateFilename() { @@ -74,7 +151,8 @@ class CImage { $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; - $quality = $this->quality == 100 ? null : "_q{$this->quality}"; + $quality = $this->quality ? "_q{$this->quality}" : 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)) { @@ -87,19 +165,39 @@ class CImage { } } } + $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; + } + else if($this->extension == 'png') { + $optimize .= $this->pngFilter ? 'f' : null; + $optimize .= $this->pngDeflate ? 'd' : null; + } + $subdir = str_replace('/', '-', dirname($this->imageName)); $subdir = ($subdir == '.') ? '_.' : $subdir; - return $this->saveFolder . '/' . $subdir . '_' . $parts['filename'] . '_' . round($this->newWidth) . '_' . round($this->newHeight) . $crop . $cropToFit . $crop_x . $crop_y . $quality . $filters . '.' . $parts['extension']; + $this->cacheFileName = $this->saveFolder . '/' . $subdir . '_' . $parts['filename'] . '_' . round($this->newWidth) . '_' . round($this->newHeight) . $offset . $crop . $cropToFit . $crop_x . $crop_y . $quality . $filters . $sharpen . $emboss . $blur . $palette . $optimize . '.' . $this->extension; + $this->Log("The cache file name is: " . $this->cacheFileName); + return $this; } + /** * Init and do some sanity checks before any processing is done. Throws exception if not valid. */ public function Init() { 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'); - is_numeric($this->quality) and $this->quality >= 0 and $this->quality <= 100 or $this->RaiseError('Quality not in range.'); + is_null($this->quality) or (is_numeric($this->quality) and $this->quality >= 0 and $this->quality <= 100) or $this->RaiseError('Quality not in range.'); //is_numeric($this->crop_x) && is_numeric($this->crop_y) or $this->RaiseError('Quality not in range.'); //filter is_readable($this->pathToImage) or $this->RaiseError('File does not exist.'); @@ -111,6 +209,12 @@ class CImage { !empty($info) or $this->RaiseError("The file doesn't seem to be an image."); $this->mime = $info['mime']; + 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."); + } + return $this; } @@ -120,34 +224,180 @@ class CImage { * */ protected function Output($file) { + if($this->verbose) { + $this->Log("Outputting image: $file"); + } + + // 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']; $time = filemtime($file); + if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $time){ + if($this->verbose) { + $this->Log("304 not modified"); + $this->VerboseOutput(); + exit; + } header("HTTP/1.0 304 Not Modified"); } else { - header('Content-type: ' . $this->mime); - header('Last-Modified: ' . gmdate("D, d M Y H:i:s",$time) . " GMT"); + $gmdate = gmdate("D, d M Y H:i:s", $time); + + if($this->verbose) { + $this->Log("Last modified: " . $gmdate . " GMT"); + $this->VerboseOutput(); + exit; + } + + header('Content-type: ' . $mime); + header('Last-Modified: ' . $gmdate . " GMT"); readfile($file); } exit; } + + + /** + * Get the type of PNG image. + * + */ + public function GetPngType() { + $this->pngType = ord (file_get_contents ($this->pathToImage, false, null, 25, 1)); + if($this->pngType == self::PNG_GREYSCALE) { + $this->Log("PNG is type 0, Greyscale."); + } + else if($this->pngType == self::PNG_RGB) { + $this->Log("PNG is type 2, RGB"); + } + else if($this->pngType == self::PNG_RGB_PALETTE) { + $this->Log("PNG is type 3, RGB with palette"); + } + else if($this->pngType == self::PNG_GREYSCALE_ALPHA) { + $this->Log("PNG is type 4, Greyscale with alpha channel"); + } + else if($this->pngType == self::PNG_RGB_ALPHA) { + $this->Log("PNG is type 6, RGB with alpha channel (PNG 32-bit)"); + } + + return $this->pngType; + } + + + /** + * Set quality of image + * + */ + protected function SetQuality() { + if(!$this->quality) { + switch($this->extension) { + case 'jpg': + $this->quality = self::JPEG_QUALITY_DEFAULT; + break; + + case 'png': + $this->quality = self::PNG_QUALITY_DEFAULT; + break; + + default: + $this->quality = null; + } + } + $this->Log("Setting quality to {$this->quality}."); + return $this; + } + + + + /** + * Set optmizing options. + * + */ + protected function SetOptimization() { + if(defined('JPEG_OPTIMIZE')) { + $this->jpegOptimize = JPEG_OPTIMIZE; + } + + if(defined('PNG_FILTER')) { + $this->pngFilter = PNG_FILTER; + } + + if(defined('PNG_DEFLATE')) { + $this->pngDeflate = PNG_DEFLATE; + } + return $this; + } + + + + /** + * Calciulate number of colors in an image. + * + * @param resource $im the image. + */ + protected 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); + } + } + + + /** * Open image. * */ protected function Open() { + $this->Log("Opening file as {$this->fileExtension}."); switch($this->fileExtension) { case 'jpg': - case 'jpeg': $this->image = @imagecreatefromjpeg($this->pathToImage); break; - case 'gif': $this->image = @imagecreatefromgif($this->pathToImage); break; - case 'png': $this->image = @imagecreatefrompng($this->pathToImage); break; + case 'jpeg': + $this->image = @imagecreatefromjpeg($this->pathToImage); + break; + + 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; $this->RaiseError('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; } - + + /** * Map filter name to PHP filter and id. * @@ -155,18 +405,18 @@ class CImage { */ 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), + '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]; @@ -176,22 +426,63 @@ class CImage { } + /** * Calculate new width and height of image. */ protected 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) { - if(empty($this->crop['width'])) { + $width = $this->crop['width']; + $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."); + + /*if(empty($this->crop['width'])) { $this->crop['width'] = $this->width - $this->crop['start_x']; } if(empty($this->crop['height'])) { $this->crop['height'] = $this->height - $this->crop['start_y']; - } - $width = $this->crop['width']; - $height = $this->crop['height']; + }*/ } // Calculate new width and height if keeping aspect-ratio. @@ -244,33 +535,177 @@ class CImage { // 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; } + + + + /** + * Set extension for filename to save as. + * + */ + private function SetSaveAsExtension() { + if($this->saveAs) { + switch(strtolower($this->saveAs)) { + case 'jpg': + $this->extension = 'jpg'; + break; + + case 'png': + $this->extension = 'png'; + break; + + case 'gif': + $this->extension = 'gif'; + break; + + default: + $this->extension = null; + } + $this->Log("Saving image as: " . $this->extension); + } + return $this; + } + + + + /** + * Use original image if possible. + * + */ + protected function UseOriginalIfPossible() { + if($this->useOriginal && + ($this->newWidth == $this->width) && + ($this->newHeight == $this->height) && + !$this->quality && + !$this->area && + !$this->crop && + !$this->filters && + !$this->saveAs && + !$this->sharpen && + !$this->emboss && + !$this->blur && + !$this->palette) { + $this->Log("Using original image."); + $this->Output($this->pathToImage); + } + } + + - + /** + * Use cached version of image, if possible + * + */ + protected function UseCacheIfPossible() { + if(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."); + } + return $this; + } + + + + /** + * Sharpen image as http://php.net/manual/en/ref.image.php#56144 + * http://loriweb.pair.com/8udf-sharpen.html + * + */ + protected 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; + } + + + + /** + * Emboss image as http://loriweb.pair.com/8udf-emboss.html + * + */ + protected function EmbossImage() { + $matrix = array( + array( 1, 1,-1,), + array( 1, 3,-1,), + array( 1,-1,-1,) + ); + $divisor = 3; + $offset = 0; + imageconvolution($this->image, $matrix, $divisor, $offset); + return $this; + } + + + + /** + * Blur image as http://loriweb.pair.com/8udf-basics.html + * + */ + protected function BlurImage() { + $matrix = array( + array( 1, 1, 1,), + array( 1,15, 1,), + array( 1, 1, 1,) + ); + $divisor = 23; + $offset = 0; + imageconvolution($this->image, $matrix, $divisor, $offset); + return $this; + } + + /** * Resize the image and optionally store/cache the new imagefile. Output the image. * - * @param integer $newWidth the new width or null. Default is null. - * @param integer $newHeight the new width or null. Default is null. - * @param boolean $keepRatio true to keep aspect ratio else false. Default is true. - * @param boolean $cropToFit true to crop image to fit in box specified by $newWidth and $newHeight. Default is false. - * @param integer $quality the quality to use when saving the file, range 0-100, default is full quality which is 100. - * @param array/string $crop converts string of 1,2,3,4 to array 'width'=>1, 'height'=>2, 'start_x'=>3, 'start_y'=>4. - * @param array $filter. + * @param array $args used when processing image. */ public function ResizeAndOutput($args) { $defaults = array( - 'newWidth'=>null, - 'newHeight'=>null, - 'keepRatio'=>true, - 'cropToFit'=>false, - 'quality'=>100, - 'crop'=>null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0), - 'filters'=>null, + 'newWidth' => null, + 'newHeight' => null, + 'keepRatio' => true, + 'area' => null, //'0,0,0,0', + 'cropToFit' => false, + 'quality' => null, + 'crop' => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0), + 'filters' => null, + 'verbose' => false, + 'useCache' => true, + 'useOriginal' => true, + 'saveAs' => null, + 'sharpen' => null, + 'emboss' => null, + 'blur' => null, + 'palette' => null, ); - + // Convert crop settins from string to array if(isset($args['crop']) && !is_array($args['crop'])) { $pices = explode(',', $args['crop']); @@ -282,6 +717,17 @@ class CImage { ); } + // 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], + ); + } + // 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) { @@ -298,7 +744,6 @@ class CImage { $args['filters'][$key] = $filter; } } - //echo "
" . print_r($args['filters'], true) . "
"; // Merge default arguments with incoming and set properties. //$args = array_merge_recursive($defaults, $args); @@ -306,32 +751,52 @@ class CImage { foreach($defaults as $key=>$val) { $this->{$key} = $args[$key]; } - //echo "
" . print_r($this, true) . "
"; + $this->Log("Resize and output image."); // Init the object and do sanity checks on arguments - $this->Init()->CalculateNewWidthAndHeight(); - //echo "
" . print_r($this, true) . "
"; + $this->Init() + ->CalculateNewWidthAndHeight() + ->UseOriginalIfPossible(); - // Use original image? - if(is_null($this->newWidth) && is_null($this->newHeight)) { - $this->Output($this->pathToImage); - } - // Check cache before resizing. - $this->newFileName = $this->CreateFilename(); - if(is_readable($this->newFileName)) { - $fileTime = filemtime($this->pathToImage); - $cacheTime = filemtime($this->newFileName); - if($fileTime <= $cacheTime) { - $this->Output($this->newFileName); - } - } + $this->SetSaveAsExtension() + ->SetQuality() + ->SetOptimization() + ->CreateFilename() + ->UseCacheIfPossible(); - // Resize and output and save new to cache - $this->Open()->ResizeAndSave(); + // Resize and output + $this->Open() + ->Resize() + ->SaveToCache() + ->Output($this->cacheFileName); } + + /** + * 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 + */ + protected 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); + } + + $this->image = $img; + } + + + /** * Create a image and keep transparency for png and gifs. * @@ -340,11 +805,34 @@ class CImage { * @returns image resource. */ public function CreateImageKeepTransparency($width, $height) { - //echo $width . "x" . $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); + */ /* - if($this->fileExtension == 'png' || ($this->fileExtension == 'gif')) { + I have had success doing it like this in the past: + + $thumb = imagecreatetruecolor($newwidth, $newheight); + imagealphablending($thumb, false); + imagesavealpha($thumb, true); + + $source = imagecreatefrompng($fileName); + imagealphablending($source, true); + + imagecopyresampled($thumb, $source, 0, 0, 0, 0, $newwidth, $newheight, $width, $height); + + imagepng($thumb,$newFilename); + + I found the output image quality much better using imagecopyresampled() than imagecopyresized() + + if($this->fileExtension == 'png' || ($this->fileExtension == 'gif')) { imagealphablending($img, false); imagesavealpha($img, true); $transparent = imagecolorallocatealpha($img, 255, 255, 255, 127); @@ -356,14 +844,36 @@ class CImage { /** - * Resize, crop and output the image. + * Resize and or crop the image. * */ - public function ResizeAndSave() { + 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; + } // Do as crop, take only part of image if($this->crop) { - //echo "Cropping"; + $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; @@ -373,73 +883,146 @@ class CImage { // Resize by crop to fit if($this->cropToFit) { - //echo "Crop to fit"; - $cropX = ($this->cropWidth/2) - ($this->newWidth/2); - $cropY = ($this->cropHeight/2) - ($this->newHeight/2); + $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; } - // as is - else { + // Resize it + else if(!($this->newWidth == $this->width && $this->newHeight == $this->height)) { + $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; } // 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($imageResized, $filter['type']); break; - case 1: imagefilter($imageResized, $filter['type'], $filter['arg1']); break; - case 2: imagefilter($imageResized, $filter['type'], $filter['arg1'], $filter['arg2']); break; - case 3: imagefilter($imageResized, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']); break; - case 4: imagefilter($imageResized, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']); break; + 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; } } } - - switch($this->fileExtension) - { + + // Convert to palette image + if($this->palette) { + $this->Log("Converting to palette image."); + $this->TrueColorToPalette(); + } + + // Blur the image + if($this->blur) { + $this->Log("Blur."); + $this->BlurImage(); + } + + // Emboss the image + if($this->emboss) { + $this->Log("Emboss."); + $this->EmbossImage(); + } + + // Sharpen the image + if($this->sharpen) { + $this->Log("Sharpen."); + $this->SharpenImage(); + } + + return $this; + } + + + + /** + * Save image to cache + * + */ + protected function SaveToCache() { + switch($this->extension) { case 'jpg': - case 'jpeg': - if(imagetypes() & IMG_JPG) { - if($this->saveFolder) { - imagejpeg($imageResized, $this->newFileName, $this->quality); + if($this->saveFolder) { + $this->Log("Saving image as JPEG to cache using quality = {$this->quality}."); + imagejpeg($this->image, $this->cacheFileName, $this->quality); + + // Use JPEG optimize if defined + if($this->jpegOptimize) { + if($this->verbose) { clearstatcache(); $this->Log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes."); } + $res = array(); + $cmd = $this->jpegOptimize . " -outfile $this->cacheFileName $this->cacheFileName"; + exec($cmd, $res); + $this->Log($cmd); + $this->Log($res); } - $imgFunction = 'imagejpeg'; - } - break; + } + break; case 'gif': - if (imagetypes() & IMG_GIF) { - if($this->saveFolder) { - imagegif($imageResized, $this->newFileName); - } - $imgFunction = 'imagegif'; - } - break; + if($this->saveFolder) { + $this->Log("Saving image as GIF to cache."); + imagegif($this->image, $this->cacheFileName); + } + break; case 'png': - // Scale quality from 0-100 to 0-9 and invert setting as 0 is best, not 9 - $quality = 9 - round(($this->quality/100) * 9); - if (imagetypes() & IMG_PNG) { - if($this->saveFolder) { - imagepng($imageResized, $this->newFileName, $quality); + if($this->saveFolder) { + $this->Log("Saving image as PNG to cache using quality = {$this->quality}."); + + // Turn off alpha blending and set alpha flag + imagealphablending($this->image, false); + imagesavealpha($this->image, true); + + imagepng($this->image, $this->cacheFileName, $this->quality); + + // Use external program to filter PNG, if defined + if($this->pngFilter) { + if($this->verbose) { clearstatcache(); $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes."); } + $res = array(); + $cmd = $this->pngFilter . " $this->cacheFileName"; + exec($cmd, $res); + $this->Log($cmd); + $this->Log($res); } - $imgFunction = 'imagepng'; - } - break; + + // Use external program to deflate PNG, if defined + if($this->pngDeflate) { + if($this->verbose) { clearstatcache(); $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes."); } + $res = array(); + $cmd = $this->pngDeflate . " $this->cacheFileName"; + exec($cmd, $res); + $this->Log($cmd); + $this->Log($res); + } + } + break; + default: $this->RaiseError('No support for this file extension.'); - break; + break; } - header('Content-type: ' . $this->mime); - header('Last-Modified: ' . gmdate("D, d M Y H:i:s", time()) . " GMT"); - $imgFunction($imageResized); - exit; + + 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)); + } + + return $this; } diff --git a/README.md b/README.md index 907fe3e..e421671 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,12 @@ Enjoy! Mikael Roos (me@mikaelroos.se) +License +------------------------------------- + +Free and opensource software. License according to MIT. + + Installation ------------------------------------- @@ -52,6 +58,24 @@ RewriteRule ^image/(.*)$ img/img.php?src=$1 [QSA,NC,L] Now you can access and resize your images through `/image/someimage.jpg?w=80`. Very handy. + +Usage +------------------------------------- + +`img.php?src=image.jpg&sharpen` + +-v, -verbose, Do verbose output and print out a log what happens. +-no-cache, Do not use the cached version, do all conversions. +-skip-original, Skip using the original image, always resize and use cached image. +-save-as, Save image as jpg, png or gif, loosing transparency. + +-sharpen to appy a filter that sharpens the image. Good to apply when resizing to smaller dimensions. +-emboss to apply a emboss effect. +-blur to apply a blur effect. + +-palette to create a palette version of the image with up to 256 colors. + + Revision history ------------------------------------- @@ -64,13 +88,16 @@ center of the image from which the crop is done. * Support for resizing opaque images. * Clean up code in `CImage.php`. * Better errorhandling for invalid dimensions. -* Crop-to-fit does not work. +* Crop-to-fit with offste top, bottom, center, left, right, center. +* Define the color of the background of the resulting image, when loosing transparency. -v0.3x (latest) +v0.3.x (latest) +* Adding grid column size as predefined size, c1-c24 for a 24 column grid. Configure in `img.php`. * Corrected error on naming cache-files using subdir. * Corrected calculation error on width & height for crop-to-fit. +* Adding effects for sharpen, emboss and blur through imageconvolution using matrixes. v0.3 (2012-10-02) @@ -101,4 +128,4 @@ v0.1 (2012-04-25) * Initial release after rewriting some older code I had lying around. . -..: Copyright 2012 by Mikael Roos (me@mikaelroos.se) +..: Copyright 2012-2013 by Mikael Roos (me@mikaelroos.se) diff --git a/cache/README.md b/cache/README.md deleted file mode 100644 index a79b244..0000000 --- a/cache/README.md +++ /dev/null @@ -1 +0,0 @@ -Make this directory writable by the webserver. diff --git a/img.php b/img.php index 5833458..2b14463 100644 --- a/img.php +++ b/img.php @@ -10,30 +10,68 @@ error_reporting(-1); set_time_limit(20); + +// Use preprocessing of images +define('PNG_FILTER', '/usr/local/bin/optipng -q'); +define('PNG_DEFLATE', '/usr/local/bin/pngout -q'); +define('JPEG_OPTIMIZE', '/usr/local/bin/jpegtran -copy none -optimize'); + + // Append ending slash $cimageClassFile = __DIR__ .'/CImage.php'; $pathToImages = __DIR__.'/img/'; $pathToCache = __DIR__.'/cache/'; $maxWidth = $maxHeight = 2000; +$gridColumnWidth = 30; +$gridGutterWidth = 10; +$gridColumns = 24; +// settings for do not largen smaller images +// settings for max image dimensions -// Set areas to map constant to value, easier to use with width or height -$area = array( +// Set sizes to map constant to value, easier to use with width or height +$sizes = array( 'w1' => 613, + 'w2' => 630, ); + + +// Add column width to $area, useful for use as predefined size for width (or height). +for($i = 1; $i <= $gridColumns; $i++) { + $sizes['c' . $i] = ($gridColumnWidth + $gridGutterWidth) * $i - $gridGutterWidth; +} + + + // Get input from querystring $srcImage = isset($_GET['src']) ? $_GET['src'] : null; $newWidth = isset($_GET['width']) ? $_GET['width'] : (isset($_GET['w']) ? $_GET['w'] : null); $newHeight = isset($_GET['height']) ? $_GET['height'] : (isset($_GET['h']) ? $_GET['h'] : null); $keepRatio = isset($_GET['no-ratio']) ? false : true; $cropToFit = isset($_GET['crop-to-fit']) ? true : false; +$area = isset($_GET['area']) ? $_GET['area'] : null; $crop = isset($_GET['crop']) ? $_GET['crop'] : (isset($_GET['c']) ? $_GET['c'] : null); -$quality = isset($_GET['quality']) ? $_GET['quality'] : (isset($_GET['q']) ? $_GET['q'] : 100); +$quality = isset($_GET['quality']) ? $_GET['quality'] : (isset($_GET['q']) ? $_GET['q'] : null); +$verbose = (isset($_GET['verbose']) || isset($_GET['v'])) ? true : false; +$useCache = isset($_GET['no-cache']) ? false : true; +$useOriginal = isset($_GET['skip-original']) ? false : true; +$saveAs = isset($_GET['save-as']) ? $_GET['save-as'] : null; +$sharpen = isset($_GET['sharpen']) ? true : null; +$emboss = isset($_GET['emboss']) ? true : null; +$blur = isset($_GET['blur']) ? true : null; +$palette = isset($_GET['palette']) ? true : null; -// Check to replace area -if(isset($area[$newWidth])) { - $newWidth = $area[$newWidth]; + + +// Check to replace predefined size +if(isset($sizes[$newWidth])) { + $newWidth = $sizes[$newWidth]; } +if(isset($sizes[$newHeight])) { + $newHeight = $sizes[$newHeight]; +} + + // Add all filters to an array $filters = array(); @@ -44,6 +82,8 @@ for($i=0; $i<10;$i++) { if($filter) { $filters[] = $filter; } } + + // Do some sanity checks function errorPage($msg) { header("Status: 404 Not Found"); @@ -58,10 +98,44 @@ is_null($newWidth) or ($newWidth > 10 && $newWidth <= $maxWidth) or errorPage('W is_null($newHeight) or ($newHeight > 10 && $newHeight <= $maxHeight) or errorPage('Hight out of range.'); $quality >= 0 and $quality <= 100 or errorPage('Quality out of range'); -// Create the image object + + +// Display image if vebose mode +if($verbose) { + $query = array(); + parse_str($_SERVER['QUERY_STRING'], $query); + unset($query['verbose']); + unset($query['v']); + unset($query['nocache']); + $url1 = '?' . http_build_query($query); + echo <<$url1
+ + +EOD; +} + + + +// Create and output the image require($cimageClassFile); $img = new CImage($srcImage, $pathToImages, $pathToCache); -$img->ResizeAndOutput(array('newWidth'=>$newWidth, 'newHeight'=>$newHeight, 'keepRatio'=>$keepRatio, - 'cropToFit'=>$cropToFit, 'quality'=>$quality, - 'crop'=>$crop, 'filters'=>$filters, - )); \ No newline at end of file +$img->ResizeAndOutput(array( + 'newWidth' => $newWidth, + 'newHeight' => $newHeight, + 'keepRatio' => $keepRatio, + 'cropToFit' => $cropToFit, + 'area' => $area, + 'quality' => $quality, + 'crop' => $crop, + 'filters' => $filters, + 'verbose' => $verbose, + 'useCache' => $useCache, + 'useOriginal' => $useOriginal, + 'saveAs' => $saveAs, + 'sharpen' => $sharpen, + 'emboss' => $emboss, + 'blur' => $blur, + 'palette' => $palette, +)); +