diff --git a/CImage.php b/CImage.php index 4c0bdc5..3096a53 100644 --- a/CImage.php +++ b/CImage.php @@ -1,121 +1,1421 @@ 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; - } - - /** - * Log an event. - * - * @param string $message to log. - */ - public function Log($message) { - if($this->verbose) { - $this->log[] = $message; + /** + * 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); + } + + + + /** + * 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; + } + + + + /** + * 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; } - } - /** - * 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 .= htmlentities($val1) . '
'; + /** + * 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; } - } - else { - $log .= htmlentities($val) . '
'; - } + + $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); + + return $this; + } + + + + /** + * 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; + } + + $this->saveFolder = $dir; + $this->cacheFileName = $dir . '/' . $src; + + 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; + } + + + + /** + * 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], + ); + } + + // 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) { + $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; + } + } + + // 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]; + } + + return $this; } - $object = null; //print_r($this, 1); - echo << 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]; + else { + throw new Exception('No such filter.'); + } + } + + + + /** + * 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}"); + } + + // 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}"); + } + + is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range'); + + // 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'); + + return $this; + } + + + + /** + * 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."); + } + + // 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; + } + + + + /** + * 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; + } + + + + /** + * 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; + } + + + + /** + * 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; + } + + + + /** + * 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); + } + return $this; + } + + + + /** + * 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}"]; + } + } + } + } + + $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; + } + + $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; + + return $this->setTarget($file, $base); + } + + + + /** + * 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."); + } + + return $this; + } + + + + /** + * 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; + + 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; + } + + + + /** + * 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; + } + + + + /** + * 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); + } + } + + + + /** + * 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; + } + + + + /** + * 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; + } + + // Do as crop, take only part of image + if ($this->crop) { + + $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']; + } + + // 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; + } + + return $this; + } + + + + /** + * 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; + } + } + } + + // 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; + } + + + + /** + * 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); + } + + $this->image = $img; + } + + + + /** + * 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; + } + + + + /** + * 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,), + ); + + $divisor = 3; + $offset = 0; + + imageconvolution($this->image, $matrix, $divisor, $offset); + + return $this; + } + + + + /** + * 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,), + ); + + $divisor = 23; + $offset = 0; + + imageconvolution($this->image, $matrix, $divisor, $offset); + + return $this; + } + + + + /** + * 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; + } + + + + /** + * 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; + } + + + + /** + * 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); + } + + 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); + + // 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); + } + 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 + 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); + } + + // 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; + } + + 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; + } + + + + /** + * 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; + } + + $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']; + $lastModified = filemtime($file); + $gmdate = gmdate("D, d M Y H:i:s", $lastModified); + + if (!$this->verbose) { + header('Last-Modified: ' . $gmdate . " GMT"); + } + + 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; + } + + + + /** + * Log an event if verbose mode. + * + * @param string $message to log. + * + * @return void + */ + private function log($message) + { + if ($this->verbose) { + $this->log[] = $message; + } + } + + + + /** + * 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) . '
'; + } + } else { + $log .= htmlentities($val) . '
'; + } + } + + echo << @@ -123,964 +1423,21 @@ class CImage {

CImage Verbose Output

{$log}
-
{$object}
EOD; - } - - - /** - * Raise error, enables to implement a selection of error methods. - * - * @param $message string the error message to display. - */ - public function RaiseError($message) { - throw new Exception($message); - } - - - - /* - * Create filename to save file in cache. - */ - public function CreateFilename() { - $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; - $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}"]; - } - } - } - } - $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; - $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 . $scale . '.' . $this->extension; - - // Sanitize filename - $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName); - $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() { - // 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."); - $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."); + + /** + * 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); } - - // 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}"); - } - - // 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}"); - } - - is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->RaiseError('Aspect ratio out of range'); - - // width & height from aspect ratio - if($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) { - // set new width and height based on current & aspect ratio, but base on largest dimension to only shrink image, not enlarge - 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}"); - } - } - else if($this->aspectRatio && is_null($this->newWidth)) { - $this->newWidth = $this->newHeight * $this->aspectRatio; - $this->Log("Setting new width based on aspect ratio to {$this->newWidth}"); - } - else if($this->aspectRatio && is_null($this->newHeight)) { - //$this->newHeight = ($this->aspectRatio >= 0) ? ($this->newWidth / $this->aspectRatio) : ($this->newWidth * $this->aspectRatio); - $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'); - 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.'); - in_array($this->fileExtension, $this->validExtensions) or $this->RaiseError('Not a valid file extension.'); - is_null($this->saveFolder) or is_writable($this->saveFolder) or $this->RaiseError('Save directory does not exist or is not writable.'); - - return $this; - } - - - /** - * Output image using caching. - * - */ - 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']; - $lastModified = filemtime($file); - $gmdate = gmdate("D, d M Y H:i:s", $lastModified); - - if(!$this->verbose) { header('Last-Modified: ' . $gmdate . " GMT"); } - - 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; - } - - - - /** - * 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 'jpeg': - 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); - $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. - * - * @param string $name the name of the filter. - */ - 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]; - else { - $this->RaiseError('No such filter.'); - } - } - - - - /** - * 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) { - $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."); - - /*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']; - }*/ - } - - // 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. - ; - } - - // Both new width and height are set. - elseif(isset($this->newWidth) && isset($this->newHeight)) { - // 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); - } - - // Use new width as max-width - elseif(isset($this->newWidth)) { - $factor = (float)$this->newWidth / (float)$width; - $this->newHeight = round($factor * $height); - } - - // Use new height as max-hight - elseif(isset($this->newHeight)) { - $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) { - /* - if($cropToFit && $newWidth && $newHeight) { - $targetRatio = $newWidth / $newHeight; - $cropWidth = $targetRatio > $aspectRatio ? $width : round($height * $targetRatio); - $cropHeight = $targetRatio > $aspectRatio ? round($width / $targetRatio) : $height; - } - */ - $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; - } - - - - /** - * 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 array $args used when processing image. - */ - public function ResizeAndOutput($args) { - $defaults = array( - 'newWidth' => null, - 'newHeight' => null, - 'aspectRatio' => null, - 'keepRatio' => true, - 'area' => null, //'0,0,0,0', - 'scale' => null, - 'cropToFit' => false, - 'quality' => null, - 'deflate' => 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']); - $args['crop'] = array( - 'width' => $pices[0], - 'height' => $pices[1], - 'start_x' => $pices[2], - 'start_y' => $pices[3], - ); - } - - // 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) { - $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 { - $this->RaiseError('Missing arg to filter, review how many arguments are needed at http://php.net/manual/en/function.imagefilter.php'); - } - } - $args['filters'][$key] = $filter; - } - } - - // Quick solution when introducing parameter deflate, just map it to quality. Should re-engineer usage of quality and deflate. - if(!isset($this->quality) && isset($this->deflate)) { - $this->quality = $this->deflate; - } - - // 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]; - } - $this->Log("Resize and output image."); - - // Init the object and do sanity checks on arguments - $this->Init() - ->CalculateNewWidthAndHeight() - ->UseOriginalIfPossible(); - - // Check cache before resizing. - $this->SetSaveAsExtension() - ->SetQuality() - ->SetOptimization() - ->CreateFilename() - ->UseCacheIfPossible(); - - // 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. - * - * $param int $width of the new image. - * @param int $height of the new image. - * @returns image resource. - */ - public 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; - } - - - - /** - * Resize and or crop the image. - * - */ - public function Resize() { - - $this->Log("Starting to Resize()"); - - // 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; - } - - // 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) { - $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']; - } - - // Resize by crop to fit - if($this->cropToFit) { - /* - $cropX = round(($width - $cropWidth) / 2); - $cropY = round(($height - $cropHeight) / 2); - $imageResized = createImageKeepTransparency($newWidth, $newHeight); - imagecopyresampled($imageResized, $image, 0, 0, $cropX, $cropY, $newWidth, $newHeight, $cropWidth, $cropHeight); - $image = $imageResized; - $width = $newWidth; - $height = $newHeight; - */ - $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; - } - - // 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($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; - } - } - } - - // 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 'jpeg': - case 'jpg': - 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); - } - } - break; - - case 'gif': - if($this->saveFolder) { - $this->Log("Saving image as GIF to cache."); - imagegif($this->image, $this->cacheFileName); - } - break; - - case 'png': - 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); - } - - // 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; - } - - 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 123877d..43c7c33 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,9 @@ Image conversion on the fly using PHP About ------------------------------------- -`CImage` is a PHP class which enables scaling, cropping, filtering effects and processing of images using PHP GD. The script `img.php` uses `CImage` to enable server-side image processing together with caching and optimization of the processed images. +`CImage` is a PHP class which enables resizing of images through scaling and cropping together with filtering effects, all using PHP GD. The script `img.php` uses `CImage` to enable server-side image processing together with caching and optimization of the processed images. -Server-side image processing is a useful tool for any web developer, `img.php` has an easy to use interface and its quite powerful when you integrate it with your website. This is a most useful tool for any web developer who has a need to create and process images for a website. - -This is free software and open source. +Server-side image processing is a most useful tool for any web developer, `img.php` has an easy to use interface and its quite powerful when you integrate it with your website. Using it might decrease the time and effort put in managing images and improve your work flow when creating content for websites. Read more on http://dbwebb.se/opensource/cimage @@ -17,22 +15,23 @@ Enjoy! Mikael Roos (me@mikaelroos.se) + License ------------------------------------- -License according to MIT. +This is free software and open source software, licensed according MIT. Installation and get going ------------------------------------- -**Latest stable version is v0.4.1 released 2014-01-27.** +**Latest stable version is v0.5 released 2014-02-11.** ```bash git clone git://github.com/mosbth/cimage.git cd cimage -git checkout v0.4.1 +git checkout v0.5 ``` Make the cache-directory writable by the webserver. @@ -41,48 +40,110 @@ Make the cache-directory writable by the webserver. chmod 777 cache ``` -Try it out by pointing your browser to the test file `test.php`. +Try it out by pointing your browser to the test file `webroot/test.php`. + +Review the settings in `webroot/img_config.php` and check out `webroot/img.php` on how it uses `CImage`. -Review the settings in `img.php`. Usage ------------------------------------- +###List of parameters {#parameters} + +The `img.php` supports a lot of parameters. Combine the parameters to get the desired behavior and resulting image. For example, take the original image, resize it using width, aspect-ratio and crop-to-fit, apply a sharpen effect, save the image as JPEG using quality 30. + +| `img.php?src=kodim13.png&w=600&aspect-ratio=4&crop-to-fit&sharpen&save-as=jpg&q=30` | +|-----------------------------------------------------------| +| | + +Here is a list of all parameters that you can use together with `img.php`, grouped by its basic intent of usage. + + +####Mandatory options and debugging + +The `src` is the only mandatory option. The other in this section is useful for debugging or deciding what version of the target image is used. + | Parameter | Explained | |----------------|----------------------------------------------| -| `src` | `src=img.png` choses the source image to use. | -| `h, height` | `h=200` sets the width to be to max 200px. `h=25%` sets the height to 25% of its original height. | -| `w, width` | `w=200` sets the height to be max 200px. `w=100%` sets the width to 100% of its original width. | -| `ar, aspect-ratio` | Use this as aspect ratio. Use together with either height or width or alone to base calculations on original image dimensions. This setting is used to calculate the resulting dimension for the image. `w=160&aspect-ratio=1.6` results in a width of 100px. | -| `s, scale` | Scale the image to a size proportional to a percentage of its original size, `scale=25` makes a image 25% of its original size and `size=200` doubles up the image size. Scale is applied before all processing and has no impact of the final width and height. | -| `nr, no-ratio, stretch` | Do *not* keep aspect ratio when resizing using both width & height constraints. Results in stretching the image, if needed, to fit in the resulting box. | +| `src` | Source image to use, mandatory. `src=img.png` or with subdirectory `src=dir/img.png`. | +| `nc, no-cache` | Do not use the cached version, do all image processing and save a new image to cache. | +| `so, skip-original`| Skip using the original image, always process image, create and use a cached version of the original image. | +| `v, verbose` | Do verbose output and print out a log what happens. Good for debugging, analyzing the process and inspecting how the image is being processed. | + + + +####Options for resizing image + +These options are all affecting the dimensions used when resizing the image. Its used to define the area to use in the source image and the resulting dimensions for the target image. + +| Parameter | Explained | +|----------------|----------------------------------------------| +| `h, height` | `h=200` sets the width to be to max 200px. `h=25%` sets the height to max 25% of its original height. | +| `w, width` | `w=200` sets the height to be max 200px. `w=100%` sets the width to max 100% of its original width. | +| `ar, aspect-ratio` | Control target aspect ratio. Use together with either height or width or alone to base calculations on original image dimensions. This setting is used to calculate the resulting dimension for the image. `w=160&aspect-ratio=1.6` results in a height of 100px. Use ar=!1.6 to inverse the ratio, useful when using portrat instead of landscape images. | +| `nr, no-ratio, stretch` | Do *not* keep aspect ratio when resizing and using both width & height constraints. Results in stretching the image, if needed, to fit in the resulting box. | | `cf, crop-to-fit` | Set together with both `h` & `w` to make the image fit into dimensions, and crop out the rest of the image. | -| `a, area` | Define the area of the image to work with. Set `area=10,10,10,10` (top,right,bottom,left) to crop out the 10% of the outermost area. It works like an offset to define which part of the image you want to process. Its an alternative to use `crop`. | -| `c, crop` | Crops an area from the original image, set width, height, start_x and start_y to define the area to crop, for example `crop=100,100,10,10` (`crop=width,height,start_x,start_y`). Left top corner is 0, 0. You can use left, right or center when setting start_x. You may use top, bottom or center when setting start_y. You can use negative values for x and y. Use 0 for width or height to get the width/height of the original image. Use negative values for width/height to get original width/height minus selected value. | -| `q, quality` | Quality affects lossy compression and file size for JPEG images by setting the quality between 1-100, default is 60. Quality has no effect on PNG or GIF. | -| `d, deflate` | For PNG images it defines the compression algorithm, values can be 1-9, default is defined by PHP GD. Quality has no effect on JPEG or GIF. | +| `a, area` | Define the area of the image to work with. Set `area=10,10,10,10` (top,right,bottom,left) to crop out the 10% of the outermost area. It works like an offset to define which part of the image you want to process. Its an alternative of using `crop`. | +| `c, crop` | Crops an area from the original image, set width, height, start_x and start_y to define the area to crop, for example `crop=100,100,10,10` (`crop=width,height,start_x,start_y`). Left top corner is 0, 0. You can use `left`, `right` or `center` when setting start_x. You may use `top`, `bottom` or `center` when setting start_y. | + + + +####Processing of image before resizing + +These options are executed *before* the image is resized. + +| Parameter | Explained | +|----------------|----------------------------------------------| +| `s, scale` | Scale the image to a size proportional to a percentage of its original size, `scale=25` makes an image 25% of its original size and `size=200` doubles up the image size. Scale is applied before resizing and has no impact of the target width and height. | + + + +####Processing of image after resizing + +These options are executed *after* the image is resized. + +| Parameter | Explained | +|----------------|----------------------------------------------| | `sharpen` | Appy a filter that sharpens the image. | | `emboss` | Appy a filter with an emboss effect. | | `blur` | Appy a filter with a blur effect. | | `f, filter` | Apply filter to image, `f=colorize,0,255,0,0` makes image more green. Supports all filters as defined in [PHP GD `imagefilter()`](http://php.net/manual/en/function.imagefilter.php). | | `f0, f1-f9` | Same as `filter`, just add more filters. Applied in order `f`, `f0-f9`. | -| `p, palette` | Create a palette version of the image with up to 256 colors. | -| `sa, save-as` | Save resulting image as JPEG, PNG or GIF, for example `?src=river.png&save-as=gif`. | -| `nc, no-cache` | Do not use the cached version, do all image processing and save a new image to cache. | -| `so, skip-original`| Skip using the original image, always process image, create and use a cached version of the original image. | -| `v, verbose` | Do verbose output and print out a log what happens. Good for debugging, analyzing what happens or analyzing the image being processed. | -Combine the parameters to get the desired behavior and resulting image. For example, take the original image, resize it, apply a sharpen effect, save the image as JPEG and use quality 30. -`img.php?src=kodim13.png&w=600&sharpen&save-as=jpg&q=30` +####Saving image, affecting quality and filesize + +Options for saving the target image. + +| Parameter | Explained | +|----------------|----------------------------------------------| +| `q, quality` | Quality affects lossy compression and file size for JPEG images by setting the quality between 1-100, default is 60. Quality only affects JPEG. | +| `co, compress` | For PNG images it defines the compression algorithm, values can be 0-9, default is defined by PHP GD. Compress only affects PNG. | +| `p, palette` | Create a palette version of the image with up to 256 colors. | +| `sa, save-as` | Save resulting image as JPEG, PNG or GIF, for example `?src=river.png&save-as=gif`. | + Revision history ------------------------------------- + +v0.5 (2014-02-07) + +* Change constant name `CImage::PNG_QUALITY_DEFAULT` to `CImage::PNG_COMPRESSION_DEFAULT`. +* Split JPEG quality and PNG compression, `CImage->quality` and `CImage->compression` +* Changed `img.php` parameter name `d, deflate` to `co, compress`. +* Separating configuration issues from `img.php` to `img_config.php`. +* Format code according to PSR-2. +* Disabled post-processing JPEG and PNG as default. +* This version is supporting PHP 5.3, later versions will require 5.5 or later. +* Using GitHub issue tracking for feature requests and planning. +* Rewrote [the manual](http://dbwebb.se/opensource/cimage). + + v0.4.1 (2014-01-27) * Changed => to == on Modified-Since. @@ -145,5 +206,5 @@ v0.1 (2012-04-25)
  .   
-..:  Copyright 2012-2013 by Mikael Roos (me@mikaelroos.se)
+..:  Copyright 2012-2014 by Mikael Roos (me@mikaelroos.se)
 
diff --git a/cache/README.md b/cache/README.md deleted file mode 100644 index 928392a..0000000 --- a/cache/README.md +++ /dev/null @@ -1 +0,0 @@ -This directory must be writable by the webserver. diff --git a/img.php b/img.php deleted file mode 100644 index 3858295..0000000 --- a/img.php +++ /dev/null @@ -1,196 +0,0 @@ -img.php: Uncaught exception:

" . $exception->getMessage() . "

" . $exception->getTraceAsString(), "
"); -} -set_exception_handler('myExceptionHandler'); - - -// 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'; // Where is the class file -$pathToImages = __DIR__.'/img/'; // Where are the image base directory -$pathToCache = __DIR__.'/cache/'; // Where is the cache directory -$gridColumnWidth = 30; -$gridGutterWidth = 10; -$gridColumns = 24; -// settings for do not largen smaller images - -// settings for max image dimensions -$maxWidth = $maxHeight = 2000; -$maxScale = 400; - -// Set sizes to map constant to value, easier to use with width or height -$sizes = array( - 'w1' => 613, - 'w2' => 630, -); - -// Predefine some common aspect ratios -$aspectRatios = array( - '3:1' => 3/1, - '3:2' => 3/2, - '4:3' => 4/3, - '8:5' => 8/5, - '16:10' => 16/10, - '16:9' => 16/9, - 'golden' => 1.618, -); - - - -// 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); -$aspectRatio = isset($_GET['aspect-ratio']) ? $_GET['aspect-ratio'] : (isset($_GET['ar']) ? $_GET['ar'] : null); -$scale = isset($_GET['scale']) ? $_GET['scale'] : (isset($_GET['s']) ? $_GET['s'] : null); -$area = isset($_GET['area']) ? $_GET['area'] : (isset($_GET['a']) ? $_GET['a'] : null); -$crop = isset($_GET['crop']) ? $_GET['crop'] : (isset($_GET['c']) ? $_GET['c'] : null); -$quality = isset($_GET['quality']) ? $_GET['quality'] : (isset($_GET['q']) ? $_GET['q'] : null); -$deflate = isset($_GET['deflate']) ? $_GET['deflate'] : (isset($_GET['d']) ? $_GET['d'] : null); -$saveAs = isset($_GET['save-as']) ? $_GET['save-as'] : (isset($_GET['sa']) ? $_GET['sa'] : null); -$sharpen = isset($_GET['sharpen']) ? true : null; -$emboss = isset($_GET['emboss']) ? true : null; -$blur = isset($_GET['blur']) ? true : null; -$palette = isset($_GET['palette']) || isset($_GET['p']) ? true : false; -$verbose = isset($_GET['verbose']) || isset($_GET['v']) ? true : false; -$useCache = isset($_GET['no-cache']) || isset($_GET['nc']) ? false : true; -$useOriginal = isset($_GET['skip-original']) || isset($_GET['so']) ? false : true; -$keepRatio = isset($_GET['no-ratio']) ? false : (isset($_GET['nr']) ? false : (isset($_GET['stretch']) ? false : true )); -$cropToFit = isset($_GET['crop-to-fit']) ? true : (isset($_GET['cf']) ? true : false); - - - -// Check to replace predefined size -if(isset($sizes[$newWidth])) { - $newWidth = $sizes[$newWidth]; -} -if(isset($sizes[$newHeight])) { - $newHeight = $sizes[$newHeight]; -} - -// Check to replace predefined aspect ratio -$negateAspectRatio = ($aspectRatio[0] == '!') ? true : false; -$aspectRatio = $negateAspectRatio ? substr($aspectRatio, 1) : $aspectRatio; - -if(isset($aspectRatios[$aspectRatio])) { - $aspectRatio = $aspectRatios[$aspectRatio]; -} - -if($negateAspectRatio) { - $aspectRatio = 1 / $aspectRatio; -} - - - -// Add all filters to an array -$filters = array(); -$filter = isset($_GET['filter']) ? $_GET['filter'] : (isset($_GET['f']) ? $_GET['f'] : null); -if($filter) { $filters[] = $filter; } -for($i=0; $i<10;$i++) { - $filter = isset($_GET["filter{$i}"]) ? $_GET["filter{$i}"] : (isset($_GET["f{$i}"]) ? $_GET["f{$i}"] : null); - if($filter) { $filters[] = $filter; } -} - -// Santize and check domain for incoming parameters. (Move to CImage) -isset($srcImage) or errorPage('Must set src-attribute.'); -preg_match('#^[a-z0-9A-Z-/_\.]+$#', $srcImage) or errorPage('Filename contains invalid characters.'); -is_file($pathToImages . '/' . $srcImage) or errorPage('Imagefile does not exists.'); -is_writable($pathToCache) or errorPage('Cache-directory does not exists or is not writable.'); -is_null($quality) or ($quality > 0 and $quality <= 100) or errorPage('Quality out of range'); -is_null($deflate) or ($defalte > 0 and $deflate <= 9) or errorPage('Deflate out of range'); -is_null($scale) or ($scale >= 0 and $quality <= 400) or errorPage('Scale out of range'); -is_null($aspectRatio) or is_numeric($aspectRatio) or errorPage('Aspect ratio out of range'); - -// width -if($newWidth[strlen($newWidth)-1] == '%') { - is_numeric(substr($newWidth, 0, -1)) or errorPage('Width % out of range.'); -} -else { - is_null($newWidth) or ($newWidth > 10 && $newWidth <= $maxWidth) or errorPage('Width out of range.'); -} - -// height -if($newHeight[strlen($newHeight)-1] == '%') { - is_numeric(substr($newHeight, 0, -1)) or errorPage('Height % out of range.'); -} -else { - is_null($newHeight) or ($newHeight > 10 && $newHeight <= $maxHeight) or errorPage('Hight out of range.'); -} - - - - -// Display image if verbose mode -if($verbose) { - $query = array(); - parse_str($_SERVER['QUERY_STRING'], $query); - unset($query['verbose']); - unset($query['v']); - unset($query['nocache']); - unset($query['nc']); - $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, - 'aspectRatio' => $aspectRatio, - 'keepRatio' => $keepRatio, - 'cropToFit' => $cropToFit, - 'scale' => $scale, - 'area' => $area, - 'quality' => $quality, - 'deflate' => $deflate, - 'crop' => $crop, - 'filters' => $filters, - 'verbose' => $verbose, - 'useCache' => $useCache, - 'useOriginal' => $useOriginal, - 'saveAs' => $saveAs, - 'sharpen' => $sharpen, - 'emboss' => $emboss, - 'blur' => $blur, - 'palette' => $palette, -)); - diff --git a/webroot/img.php b/webroot/img.php new file mode 100644 index 0000000..67e81e7 --- /dev/null +++ b/webroot/img.php @@ -0,0 +1,431 @@ +img.php: Uncaught exception:

" . $exception->getMessage() . "

" . $exception->getTraceAsString(), "
"); +}); + + + +/** + * Get input from query string or return default value if not set. + * + * @param mixed $key as string or array of string values to look for in $_GET. + * @param mixed $default value to return when $key is not set in $_GET. + * + * @return mixed value from $_GET or default value. + */ +function get($key, $default = null) +{ + if (is_array($key)) { + foreach ($key as $val) { + if (isset($_GET[$val])) { + return $_GET[$val]; + } + } + } elseif (isset($_GET[$key])) { + return $_GET[$key]; + } + return $default; +} + + + +/** + * Get input from query string and set to $defined if defined or else $undefined. + * + * @param mixed $key as string or array of string values to look for in $_GET. + * @param mixed $defined value to return when $key is set in $_GET. + * @param mixed $undefined value to return when $key is not set in $_GET. + * + * @return mixed value as $defined or $undefined. + */ +function getDefined($key, $defined, $undefined) +{ + return get($key) === null ? $undefined : $defined; +} + + + +/** + * Log when verbose mode, when used without argument it returns the result. + * + * @param string $msg to log. + * + * @return void or array. + */ +function verbose($msg = null) +{ + global $verbose; + static $log = array(); + + if (!$verbose) { + return; + } + + if (is_null($msg)) { + return $log; + } + + $log[] = $msg; +} + + + +/** + * Default configuration options, can be overridden in own config-file. + */ +$config = array( + +); + +$configFile = __DIR__.'/'.basename(__FILE__, '.php').'_config.php'; +$config = array_merge($config, require $configFile); + +call_user_func($config['error_reporting']); + + + +/** + * verbose, v - do a verbose dump of what happens + */ +$verbose = getDefined(array('verbose', 'v'), true, false); + + + +/** + * src - the source image file. + */ +$srcImage = get('src') + or errorPage('Must set src-attribute.'); + +preg_match('#^[a-z0-9A-Z-/_\.]+$#', $srcImage) + or errorPage('Filename contains invalid characters.'); + +verbose("src = $srcImage"); + + + +/** + * width, w - set target width, affecting the resulting image width, height and resize options + */ +$newWidth = get(array('width', 'w')); + +// Check to replace predefined size +$sizes = call_user_func($config['size_constant']); +if (isset($sizes[$newWidth])) { + $newWidth = $sizes[$newWidth]; +} + +// Support width as % of original width +if ($newWidth[strlen($newWidth)-1] == '%') { + is_numeric(substr($newWidth, 0, -1)) + or errorPage('Width % not numeric.'); +} else { + is_null($newWidth) + or ($newWidth > 10 && $newWidth <= $config['max_width']) + or errorPage('Width out of range.'); +} + +verbose("new width = $newWidth"); + + + +/** + * height, h - set target height, affecting the resulting image width, height and resize options + */ +$newHeight = get(array('height', 'h')); + +// Check to replace predefined size +if (isset($sizes[$newHeight])) { + $newHeight = $sizes[$newHeight]; +} + +// height +if ($newHeight[strlen($newHeight)-1] == '%') { + is_numeric(substr($newHeight, 0, -1)) + or errorPage('Height % out of range.'); +} else { + is_null($newHeight) + or ($newHeight > 10 && $newHeight <= $config['max_height']) + or errorPage('Hight out of range.'); +} + +verbose("new height = $newHeight"); + + + +/** + * aspect-ratio, ar - affecting the resulting image width, height and resize options + */ +$aspectRatio = get(array('aspect-ratio', 'ar')); + +// Check to replace predefined aspect ratio +$aspectRatios = call_user_func($config['aspect_ratio_constant']); +$negateAspectRatio = ($aspectRatio[0] == '!') ? true : false; +$aspectRatio = $negateAspectRatio ? substr($aspectRatio, 1) : $aspectRatio; + +if (isset($aspectRatios[$aspectRatio])) { + $aspectRatio = $aspectRatios[$aspectRatio]; +} + +if ($negateAspectRatio) { + $aspectRatio = 1 / $aspectRatio; +} + +is_null($aspectRatio) + or is_numeric($aspectRatio) + or errorPage('Aspect ratio out of range'); + +verbose("aspect ratio = $aspectRatio"); + + + +/** + * crop-to-fit, cf - affecting the resulting image width, height and resize options + */ +$cropToFit = getDefined(array('crop-to-fit', 'cf'), true, false); + +verbose("crop to fit = $cropToFit"); + + + +/** + * no-ratio, nr, stretch - affecting the resulting image width, height and resize options + */ +$keepRatio = getDefined(array('no-ratio', 'nr', 'stretch'), false, true); + +verbose("keep ratio = $keepRatio"); + + + +/** + * crop, c - affecting the resulting image width, height and resize options + */ +$crop = get(array('crop', 'c')); + +verbose("crop = $crop"); + + + +/** + * area, a - affecting the resulting image width, height and resize options + */ +$area = get(array('area', 'a')); + +verbose("area = $area"); + + + +/** + * skip-original, so - skip the original image and always process a new image + */ +$useOriginal = getDefined(array('save-as', 'sa'), false, true); + +verbose("use original = $useOriginal"); + + + +/** + * no-cache, nc - skip the cached version and process and create a new version in cache. + */ +$useCache = getDefined(array('no-cache', 'nc'), false, true); + +verbose("use cache = $useCache"); + + + +/** + * quality, q - set level of quality for jpeg images + */ +$quality = get(array('quality', 'q')); + +is_null($quality) + or ($quality > 0 and $quality <= 100) + or errorPage('Quality out of range'); + +verbose("quality = $quality"); + + + +/** + * compress, co - what strategy to use when compressing png images + */ +$compress = get(array('compress', 'co')); + + +is_null($compress) + or ($compress > 0 and $compress <= 9) + or errorPage('Compress out of range'); + +verbose("compress = $compress"); + + + +/** + * save-as, sa - what type of image to save + */ +$saveAs = get(array('save-as', 'sa')); + +verbose("save as = $saveAs"); + + + +/** + * scale, s - Processing option, scale up or down the image prior actual resize + */ +$scale = get(array('scale', 's')); + +is_null($scale) + or ($scale >= 0 and $quality <= 400) + or errorPage('Scale out of range'); + +verbose("scale = $scale"); + + + +/** + * palette, p - Processing option, create a palette version of the image + */ +$palette = getDefined(array('palette', 'p'), true, false); + +verbose("palette = $palette"); + + + +/** + * sharpen - Processing option, post filter for sharpen effect + */ +$sharpen = getDefined('sharpen', true, null); + +verbose("sharpen = $sharpen"); + + + +/** + * emboss - Processing option, post filter for emboss effect + */ +$emboss = getDefined('emboss', true, null); + +verbose("emboss = $emboss"); + + + +/** + * blur - Processing option, post filter for blur effect + */ +$blur = getDefined('blur', true, null); + +verbose("blur = $blur"); + + + +/** + * filter, f, f0-f9 - Processing option, post filter for various effects using imagefilter() + */ +$filters = array(); +$filter = get(array('filter', 'f')); +if ($filter) { + $filters[] = $filter; +} + +for ($i = 0; $i < 10; $i++) { + $filter = get(array("filter{$i}", "f{$i}")); + if ($filter) { + $filters[] = $filter; + } +} + +verbose("filters = " . print_r($filters, 1)); + + + +/** + * Display image if verbose mode + */ +if ($verbose) { + $query = array(); + parse_str($_SERVER['QUERY_STRING'], $query); + unset($query['verbose']); + unset($query['v']); + unset($query['nocache']); + unset($query['nc']); + $url1 = '?' . http_build_query($query); + $log = htmlentities(print_r(verbose(), 1)); + echo <<$url1
+ +
$log
+EOD; +} + + + +/** + * Create and output the image + */ +require $config['cimage_class']; + +$img = new CImage(); + +$img->setVerbose($verbose) + ->setSource($srcImage, $config['image_path']) + ->setOptions( + array( + // Options for calculate dimensions + 'newWidth' => $newWidth, + 'newHeight' => $newHeight, + 'aspectRatio' => $aspectRatio, + 'keepRatio' => $keepRatio, + 'cropToFit' => $cropToFit, + 'crop' => $crop, + 'area' => $area, + + // Pre-processing, before resizing is done + 'scale' => $scale, + + // Post-processing, after resizing is done + 'palette' => $palette, + 'filters' => $filters, + 'sharpen' => $sharpen, + 'emboss' => $emboss, + 'blur' => $blur, + ) + ) + ->initDimensions() + ->calculateNewWidthAndHeight() + ->setSaveAsExtension($saveAs) + ->setJpegQuality($quality) + ->setPngCompression($compress) + ->useOriginalIfPossible($useOriginal) + ->generateFilename($config['cache_path']) + ->useCacheIfPossible($useCache) + ->load() + ->preResize() + ->resize() + ->postResize() + ->setPostProcessingOptions($config['postprocessing']) + ->save() + ->output(); diff --git a/img/ball24.png b/webroot/img/ball24.png similarity index 100% rename from img/ball24.png rename to webroot/img/ball24.png diff --git a/img/ball8.png b/webroot/img/ball8.png similarity index 100% rename from img/ball8.png rename to webroot/img/ball8.png diff --git a/img/car.png b/webroot/img/car.png similarity index 100% rename from img/car.png rename to webroot/img/car.png diff --git a/img/higher.jpg b/webroot/img/higher.jpg similarity index 100% rename from img/higher.jpg rename to webroot/img/higher.jpg diff --git a/img/kodim04.png b/webroot/img/kodim04.png similarity index 100% rename from img/kodim04.png rename to webroot/img/kodim04.png diff --git a/img/kodim07.png b/webroot/img/kodim07.png similarity index 100% rename from img/kodim07.png rename to webroot/img/kodim07.png diff --git a/img/kodim08.png b/webroot/img/kodim08.png similarity index 100% rename from img/kodim08.png rename to webroot/img/kodim08.png diff --git a/img/kodim13.png b/webroot/img/kodim13.png similarity index 100% rename from img/kodim13.png rename to webroot/img/kodim13.png diff --git a/img/kodim15.png b/webroot/img/kodim15.png similarity index 100% rename from img/kodim15.png rename to webroot/img/kodim15.png diff --git a/img/kodim22.png b/webroot/img/kodim22.png similarity index 100% rename from img/kodim22.png rename to webroot/img/kodim22.png diff --git a/img/kodim23.png b/webroot/img/kodim23.png similarity index 100% rename from img/kodim23.png rename to webroot/img/kodim23.png diff --git a/img/kodim24.png b/webroot/img/kodim24.png similarity index 100% rename from img/kodim24.png rename to webroot/img/kodim24.png diff --git a/img/round24.png b/webroot/img/round24.png similarity index 100% rename from img/round24.png rename to webroot/img/round24.png diff --git a/img/round8.png b/webroot/img/round8.png similarity index 100% rename from img/round8.png rename to webroot/img/round8.png diff --git a/img/wider.jpg b/webroot/img/wider.jpg similarity index 100% rename from img/wider.jpg rename to webroot/img/wider.jpg diff --git a/img_config.php b/webroot/img_config.php similarity index 78% rename from img_config.php rename to webroot/img_config.php index 4fd6fda..a06a238 100644 --- a/img_config.php +++ b/webroot/img_config.php @@ -11,9 +11,9 @@ return array( * Paths, where are all the stuff I should use? * Append ending slash on directories. */ - 'cimage_class' => __DIR__.'/CImage.php', + 'cimage_class' => __DIR__.'/../CImage.php', 'image_path' => __DIR__.'/img/', - 'cache_path' => __DIR__.'/cache/', + 'cache_path' => __DIR__.'/../cache/', @@ -94,27 +94,5 @@ return array( error_reporting(-1); set_time_limit(20); }, - - - - /** - * Set callable handler for exception handeler or set null to use default. - */ - 'exception_handler' => null, - /*'exception_handler' => function($exception) { - header("Status: 404 Not Found"); - die('404: ' . $msg); - },*/ - - - - /** - * Set callable handler for error page or set null to use default. - */ - 'error_page' => null, - /*'error_page' => function($msg) { - header("Status: 404 Not Found"); - die('404: ' . $msg); - },*/ ); diff --git a/test.php b/webroot/test.php similarity index 100% rename from test.php rename to webroot/test.php