Image Version Component
Generate image thumbnail versions dynamically from a source image. Also includes a helper to call the component and generate full HTML code to render the image in a view template.
Want to make multiple version of an image? Don't want to do it from a behavior or store any data in a database? This component (and optional companion helper) will let you generate thumbnails from source images (png/jpg/gif) on the fly. The component itself has a method that returns a path for you. The helper returns full HTML code to display an image in your view.
The great thing is the image version isn't generated every time the page loads. It will only be created when the source image is newer than the image version being requested...Or when you call the flushVersion() method and delete image versions.
This component and helper was inspired by the imagecache plugin that was cleverly crafted for Drupal. The code is much shorter and should hopefully provide a quick lightweight solution that will work for many situations.
Requirements: GD Library and php modules to work with GD. Most servers have this. I also tried to be as careful as possible to make it PHP4 compliant. There was one case in particular that I resisted the temptation to use a PHP5 specific function.
...Then the optional helper...
That should do it. Just include the component in your controller and/or the helper. You can include the helper without including the component, but you MUST have the component in the components folder. The helper will init it. I didn't want to write the code out twice... I could have just made it a helper strictly, but I also wanted flexibility.
The component and helper are both well commented, but basically you're passing in the source image path (anything from the webroot of your application) and then an array with X and Y sizes, then optionally, quality and if you want to crop or not (true/false). In the example above I also call the flushVersion() method which actually deletes the files I'm creating each time. I would recommend making such a call during development, or on a link to have perhaps a control for a visitor to clear the images.
The resizing is smart. It will fit the image to the dimensions unless you specify true to crop, then it will go by the largest dimension and crop the shorter. If the image version desired is square, it will also resize and crop appropriately.
The great thing is the image version isn't generated every time the page loads. It will only be created when the source image is newer than the image version being requested...Or when you call the flushVersion() method and delete image versions.
This component and helper was inspired by the imagecache plugin that was cleverly crafted for Drupal. The code is much shorter and should hopefully provide a quick lightweight solution that will work for many situations.
Requirements: GD Library and php modules to work with GD. Most servers have this. I also tried to be as careful as possible to make it PHP4 compliant. There was one case in particular that I resisted the temptation to use a PHP5 specific function.
/controllers/components/image_version.php
Download code
<?php
/** Image Version Component
*
* A custom component for automagically creating thumbnail versions of any image within your app.
* Example controller use:
* $images = $this->{$this->modelClass}->find('first');
* $this->set('clear', $this->ImageVersion->flushVersion($images['Piece']['file'], array(150, 75), true));
* $this->set('thumbnail', $this->ImageVersion->version($images['Piece']['file'], array(150, 75)));
* (that would clean out the entire folder 150x75 and then make a thumbnail again and return a path to $thumbnail for the view)
*
* @link http://www.concepthue.com
* @author Tom Maiaroto
* @modifiedby Tom
* @lastmodified 2008-09-25 01:00:00
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
class ImageVersionComponent extends Object {
/**
* Components
*
* @return void
*/
//var $components = array('Session');
var $controller;
/**
* Startup
*
* @param object $controller
* @return void
*/
function initialize(&$controller) {
$this->controller = $controller;
}
/**
* Returns a path to the generated thumbnail.
* It will only generate a thumbnail for an image if the source is newer than the thumbnail,
* or if the thumbnail doesn't exist yet.
*
* Note: Changing the quality later on after a thumbnail is already generated would have
* no effect. Original source images would have to be updated (re-uploaded or modified via
* "touch" command or some other means). Or the existing thumbnail would have to be destroyed
* manually or with the flushVersions() method below.
*
* @param $image String[required] Location of the source image.
* @param $size Array[optional] Size of the thumbnail. Default: 75x75
* @param $thumbQuality Int[optional] Quality of the thumbnail. Default: 85%
*
* @return String path to thumbnail image.
*/
function version($source=null, $thumbSize=array(75, 75), $thumbQuality=85, $crop=false) {
// if no source provided, don't do anything
if(empty($source)): return false; endif;
$webroot = new Folder(WWW_ROOT);
$this->webRoot = $webroot->path;
// set the size
$thumb_size_x = $thumbSize[0];
$thumb_size_y = $thumbSize[1];
// round the thumbnail quality in case someone provided a decimal
$thumbQuality = ceil($thumbQuality);
// or if a value was entered beyond the extremes
if($thumbQuality > 100): $thumbQuality = 100; endif;
if($thumbQuality < 0): $thumbQuality = 0; endif;
// get full path of source file (note: a beginning slash doesn't matter, the File class handles that I believe)
$originalFile = new File($this->webRoot . $source);
$source = $originalFile->Folder->path.DS.$originalFile->name().'.'.$originalFile->ext();
// if the source file doesn't exist, don't do anything
if(!file_exists($source)): return false; endif;
// get the destination where the new file will be saved (including file name)
$pathToSave = $this->createPath($originalFile->Folder->path.DS.$thumbSize[0].'x'.$thumbSize[1]);
$dest = $originalFile->Folder->path.DS.$thumb_size_x.'x'.$thumb_size_y.DS.$originalFile->name().'.'.$originalFile->ext();
// First make sure it's an image that we can use (bmp support isn't added, but could be)
switch($originalFile->ext()):
case 'jpg':
case 'jpeg':
case 'gif':
case 'png':
break;
default:
return false;
break;
endswitch;
// Then see if the size version already exists and if so, is it older than our source image?
if(file_exists($originalFile->Folder->path.DS.$thumb_size_x.'x'.$thumb_size_y.DS.$originalFile->name().'.'.$originalFile->ext())):
$existingFile = new File($dest);
if( date('YmdHis', $existingFile->lastChange()) > date('YmdHis', $originalFile->lastChange()) ):
// if it's newer than the source, return the path. the source hasn't updated, so we don't need a new thumbnail.
return substr(strstr($existingFile->Folder->path.DS.$existingFile->name().'.'.$existingFile->ext(), 'webroot'), 7);
endif;
endif;
// Get source image dimensions
$size = getimagesize($source);
$width = $size[0];
$height = $size[1];
// $x and $y here are the image source offsets
$x = NULL;
$y = NULL;
// don't allow new width or height to be greater than the original (Thanks to TimThumb for thinking about something I didn't)
if( $thumb_size_x > $width ) { $thumb_size_x = $width; }
if( $thumb_size_y > $height ) { $thumb_size_y = $height; }
// generate new w/h if not provided (cool, idiot proofing)
if( $thumb_size_x && !$thumb_size_y ) {
$thumb_size_y = $height * ( $thumb_size_x / $width );
}
elseif($thumb_size_y && !$thumb_size_x) {
$thumb_size_x = $width * ( $thumb_size_y / $height );
}
elseif(!$thumb_size_x && !$thumb_size_y) {
$thumb_size_x = $width;
$thumb_size_y = $height;
}
// If the thumbnail is square
if($thumbSize[0] == $thumbSize[1]) {
if($width > $height) {
$x = ceil(($width - $height) / 2 );
$width = $height;
} elseif($height > $width) {
$y = ceil(($height - $width) / 2);
$height = $width;
}
// else if the thumbnail is rectangular, don't stretch it
} else {
// if we aren't cropping then keep aspect ratio and contain image within the specified size
if($crop === false) {
$ratio_orig = $width/$height;
if ($thumb_size_x/$thumb_size_y > $ratio_orig) {
$thumb_size_x = $thumb_size_y*$ratio_orig;
} else {
$thumb_size_y = $thumb_size_x/$ratio_orig;
}
}
// if we are cropping...
if($crop === true) {
// Next 10 lines. Big thanks to: TimThumb script created by Tim McDaniels and Darren Hoyt with tweaks by Ben Gillbanks (http://code.google.com/p/timthumb/)
// I would reccommend TimThumb to anyone (and myself if I didn't have this thing nearly complete).
$cmp_x = $width / $thumb_size_x;
$cmp_y = $height / $thumb_size_y;
// calculate x or y coordinate and width or height of source
if ( $cmp_x > $cmp_y ) {
$width = round( ( $width / $cmp_x * $cmp_y ) );
$x = round( ( $width - ( $width / $cmp_x * $cmp_y ) ) / 2 );
}
elseif ( $cmp_y > $cmp_x ) {
$height = round( ( $height / $cmp_y * $cmp_x ) );
$y = round( ( $height - ( $height / $cmp_y * $cmp_x ) ) / 2 );
}
/////
}
}
switch($originalFile->ext()):
case 'png':
// Create PNG
if($thumbQuality != 0):
$thumbQuality = ceil(($thumbQuality / 10)); // 0-9 is the range for png
endif;
$new_im = ImageCreatetruecolor($thumb_size_x,$thumb_size_y);
$im = imagecreatefrompng($source);
imagecopyresampled($new_im,$im,0,0,$x,$y,$thumb_size_x,$thumb_size_y,$width,$height);
imagepng($new_im,$dest,$thumbQuality);
$outputPath = new File($dest);
$finalPath = substr(strstr($outputPath->Folder->path.DS.$outputPath->name().'.'.$outputPath->ext(), 'webroot'), 7);
break;
case 'gif':
// Create GIF
$new_im = ImageCreatetruecolor($thumb_size_x,$thumb_size_y);
$im = imagecreatefromgif($source);
imagecopyresampled($new_im,$im,0,0,$x,$y,$thumb_size_x,$thumb_size_y,$width,$height);
imagegif($new_im,$dest); // no quality setting
$outputPath = new File($dest);
$finalPath = substr(strstr($outputPath->Folder->path.DS.$outputPath->name().'.'.$outputPath->ext(), 'webroot'), 7);
break;
case 'jpg':
case 'jpeg':
default:
// Create JPG
$new_im = ImageCreatetruecolor($thumb_size_x,$thumb_size_y);
$im = imagecreatefromjpeg($source);
imagecopyresampled($new_im,$im,0,0,$x,$y,$thumb_size_x,$thumb_size_y,$width,$height);
imagejpeg($new_im,$dest,$thumbQuality);
$outputPath = new File($dest);
$finalPath = substr(strstr($outputPath->Folder->path.DS.$outputPath->name().'.'.$outputPath->ext(), 'webroot'), 7);
// PHP 5.3.0 would allow for a true flag as the third argument in strstr()...
// which would take out "webroot" so substr() wasn't required, but for the PHP 4 people...
break;
endswitch;
return $finalPath;
}
/**
* Deletes a single thumbnail or a directory of thumbnail versions created by the component.
* Useful during development, or when changing the crop flag or dimensions often to keep tidy.
* Maybe say a hypothetical CMS has an admin option for a user to change the thumbnail size of
* a profile photo...well, we might want to run this to clean out the old versions right?
* Or when a record was deleted containing an image that has a version...afterDelete()...
*
* @param $source String[required] Location of a source image.
* @param $thumbSize Array[optional] Size of the thumbnail. Default: 75x75
* @param $clearAll Boolean[optional] Clear all the thumbnails in the same directory. Default: false
*
* @return
*/
function flushVersion($source=null, $thumbSize=array(75, 75), $clearAll=false) {
if((is_null($source)) || (!is_string($source))): return false; endif;
$webroot = new Folder(WWW_ROOT);
// take off any beginning slashes (webroot has a trailing one)
if(substr($source, 0, 1) == '/'):
$source = substr($source, 1);
endif;
$pathToFile = $webroot->path . $source;
$file = new File($pathToFile);
//debug($file->Folder->path.DS.$thumbSize[0].'x'.$thumbSize[1].DS.$file->name);
// REMOVE THE FILE (doesn't matter if we remove the directory too later on)
if(file_exists($file->Folder->path.DS.$thumbSize[0].'x'.$thumbSize[1])):
if(unlink($file->Folder->path.DS.$thumbSize[0].'x'.$thumbSize[1].DS.$file->name)):
//debug('The file was deleted.');
else:
//debug('The file could not be deleted.');
endif;
endif;
// IF SPECIFIED, REMOVE THE DIRECTORY AND ALL FILES IN IT
if($clearAll === true):
if($webroot->delete($file->Folder->path.DS.$thumbSize[0].'x'.$thumbSize[1])):
//debug('All files in the folder: '.$file->Folder->path.DS.$thumbSize[0].'x'.$thumbSize[1].' have been deleted including the folder.');
else:
//debug('The folder: '.$file->Folder->path.DS.$thumbSize[0].'x'.$thumbSize[1].' and its files could not be deleted.');
endif;
endif;
return;
}
/**
* Pass a full path like /var/www/htdocs/app/webroot/files
* Don't include trailing slash.
*
* @param $path String[optional]
* @return String Path.
*/
function createPath($path = null) {
//$path = $this->webRoot . 'files' . DS . $path;
$directories = explode('/', $path);
$root = '';
// looks to see if a slash was included in the path to begin with and if so it removes it
if($directories[0] == '') {
array_shift($directories);
}
foreach($directories as $directory) {
if(!file_exists($root .DS. $directory)) {
mkdir($root .DS. $directory);
}
$root = $root .DS. $directory;
}
// put a trailing slash on
$root = $root . DS;
return $root;
}
}
?>
...Then the optional helper...
/views/helpers/image_version.php
Download code
<?php
/**
* Image Version Helper class to embed thumbnail images on a page.
*
* @link http://www.concepthue.com
* @author Tom Maiaroto
* @modifiedby Tom
* @lastmodified 2008-10-04 16:11:00
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
class ImageVersionHelper extends AppHelper {
var $helpers = array('Html');
var $component;
/**
* Returns a block of HTML code that embeds a thumbnail image into a page.
* It uses the built in CakePHP HTML helper image method for additional options.
*
* @param $image String[required] Location of the source image.
* @param $size Array[optional] Size of the thumbnail. Default: 75x75
* @param $thumbQuality Int[optional] Quality of the thumbnail. Default: 85%
* @param $options Object[optional] An array of options, same as Html->image() helper.
*
* @return HTML string including image tag and src attribute, along with any additional options.
*/
function version($image=null, $size=array(75, 75), $thumbQuality=85, $crop=false, $options=array()) {
// remove a slash if one was added accidentally. it doesn't matter either way now.
// we're always going from the webroot to cover any image in the cake app (typically).
if(substr($image, 0, 1) == '/'): $image = substr($image, 1); endif;
// init the component, if it hasn't been initialized
if(!$this->component):
$this->component =& ClassRegistry::init('ImageVersionComponent', 'Component');
endif;
$outputImage = $this->component->version($image, $size, $thumbQuality, $crop);
$link = $this->Html->image($outputImage, $options);
return $this->output("$link");
}
/**
* Deletes a single version thumbnail and/or deletes the entire directory of versions.
*
* @param $source String[required] Location of the source image.
* @param $size Array[optional] Image version.
* @param $clearAll Boolean[optional] Specify whether or not to remove all versions in a folder.
* @return
*/
function flushVersion($source=null, $size=array(75,75), $clearAll=false) {
if((is_null($source)) || (!is_string($source))): return false; endif;
// init the component, if it hasn't been initialized
if(!$this->component):
$this->component =& ClassRegistry::init('ImageVersionComponent', 'Component');
endif;
$flush = $this->component->flushVersion($source, $size, $clearAll);
return;
}
}
?>
That should do it. Just include the component in your controller and/or the helper. You can include the helper without including the component, but you MUST have the component in the components folder. The helper will init it. I didn't want to write the code out twice... I could have just made it a helper strictly, but I also wanted flexibility.
Example Component Usage
Download code
// Example controller method...don't forget to include the component.
var $components = array('ImageVersion');
function view() {
$image = $this->Model->find('first');
$clear = $this->ImageVersion->flushVersion($image['Model']['file'], array(65, 50), true);
$this->set('thumbnail', $this->ImageVersion->version($image['Model']['file'], array(65, 50)));
}
// Example view template
<img src="<?php echo $image; ?>" />
Example Helper Usage
Download code
// In a view template...I have a Model with a file column that has a path to the source image.
// Of course be sure to put in your controller var $helpers = array('ImageVersion');
<?php
$modelName = Inflector::singularize($this->name);
foreach (${strtolower($this->name)} as $entry):
// echo $entry[$modelName]['file'];
echo $imageVersion->flushVersion($entry[$modelName]['file'], array(75, 75));
echo $imageVersion->version($entry[$modelName]['file'], array(75, 75), 90, true);
endforeach;
?>
The component and helper are both well commented, but basically you're passing in the source image path (anything from the webroot of your application) and then an array with X and Y sizes, then optionally, quality and if you want to crop or not (true/false). In the example above I also call the flushVersion() method which actually deletes the files I'm creating each time. I would recommend making such a call during development, or on a link to have perhaps a control for a visitor to clear the images.
The resizing is smart. It will fit the image to the dimensions unless you specify true to crop, then it will go by the largest dimension and crop the shorter. If the image version desired is square, it will also resize and crop appropriately.
Comments
Question
1 Creating a transparent png thumbnail?
Any fix?
Comment
2 Thanks! Will fix.
Thanks for pointing that out...I'll figure it out and post a fix when I get a free moment. Should be under the " case 'png': " part somewhere, perhaps PHP had a simple setting. I know you can change the background color...but I don't immediately know if it can be transparent. I'm sure it can. More on this later...
edit: You can probably add something like the following to right above where the case for png creates the image.
imagealphablending($new_im, false);
imagesavealpha($new_im, true);
Comment
3 Note about GD and bad jpgs
To solve this, I had to change a php.ini setting:
gd.jpeg_ignore_warning
Had to be set to 1.
For shared hosting I know this may be problematic, perhaps try ini_set() in the component somewhere.
Otherwise, you'll have to take all those "bad" jpg files and re-save them in some image editing program that doesn't make a mess of them.
Comment
4 About webroot
When returning path, you may have trouble if you have your webroot defined somewhere else, i.e. "public_html" (for CPanel in my case) instead of 'webroot':
$finalPath = substr(strstr($outputPath->Folder->path.DS.$outputPath->name().'.'.$outputPath->ext(), 'webroot'), 7);
I corrected this using
where WEBROOT_URL is a custom defined$finalPath = substr(strstr($outputPath->Folder->path.DS.$outputPath->name().'.'.$outputPath->ext(), WEBROOT_URL), strlen(WEBROOT_URL));
varconstant by me, in all ocurrences.Anyway, great job, it works great!
cheers
Comment
5 A small note...
Comment
6 Headers already sent
I use Cake 1.2.2.8120....