Copyright Notice

This text is copyright by CMP Media, LLC, and is used with their permission. Further distribution or use is not permitted.

This text has appeared in an edited form in WebTechniques magazine. However, the version you are reading here is as the author originally submitted the article for publication, not after their editors applied their creativity.

Please read all the information in the table of contents before using this article.
Download this listing!

Web Techniques Column 47 (Mar 2000)

[suggested title: Building an Icon Factory]

There's no doubt about it. I'll never be mistaken as a ``web designer'', as the term has come to be called. I'm not very good at drawing things, and I tend to spend more of my energy making sure the content gets delivered in accessible ways, without making sure it's also pretty to look at.

But I was tired of people talking about my company's site at www.stonehenge.com as ``great content, but it sure looks ugly''. So, I consulted some designer friends of mine, and came up with a new makeover. As always, it's a work in progress, but if you compare it with the old site, you'll see that I was at least open to new ideas.

A lot of the design process involved looking at variously colored rounded corners, anchoring the corners of a box made with table elements. The designer traditionally makes these corners using some graphic tool, then laboriously uploads the file to the server along with the edited HTML to see if everything plays correctly.

But that's because most designers aren't programmers. Since I didn't want to go through all that work while I was dinking around, and I was already editing the HTML directly on the server, I decided to make a smart URL that generated the corner images as needed.

Here's the basic strategy. Let's start with a blue box with nice rounded corners, and some content in the middle. We'd put something like this into an HTML page:

  <table bgcolor=white cellspacing=0 cellpadding=0>
  <tr>
    <td><img src="/icon/16x16-nw-0000ff.gif"></td>
    <td bgcolor="#0000ff"><img src="/icon/1x1.gif"></td>
    <td><img src="/icon/16x16-ne-0000ff.gif"></td>
  <tr>
  <tr>
    <td bgcolor="#0000ff"><img src="/icon/1x1.gif"></td>
    <td>My content goes here</td>
    <td bgcolor="#0000ff"><img src="/icon/1x1.gif"></td>
  <tr>
  <tr>
    <td><img src="/icon/16x16-sw-0000ff.gif"></td>
    <td bgcolor="#0000ff"><img src="/icon/1x1.gif"></td>
    <td><img src="/icon/16x16-se-0000ff.gif"></td>
  <tr>
  </table>

Now, there's a few things to note here... I have a lot of image links that look like:

  /icon/WIDTHxHEIGHT-DIRECTION-COLOR.gif

and that means what it means... I'll need to stick a corner icon GIF there, with the appropriate pixel width and height, and hex-defined color (here 00, 00, ff meaning 0 red, 0 green, and maximum blue). The ``direction'' is a two-character compass direction, where ``ne'' means ``northeast corner''. The color is the ``foreground color'', presuming that the background is transparent.

There's also a few cells that have an image URL like:

  /icon/WIDTHxHEIGHT.gif

and these are for those ugly 1 by 1 purely transparent GIFs required because some browsers refuse to display or decorate empty TD elements. Bleh. We need this to color the sides of the box, using a table element with a controlled background color and no content except for the transparent GIF. The nice thing about this approach is that it is entirely flexible, and depends only on the size of the contents.

Now for the tricky part: making those GIFs. Well, not really. You see, in the directory served for the URL /icon, I have the following .htaccess file (this is Apache with mod_perl, of course):

  ErrorDocument 404 /perl/makeicon

Now, what does that mean, when someone asks for /icon/1x1.gif? Well, if there was nothing else in that directory, we normally would have gotten a 404 error directly to the browser. But the ErrorDocument directive says to handle that 404 error via an ``internal redirect'' to the new URL.

In this case, I have a Perl program that runs under mod_perl at that URL. The directory of /perl is set to be handled by Apache::Registry, which allows CGI-like programs to be embedded directly into the server as needed. In my /perl directory, I have this .htaccess file:

    SetHandler perl-script
    PerlHandler Apache::Registry
    PerlSendHeader On
    Options +ExecCGI

which causes a URL referencing into the directory to trigger all the Apache::Registry magic.

But now for the cool part. This /perl/makeicon program looks at the filename that wasn't found, computes the GIF needed to satisfy the specifications, then both returns that GIF and saves it into the /icon directory with the right name! That means that any later hit with the same URL will simply be treated as a normal fetch. Like magic, it's a self-building icon repository. So the only overhead is on the very first hit when the icon isn't found.

The program is presented here in [the listing, below].

Line 1 begins with the path to my installed Perl. This isn't needed for an Apache::Registry script, but it helps GNU Emacs figure out that I want to go into Perl editing mode.

Lines 2 and 3 turn on compiler restrictions, and turn off output buffering, common for scripts like this.

Lines 5 through 9 set up a die handler. Throughout the rest of the code, I use a die to declare that something is wrong. Most usage errors should simply result in the server returning a normal 404 error, but for unexpected things, I'll want a log entry made into the Apache error log. Thus, I use die 404 in various places where I simply want to return a 404. Any other death message (that doesn't contain a literal 404 somewhere) will be logged via the warn operator, but still yield a 404 status return to the web client.

Line 12 gets the Apache::Request object for the failed request. The parameter extracted with shift is the object for this request, but we want the previous request to find out what GIF was requested; hence the call to both shift and prev.

Lines 18 and 19 extract the Unix $filename of the requested URL. That is, if there'd been a file there, we wouldn't have been invoked as a 404 handler. And then we get the directory and the filename within that directory into $dir and $base, respectively.

Lines 22 through 24 extract the specification for this icon from the filename. It must end in .gif, and have a height and width. In addition, a color may be specified as 6 hex characters, making up the red, green, and blue values defined in hex pairs. If the color is absent, a completely transparent GIF of the requested size is made instead.

And then the style can be added, to define a corner. If the style is absent, we get a rectangle of the requested color. The style is one of the four compass corners, and may be followed by an optional i to indicate inverse (or inside, I can't recall which). Thus, a style of nw generates a northwest outer corner of color-on-transparent, while sei generates a southeast inner corner of transparent-on-color.

If any part of the filename is unexpected, we punt and generate a 404 error. Similarly, line 27 detects preposterous GIF sizes, and punts as well.

Line 30 brings in the (old) GD library, formerly found in the CPAN. If you didn't grab and install the one that can generate GIFs, you're out of luck now. It's no longer there, having been replaced by a legally safer one that makes only PNGs. I don't know whether to be more mad at Unisys (who are enforcing the patent that makes GIFs a licensed commodity), Compuserve (who created a popular file format that used patented processes), the US Patent Office (who permit software patents), or the big browser makers (for not having good PNG support in existing browsers). But in any event, I'm still running the GIF-making GD so that this works. Just don't tell Unisys.

Line 31 makes a blank image of the right size as an object. Again, if anything breaks, we just punt to 404 and hope that someone is watching the error log.

Lines 32 and 33 make the transparent color. It has to be a defined color, and I pick a slightly off-grey in hopes that no designer will ever want to use precisely this ugly color (because that would break everything else). If I were really paranoid, I would watch for color selections of 7e7f80 above, but I don't care enough to add that. Also, as the first color allocated, it's automatically the background for the rest of the image.

At this point, we have a completely transparent GIF of the right size, but if any color is defined, lines 35 to 51 kick in to finish the job.

Line 36 transfers the hex color specification into a real color object, saved in $ink. We'll use this to make a solid color, or the corner, as needed.

Lines 38 to 47 handle a rounded-corner GIF, by picking the correct center point for an oval and then painting it. I thought I would need to get clever and decide how much of each oval to draw, but as it turns out, GD does the right thing anyway, even when I'm apparently drawing ``outside'' the GIF. Lines 42 to 45 handle the ``invert'' corners by first filling the entire GIF with the requested color, and then selecting the transparent color as the ink.

Once we've finished inking the GIF, line 55 creates the actual GIF89a image into a string. This string now needs to go two places: out to the browser, and into the file (for the next hit).

Lines 58 to 66 create the icon file. The Apache::File module is like IO::File with a lot less overhead, and creates a scoped filehandle. We save the GIF information into a file that is near to the final location, but named uniquely. That way, the final step is a rename operation that happens ``atomically'', in a way that no process can ever see a partially written GIF.

Now, in order for this to work, the $dir directory must be writable by the Apache process. I just set mine to permissions of 777, and that works just fine. If that isn't done, the file isn't created, although we would still dump out the GIF to the web client.

Speaking of that, lines 68 to 70 dump out the computed GIF. And we're done!

Even if you aren't using mod_perl, you can still set up an Apache error handler to trigger a CGI program that does much of the same thing as what I've done here. You'll need to look at the environment variables beginning with REDIRECT_... in the CGI program to know what GIF was requested; that's a bit simpler with the mod_perl interface as you have seen. Also, you can't use Apache::File, but changing that to IO::File would work nearly identically.

You'll probably want to purge old unused icons, so a cron job that comes along once a night purging any icons that haven't been accessed in a week or so might just do the trick. It's safe to delete too much, because even if we accidentally deleted every GIF at once, the next hit would simply trigger the 404 handler to recreate it once again!

I hope this technique inspires you to think of 404 handlers as something more than just a fancy way of saying ``we don't have this''. Until next month, enjoy!

Listings

        =1=     #!/usr/bin/perl
        =2=     use strict;
        =3=     $|++;
        =4=     
        =5=     $SIG{__DIE__} = sub {
        =6=       my $arg = shift;
        =7=       warn "makeicon died with $arg" unless $arg =~ /404/;
        =8=       print "Status: 404\n\n";
        =9=     };
        =10=    
        =11=    ## we are a 404 handler, so we need to get the request that triggered the 404:
        =12=    my $r = shift->prev;
        =13=    
        =14=    ## if the request was for a subdirectory or had args, bad news:
        =15=    die 404 if not $r or $r->args or $r->path_info;
        =16=    
        =17=    ## now extract the expected filename, which we'll need to generate:
        =18=    my $filename = $r->filename;
        =19=    my ($dir,$base) = $filename =~ m{(.*)/(.*)}s;
        =20=    
        =21=    ## and ensure that it's a gif that we want to make:
        =22=    my ($height, $width, $style, $color) =
        =23=      $base =~ m{^(\d+)x(\d+)(?:-(?:([ns][ew]i?)?-)?([0-9a-fA-F]{6})?)?\.gif$} or
        =24=      die 404;
        =25=    
        =26=    ## don't let people burn my CPU
        =27=    die 404 if $height * $width > 2000 or $height * $width < 1;
        =28=    
        =29=    ## Time to make the gif:
        =30=    require GD;
        =31=    my $im = GD::Image->new($width,$height) or die 404;
        =32=    my $grey = $im->colorAllocate(126,127,128); # I hope we don't collide
        =33=    $im->transparent($grey);
        =34=    
        =35=    if (defined $color) {           # if no color, make transparent spacer gif
        =36=      my $ink = $im->colorAllocate(unpack "C*", pack "H*", $color);
        =37=    
        =38=      if (defined $style) {         # it's a corner arc
        =39=        my $center_x = $style =~ /^.e/i ? 0 : $width-1;
        =40=        my $center_y = $style =~ /^s/i ? 0 : $height-1;
        =41=    
        =42=        if ($style =~ /^..i/) {     # invert
        =43=          $im->fill(0,0,$ink);
        =44=          $ink = $grey;
        =45=        }
        =46=        $im->arc($center_x,$center_y,$width*2,$height*2, 0, 360, $ink);
        =47=        $im->fill($center_x,$center_y,$ink);
        =48=      } else {                      # it's a rectangle
        =49=        $im->fill(0,0,$ink);
        =50=      }
        =51=    }
        =52=    
        =53=    ## good job! time to write it out:
        =54=    
        =55=    my $gif = $im->gif;
        =56=    
        =57=    ## if this fails, we just generate dynamically each time, no biggy
        =58=    require Apache::File;
        =59=    my $tmpname = "$dir/.$$.$base";
        =60=    if (my $tmp = Apache::File->new(">$tmpname")) {
        =61=      print $tmp $gif;
        =62=      close $tmp;
        =63=      rename $tmpname, $filename or warn "cannot rename $tmpname to $filename: $!";
        =64=    } else {
        =65=      warn "couldn't create $tmpname: $!";
        =66=    }
        =67=    
        =68=    print "Status: 200\nContent-type: image/gif\n\n";
        =69=    print $gif;
        =70=    exit 0;

Randal L. Schwartz is a renowned expert on the Perl programming language (the lifeblood of the Internet), having contributed to a dozen top-selling books on the subject, and over 200 magazine articles. Schwartz runs a Perl training and consulting company (Stonehenge Consulting Services, Inc of Portland, Oregon), and is a highly sought-after speaker for his masterful stage combination of technical skill, comedic timing, and crowd rapport. And he's a pretty good Karaoke singer, winning contests regularly.

Schwartz can be reached for comment at merlyn@stonehenge.com or +1 503 777-0095, and welcomes questions on Perl and other related topics.