CakePHP controller for Imagemagick

by c

URL Example:

http://yourcakedomain.com/images/thumbnail/100_1/example.jpg

This would load the source image (example.jpg), apply Imagick::thumbnailImage(100, 100, 1) to the file, then save it in a special cache directory.

You may also define your own methods within the controller.

Before images are loaded, _preimage_{method} functions are checked for existence. this allows you to change image paths and do any other preliminary data calls you may need.

Once the image is loaded into memory, _image_{method} functions are called.

One example included is _preimage_in(), which redefines the source image before anything is loaded by Imagick:

http://yourcakedomain.com/images/in/infile.gif/outfile.jpg

Another example is _image_addborder(), and would be called similarly:

http://yourcakedomain.com/images/addborder/10_10/example.jpg

Both image methods and filetypes are whitelisted, so make sure you update $whitelisted_* if you want access to new methods.

Lastly, you can stack methods as you wish:

http://yourcakedomain.com/images/thumbnail/100_1/addborder/10_10/example.jpg

But you may want to hide method arguments from public view with custom methods.

Add the following to your routes files to remove the view method from the URL:
config/routes.php

Router::connect('/images/*', array('controller' => 'images', 'action' => 'view'));

because /images/stuff is prettier than /images/view/stuff.

controllers/images_controller.php

<?php
/**
 * Imagemagick controller
 *
 * This controller processes images with PHP Imagick and serves up cached files.
 * It takes arguments as key/value pairs in URL, and supports custom methods.
 * 
 * _preimage_{method name}() and _image_{method name}() functions defined in
 * this class are given priority. If they don't exist, Imagick methods are
 * checked also.
 *
 * Example:
 * http://mysite.com/images/method/argument_argument/image.jpg
 *
 * Would first run
 * 	_preimage_method(argument, argument)
 * before the image is loaded, and then run
 * 	_image_method(argument, argument)
 * after the image is loaded.
 * 
 * Methods can be stacked as so:
 * http://mysite.com/images/method1/argument_argument/method2/argument_argument/image.jpg
 *
 * If desired, arguments can be hidden from public access with a custom method:
 * function _image_t () { $this->im('thumbnail',Array(100,1); }
 * Would allow for thumbnail creation with this URL:
 * http://mysite.com/images/t/_/image.jpg
 *
 *
 * PHP version 5 and Imagick library are required.
 *
 * Comments and bug reports welcome to http://awnist.com/
 *
 * Full list of Imagick methods: http://www.php.net/imagick
 * See also: http://cvs.horde.org/co.php/framework/Image/Image/imagick.php
 * 
 * Licensed under The MIT License
 *
 * TODO: - relative paths
 *       - restrict number of args
 *       - check and enforce argument integrity
 *       - allow for image methods with no args (use /method/_/ for now)
 *       - solve for long cache names
 *
 * @writtenby      Awnist
 * @lastmodified   Date: March 7, 2009
 * @license        http://www.opensource.org/licenses/mit-license.php The MIT License
 */ 

class ImagesController extends AppController {

var $uses = array();

/**
 * Full path to cache directory. Images should be cached after creation
 * due to the process-heavy nature of Imagemagick.
 *
 * @var string
 * @access public
 */
var $dir_cache = '/full/path/to/cache/';

/**
 * Full path to source image directory. This is not technically required
 * if you are supplying images programmatically, but it is convention
 * otherwise.
 *
 * @var string
 * @access public
 */
var $dir_images = '/full/path/to/source/images/';

/**
 * List of allowed methods to be called from URL. You can directly expose
 * Imagick methods by listing them here, but it's probably a better idea
 * to only expose custom methods that check and enforce argument ranges.
 *
 * @var array
 * @access public
 */
var $whitelist_methods = Array('in','thumbnail','addborder');

/**
 * Pipe delimited string of allowed image/file extensions. You can
 * technically read anything that Imagemagick can, but it's a good
 * idea to limit at this level for security's sake.
 *
 * @var string
 * @access public
 */
var $whitelist_types = 'gif|jpeg|jpg|png|bmp|tif';

/**
 * Full image URL to redirect to on fatal errors. This should be outside
 * of this controller's reach to prevent infinite loops.
 *
 * @var string
 * @access public
 */
var $image_error = 'http://mysite.com/global/images/nav-dot.gif';

/**
 * When this image is supplied as source image, don't load anything. This is for
 * custom methods that supply their own source image or create one programmatically.
 * Example: "/images/in/loadme.gif/image.jpg" would load "loadme.gif" and save as jpg.
 * "image.jpg" is the dummy image, only provided as a convenience to the browser. 
 *
 * @var string
 * @access public
 */
var $image_dummy = 'image.jpg';


/**
 * Character to split method arguments in URL.
 * Example: /resize/100_100/ would call the resize method with
 * (100,100) as arguments.
 *
 * @var string
 * @access public
 */
var $arg_delimiter = '_';


/**
 * Eventually contains the type of image that was requested (jpg, gif, etc.)
 *
 * @var string
 * @access public
 */
var $request_type = null;

/**
 * Eventually contains the file name of the source image (without path)
 *
 * @var string
 * @access public
 */
var $image_name = null;

/**
 * Eventually contains the full file path of the source image.
 *
 * @var string
 * @access public
 */
var $image_source = null;

/**
 * Eventually contains the file name of the cached image (without path)
 *
 * @var string
 * @access public
 */
var $cache_name = null;

/**
 * Eventually contains the full file path of the cached image.
 *
 * @var string
 * @access public
 */
var $cache_source = null;


function beforeFilter() {
	// Pull last element in url off as image. (foo/bar/image.jpg)
		$this->request_name = array_pop($this->params['pass']);

	// Check image suffix against whitelist
		if (!preg_match('/^.+\.('.$this->whitelist_types.')$/',$this->request_name,$matches))
			$this->_serve_error();

	// Set request_type to the requested image type
		$this->request_type = $matches[1];

	// If the image matches $image_dummy, we don't want to load the source image.
		if ($this->image_dummy != $this->request_name)
			$this->source_name = $this->request_name;
}

function view() {
	// Iterate through url arguments and make sure they're all whitelisted actions. 
		$p = &$this->params['pass'];
		$_actions = Array();

		// TODO: bug, this will break if current($p) = 0
		while ($u = current($p)) 
		{
			if (array_search($u, $this->whitelist_methods) !== false)
				$_actions[$u] = next($p);

			next($p);
		}

	// If there are no valid actions found for image, serve the original.
		if (!$_actions)
			$this->_serve_image($this->dir_images . $this->request_name);

	// Alphabetize array so files are cached uniformly if url args are out of order.
		$_cactions = $_actions;
		ksort($_cactions);

	// Run prefilters on image
		foreach($_actions as $k=>$v)
		{
			// Check for method _preimage_{function} in this controller
			if (method_exists($this, '_preimage_'.$k))
				call_user_func_array(array($this, '_preimage_'.$k), explode($this->arg_delimiter, $v));
		}

	// See if the source file has been set yet, and append directory if needed
		if (!$this->image_source && $this->source_name)
			{ $this->image_source = $this->dir_images . $this->source_name; }

	// Check if the requested source image exists
		if ($this->image_source && !file_exists($this->image_source))
			$this->_serve_image(false);

	// If cache name was not set in a _preimage method, generate it programmatically.
		if (!$this->cache_name)
			{ $this->cache_name = preg_replace('/[^\w]/', '', urldecode(http_build_query($_cactions, '', '_'))).'_'.$this->request_name; }
		else
			{
				// TODO: normalize user-supplied cache_name here
			}

	// Set full path to cache file
		if (!$this->cache_source)
			$this->cache_source = $this->dir_cache.$this->cache_name;

	// Decide if we should serve the cached file
		if(	(!$this->image_source &&
			file_exists($this->cache_source)) ||

			($this->image_source &&
			file_exists($this->cache_source) &&
			@filemtime($this->cache_source) > @filemtime($this->image_source))
		){
			$this->_serve_image($this->cache_source);
		}

	// Begin processing image with Imagemagick
		$this->imagick = new Imagick();

		if ($this->image_source)
			$this->im('readImage', $this->image_source);

	// Apply each of the requested methods.
		foreach($_actions as $k=>$v)
		{
			// check for method _image_{function} in this controller
			if (method_exists($this, '_image_'.$k))
				call_user_func_array(array($this, '_image_'.$k), explode($this->arg_delimiter, $v));

			// check for method Imagick::{function}Image()
			elseif (method_exists($this->imagick, $k.'Image'))
				$this->im($k.'Image', explode($this->arg_delimiter, $v));

			// check for method Imagick::{function}()
			elseif (method_exists($this->imagick, $k))
				$this->im($k, explode($this->arg_delimiter, $v));
		}

	// Set cache filename in Imagemagick
		$this->im('setImageFileName',$this->cache_source);

	// Set image format to the requested type
		$this->im('setFormat',$this->request_type);

	// Write to cache
		$this->im('writeImage');

	// Display the image
		header("Content-type: ".$this->im('getFormat'));
		echo $this->im('getimageblob');
		exit;
}

/**
 * Wrapper to perform safe Imagick methods with try and catch.
 *
 * Accepts alternate usage with multiple Imagick calls passed within
 * first argument as Array: Array('method'=>Array(args), 'method'=>Array(args),..)
 *
 * @param string $method Imagick method to run
 * @param array $args Args to pass into method
 * @access public
 */   
function im($method, $args=null)
{
	if (gettype($method) == 'array')
	{
		foreach ($method as $m=>$a) { $this->__im($m,$a); }
	}
	else
	{
		return $this->_im($method,$args);
	}
}

/**
 * Sub-wrapper that actually runs methods and traps errors. Should
 * not be used directly.
 * 
 * @param string $method Imagick method to run
 * @param array $args Args to pass into method
 * @access protected
 */
function _im($method, $args=null)
{
	if (!method_exists($this->imagick, $method))
		return;

	if (gettype($args) != 'array')
		$args = Array($args);

	try {
		return call_user_func_array(array($this->imagick, $method), $args);
	} catch (ImagickException $e) {
		$this->_serve_error();
	}
}

/**
 * Serves images from local filepaths.
 * 
 * @param string $image Full path of image to serve
 * @access protected
 */
function _serve_image($image=null) {
	if ($image && file_exists($image))
		{
			// Kludge
				$size	= getimagesize($image);
				$mime	= $size['mime'];
			$data = file_get_contents($image);
			header("Content-type: $mime");
			header('Content-Length: ' . strlen($data));
			echo $data;
			exit;
		}
	else
		{ $this->_serve_error(); }
}

/**
 * Redirects users to error image. 
 * 
 * @access protected
 */
function _serve_error()	{ $this->redirect($this->image_error); }



//----------------
// Begin custom image methods.
// These are called from the URL and applied to images.
//----------------

// Example custom _preimage method. This will grab a different source file.
// This technique is handy for converting files: /in/convert-me.gif/outfile.jpg
function _preimage_in ($image) { $this->source_name = $image; }

// Example custom _image method.
// Call it with /images/addborder/width_height/image.jpg
function _image_addborder ($border_width, $border_height) {
	$this->im('borderImage', Array(new ImagickPixel("white"),$border_width,$border_height));
}


}
?>