An AxKit Image Gallery
by Barrie Slaymaker
|
Pages: 1, 2, 3, 4
My::ProofSheet
Once the data is in Perl data structure, it's easy to tweak it (making
mtime fields into something readable, for instance)
and extend it (adding information about thumbnail images and .meta
files, for instance). This is what My::ProofSheet does:
package My::ProofSheet;
use XML::SAX::Base;
@ISA = qw( XML::SAX::Base );
# We need to access the Apache request object to
# get the URI of the directory we're presenting,
# its physical location on disk, and to probe
# the files in it to see if they are images.
use Apache;
# My::Thumbnailer is an Apache/mod_perl module that
# creates thumbnail images on the fly. See below.
use My::Thumbnailer qw( image_size thumb_limits );
# XML::Generator::PerlData lets us take a Perl data
# structure and emit it to the next filter serialized
# as XML.
use XML::Generator::PerlData;
use strict;
sub generate {
my $self = shift;
my ( $data ) = @_;
# Get the AxKit request object so we can
# ask it for the URI and use it to test
# whether files are images or not.
my $r = $self->{Request};
my $dirname = $r->uri; # "/04/Baby_Pictures/Other/"
my $dirpath = $r->filename; # "/home/me/htdocs/...Other/"
my @images = map $self->file2image( $_, $dirpath ),
sort {
$a->{filename} cmp $b->{filename}
} @{$data->/2002/09/24/axkit.html};
# Use a handy SAX module to generate XML from our Perl
# data structures. The XML will look basically like:
# Write XML that looks like
#
# <proofsheet>
# <images>
# <image>...</image>
# <image>...</image>
# ...
# </images>
# <title>/04/BabyePictures/Others</title>
# </proofsheet>
#
XML::Generator::PerlData->new(
rootname => "proofsheet",
Handler => $self,
)->parse( {
title => $dirname,
images => { image => \@images },
} );
}
sub file2image {
my $self = shift;
my ( $file, $dirpath ) = @_;
# Remove the filename from the fields so it won't
# show up in the <image> structure.
my $fn = $file->{filename};
# Ignore hidden files (first char is a ".").
# Thumbnail images are cached as hidden files.
return () if 0 == index $fn, ".";
# Ignore files Apache knows aren't images
my $type = $self->{Request}->lookup_file( $fn )->content_type;
return () unless
defined $type
&& substr( $type, 0, 6 ) eq "image/";
# Strip the extension(s) off.
( my $name = $fn ) =~ s/\..*//;
# A meta filename is the image filename with a ".meta"
# extension instead of whatever extension it has.
my $meta_fn = "$name.meta";
my $meta_path = "$dirpath/$meta_fn";
# The thumbnail file is stored as a hidden file
# named after the image file, but with a leading
# '.' to hide it.
my $thumb_fn = ".$fn";
my $thumb_path = "$dirpath/$thumb_fn";
my $last_modified = localtime $file->{mtime};
my $image = {
%$file, # Copy all fields
type => $type, # and add a few
name => $name,
thumb_uri => $thumb_fn,
path => "$dirpath/$fn",
last_modified => $last_modified,
};
if ( -e $meta_path ) {
# Only add a URI to the meta info, metamerger.xsl will
# slurp it up if and only if <meta_uri> is present.
$image->{meta_filename} = $meta_fn;
$image->{meta_uri} = "file://$meta_path";
}
# If the thumbnail exists, grab its width and height
# so later stages can populate the <img> tag with them.
# The eval {} is in case the image doesn't exist or
# the library can't cope with the image format.
# Disable caching AxKit's output if a failure occurs.
eval {
( $image->{thumb_width}, $image->{thumb_height} )
= image_size $thumb_path;
} or $self->{Request}->no_cache( 1 );
return $image;
}
1;
When My::Filelist2Data calls generate(),
generate() sorts and scans the list of files by filename,
converts each to an image and sends a page title and the resulting list
of images to the next filter (XML::Filter::TableWrapper). Kip Hampton's
XML::Generator::PerlData
is a Perl data -> XML serialization module. It's not meant for
generating generic XML; it focuses purely on building an XML
representation of a Perl data structure. In this case, that's ideal,
because we will be generating the output document with XSLT templates
and we don't care about the exact order of the elements in each
<image> element, each <image>
element is just a hash of key/value pairs. We do control the order of
the <image> elements, however, by passing an ordered
list of them in to XML::Generator::PerlData as an array.
Sorting by filename may not be the preferred thing to do for all applications, because users may prefer to sort by the caption title for the image, but then again they may not, and this allows the site administrator to control sort order by naming the files appropriately. We can add always add sorting later.
Another peculiarity of this code is that it doesn't guarantee that
there will be thumb_width and thumb_height
values available. If you just drop the source images in a directory,
then the first time the server generates this page, there will be no
thumbnails available. In this case, the call to
no_cache(1) prevents AxKit from caching the output page so
that suboptimal HTML does not get stuck in the cache. This will give
the server another chance at generating it with proper tags, hoping of
course that by the next time this page is requested, the requisite
thumbnails will be available to measure.
This approach gets the HTML to the browser fast, so the user's
browser window will clear quickly and start filling with the top of ths
page, so the user will see some activity and be less likely to get
impatient. The thumbnails will be generated when the browser sees all
the <img> tags. The alternative approach would be to
thumbnail the images inline, which would result in a significant delay
on large listings before the first HTML hits the browser, or
prethumbnailing.
One thing to note about this approach is that many browsers will request images several at a time, which will cause several server processes to be thumbnailing several different images at once. This should result in lower lag on low-load servers because processes can interleave CPU time and disk I/O waits, and can take advantage of multiple processors, if present. On heavily loaded servers, of course, this might be a bad thing; pregenerating thumbnails there would be a good idea.
The output from this filter looks like:
<?xml version="1.0"?>
<proofsheet>
<images>
<image>
<path>
/home/barries/src/mball/AxKit/www/htdocs/04/Baby_Pictures/Others/a-look.jpeg
</path>
<writable>1</writable>
<filename>a-look.jpeg</filename>
<thumb_uri>.a-look.jpeg</thumb_uri>
<meta_filename>a-look.meta</meta_filename>
<name>a-look</name>
<last_modified>Wed Sep 4 13:32:46 2002</last_modified>
<ctime>1032552249</ctime>
<meta_uri>
file:///home/barries/src/mball/AxKit/www/htdocs/04/Baby_Pictures/Others/a-look.meta
</meta_uri>
<mtime>1031160766</mtime>
<size>8522</size>
<readable>1</readable>
<type>image/jpeg</type>
<atime>1032553327</atime>
</image>
<image>
<path>
/home/barries/src/mball/AxKit/www/htdocs/04/Baby_Pictures/Others/a-lotery.jpeg
</path>
<writable>1</writable>
<filename>a-lotery.jpeg</filename>
<thumb_uri>.a-lotery.jpeg</thumb_uri>
<name>a-lotery</name>
<last_modified>Wed Sep 4 13:33:07 2002</last_modified>
<ctime>1032552249</ctime>
<mtime>1031160787</mtime>
<size>10113</size>
<readable>1</readable>
<type>image/jpeg</type>
<atime>1032553327</atime>
</image>
</images>
...
<title>/04/Baby_Pictures/Others</title>
</proofsheet>
All the data from the original <file> elements are
in each <image> element along with the new fields.
Note that the first <image> contains the
<meta_uri> (pointing to a-look.meta)
while the second doesn't because there is no a-lotery.meta. As
expected both have the <thumb_uri> tags. The parts
in bold face are the bits that our presentation happens to want;
yours might want more or different bits.
While there is a lot of extra information in this structure, it's
really just the output from one system call (stat()) and
some possibly useful byproducts of the My::ProofSheet machinations, so
it's very cheap information that some front end somewhere might want.
It's also easier to leave it all in than to emit just what our example
frontend might want and will enable any future upstream filters or
extentions to AxKit's directory scanning to shine through.
No <thumb_width> or
<thumb_height> tags are present because I copied this
file from the axtrace directory (see the
AxTraceIntermediate directive in our
httpd.conf file) after viewing a newly added directory. Here's what
the first <image> element looks like when viewing
after my browser had requested all thumbnails:
<?xml version="1.0"?>
<proofsheet>
<images>
<image>
<thumb_width>72</thumb_width>
<path>
/home/barries/src/mball/AxKit/www/htdocs/04/Baby_Pictures/Others/a-look.jpeg
</path>
<writable>1</writable>
<filename>a-look.jpeg</filename>
<thumb_height>100</thumb_height>
<thumb_uri>.a-look.jpeg</thumb_uri>
<meta_filename>a-look.meta</meta_filename>
<name>a-look</name>
<last_modified>Wed Sep 4 13:32:46 2002</last_modified>
<ctime>1032552249</ctime>
<meta_uri>
file:///home/barries/src/mball/AxKit/www/htdocs/04/Baby_Pictures/Others/a-look.meta
</meta_uri>
<mtime>1031160766</mtime>
<size>8522</size>
<readable>1</readable>
<type>image/jpeg</type>
<atime>1032784360</atime>
</image>
...
</images>
<title>/04/Baby_Pictures/Others</title>
</proofsheet>
XML::Filter::TableWrapper
XML::Filter::TableWrapper
is a CPAN module is used to take the <images> list
and segmenting it by insert <tr>...</tr> tags
around every (it's configurable) <image>
elements. This configuration is done by the My::ProofSheetMachine module we showed
earlier:
XML::Filter::TableWrapper->new(
ListTags => "{}images",
Columns => $r->dir_config( "MyColumns" ) || 3,
),
The output, for our list of 9 images, looks like:
<?xml version="1.0"?>
<proofsheet>
<images>
<tr>
<image>
...
</image>
... 4 more image elements...
</tr>
<tr>
<image>
...
</image>
... 3 more image elements...
</tr>
</images>
<title>/04/Baby_Pictures/Others</title>
</proofsheet>
Now all the presentation stylesheet (pagestuler.xsl) can key off the
<tr> tags to build an HTML <table>
or ignore them (and not pass them through) if it wants to display in a
list format.
While I'm sure this is possible in XSLT, I have no idea how to do it easily.
rowsplitter.xsl
Experimentation with an early version of this application showed that presenting captions in the same table cell as the thumbnails when the thumbnails are of differing heights caused the captions to be showed at varying heights. This made it hard to scan the captions and added a lot of visual clutter to the page.
One solution is to add an XSLT filter that splits each table row of image data in to two rows, one for the thumbnail and another for the caption:
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:template match="image" mode="caption">
<caption>
<xsl:copy-of select="@*|*|node()" />
</caption>
</xsl:template>
<xsl:template match="images/tr">
<xsl:copy-of select="." />
<tr><xsl:apply-templates select="image" mode="caption" /></tr>
</xsl:template>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
The second template in this stylesheet matches each row
(<tr> element) in the <images>
element and copies it verbatim and then emits a second
<tr> element right after it with a list of
<caption> elements with copies of the content of each
of the <image> tags in the original row. The first
template is applied only to the <image> tags when
creating this second row due to the mode="caption"
attributes.
The third template is a standard piece of XSLT boilerplate that passes through all the XML that is not matched by the first two templates. This XML would otherwise be mangled (stripped of elements, to be specific) by the wacky default XSLT rules.
Now, I know several ways to do this in Perl in the AxKit environment and none are so easy for me as using XSLT. YMMV.
The output from that stage looks like:
<?xml version="1.0"?>
<proofsheet>
<images>
<tr><image>... </image> ...total of 5... </tr>
<tr><caption>...</caption> ...total of 5... </tr>
<tr><image>... </image> ...total of 4... </tr>
<tr><caption>...</caption> ...total of 4... </tr>
</images>
<title>/04/Baby_Pictures/Others</title>
</proofsheet>
The content of each <image> tag and each
<caption> tag is identical. It's easier to do the
transform this way and allows the frontend stylesheets the flexibility
of doing things like putting the image filename or modification time in
the same cell as the thumbnail.
metamerger.xsl
As with the row splitter, expressing the metamerger in XSLT is an expedient way of merging in external XML documents, for several reasons. The first is for efficiency's sake: We're already using XSLT before and after this filter, and AxKit optimizes XSLT->XSLT handoffs to avoid reparsing. Another is that the underlying implementation of AxKit's XSLT engine is the speedy C of libxslt. A third is that we're not altering the incoming file at all in this stage, so the XSLT does not get out of hand (I do not consider XSLT to be a very readable programming language; its XML syntax makes for very opaque source code).
Another approach would be to go back and tweak My::ProofSheet to inherit from XML::Filter::Merger and insert it using a SAX parser. That would be a bit slower, I suspect, because SAX parsing in general tends to be slower than XSLT's internal parsing. It would rob the application of the configurability that having merging as a separate step engenders. By factoring this functionality in to the metamerger.xsl stylesheet, we offer the site designer the ability to pull data from other sources, or even to fly without any metadata at all.
Here's what metamerger.xsl looks like:
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:template match="caption">
<caption>
<xsl:copy-of select="*|@*|node()" />
<xsl:copy-of select="document( meta_uri )" />
</caption>
</xsl:template>
<xsl:template match="*|@*">
<xsl:copy>
<xsl:apply-templates select="*|@*|node()" />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
The first template does all the work of matching each
<caption> element and copying its content, then
parsing and inserting the document indicated by the
<meta_uri> element, if present. The
document() function turns into a noop if
<meta_uri> is not present. The second template is
that same piece of boilerplate we saw in rowsplitter.xsl to copy through everything we don't explicitly match.





