Main Page Content
Php Frontend To Imagemagick
What is ImageMagick?
title="Download ImageMagick (Link opens in a new window)">ImageMagick isa powerful set of image manipulation utilities. It can read, write andmanipulate images in many image formats. It can resize, rotate, sharpen,color reduce or add any other special effect to your images. And, best of all,ImageMagick is directly available from the command line. In this article, we willwrite a script to make it available from the query string. You can then useit, for example, to automatically generate thumbnails of your images.What our script will do
We will write a script that we can copy-paste in a directory with images andthat enables us to use ImageMagick's href="http://www.imagemagick.com/www/convert.html" target="_new"title="Read more about convert (Link opens in a new window)">convertutility on each of the images in that directory. The script will enable us togiveconvert
commands by changing the query string.Maybe a simple example will better explain this idea. You've got an image:http://wwww.example.com/img/image.jpg
. You copy theImageMagick script magick.php
to the same directory. The image isnow also available ashttp://www.example.com/img/magick.php/image.jpg
. So far,your image hasn't changed. Now, imagine you want a thumbnail of the imagewith a width of exactly 200 pixels. You can get that image by requesting theurl:http://www.example.com/img/magick.php/image.jpg?resize(200)
. On receiving a request, the script will:- Parse the query string;
- Convert the query string to an ImageMagick command string;
- Run ImageMagick on the image;
- Send the modified image to the browser.
Commands
You can use the href="http://www.imagemagick.com/www/ImageMagick.html#opti" target="_new"title="A list of all ImageMagick commands (Link opens in a new window">standard commands/options of ImageMagick's convert utility. Thecommand is followed by the command's parameters. These parameters areenclosed in brackets. Multiple commands are separated by a plus sign. ImageMagick uses < and > in some parameters. You can't use these inhtml-documents. Instead of < and >, you may use { and } in your querystring. The scripts then converts { to < and } to >. Here are a few example convert commands and their query equivalent.Command line | Query string |
---|---|
-resize "100x150" | ?resize(100x150) |
-resize "800x600>" -roll "90" | ?resize(800x600})+roll(90) |
-flip -resize "800x600>" -flop | ?flip+resize(800x600})+flop |
Extra commands
The long list of ImageMagick commands didn't contain some things I wanted todo. I added three 'extra' commands to the script to do this.part
The first of these commands ispart(widthxheight)
. WithImageMagick's crop
command, it is possible to get a part of theimage. Unfortunately, this command only accepts absolute parameters. It cancrop w by h pixels, starting x pixels from the left andy pixels from the top of the image. But what if I want to get 100x100pixels from the center of the image? That's impossible if I don't know thesize of the image.Enter the part
command. It resizes the image to match eitherthe preferred width or the preferred height. Then it crops the image to getthe center part of that resized image. And that's what I wanted to do.colorizehex
ImageMagick'scolorize
command accepts only decimal RGBnumbers, on a 0 to 100 scale. To colorize with red gives colorize100/0/0
. This isn't ideal for web use, since html uses hex codes toidentify colors. The colorizehex(hex)
command does accept hex colors. It converts them to the ImageMagicknotation. Example: a red colorize is done withcolorizehex(FF0000)
.type
Thetype(type)
is available inImageMagick. It's just not a part of the commands, but is appended to thename of the output file (e.g. jpg:output.jpg
). I wanted toinclude it in the query string, so I made it a command. You can now convertthe image to jpeg by using type(jpg)
in your query. Before we start
There are just two minor points left before we can start coding.Do you have ImageMagick?
ImageMagick should be installed on your system before you can use it in yourscripts. This means you will either have to href="http://www.imagemagick.com/" target="_new"title="Download ImageMagick (Link opens in a new window)">installit yourself, or have your server admin do it for you.If your server is running PHP in href="http://www.php.net/manual/en/features.safe-mode.php" target="_new"title="Read more about safe mode (Links opens in a new window)">safemode, which it is likely to be if you're using a (free) shared host,your scripts don't have the right to execute shell commands. As this scriptruns ImageMagick as a shell command, you won't be able to use it. You coulda. ask your hosting provider to disable safe mode or b. use the href="http://www.evolt.org/article/Automated_Creation_of_Thumbnails_With_PHP/20/24498/index.html"title="Automated Creation of Thumbnails With PHP">GD library to generateyour images. ImageMagick is far more powerful than the GD library, but youcan use the latter even in safe mode.Why write your own script?
Directly running convert isn't the only way to use ImageMagick in your scripts.The title="Imagick, part of PEAR (Link opens in a new window)">Imagickmodule from the PEAR library,title="PerlMagick, for Perl (Link opens in a new window)">PerlMagick,a Perl interface to ImageMagick, can do this too. Then why bother and writeyour own script? Because it gives you a far more flexible system. You justenter your commands as the query string, and the script just sends them toImageMagick. The PEAR module, for instance, has a special PHP function foreach ImageMagick command. The script would have to translate the commands tothe corresponding functions, for which it would need an array with allpossible commands and functions. The direct method, withouth PEAR module, istherefore faster to write.The script
And, finally, here's the script that makes it all possible. If you copy allparts, you'll end up with one script. Place it in your image directory, andit's ready for use.Configuration
You can specify where your images are and where you want the script to cachethe processed images. It defaults to the current directory, which is probablywhere you want it. If theconvert
utility isn't available in thePATH environment variable of your web server, you'll need to specify the fullpath.<?php// location of source images (no trailing /)$image_path = '.';// location of cached images (no trailing /)
$cache_path = '.';// location of imagemagick's convert utility
$convert_path = 'convert';
Check input
The path and file name of the requested image is available as$_SERVER['PATH_INFO']
. We need to check that such information isgiven, and that the file exists.// first, check if an image location is givenif (!isset($_SERVER['PATH_INFO'])) { die('ERROR: No image specified.');}$image = $image_path.$_SERVER['PATH_INFO'];// next, check if the file exists
if (!file_exists($image)) { die('ERROR: That image does not exist.');}
Parse commands
We need a regular expression to parse the query string and extract commandsand parameters.// extract the commands from the query string// eg.: ?resize(....)+flip+blur(...)preg_match_all('/\+*(([a-z]+)(\(([^\)]*)\))?)\+*/', $_SERVER['QUERY_STRING'], $matches, PREG_SET_ORDER);We now have an array
$matches
. Each element in that array isanother array, with in the third element (position 2) the command name andon position 4 the parameters.The cache file name will contain the name of the original file. We then addthe commands and parameters to it, so we get an unique name for each versionof the image.// concatenate commands for use in cache file name$cache = $_SERVER['PATH_INFO'];foreach ($matches as $match) { $cache .= '%'.$match[2].':'.$match[4];}$cache = str_replace('/','_',$cache);$cache = $cache_path.'/'.$cache;$cache = escapeshellcmd($cache);
Run convert
Now that we have the cache file name, we can look if we already have a cachedversion of the requested image. If we do, we can just send that to thebrowser. If we don't, we will askconvert
to create it.We will add each command to the string $commands
. We will sendthat string to convert
to generate the image.if (!file_exists($cache)) { // there is no cached image yet, so we'll need to create it firstAfter we've checked the input and converted { to < and } to >, we willadd this command to the// convert query string to an imagemagick command string
$commands = ''; foreach ($matches as $match) { // $match[2] is the command name // $match[4] the parameter // check input if (!preg_match('/^[a-z]+$/',$match[2])) { die('ERROR: Invalid command.'); } if (!preg_match('/^[a-z0-9\/{}+-<>!@%]+$/',$match[4])) { die('ERROR: Invalid parameter.'); } // replace } with >, { with < // > and < could give problems when using html $match[4] = str_replace('}','>',$match[4]); $match[4] = str_replace('{','<',$match[4]);
$convert
string. But, since we used ourown, special commands, we will have to check if this command is one of them.If it is, we will have to do a bit more work.The colorizehex
is quite simple. We will convert hex to decimal,and then convert the 0-255 scale to ImageMagick's 0-100.// check for special, scripted commands switch ($match[2]) { case 'colorizehex': // imagemagick's colorize, but with hex-rgb colors // convert to decimal rgb $r = round((255 - hexdec(substr($match[4], 0, 2))) / 2.55); $g = round((255 - hexdec(substr($match[4], 2, 2))) / 2.55); $b = round((255 - hexdec(substr($match[4], 4, 2))) / 2.55); // add command to list $commands .= ' -colorize "'."$r/$g/$b".'"'; break;The
part
command requires more work. We first get the size of thesource image using the getimagesize()
function. After that welet ImageMagick resize the image to match either the new width or the newheight. We want one of the image's dimensions to be equal to the new size, andthe other one to exceed that size. We can then crop the image to the requestedsize.case 'part': // crops the image to the requested size if (!preg_match('/^[0-9]+x[0-9]+$/',$match[4])) { die('ERROR: Invalid parameter.'); }Thelist($width, $height) = explode('x', $match[4]);
// get size of the original $imginfo = getimagesize($image); $orig_w = $imginfo[0]; $orig_h = $imginfo[1]; // resize image to match either the new width // or the new height // if original width / original height is greater // than new width / new height if ($orig_w/$orig_h > $width/$height) { // then resize to the new height... $commands .= ' -resize "x'.$height.'"'; // ... and get the middle part of the new image // what is the resized width? $resized_w = ($height/$orig_h) * $orig_w; // crop $commands .= ' -crop "'.$width.'x'.$height. '+'.round(($resized_w - $width)/2).'+0"'; } else { // or else resize to the new width $commands .= ' -resize "'.$width.'"'; // ... and get the middle part of the new image // what is the resized height? $resized_h = ($width/$orig_w) * $orig_h; // crop $commands .= ' -crop "'.$width.'x'.$height. '+0+'.round(($resized_h - $height)/2).'"'; } break;
type
command is really simple. We can just save the type namefor now.case 'type': // convert the image to this file type if (!preg_match('/^[a-z]+$/',$match[4])) { die('ERROR: Invalid parameter.'); } $new_type = $match[4]; break;If this command isn't special, we can simply add the command and parameters tothe command string.
default: // nothing special, just add the command if ($match[4]=='') { // no parameter given, eg: flip $commands .= ' -'.$match[2].''; } else { $commands .= ' -'.$match[2].' "'.$match[4].'"'; } } }After we've run through the array we've got a list of commands in
$commands
. We can now run convert
.convert
needs the commands, the location of the source image andthe location of the output image to work. If a new file type is specified, weadd that type and a colon to the output file name.// create the convert-command $convert = $convert_path.' '.$commands.' "'.$image.'" '; if (isset($new_type)) { // send file type-command to imagemagick $convert .= $new_type.':'; } $convert .= '"'.$cache.'"';// execute imagemagick's convert, save output as $cache
exec($convert);}
Output
The$cache
variable should now point to the file containing therequested image. It was already cached or it was generated byconvert
. If the file exists, we can retrieve some informationabout that image to put in the http headers.// there should be a file named $cache nowif (!file_exists($cache)) { die('ERROR: Image conversion failed.');}We can now check if the browser sent us a If-Modified-Since header. This isused to update the browser cache. If the If-Modified-Since date of the browseris equal to the date the image was last modified, we don't have to send theimage again. The cache of the browser still has an updated version.// get image data for use in http-headers
$imginfo = getimagesize($cache);$content_length = filesize($cache);$last_modified = gmdate('D, d M Y H:i:s',filemtime($cache)).' GMT';// array of getimagesize() mime types
$getimagesize_mime = array(1=>'image/gif',2=>'image/jpeg',3=>'image/png', 4=>'application/x-shockwave-flash',5=>'image/psd', 6=>'image/bmp',7=>'image/tiff',8=>'image/tiff', 9=>'image/jpeg', 13=>'application/x-shockwave-flash', 14=>'image/iff');
// did the browser send an if-modified-since request?if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { // parse header $if_modified_since = preg_replace('/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE']);The browser does really want a (new) version of the image. We send some headersand then the image.Theif ($if_modified_since == $last_modified) {
// the browser's cache is still up to date header("HTTP/1.0 304 Not Modified"); header("Cache-Control: max-age=86400, must-revalidate"); exit; }}
Content-Type
header is a bit special. We have to send a MIMEcontent type, but the PHP getimagesize()
command only gives usthe number of the image type. With the $getimagesize_mime
arraywe can find the MIME type of that number. In case there is no number we usethe application/octet-stream
type. I haven't tested that, butit's probably better than text/html
. (Note: Starting withPHP 4.3, getimagesize()
does return a MIME type. I didn't use itto make the script compatible with older versions.)// send other headersheader('Cache-Control: max-age=86400, must-revalidate');header('Content-Length: '.$content_length);header('Last-Modified: '.$last_modified);if (isset($getimagesize_mime[$imginfo[2]])) { header('Content-Type: '.$getimagesize_mime[$imginfo[2]]);} else { // send generic header header('Content-Type: application/octet-stream');}// and finally, send the image
readfile($cache);?>
Concluding
If you copied the parts of the script and saved it in your image directory,it's ready for use. Just enter the url to the script, a slash, then the nameof your image and a query string. You should now get the image, modified tosuit your needs.For those of you who don't like to copy-paste: you can download the href="http://gvtulder.f2o.org/evolt/magick/magick.php.txt" target="_new"title="Download the full script (Link opens in a new window)">full script.Tip
Maybe you don't like the ugly.php
part inyour url (I don't). You can edit your Apache's configuration file, orplace a .htaccess file in your images directory and add the line:DefaultType application/x-httpd-phpYou can then rename the script to something without
.php
(ie. justmagick
). The url is now much nicer.