Полезная информация

CGI Programming on the World Wide Web

Previous Chapter 6
Hypermedia Documents
Next
 

6.3 The gd Graphics Library

The gd graphics library, though not as powerful as PostScript, allows us to quickly and easily create dynamic images. One of the major advantages of this library is that it can be used directly from Perl, Tcl, and C; there is no need to invoke another application to interpret and produce graphic images. As a result, the CGI programs we write will not tax the system anywhere near as those in the previous section (which needed to call GhostScript). Other major advantages of the gd library are the functions that allow you to cut and paste from existing images to create new ones.

The gd library was written by Thomas Boutell for the Quest Protein Database Center of Cold Spring Harbor Labs, and has been ported to Tcl by Spencer Thomas, and to Perl version 5.0 by Lincoln Stein and Roberto Cecchini. There are ports of gd for Perl 4.0 as well, but they are not as elegant, because they require us to communicate through pipes. So, we will use Stein's Perl 5.0 port for the examples in this book.

Appendix E, Applications, Modules, Utilities, and Documentation lists URLs from which you can retrieve the gd libraries for various platforms.

Digital Clock

Here is an example of a digital clock, which is identical to the PostScript version in functionality. However, the manner in which it is implemented is totally different. This program loads the gd graphics library, and uses its functions to create the image.

#!/usr/local/bin/perl5
use GD;
$| = 1;
print "Content-type: image/gif", "\n\n";

In Perl 5.0, external modules, such as gd, can be "included" into a program with the use statement. Once the module is included, the program has full access to the functions within it.

($seconds, $minutes, $hour) = localtime (time);
if ($hour > 12) {
        $hour -= 12;
        $ampm = "pm";
} else {
        $ampm = "pm";
}
if ($hour == 0) {
    $hour = 12;
}
$time = sprintf ("%02d:%02d:%02d %s", $hour, $minutes, $seconds, $ampm);
$time_length = length($time);
$font_length = 8;
$font_height = 16;
$x = $font_length * $time_length;
$y = $font_height;

Unlike the analog clock PostScript example, we will actually calculate the size of the image based on the length of the string stored in the variable $time. The reason we didn't elect to do this in the PostScript version is because Times-Roman is not a constant-width font, and so we would have to do numerous calculations to determine the exact dimensions of our dynamic image. But with gd, there are only a few constant-width fonts, so we can calculate the size of the image rather easily.

We use the length function to determine the length (i.e., the number of characters) of the string stored in $time. The image length is calculated by multiplying the font length with the string length. The font we will use is gdLarge, which is an 8x16 constant-width font.

$image = new GD::Image ($x, $y);

Images are "created" by calling the method Image within the GD class, which creates a new instance of the object. For readers not familiar with object-oriented languages, here is what the statement means:

Note that the whole statement creating an image ends up returning a handle, which we store in $image. Now, following traditional object-oriented practice, we can call functions that are associated with an object method, which operates on the object. You'll see that below.

The dimensions of the image are passed as arguments to the Image method. An important difference between PostScript and gd with regard to drawing is the location of the origin. In gd, the origin is located in the upper-left corner, compared to the lower-left corner for PostScript.

$black = $image->colorAllocate (0, 0, 0);
$red = $image->colorAllocate (255, 0, 0);

The -> part of the function is another object-oriented idea. When you set a color, you naturally have to specify what you're coloring. In object-oriented programming, $image is the object and you tell that object to execute the method. So $image->colorAllocate is Perl 5.0's way of saying, "color the object denoted by $image." The three arguments that the colorAllocate method expects are the red, blue, and green indices in the range 0--255.

The first color that we allocate automatically becomes the background color. In this case, the image will have a black background.

$image->string (gdLargeFont, 0, 0, $time, $red);
print $image->gif;
exit(0);

The string method displays text at a specific location on the screen with a certain font and color. In our case, the time string is displayed using the red large font at the origin. The most important statement in this entire program is the print statement, which calls the gif method to display the drawing in GIF format to standard output.

You should have noticed some major differences between PostScript and gd. PostScript has to be run through an interpreter to produce GIF output, while gd can be smoothly intermixed with Perl. The origin in PostScript is located in the lower-left corner, while gd's origin is the upper left corner. And most importantly, simple images can be created in gd much more easily than in PostScript; PostScript should be used for creation of complex images only.

System Load Average

The example below graphs the system load average of the system, and is identical to the PostScript version presented earlier in the chapter. As you look at this example, you will notice that gd makes image creation and manipulation very easy.

#!/usr/local/bin/perl5
use GD;
$| = 1;
print "Content-type: image/gif", "\n\n";
$max_length = 175;
$image = new GD::Image ($max_length, $max_length);
$white = $image->colorAllocate (255, 255, 255);
$red = $image->colorAllocate (255, 0, 0);
$blue = $image->colorAllocate (0, 0, 255);

The image is defined to be 175x175 pixels with a white background. We also allocate two other colors, red and blue.

@origin = (30, 140);

This is a two-element array that holds the coordinates for the origin, or lower-left corner, of the graph. Since the natural origin is defined to be the upper-left corner in gd, the point (30, 140) is identical to the (30, 30) origin in the PostScript version. Of course, this is assuming the dimensions of the image are 175x175 pixels.

$image->string (gdLargeFont, 12, 15, "System Load Average", $blue);
$image->line (@origin, 105 + $origin[0], $origin[1], $blue);
$image->line (@origin, $origin[0], $origin[1] - 105, $blue);

We're using the string method to display a blue string "System Load Average" at coordinate (12, 15) using the gdLarge font. We then draw two blue lines, one horizontal and one vertical, from the "origin" whose length is 105 pixels. Notice that a two-element array is passed to the line method, instead of two separate values. The main reason for storing the "origin" in an array is that it is used repeatedly throughout the program. Whenever you use any piece of data multiple times, it is always a good programming technique to store that information in a variable.

for ($y_axis=0; $y_axis <= 100; $y_axis = $y_axis + 10) {
    $image->line (  $origin[0] - 5, 
                    $origin[1] - $y_axis,
                    $origin[0] + 5,
                    $origin[1] - $y_axis,
                    $blue  );
}
for ($x_axis=0; $x_axis <= 100; $x_axis = $x_axis + 25) {
    $image->line ( $x_axis + $origin[0],
                   $origin[1] - 5,
                   $x_axis + $origin[0],
                   $origin[1] + 5,
                   $blue );
}

These two for loops draw the tick marks on the y and x axes, respectively. The only difference between these loops and the ones used in the PostScript version of this program is that the origin is used repeatedly when drawing the ticks because gd lacks a function to draw lines relative to the current point (such as rlineto in PostScript).

$uptime = `/usr/ucb/uptime`;
($load_averages) = ($uptime =~ /average: (.*)$/);
@loads[0..2] = split(/,\s/, $load_averages);
for ($loop=0; $loop <= 2; $loop++) {
    if ($loads [$loop]>10) {
        $loads[$loop]=10;
        }
}

We store the system load averages in the @loads array.

$polygon = new GD::Polygon;

An instance of a Polygon object is created to draw a polygon with the vertices representing the three load average values. Drawing a polygon is similar in principle to creating a closed path with several points.

$polygon->addPt (@origin);
for ($loop=1; $loop <= 3; $loop++) {
    $polygon->addPt (    $origin[0] + (25 * $loop), 
                        $max_length - ($loads[$loop - 1] * 10)  );
}
$polygon->addPt (100 + $origin[0], $origin[1]);

We use the addPt method to add a point to the polygon. The origin is added as the first point. Then, each load average coordinate is calculated and added to the polygon. To "close" the polygon, we add a final point on the x axis.

$image->filledPolygon ($polygon, $red);
print $image->gif;
exit(0);

The filledPolygon method fills the polygon specified by the $polygon object with solid red. And finally, the entire drawing is printed out to standard output with the gif method.

Analog Clock

Remember how PostScript allows us to rotate the coordinate system? The PostScript version of the analog clock depended on this rotation ability to draw the ticks on the clock. Unfortunately, gd doesn't have functions for performing this type of manipulation. As a result, we use different algorithms in this program to draw the clock.

#!/usr/local/bin/perl5
use GD;
$| = 1;
print "Content-type: image/gif", "\n\n";
$max_length = 150;
$center = $radius = $max_length / 2;
@origin = ($center, $center);
$marker = 5;
$hour_segment = $radius * 0.50;
$minute_segment = $radius * 0.80;
$deg_to_rad = (atan2 (1,1) * 4)/180;
$image = new GD::Image ($max_length, $max_length);

The @origin array contains the coordinates that represent the center of the image. In the PostScript version of this program, we translated (or moved) the origin to be at the center of the image. This is not possible with gd.

$black = $image->colorAllocate (0, 0, 0);
$red = $image->colorAllocate (255, 0, 0);
$green = $image->colorAllocate (0, 255, 0);
$blue = $image->colorAllocate (0, 0, 255);

We create an image with a black background. The image also needs the red, blue, and green colors to draw the various parts of the clock.

($seconds, $minutes, $hour) = localtime (time);
$hour_angle = ($hour + ($minutes / 60) - 3) * 30 * $deg_to_rad;
$minute_angle = ($minutes + ($seconds / 60) - 15) * 6 * $deg_to_rad;
$image->arc (@origin, $max_length, $max_length, 0, 360, $blue);

Using the current time, we calculate the angles for the hour and minute hands of the clock. We use the arc method to draw a blue circle with the center at the "origin" and a diameter of max_length.

for ($loop=0; $loop < 360; $loop = $loop + 30) {
local ($degrees) = $loop * $deg_to_rad;
$image->line ($origin[0] + (($radius - $marker) * cos ($degrees)),
              $origin[1] + (($radius - $marker) * sin ($degrees)),
              $origin[0] + ($radius * cos ($degrees)),
              $origin[1] + ($radius * sin ($degrees)),
              $red);

This loop draws the ticks representing the twelve hours on the clock. Since gd lacks the ability to rotate the axes, we need to calculate the coordinates for these ticks. The basic idea behind the loop is to draw a red line from a point five pixels away from the edge of the circle to the edge.

$image->line ( @origin,
        $origin[0] + ($hour_segment * cos ($hour_angle)), 
        $origin[1] + ($hour_segment * sin ($hour_angle)),
                $green  );
                
$image->line (   @origin,
        $origin[0] + ($minute_segment * cos ($minute_angle)),
        $origin[1] + ($minute_segment * sin ($minute_angle)),
                $green  );

Using the angles that we calculated earlier, we proceed to draw the hour and minute hands with the line method.

$image->arc (@origin, 6, 6, 0, 360, $red);
$image->fill ($origin[0] + 1, $origin[1] + 1, $red);
print $image->gif;
exit(0);

We draw a red circle with a radius of 6 at the center of the image and fill it. Finally, the GIF image is output with the gif method.

Graphic Counter

Now for something different! In the last chapter, we created a counter to display the number of visitors accessing a document. However, that example lacked file locking, and displayed the counter as text value. Now, let's look at the following CGI program that uses the gd graphics library to create a graphic counter. You can include the graphic counter in your HTML document with the <IMG> tag, as described earlier in this chapter.

What is file locking? Perl offers a function called flock, which stands for "file lock," and uses the underlying UNIX call of the same name. You simply call flock and pass the name of the file handle like this:

flock (FILE, 2);

This call grants you the exclusive right to use the file. If another process (such as another instance of your own program) is currently locking the file, your program just waits until the file is free. Once you've got the lock, you can safely do anything you want with the file. When you're finished with the file, issue the following call:

flock (FILE, 8);

Other values are possible besides 2 and 8, but these are the only ones you need. Others are useful when you have lots of processes reading a file and you rarely write to it; it's nice to give multiple processes access so long as nobody is writing.

#!/usr/local/bin/perl5
use GD;
$| = 1;
$webmaster = "shishir\@bu\.edu";
$exclusive_lock = 2;
$unlock_lock = 8;
$counter_file =  "/usr/local/bin/httpd_1.4.2/count.txt";
$no_visitors = 1;

You might wonder why a MIME content type is not output at the start of the program, as it was in all of the previous programs. The reason is that file access errors could occur, in which case an error message (in text or HTML) has to be output.

if (! (-e $counter_file)) {
    if (open (COUNTER, ">" . $counter_file)) {
        flock (COUNTER, $exclusive_lock);
        print COUNTER $no_visitors;
        flock (COUNTER, $unlock_lock);
        close (COUNTER);
        } else {
        &return_error (500, "Counter Error", "Cannot create data file to store counter information.");
}

The -e operator checks to see whether the counter file exists. If the file does not exist, the program will try to create one using the ">" character. If the file cannot be created, we call the return_error subroutine (shown in Chapter 4) to return an error message (subroutines are executed by prefixing an "&" to the subroutine name). However, if a file can be created, the flock command locks the counter file exclusively, so that no other processes can access it. The value stored in $no_visitors (in this case, a value of 1) is written to the file. The file is unlocked, and closed. It is always good practice to close files once you're done with them.

} else {
    if (! ((-r $counter_file) && (-w $counter_file)) ) {
        &return_error (500, "Counter Error",
                "Cannot read or write to the counter data file.");

If the program cannot read or write to the file, we call the return_error subroutine with a specific message.

    } else {
        open (COUNTER, "<" . $counter_file);
        flock (COUNTER, $exclusive_lock);
        $no_visitors = <COUNTER>;
        flock (COUNTER, $unlock_lock);
        close (COUNTER);

If the file exists, and we can read and write to it, the counter file is opened for input (as specified by the "<" symbol). The file is locked, and a line is read using the <COUNTER>notation. Then, we unlock the file and close it.

        $no_visitors++;
        open (COUNTER, ">" . $counter_file);
        flock (COUNTER, $exclusive_lock);
        print COUNTER $no_visitors;
        flock (COUNTER, $unlock_lock);
        close (COUNTER);
    }
}

We increment the counter, open the file for output, and write the new information to the file.

    &graphic_counter();
exit(0);

We call the graphic_counter subroutine and exit. This subroutine creates the image and outputs it to standard output.

This is the end of the program. We will now look at the subroutines. Subroutines should be placed at the end of the main program for clarity.

sub graphic_counter 
{   
    local ( $count_length, $font_length, $font_height, $distance,
    $border, $image_length, $image_height, $image, $black, $blue, $red,
    $loop, $number, $temp_x);

All the variables used exclusively within this subroutine are defined as local variables. These variables are meaningful only within the subroutine; you can't set or retrieve their values in the rest of the program. They are not available once the subroutine has finished executing. It is not mandatory to define local variables, but it is considered good programming practice.

    $count_length = length ($no_visitors);
    $font_length = 8;
    $font_height = 16;

We use the length function to determine the length of the string that represents the visitor count. This might be slightly confusing if you are used to working with other programming languages, where you can obtain only the length of a string, and not a numerical value. In this case, Perl converts the number to a string automatically and determines the length of that string. This is one of the more powerful features of Perl; strings and numbers can be intermixed without any harmful consequences. This length and the font length and height are used to calculate the size of the image.

    $distance = 3;
    $border = 4;

The $distance variable represents the number of pixels (or distance) from one character to the other in our image, and $border is the sum of the length from the left edge to the first character and from the last character to the right edge. The graphics counter is illustrated in Figure 6.4.

Now, let's continue with the rest of the program.

    $image_length = ($count_length * $font_length) + 
                    (($count_length - 1) * distance) + $border;
    $image_height = $font_height + $border;
    $image = new GD::Image ($image_length, $image_height);

The length and height of the image are determined taking into account the number of characters that represent the counter, the font length, and the distance between characters and the border. We then create a new image with the calculated dimensions:

    $black = $image->colorAllocate (0, 0, 0);
    $blue = $image->colorAllocate (0, 0, 255);
    $red = $image->colorAllocate (255, 0, 0);
    $image->rectangle (0, 0, $image_length - 1, $image_height - 1, $blue);

The image consists of a black background with red text and blue lines separating the characters. We also draw a blue rectangle around the entire image. To reiterate, the border variable represents the sum of the number of pixels from this rectangle to the characters on both sides of the image.

    for ($loop=0; $loop <= ($count_length - 1); $loop++) {
        $number = substr ($no_visitors, $loop, 1);

This loop iterates through each character of the counter string, prints the character, and draws a line separating each one. Of course, the separating lines will be drawn only if the length of the counter string is more than one--in other words, if the number of visitors is greater than or equal to 10. The substr function returns one character (as specified by the third argument) each time through the loop.

      if ($count_length > 1) {
    
          $temp_x = ($font_length + $distance) * ($loop + 1);
    
          $image->line (  $temp_x,
                          0,
                          $temp_x,
                          $image_height,
                          $blue  );
      }

We draw a blue line separating each character. The x coordinate corresponding to the line is calculated using the font length, the character position, and the distance between characters. Basically, we leave enough space to hold a character (that's what $font_length is for) plus the space between characters (that's what $distance is for).

    $image->char ( gdLargeFont, 
                 ($border / 2) + ($font_length * $loop) + 
                    ($loop * $distance),
                  $distance, 
                  $number, 
                  $red  );
    }

We use the char method to output each successive character every time through the loop. The x coordinate is calculated using the border, the font length, the character position, and the distance between characters. We could have used the string method to output the character, but since we're dealing with only one character at a time, it is better to use a method created for such a purpose.

    print "Content-type: image/gif", "\n\n";
    print $image->gif;
}

Finally, we output the MIME content type, print the GIF graphic data, and exit.


Previous Home Next
CGI Examples with PostScript Book Index CGI Examples with gnuplot

HTML: The Definitive Guide CGI Programming JavaScript: The Definitive Guide Programming Perl WebMaster in a Nutshell