skip to content

System: Saving bandwidth with mod_rewrite and ImageMagick

 Tweet Share0 Tweets

We recently took over hosting of a website that we had built a few years ago for a client who then decided to host elsewhere. Ultimately that resulted in them losing all their data, but that's another story.

When discussing the transfer, the client mentioned that the two webcams displayed on the website (JPEG, 960 x 720 pixels, updated every 5 minutes) were now consuming up to 10Gb of bandwidth each month.

We first considered hosting those images on AmazonS3 ('the cloud'), but there are complications because it's still quite difficult to upload via FTP directly to their servers without a commercial client, especially from a Windows computer.

But it turns out that most of the traffic is from websites displaying (or hot-linking) the webcam images at much smaller sizes (e.g. 115 x 65 pixels, or 300 x 213 pixels) and not from people viewing the full-size images, so that suggested other solutions.

Using ImageMagick to resize images

ImageMagick is a fantastic command-line tool which lets you do just about anything with images. Most PHP programmers now rely on GD which is a shame because ImageMagick is generally more powerful and more efficient.

The command in ImageMagick for resizing an image to fit inside new (smaller) dimensions is as follows:

convert -geometry 'widthxheight>' oldimage newimage

This takes the image oldimage as the input, resizes it (but only making it smaller, never larger) to fit inside the new dimensions (width x height) and saves the output to newimage.

It's also possible to display the output directly rather than saving to a separate file:

convert -geometry 'widthxheight>' oldimage JPG:-

To call these functions from PHP we simply pass them to the exec or passthru functions.

PHP script for resizing images

The following script takes as parameters the path to the image and the desired maximum dimensions. It will output a JPEG image directly to the browser, but not save or cache the new image:

<?PHP if(!isset($imgpath) || !$imgpath) exit; if(!isset($width) || !$width = intval($width)) exit; if(!isset($height) || !$height = intval($height)) exit; $oldimg = escapeshellarg("{$_SERVER['DOCUMENT_ROOT']}/webcams/$imgpath.jpg"); header("Content-type: image/jpeg"); passthru("convert -geometry '{$width}x{$height}>' $oldimg JPG:-"); ?>

This is useful for testing, but not for a production website.

Rather than generating a new image for every request, it makes more sense to store the resized images and only generate a new version when the source image is updated:

<?PHP if(!isset($imgpath) || !$imgpath) exit; if(!isset($width) || !$width = intval($width)) exit; if(!isset($height) || !$height = intval($height)) exit; $CACHEPATH = "/tmp"; $oldimg = escapeshellarg("{$_SERVER['DOCUMENT_ROOT']}/webcams/$imgpath.jpg"); $newimg = escapeshellarg("$CACHEPATH/{$imgpath}_{$width}_{$height}.jpg"); if(!file_exists($newimg) || (filemtime($oldimg) > filemtime($newimg))) { exec("convert -geometry '{$width}x{$height}>' $oldimg $newimg"); } header("Content-type: image/jpeg"); readfile($newimg); ?>

This version of the script first checks to see whether there is a cached version of the image, and whether that version is newer than the source image. If so, the cached version is displayed.

Only when there is no cached version, or the cached version is older than the source image, do we generate a new version.

This script, which we save as /scripts/resize.php, assumes that the webcam images are uploaded at (for example):

http://www.example.net/webcams/cam3.jpg

and that the webserver has permission to write the resized versions to be cached at (for example):

/tmp/cam3_160_120.jpg

Using mod_rewrite to call the script

The following rewrite rule added to your .htaccess file:

RewriteRule ^webcams/(cam[0-9]+)_([0-9]+)_([0-9]+)\.jpg /scripts/resize.php?imgpath=$1&w=$2&h=$3

converts requests for:

http://www.example.net/webcams/cam1_160_120.jpg

into a request for our image resizing script:

http://www.example.net/scripts/resize.php?imgpath=cam1&width=160&height=120

Now the webcam images can be displayed at any (smaller) size using a simple URL and the output will be cached until the source images are updated.

This rewrite rule assumes that webcam images are stored in a directory /webcams/ and named cam1.jpg, cam2.jpg, etc.

Redirecting hot-linkers to smaller images

Finally, for this to make a difference, we need to prevent other websites linking directly to the full-size images unless they actually need them at that size. From our own website we don't want any restrictions.

The following rewrite rule does this for us:

RewriteCond %{HTTP_REFERER} !^http://www\.example\.net/ RewriteRule ^webcams/(cam[0-9]+)\.jpg /webcams/$1_160_120\.jpg [R]

Translation:

Now we can tell the sites hot-linking the webcam images to update their links to include the dimensions they need. For example, if they are displaying thumbs at only 115 x 65 pixels, they need to change the links as follows:

Before:

<a href="http://www.example.net/wecams/cam2.jpg"> <img src="http://www.example.net/wecams/cam2.jpg" width="115" height="65"> </a>

After:

<a href="http://www.example.net/wecams/cam2_960_720.jpg"> <img src="http://www.example.net/wecams/cam2_115_65.jpg" width="115" height="65"> </a>

They can also specify other sizes, all the way up to the original image size of 960 x 720 pixels. Until then all hot-linked images will display only the version at 160 x 120 pixels which we've set as the default. As long as the dimensions are specified the redirect will be bypassed.

This makes their pages load faster and saves us from serving all those extra gigabyes of image data.

Mission accomplished!

Note: While a version of this code is working in practice, some errors can sneak in during editing. Please let us know using the Feedback form below if you encounter any problems.

< System

Send a message to The Art of Web:


used only for us to reply, and to display your gravatar.

<- copy the digits from the image into this box

press <Esc> or click outside this box to close

Post your comment or question
top