|
|
Chapter 6
Hypermedia Documents |
|
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.
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:
- The new keyword
causes space to be allocated for the image.
- The GD is the class, which means what kind of object
we're making (it happens to have the same name as the package we
loaded with the use statement).
- Within that class is a function (or method) called
Image, which takes two arguments.
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.
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.
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.
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.
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:
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:
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.
|
|