CakePHP Image Resizer
I'm currently in the process of building a new CMS in CakePHP and wanted to beef up the image capabilities so that I could upload an image once and then be able to resize the image on the fly by specifying a width/height.
I found a nice plain PHP script which does the job but I wanted something that works with CakePHP so I initially went the Helper route using this I found in the bakery. Although this is great I really wanted to resize images by changing values in the url and this is what I came up with.
Requirements
Instead of using a helper to resize my images I'm going to pass all the variables through the url and direct them to a special Controller that will resize the image, save the image in a cache directory and finally output the image to the screen.
// 'width' = width of resized image // 'height' = height of resized image // 'resized' = set to false if no resizing is required // 'url' = relative path to image from web root // sample image <img src='/images/view/width/height/resize/url' alt='Alt Tag' /> // another example <img src='/images/view/200/100/false/dir/image.jpg' alt='Alt Tag' />
By using an image resizer this way instead of a Helper I can put this img tag inside a CMS post/article/page and the image will be automatically resized and displayed. Another advantage is that other non-technical users can upload files and resize easily.
Images Controller
The Images controller has just one public method view() which accepts a number of paramters that are not defined when the method is executed. These parameters are then parsed so that the Image can be located and then resized.
In a nutshell the method first checks the file exists, if not a special error image is displayed. If the image exists the image size and mime type are collected and then some calculations are used to create a new width and height. With these a new filename is created which will be used to create a new image. A check is made on the new filename to see if it hasn't already been created. This will save us some time resizing the image and will work as a cache.
The cache directory incidentally is declared at the top of the class and must be a writable directory on the server. If the image needs to be resized then this done depending on the mimetype of the file and finally copied to the cache directory. Because I need to display the image I need to output a new header with the image mimetype and contents of the image as read from the file.
<?php
class ImagesController extends AppController {
var $name = 'Image';
var $uses = array();
// variables
var $cache_dir = 'img/cache';
var $error_img = 'img/cache/error.jpg';
var $types = array(1 => "gif", "jpeg", "png", "swf", "psd", "wbmp");
/**
* displays and resizes an image
* @width = width to resize image to
* @height = height to resize image to
* @resize = true/false
* @src = src dir of image from root
*/
function view() {
// get params
$width = $this->params['pass'][0];
$height = $this->params['pass'][1];
$noresize = $this->params['pass'][2];
$url = $this->_get_url($this->params['pass']);
// get full image path
$full_path = WWW_ROOT.$url;
// check file exists
if(file_exists($full_path)) {
// get size of image
$size = getimagesize($full_path);
// get mimetype
$mime = $size['mime'];
// if either width or height is an asterix
if($width == '*' || $height == '*') {
if($height == '*') {
// recalculate height
$height = ceil($width / ($size[0]/$size[1]));
} else {
// recalculate width
$width = ceil(($size[0]/$size[1]) * $height);
}
} else {
if (($size[1]/$height) > ($size[0]/$width)) {
$width = ceil(($size[0]/$size[1]) * $height);
} else {
$height = ceil($width / ($size[0]/$size[1]));
}
}
// include folder in filename
$dir_path = preg_replace("/[^a-z0-9_]/", "_", strtolower(dirname($url)));
$dir_path .= '-'.basename($url);
// create new file names
$file_relative = $this->cache_dir.'/'.$width.'x'.$height.'_'.$dir_path;
$file_cached = WWW_ROOT.$this->cache_dir.DS.$width.'x'.$height.'_'.$dir_path;
// if cached file already exists
if(file_exists($file_cached)) {
// get image sizes
$csize = getimagesize($file_cached);
// check that cached file is correct dimensions
$cached = ($csize[0] == $width && $csize[1] == $height);
// check file age
if (@filemtime($cachefile) < @filemtime($url))
$cached = false;
} else {
$cached = false;
}
// if file not cached
if(!$cached) {
$resize = ($size[0] > $width || $size[1] > $height) || ($size[0] < $width || $size[1] < $height);
} else {
$resize = false;
}
// do not resize if set to true
if($noresize == 'true') {
$resize = false;
$cached = false;
}
// if image resize is necessary
if($resize) {
// image
$image = call_user_func('imagecreatefrom'.$this->types[$size[2]], $full_path);
if (function_exists("imagecreatetruecolor") && ($temp = imagecreatetruecolor ($width, $height))) {
imagecopyresampled ($temp, $image, 0, 0, 0, 0, $width, $height, $size[0], $size[1]);
} else {
$temp = imagecreate($width, $height);
imagecopyresized($temp, $image, 0, 0, 0, 0, $width, $height, $size[0], $size[1]);
}
call_user_func("image".$this->types[$size[2]], $temp, $file_cached);
imagedestroy($image);
imagedestroy($temp);
} elseif(!$cached) {
// copy original file
copy($full_path, $file_cached);
}
// get file contents
$data = file_get_contents($file_cached);
} else {
$size = getimagesize($full_path);
$mime = $size['mime'];
$data = file_get_contents($full_path);
}
// set headers and output image
header("Content-type: $mime");
header('Content-Length: ' . strlen($data));
echo $data;
exit();
}
/**
* gets the url from the parameters
*/
function _get_url($params) {
// init
$url = '';
// unset unwanted params
unset($params[0], $params[1], $params[2]);
// loop through params
foreach($params as $p) {
$url .= $p.'/';
}
// remove last slash
$url = substr($url, 0, strrpos($url, '/'));
return $url;
}
}
?>
Wrapping Up
This has just been a quick article with a useful bit of CakePHP functionality that I've created very recently and I wish I developed something like this on my current blog, would of made things alot easier. Any problems or suggestions then drop me an email or leave a comment.
Comments
James (15/09/2008 - 04:50)
@Nd: hmm very good point, I suppose you could write in a small check to combat this and also include a check to see that the image is on the same domain as your site to stop external images being saved. Thanks for commenting.
Ricardas (25/02/2010 - 04:16)
Thats a fine resizer, thanks!
But the line
@filemtime($cachefile) should be replaced by
@filemtime($file_cached) i think.
Jpsy (03/04/2010 - 02:40)
Ricardas is absolutely correct. Without this correction the cache system is defunct and the image will be rerendered on each call.
Another bug effects the handling of non existend source images. In the current form the view returns a PHP warning if the source image does not exist. There is a definition of an $error_img in the class vars, but is not used, and there is a senseless block of code in the else clause that checks for image existence. Here is the patch to correct this:
Change this...
} else {
$size = getimagesize($full_path);
$mime = $size['mime'];
$data = file_get_contents($full_path);
}
...to that...
} else {
$size = getimagesize($this->error_img);
$mime = $size['mime'];
$data = file_get_contents($this->error_img);
}
The last change I did to my version is purely cosmetic and you can ignore this. To replace 4 lines of code with just one short line:
Change this...
// loop through params
foreach($params as $p) {
$url .= $p.'/';
}
// remove last slash
$url = substr($url, 0, strrpos($url, '/'));
...to that...
$url = implode('/', $params);
Cheers, Jörg.
James (03/04/2010 - 03:00)
@Jpsy: Thanks for the updated code!
Jpsy (03/04/2010 - 03:58)
@James: Nope, James. I have to thank YOU for this nice piece of code. I am especially thrilled by your elegant approach. It is just fun to work with because it integrates so easily into existing systems.