SCALING IMAGES ON DEMAND
OVERVIEW
If a website gets its material from various sources or must display variable-sized items in a uniform way you will need some kind of image scaler. Sending the high resolution images to the browser is not wise and consumes too much bandwidth. Scaling images in advance to every required size could be a way that works if you have enough resources for this preparation.
Assuming your website does not have too much traffic, you might think about scaling images on-demand and keeping the scaled version for future requests. This makes you flexible, saves bandwidth on transmission and only consumes valuable CPU cycles on initial requests for a given image and size.
When I first came across a request for scaling images, I could not find a library that suited my needs. So I decided to roll my own. I called it Catalyst::Controller::Imager as it internally uses Imager as its engine.
Today's article will show some tricks you might do with this little module.
Prepare your system
Imager uses several libraries written in C for reading, manipulating and writing images. Depending on the formats you like to support more or less libraries are needed. Imager is very flexible in its configuration. Simply run Makefile.PL by hand after downloading Imager and you will get an idea of what I am talking about...
For a typical web scenario you will probably need to install libgif
,
libpng
, libjpeg
and libtif
. The packages for Ubuntu Linux are named
libgif4
, libpng12-0
, libjpeg8
and libtiff4
. For compiling Imager
you will have to install the "-dev" versions of these libs as well.
For Mac OS-X I recommend using MacPorts and
installing the packages jpeg
, giflib@4.2.1+no_x11
, tiff
and libpng
.
The command you will need to make a friend is named port.
After that, Imager
and Catalyst::Controller::Imager
should install
without any complains.
Create a simple app
$ catalyst.pl ImageDemo $ cd ImageDemo $ ./script/imagedemo_create.pl controller Image Imager $ mkdir root/cache $ ./script/imagedemo_server.pl -d -r -f
And for testing, please put some images of any size into root/static/images
Explore the default options
Assumed you have an original image located in root/static/images/house.jpg you can try the following URLs:
- the original image:
-
http://localhost:3000/static/images/house.jpg
- a thumbnail (80x80)
-
http://localhost:3000/image/thumbnail/house.jpg
- width 100 pixels
-
http://localhost:3000/image/w-100/house.jpg
- height 100 pixels
-
http://localhost:3000/image/h-100/house.jpg
- height 10000 pixels
-
http://localhost:3000/image/h-10000/house.jpg
oops. we get an error. See Controller::Image in your app to find out why
- width 200 pixels and height 100 pixels
-
http://localhost:3000/image/w-200-h-100/house.jpg
Extending the logic behind the scenes
Until now, you did not touch the auto-generated Controller::Imager in your application. Every URL you called could get serviced from the logic of the base class.
The whole logic inside the base class operates in these stages:
- collecting
-
The URL is split into parts. The first part after the namespace contains a dash-separated list of keywords with optional parameters. Several keywords with their parameters may exist and are again separated by a single dash.
For every keyword a method named like the keyword prefixed by
want_
must exist.During URL traversal, all
want_xxx
methods are called. Each of these methods may add things to stash variables, typicallyscale
. - before_scale
-
if the array-ref
$c-
stash->{before_scale}> contains action names, a forward to every of these actions is performed. - scaling
-
The
scale
stash variable contains a hash-ref with options for scaling, likew
,h
andmode
. Depending on the mode, a method namedscale_xxx
is called to execute the desired scaling. The default is to callscale_min
. In this case the minimum scaling factor is chosen which will ensure that the resulting image is not bigger than requested. - after_scale
-
if the array-ref
$c-
stash->{after_scale}> contains action names, a forward to every of these actions is performed. - delivery
-
the image is converted to the desired format (based on the extension of the URL) and delivered to the client.
If you like to support an URI like /image/small/house.jpg
, simply implement a
method named want_small
with an ":Args(0)" attribute that puts the required
scaling options into a stash variable.
sub want_small :Action :Args(0) { my ($self, $c) = @_; # set the desired scaling. # fill means scaling by minimum factor and filling the gaps in order # to get an image of the desired size $c->stash(scale => {w => 100, h => 80, mode => 'fill'}); }
If you change the mode
to 'fit', 'min', 'max' and 'fill' you will quickly
see the difference between the various modes. If you need another mode,
you may create one.
For getting a URI /image/foo-42/house.jpg
, you might guess that a method
named want_foo
with ":Args(1)" is required. Simple. Here we also define
and implement a scaling method of our own which also blurs the image. Actually
using the after_scale
hook would be more wise but as the example below
should demonstrate how to create a scaler action.
sub want_foo :Action :Args(1) { my ($self, $c, $mode) = @_; $c->stash(scale => {w => $mode * 4, h => $mode * 3, mode => 'blur'}); } sub scale_blur :Action { my ($self, $c) = @_; my $scale = $c->stash->{scale}; my %scale_options = ( ($scale->{w} ? (xpixels => $scale->{w}) : ()), ($scale->{h} ? (ypixels => $scale->{h}) : ()) ); my $scaled_image = keys %scale_options ? $c->stash->{image}->scale(%scale_options, type => 'min') : $c->stash->{image}; my $scaled_and_blurred_image = $scaled_image->filter( type => 'gaussian', stddev => 5, ); $c->stash->{image} = $scaled_and_blurred_image; }
If you created a root/cache directory you may enable the caching option in the config section. After that you will see that it then contains all images you requested afterwards.
Simply change the cache_dir entry in the config section
__PACKAGE__->config( # the directory to look for files (inside root) # defaults to 'static/images' #root_dir => 'static/images', # specify a cache dir if caching is wanted # defaults to no caching (more expensive) cache_dir => 'root/cache', # ... nothing to change below
As soon as you enabled the caching, you will immediately feel that reloading the same image becomes much faster. However, changing scaling options will need to clear the cache.
You could raise the performance further if you add a redirect rule into your frontend Webserver's configuration that delivers a cached image when available and only falls back to your Catalyst Application if no cached image is present.
See also
Catalyst::View::Thumbnail::Simple
Catalyst::Plugin::Upload::Image::Magick::Thumbnail
the fastest scaler I have ever seen for JPEG images:
These earlier Catalyst advent calendars had similar recipes:
Summary
With this simple module, image manipulations can get encapsulated into a single controller. No additional view is needed, the controller also delivers the scaled image data to the browser. By extending the logic that drives the scaling process you can get nice URLs with more or less complicated logic behind the scenes. Caching allows fast redelivery of already requested images. By leveraging the powerful operations Imager offers, you can do a lot of cool things.
Author
Wolfgang Kinkeldei <wolfgang [at] kinkeldei [dot] de>
- Previous
- Next