dcsimg

Who’s Got the Button?

Who has time to make those cute little graphic buttons for their Web site -- especially when you're rede- signing it and are changing the text(or, perhaps the text varies sometimes)? Well, I was faced with that issue the other day while contemplating Yet Another Redesign for my Web site at perltraining.stonehenge. com. I wanted 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.

Who has time to make those cute little graphic buttons for their Web site — especially when you’re rede- signing it and are changing the text(or, perhaps the text varies sometimes)? Well, I was faced with that issue the other day while contemplating Yet Another Redesign for my Web site at perltraining.stonehenge. com. I wanted 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 it was just for the rounded corners of table-ish buttons. This time, I was much more ambitious; I wanted to go whole hog and do the entire thing as a single 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 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 is mostly absent.

So, after invoking the convert command about 200 to 300 times, slowly varying the parameters, trying new things, and then trying to figure out how to convert that to the Perl bindings, invoking it again some 200 or so times (I’m really not joking about these numbers but wish I were), I came up with Listing One .




Listing One: Buttonmaker — Part I


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(“0×0+$border+$border”);
61 ## debugging (change to “1 and”) to see bounding box:
62 0 and $label->Border(color => ‘red’, geom => ’1×1′);
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;

Please don’t ask me to fully explain the mappings between the code in Listing One and the convert lines I issued before; it all appears magical to me. I don’t think anyone ever actually writes a PerlMagick program — they just start with the examples in the distribution test suite and cut-and-paste until something finally ends up working. I had to search through Magick.xs (the Perl binding code to the Magick library) at least a dozen times before finding some of the parameters to even try.

In the hope 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.

Listing One is meant to be used either as a CGI program or under mod_perl‘s Apache::Registry. Obviously, if your Web site is taking a heck of 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, 72 to the inch) for the text. Note that the size of the button in pixels depends on whether the text has ascenders, descenders, or both. (This messes up a series of uniform buttons; maybe some kind person out there can tell me how to trim left-to-right without trimming top-to-bottom with ImageMagick so I can fix the height.)

Line 22 starts a stopwatch for real (wall clock) time and CPU times. We use these numbers in lines 87 and 88 to print the burden on the system to the Web error log. Line 87 subtracts out the new wall clock 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); we need a unique key for this image to cache it.

Lines 27 through 31 is where we get to set up the cache. 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 them after one day by default. We define a particular namespace so that items from this cache don’t collide with any other possibly 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 package other than Cache::FileCache.

Line 33 declares the $image variable that will eventually hold 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 accessing it again. If it’s not present, or for some reason expired between getting the cache object and getting its payload (in line 36), then $image remains undefined, which is fine for our purposes.

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 I didn’t want to have as user-input, but might (and did) need to tweak during development and deployment. The font was set to Helvetica, and the rounded border was set to be the fontsize divided by two. For a more squared-looking button, you can make this smaller, but this value tends to make buttons with semicircles on both ends (except for text with ascenders and/or descenders).

Lines 46 to 49 create a canvas on which to draw the text. This canvas needs to be larger than the size the text will take up. This was the hard part, because there’s no way (I could find) to ask ImageMagick, “How big will this render?” So, after 100 or so tweaks, I settled on the values as given. The width is the length of text times the pointsize, plus the border size times two, plus a fudge of two. (Don’t ask about the fudge.) I wanted to make it smaller, but capital Ws take up 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 of the text color and the background color, making some pretty nice looking characters, especially at some of the smaller point sizes. (Anti-aliasing matted items is always a pain.)

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 (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 actually 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 it!”

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 want 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 the edges of or beyond the rendered text. You can turn it on by changing the “0 and” to “1 and.” In fact, changing both of these settings allowed me to fine-tune the setup size back 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 is 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 we can pick out background color vs. drawn color later. There might be a way to matte the whole thing to begin with, but I got frustrated and stopped looking (and, anyway, this 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 are a lot of things with ImageMagick).

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

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

Finally, the fun work happens in line 77, where the text is placed on the background button. Once we’ve done this, $background has the final image that we want to cache and sends 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 discovered the magick parameter, which can be set (as in line 80) so that ImageToBlob (in line 81) pulls out bytes in that format. I’m using the GIF format, because it’s the only popular format that supports a transparency matte. Yes, I’m probably violating the Unisys patent by creating GIFs with unlicensed software. Don’t tell anyone.

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

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

There you have it — dynamically generated buttons that allow us to edit our layout as we wish, without having to worry about firing up an image editor to actually create the buttons. For example, I would install this as /perl/buttonmaker, presuming that 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 a 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.

I was inspired to write this code by Perrin Harkins, a marvel at Template Toolkit and mod_perl (and templating engines in general), after he complained that PHP has a mechanism to make graphical headlines that mod_ perl lacks. Thanks for the idea, Perrin. Now you have absolutely no excuse to procrastinate updating your Web layout. Until next time, enjoy!



Randal L. Schwartz is the chief Perl guru at Stonehenge Consulting and co-author of Learning Perl and Programming Perl. He can be reached at merlyn@stonehenge.com. Code listings for this column can be found at: http://www.stonehenge.com/merlyn/LinuxMag/.

Comments are closed.