How dull would the web be without images? No photo sharing. No screenshots. And who would buy something without seeing what it looks like first? Big pictures are even better, but then you'll also need thumbnails which can be awkward to manage. However with Catalyst::View::Thumbnail, you can now produce them automatically with just a few lines of code.
As you'll have guessed from the name, Catalyst::View::Thumbnail is implemented as a view, so it fits in to the Catalyst MVC architecture like this:
Our Model will provide the source image. This could be from a database, a file, or even dynamically generated.
The job of the Controller is very simple. It takes the source image from the model and puts it into the stash, then sets the size of the thumbnail and any other options.
Finally, the View looks at the options on the stash, renders the thumbnail and serves it to the client.
The code examples in this article use a simple file-based model (described in the Notes section below), but can be easily adapted to use a database model instead.
Start by installing Catalyst::View::Thumbnail and create a thumbnail view:
script/myapp_create.pl view Thumbnail Thumbnail
Then place raw image data from your model into $c->stash->{image}, set
thumbnail options as shown and forward to View::Thumbnail.
Catalyst::View::Thumbnail gives you various choices of how your thumbnails will look. The simplest of these is to resize images to a given width or height.
Let's write a controller that produces thumbnails 100 pixels high. The width will be determined by the aspect ratio of the original image, i.e. if a source image measuring 1024x768 was resized to 100 pixels high, the thumbnail's width would be 133 pixels.
sub thumbnail :Local :Args(1) {
my ($self, $c, $filename) = @_;
$c->stash->{image} = $c->model('Images')->slurp($filename);
$c->stash->{y} = 100;
$c->forward('View::Thumbnail');
}
But what if we want our thumbnails to be a specific height and width? In this
case, just set both the x and y parameters. So to create a square
thumbnail, change your controller to this:
sub thumbnail :Local :Args(1) {
my ($self, $c, $filename) = @_;
$c->stash->{image} = $c->model('Images')->slurp($filename);
$c->stash->{x} = 100;
$c->stash->{y} = 100;
$c->forward('View::Thumbnail');
}
If your source image was rectangular, you'll notice that it has been cropped, rather than stretched, to make a square.
If you don't want the image to be cropped, but instead resized to fit within the x and y dimensions (while keeping the correct aspect ratio), then set the scaling option to fit.
sub thumbnail :Local :Args(1) {
my ($self, $c, $filename) = @_;
$c->stash->{image} = $c->model('Images')->slurp($filename);
$c->stash->{x} = 100;
$c->stash->{y} = 100;
$c->stash->{scaling} = 'fit';
$c->forward('View::Thumbnail');
}
In this example, our source image is resized to completely fit within a 100 pixel square, resulting in a thumbnail measuring 100px x 75px.
Thumbnails for photographic images often look better if they are slightly 'zoomed in', i.e. with part of the background removed to concentrate on the subject.
With Catalyst::View::Thumbnail this is easy to achieve - simply set the zoom
property in the stash to a value between 1 and 100. This controls the percentage
of the source image used to produce the thumbnail. A value of 100 (the default)
means to use the whole image, a value of 80 means to use the central 80%, and so on.
sub thumbnail :Local :Args(1) {
my ($self, $c, $filename) = @_;
$c->stash->{image} = $c->model('Images')->slurp($filename);
$c->stash->{x} = 100;
$c->stash->{y} = 100;
$c->stash->{zoom} = 80;
$c->forward('View::Thumbnail');
}
In the screenshot below you can see the effect of various levels of zoom.
The one problem with the examples so far is that we've hard-coded the thumbnail size. So when your designer asks for them to be a few pixels bigger, that means re-writing your app.
A more flexible approach would be to control the size and zoom level using parameters within the URL. We can implement this quite easily using a set of Catalyst chained actions:
sub load :Chained('/') :PathPart('thumbnail') :CaptureArgs(1) {
my ($self, $c, $filename) = @_;
$c->stash->{image} = $c->model('Images')->slurp($filename);
}
sub zoom :Chained('load') :CaptureArgs(1) {
my ($self, $c, $zoom) = @_;
$c->stash->{zoom} = $zoom;
}
sub x :Chained('zoom') :Args(1) {
my ($self, $c, $x) = @_;
$c->stash->{x} = $x;
$c->forward('View::Thumbnail');
}
sub y :Chained('zoom') :Args(1) {
my ($self, $c, $y) = @_;
$c->stash->{y} = $y;
$c->forward('View::Thumbnail');
}
sub s :Chained('zoom') :Args(1) {
my ($self, $c, $s) = @_;
$c->stash->{x} = $s;
$c->stash->{y} = $s;
$c->forward('View::Thumbnail');
}
The load method is called first, putting the source image onto the stash. The
second action sets the zoom level, and finally we branch off into three end actions
to set the size, either by width (x), height (y), or square (s).
The result is a URL structure like this:
.---------------------------+--------------------------. | Path Spec | Private | +---------------------------+--------------------------+ | /thumbnail/*/zoom/*/s/* | /image/load (1) | | | -> /image/zoom (1) | | | => /image/s | | /thumbnail/*/zoom/*/x/* | /image/load (1) | | | -> /image/zoom (1) | | | => /image/x | | /thumbnail/*/zoom/*/y/* | /image/load (1) | | | -> /image/zoom (1) | | | => /image/y | '---------------------------+--------------------------'
So your designer can now use a URL like http://localhost:3000/thumbnail/myimage.jpg/zoom/90/s/100 to produce a 100px square thumbnail at 90% zoom, then simply adjust it in the template if they need to update the design.
To get maximum performance from your application, it's a good idea to cache the output of your Thumbnail view, which will reduce the CPU overhead of creating new images on each request.
Thanks to a couple of great plugins, setting up caching in Catalyst is remarkably straight forward.
Firstly, install Catalyst::Plugin::Cache::FileCache and Catalyst::Plugin::PageCache from CPAN, and add these at the end of the plugin list in MyApp.pm.
use Catalyst qw/
ConfigLoader
Static::Simple
Cache::FileCache
PageCache
/;
Any controller can now tell Catalyst that its output should be cached like this:
$c->cache_page(3600); # cache for 1 hour (3600 seconds)
You can also specify default cache times, and automatically apply caching to a complete URL hierarchy via configuration options to PageCache - see the Catalyst::Plugin::PageCache documentation for details.
One thing I like to do is specify different cache times depending if the application is running in a development or live environment.
By using the following begin method in your controller, you can have a cache
which expires quickly when running on http://localhost:3000 for testing, and a
longer expiry time when your application is live.
sub begin :Private {
my ($self, $c) = @_;
if ($c->request->hostname eq 'localhost') {
$c->cache_page(30); # 30 seconds
} else {
$c->cache_page(24*60*60); # 24 hours
}
}
To run the examples, first install Catalyst::Devel and Catalyst::Model::File (see http://perl.jonallen.info/writing/articles/install-perl-modules-without-root for more information on installing modules).
Please note that Catalyst::View::Thumbnail requires Imager, which in turn requires libraries for whichever image formats you want to use (PNG, JPG, etc).
If you are running Linux, these libraries should be available as packages from your vendor. For example if you're using Ubuntu and want to handle PNG and JPG files you would need to install the packages libpng12-dev and libjpeg62-dev.
Alternatively you can compile the image libraries yourself or download pre-compiled binaries for many other operating systems - see the links below for more information:
When you've got the image libraries, install Catalyst::View::Thumbnail and then use the catalyst.pl script to set up a new Catalyst application.
catalyst.pl MyApp cd MyApp perl Makefile.PL
Copy an image file into the root/static/images folder and start the Catalyst development server:
script/myapp_server.pl -r -d
You can now browse to http://localhost:3000/static/images/myfile.jpg (or whatever your image file was called) to see the full-size picture.
For the model, we'll use Catalyst::Model::File to take image files directly from the root/static/images directory. Set up the model with the following command:
script/myapp_create.pl model Images File root/static/images
Finally, let's create our controller and view.
script/myapp_create.pl view Thumbnail Thumbnail script/myapp_create.pl controller Image
And that's it - you're ready to go through the examples! All of the sample methods will go into the Image controller - lib/MyApp/Controller/Image.pm - so you'll see the results at http://localhost:3000/image/thumbnail/myfile.jpg.
Jon Allen - freelance Perl & Catalyst developer, web designer, and technical manager.
Available for contract work and consultancy.
Contact jj@jonallen.info.
Follow me on Twitter at twitter.com/pennysarcade.