Copyright Notice

This text is copyright by InfoStrada Communications, Inc., and is used with their permission. Further distribution or use is not permitted.

This text has appeared in an edited form in Linux Magazine 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!

Linux Magazine Column 33 (Feb 2002)

[suggested title: Who's got the button?]

Those cute little graphic buttons for your website. Who has time to make them? Especially when you're redesigning your website and changing the text, or perhaps even the text varies occasionally or frequently.

Well, I was faced with that issue the other day while I was thinking about Yet Another Redesign for my website at perltraining.stonehenge.com. I want to include some ``next'' and ``previous'' buttons, but didn't want to spend a lot of time in some bitmap-drawing program coming up with them.

I've tackled a task similar to this before using the GD library, but that was just the rounded corners for table-ish buttons. This time, I wanted to go whole hog and do the whole thing as one graphic, so that the ALT text would be properly replaced on a non-graphical browser.

GD's font capabilities never really impressed me, so I turned (with some great hesitation) to the ImageMagick library, and its PerlMagick binding. I believe I know where the ``Magick'' part of the library gets its name: when you finally figure out how to do what you want, it appears to be ``magick'', since the documentation, well, is mostly absent.

So, after invoking convert some 200 to 300 times, slowly varying the parameters, and trying new things, and then trying to figure out how to convert that to the Perl bindings, again invoking it some 200 times or so (I'm not joking about these numbers, and I wish I was), I've come up with [listing one, below].

No, please don't ask me to fully explain the mappings between the code here and the convert lines I issued before: it all appears magically to me. I don't think anyone ever writes a PerlMagick program... they just start with the examples in the distribution test suite, and try to cut-and-paste until something works. I know I had to stare at Magick.xs (the Perl binding code to the Magick library) at least a dozen times to get some of the parameters to even try.

In hopes that I'll save you a bit of time, and provide Yet Another Working Sample for PerlMagick, let's take a look at the code.

This code is meant to be used either as a CGI program or under mod_perl's Apache::Registry. Obviously, if you're taking a lot of hits, you'll want the Apache::Registry version. With that in mind, look at lines 5 through 13, in which we create the appropriate ``query'' object. If we're under mod_perl then the test in line 6 is true, and we fetch the Apache::Request module to get a query object from there. If not, we use the normal CGI module to get a query object. The object must respond to a param method to give us the input parameters, and nicely, both of these do.

Once we have the query object, lines 16 to 19 fetch the parameters. Line 16 gets the label for the button, defaulting to submit. Line 17 gets the foreground color, and line 18 gets the background color. Line 19 gets the size (in typesetting points) for the text. Note that the size of the button in pixels depends on whether the text has ascenders or descenders. (That messes up a series of uniform buttons, but maybe one of you can tell me how to trim left-to-right without trimming top-to-bottom with ImageMagick, and then I could fix the height.)

Line 22 starts a stopwatch for real (wallclock) time and CPU times. We use the numbers in lines 87 and 88 to print the burden on the system to the web error log. Line 87 subtracts out the new wallclock and CPU times, and line 88 formats the values nicely for the log. (Child-user and child-system times are discarded, since they would always be 0, as we did not fork.)

Line 24 preens the possibly messy characters from the four input parameters. Line 25 builds a ``key'' for the cache from those parameters. As it would be prohibitively expensive to regenerate the button on each page visit, we'll instead cache the generated image for up to a day (extending that if it's being accessed), and we need a unique key for this image under which to cache the image.

Speaking of the cache, this gets set up in lines 27 to 31. Cache::FileCache is part of the Cache::Cache module in the CPAN. The $cache object gives us access to a particular cache: one that autopurges items once a day, and expires those items after one day by default. We define a particular namespace so that items from this cache don't collide with any other possible cached items. Note that the interface of the Cache::Cache suite means that we could easily change this to a memory-based cache or a per-process cache simply by using a different package instead of Cache::FileCache.

Line 33 declares the $image variable that will eventually have either a cached image or a newly-created image.

Lines 34 to 37 attempt to fetch this image from the cache. First, the cache is asked for the unique key for the desired image. If the item is present, then the expiration time is updated to extend its life for another day, since we are now accesssing it again. If it's not present, or for some reason expires between getting the cache object and getting its payload (in line 36), then $image remains undefined, which works fine for our use.

If the cache comes up empty, line 39 begins the code to construct the image from scratch. Line 40 brings in the Image::Magick module, which glues Perl to the ImageMagick library.

Lines 42 and 43 adjust a couple of things that I didn't want to have as user-input, but might (and did) need tweaking during development or deployment. The font is set to helvetica, and the rounded border was set to be the fontsize over 2. For a more squarish-looking button, you can make this smaller, but at this value, it tended to make buttons with semicircles on both ends (except for text with ascenders or descenders).

Lines 46 to 49 create a canvas on which to draw the text. This canvas needs to be larger than the text will render. And that was the hard part, because there's no way (that I could find) to ask ImageMagick ``how big will this render?''. So, after 100 or so tweaks, I've settled on the values as given. The width is the length of text times the pointsize, plus the border size times 2, plus a fudge of 2. (Don't ask about the fudge.) I wanted to make it smaller, but capital W's seemed to take a huge amount of space. On a typical mix of text, we need less than half that number, but you have to prepare for the worst.

The image is then ``read'' from a constant color swatch in line 50, which effectively fills the background color. The background color is set to the background color of the button. This permits the anti-aliased text to have edges that are a blend between the text color and the background color, making nice looking characters, especially at the smaller point sizes. (Antialiasing matted items is always a pain.)

And now for the part I must've tweaked 20 or 30 times: the text annotation in lines 51 to 56. The gravity value of West starts the string at the horizontal position specified by the x parameter, centered vertically on the canvas. The font gives the font, of course, at the pointsize of pointsize. And text is the text. Yes, it seems obvious now, but the docs were extremely vague, especially since I was looking at the docs for convert which tells me to draw text using Draw instead. Annotate, who knew?

And in another sort of ``and now magic happens'' moment, I figured out how to make the background transparent, in line 58. I request that whatever color occupying the corner at 0,0 be considered transparent, and the matte is set from that. Of course, if someone is crazy enough to pick the same text color and background color, this makes the entire image transparent. That may just work, but my advice is ``don't do that!''.

Line 60 crops the resulting image to within $border pixels of the text. While I was debugging the starting canvas size, I changed the ``1 and'' to ``0 and'', which left the entire original canvas alone, and I could see how much extra space had been allocated in case the text for that length was wider. I left the debugging hook in here in case you wanted to play as well.

Line 62 is effectively commented out, but it puts a red border around the entire image. This lets me see if the button values I'm setting up later are actually going to edges of rendered text or beyond. Again, commented out, but you can turn it on by changing the ``0 and'' to ``1 and''. In fact, changing both of these settings let me really fine-tune the setup size in lines 47 through 49.

Line 64 gets the width and height of the rendered text image, which includes the border space left during the crop. We need this to be able to draw the button that will go ``beneath'' the text, which gets created in line 67. Line 68 sets the background of this new image to be a value that is unlikely to appear in the wild. We just need it to be distinct so that we can pick out background color vs. drawn color later. There might be a way to simply have the whole thing be matted as transparent to begin with, but I got frustrated enough that I stopped looking, and this way works. The color is something slightly off-white, with RGB values of 253, 254, and 255 respectively, given in X Window System color format (as a lot of things are with ImageMagick).

Lines 69 through 73 draw the round rectangle button. Note that I must disable anti-aliasing for this button, or else the background color used in line 68 would ``leak through'', creating an ugly halo when we matte this against the browser's background image or color. So, we trade jaggy corners for halos. Such is the choice. (I understand PNGs can permit a true blend for mattes, in theory, but that no popular browser seems to have implemented it yet. Gotta love the net.)

Line 74 makes the background of this image be transparently matted, just as we did before (making whatever color is at 0,0 be a transparent color).

And finally, the fun work happens in line 77, to place the text onto the background button. Once we've done this, $background has the final image we want to cache and send to the browser. However, we need to get it out of the internal ImageMagick object and into a sequence of bytes. Again, through poking and prodding, I discoved the magick parameter which can be set (as I do in line 80) so that ImageToBlob (in line 81) pulls out bytes in that format. I'm using the GIF format, because that's the only popular format that supports a transparency matte. Yes, I'm probably violating the UNISYS patent by creating GIFs with unlicensed software. Just don't tell anyone.

Line 82 shoves this newly created image into the cache, so that we don't have to repeat that mess for at least a day or so for this particular button.

Line 85 dumps the image to the browser with the proper header, and we're done!

So, there you have it. Dynamically generated buttons, allowing us to edit our layout as we wish, without having to worry about firing up an image editor to create the button. For example, I would install this as /perl/buttonmaker, presuming my web server processes things in /perl as Apache::Registry scripts. Otherwise, I could put it into /cgi/buttonmaker, and it would still work, although slower. Then, to make a button referring to /perltraining/, I could simply include in an HTML page:

  <a href="/perltraining/"><img border=0
    src="/perl/buttonmaker?text=Perl+Training"
    alt="[Perl Training]"></a>

Of course, the best way to do this consistently is with some templating engine. For example, on pages that are processed with Template Toolkit, I could simply define in my template:

    [%- BLOCK button; 
      USE perlbuttonmaker = url('/perl/buttonmaker'); 
      '<a href="'; 
      where | html; 
      '"><img border=0 src="'; 
      perlbuttonmaker(text = label) | html; 
      '" alt="['; 
      label | html; 
      ']"></a>'; 
    END -%]

and then use:

    [% INCLUDE button
      where = "/perltraining/"
      label = "Perl Training" %]

to make that button. Cool. The idea that inspired me to write this column was suggested by Perrin Harkins, a marvel at Template Toolkit and mod_perl (and templating engines in general), after complaining that PHP has a mechanism to make graphical headlines, which mod_perl lacks. Thanks for the idea, Perrin. Until next time, enjoy!

Listing

        =1=     #!/usr/local/bin/perl
        =2=     use strict;
        =3=     $|++;
        =4=     
        =5=     my $q = do {
        =6=       if ($ENV{MOD_PERL}) {
        =7=         require Apache::Request;
        =8=         Apache::Request->new(Apache->request);
        =9=       } else {
        =10=        require CGI;
        =11=        CGI->new;
        =12=      }
        =13=    };
        =14=    
        =15=    ## inputs
        =16=    my $text = $q->param('text') || "submit";
        =17=    my $fgcolor = $q->param('color') || "blue";
        =18=    my $bgcolor = $q->param('bg') || "lightblue";
        =19=    my $size = $q->param('size') || 18;
        =20=    
        =21=    ## start the clock
        =22=    my @times = (time, times);
        =23=    
        =24=    tr/\020-\176//cd for $text, $fgcolor, $bgcolor, $size;
        =25=    my $key = join "\n", $text, $fgcolor, $bgcolor, $size;
        =26=    
        =27=    require Cache::FileCache;
        =28=    my $cache = Cache::FileCache->new
        =29=      ({default_expires_in => 86400,
        =30=        auto_purge_interval => 86400,
        =31=        namespace => 'button'});
        =32=    
        =33=    my $image;
        =34=    if (my $obj = $cache->get_object($key)) {
        =35=      $obj->set_expires_at(time + 86400); # keep it fresh
        =36=      $image = $obj->get_data;      # still might fail, perhaps
        =37=    }
        =38=    
        =39=    if (not $image) {
        =40=      require Image::Magick;
        =41=      ## configuration constants
        =42=      my $border = $size / 2;
        =43=      my $font = 'helvetica';
        =44=    
        =45=      ## first, draw the text so that we know how big to make the button
        =46=      my $label = Image::Magick->new
        =47=        (size => sprintf("%dx%d",
        =48=                         $size * length($text) + $border * 2 + 2,
        =49=                         $size * 2 + $border * 2));
        =50=      $label->Read("xc:$bgcolor");
        =51=      $label->Annotate(gravity => 'West',
        =52=                       font => $font,
        =53=                       fill => $fgcolor,
        =54=                       pointsize => $size,
        =55=                       x => $border + 1,
        =56=                       text => $text);
        =57=      ## make background transparent:
        =58=      $label->Draw(primitive => 'matte', points => '0,0', meth => 'replace');
        =59=      ## change to "0 and" to see original canvas (for tweaking size above)
        =60=      1 and $label->Crop("0x0+$border+$border");
        =61=      ## debugging (change to "1 and") to see bounding box:
        =62=      0 and $label->Border(color => 'red', geom => '1x1');
        =63=      ## get size for the button
        =64=      my ($w, $h) = $label->Get(qw(width height));
        =65=    
        =66=      ## now, draw the button to be the size of the text
        =67=      my $background = Image::Magick->new(size => $w . 'x' . $h);
        =68=      $background->Read('xc:#fdfeff');  # unlikely color
        =69=      $background->Draw(prim => 'roundRectangle',
        =70=                        antialias => 0, # prevent partial background fluff
        =71=                        fill => $bgcolor,
        =72=                        points => sprintf("0,%d %d,0 %d,%d",
        =73=                                          $h - 1, $w - 1, $border * 2, $border));
        =74=      $background->Draw(primitive => 'matte', points => '0,0', meth => 'replace');
        =75=    
        =76=      ## and put the text on the button
        =77=      $background->Composite(image => $label);
        =78=    
        =79=      ## render it
        =80=      $background->Set(magick => 'gif');
        =81=      $image = $background->ImageToBlob;
        =82=      $cache->set($key, $image);
        =83=    }
        =84=    
        =85=    print "Content-type: image/gif\n\n$image";
        =86=    
        =87=    @times = map { $_ - shift @times } time, times;
        =88=    printf STDERR "buttonmaker: real=%d user=%.2f sys=%.2f\n", @times;

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.