An AxKit Image Gallery
by Barrie Slaymaker
|
Pages: 1, 2, 3, 4
And here's what the <caption> for
a-look.jpeg now looks like (all the other
<caption> elements were left untouched because there
are no other .meta files in this directory):
<caption>
<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>
<meta>
<title>A baby picture</title>
<comment><b>ME!</b>. Well, not really. Actually, it's some random image from the 'net.
</comment>
</meta>
</caption>
As mentioned before, this stylesheet does not care what you put in the meta file, it just inserts anything in that file from the root element on down. So you are free to put any meta information your application requires in the meta file and adjust the presentation filters to style it as you will.
The .meta information is not inserted in to the
<image> tags because we know that none of our
presentation will not need any of it there.
captionstyler.xsl
The last two stages of our pipeline turn the data assembled so far into HTML. This is done in two stages in order to separate general layout and presentation from the presentation of the caption because the these portions of the presentation might need to vary independently between one collection of images and another.
The caption stylesheet for this example is:
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:template match="caption">
<caption width="100" align="left" valign="top">
<a href="{filename}">
<xsl:choose>
<xsl:when test="meta/title and string-length( meta/title )">
<xsl:copy-of select="meta/title/node()" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="name" />
</xsl:otherwise>
</xsl:choose>
</a><br />
<font size="-1" color="#808080">
<xsl:copy-of select="last_modified/node()" />
<br />
</font>
<xsl:copy-of select="meta/comment/node()" />
</caption>
</xsl:template>
<xsl:template match="*|@*|node()">
<xsl:copy>
<xsl:apply-templates />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
The first template replaces all <caption> elements
with new <caption> cells with a default width and
alignment, and then fills these with the name of the image, which is
also a link to the underling image file, and the
<last_modified time string formatted by My::ProofSheet and any
<comment> that might be present in the meta
file.
The <xsl:choose> element is what selects the title
to display for the image. The first <xsl:when>looks
to see if there is a <title> element in the meta file
and uses it if present. The
<xsl:otherwise> defaults the name to the
<name> set by My::ProofSheet.
The captions output by this stage look like:
<caption width="100" align="left" valign="top">
<a href="a-look.jpeg">A baby picture</a>
<br/>
<font size="-1" color="#808080">Wed Sep
4 13:32:46 2002<br/>
</font>
<b>ME!</b>. Well, not really. Actually, it's
some random image from the 'net.
</caption>
<caption width="100" align="left" valign="top">
<a href="a-lotery.jpeg">a-lotery</a>
<br/>
<font size="-1" color="#808080">Wed Sep
4 13:33:07 2002<br/></font>
</caption>
The former is what comes out when a .meta file is found, the latter when it is not.
pagestyler.xsl
And now, the final stage. If you've made it this far, congratulations; this is the start of a real application and not just a toy, so it's taken quite some time to get here.
The final stage of the processing pipeline generates an HTML page from the raw data, except for the attributes and content of <caption> tags, which it passes through as-is:
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:template match="/*">
<html>
<head>
<title>Images in <xsl:value-of select="title" /></title>
</head>
<body bgcolor="#ffffff">
<xsl:apply-templates select="images" />
</body>
</html>
</xsl:template>
<xsl:template match="images">
<table>
<xsl:apply-templates />
</table>
</xsl:template>
<xsl:template match="tr">
<xsl:copy>
<xsl:apply-templates select="*" />
</xsl:copy>
</xsl:template>
<xsl:template match="image">
<td align="left" valign="top">
<a href="{filename}">
<img border="0" src="{thumb_uri}">
<xsl:if test="thumb_width">
<xsl:attribute name="width">
<xsl:value-of select="thumb_width" />
</xsl:attribute>
</xsl:if>
<xsl:if test="thumb_height">
<xsl:attribute name="height">
<xsl:value-of select="thumb_height" />
</xsl:attribute>
</xsl:if>
</img>
</a>
</td>
</xsl:template>
<xsl:template match="@*|node()" mode="caption">
<xsl:copy>
<xsl:apply-templates select="@*|node()" mode="caption" />
</xsl:copy>
</xsl:template>
<xsl:template match="caption">
<td>
<xsl:apply-templates select="@*|node()" mode="caption" />
</td>
</xsl:template>
</xsl:stylesheet>
The first template generates the skeleton of the HTML page, the
second one grabs the <images> list from the source
document, emits a <table>, the third copies the
<tr> tags, the fourth replaces all
<image> tags with <td> tags
containing the thumbnail image as a link to the underlying image
(similar to what captionstyler.xsl did
with the picture name). The only subtlety here is that the optional
<thumb_width> and <thumb_height>
elements are used, if present, to inform the browser of the size of the
thumbnail in order to speed up the layout process (as mentioned before, pages
that don't contain this information are not cached so that when the
thumbnails are generated, new HTML will be generated with it).
The fourth template converts the <caption>
elements to <td> elements and copies all their
content through, since captionstyler.xsl already did the
presentation for them.
Tweaking this stylesheet or replacing it controls the entire page
layout other than thumbnail sizing (which is set by the optional
MyMaxX and MyMaxY PerlSetVar
settings in httpd.conf). A different
stylesheet in this point in the chain could choose to ignore the
<tr> tags and present a list style output. A later
stylesheet could be added to add branding or advertising to the site,
etc., etc.
My::ThumbNailer
Here's the apache module that generates thumbnails. The key thing to remember is that, unlike all the other code and XML shown in this article, this is called once per thumbnail image, not once per directory. When a browser requests a directory listing, it gets HTML from the pipeline above with lots of URIs for thumbnail images. It will then usually request each of those in turn. The httpd.conf file directs all requests for dotfiles to this module:
package My::Thumbnailer;
# Allow other modules like My::ProofSheet to use some
# of our utility routines.
use Exporter;
@ISA = qw( Exporter );
@EXPORT_OK = qw( image_size thumb_limits );
use strict;
use Apache::Constants qw( DECLINED );
use Apache::Request;
use File::Copy;
use Imager;
sub image_size {
my $img = shift;
if ( ! ref $img ) {
my $fn = $img;
$img = Imager->new;
$img->open( file => $fn )
or die $img->errstr(), ": $fn";
}
( $img->getwidth, $img->getheight );
}
sub thumb_limits {
my $r = shift;
# See if the site admin has placed MyMaxX and/or
# MyMaxY in the httpd.conf.
my ( $max_w, $max_h ) = map
$r->dir_config( $_ ),
qw( MyMaxX MyMaxY );
return ( $max_w, $max_h )
if $max_w || $max_h;
# Default to scaling down to fit in a 100 x 100
# pixel area (aspect ration will be maintained).
return ( 100, 100 );
}
# Apache/mod_perl is configured to call
# this handler for every dotfile
# requested. All thumbnail images are dotfiles,
# some dotfiles may not be thumbnails.
sub handler {
my $r = Apache::Request->new( shift );
# We only want to handle images.
# Let Apache handle non-images.
goto EXIT
unless substr( $r->content_type, 0, 6 ) eq "image/";
# The actual image filename is the thumbnail
# filename without the leading ".". There's
( my $orig_fn = $r->filename ) =~ s{/\.([^/]+)\z}{/$1}
or die "Can't parse ", $r->filename;
# Let Apache serve the thumbnail if it already
# exists and is newer than the original file.
{
my $thumb_age = -M $r->finfo;
my $orig_age = -M $orig_fn;
goto EXIT
if $thumb_age && $thumb_age <= $orig_age;
}
# Read in the original file
my $orig = Imager->new;
unless ( $orig->open( file => $orig_fn ) ) {
# Imager can't hack the format, fall back
# to the original image. This can happen
# if you forget to install libgif
# (as I have done).
goto FALLBACK
if $orig->errstr =~ /format not supported/;
# Other errors are probably more serious.
die $orig->errstr, ": $orig_fn\n";
}
my ( $w, $h ) = image_size( $orig );
die "!\$w for ", $r->filename, "\n" unless $w;
die "!\$h for ", $r->filename, "\n" unless $h;
my ( $max_w, $max_h ) = thumb_limits( $r );
# Scale down only, If the image is smaller than
# the thumbnail limits, let Apache serve it as-is.
# thumb_limits() guarantees that either $max_w
# or $max_h will be true.
goto FALLBACK
if ( ! $max_w || $w < $max_w )
&& ( ! $max_h || $h < $max_h );
# Scale down to the maximum dimension to the
# requested size. This can mess up for images
# that are meant to be scaled on each axis
# independantly, like graphic bars for HTML
# page seperators, but that's a very small
# demographic.
my $thumb = $orig->scale(
$w > $h
? ( xpixels => $max_w )
: ( ypixels => $max_h )
);
$thumb->write( file => $r->filename,)
or die $thumb->errstr, ": ", $r->filename;
goto BONK;
FALLBACK:
# If we can't or don't want to build the thumbnail,
# just copy the original and let Apache figure it out.
warn "Falling back to ", $orig_fn, "\n";
copy( $orig_fn, $r->filename );
BONK:
# Bump apache on the head just hard enough to make it
# forget the thumbnail file's old stat() and
# mime type since we've most likely changed all
# that now. This is important for the headers
# that control downstream caching, for instance,
# or in case Imager changed mime types on us
# (unlikely, but hey...)
$r->filename( $r->filename );
EXIT:
# We never serve the image data, Apache is perfectly
# good at doing this without our help. Returning
# DECLINED causes Apache to use the next handler in
# its list of handlers. Normally this is the default
# Apache file handler.
return DECLINED;
}
1;
There should be enough inline commentary to explain that lot. The
only thing I'll say is that, to head off the gotophobes, I think the use
of goto makes this routine a lot clearer than the
alternatives; the early versions did not use it and were less
readable/maintainable. This is because the three normal exit routes
happen to stack nicely up from the bottom so the fallthrough from one
labeled chunk to the next happens nicely.
The most glaring mistake here is that there is no file locking. We'll add that in next time.
Summary
The final result of the code in this article is to build the image proofsheet section of the page we showed at the beginning of the article. The next article will complete that page, and then we'll build the image presentation page and a metadata editor in future articles.
Help and thanks
In case of trouble, have a look at some of the helpful resources we listed in the first article.



