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 13 (May 1997)

I'm not a real big fan of visitor counts. For one, they aren't really accurate... because of proxies and reloads, you could have an artificially high or low value. Second, what do they really communicate to the person browsing the site? Obviously, if I'm coming there, I'm either interested in the information or not. I'm not about to hit a web page, notice that the visitor counter is over 22,435, and then leave because too many people have been there!

So, in response, I created my ``non visitor counter'' (NVC). Every time someone reloads my home page (at http://www.stonehenge.com/merlyn/), a random new number from 1 to 99,999 is generated, and then the appropriate image (or text) is displayed. And, even though this demonstration is pointless (except for its humor value), some of the tricks I use are applicable in other areas, so stay with me here.

To get the NVC into the page in the first place, I use ``server side includes'' (SSI). This means that the server (Apache here) scans through the document, looking for items like:

        <!--# ... -->

and replaces them with other text as the server delivers the document to the requesting client (like a browser). The client never sees these lines, and because of this, it's a little hard to look for examples on the net, because you won't see them when you view the source.

In particular, what you'll never see is what I see when I edit the file is stuff like the following:

        <p>You are not likely to be the
        <!--#include virtual="/cgi/random_visitor_th" -->
        visitor to my page.

Here, a specific SSI directive ``include virtual'' (not the real name, but let that one slide) causes the CGI script random_visitor_th to be executed, and its output to be inserted in place of the SSI directive. One sample output of this script is:

        Content-type: text/html
        
        <img src="/cgi/bigword?29412th" alt="29412th">

Now, the body of that message is then inserted, replacing the SSI directive, so the user finally sees:

        <p>You are not likely to be the
        <img src="/cgi/bigword?29412th" alt="29412th">
        visitor to my page.

Let's see how random_visitor_th looks, in [Listing one].

Line 1 is the standard ``shebang'' line. No options this time. Didn't care much for them, apparently. Line 2 scrambles the random number generator, seeding it with a bitwise exclusive-or between the current process ID number ($$), and the result of the time operator.

Line 3 computes a random visitor number, by invoking the rand operator with a parameter of 100,000. This will generate a floating value somewhere between 0 and 99999.9999, which when truncated with the int operator, is exactly what we need.

The next step is to transform this random visitor number into its ordinal form. The normal english rules apply here, as implemented in the block extending from line 4 to line 10. Each line works similarly: if the substitute is completed, the block gets exited. (Who keeps saying we need a case statement?) The comments indicate the cases handled.

So, after the block is complete in line 10, $VISITOR is a good ordinal word. Next, it's time to generate the output for the SSI processor, handled in lines 11 through 15.

Line 11 is a print operator, with its sole argument being a ``here'' string, extending from line 12 to line 14. This is often easier than typing a series of print operations, although here it would have been a close draw.

Line 12 declares the MIME type of the output. The output type must be text/html, because we are including it into an HTML document. The only other valid header might have been a ``location:'' redirect, which would cause the server to go fetch that item instead. (I showed an example of that a few columns back.)

Notice that the final output text (on line 14) includes both an ``img src='' and an ``alt'' tag. Having both of those is very important. On browsers that aren't displaying graphics (such as lynx, w3-mode, or any of the graphical browsers with image loading turned off), the ``alt'' text provides exactly the right information to read the line.

On browsers that do have image loading turned on, the browser turns around and does one more fetch back to the server. This time, it asks for ``/cgi/bigword?29412th'', invoking my second script, bigword. This script takes its one argument, and returns a GIF of the argument displayed as a word in large type. Not only that, but each letter is randomly jiggled up and down a little bit, just like those stupid ``odometer'' visitor counters I've stumbled across.

Now, the bigword script does its drawing maching using the GD module. I'm no expert on GD; in fact, this is my first little toy program with it. However, I was able to get the program up and going in about 15 minutes, which tells me that it works the way I would expect it to work, and that's a good thing.

The GD module is really just a wrapper around the industrial-strength GD library (included in the GD module distribution) written by Thomas Boutell (www.boutell.com). Thomas is probably best known as the author of the comp.infosystems.www.authoring.cgi newsgroup FAQ. With the library, you can read and write images (generally GIFs), and then scribble on them, or make up new ones from scratch. The scribbling can include lines, polygons, roundish-things, and (most importantly for my program), text in four sizes.

Now, even though the GD library comes with the GD module, the GD module does not come with the Perl distribution. So if you don't already have GD.pm (and friends) in your @INC path, you'll need to fetch it from the CPAN (http://www.perl.com/CPAN/ or http://www.perl.org/CPAN/) under modules/by-module/GD/. Get the latest one.

Let's see how that works in [Listing two].

Lines 1-4 here precede all of my ``bigger than a screenful'' CGI programs (which this one clearly is).

Line 6 pulls in the GD.pm module, which in turn causes a ``dynamic load'' of the GD library. This means that the running Perl binary now has additional C-based code, not just additional Perl code.

Line 8 spins the dials for the random number generator. Here, I thought it simple enough just to use the default arg to srand.

Line 10 declares a file-lexical variable named $num, the word to be printed ($num is just an artifact that it had only been a number in other versions of this program).

Lines 12 through 15 make this program easier to debug. If the $< variable is equal to 60001 (the user nobody on my machine), then the script is being run under a webserver. If not, then I'm probably running this script interactively. If I'm running it interactively, I don't want to output the MIME header (here, image/gif), and there's no point in killing off STDERR. So I don't.

Lines 17 to 21 grab the only parameter to this CGI script by scanning the QUERY_STRING environment variable for up to 30 alphanumeric characters. This value will be equal to whatever is found after the ? in the URL. If a bad format is detected, the word ``bogus'' is used, but otherwise, we run with it.

Lines 23 to 25 define the font of my output. gdLargeFont is a constant value defined in the GD library, and returns back a font object, for which I also get its width and height for reference.

Line 27 defines a ``jiggle'' constant, which controls how far up and down each letter can randomly be placed. I define it here, because I need it to figure out the overall GIF size, computed in lines 28 and 29. The ``* 1'' was just for symmetry, and actually an early errant version of this program had ``* length($num)'' there before I realized I was ending up with a square instead of a rectangle. Duh.

Line 31 creates the ``image object''. The call to GD::Image->new creates an empty canvas of the indicated size. Line 32 declares a color of value RGB = (127,127,127) (out of 255), which is kind of an ugly medium gray. Now, the user doesn't actually see this, unless their browser doesn't support transparent GIFs, so the color really doesn't matter. Line 33 makes this color the transparent color (there can be only one transparent color in an image). (I commented this out during testing to make sure I was allocating a big enough image space.)

Line 34 enables interlacing, pretty pointless for a tiny GIF like this, but in case I re-use this code on a bigger thing, I'm already home free. Line 35 declares a ``red'' color with RGB = (255,0,0). I'll use this to write the actual text.

Lines 37 to 43 perform the actual image creation. Line 37 starts a ``left edge'' value at an x-value of 1 (one pixel in from the edge, so we'll always have at least one pixel border). Line 38 breaks up the string into a list of strings, one character per element in the list.

Lines 39 through 43 are performed once for each character in the original string. Now line 39 is best read as ``while there are things in @chars'', because that's essentially what happens. To use a loop like this, I must be very careful to shorten the size of @chars somewhere in the loop, or else I'm gonna have a pretty long wait (like forever).

Line 40 takes care of shortening @chars, by taking its first element and shifting it off into $char. Of course, now looking at this, I could have also written this as a foreach loop. Yes, as the Perl motto goes, ``there's more than one way to do it.''

Line 41 is where the good stuff happens. A character is placed into the image, in the indicated font $font, at the computed x and y position, using the indicated text and color. The y position is 1 (for the border) plus a jiggling amount, causing the letter to move up a random amount. Then, line 42 advances the X position an appropriate amount for the next pass.

Finally, line 45 dumps the image as a GIF. In this case, it'll be a GIF89a, because I requested transparency and interlace. And that's all there is to it!

Now, obviously, bigword can be used for more things than just the random visitor count, although you'd probably want to make the size and color user selectable, as well as turn off that jiggle somehow. Sounds like it could be a nice general-purpose program. (Hmm. Maybe in a future column?) See you next time.

Listings

        =0=     ### LISTING ONE [random_visitor_th]
        =1=     #!/usr/bin/perl
        =2=     srand ($$ ^ time);
        =3=     $VISITOR = int rand 100_000;
        =4=     {
        =5=       next if $VISITOR =~ s/1\d$/$&th/; # 10-19
        =6=       next if $VISITOR =~ s/1$/1st/; # 1
        =7=       next if $VISITOR =~ s/2$/2nd/; # 2
        =8=       next if $VISITOR =~ s/3$/3rd/; # 3
        =9=       next if $VISITOR =~ s/\d$/$&th/; # everything else
        =10=    }
        =11=    print <<"EOT";
        =12=    Content-type: text/html
        =13=    
        =14=    <img src="/cgi/bigword?$VISITOR" alt="$VISITOR">
        =15=    EOT
        =0=     ### LISTING TWO [bigword]
        =1=     #!/home/merlyn/bin/perl -wT
        =2=     use strict;
        =3=     $|++;
        =4=     $ENV{PATH} = "/usr/bin:/usr/ucb";
        =5=     
        =6=     use GD;
        =7=     
        =8=     srand;
        =9=     
        =10=    my $num;
        =11=    
        =12=    if ($< == 60001) {
        =13=      open STDERR, ">/dev/null";
        =14=      print "Content-type: image/gif\n\n";
        =15=    }
        =16=    
        =17=    if ($ENV{'QUERY_STRING'} =~ /^(\w{1,30})$/) {
        =18=        $num = $1;
        =19=    } else {
        =20=        $num = "bogus";
        =21=    }
        =22=    
        =23=    my $font = gdLargeFont;
        =24=    my $char_x = $font->width;
        =25=    my $char_y = $font->height;
        =26=    
        =27=    my $jiggle = 4;
        =28=    my $picture_x = (1 + $char_x) * length($num) + 1;
        =29=    my $picture_y = (1 + $char_y) * 1 + $jiggle;
        =30=    
        =31=    my $image = new GD::Image($picture_x, $picture_y);
        =32=    my $background = $image->colorAllocate(127,127,127);
        =33=    $image->transparent($background);
        =34=    $image->interlaced('true');
        =35=    my $red = $image->colorAllocate(255,0,0);
        =36=    
        =37=    my $x = 1;
        =38=    my @chars = split //, $num;
        =39=    while (@chars) {
        =40=      my $char = shift @chars;
        =41=      $image->string($font,$x,int(1+rand($jiggle)),$char,$red);
        =42=      $x += $char_x + 1;
        =43=    }
        =44=    
        =45=    print $image->gif;

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.