diff --git a/CImage.php b/CImage.php index 4ffade1..87b7346 100644 --- a/CImage.php +++ b/CImage.php @@ -12,7 +12,9 @@ class CImage { * Properties */ private $image = null; // Object for open image - public $pathToImage; + public $imageFolder; // root folder of images + public $imageName; // image filename, may include subdirectory, relative from $imageFolder + public $pathToImage; // $imageFolder . '/' . $imageName; private $fileExtension; public $newWidth; public $newHeight; @@ -39,12 +41,15 @@ class CImage { /** * Constructor, can take arguments to init the object. * - * @param $pathToImage string the filepath to the image. - * @param $saveFolder string path to folder where to save the new file or null to skip saving. - * @param $newName string new filename or leave to null to autogenerate filename. + * @param string $imageName 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 $newName new filename or leave to null to autogenerate filename. */ - public function __construct($pathToImage=null, $saveFolder=null, $newName=null) { - $this->pathToImage = $pathToImage; + public function __construct($imageName=null, $imageFolder=null, $saveFolder=null, $newName=null) { + $this->imageName = ltrim($imageName, '/'); + $this->imageFolder = rtrim($imageFolder, '/'); + $this->pathToImage = $this->imageFolder . '/' . $this->imageName; $this->fileExtension = pathinfo($this->pathToImage, PATHINFO_EXTENSION); $this->saveFolder = $saveFolder; $this->newName = $newName; @@ -66,20 +71,24 @@ class CImage { */ public function CreateFilename() { $parts = pathinfo($this->pathToImage); - $crop = $this->cropToFit ? '_cf' : null; + $cropToFit = $this->cropToFit ? '_cf' : null; $crop_x = $this->crop_x ? "_x{$this->crop_x}" : null; $crop_y = $this->crop_y ? "_y{$this->crop_y}" : null; $quality = $this->quality == 100 ? null : "_q{$this->quality}"; + $crop = $this->crop ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y'] : null; $filters = null; - foreach($this->filters as $filter) { - if(is_array($filter)) { - $filters .= "_f{$filter['id']}"; - for($i=1;$i<=$filter['argc'];$i++) { - $filters .= ":".$filter["arg{$i}"]; + 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}"]; + } } } } - return $this->saveFolder . '/' . $parts['filename'] . '_' . round($this->newWidth) . '_' . round($this->newHeight) . $crop . $crop_x . $crop_y . $quality . $filters . '.' . $parts['extension']; + $subdir = str_replace('/', '-', dirname($this->imageName)); + return $this->saveFolder . '/' . $subdir . '_' . $parts['filename'] . '_' . round($this->newWidth) . '_' . round($this->newHeight) . $crop . $cropToFit . $crop_x . $crop_y . $quality . $filters . '.' . $parts['extension']; } @@ -111,7 +120,7 @@ class CImage { */ protected function Output($file) { $time = filemtime($file); - if(isset($_SERVER['If-Modified-Since']) && strtotime($_SERVER['If-Modified-Since']) >= $time){ + if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $time){ header("HTTP/1.0 304 Not Modified"); } else { header('Content-type: ' . $this->mime); @@ -120,7 +129,7 @@ class CImage { } exit; } - + /** * Open image. @@ -170,49 +179,65 @@ class CImage { * Calculate new width and height of image. */ protected function CalculateNewWidthAndHeight() { - // Only calculate new width and height if keeping aspect-ratio. + // Crop, use cropped width and height as base for calulations + $width = $this->width; + $height = $this->height; + if($this->crop) { + if(empty($this->crop['width'])) { + $this->crop['width'] = $this->width - $this->crop['start_x']; + } + if(empty($this->crop['height'])) { + $this->crop['height'] = $this->height - $this->crop['start_y']; + } + $width = $this->crop['width']; + $height = $this->crop['height']; + } + + // Calculate new width and height if keeping aspect-ratio. if($this->keepRatio) { - + // Both new width and height are set. if(isset($this->newWidth) && isset($this->newHeight)) { - - // Use newWidth and newHeigh as min width/height, image should fit the area. - if($this->cropToFit) { - $ratioWidth = $this->width/$this->newWidth; - $ratioHeight = $this->height/$this->newHeight; - $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight; - $this->cropWidth = $this->width / $ratio; - $this->cropHeight = $this->height / $ratio; - } - // Use newWidth and newHeigh as max width/height, image should not be larger. - else { - $ratioWidth = $this->width/$this->newWidth; - $ratioHeight = $this->height/$this->newHeight; - $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight; - $this->newWidth = $this->width / $ratio; - $this->newHeight = $this->height / $ratio; - } + $ratioWidth = $width / $this->newWidth; + $ratioHeight = $height / $this->newHeight; + $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight; + $this->newWidth = $width / $ratio; + $this->newHeight = $height / $ratio; } // Use new width as max-width elseif(isset($this->newWidth)) { - $factor = (float)$this->newWidth / (float)$this->width; - $this->newHeight = $factor * $this->height; + $factor = (float)$this->newWidth / (float)$width; + $this->newHeight = $factor * $height; } // Use new height as max-hight elseif(isset($this->newHeight)) { - $factor = (float)$this->newHeight / (float)$this->height; - $this->newWidth = $factor * $this->width; - } + $factor = (float)$this->newHeight / (float)$height; + $this->newWidth = $factor * $width; + } + + // Use newWidth and newHeigh as min 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 = $width / $ratio; + $this->cropHeight = $height / $ratio; + } } - // Do not keep aspect ratio, but both newWidth and newHeight must be set - else { - $this->newWidth = isset($this->newWidth) ? $this->newWidth : $this->width; - $this->newHeight = isset($this->newHeight) ? $this->newHeight : $this->height; - } + // Crop, ensure to set new width and height + if($this->crop) { + $this->newWidth = isset($this->newWidth) ? $this->newWidth : $this->crop['width']; + $this->newHeight = 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); + return $this; } @@ -225,7 +250,7 @@ class CImage { * @param boolean $keepRatio true to keep aspect ratio else false. Default is true. * @param boolean $cropToFit true to crop image to fit in box specified by $newWidth and $newHeight. Default is false. * @param integer $quality the quality to use when saving the file, range 0-100, default is full quality which is 100. - * @param array $crop. + * @param array/string $crop converts string of 1,2,3,4 to array 'width'=>1, 'height'=>2, 'start_x'=>3, 'start_y'=>4. * @param array $filter. */ public function ResizeAndOutput($args) { @@ -235,12 +260,19 @@ class CImage { 'keepRatio'=>true, 'cropToFit'=>false, 'quality'=>100, - 'crop'=>array('w'=>null, 'h'=>null, 'x'=>0, 'y'=>0), + 'crop'=>null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0), 'filters'=>null, ); + // Convert crop settins from string to array if(isset($args['crop']) && !is_array($args['crop'])) { - $args['crop'] = array(); + $pices = explode(',', $args['crop']); + $args['crop'] = array( + 'width' => $pices[0], + 'height' => $pices[1], + 'start_x' => $pices[2], + 'start_y' => $pices[3], + ); } // Convert filter settins from array of string to array of array @@ -262,13 +294,16 @@ class CImage { //echo "
" . print_r($args['filters'], true) . "
"; // 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]; } + //echo "
" . print_r($this, true) . "
"; // Init the object and do sanity checks on arguments $this->Init()->CalculateNewWidthAndHeight(); + //echo "
" . print_r($this, true) . "
"; // Use original image? if(is_null($this->newWidth) && is_null($this->newHeight)) { @@ -278,35 +313,75 @@ class CImage { // Check cache before resizing. $this->newFileName = $this->CreateFilename(); if(is_readable($this->newFileName)) { - $fileTime = filemtime($this->pathToImage); - $cacheTime = filemtime($this->newFileName); + $fileTime = filemtime($this->pathToImage); + $cacheTime = filemtime($this->newFileName); if($fileTime <= $cacheTime) { $this->Output($this->newFileName); } } - // Resize and output + // Resize and output and save new to cache $this->Open()->ResizeAndSave(); } + /** + * 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) { + //echo $width . "x" . $height . "
"; + $img = imagecreatetruecolor($width, $height); + +/* + if($this->fileExtension == 'png' || ($this->fileExtension == 'gif')) { + imagealphablending($img, false); + imagesavealpha($img, true); + $transparent = imagecolorallocatealpha($img, 255, 255, 255, 127); + imagefilledrectangle($img, 0, 0, $width, $height, $transparent); + } +*/ + return $img; + } + + /** * Resize, crop and output the image. * */ public function ResizeAndSave() { + + // Do as crop, take only part of image + if($this->crop) { + //echo "Cropping"; + $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) { + //echo "Crop to fit"; $cropX = ($this->cropWidth/2) - ($this->newWidth/2); $cropY = ($this->cropHeight/2) - ($this->newHeight/2); - $imgPreCrop = imagecreatetruecolor($this->cropWidth, $this->cropHeight); - $imageResized = imagecreatetruecolor($this->newWidth, $this->newHeight); + $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); - } else { - $imageResized = imagecreatetruecolor($this->newWidth, $this->newHeight); + } + + // as is + else { + $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopyresampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height); } + // Apply filters if(isset($this->filters) && is_array($this->filters)) { foreach($this->filters as $filter) { switch($filter['argc']) { @@ -342,10 +417,10 @@ class CImage { case 'png': // Scale quality from 0-100 to 0-9 and invert setting as 0 is best, not 9 - $quality = 9 - round(($imageQuality/100) * 9); + $quality = 9 - round(($this->quality/100) * 9); if (imagetypes() & IMG_PNG) { if($this->saveFolder) { - imagepng($imageResized, $this->newFileName, $this->quality); + imagepng($imageResized, $this->newFileName, $quality); } $imgFunction = 'imagepng'; } @@ -355,7 +430,7 @@ class CImage { break; } header('Content-type: ' . $this->mime); - header('Last-Modified: ' . gmdate("D, d M Y H:i:s",time()) . " GMT"); + header('Last-Modified: ' . gmdate("D, d M Y H:i:s", time()) . " GMT"); $imgFunction($imageResized); exit; } diff --git a/README.md b/README.md index 0bcbad3..98db3d2 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,23 @@ Image conversion on the fly using PHP ===================================== -The `CImage.php` contains a class that can resize and crop images and output them to -a webpage. The class has cache of generated images. +The `CImage.php` is a class that can resize and crop images and output them to +a webpage. The class preserves a cache of the generated images and responds with +HTTP 304 if the image has not changed. The file `img.php` uses `CImage.php` to resize images. It is a usecase on how to use -the class. +the class. `img.php` is useful for webpages which want to dynamically resize the images. -The file `test.php` has some testcases that show the results of `img.php` with different +The file `test.php` has testcases that show the results of `img.php` with different settings. -Start by reviewing the `test.php`, then have a look at `img.php` and finally go through -`CImage.php`. +The file `example.php` makes an example on how to use and integrate `img.php` with your +website and shows why this might be a handy tool for content providers. -Enjoy. +Start by reading the `example.php`, proceed by reviewing the `test.php`, then have a look +at `img.php` and finally go through `CImage.php`. + +Enjoy! Mikael Roos (mos@dbwebb.se) @@ -23,15 +27,24 @@ Revision history ToDo. -* crop * Pre-defined sizes. +* crop-to-fit, add parameter for offset x and y to enable to define which area is the +center of the image from which the crop is done. +* Show how to integrate with WordPress. +v0.3 (2012-08-28) +* Added crop. Can crop a area (`width`, `height`, `start_x`, `start_y`) from the original +image. +* Corrected to make the 304 Not Modified header work. +* Added `example.php`to walk through a real live example on how to use `img.php` in a +website. + v0.2 (2012-05-09) * Implemented filters as in http://php.net/manual/en/function.imagefilter.php -* Changed `crop` to `crop_to_fit`, woks the same way. -* Changed arguments to method and sends them in array. +* Changed `crop` to `crop_to_fit`, works the same way. +* Changed arguments and sends them in array. * Added quality-setting. * Added testcases for above. @@ -43,3 +56,6 @@ v0.1.1 (2012-04-27) v0.1 (2012-04-25) * Initial release after rewriting some older code I had lying around. + + . +..: Copyright 2012 by Mikael Roos (me@mikaelroos.se) diff --git a/img.php b/img.php index d10c388..e9c931c 100644 --- a/img.php +++ b/img.php @@ -1,23 +1,39 @@ 613, +); // Get input from querystring -$srcImage = isset($_GET['src']) ? $pathToImages . basename($_GET['src']) : null; -$newWidth = isset($_GET['width']) ? $_GET['width'] : (isset($_GET['w']) ? $_GET['w'] : null); -$newHeight = isset($_GET['height']) ? $_GET['height'] : (isset($_GET['h']) ? $_GET['h'] : null); -$keepRatio = isset($_GET['no-ratio']) ? false : true; -$cropToFit = isset($_GET['crop-to-fit']) ? true : false; -$crop = isset($_GET['crop']) ? $_GET['crop'] : (isset($_GET['c']) ? $_GET['c'] : null); -$quality = isset($_GET['quality']) ? $_GET['quality'] : (isset($_GET['q']) ? $_GET['q'] : 100); +$srcImage = isset($_GET['src']) ? $_GET['src'] : null; +$newWidth = isset($_GET['width']) ? $_GET['width'] : (isset($_GET['w']) ? $_GET['w'] : null); +$newHeight = isset($_GET['height']) ? $_GET['height'] : (isset($_GET['h']) ? $_GET['h'] : null); +$keepRatio = isset($_GET['no-ratio']) ? false : true; +$cropToFit = isset($_GET['crop-to-fit']) ? true : false; +$crop = isset($_GET['crop']) ? $_GET['crop'] : (isset($_GET['c']) ? $_GET['c'] : null); +$quality = isset($_GET['quality']) ? $_GET['quality'] : (isset($_GET['q']) ? $_GET['q'] : 100); + +// Check to replace area +if(isset($area[$newWidth])) { + $newWidth = $area[$newWidth]; +} // Add all filters to an array $filters = array(); @@ -29,14 +45,22 @@ for($i=0; $i<10;$i++) { } // Do some sanity checks -!preg_match('/^[\w-\.]+$/', $srcImage) or die('Filename contains invalid characters.'); -is_null($newWidth) or ($newWidth > 10 && $newWidth < 1000) or die('Width out of range.'); -is_null($newHeight) or ($newHeight > 10 && $newHeight < 1000) or die('Hight out of range.'); -$quality >= 0 and $quality <= 100 or die('Quality out of range'); +function errorPage($msg) { + header("Status: 404 Not Found"); + die('404: ' . $msg); +} + +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($newWidth) or ($newWidth > 10 && $newWidth <= $maxWidth) or errorPage('Width out of range.'); +is_null($newHeight) or ($newHeight > 10 && $newHeight <= $maxHeight) or errorPage('Hight out of range.'); +$quality >= 0 and $quality <= 100 or errorPage('Quality out of range'); // Create the image object -require(__DIR__.'/CImage.php'); -$img = new CImage($srcImage, $pathToCache); +require($cimageClassFile); +$img = new CImage($srcImage, $pathToImages, $pathToCache); $img->ResizeAndOutput(array('newWidth'=>$newWidth, 'newHeight'=>$newHeight, 'keepRatio'=>$keepRatio, 'cropToFit'=>$cropToFit, 'quality'=>$quality, 'crop'=>$crop, 'filters'=>$filters, diff --git a/test.php b/test.php index cf1caca..b4e7c29 100644 --- a/test.php +++ b/test.php @@ -9,10 +9,12 @@

Supported arguments throught the querystring are: