Sign In/My Account | View Cart  
advertisement


Listen Print

An AxKit Image Gallery
by Barrie Slaymaker | Pages: 1, 2, 3, 4

My::ProofSheet

My::ProofSheet's position in the processing pipeline.

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

My::TableWrapper's position in the processing pipeline

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

rowsplitter.xsl's position in the processing pipeline.

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

metamerger.xsl's position in the processing pipeline

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.

Pages: 1, 2, 3, 4

Next Pagearrow