Simple Graphics Programming with the GD.pm module

Introduction

The ability to create graphics images is an important weapon in the programmer's arsenal. Many forms of data are much easier to understand if they can be reduced to an image: note the common cliche "Seeing is believing". Also, despite the fact that graphic images are easy to create (as you will see), they tend to impress the uninitiated and contribute to the mystique of programming as a skill beyond the capabilities of lab scientists.

GD.pm is a module that provides a Perl programming interface to the gd graphics library (libgd) written by Thomas Boutell. The gd library is written in the C language and it allows you to create color graphic images in several formats, based on simple graphics objects like lines, rectangles, polygons, and arcs. GD.pm itself was written by Lincoln Stein, a prominent figure in the bioinformatics field.

The standard image type created by the GD module is a PNG (Portable Network Graphics) file. PNG is an Open Source type created to replace the GIF format, which is a copyrighted format subject to burdensome intellectual property laws. PNG image files are generally given a ".png" extension. All current web browsers can interpret PNG files properly. The web browser can be used to view a graphics file present on your computer or an image sent to it by a server. Because web browsers are universally available, I find PNG images to be an excellent form of graphics to use for the applications I write. However, the images created by GD.pm are not as fancy as those created by a commercial graphics program such as SigmaPlot: there are many fewer options available. Looking on the bright side, GD.pm forces you to keep things simple.

Computer organization

To make files acessible to the World Wide Web, they must be put into a special directory. On biolinx, the WWW root directory is /home/httpd/html/. You should have a sub-directory under this: mine is /home/httpd/html/t80maj1/ and the BIOS 546 sub-directory for HTML files is /home/httpd/html/bios546/. All my graphics files are placed in these directories.

For access from the web, the WWW root directory is given an alias: http://biolinx.bios.niu.edu. By itself, this address will give you a default page called "index.html", which is the NIU Bioinformatics home page. To access your web pages you need to add to this address: http://biolinx.bios.niu.edu/bios546/gd_mod.htm is the address of this page, for instance. You just need to substitute your sub-directory and file name.

Although it is possible to view PNG files directly with the web browser, it is generally better to write an HTML file as a wrapper. The HTML file can be very simple (see the HTML commands web page). To place the image on the web page, put in a line like <img src="gd1.png"> in the HTML file. Here, the image file gd1.png is in the same directory as the HTML file; if you want to put it elsewhere you will need to include the path.

Coordinate system

For reasons that probably made sense to someone at some time, computer graphic coordinates are not the same as in standard graphing. Coordinates are listed as ordered pairs indicating the x- and y-coordinates. The top left corner of the screen is (0,0). The size of the image as seen on your monitor depends on the settings on your computer: common settings are 800 x 600 (meaning a width of 800 pixels and a height of 600 pixels) and 640 x 480. It is also important to notice that the web browser and operating system usually take up a chunk of your screen. I have found that a 760 x 420 image fits a single screen when set to the 800 x 600 display, so I will use that size as an example. Using these settings, the bottom left corner is (0,419), the top right corner is (759,0), and the bottom right corner is (759,419).

Using Perl to create the image

The point of all this is to create a script to write the PNG file that the web browser will read. Your Perl script will usually reside in your home directory (or possibly in the cgi-bin for interactive web pages).

Object-oriented syntax

GD.pm, like most of the high-quality Perl modules, uses object-oriented syntax. There is a whole philosophy behind this, but for our purposes it mostly means a slightly different form of writing Perl statements than usual.

To start out, we need to create a new image object, which we will call "$im". $im is a scalar variable that points to the data structure of the image we are creating: all the data structure issues are handled behind the scenes by the module so we don't have to worry about them. For most commands, the image object is followed by an arrow (->), the command name, and the command parameters in parentheses. As an example, $im->setPixel(50, 50, $red) will create a red pixel at the coordinate 50, 50.

Some of the basic things your script will need:

#! /usr/bin/perl -w

use strict;
use GD;

open PICFILE, ">/home/httpd/html/bios546/gd1.png" or die "Couldn't open image file: $!\n";

In our usual mode of teaching good habits, we use the "-w" switch to turn on the warnings module and "use strict" to enforce variable scoping and the like. Also note the use of the "or die" section of the file opening statement; the "$!" prints out whatever error message the system produces in addition to the "Couldn't open image file:" message.

The "use GD" statement invokes the GD module, allowing you to use all of its commands in addition to the regular Perl commands. Also note the ">" at the beginning of file open statement: this causes the file to be opened for writing.

Starting the image

To create the initial blank image object:

    my $im = new GD::Image(760, 420);

The numbers in parentheses are the horizontal and vertical dimensions of the image, in pixels.

Next, we need to allocate some colors. The syntax here uses three integers between 0 and 255. These numbers indicate the intensity of the red, green, and blue (RGB) components of each pixel: 0 means the color is off and 255 means full on. There are nice color charts available here and here, but I generally just use a few simple colors that are easy to see and have good contrast with each other. The colors are allocated as follows:

    my $white = $im->colorAllocate(255,255,255);
    my $black = $im->colorAllocate(0, 0, 0);
    my $red = $im->colorAllocate(255, 0, 0);
    my $blue = $im->colorAllocate(0, 0, 255);
    my $green = $im->colorAllocate(50, 200, 0);
    my $purple = $im->colorAllocate(200, 0, 255);
    my $orange = $im->colorAllocate(255, 200, 0);                        

In addition, we need to set the background to transparent, white, and interlaced:

    $im->transparent($white);
    $im->interlaced('true');

I also like to put a border around the image:

    $im->rectangle(0, 0, 759, 419, $black); 

Printing the image

Although we haven't put anythin in the image yet, it is important to get down the ending lines needed to get the image. We need to make sure we are printing in binary mode (as opposed to ASCII--this is mostly a PC issue, not Unix, but Perl scripts are supposed to be portable between systems), then print the file as a PNG image, then close the file:

    binmode PICFILE;
    print PICFILE $im->png;
    close PICFILE;        

This stuff all goes at the end of your script, after you have put everything in the image that you want in it.

Some useful drawing commands

I am including only the commands I have found most useful for drawing. There are more commands listed in the documentation for GD.pm, mostly for manipulating the whole image. There are also some useful derived modules, such as GD::Graph and GD::Text, which can be found in the CPAN archive.

For these commands, $x1 and $y1 refer to the coordinates of one point in the image, and $x2 and $y2 refer to the coordinates of a second point. Colors are given names as defined by the colorAllocate statements above.

# a single PIXEL
$im->setPixel($x1, $y1, $blue);

# A LINE, with beginning and end points
$im->line($x1, $y1, $x2, $y2, $red);

# A DASHED LINE
$im->dashedLine($x1, $y1, $x2, $y2, $red);

# A RECTANGLE
$im->rectangle($x1, $y1, $x2, $y2, $green);

# A RECTANGLE FILLED with color
$im->filledRectangle($x1, $y1, $x2, $y2, $green);

# An ARC: $x1 and $y1 are the center, $width and $height are the width
# and height; $start and $end are the start and end points, given in
# degrees (numbers between 0 and 360), with 0 horizontal to the right and angles
# increasing clockwise.  
# To get a complete CIRCLE or ELLIPSE, use 0 and 360 for the start 
# end points.
# To get a CIRCLE (as opposed to an ellipse), use the same values
# for width and height

$im->arc($x1, $y1, $width, $height, $start, $end, $black);


# FILL any object (or the whole screen) with color
# the point (x1, $y1) is any point within the area to be filled

$im->fill($x1, $y1, $blue);


# a POLYGON (which must have at least 3 points)
# Polygons are more complicated than other objects:
# each polygon must be created, then its vertex points must
# be defined, then finally it must be added to the image.
# Here is the appropriate syntax:

my $poly = new GD::Polygon;

$poly->addPt($x1, $y1);
$poly->addPt($x2, $y2);
$poly->addPt($x3, $y3);

$im->polygon($poly, $purple);

# if you want a FILLED POLYGON, change the last line to:
$im->filledPolygon($poly, $purple);

Text

The GD module provides 5 built-in fonts (in the spirit of keeping it simple, I guess). You can print them horizontally or rotated 90 degrees. The fonts, with their character sizes in pixels, are: gdGiantFont (9x15), gdLargeFont (8x16), gdMediumBoldFont (7x13), gdSmallFont (6x12), and gdTinyFont (5x8). These fonts are used in the $font variable in the commands listed below. For example, $im->string(gdSmallFont, 10, 20, "A few words", $black) would print "A few words" in black using the small font starting at coordinates (10,20).

# print a string (either as a variable or it can be directly
# inserted in quotes)

$im->string($font, $x1, $y1, $your_string, $purple);


# print the string rotated 90 degrees

$im->stringUp($font, $x1, $y1, $your_string, $purple);

An Example

This image was generated by the script below.

#!/usr/bin/perl -w

# This is a script that demonstrates some of the capabilities of the GD.pm
# graphics module: gd_test.pl
# The script can be run anywhere on biolinx (but you need to modify the path to
# the image file) and the resulting image
# is seen at the bottom of http://biolinx.bios.niu.edu/bios546/gd_mod.htm


use strict;
use GD;

    # open the image file
open PICFILE, ">/home/httpd/html/bios546/gd1.png" or die "Couldn't open image file: $!\n";

   # create new image object
my $im = new GD::Image(760, 420);

    # create a set of colors
my $white = $im->colorAllocate(255,255,255);
my $black = $im->colorAllocate(0, 0, 0);
my $red = $im->colorAllocate(255, 0, 0);
my $blue = $im->colorAllocate(0, 0, 255);
my $green = $im->colorAllocate(50, 200, 0);
my $purple = $im->colorAllocate(200, 0, 255);
my $orange = $im->colorAllocate(255, 200, 0);                        

    # set background and interlacing
$im->transparent($white);
$im->interlaced('true');

    # draw a border around the image
$im->rectangle(0, 0, 759, 419, $black); 

    # a horizontal line
$im->line(10, 20, 300, 20, $red);

    # a diagonal line
$im->line(10, 20, 30, 40, $orange);

    # a small box
$im->rectangle(500, 30, 550, 80, $green);

    # a small filled box
$im->filledRectangle(570, 30, 620, 80, $green);

    # an arc
$im->arc(100, 100, 50, 80, 90, 270, $black);

    # an arc the other way
$im->arc(300, 100, 50, 80, 270, 90, $black);

    # a shorter arc
$im->arc(200, 100, 80, 80, 0, 45, $black);


    # a filled circle
$im->arc(450, 100, 60, 60, 0, 360, $red);
$im->fill(450, 100, $blue);

    # polygon
my $poly = new GD::Polygon;
$poly->addPt(60, 220);
$poly->addPt(80, 280);
$poly->addPt(200, 300);
$im->polygon($poly, $purple);

    # some text
$im->string(gdTinyFont, 20, 330, "This is the tiny font", $blue);
$im->string(gdSmallFont, 20, 350, "This is the small font", $purple);
$im->string(gdMediumBoldFont, 20, 370, "This is the medium font", $green);
$im->string(gdLargeFont, 20, 390, "This is the large font", $red);
$im->string(gdGiantFont, 400, 350, "This is the giant font", $orange);


$im->stringUp(gdSmallFont, 250, 400, "This is the small font rotated 90 degrees", $purple);


    # print the image as PNG 
binmode PICFILE;
print PICFILE $im->png;
close PICFILE;        

Hash of Colors

The Colors.pm module allows you to use any of about 100 named colors, by loading a hash with the $im->colorAllocate command. This is explained at colors.htm.

Drawing Graphs

First, consider what is needed to plot a set of data on a horizontal line. The line has an origin and a length (both defined in pixels), and you need to fit the entire range of your data onto it. The key element to this is a conversion factor, the number of pixels per unit of data. To obtain this, divide the total range of your data (plus a bit extra for the sake of neatness) by the length of the line in pixels: this number becomes the variable "$pixels_per_unit". To position your data on the line, simply multiply the units of data by $pixels_per_unit, then add the position of the line's origin.

For example, your line starts at x-coordinate 50 (i.e., $x_origin = 50) and extends to x-coordinate 650: it has a length of 600 pixels. Your data ranges from 0 to 947, so you decide to have your graph go up to 1000. The $pixels_per_unit is thus 600 / 1000 = 0.600. Thus, to convert data point 512 to its position on the line, do: $x_position = 512 * $pixels_per_unit + $x_origin = 357.2. (Note that Perl will automaticaly round this number to an integer value).

To extend this to 2 dimensions, you will need to have two separate conversion factors: $x_pixels_per_unit and $y_pixels_per_unit. Also, because computer graphics has its y-origin at the top of the screen, you need to do one more conversion for the y-axis. To get the position on the y-axis, first multiply your data point by $y_pixels_per_unit, then subtract this value from the origin of the y-axis. For example, if the origin of the y-axis on your graph is at pixel 380 ($y_origin = 380) and your $y_pixels_per_unit is 15.36 and your data value is 7.2, then its position on the y-axis is: $y_origin - $y_pixels_per_unit * 7.2 = 269.41. This gets rounded to pixel 269, which you will note is above the origin at 380.

These conversion factors, $x_pixels_per_unit and $y_pixels_per_unit, can also be used to locate the positions of tick marks on the axes.

Symbols. I generally use either asterisks ( * ) or periods ( . ) for graph symbols. Using the gdSmallFont, it is necessary to adjust their positions by a few pixels to get them properly centered. The line: $im->string(gdSmallFont, $x_pos-2, $y_pos-10, ".", $black); gives dots centered properly. Note that the dot has been moved 2 pixels to the left and 10 pixels up from the calculated position of the data point being plotted.

Similarly, the line: $im->string(gdSmallFont, $x_pos-2, $y_pos-6, "*", $black); gives good centering for asterisks. The star has been moved 2 pixels to the left and 6 pixels up from the calculated position of the data point being plotted.

Graphing Example

We want to plot these data on a graph with "Time" on the x-axis and "Activity" on the y-axis.
Time (min) Activity (units)
0 0.002
10 0.045
20 0.088
30 0.132
40 0.178
50 0.215
60 0.256

The range of times is 0-60 minutes, and we want the x-axis origin at pixel 50 and the x-axis end at pixel 650, for a length of 600 pixels. Thus, $x_pixels_per_unit = 600 / 60 = 10.0 pixels per minute.

The range of enzyme activities is 0.002 to 0.256. These values can be rounded to 0.0 and 0.300. The y axis origin is at pixel 380 and the y axis end is at pixel 30, for a length of 350 pixels. Thus, $y_pixels_per_unit is 350 / 0.300 = 1167 pixels per enzyme unit.

The script below shows how to draw the border around the image, the axes and their labels, the tick marks on each axis, the data points, and lines connecting the data points. To get labels positioned properly, you usually have to make repeated minor changes to your script: trial and error.

#!/usr/bin/perl -w

# a simple graphing plotting program: gd_graph.pl

use strict;
use GD;

    # the data
my @x_vals = (0, 10, 20, 30, 40, 50, 60);
my @y_vals = (0.002, 0.045, 0.088, 0.132, 0.178, 0.215, 0.256);


    # conversion factors
my $x_pixels_per_unit = 10.0;
my $y_pixels_per_unit = 1167;


open GRFFILE, ">/home/httpd/html/bios546/gd_graph.png" or die "Couldn't open graph file: $!\n";

    # create new image object
my $im = new GD::Image(760, 420);  # fits 800 X 600 17inch display well

    # allocate some colors
my $white = $im->colorAllocate(255,255,255);
my $black = $im->colorAllocate(0, 0, 0);
my $red = $im->colorAllocate(255, 0, 0);
my $blue = $im->colorAllocate(0, 0, 255);

$im->transparent($white);
$im->interlaced('true');

    # border
$im->rectangle(0, 0, 759, 419, $black);

    #axes
my $x_axis_origin = 50;
my $x_axis_end = 650;
my $y_axis_origin = 380;
my $y_axis_end = 30;

$im->line($x_axis_origin, $y_axis_origin, $x_axis_end, $y_axis_origin, $black);   # x-axis
$im->string(gdSmallFont, 300, $y_axis_origin + 25, "Time (min)", $black);    # axis label

$im->line($x_axis_origin, $y_axis_origin, $x_axis_origin, $y_axis_end, $black);   # y-axis    
$im->stringUp(gdSmallFont, $x_axis_origin - 45, 250, "Enzyme Activity (units)", $black);

    # tick marks (extend 3 pixels on either side of the axis)
my @x_ticks = (0, 10, 20, 30, 40, 50, 60);
foreach my $tick (@x_ticks) {
    my $tick_pos = $tick * $x_pixels_per_unit + $x_axis_origin;
    $im->line($tick_pos, $y_axis_origin - 3, $tick_pos, $y_axis_origin + 3, $black);
    $im->string(gdSmallFont, $tick_pos - 5, $y_axis_origin + 10, $tick, $black);
}

my @y_ticks = qw(0.00 0.05 0.10 0.15 0.20 0.25 0.30);
foreach my $tick (@y_ticks) {
    my $tick_pos = $y_axis_origin - $tick * $y_pixels_per_unit;
    $im->line($x_axis_origin - 3, $tick_pos, $x_axis_origin + 3, $tick_pos, $black);
    $im->stringUp(gdSmallFont, $x_axis_origin - 20, $tick_pos + 10, $tick, $black);
}

    # data plotting
for (my $i = 0; $i <= $#x_vals; $i++) {
    my $x_pos = $x_vals[$i] * $x_pixels_per_unit + $x_axis_origin;
    my $y_pos = $y_axis_origin - $y_vals[$i] * $y_pixels_per_unit; 
    $im->string(gdSmallFont, $x_pos- 2, $y_pos- 6, "*", $red);
}

    # lines drawn between points
for (my $i = 0; $i < $#x_vals; $i++) {
    my $x1 = $x_vals[$i] * $x_pixels_per_unit + $x_axis_origin;
    my $x2 = $x_vals[$i+1] * $x_pixels_per_unit + $x_axis_origin;

    my $y1 = $y_axis_origin - $y_vals[$i] * $y_pixels_per_unit; 
    my $y2 = $y_axis_origin - $y_vals[$i+1] * $y_pixels_per_unit; 

    $im->line($x1, $y1, $x2, $y2, $blue);
}


    # print the file
binmode GRFFILE;
print GRFFILE $im->png;
close GRFFILE;