PHP Cookbook/Graphics

From WikiContent

< PHP Cookbook
Revision as of 22:30, 6 March 2008 by Evanlenz (Talk | contribs)
Jump to: navigation, search
PHP Cookbook


Contents

Introduction

With the assistance of the GD library, you can use PHP to create applications that use dynamic images to display stock quotes, reveal poll results, monitor system performance, and even create games. However it's not like using Photoshop or GIMP; you can't draw a line by moving your mouse. Instead, you need to precisely specify a shape's type, size, and position.

GD has an existing API, and PHP tries to follows its syntax and function-naming conventions. So, if you're familiar with GD from other languages, such as C or Perl, you can easily use GD with PHP. If GD is new to you, it may take a few minutes to figure it out, but soon you'll be drawing like Picasso.

The feature set of GD varies greatly depending on which version GD you're running and which features were enabled during configuration. Versions of GD up to 1.6 supported reading and writing GIFs, but this code was removed due to patent problems. Instead, newer versions of GD support JPEGs, PNGs, and WBMPs. Because PNGs are generally smaller than GIFs, allow you to use many more colors, have built-in gamma correction, and are supported by all major web browsers, the lack of GIF support is classified as a feature, not a bug. For more on PNG, go to http://www.libpng.org/pub/png/ or read Chapter 21, "PNG Format," of Web Design in a Nutshell written by Jennifer Niederst (O'Reilly).

Besides supporting multiple file formats, GD lets you draw pixels, lines, rectangles, polygons, arcs, ellipses, and circles in any color you want. Recipe 15.2 covers straight shapes, while Recipe 15.3 covers the curved ones. To fill shapes with a pattern instead of a solid color, see Recipe 15.4.

You can also draw text using a variety of font types, including built-in, TrueType, and PostScript Type 1 fonts. Recipe 15.5 shows the ins and outs of the three main text-drawing functions, and Recipe 15.6 shows how to center text within a canvas. These two recipes form the basis for Recipe 15.7, which combines an image template with real-time data to create dynamic images. GD also lets you make transparent GIFs and PNGs. Setting a color as transparent and using transparencies in patterns are discussed in Recipe 15.8.

Recipe 15.9 moves away from GD and shows how to securely serve images by restricting user access. Last, we provide an example application — taking poll results and producing a dynamic bar graph showing what percentage of users voted for each answer.

All these features work with GD 1.8.4, which is the latest stable version of the library. If you have an earlier version, you should not have a problem. However, if a particular recipe needs a specific version of GD, we note it in the recipe.

PHP also supports GD 2.x, which, as of this writing, is still in beta. Despite its beta status, the new version is relatively stable and has many new features. In particular, Version 2.x allows true-color images, which lets GD read in PNGs and JPEGs with almost no loss in quality. Also, GD 2.x supports PNG alpha channels, which allow you to specify a transparency level for each pixel.

Both versions of GD are available for download from the official GD site at http://www.boutell.com/gd/. The GD section of the online PHP Manual at http://www.php.net/image also lists the location of the additional libraries necessary to provide support for JPEGs and Type 1 fonts.

There are two easy ways to see which version, if any, of GD is installed on your server and how it's configured. One way is to call phpinfo( ) . You should see --with-gd at the top under "Configure Command"; further down the page there is also a section titled "gd" that has more information about which version of GD is installed and what features are enabled. The other option is to check the return value of function_exists('imagecreate'). If it returns true, GD is installed. The imagetypes( ) function returns a bit field indicating which graphics formats are available. See http://www.php.net/imagetypes for more on how to use this function. If you want to use a feature that isn't enabled, you need to rebuild PHP yourself or get your ISP to do so.

The basic image generation process has three steps: creating the image, adding graphics and text to the canvas, and displaying or saving the image. For example:

$image = ImageCreate(200, 50);
$background_color = ImageColorAllocate($image, 255, 255, 255); // white
$gray             = ImageColorAllocate($image, 204, 204, 204); // gray

ImageFilledRectangle($image, 50, 10, 150, 40, $gray);

header('Content-type: image/png');
ImagePNG($image);

The output of this code, which prints a gray rectangle on a white background, is shown in Figure 15-1.

Figure 15-1. A gray rectangle on a white background

A gray rectangle on a white background

To begin, you create an image canvas. The ImageCreate( ) function doesn't return an actual image. Instead, it provides you with a handle to an image; it's not an actual graphic until you specifically tell PHP to write the image out. Using ImageCreate( ), you can juggle multiple images at the same time.

The parameters passed to ImageCreate( ) are the width and height of the graphic in pixels. In this case, it's 200 pixels across and 50 pixels high. Instead of creating a new image, you can also edit existing images. To open a graphic, call ImageCreateFromPNG( ) or a similarly named function to open a different file format. The filename is the only argument, and files can live locally or on remote servers:

// open a PNG from the local machine
$graph = ImageCreateFromPNG('/path/to/graph.png');

// open a JPEG from a remote server
$icon  = ImageCreateFromJPEG('http://www.example.com/images/icon.jpeg');

Once you have an editable canvas, you get access to drawing colors by calling ImageColorAllocate( ) :

$background_color = ImageColorAllocate($image, 255, 255, 255); // white
$gray             = ImageColorAllocate($image, 204, 204, 204); // gray

The ImageColorAllocate( ) function takes an image handle to allocate the color to and three integers. The three integers each range from 0 to 255 and specify the red, green, and blue components of the color. This is the same RGB color combination that is used in HTML to set a font or background color. So, white is 255, 255, 255; black is 0, 0, 0, and everything else is somewhere in between.

The first call to ImageAllocateColor( ) sets the background color. Additional calls allocate colors for drawing lines, shapes, or text. Therefore, set the background color to 255, 255, 255 and then grab a gray pen with ImageAllocateColor($image, 204, 204, 204). It may seem odd that the background color is determined by the order ImageAllocateColor() is called and not by a separate function. But, that's how things work in GD, so PHP respects the convention.

Call ImageFilledRectangle( ) to place a box onto the canvas. ImageFilledRectangle( ) takes many parameters: the image to draw on, the x and y coordinates of the upper left corner of the rectangle, the x and y coordinates of the lower right corner of the rectangle, and finally, the color to use to draw the shape. Tell ImageFilledRectangle( ) to draw a rectangle on $image, starting at (50,10) and going to (150,40), in the color gray:

ImageFilledRectangle($image, 50, 10, 150, 40, $gray);

Unlike a Cartesian graph, (0,0) is not in the lower left corner; instead, it's in the upper left corner. So, the vertical coordinate of the spot 10 pixels from the top of a 50 pixel high canvas is 10 because it's 10 pixels down from the top of the canvas. It's not 40, because you measure from the top down, not the bottom up. And it's not -10, because down is considered the positive direction, not the negative one.

Now that the image is all ready to go, you can serve it up. First, send a Content-type header to let the browser know what type of image you're sending. In this case, we display a PNG. Next, have PHP write the PNG image out using ImagePNG( ) . Once the image is sent, your task is over:

header('Content-Type: image/png');
ImagePNG($image);

To write the image to disk instead of sending it to the browser, provide a second argument to ImagePNG( ) with where to save the file:

ImagePng($image, '/path/to/your/new/image.png');

Since the file isn't going to the browser, there's no need to call header( ). Make sure to specify a path and an image name, and be sure PHP has permission to write to that location.

PHP cleans up the image when the script ends, but, if you wish to manually deallocate the memory used by the image, calling ImageDestroy($image) forces PHP to get rid of the image immediately.

Drawing Lines, Rectangles, and Polygons

Problem

You want to draw a line, rectangle, or polygon. You also want to be able to control if the rectangle or polygon is open or filled in. For example, you want to be able to draw bar charts or create graphs of stock quotes.

Solution

To draw a line, use ImageLine( ):

ImageLine($image, $x1, $y1, $x2, $y2, $color);

To draw an open rectangle, use ImageRectangle( ):

ImageRectangle($image, $x1, $y1, $x2, $y2, $color);

To draw a solid rectangle, use ImageFilledRectangle( ) :

ImageFilledRectangle($image, $x1, $y1, $x2, $y2, $color);

To draw an open polygon, use ImagePolygon( ):

$points = array($x1, $y1, $x2, $y2, $x3, $y3);
ImagePolygon($image, $points, count($points)/2, $color);

To draw a filled polygon, use ImageFilledPolygon( ):

$points = array($x1, $y1, $x2, $y2, $x3, $y3);
ImageFilledPolygon($image, $points, count($points)/2, $color);

Discussion

The prototypes for all five functions in the Solution are similar. The first parameter is the canvas to draw on. The next set of parameters are the x and y coordinates to specify where GD should draw the shape. In ImageLine( ) , the four coordinates are the end points of the line, and in ImageRectangle( ) , they're the opposite corners of the rectangle. For example, ImageLine($image, 0, 0, 100, 100, $color) produces a diagonal line. Passing the same parameters to ImageRectangle( ) produces a rectangle with corners at (0,0), (100,0), (0,100), and (100,100). Both shapes are shown in Figure 15-2.

Figure 15-2. A diagonal line and a square

A diagonal line and a square

The ImagePolygon( ) function is slightly different because it can accept a variable number of vertices. Therefore, the second parameter is an array of x and y coordinates. The function starts at the first set of points and draws lines from vertex to vertex before finally completing the figure by connecting back to the original point. You must have a minimum of three vertices in your polygon (for a total of six elements in the array). The third parameter is the number of vertices in the shape; since that's always half of the number of elements in the array of points, a flexible value for this is count($points) / 2 because it allows you to update the array of vertices without breaking the call to ImageLine().

Last, all the functions take a final parameter that specifies the drawing color. This is usually a value returned from ImageColorAllocate( ) but can also be the constants IMG_COLOR_STYLED or IMG_COLOR_STYLEDBRUSHED, if you want to draw nonsolid lines, as discussed in Recipe 15.4.

These functions all draw open shapes. To get GD to fill the region with the drawing color, use ImageFilledRectangle( ) and ImageFilledPolygon( ) with the identical set of arguments as their unfilled cousins.

See Also

Recipe 15.3 for more on drawing other types of shapes; Recipe 15.4 for more on drawing with styles and brushes; documentation on ImageLine( ) at http://www.php.net/imageline, ImageRectangle( ) at http://www.php.net/imagerectangle, ImagePolygon( ) at http://www.php.net/imagepolygon, and ImageColorAllocate( ) at http://www.php.net/imagecolorallocate.

Drawing Arcs, Ellipses, and Circles

Problem

You want to draw open or filled curves. For example, you want to draw a pie chart showing the results of a user poll.

Solution

To draw an arc, use ImageArc( ):

ImageArc($image, $x, $y, $width, $height, $start, $end, $color);

To draw an ellipse, use ImageArc( ) and set $start to 0 and $end to 360:

ImageArc($image, $x, $y, $width, $height, 0, 360, $color);

To draw a circle, use ImageArc( ), set $start to 0, set $end to 360, and use the same value for both $width and $height:

ImageArc($image, $x, $y, $diameter, $diameter, 0, 360, $color);

Discussion

Because the ImageArc( ) function is highly flexible, you can easily create common curves such as ellipses and circles by passing it the right values. Like many GD functions, the first parameter is the canvas. The next two parameters are the x and y coordinates for the center position of the arc. After that comes the arc width and height. Since a circle is an arc with the same width and height, to draw a circle, set both numbers to your circle's diameter.

The sixth and seventh parameters are the starting and ending angles, in degrees. A value of 0 is at 3 o'clock. The arc then moves clockwise, so 90 is at 6 o'clock, 180 is at 9 o'clock, and 270 is at the top of the hour. (Be careful, this behavior is not consistent among all GD functions. For example, when you rotate text, you turn in a counter-clockwise direction.) Since the arc's center is located at ($x,$y), if you draw a semicircle from 0 to 180, it doesn't start at ($x,$y); instead, it begins at ($x+($diameter/2),$y).

As usual, the last parameter is the arc color.

For example, this draws an open black circle with a diameter of 100 pixels centered on the canvas, as shown in left half of Figure 15-3:

$image = ImageCreate(100,100);
$bg = ImageColorAllocate($image, 255, 255, 255);
$black = ImageColorAllocate($image, 0, 0, 0);
ImageArc($image, 50, 50, 100, 100, 0, 360, $black);

To produce a solid-colored ellipse or circle, call ImageFillToBorder( ):

ImageArc($image, $x, $y, $diameter, $diameter, 0, 360, $color);
ImageFillToBorder($image, $x, $y, $color, $color);

The ImageFillToBorder( ) function floods a region beginning at ($x,$y) with the color specified as the last parameter until it hits the edge of the canvas or runs into a line with the same color as the third parameter.

Incorporating this into the earlier example gives:

$image = ImageCreate(100,100);
$bg = ImageColorAllocate($image, 255, 255, 255);
$black = ImageColorAllocate($image, 0, 0, 0);
ImageArc($image, 50, 50, 100, 100, 0, 360, $black);
ImageFillToBorder($image, 50, 50, $black, $black);

The output is shown in the right half of Figure 15-3.

Figure 15-3. An open black circle and a filled black circle

An open black circle and a filled black circle

If you're running GD 2.x, you can call ImageFilledArc( ) and pass in a final parameter that describes the fill style. GD 2.x also supports specific ImageEllipse( ) and ImageFilledEllipse( ) functions.

See Also

Recipe 15.2 for more on drawing other types of shapes; Recipe 15.4 for more on drawing with styles and brushes; documentation on ImageArc( ) at http://www.php.net/imagearc, ImageFilledArc( ) at http://www.php.net/imagefilledarc, and ImageFillToBorder( ) at http://www.php.net/imagefilltoborder.

Drawing with Patterned Lines

Problem

You want to draw shapes using line styles other than the default, a solid line.

Solution

To draw shapes with a patterned line, use ImageSetStyle( ) and pass in IMG_COLOR_STYLED as the image color:

$black = ImageColorAllocate($image,   0,   0,   0);
$white = ImageColorAllocate($image, 255, 255, 255);

// make a two-pixel thick black and white dashed line
$style = array($black, $black, $white, $white);
ImageSetStyle($image, $style);

ImageLine($image, 0, 0, 50, 50, IMG_COLOR_STYLED);
ImageFilledRectangle($image, 50, 50, 100, 100, IMG_COLOR_STYLED);

Discussion

The line pattern is defined by an array of colors. Each element in the array is another pixel in the brush. It's often useful to repeat the same color in successive elements, as this increases the size of the stripes in the pattern.

For instance, here is code for a square drawn with alternating white and black pixels, as shown in left side of Figure 15-4:

$style = array($white, $black);
ImageSetStyle($image, $style);
ImageFilledRectangle($image, 0, 0, 49, 49, IMG_COLOR_STYLED);

This is the same square, but drawn with a style of five white pixels followed by five black ones, as shown in the middle of Figure 15-4:

$style = array($white, $white, $white, $white, $white, 
               $black, $black, $black, $black, $black);
ImageSetStyle($image, $style);
ImageFilledRectangle($image, 0, 0, 49, 49, IMG_COLOR_STYLED);

Figure 15-4. Three squares with alternating white and black pixels

Three squares with alternating white and black pixels

The patterns look completely different, even though both styles are just white and black pixels.

If the brush doesn't fit an integer number of times in the shape, it wraps around. In the previous examples, the square is 50 pixels wide. Since the first brush is 2 pixels long, it fits exactly 25 times; the second brush is 10 pixels, so it fits 5 times. But, if you make the square 45 by 45 and used the second brush, you don't get straight lines as you did previously, as shown in the right side of Figure 15-4:

ImageFilledRectangle($image, 0, 0, 44, 44, IMG_COLOR_STYLED);

See Also

Recipe 15.2 and Recipe 15.3 for more on drawing shapes; documentation on ImageSetStyle( ) at http://www.php.net/imagesetstyle.

Drawing Text

Problem

You want to draw text as a graphic. This allows you to make dynamic buttons or hit counters.

Solution

For built-in GD fonts, use ImageString( ):

ImageString($image, 1, $x, $y, 'I love PHP Cookbook', $text_color);

For TrueType fonts, use ImageTTFText( ):

ImageTTFText($image, $size, 0, $x, $y, $text_color, '/path/to/font.ttf', 
             'I love PHP Cookbook');

For PostScript Type 1 fonts, use ImagePSLoadFont( ) and ImagePSText( ):

$font = ImagePSLoadFont('/path/to/font.pfb');
ImagePSText($image, 'I love PHP Cookbook', $font, $size, 
            $text_color, $background_color, $x, $y);

Discussion

Call ImageString( ) to place text onto the canvas. Like other GD drawing functions, ImageString( ) needs many inputs: the image to draw on, the font number, the x and y coordinates of the upper right position of the first characters, the text string to display, and finally, the color to use to draw the string.

With ImageString( ), there are five possible font choices, from 1 to 5. Font number 1 is the smallest, while font 5 is the largest, as shown in Figure 15-5. Anything above or below that range generates a size equivalent to the closest legal number.

Figure 15-5. Built-in GD font sizes

Built-in GD font sizes

To draw text vertically instead of horizontally, use the function ImageStringUp( ) instead. Figure 15-6 shows the output.

ImageStringUp($image, 1, $x, $y, 'I love PHP Cookbook', $text_color);

Figure 15-6. Vertical text

Vertical text

To use TrueType fonts, you must also install the FreeType library and configure PHP during installation to use FreeType. The FreeType main site is http://www.freetype.org. To enable FreeType 1.x support, use --with-ttf and for FreeType 2.x, pass --with-freetype-dir=DIR.

Like ImageString( ), ImageTTFText( ) prints a string to a canvas, but it takes slightly different options and needs them in a different order:

ImageTTFText($image, $size, $angle, $x, $y, $text_color, '/path/to/font.ttf', 
             $text);

The $size argument is the font size in pixels; $angle is an angle of rotation, in degrees going counter-clockwise; and /path/to/font.ttf is the pathname to TrueType font file. Unlike ImageString( ), ($x,$y) are the lower left coordinates of the baseline for the first character. (The baseline is where the bottom of most characters sit. Characters such as "g" and "j" extend below the baseline; "a" and "z" sit on the baseline.)

PostScript Type 1 fonts require t1lib to be installed. It can be downloaded from ftp://sunsite.unc.edu/pub/Linux/libs/graphics/ and built into PHP using --with-t1lib.

Again, the syntax for printing text is similar but not the same:

$font = ImagePSLoadFont('/path/to/font.pfb');
ImagePSText($image, $text, $font, $size, $text_color, $background_color, $x, $y);
ImagePSFreeFont($font);

First, PostScript font names can't be directly passed into ImagePSText( ) . Instead, they must be loaded using ImagePSLoadFont( ) . On success, the function returns a font resource usable with ImagePSText( ). In addition, besides specifying a text color, you also pass a background color to be used in antialiasing calculations. The ($x,$y) positioning is akin to the how the TrueType library does it. Last, when you're done with a font, you can release it from memory by calling ImagePSFreeFont( ) .

Besides the mandatory arguments listed above, ImagePSText( ) also accepts four optional ones, in this order: space , tightness, angle, and antialias_steps. You must include all four or none of the four (i.e., you can't pass one, two, or three of these arguments). The first controls the size of a physical space (i.e., what's generated by hitting the space bar); the second is the tightness of the distance between letters; the third is a rotation angle, in degrees, counter-clockwise; and the last is an antialiasing value. This number must be either 4 or 16. For better looking, but more computationally expensive graphics, use 16 instead of 4.

By default, space, tightness, and angle are all 0. A positive number adds more space between words and letters or rotates the graphic counterclockwise. A negative number kerns words and letters or rotates in the opposite direction. The following example has the output shown in Figure 15-7:

// normal image
ImagePSText($image, $text, $font, $size, $black, $white, $x, $y,
            0, 0, 0, 4);

// extra space between words
ImagePSText($image, $text, $font, $size, $black, $white, $x, $y + 30, 
            100, 0, 0, 4);

// extra space between letters
ImagePSText($image, $text, $font, $size, $black, $white, $x, $y + 60, 
            0, 100, 0, 4);

Figure 15-7. Words with extra space and tightness

Words with extra space and tightness

See Also

Recipe 15.6 for drawing centered text; documentation on ImageString( ) at http://www.php.net/imagestring, ImageStringUp( ) at http://www.php.net/imagestringup, ImageTTFText( ) at http://www.php.net/imagettftext, ImagePSText( ) at http://www.php.net/imagepstext, and ImagePSLoadFont( ) at http://www.php.net/imagepsloadfont.

Drawing Centered Text

Problem

You want to draw text in the center of an image.

Solution

Find the size of the image and the bounding box of the text. Using those coordinates, compute the correct spot to draw the text.

For built-in GD fonts, use the pc_ImageStringCenter( ) function shown in Example 15-1.

Example 15-1. pc_ImageStringCenter( )

function pc_ImageStringCenter($image, $text, $font) {
    
    // font sizes
    $width  = array(1 => 5, 6, 7, 8, 9);
    $height = array(1 => 6, 8, 13, 15, 15);

    // find the size of the image
    $xi = ImageSX($image);
    $yi = ImageSY($image);

    // find the size of the text
    $xr = $width[$font] * strlen($text);
    $yr = $height[$font];

    // compute centering
    $x = intval(($xi - $xr) / 2);
    $y = intval(($yi - $yr) / 2);

    return array($x, $y);
}

For example:

list($x, $y) = pc_ImageStringCenter($image, $text, $font);
ImageString($image, $font, $x, $y, $text, $fore);

For PostScript fonts, use the pc_ImagePSCenter( ) function shown in Example 15-2.

Example 15-2. pc_ImagePSCenter( )

function pc_ImagePSCenter($image, $text, $font, $size, $space = 0,
                           $tightness = 0, $angle = 0) {

    // find the size of the image
    $xi = ImageSX($image);
    $yi = ImageSY($image);

    // find the size of the text
    list($xl, $yl, $xr, $yr) = ImagePSBBox($text, $font, $size,
                                     $space, $tightness, $angle);

    // compute centering
    $x = intval(($xi - $xr) / 2);
    $y = intval(($yi + $yr) / 2);

    return array($x, $y);
}

For example:

list($x, $y) = pc_ImagePSCenter($image, $text, $font, $size);
ImagePSText($image, $text, $font, $size, $fore, $back, $x, $y);

For TrueType fonts, use the pc_ImageTTFCenter( ) function shown in Example 15-3.

Example 15-3. pc_ImageTTFCenter( )

function pc_ImageTTFCenter($image, $text, $font, $size) {

    // find the size of the image
    $xi = ImageSX($image);
    $yi = ImageSY($image);

    // find the size of the text
    $box = ImageTTFBBox($size, $angle, $font, $text);

    $xr = abs(max($box[2], $box[4]));
    $yr = abs(max($box[5], $box[7]));

    // compute centering
    $x = intval(($xi - $xr) / 2);
    $y = intval(($yi + $yr) / 2);

    return array($x, $y);
}

For example:

list($x, $y) = pc_ImageTTFCenter($image, $text, $font, $size);
ImageTTFText($image, $size, $angle, $x, $y, $fore, $font, $text);

Discussion

All three solution functions return the x and y coordinates for drawing. Of course, depending on font type, size, and settings, the method used to compute these coordinates differs.

For PostScript Type 1 fonts, pass pc_ImagePSCenter( ) an image allocated from ImageCreate( ) (or one of its friends) and a number of parameters to specify how to draw the text. The first three parameters are required: the text to be drawn, the font, and the font size. The next three are optional: the space in a font, the tightness between letters, and an angle for rotation in degrees.

Inside the function, use ImageSX( ) and ImageSY( ) to find the size of the canvas; they return the width and height of the graphic. Then call ImagePSBBox( ) . It returns four integers: the x and y coordinates of the lower-leftmost location the text and the x and y coordinates of the upper-rightmost location. Because the coordinates are relative to the baseline of the text, it's typical for these not to be 0. For instance, a lowercase "g" hangs below the bottom of the rest of the letters; so, in that case, the lower left y value is negative.

Armed with these six values, we can now calculate the correct centering values. Because coordinates of the canvas have (0,0) in the upper left corner, but ImagePSBText( ) wants the lower left corner, the formula for finding $x and $y isn't the same. For $x, we take the difference between the size of the canvas and the text. This gives the amount of whitespace that surrounds the text. Then we divide that number by two, to find the number of pixels we should leave to the left of the text. For $y, we do the same, but add $yi and $yr. By adding these numbers, we can find the coordinate of the far side of the box, which is what is needed here because of the inverted way the y coordinate is entered in GD.

We intentionally ignore the lower left coordinates in making these calculations. Because the bulk of the text sits above the baseline, adding the descending pixels into the centering algorithm actually worsens the code; it appears off-center to the eye.

To center text, put it together like this:

function pc_ImagePSCenter($image, $text, $font, $size, $space = 0,
                           $tightness = 0, $angle = 0) {

    // find the size of the image
    $xi = ImageSX($image);
    $yi = ImageSY($image);

    // find the size of the text 
    list($xl, $yl, $xr, $yr) = ImagePSBBox($text, $font, $size,
                                     $space, $tightness, $angle);

    // compute centering 
    $x = intval(($xi - $xr) / 2);
    $y = intval(($yi + $yr) / 2);

    return array($x, $y);
}

$image = ImageCreate(500,500);
$text = 'PHP Cookbook Rules!';
$font = ImagePSLoadFont('/path/to/font.pfb');
$size = 20;
$black = ImageColorAllocate($image, 0, 0, 0);
$white = ImageColorAllocate($image, 255, 255, 255);

list($x, $y) = pc_ImagePSCenter($image, $text, $font, $size);
ImagePSText($image, $text, $font, $size, $white, $black, $x, $y);
ImagePSFreeFont($font); 

header('Content-type: image/png');
ImagePng($image);

ImageDestroy($image);

Unfortunately, this example doesn't work for GD's built-in fonts nor for TrueType fonts. There's no function to return the size of a string using the built-in fonts, and ImageTTFBBox( ) returns eight values instead of four. With a few modifications, however, we can accommodate these differences.

Because the built-in fonts are fixed-width, we can easily measure the size of a character to create a function that returns the size of the text based on its length. Table 15-1 isn't 100% accurate, but it should return results within one or two pixels, which should be good enough for most cases.

Table 15-1. GD Built-in font character sizes

Font number Width Height
1 5 6
2 6 8
3 7 13
4 8 15
5 9 15


Inside pc_ImageStringCenter( ) , we calculate the length of the string as an integral multiple based on its length; the height is just one character high. Note that ImageString( ) takes its y coordinate as the uppermost part of the text, so we should switch the sign back to a minus when you compute $y.

Here is an example using all five fonts that centers text horizontally:

$text = 'The quick brown fox jumps over the lazy dog.';
for ($font = 1, $y = 5; $font <= 5; $font++, $y += 20) {
    list($x, $y) = pc_ImageStringCenter($image, $text, $font);
    ImageString($image, $font, $x, $y, $text, $color);
}

The output is shown in Figure 15-8.

Figure 15-8. Centered GD built-in fonts

Centered GD built-in fonts

For TrueType fonts, we need to use ImageTTFBBox( ) or the more modern ImageFtBBox( ). (The function with TTF in the name is for FreeType version 1.x; the one with Ft is for FreeType 2.x.) It returns eight numbers: the (x,y) coordinates of the four corners of the text starting in the lower left and moving around counter clockwise. So, the second two coordinates are for the lower right spot, and so on.

To make pc_ImageTTFCenter( ) , begin with pc_ImagePSCenter( ) and swap this line:

    // find the size of the text 
    list($xl, $yl, $xr, $yr) = ImagePSBBox($text, $font, $size,
                                     $space, $tightness, $angle);

with these:

    // find the size of the text 
    $box = ImageTTFBBox($size, $angle, $font, $text);

    $xr = abs(max($box[2], $box[4]));
    $yr = abs(max($box[5], $box[7]));

Here's an example of pc_ImageTTFCenter() in use:

list($x, $y) = pc_ImageTTFCenter($image, $text, $font, $size);
ImageTTFText($image, $size, $angle, $x, $y, $white, $black,
             '/path/to/font.ttf', $text);

See Also

Recipe 15.6 for more on drawing text; Recipe 15.7 for more on centering text; documentation on ImageSX( ) at http://www.php.net/imagesx, ImageSY( ) at http://www.php.net/imagesy, ImagePSBBox( ) at http://www.php.net/imagepsbbox, ImageTTFBBox( ) at http://www.php.net/imagettfbbox, ImageFtBBox( ) at http://www.php.net/imageftbbox.

Building Dynamic Images

Problem

You want to create an image based on a existing image template and dynamic data (typically text). For instance, you want to create a hit counter.

Solution

Load the template image, find the correct position to properly center your text, add the text to the canvas, and send the image to the browser:

// Configuration settings
$image    = ImageCreateFromPNG('button.png');
$text     = $_GET['text'];
$font     = ImagePSLoadFont('Times');
$size     = 24;
$color    = ImageColorAllocate($image,   0,   0,   0); // black
$bg_color = ImageColorAllocate($image, 255, 255, 255); // white

// Print centered text
list($x, $y) = pc_ImagePSCenter($image, $text, $font, $size);
ImagePSText($image, $text, $font, $size, $color, $bg_color, $x, $y);

// Send image
header('Content-type: image/png');
ImagePNG($image);

// Clean up
ImagePSFreeFont($font);
ImageDestroy($image);

Discussion

Building dynamic images with GD is easy; all you need to do is combine a few recipes together. At the top of the code in the Solution, we load in an image from a stock template button; it acts as the background on which we overlay the text. We define the text to come directly from the query string. Alternatively, we can pull the string from a database (in the case of access counters) or a remote server (stock quotes or weather report icons).

After that, we continue with the other settings: loading a font and specifying its size, color, and background color. Before printing the text, however, we need to compute its position; pc_ImagePSCenter( ) from Recipe 15.7 nicely solves this task. Last, we serve the image, and deallocate the font and image from memory.

For example, the following code generates a page of HTML and image tags using dynamic buttons, as shown in Figure 15-9:

<?php
if (isset($_GET['button'])) {

    // Configuration settings
    $image    = ImageCreateFromPNG('button.png');
    $text     = $_GET['button'];      // dynamically generated text
    $font     = ImagePSLoadFont('Times');
    $size     = 24;
    $color    = ImageColorAllocate($image,   0,   0,   0); // black
    $bg_color = ImageColorAllocate($image, 255, 255, 255); // white

    // Print centered text
    list($x, $y) = pc_ImagePSCenter($image, $text, $font, $size);
    ImagePSText($image, $text, $font, $size, $color, $bg_color, $x, $y);

    // Send image
    header('Content-type: image/png');
    ImagePNG($image);

    // Clean up
    ImagePSFreeFont($font);
    ImageDestroy($image);

} else {
?>
<html>
<head>
    <title>Sample Button Page</title>
</head>
<body>
    <img src="<?php echo $_SERVER['PHP_SELF']; ?>?button=Previous" 
         alt="Previous" width="132" height="46">
    <img src="<?php echo $_SERVER['PHP_SELF']; ?>?button=Next" 
         alt="Next"     width="132" height="46">
</body>
</html>
<?php
}
?>

Figure 15-9. Sample button page

Sample button page

In this script, if a value is passed in for $_GET['button'], we generate a button and send out the PNG. If $_GET['button'] isn't set, we print a basic HTML page with two embedded calls back to the script with requests for button images — one for a Previous button and one for a Next button. A more general solution is to create a separate button.php page that returns only graphics and set the image source to point at that page.

See Also

Recipe 15.6 for more on drawing text; Recipe 15.7 for more on centering text; an excellent discussion on dynamic image caching in Chapter 9, "Graphics," of Programming PHP, by Kevin Tatroe and Rasmus Lerdorf (O'Reilly).

Getting and Setting a Transparent Color

Problem

You want to set one of an image's colors as transparent. When the image is overlayed on a background, the background shows through the transparent section of the image.

Solution

Use ImageColorTransparent( ) :

$color = ImageColorAllocate($image, $red, $green, $blue);
ImageColorTransparent($image, $color);

Discussion

Both GIFs and PNGs support transparencies; JPEGs, however, do not. To refer to the transparent color within GD, use the constant IMG_COLOR_TRANSPARENT. For example, here's how to make a dashed line that alternates between black and transparent:

// make a two-pixel thick black and white dashed line
$style = array($black, $black, IMG_COLOR_TRANSPARENT, IMG_COLOR_TRANSPARENT);
ImageSetStyle($image, $style);

To find the current transparency setting, take the return value of ImageColorTransparent( ) and pass it to ImageColorsForIndex( ) :

$transparent = ImageColorsForIndex($image, ImageColorTransparent($image));
print_r($transparent);
Array
               (
                   [red] => 255
                   [green] => 255
                   [blue] => 255
               )
            

The ImageColorsForIndex( ) function returns an array with the red, green, and blue values. In this case, the transparent color is white.

See Also

Documentation on ImageColorTransparent( ) at http://www.php.net/imagecolortransparent and on ImageColorsForIndex( ) at http://www.php.net/imagecolorsforindex.

Serving Images Securely

Problem

You want to control who can view a set of images.

Solution

Don't keep the images in your document root, but store them elsewhere. To deliver a file, manually open it and send it to the browser:

header('Content-Type: image/png');
readfile('/path/to/graphic.png');

Discussion

The first line in the Solution sends the Content-type header to the browser, so the browser knows what type of object is coming and displays it accordingly. The second opens a file off a disk (or from a remote URL) for reading, reads it in, dumps it directly to the browser, and closes the file.

The typical way to serve up an image is to use an <img> tag and set the src attribute to point to a file on your web site. If you want to protect those images, you probably should use some form of password authentication. One method is HTTP Basic Authentication, which is covered in Recipe 8.10.

The typical way, however, may not always be the best. First, what happens if you want to restrict the files people can view, but you don't want to make things complex by using usernames and passwords? One option is to link only to the files; if users can't click on the link, they can't view the file. They might, however, bookmark old files, or they may also try and guess other filenames based on your naming scheme and manually enter the URL into the browser.

If your content is embargoed, you don't want people to be able to guess your naming scheme and view images. When information is embargoed, a select group of people, usually reporters, are given a preview release, so they can write stories about the topic or be ready to distribute it the moment the embargo is lifted. You can fix this by making sure only legal content is under the document root, but this requires a lot of file shuffling back and forth from directory to directory. Instead, you can keep all the files in one constant place, and deliver only files that pass a check inside your code.

For example, let's say you have a contract with a publishing corporation to redistribute one of their comics on your web site. However, they don't want you to create a virtual archive, so you agree to let your users view only the last two weeks worth of strips. For everything else, they'll need to go to the official site. Also, you may get comics in advance of their publication date, but you don't want to let people get a free preview; you want them to keep coming back to your site on a daily basis.

Here's the solution. Files arrive named by date, so it's easy to identify which files belong to which day. Now, to lock out strips outside the rolling 14-day window, use code like this:

// display a comic if it's less than 14 days old and not in the future

// calculate the current date
list($now_m,$now_d,$now_y) = explode(',',date('m,d,Y'));
$now = mktime(0,0,0,$now_m,$now_d,$now_y);

// two hour boundary on either side to account for dst
$min_ok = $now - 14*86400 - 7200; // 14 days ago
$max_ok = $now + 7200;            // today

// find the time stamp of the requested comic
$asked_for = mktime(0,0,0,$_REQUEST['mo'],$_REQUEST['dy'],$_REQUEST['yr']);

// compare the dates
if (($min_ok > $asked_for) || ($max_ok < $asked_for)) {
    echo 'You are not allowed to view the comic for that day.';
} else {
    header('Content-type: image/png');
    readfile("/www/comics/$_REQUEST['mo']$_REQUEST['dy'] $_REQUEST['yr'].png");
}

See Also

Recipe 18.6 for more on reading files.

Program: Generating Bar Charts from Poll Results

When displaying the results of a poll, it can be more effective to generate a colorful bar chart instead of just printing the results as text. The function shown in Example 15-4 uses GD to create an image that displays the cumulative responses to a poll question.

Example 15-4. Graphical bar charts

function pc_bar_chart($question, $answers) {

    // define colors to draw the bars
    $colors = array(array(255,102,0), array(0,153,0),
                    array(51,51,204), array(255,0,51),
                    array(255,255,0), array(102,255,255), 
                    array(153,0,204));

    $total = array_sum($answers['votes']);
    
    // define some spacing values and other magic numbers
    $padding = 5;
    $line_width = 20;
    $scale = $line_width * 7.5;
    $bar_height = 10;

    $x = $y = $padding;

    // allocate a large palette for drawing, since we don't know
    // the image length ahead of time
    $image = ImageCreate(150, 500);
    $bg_color = ImageColorAllocate($image, 224, 224, 224);
    $black = ImageColorAllocate($image, 0, 0, 0);

    // print the question
    $wrapped = explode("\n", wordwrap($question, $line_width));
    foreach ($wrapped as $line) {
        ImageString($image, 3, $x, $y , $line, $black);
        $y += 12;
    }

    $y += $padding;

    // print the answers
    for ($i = 0; $i < count($answers['answer']); $i++) { 

        // format percentage
        $percent = sprintf('%1.1f', 100*$answers['votes'][$i]/$total);
        $bar = sprintf('%d', $scale*$answers['votes'][$i]/$total);

        // grab color
        $c = $i % count($colors); // handle cases with more bars than colors
        $text_color = ImageColorAllocate($image, $colors[$c][0], 
                                 $colors[$c][1], $colors[$c][2]);

        // draw bar and percentage numbers
        ImageFilledRectangle($image, $x, $y, $x + $bar,
                             $y + $bar_height, $text_color);
        ImageString($image, 3, $x + $bar + $padding, $y, 
                    "$percent%", $black);

         $y += 12;

         // print answer
         $wrapped = explode("\n", wordwrap($answers['answer'][$i], $line_width));
         foreach ($wrapped as $line) {
             ImageString($image, 2, $x, $y, $line, $black);
             $y += 12;
         }

         $y += 7;
     }

     // crop image by copying it
     $chart = ImageCreate(150, $y);
     ImageCopy($chart, $image, 0, 0, 0, 0, 150, $y);

     // deliver image
     header ('Content-type: image/png');
     ImagePNG($chart);

     // clean up
     ImageDestroy($image);
     ImageDestroy($chart);
}

To call this program, create an array holding two parallel arrays: $answers['answer'] and $answer['votes']. Element $i of each array holds the answer text and the total number of votes for answer $i. Figure 15-10 shows this sample output.

// Act II. Scene II. 
$question = 'What a piece of work is man?';

$answers['answer'][  ] = 'Noble in reason';
$answers['votes'][  ]  = 29;

$answers['answer'][  ] = 'Infinite in faculty';
$answers['votes'][  ]  = 22;

$answers['answer'][  ] = 'In form, in moving, how express and admirable';
$answers['votes'][  ]  = 59;

$answers['answer'][  ] = 'In action how like an angel';
$answers['votes'][  ]  = 45;

pc_bar_chart($question, $answers);

Figure 15-10. Graphical bar chart of poll results

Graphical bar chart of poll results

Here the answers are manually assigned, but for a real poll, this data could be pulled from a database instead.

This program is a good start, but because it uses the built-in GD fonts, there are a lot of magic numbers embedded in the program corresponding to the font height and width. Also, the amount of space between each answer is hardcoded. If you modify this to handle more advanced fonts, such as PostScript or TrueType, you'll need to update the algorithms that control those numbers.

At the top of the function, a bunch of RGB combinations are defined; they are used as the colors to draw the bars. A variety of constants are broken out, such as $line_width, which is the maximum number of characters per line. The $bar_height variable determines how high the bars should be, and $scale scales the length of the bar as a function of the longest possible line. $padding is used to push the results five pixels away from the edge of the canvas.

We then make a very large canvas to draw the chart; later, we will crop the canvas down to size, but it can be difficult to know ahead of time how large our total size will be. The default background color of the bar chart is (224, 224, 224), or a light gray.

In order to restrict the width of the chart to a reasonable size, we use wordwrap( ) to break our $question down to size and explode( ) it on \n. This gives us an array of correctly-sized lines, which we loop on to print out one line at a time.

After printing the question, we move on to the answers. First, we format the results numbers with sprintf( ) . To format the total percentage of votes for an answer as a floating-point number with one decimal point, we use %1.1f. To find the length of the bar corresponding to that number, you compute a similar number, but instead of multiplying it by 100, we multiply by a magic number, $scale, and return an integer.

The text color is pulled from the $colors array of RGB triplets. Then, we call ImageFilledRectangle( ) to draw the bar and ImageString( ) to draw the percentage text to the right of the bar. After adding some padding, we print the answer using the same algorithm used to print the question.

When all the answers have been printed, the total size of bar chart is stored in $y. Now we can correctly crop the graphic to size, but there's no ImageCrop( ) function. To work around this, we make a new canvas of the appropriate size and ImageCopy( ) over the part of the original canvas you want to keep. Then we serve the correctly sized image as a PNG using ImagePNG( ) and clean up with two calls to ImageDestroy( ).

As we mentioned at the beginning of this section, this is just a quick-and-dirty function to print bar charts. It works, and solves some problems, such a wrapped lines, but isn't 100% perfect. For instance, it's not very customizable. Many settings are baked directly into the code. Still, it shows how to put together a variety of GD's functions to create a useful graphical application.

Personal tools