I've been working on a fairly simple Perl/Gtk2 application to help me catalogue my photographs. I wanted to be able to see thumbnails along with various bits of metadata when I searched for things, so I wrote some code to calculate an appropriate size for a thumbnail and generate a scaled-down version of the image using Image::Magick from Perl. Here's basically what I ended up with:
my $img = Image::Magick->new; $img->Read($orig_filename); $img->Scale(width => $width, height => $height); $img->Rotate(degrees => $rotation) if $rotation; $img->Write($thm_filename);
The rotation part is there because some modern digital cameras record the orientation of the camera in the file, so we can show thumbnails for vertical images correctly. I use Image::Info to extract this, from the ‘Orientation’ field. You have to convert the name to a numeric angle in degrees.
Image::Magick basically works, and has the advantage that it will handle a lot of file formats, but I've come across a few problems. Firstly, I save all the thumbnails as JPEG for simplicity, but when an animated GIF is saved it gets written to lots of separate files, one for each frame. The filenames have suffixes like ‘.0’, ‘.1’, and so on. The filename you supply isn't written to at all. I came up with some nasty code to delete and rename files, but the nice way of doing it is to delete all but the first frame of animation. It turns out that Image::Magick objects can behave like array references, so this is actually quite simple. Here's a complete example program that reads a file, discards any extra frames, then saves the remaining one to a separate file (using the file extension to decide the format):
#!/usr/bin/perl -w use strict; use Image::Magick; die "Usage: $0 input-filename output-filename\n" unless @ARGV == 2; my ($input_filename, $output_filename) = @ARGV; my $img = Image::Magick->new; $img->Read($input_filename); my $num_frames = scalar @$img; print STDERR "Number of frames in original: $num_frames\n"; # Delete all but the first frame in the image. The object # can be treated like an array reference, so individual # frames can be deleted or moved around without using any # special methods. $#$img = 0; $img->Write($output_filename); # vi:ts=4 sw=4 expandtab
The other problems are that it is slower than I'd like and produces JPEGs which are too large. The size issue turns out to be because (at least for images from my 20D) the original thumbnail image embedded inside the large JPEG by the camera, and all the EXIF and Canon-specific metadata, is copied over to the thumbnail. Together this all takes up much more space than the tiny thumbnail image I want.
At least for JPEG images both of these problems can be solved using the Epeg library, from the Enlightenment project (source). This library is just for thumbnailing JPEGs: it doesn't work with other formats, and can't resize images to a larger size, only make them smaller. It's perfect for my use though, because it's more than 10 times as fast as Image::Magick for thumbnailing (in a quick-and-dirty test) and doesn't copy any of the extraneous data into the output file, so the thumbnails are much smaller. There's a Perl interface called Image::Epeg.
(At least as of version 0.07 you might need to tweak paths in the XS code and Makefile.PL to get Image::Epeg to compile, or apply my patch from cpan bug 13152.)
The only problem with using this for JPEG images (I'll still have to use Image::Magick or something for other formats) is that it can't rotate the image. I'll live without that for now, but I'll look in to whether there's a nice simple way of rotating JPEGs. Ideally a low-level binding to libjpeg, which would allow fast lossless rotation of the finished thumbnail image.
Anyway, here's my little test program for Image::Epeg, which squishes a whole batch of JPEGs and puts the finished thumbnails into a new directory:
#!/usr/bin/perl -w # Make thumbnails of JPEG files, using the Epeg library. # By Geoff Richards. # You may do what you want with this code. There is no warranty. use strict; use Image::Epeg qw( :constants ); use File::Basename qw( basename ); use File::Spec::Functions qw( catfile ); die "Usage: $0 max-dimension output-directory jpeg-filenames...\n" unless @ARGV >= 2; my $max_dimen = shift; my $output_dir = shift; mkdir $output_dir, 0777 or die "error creating directory '$output_dir': $!"; for (@ARGV) { print STDERR "Shrinking $_\n"; my $img = Image::Epeg->new($_); my $orig_width = $img->get_width; my $orig_height = $img->get_height; if ($max_dimen < $orig_width || $max_dimen < $orig_height) { $img->resize($max_dimen, $max_dimen, MAINTAIN_ASPECT_RATIO); } $img->write_file(catfile($output_dir, basename($_))); } # vi:ts=4 sw=4 expandtab