Photo Galleries with Mason and Imager
by Casey West
|
Pages: 1, 2, 3, 4
The Images dhandler
You've probably guessed by now that we intend to use Mason to process images. Mason is well suited to outputting many forms of data, not just text, and we'll be exploiting that fact for our image gallery.
<%args>
$xsize => undef
$ysize => undef
</%args>
This component accepts two parameters that we've already described. $xsize is the maximum width an image can be, and $ysize is the maximum height an image can be.
<%flags>
inherit => undef
</%flags>
This is the important part. Because components have inheritance, the dhandler would normally inherit from the autohandler. That's bad news when the autohandler is tuned to sending out HTML and our dhandler is trying to send binary image data. Setting the inherit flag to undef tells Mason that the dhandler doesn't inherit anything, that it's responsible for its own output.
The only code remaining in this template resides in the <%init> block, so let's step through that now.
<%init>
$m->clear_buffer;
The very first thing we do is clear Mason's output buffer. This clears any headers that have already been built up in the buffer.
use Imager;
use File::Type;
Next we use the modules that will help scale the images, Imager and File::Type. Imager has already been discussed. File::Type uses magic to discover the type of files, and does so in a very memory-sensitive way.
my $send_img = sub {
$r->content_type( "image/$_[0]" );
$r->send_http_header;
$m->print($_[1]);
$m->abort(200);
};
This anonymous subroutine just encapsulates code being executed twice, as a means to remove duplication. It sets the HTTP Content-Type header to the image type passed as the first argument. Next it sends the HTTP header out. Then it sends the image data out, which is the second argument passed to this subroutine. Finally, it aborts execution with an HTTP 200 status code, everything is OK.
( my $file = $r->document_root . $r->uri ) =~ s/images/pictures/;
Discovering the proper file name for the image takes just a little work. After concatenating the document_root() with the uri(), we replace the images portion of the file path with pictures. Remember, none of the images are actually in the images directory.
my ($image, $type) = split /\//, File::Type->checktype_filename($file);
$type = 'png' if $type eq 'x-png';
With the knowledge of the proper file name, File::Type can figure out what type of file we have. This is more foolproof than attempting a guess based on filename extensions. As a minor oddity, File::Type returns a non-HTTP friendly $type for PNG images, so we need to fix that problem if it exists.
my $key = "$file|$xsize|$ysize";
if ( my $data = $m->cache->get( $key ) ) {
$send_img->($type, $data);
}
Generating scaled images from huge photos is a time-consuming function. It also has the potential to eat memory like a sieve. As a result, it's imperative that we take advantage of Mason's built-in caching functionality. The key for each entry in our cache must be unique for each file, and the dimensions we're trying to scale it to. Those three pieces of data will make up our $key. If data is returned from the cache using the $key, then the image data is sent and the request is immediately aborted. This is a quick short-circuit that allows us to grab an image from the cache and return it at the earliest possible moment. Later in the article you'll see how to set the data into the cache.
$m->abort(500) if $image ne 'image' || ! exists $Imager::formats{$type};
It's possible that the file being requested isn't an image. It's also possible that our installation of Imager doesn't support this type of image. If either of these conditions are true, we should abort immediately with a 500 HTTP status code, Internal Server Error.
my $img = Imager->new;
if ( $img->open(file => $file, type => $type) ) {
if ( $xsize ) {
$img = $img->scale( xpixels => $xsize )
unless $img->getwidth < $xsize;
}
if ( $ysize ) {
$img = $img->scale( ypixels => $ysize )
unless $img->getheight < $ysize;
}
my $img_data;
$img->write(data => \$img_data, type => $type);
$m->cache->set( $key => $img_data );
$send_img->($type, $img_data);
}
Now the heart and soul of image manipulation. The first step is to create a new Imager object. Next we try to open the image $file. If that succeeds, we can proceed to scaling the image.
When scaling, it's more important (to me) that the height of the image is exactly how I want it, so width is scaled first. Before the image is scaled its size is tested against the size of the image to be created. No scaling should occur if the image is smaller than the preferred size.
Once scaling has finished the image data can be extracted from the Imager object. When calling write() on the object we can pass a data option to let Imager write to a scalar reference. After the image data has been retrieved it is placed in the cache using the same $key that we first used when attempting to get information out of the cache. Finally, the image is sent out and the request is aborted.
warn "[$file] [$image/$type] " . $img->errstr;
$m->abort(500);
</%init>
In the event that Imager wasn't able to open the $file, the request should be aborted with a 500 HTTP status code, Internal Server Error. Before abortion, however, it would be useful to get some information in the error_log. The requested $file, its type information, and the error produced by Imager are all printed to STDOUT via warn.
What It Looks Like
For the less adventurous, yet overly curious members of the audience, a screenshot of our photo gallery follows.

As an aside, that image was originally much larger, but I really wanted it to be just 450 pixels wide. I don't have any image manipulation tools to do that job, but I do have Imager. Thanks to Imager, it took me 30 seconds to whip up the following command line snippet.
perl -MImager -le'Imager->new->open(file=>shift,type=>"jpeg")
->scale(xpixels=>450)
->write(file=>shift,type=>"jpeg")' figure_0.jpg figure_0_0.jpg
Conclusion
We've just created a photo gallery that takes all the hard work out of maintaining photo galleries. There's no need to pre-generate HTML or thumbnails. There's no web application interface so you don't have to change ownership of your gallery directory to the same user that Apache runs as. Using Mason's built-in caching, photo galleries are nearly as fast as accessing the data directly from the file system. Well, at least on the second request. Our galleries have paging and infinite sub-galleries. Most importantly, using Mason to its full potential has given us a fully customizable, very tiny web application that can be dropped into any existing web site or framework.
In fact, this code is the majority of the faceplant project. The source code can be downloaded from http://search.cpan.org/dist/faceplant. faceplant implements a few more features and is a bit more customizable. As such, its code is an excellent follow-up to this article. Go forth, now, and plant thy face on the Internet!

