Sami Greenbury
Technology, Teaching & Travel

Magical Image Resizing URL’s with Zend_Framework (and cached for Apache)

As a lazy programmer I don’t like resizing images all the time. So I made this bit of code to let me write a URL including the file size and have the work done for me.

  • Written for Zend Framework 1.11.12
  • It will resize the image and save it into the correct location so that Apache will find it next time and not call the PHP.
  • You can specify the required width, height, both or neither.
  • If a width or height is given as 0 then the original dimension will be used.

Add some settings to application.ini

cache.publicDir = '/cache'
cache.dataDir = APPLICATION_PATH "/../public/cache"
data.dataDir = APPLICATION_PATH "/../public/data

Add a route in the Bootstrap

The regex router must match a URL containing the required file sizes, the file name and as many sub directories as thrown at it. It matches the following and sends them to the ThumbAction on FilesController:

  • /cache/100/200/file.jpg
  • /cache/100/200/subdir/file.jpg
  • /cache/100/200/another-subdir/file.jpg
$router->addRoute('file_resized_thumbnail', new Zend_Controller_Router_Route_Regex(
	'cache/([0-9]*)/([0-9]*)(.*/?)/([^/]*?)',
		array(
			'controller'	=> 'files',
			'action'		=> 'thumb',
		),
		array(
			1	=> 'width',
			2	=> 'height',
			3	=> 'subdir',
			4	=> 'file'
		),
		'/cache/%d/%d/%s/%s'
));

FilesController.php::ThumbAction()

/**
*	Loads the file, gets a resized version and saves it in a location identical to 
*	the current URL so Apache can find it next time.
**/
public function thumbAction()
{
	$this->view->layout()->disableLayout();
	$options = $this->_helper->container->get('options');
	
	$fileName = $this->getRequest()->getParam('file');
	$subdir = $this->getRequest()->getParam('subdir');
	$width = $this->getRequest()->getParam('width');
	$height = $this->getRequest()->getParam('height');
	
	$cacheUrlPrefix = $options['cache']['publicDir'];
	$cachePathPrefix = $options['cache']['dataDir'];
	$dataPathPrefix = $options['data']['dataDir'];
	
	$fileName = urldecode($fileName);
	$subdir = ($subdir == "") ? "" : urldecode($subdir);
	
	$fullPath = $dataPathPrefix.'/'.$subdir.$fileName;
	
	if(realpath($fullPath) == false){
		throw new Zend_Controller_Action_Exception('File not Found: '.$fullPath, 404);
	}
	$file = new Pata_File(array(
		'path'			=>	$fullPath
	));
	
	if ($file == false) {
		throw new Zend_Controller_Dispatcher_Exception('That file was not found in that subdir');
	}
	$cacheDir = $cachePathPrefix.'/'.$width.'/'.$height.$subdir;
	$cacheFile = $cacheDir.'/'.$fileName;
	if (file_exists($cacheDir) == false) {
		mkdir($cacheDir, 0777, true);
	}
	imagepng($file->getThumbnailPng($width, $height), $cacheFile);
	
	header('content-type: image/png');
	readfile($cacheFile);
}

library/Pata/File.php

/*
*	This is a model I use to make working with files a bit easier. Several of the functions are used by thumbAction but the code could be moved to the controller if you so desired.
*/
<?php
class Pata_File
{
	protected $path;
	protected $relativePath;
	protected $url;
	protected $image;
	protected $thumbUrl;
	protected $thumbnailDimensions = array();

	protected $thumbnailPng = array();
	
	protected $description;
	
	public function __construct($options)
	{
		if (isset($options['path'])) {
			$this->path = $options['path'];
		}
		if (isset($options['relativePath'])) {
			$this->relativePath = $options['relativePath'];
		}
		if (isset($options['url'])) {
			$this->url = $options['url'];
		}
		if (isset($options['thumbUrl'])) {
			$this->thumbUrl = $options['thumbUrl'];
		}
	}

	public function getName()
	{
		return basename($this->getPath());
	}
	
	public function getPath()
	{
		if ($this->path == null) {
			throw new Exception('No path has been set');
		}
		return $this->path;
	}
	
	public function getRelativePath()
	{
		return $this->relativePath;
	}
	
	public function getDir()
	{
		return substr($this->getPath(), 0, - strlen($this->getName()));
	}
	
	public function setDescription($description)
	{
		$this->description = $description;
	}
	
	public function getDescription()
	{
		return $this->description;
	}
	
	/**
	 * The URL to the actual file itself - not a page displaying it, see getPageUrl()
	 * @return type 
	 */
	public function getUrl($width = 800, $height = null)
	{
		if ($this->url == null) {
			throw new Exception('No url has been set');
		}
		$height = $height ?: round($width * 0.75, 0);
		return str_replace(array(':width:', ':height:'), array($width, $height), $this->url);
	}
	
	public function getThumbUrl($width = 120, $height = null)
	{
		if ($this->thumbUrl == null) {
			throw new Exception('No thumbUrl has been set');
		}
		$height = $height ?: round($width * 0.75, 0);
		return str_replace(array(':width:', ':height:'), array($width, $height), $this->thumbUrl);
	}

	public function setPath($new)
	{
		$this->path = $new;
	}
	
	public function setUrl($new)
	{
		$this->url = $new;
	}
	
	public function setThumbUrl($new)
	{
		$this->thumbUrl = $new;
	}
	
	public function setPageUrl($new)
	{
		$this->pageUrl = $new;
	}
	
	/**
	 *	Page Url is the URL to the page on which this image can be viewed
	 */
	public function getPageUrl()
	{
		if ($this->pageUrl == null) {
			throw new Exception('No pageUrl has been set');
		}
		return $this->pageUrl;
	}
	
	public function getExtention()
	{
		if (preg_match('/\.[A-z0-9]+$/', $this->getPath(), $ext)) {
			$ext = $ext[0];
			$ext = substr($ext, 1);
		} else {
			$finfo = new finfo(FILEINFO_MIME);
			$mime = $finfo->file($this->getPath());
			$mime = explode(";", $mime);
			$mime = $mime[0];
			$mimeToExt = array(
				"text/plain" => "txt",
			);
			if (isset($mimeToExt[$mime])) {
				$ext = $mimeToExt[$mime];
			} else {
				$ext = "";
			}
		}
		return $ext;
	}
	
	public function freeMemory()
	{
		imagedestroy($this->getImage());
		$this->img = null;
	}
	
	public function getImage()
	{
		if ($this->image == null) {
			switch(strtolower($this->getExtention())){
				case "jpg":
				case "jpeg":
					$this->img = imagecreatefromjpeg($this->getPath());
					break;
				case "png":
					$this->img = imagecreatefrompng($this->getPath());
					break;
				case "gif";
					$this->img = imagecreatefromgif($this->getPath());
					break;
				case "bmp";
					$this->img = imagecreatefrombmp($this->getPath());
					break;
				case ".txt";
				case "doc";
					$this->img = imagecreatefrompng(realpath(dirname(__FILE__).'/File/Images/thumb_doc.png'));
					break;
				case "xls";
					$this->img = imagecreatefrompng(realpath(dirname(__FILE__).'/File/Images/thumb_excel.png'));
					break;
				default;
					$this->img = imagecreatefrompng(realpath(dirname(__FILE__).'/File/Images/thumb_unknown.png'));
					break;
			}
		}
		return $this->img;
	}
	
	public function getThumbnailDimensions($width, $height, $return = 'both')
	{
		$cacheName = $width.'x'.$height.$return;
		if (isset($this->thumbnailDimensions[$cacheName])) {
			return $this->thumbnailDimensions[$cacheName];
		}
		
		if ($this->getType() == 'images') {
			$imgSize = getimagesize($this->getPath());
			$imgWidth = $imgSize[0];
			$imgHeight = $imgSize[1];
		} else {
			$img = $this->getImage();
			$imgWidth = imagesx($img);
			$imgHeight = imagesy($img);
		}
		if ($width > 0) {
			$thumbWidth = $width;
		} else {
			$thumbWidth = ($imgWidth / $imgHeight) * $height;
		}
		if ($height > 0) {
			$thumbHeight = $height;
		} else {
			$thumbHeight = ($imgHeight / $imgWidth) * $width;
		}
		
		$thumbWidth = floor($thumbWidth);
		$thumbHeight = floor($thumbHeight);
		$returnValue = null;
		if ($return == 'both') {
			$returnValue = array('width' => $thumbWidth, 'height' => $thumbHeight);
		} elseif ($return == 'width') {
			$returnValue = $thumbWidth;
		} elseif ($return == 'height') { 
			$returnValue = $thumbHeight;
		} else {
			throw new Exception($return . ' is not a valid parameter for return type for getThumbnailDimensions in Pata_File');
		}
		
		$this->thumbnailDimensions[$cacheName] = $returnValue;
		return $returnValue;
	}
	
	public function getThumbnailPng($width = 0, $height = 0)
	{
		if (!isset($this->thumbnailPng[$width.'x'.$height])) {
			$img = $this->getImage();
			$imgWidth = imagesx($img);
			$imgHeight = imagesy($img);
			$thumbTop = 0;
			$thumbLeft = 0;
			// If we have specified widths and heights, use them. Otherwise calculate them
			if ($width == 0 && $height == 0) {
				$thumbWidth = $imgWidth;
				$thumbHeight = $imgHeight;
			} else {
				$dimensions = $this->getThumbnailDimensions($width, $height);
				$thumbWidth = $dimensions['width'];
				$thumbHeight = $dimensions['height'];
			}
			// These default to not being changed
			$scaleWidth = $thumbWidth;
			$scaleHeight = $thumbHeight;

			if ($imgWidth > $imgHeight) {
				$scaleHeight = ($imgHeight / $imgWidth) * $thumbWidth;
				$thumbTop = ($thumbHeight - $scaleHeight) / 2;
			} elseif($imgWidth < $imgHeight) {
				$scaleWidth = ($imgWidth / $imgHeight) * $thumbHeight;
				$thumbLeft = ($thumbWidth - $scaleWidth) / 2;
			}
			
			$thumb = ImageCreateTrueColor($thumbWidth, $thumbHeight);
			// Fill the image with transparency
			imagesavealpha($thumb, true);
			$trans_colour = imagecolorallocatealpha($thumb, 0, 0, 0, 127);
			imagefill($thumb, 0, 0, $trans_colour);
	
			imagecopyresampled($thumb, $img, $thumbLeft, $thumbTop, 0, 0, $scaleWidth, $scaleHeight, $imgWidth, $imgHeight);
			imageinterlace($thumb, true);
			$this->thumbnailPng[$width.'x'.$height] = $thumb;
		}
		return $this->thumbnailPng[$width.'x'.$height];
	}
	
	function getType()
	{
		$types = $this->getFileTypeExtentions();
		foreach ($types as $type => $exts) {
			if (in_array(strtolower($this->getExtention()), $exts)) {
				return $type;
			}
		}
		return 'unknown';
	}
	
	function getFileTypeExtentions($type = null)
	{
		$types = array(
			'images'		=> array('jpg','jpeg','gif','png','tiff','bmp'),
			'documents'		=> array('doc', 'docx', 'txt', 'rtf', 'odt', 'pdf'),
			'spreadsheets'	=> array('xls'),
		);
		if ($type != null) {
			if (!isset($types[$type])) {
				throw new Exception('Pata_Gallery->getFileTypeExtentions() does not have a list of extentions for the "'.$type.'" type');
			}
			return $types[$type];
		}
		return $types;
	}
}
Tags:
Category:

Leave a Reply

Your e-mail address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.