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 53 (Sep 2000)

[suggested title: Getting image colors to text]

I've been participating in the perlmonks.org website for the past few months. It's a nice setup, based on some discussion software that resembles slashdot.org in many ways, although I think it uses a different codebase. One day recently, someone posted a comment that pointed me at a curious web page, made up of a series of characters of varying colors forming an image when taken as a whole. I thought this rather interesting, and examined the source to see that it was huge pile of HTML that looked like:

  <font color="#ff00ff">X<font color="#ffff00">Y

which would generate a purple X followed by a yellow Y. Now, this is illegal HTML, since the font tags are not closed, but my browser didn't seem to complain. I complained at the download time, but that was another matter.

In the thread of messages that followed, it was revealed that the curious page was actually the product of png2html, which can be found currently at http://www.engr.mun.ca/~holden/png2html.html. I downloaded the code, and screamed that there was certainly no point in using C to do something I could easily do in Perl. png2html used the famous graphics library called GD, which had a Perl binding, so I thought I would just create a new version in Perl, and publish it here.

Well, I started with GD (actually, the GD perl module found in the CPAN, written by fellow WebTechniques columnist Lincoln Stein), and realized that I would have to generate a PNG for input. I got a version working that emulated png2html but was mad at having to invoke an external command (convert, from ImageMagick distribution) to get my JPEG file into PNG format and in the proper size. Then it occurred to me: I could use the PerlMagick interface of ImageMagick, pull in a file of nearly arbitary image format, perform the scaling, and then extract the needed RGB values all without an external program!

And I got a version of that working, but was still upset by the illegal HTML being generated, so I recalled an earlier program that converted an image into a series of appropriately colored table cells, and decided to shift to that. And wouldn't you know, I now have a nice legal HTML output that splashes interesting text in front of an image, even though the image is really just the background colors of various table cells. A sample is shown in col53-fig.gif. The image is of the rare occasion when I found myself in a tux, while attending a formal banquet on board the ms Vollendam off the coast of Alaska, as part of the PerlWhirl 2000 activities.

And the code to make this weird and wacky HTML-only image is given in [listing 1, below].

The usual quartet of lines begins this program. Line 1 enables taint mode (my usual for CGI programs) and warnings. Line 2 enables compiler restrictions, requiring most variables to be declared before use, and disabling both ``poetry mode'' and symbolic references. Line 3 disables buffering, typically to ensure that CGI headers are not out of order with child-process output. And line 4 cleans up the PATH environment variable so that we can launch child processes.

The configuration section from lines 6 through 10 come next. The $WIDTH scalar defines the number of table columns. $IMAGE gives the path to the image being read in ImageMagick format: the type of the image must either be clear from the suffix, or from a colon-prefix. See the ImageMagick documentation for details.

Line 9 defines one or more files that will provide the overlay text. These files will be sucked in, and all non-printing characters will be discarded. The text will be repeated as necessary, so a short file here is no concern. For grins, I'm referencing the horrible piece of code I wrote more than a decade ago, which unbeknownst to me was added to the Perl distribution, then other people wrote code using it, so it had to stay, and well, the rest is history. The file is located automatically using @INC so that you don't have to do it yourself. And I'd advise you not to ask further questions about chat2.pl -- just look at Expect in the CPAN instead.

Line 12 brings in the CGI module, with all the shortcuts (allowing me to avoid the awkward and verbose OO invocation notation). Line 13 pulls in CGI::Carp, enabling me to see error messages in my browser rather than having to hunt them down in the web error log. Note that this is dangerous, and a potential security violation, so don't put this into production code. As always, I'm showing you technique here, not ready-to-run code, so bite the idea -- don't idolize the bytes.

Line 15 pulls in the Image::Magick module, commonly known as PerlMagick in every way except in the documentation and the code. This is a module found in the CPAN, and is a bit persnickity to install, so I'd recommend getting the ImageMagick distribution directly instead, and installing whatever PerlMagick comes with that particular distribution, rather than doing it separately.

Line 16 creates an Image::Magic object to hold the image we're sucking in. If death is called for here, it'll be redirected as a browser message (thanks to CGI::Carp earlier), so it's now safe to die.

Line 18 reads the image defined in the configuration. Any of the ImageMagick formats are acceptable here. The return value from the Read method is placed into $_, which should normally be empty. If there's something in there, it's an error, and we'll die with the message for reference.

Likewise, line 20 sets the output format for the Blob output later. The rgb format sets the output as 3 byte r-g-b tuples which we can easily break apart later.

Line 22 performs the scaling needed to bring most pictures down to a reasonably small number of horizontal bits. The width and height are both maximums, with the actual picture being scaled proportionally so that it fits within the box. Thus, the 1000 is just an upper reasonable bound, or perhaps unreasonable given the size of the output.

Lines 24 and 25 extract the width and height of the resulting image. We'll need that to generate the table, and know how to break up the r-g-b bytes coming out of the blob.

Line 27 is the centerpiece of the use of ImageMagick here. The ImageToBlob converts the image from whatever internal format it is into a series of r-g-b bytes. The unpack streams them into a series of integers. For a 20 by 10 image, we're talking 200 tuples, or 600 integers total.

Lines 29 to 31 extract the text from the files. If no @ARGV is given, the program text itself is used by seeking the DATA filehandle back to the beginning. The special code assigned to @ARGV in line 30 duplicates the DATA filehandle.

Lines 33 to 39 process this text into a series of strings that are properly HTML entitized. Line 33 first strips all the non-printable characters from the string. Line 38 splits the string into individual characters. The map block in lines 34 to 38 identify the troublesome characters, replacing them with the right entity strings. And if nothing remains from that, we'll just use a single underscore, inserted in line 39.

Line 40 sets a rotating index variable, allowing us to walk through the array to pick up each new character each time it's used.

And once we've done all that, it's time to start dumping out HTML. Lines 42 through 45 print the required HTTP header (identifying the content as text/html) and the HTML header, given a title of Test and a text color of a light green.

Lines 47 to 62 print out the table. The first step is having the right parameters on the table tag, established in lines 48 to 50. Cell spacing and cell padding both have to be 0, even though some browsers default one or the other or both to 0. Likewise, the border also has to be 0, or we get some interesting gaps in the cells on some of the browsers I tried. Obviously, experimentation with many browsers will tell how this works, since this is a highly visual thing I'm doing. I don't imagine this text will work very well in Lynx, for example.

Line 51 also requests that the following items of the list to be handed to table all be joined with newlines. This is not strictly necessary, as table will insert spaces anyway, but I figured as long as I was putting whitespace in there, I'd at least put some whitespace that let me see the beginning of each row at the beginning of each HTML source line.

Lines 52 to 62 generate each row, by executing the block once for each element in the array from 1 to $height, and gathering the results as a list. Lines 53 to 61 generate the contents of each row, and wrap the contents in a Tr shortcut, which generates a tr tag. The unusual capitalization is needed because the HTML shortcut would otherwise conflict with the built-in tr operator.

The join in line 53 brings together all the elements of the values generated in lines 54 to 61 with no intervening whitespace. Again, I could have omitted this, and Tr would have been happy to join them all with single spaces, but I'm trying to save characters whereever I can.

Lines 54 to 61 generate each cell within a given row, again by executing a map-block once for each of the elements in the list in line 61 from 1 to the $width value. In an early version of this program, I had the current row and column available to the inner map block, but by the time I finished, I no longer needed it. The tricky part is getting the $_ from the outer loop visible in the inner loop. Well, tricky unless you consider the use of the right local variables:

  @result = map {
    my $row = $_;
    somefunc(
      map {
        my $column = $_;
        ## now use $row and $column here
      } 1..$width
    );
  } 1..$height;

Line 55 is a key line, once again. Recall that we broke the image into r-g-b triplets earlier, and placed the result into @image. Each time we execute the splice, the first three elements end up in $r, $g, and $b, thus providing us with our three values. And this just happens to be done in the right order to pull the whole image apart, since we're dumping table elements from upper left to lower right. How nice.

Line 56 figures out which text item to place in the cell. The value of $which rotates cyclically through the indicies of the @string array.

Lines 57 to 60 dump each table cell. The contents of the cell are the string selected by $which, in line 59. The background color is computed as a hex r-g-b value, created by using a hex sprintf with each of the values.

That does it for the table, and so line 64 closes down the end of the HTML.

Because we might potentially use the DATA handle earlier, we need an __END__ marker to set it up, so I placed that in line 67.

And there you have it. Not terribly useful as is, unless you want to generate a table to include in your web page, in which case you should set the configuration parameters then drop the program into a CGI bin you have. No form parameters, so you'll get one good run, which you should cut-n-paste into your desired web page.

But hopefully, besides the cuteness factor, I hope you've at least seen that you can take an image and extract the RGB pieces using ImageMagick in a fairly straightforward way. And also that you can really do some stupid tricks with HTML background colors. Until next time, enjoy.

Listings

        =1=     #!/usr/bin/perl -Tw
        =2=     use strict;
        =3=     $|++;
        =4=     $ENV{PATH} = "/usr/local/bin:/bin:/usr/bin";
        =5=     
        =6=     ## config
        =7=     my $WIDTH = 75;
        =8=     my $IMAGE = "/path/to/some_random_pic.jpg";
        =9=     @ARGV = (grep -r, map "$_/chat2.pl", @INC)[0];
        =10=    ## end config
        =11=    
        =12=    use CGI qw(:all);
        =13=    use CGI::Carp qw(fatalsToBrowser);
        =14=    
        =15=    use Image::Magick;
        =16=    my $im = Image::Magick->new or die "Cannot create im: $!";
        =17=    
        =18=    $_ and die "cannot read: $_" for $im->Read($IMAGE);
        =19=    
        =20=    $_ and die "cannot set magick: $_" for $im->Set(magick => 'rgb');
        =21=    
        =22=    $_ and die "cannot scale: $_" for $im->Scale(geometry => "${WIDTH}x1000");
        =23=    
        =24=    my $width = $im->Get('width');
        =25=    my $height = $im->Get('height');
        =26=    
        =27=    my @image = unpack "C*", $im->ImageToBlob; # rgb triples
        =28=    
        =29=    seek DATA, 0, 0;
        =30=    @ARGV = "<&DATA" unless @ARGV;
        =31=    my $string = join "", <ARGV>;
        =32=    
        =33=    $string =~ tr/!-\x7e//cd;
        =34=    my @string = map {
        =35=      /[<>&\"]/
        =36=        ? sprintf "&#%d;", ord $_
        =37=          : $_;
        =38=    } split //, $string;
        =39=    @string = "_" unless @string;
        =40=    my $which = -1;
        =41=    
        =42=    print header, start_html(
        =43=                             -title => "Test",
        =44=                             -text => '#88ff88',
        =45=                            );
        =46=    
        =47=    print table({
        =48=                 Cellspacing => 0,
        =49=                 Cellpadding => 0,
        =50=                 Border => 0,
        =51=                }, join "\n",
        =52=                map {
        =53=                  Tr(join "",
        =54=                     map {
        =55=                       my ($r,$g,$b) = splice @image, 0, 3;
        =56=                       $which = 0 if ++$which >= @string;
        =57=                       td({
        =58=                           Bgcolor => sprintf("#%02x%02x%02x", $r, $g, $b),
        =59=                          }, $string[$which]
        =60=                         );
        =61=                     } 1..$width)
        =62=                } 1..$height);
        =63=    
        =64=    print end_html;
        =65=    
        =66=    # for DATA above
        =67=    __END__

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.