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 60 (Apr 2001)

[suggested title: Embedding a dynamic image in CGI output]

One frequent sort of confusion with CGI scripting is how to return a dynamic image as a part of the response. I get questions like ``how do I embed the image in my HTML'' or ``how do I return two different MIME types in one response'' all the time. Well, you can't.

But you can have a CGI response have a graphic that changes on the basis of the query. So how is it done then? Simply put, the HTML response for the CGI query embeds an IMG tag to call back to another CGI invocation (either the same script, or a companion script), with enough information in that invocation to generate the appropriate image.

The key here is ``enough information''. The first CGI response could calculate everything needed to generate the picture, and save the image into a temporary directory on the server under a unique name, then return back the URL for that name. Or, as we're doing here, the image-fetching URL can contain all the information itself to generate the image. The first method requires that you clean up the temporary directory frequently, while the second really suffers if someone presses ``reload'' if the graphic is expensive to calculate, so as with everything else on the web, there's tradeoffs to be made.

As a simple example of the latter ``no server side storage'' strategy, let's take a look at a simple application. A nice module to spit out various barcode types showed up on the CPAN recently, and I wanted to play with it. The module uses the GD library to generate a PNG, so I wanted a webform that would let me modify the parameters, call the barcode generation, and return the result. But I wanted the form to be persistent so I could just fiddle with things. Simple enough: let the response be another form (using CGI.pm's sticky fields), and embed the image using a repeat invocation to the same script, this time generating the barcode image. And that leads us to [listing one, below].

Lines 1 through 3 start out with my usual CGI trio, enabling warnings, turning on taint mode, enabling the compiler restricts, and disabling buffering on STDOUT.

Line 5 pulls in the CGI module, along with all the HTML-generation shortcuts.

Lines 7 through 44 handle the ``get the inline image'' invocation of this script. I'm going to set that aside for the moment, because it makes more sense to talk about this program in the order that things typically happen.

Lines 47 through the end of the program handle the ``normal'' invocation of this script. Line 47 in particular dumps out the HTTP header, and the HTML head section, along with a nice H1 labelling the page.

Lines 49 to 53 are executed only if this is a response to a previous form submission. Again, I'll set this aside for now, and plow on in the main code.

Lines 55 to 70 display a form, using the typical form-generation stuff. No rocket science here. Note that the form action URL defaults in line 57 to this script itself. This is handy for persistent sticky fields, and because we don't have to install more than one script.

Lines 58 to 69 generate a table for layout (mostly because CSS isn't supported well enough in the browsers I use to avoid this yet) for the four form fields needed for barcode generation.

The text field in line 59 requests the input data for which we are generating bar code. Here, I'm defaulting on the initial invocation to the classic UPC-A ``string of digits'' often used for testing. The checksums for many of the barcodes are generated automatically: in particular, I didn't need to add the trailing ``5'' to this number, as it came out automatically.

Lines 60 through 65 select the particular barcode type, by enumerating all possible types (as of this writing) supported by the barcode suite. The default UPCA is also selected, to generate UPC-A codes. Most of these codes are numeric only, although I do no checking in the response. I leave that to the particular barcode module.

Lines 66 to 67 grab the optional Height parameter. All of the modules permit such a parameter passed in, and alter the generated barcode from its default of 50 (at least, that seemed to be the consistent default).

Lines 68 and 69 permit the selection of the NoText parameter, which if true, seems to suppress the human-readable text at the bottom of the barcode.

Line 70 generates the end of the form with the mandatory submit button, as well as closing out the HTML output.

On first invocation, we thus get the nice form, and we come back into the same script, recreating the same form, but in addition, the code in lines 49 to 53 now fires, because we have a form parameter. Line 51 creates a URL from many pieces.

First, we have the initial part of the URL being the same as our own CGI invocation. This means the URL when followed will trigger this same script.

Next, we have a slash, followed by the Unix epoch time as an integer. This number is 900-million something as I write this, and rolls over to one billion sometime in 2001 (a dangerous time for some Unix software that won't handle this correctly). We insert the timestamp as part of the URL to make a ``time-limited'' URL. More on that in a moment.

Then comes the process ID followed by .png. I didn't really need the process ID here, except to create an extremely unique URL even if this script is invoked twice in the same second. The program would still work fine without it, but if I were storing some state server-side, I'd have to narrow down which invocation of the script was coming back for an image. So consider this ``defensive programming''. The png at the end of the URL keeps certain non-standard browsers from ignoring the response MIME type and displaying it any old way they want.

So far, we've got a URL something like:

  http://www.stonehenge.comm/cgi/barcode/994410023.12345.png

And this will invoke the barcode script passing it a path_info value of /994410023.12345.png, even though it looks like it just might be a normal PNG file to the browser.

Now for the fun part. We append to that URL a question mark followed by the query string, containing all the form parameters. So, if ``text'' was ``HELLO'' and bartype is UPCA, we get:

  http://www.stonehenge.comm/cgi/barcode/994410023.12345.png?text=HELLO&bartype=UPCA

So, not only will that CGI invocation be able to notice that there's some extra path info, but there's also some form data there. And access to the form data gives us the particular desired bar code parameters!

The HTML generated by line 52 consists of an anchor link around an image. The image URL is the constructed as above. However, by placing this inside a link, we can also click on the image to get the image itself, making it easier for most people to save the image using 'save as' on the browser.

And there's where the ``time of day'' becomes valuable. Suppose someone saves that URL into a web page. Every reference to that web page (even if it's not on our server) causes the browser to fetch the URL from this CGI script. And since this is a computed image (not a trivial task), we'd be forever burning CPU on someone else's behalf! The way to defeat this is to notice the time of day in the URL. If it's older than some number of seconds (like 60), then it wasn't a browser kicking back in response to a recently generated page, so we bomb out.

The timeout is handled in lines 10 to 14. We first get the ``path info'' information in line 7. If that's not the right format, or the timestamp is not within 60 seconds prior to now, line 12 triggers the 404 error, appearing to the requestor as if nothing at all exists at that URL: a sensible response.

But I skipped over line 8. What's happening there? Lines 8 to 30 form an error block. If something weird happens (like a trappable death), or we don't somehow execute a exit before reaching the end of the block, the error text is captured into $error. If we've made it that far, we're probably expected to be an image of some kind (after all, we've been invoked with something in our ``path info''). So we can't just switch gears and dump this error message as text now!

So instead, lines 32 to 41 make up an image containing the string from the error. First, we pull in the GD module in line 33, select the builtin ``large'' font in line 36, and make an image of the right size in line 37. Next, line 38 allocates a ``netscape grey'' background, making it transparent in line 39, and allocates a pure red color in line 40. The string is inserted into the image in line 41, and we dump the image as a PNG in line 42.

Well, that handles the errors from death, or returned as the last line of the error block. But what's the normal execution through there? After all, the whole point of this program is to generate barcode: isn't it time we did that? Yup. Line 16 pulls in the top-level barcode module.

Lines 17 and 18 try to create a barcode object, based on the incoming bartype and text parameters. The regular expression ``untaints'' the bartype by limiting input to word-type characters, and the scalar ensures that an artificially constructed list of text elements won't end up as multiple parameters to new. We'd never do that to ourselves, but evil people out there might try anything to break in, so never trust the input data.

If we've got a good value in $bc, we can proceed to generate the code. Lines 20 through 24 pull out the Height and NoText parameters, but only if they are simple integers. Again, distrust the input in a very conservative way.

Finally, line 25 takes the barcode object, plots it with the requested parameters (which returns a GD::Image object), and converts that to a PNG, dumping it out with the right HTTP header and MIME type.

If line 17 fails to produce a good barcode object, the documentation for the module says that we should look at $GD::Barcode::errStr for the reason, which then becomes the return value of the error block and ends up in $error in line 8, and thus as the nice little imaged error message. The most frequent two error messages I got while I was playing were Invalid Character and Invalid Length, for the obvious reasons.

And that's all there is. Sorry for the bouncing around in describing the program, but I wanted to talk about it in the typical invocation order.

This column marks my 60th contribution to WebTechniques magazine, a full ``five year mission'' completed. It's been an honor to be with the magazine from its inception, and I hope you've found at least a few tidbits along the way to help you program for the web, or perhaps just get better at Perl hacking. Thank you for all the email confirming that, as well. Until next time, enjoy!

Listings

        =1=     #!/usr/bin/perl -Tw
        =2=     use strict;
        =3=     $|++;
        =4=     
        =5=     use CGI ":all";
        =6=     
        =7=     if (length (my $info = path_info())) { # I am the image
        =8=       my $error = eval {            # begin Error Block
        =9=     
        =10=        unless ($info =~ m{\A/(\d+)\.\d+\.png\z} and
        =11=                $1 <= time and $1 >= time - 60) { # no bookmarking!
        =12=          print header(-status => '404 Not Found');
        =13=          exit 0;
        =14=        }
        =15=    
        =16=        require GD::Barcode;
        =17=        if (my $bc = GD::Barcode->new(param('bartype') =~ /\A(\w+)\z/, # untaint
        =18=                                      scalar param('text'))) {
        =19=          ## good barcode, go for plot
        =20=          my %parms;
        =21=          for (qw(Height NoText)) {
        =22=            $parms{$_} = $1 if
        =23=              defined param("p_$_") and param("p_$_") =~ /\A(\d+)\z/;
        =24=          }
        =25=          print header('image/png'), $bc->plot(%parms)->png;
        =26=          exit 0;
        =27=        }
        =28=        $GD::Barcode::errStr;
        =29=    
        =30=      } || $@;                      # end Error Block
        =31=    
        =32=      ## make up an image from the error message
        =33=      require GD;
        =34=      ## fix $error here to contain clean chars?
        =35=    
        =36=      my $font = GD::gdLargeFont();
        =37=      my $image = GD::Image->new($font->width * length $error, $font->height);
        =38=      my $background = $image->colorAllocate(127,127,127);
        =39=      $image->transparent($background);
        =40=      my $red = $image->colorAllocate(255,0,0);
        =41=      $image->string($font, 0, 0, $error, $red);
        =42=      print header('image/png'), $image->png;
        =43=      exit 0;
        =44=    }
        =45=    
        =46=    ## I am the form
        =47=    print header, start_html("Bar Code sampler"), h1("Bar Code sampler");
        =48=    
        =49=    ## inline image reference if this is a response
        =50=    if (param) {
        =51=      my $url = url()."/".time.".$$.png?".query_string();
        =52=      print a({href => $url}, img({src => $url}));
        =53=    }
        =54=    
        =55=    ## form
        =56=    print
        =57=      hr, startform,
        =58=      table({border => 0, cellspacing => 0, cellpadding => 2},
        =59=            Tr(td("text"), td(textfield("text", "01234567890"))),
        =60=            Tr(td("barcode type"),
        =61=               td(radio_group(-name => 'bartype',
        =62=                              -values => [qw(COOP2of5 Code39 EAN13 EAN8 IATA2of5
        =63=                                             ITF Industrial2of5 Matrix2of5 NW7
        =64=                                             UPCA UPCE)],
        =65=                              -default => 'UPCA', -rows => 3))),
        =66=            Tr(td("height in pixels", br, "(leave blank to default)"),
        =67=               td(textfield("p_Height"))),
        =68=            Tr(td("suppress text (1 = true)"),
        =69=               td(radio_group("p_NoText", [qw(0 1)])))),
        =70=      submit, endform, hr, end_html;

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.