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:

Using WYMEditor with Dojo (by including jQuery when needed)

I’ve decided to use Dojo for a project – to test the waters. However I’d like to still use WYMEditor. WYMEditor requires jQuery internally as well as for it’s handy jQuery plugin. Obviously I don’t want to always load jQuery in addition to Dojo for the few pages it’s required on, so here’s the solution I’ve come up with to dynamically load jQuery and any other required files using a DeferredList.

I have this as a function to which I pass a form name; in Dojo I then check whether there are any .richTextEditor elements and begin the initialisation if required.

I guess it would be good to wrap this into a dojo module of some sort.

Versions:

  • Dojo version: 1.7.2
  • jQuery: 1.7.1
  • WYMEditor: 1.0.0a5
require(['dojo/DeferredList', 'dojo/io/script'], function(DeferredList, script) {
	// We don't want to include things twice because it's bad and breaks WYMEditor.
	var requireds = [];
	if (typeof jQuery == "undefined") {
		requireds.push(script.get({ url: '/js/jquery/jquery-1.7.1.min.js', checkString: 'jQuery' }));
	}
	// If you wanted, it would not be a bad idea to check for each plugin individually.
	if (typeof WYMeditor == "undefined") {
		requireds.push(script.get({ url: '/js/jquery/wymeditor/jquery.wymeditor.js', checkString: 'WYMeditor' }));
		requireds.push(script.get({ url: '/js/jquery/wymeditor/plugins/hovertools/jquery.wymeditor.hovertools.js', checkString: 'WYMeditor.editor.prototype.hovertools' }));
	};

	var dl = new DeferredList(requireds);
	dl.then(function(res){
		jQuery(function(){
			// Fill in your options here
			var editorOptions = {};
			jQuery('.richTextEditor').wymeditor(editorOptions);
			// You might also want to add the wymupdate class to your submit button here
		});
	});
});
Tags:
Categories:

Scan Text for Postcodes and Plot Them on a Map

I’ve often been frustrated when sites list addresses (such as store locations) without providing a map to help you visually see the closest one to you, or the best one to stop by on your way somewhere.

So I dreamed up a tool to parse a web page for postcodes and plot them, this afternoon my brother asked me for one so I finally got around to making one.

It is here: http://www.pata.cat/maps/postcode-picker

Comments, ideas and feedback are welcome. The source is also yours on request – it’s a part of a bigger Zend Framework project and a little finickety to share easily.

Category:

Installing PEAR, PHPUnit and Selenium on Windows

I wrote this for use at work, hence some of the wording. I may re-edit it one day, but it should do the trick. Feel free to leave any comments or questions below.

Installing PHPUnit on Windows is done with PEAR.

Installing PEAR

  1. Create the folder c:\program files\PEAR (This is your PEAR path)
  2. Download go-pear.phar from here: http://pear.php.net/go-pear.phar (the one which comes with PHP is usually broken) and save it in your PEAR path.
  3. Open a command prompt and navigate to your new PEAR folder, type php go-pear.phar
  4. Hit Enter to agree to a System install then Enter again to confirm all the settings are good (after checking them, of course).
  5. PEAR is now installed. When we use PEAR to install things, it creates a .bat files in our PEAR directory which can is used to run them. For example by the end of this page PHPUnit.bat will be installed and we can call it from the command line as ‘phpunit’ since windows is clever like that.

To be able to run these commands from outside of the PEAR directory we must add the PEAR directory to our PATH Environment Variable.

  1. Press Windows Key and Pause/Break to load the System screen.
  2. On the left click on Advanced System Settings.
  3. Click on the Advanced tab then “Environment Variables..” button at the bottom.
  4. In the bottom list, “System Variables” find and select the PATH variable and click Edit.
  5. Add a semi colon to the end of the list, and enter the path to your PEAR directory.
  6. Click OK and then OK again.

You will need to open a new CMD window if you already have one open so that you get one with the new environment variables available.

  1. Run “pear upgrade” to test and get the latest version in one handy step.

Installing PHPUnit

Navigate to your PEAR folder and run the following commands

  1. pear config-set auto_discover 1
  2. pear install --alldeps pear.phpunit.de/PHPUnit

PEAR creates a file called PHPUnit.bat, which we can run from anywhere because we put our PEAR directory into our PATH variable in the first step. It’s also created a bunch of other files which PHPUnit.bat uses.

You may wish to add C:\Program Files\PEAR\pear\PHPUnit to your php.ini include_path.

Installing Selenium for Integration Testing

Our Integration Tests use Selenium and involve launching a browser and running through things, they take siginificantly longer to run than Unit Tests and you’ll probably not run them every day. They’re useful when you’re changing the back end but not the UI, and will  need updating if you’re making changes to the UI. They also pick up JavaScript errors.

Selenium is a server to which PHPUnit will connect and issue commands, the server then launches a browser and tells it what to do.

  1. You need Java installed, take the Offline version from here: http://java.com/en/download/manual.jsp
  2. Download Selenium Server here: http://seleniumhq.org/download/ Create a folder and save this in c:\program files\selenium\
  3. Right click on your desktop, go to New > Shortcut. For Location simply type “Java”, hit Next and type Selenium Server as the name.
  4. Right click your new shortcut and click Properties, at the end of the “Target” field add -jar “C:\Program Files (x86)\Selenium\selenium-server-standalone-2.23.1.jar”
  5. Running this shortcut should give you a console screen with happy output.
  6. To install the PHPUnit Selenium extensions (to be able to run the Integration Tests) download this file (or check for a later version of PHPUnit_Selenium from here) and merge the PHPUnit_Selenium-1.2.7\PHPUnit\Extentions folder with it’s sister in your PEAR Directory under pear\PHPUnit

Note that we are using Selenium 2, so you must use the Selenium2 classes.

Running the tests

Running the tests should take a matter of seconds and are run from the command line. From within the Squadify code base cd into the tests folder, type “phpunit” and hit enter, you should see something like:

PHPUnit 3.5.15 by Sebastian Bergmann.
.... Errors Come out here ...
Time: 0 seconds, Memory: 5.25Mb

PHPUnit looks for a file called phpunit.xml from which it learns how you with the tests to run. There are command line options for running only sub sets of the tests specified, controlling the output and other cool things.

Writing or Editing Tests

Tests are seperated into TestCases, each TestCase will test one unit. Examples of units are models, pages and forms. Refer to PHPUnit’s documentation for specific details on how to write tests.

It’s very easy to write useless tests, some pointers to avoid doing so:

  • Before writing tests, write a Doc Comment description before the Test Case explaining why the test exists.
  • Write descriptive names for tests, similar to a Doc Comment these force you to think about what and why you’re testing.
  • You’re supposed to write tests that fail, this means running it so you can see it fail, then fix the code to make it pass. It’s important to remember that you must change the code being tested, not the test itself, for this.

For the sake of maintainability put as much care into reusing your test cases code as any other code, without violating these:

  • TestCases should never be dependant on another Test Case, individual tests can be when it makes lots of sense but it should be avoided. The reason is so that (for speed) you run a subset of the tests (hence the name Unit Test).
  • Factory methods (such as getEmptyUser() or getYoungUser()) should be begin with “get” and placed at the bottom of the class. They’re used to make maintaining easier.

More Information

PHP Unit Docs: http://www.phpunit.de/manual/3.2/en/
Advice on writing tests: http://blog.stevensanderson.com/2009/08/24/writing-great-unit-tests-best-and-worst-practises/

Tags:
Category: