12
Multimedia


Multimedia is an ambiguous term that is used to describe many different things. This chapter focuses on multimedia as that which goes beyond ordinary text and graphics, pushing the limits of HTML to bring Web pages to life. An immediate thought that may come to mind is that of adding audio and video. Small, animated graphics are a popular attraction for many Web site builders. Although many animation techniques might be best done using a client-side language such as Java, you can do several things from the server using Perl.

In this chapter, you explore the use of the GD Perl modules developed by Lincoln Stein. You also look into server push techniques and quickly review other ways you can embed multimedia into your Web pages.

Implementation Issues

One important consideration when discussing multimedia is performance. Bandwidth is once again the limiting factor when you're creating good animation representation on the Web. You should consider some of the ideas discussed previously in this book to reduce the size of your images. Although your animated graphics may entice people to visit your Web site, the visitors can just as easily be driven away by those same graphics because of the time it may take to download them.

Another important consideration is color depth. Images are best represented using 24-bit color; however, using this color can be very expensive in terms of file size. You have to measure the trade-off between representation of true color and performance. One way to do so is to use a lower depth of color and create your own logical color space. You can create an image that uses only 256 colors but define your own custom colors to suit the image. Color depth becomes even more important as you start using multiple images to create the animation.

GD: Dynamic Images

In CPAN, you can find a module called GD. This module provides a Perl interface to the GD graphics library, which was written by Thomas Boutell. This library provides several routines for creating, reading, writing, and manipulating GIF files. The routines are implemented as graphic primitives such as line and arc drawing and fill routines. Using this library, you also can render simple fonts as well as read and utilize existing GIF files.

For the examples in this chapter, you will use the GD module to dynamically create images for a Web page. First, however, you need to go over a few of the basics concerning the GD module.

GD::Image Module

The GD::Image module provides an interface to the image data and graphics primitive aspect of the GD library. This class contains interfaces for reading and writing GIF files and dynamically creating GIF file contents. In the following sections, you examine some of the functions provided in this class.

GD::Image::new(width, height)
The GD::Image::new(width, height) function creates a new empty image of width width and height height. You begin here if you want to create a dynamic image using the drawing primitives. Alternatively, you can construct a GD::Image object using newFromGif(FILEHANDLE), newFromXbm(FILEHANDLE), or newFromGd(FILEHANDLE). These constructors load the image content data from FILEHANDLE, which can point to a file of type GIF, XBM, or GD format. Storing files in GD format is not recommended because no compression is performed, causing these files to potentially become rather large.

GD::Image::gif() and GD::Image::gd() The GD::Image::gif()and GD::Image::gd() methods are used to emit a GD::Image object in
either GIF or GD format. You generally use them after you have finished creating your image content and are ready to either save the image to disk or return the image data from a CGI script.

GD::Image::colorAllocate(red, green, blue) The GD::Image::colorAllocate(red, green, blue) method allocates a color corresponding to an RGB triplet. It returns a color index value that can later be used with the drawing primitives. This color index can also be passed into the colorDeallocate(colorIndex) method. A number of special color index values are automatically allocated. These constants are contained within the GD:: namespace but are automatically exported when the GD module is loaded. I mention these special color indices as I cover their uses.

GD::Image::transparent(colorIndex) The GD::Image::transparent(colorIndex) method marks the color specified by colorIndex as transparent color. All pixels using this color are invisible, thus allowing the background to appear. This capability is useful for creating the illusion of nonrectangular images, because all images are, in fact, rectangles, regardless of how they might appear.

A few other methods are related to color control, but I don't mention them here. To get a complete reference on this module, be sure to check the latest version in CPAN.

GD::Image::setBrush(GD::Image) The GD::Image::setBrush(GD::Image) method establishes a brush or pattern that you can use when drawing lines or arcs. You first create a GD::Image object that acts as your brush, pass it as an argument to this method, and then use the gdBrushed special color index when calling one of the drawing primitives.

GD::Image::setTile(GD::Image)
The GD::Image::setTile(GD::Image) method establishes a tile pattern that you can use when filling areas. You first create a GD::Image object that defines your pattern, pass it as an argument to this method, and then use the gdTiled special color index as the fill color for one of the drawing methods.

GD::Image::setPixel(x,y,colorIndex)
The GD::Image::setPixel(x,y,colorIndex) method sets the color of a single pixel value at location (x,y) to the color specified by colorIndex.

GD::Image::line(x1,y1,x2,y2,colorIndex)
The GD::Image::line(x1,y1,x2,y2,colorIndex) method draws a line from (x1,y1) to (x2,y2) using the color specified by colorIndex. You can also use the dashedLine() method to draw a dashed or dotted line. To obtain greater control over the appearance of your dotted line, you can also use the setStyle() method and specify the gdStyled special color index value as your colorIndex for the line() method.

GD::Image::rectangle(x1,y1,x2,y2,colorIndex)
The GD::Image::rectangle(x1,y1,x2,y2,colorIndex) method draws a rectangle using the color specified in colorIndex. (x1,y1) refers to the upper-left corner of the rectangle, and (x2,y2) refers to the lower-right corner. You can draw a filled rectangle using the filledRectangle() method.

GD::Image::polygon(polygon,colorIndex) The GD::Image::polygon(polygon,colorIndex) method draws a polygon defined by a GD::Polygon object, which I describe later in this chapter. The colorIndex specifies the color in which to draw the polygon. You can also draw a filled polygon using the filledPolygon() method.

GD::Image::arc(cx,cy,width,height,start,end,colorIndex)
The GD::Image::arc(cx,cy,width,height,start,end,colorIndex) method draws arcs or ellipses using the color specified in colorIndex. (cx,cy) defines the center point, and width and height specify the width and height of the ellipse. start and end specify the angles at which to begin and end the arc. These values are specified in degrees between 0 and 360. A start of 0 and end of 360 with height equal to width would produce a full circle.

GD::Image::fill(x,y,colorIndex)
The GD::Image::fill(x,y,colorIndex) method fills a region with the color specified by colorIndex. It starts at location (x,y) and continues setting the color of adjacent pixels until it reaches a pixel of a different color than the starting pixel. In addition to normal RGB colors, you can also use the special color gdTiled to fill with a pattern.

GD::Image::string(font,x,y,string,colorIndex)
The GD::Image::string(font,x,y,string,colorIndex) method enables you to draw text into your image using the specified font and colorIndex. (x,y) defines the pixel location at which to draw the string. The fonts from which you can choose are gdSmallFont, gdMediumBoldFont, gdTinyFont, and gdLargeFont.

GD::Image::stringUp(font,x,y,string,colorIndex)
The GD::Image::stringUp(font,x,y,string,colorIndex) method is the same as string(), except that it draws the text rotated counterclockwise at 90 degrees.

GD::Image::interlaced()
The GD::Image::interlaced() method enables you to set or query whether the image is interlaced. An interlaced image provides a venetian-blinds effect on some viewers while the image is first being displayed. Calling this method with no parameters returns 1 or 0, depending on whether the image is interlaced or not. Calling this method with a 1 or 0 sets or removes the interlaced attribute of the image.

GD::Image::getBounds()
The GD::Image::getBounds() method returns the width and height of the image in a two-
member list. You can query the width and height of an image, but you cannot change these values after an image has been created.

GD::Image::copy(srcImage,dstX,dstY,srcX,srcY,width,height)
The GD::Image::copy(srcImage,dstX,dstY,srcX,srcY,width,height) method enables you to copy one image into another. You call this method on the target image and specify the source image as srcImage. (dstX,dstY) specifies the point in the destination image at which you want to copy to. (srcX,srcY) specifies the point in the source image at which you want to copy from. width and height specify the dimensions of the area being copied.

You can also resize or scale an image while copying by using the copyResized() method. In addition to source and destination coordinates, you can specify source and destination width and height. This way, you can extend or reduce an image as well as modify the proportions of an image.

GD::Polygon Module

The GD::Polygon module provides an interface to a polygon shape for use with the GD::Image::polygon() and GD::Image::filledPolygon() methods. Polygons consist of vertices that are defined as Cartesian coordinates. You construct polygon objects by using the new() method and the addPt(x,y) method. addPt(x,y) adds a vertex to the polygon. A triangle, for example, might have vertices of (0,0), (10,10), and (10,0). getPt(index), setPt(index,x,y), and deletePt(index) getPt() returns the point corresponding to the vertex at a given index. setPt()enables you to modify the vertex at index to contain the new point (x,y). deletePt() deletes a vertex from the polygon. length() and vertices() length()returns the number of vertices in the polygon, and vertices()returns a list of those vertices. The list returned by vertices() contains references to arrays containing the (x,y) coordinates of each vertex. bounds() The bounds()method returns the smallest bounding rectangle of the polygon specified as the list ($left,$top,$right,$bottom), where each of these variables is a reference to an array specifying the (x,y) coordinate values. offset(dx,dy) The offset(dx,dy) method enables you to move a polygon by a specified offset of dx and dy. Each vertex is offset by these amounts. This method is useful for the animation of a polygon. map(srcL,srcT,srcR,srcB,destL,destT,destR,destB) The map(srcL,srcT,srcR,srcB,destL,destT,destR,destB) method maps a polygon from a source rectangle to an equivalent position within a destination rectangle. The polygon is moved and resized according to the mapping between the two rectangles. You can use the bounding box of the polygon itself as the srcL, srcT, srcR, and srcB values.

GD::Font Module

As I mentioned previously, four fonts are available for use within the GD module. These fonts are specified as constants and are exported into your namespace when you load the GD module. This class additionally provides methods for querying the font. nchars() returns the number of glyphs contained in the font, offset() returns the ASCII value of the first character in the font, and width() and height() return the width and height of the font.

Currently, you cannot create your own fonts or manipulate the predefined fonts. Perhaps this support will be added to the GD library one day.

You can find the latest GD module in your nearest CPAN location, along with a more complete reference of the functionality. For more information on the GD library, refer to http://www.boutell.com/gd.

Hangman Using the GD Module

Now you're ready to use the GD module to produce a simple game. Hangman is a game that involves some dynamically drawn graphics. This example contains some solid filled shapes, outline shapes, lines, and transparency. To create this game, you combine what you've learned so far about CGI programming with this new concept of dynamic image creation.

You start by writing the function that draws the entire image. It takes an argument to determine how much to draw; for example, the value 1 indicates to simply draw the head. The value 6 indicates to draw the entire body, which, of course, means that the Hangman game is over. Here's how to get started:

sub drawMan {

   my($level)=@_;

   # create the image.

   my($image) = new GD::Image(100,100);

   my($trans) = $image->colorAllocate(128,128,128);

   my($white) = $image->colorAllocate(255,255,255);

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

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

   my($blue) = $image->colorAllocate(0,0,255);

   my($green) = $image->colorAllocate(0,255,0);

   $image->transparent($trans);

   $image->interlaced(1);

   $gallows=new GD::Polygon();

   $gallows->addPt(10,80);

   $gallows->addPt(10,10);

   $gallows->addPt(50,10);

   $gallows->addPt(50,20);

   $gallows->addPt(25,20);

   $gallows->addPt(25,80);

   $image->filledPolygon($gallows,$blue);

   $image->line(50,20,50,30,$green);

   $image->line(10,80,80,80,$red);

   if ($level>0) {

      $image->arc(50,35,10,10,0,360,$black);

      if ($level>1) {

         $image->line(50,40,50,60,$black);

         if ($level>2) {

            $image->line(60,35,50,50,$black);

            if ($level>3) {

               $image->line(40,35,50,50,$black);

               if ($level>4) {

                  $image->line(50,60,60,75,$black);

                  if ($level>5) {

                     $image->line(50,60,40,75,$black);

                  }

               }

            }

         }

      }

   }

   print $image->gif;

}

You now create the Hangman game around this drawing function. To keep the game simple, use a small pool of words from which users can choose. To display an image within your HTML document, you must first store it to a file. Then you can use the <IMG> tag to embed it into your document. For this example, simply store the image to a temporary file where the name of the file is determined by the process ID. Here is how the main program looks:

#!/usr/local/bin/Perl



use GD;

use CGI::Form;



@wordlist=(`navigator`,`explorer`,`hypertext`,`practical',`extraction`,

           `reporting',`language',`portable',`document`,`format`,

           `graphic',`interchange',`multimedia',`programming',

           `implementation','management');



@letters=(`a',`b',`c',`d',`e',`f',`g',`h',`i',

          `j',`k',`l',`m','n',`o',`p',`q',`r',

          `s',`t',`u',`v',`w',`x',`y',`z');



$guesses="";

$numguesses=0;

$img_file="/user/bdeng/Web/docs/temp/$$.gif";

$q = new CGI::Form;



print $q->header();

print $q->start_html(-title=>`Hangman!');



print "<H1>Hangman</H1>\n";

if ($q->cgi->var(`REQUEST_METHOD') eq `GET') {

   &gameIntro($q);

} else {

   $action=$q->param(`Action');

   if ($action eq "New Game") {

      &gameIntro($q);

   } else {

      &playGame($q);

   }

}



print $q->end_html();

As you can see from the main program, you have two main subroutines. The first, which follows, is called gameIntro(); it is used to initialize the game. Initialization involves choosing a random word, drawing the empty gallows, and setting up some variables used to keep track of the user's progress.

sub gameIntro {

   my($q)=@_;

   $numWords=@wordlist;

   srand(time|$$);

   my($index)=int(rand($numWords));

   $word=$wordlist[$index];

   $guesses="";

   $numguesses=0;

   $q->param(`word',$word);

   $q->param(`guesses',$guesses);

   $q->param(`numguesses',$numguesses);

   print "<P>Welcome to Hangman.";

   print "Select a letter and click `Guess' to start playing<BR>\n";

   &drawHangman($q,$numguesses);

   &drawWord($word,$guesses);

   &gameForm($q);

}

You use the srand() Perl function to set the seed for generating random numbers. Using the current time together with the process ID should give you a random value. This subroutine calls three other subroutines, which are shared with the next subroutine.

The other main subroutine, which follows, is called playGame(); here you query the letter that the user guesses and fill in the found letters on each subsequent call. This routine is also responsible for determining when the user wins or loses. It also makes use of the drawHangman(), drawWord(), and gameForm() subroutines.

sub playGame {

   my($q)=@_;

   $word=$q->param(`word');

   $letter=$q->param(`Letter');

   $numguesses=$q->param(`numguesses');

   $guesses=$q->param(`guesses');

   if (index($word,$letter)>=0) {

      print "<P>Good Guess!<BR>\n";

   } else {

      $numguesses++;

      $q->param(`numguesses',$numguesses);

      if ($numguesses==6) {

         print "<P>Sorry! You lose! The word was $word<BR>";

      } else {

         print "<P>Sorry. Please try again.<BR>";

      }

   }

   $guesses .= $q->param(`Letter');

   $q->param(`guesses',$guesses);

   $q->param(`word',$word);

   if (&guessedFullWord($word,$guesses)) {

      print "<P><H3>Congratulations! You Win!</H3><BR>";

   }

   &drawHangman($q,$numguesses);

   &drawWord($word,$guesses);

   &gameForm($q);

}

Next, look at the three common subroutines responsible for emitting the rest of the HTML. drawHangman() is just a wrapper around the drawMan() routine you saw earlier. Its role is to create a temporary file and print out the <IMG> tag for displaying the dynamic GIF. drawWord()is a bit more complicated. Its job is to mask the part of the word that the user has not already guessed. It does so using nested for loops. And gameForm() should look very familiar to you by now. This routine uses the CGI::Form methods to display all the form fields. The hidden() fields enable you to maintain the state of the game across multiple CGI requests. The subroutines are as follow:

sub drawHangman {

   my($q,$numguesses)=@_;

   open(TEMP,"> $img_file");

   select(TEMP);

   &drawMan($numguesses);

   select STDOUT;

   close(TEMP);

   chmod 0755, $img_file;

   print "<IMG SRC=/temp/$$.gif>";



}



sub drawWord {

   my($word,$guesses)=@_;

   my($displayWord)="";

   for ($i=0;$i<length($word);$i++) {

      my($found)=0;

      for ($j=0;$j<length($guesses);$j++) {

         if (substr($word,$i,1) eq substr($guesses,$j,1)) {

            $found=1;

         }

      }

      if ($found) {

         $displayWord .= substr($word,$i,1);

      } else {

         $displayWord .= "_";

      }

      $displayWord .= " ";

   }

   print "<BR><H2>$displayWord</H2><BR>\n";



}



sub gameForm {

   my($q)=@_;

   print $q->start_multipart_form();

   print $q->popup_menu(-name=>`Letter',-value=>\@letters);

   print $q->hidden(-name=>`word', -value=>"$word");

   print $q->hidden(-name=>`guesses', -value=>"$guesses");

   print $q->hidden(-name=>`numguesses', -value=>"$numguesses");

   print $q->submit(-name=>`Action',-value=>`Guess');

   print " ";

   print $q->submit(-name=>`Action',-value=>`New Game');

   print $q->endform;

}

Finally, you need the guessedFullWord() function, which returns whether the user has completely guessed the entire word. It uses the same algorithm that drawWord uses, except that it returns FALSE as soon as it detects a letter that has not been found.

sub guessedFullWord {

   my($word,$guesses)=@_;

   my($i,$j);

   for ($i=0;$i<length($word);$i++) {

      my($found)=0;

      for ($j=0;$j<length($guesses);$j++) {

     if (substr($word,$i,1) eq substr($guesses,$j,1)) {

        $found=1;

     }

      }

      if (!$found) {

     return 0;

      }

   }

   return 1;

}

Images of the game in action are shown in Figures 12.1 through 12.3.

Figure 12.1. The Hangman game within the Netscape browser.

Figure 12.2. The game after two wrong guesses.

Figure 12.3. The game after the user lost.

The Hangman game is a simple example of how to use the GD module to draw graphics dynamically from a CGI script. This example runs on UNIX, although you could run it on Windows using a slightly modified version of GD.pm from David Roth. This version is available at ftp://roth.net.com/pub/ntPerl/win32GD.zip. The current version of MacPerl (5.10r2) also supports the standard GD module.

Server Push Animation Techniques

Server push, which is a mechanism developed by Netscape, allows a server to maintain an open connection with the client. The technique I describe here concerns the use of the x-multi-
replace
MIME type. Document content defined by this MIME type can be automatically refreshed on a specific interval. This capability enables you to draw a sequence of images in the same frame to construct a "poor man's animation." This animation can either be continuous or finite.

To draw a finite animation sequence, you include in your Perl script a finite loop that completes on the last image frame. A continuous animation sequence does not stop until the user clicks Stop in the browser or leaves the page entirely. You must consider that, while a user is connected to a page that has a continuous animation sequence, the TCP port connection remains open for the duration of that user's visit. Keeping this connection open can prove expensive, depending on the capacity of your Web server. You need to consider several points if you decide to use a server push animation technique. The following URL points to some good documentation on this subject:

http://home.netscape.com/assist/net_sites/pushpull.html

The following is an example of how to write a server push CGI script. Assume that you have a sequence of images, frame1.gif through frame10.gif. The important point is to declare the document as multipart/x-mixed-replace; and to specify a random string as the boundary.

#!/usr/local/bin/Perl



use GD;



$frmLoc="/user/bdeng/Web/docs/images";

$header="Content-type: multipart/x-mixed-replace;" . 

        "boundary=***Boundary_String***\n";

$boundary="\n--***Boundary_String***\n";

$giftype="Content-type: image/gif\n\n";



print $header;

print $boundary;

$i=1;

while (1) {

   sleep 1;

   print $giftype;

   open(GIFH,"< ${frmLoc}/frame${i}.gif");

   $img = newFromGif GD::Image(GIFH);

   close(GIFH);

   print $img->gif;

   print $boundary;

   if ($i == 10) {

      $i=1;

   } else {

      $i++

   }

}

To view this animation on a Web page, simply point the SRC attribute of your <IMG> tag to the CGI script, like this:

<HTML>

<BODY>

<H1>Animated GIF using Server-push</H1>

<IMG SRC=/cgi-bin/frames.pl>

<P>This is a poor man's animation that downloads the next frame continuously

until you leave the page.

</BODY>

</HTML>

Figure 12.4 shows the resulting animation.

Figure 12.4. One frame of the server push animation page.

Other Techniques

You can, of course, embed multimedia into your Web documents in other ways by using some of the conventional multimedia standard file formats. In the following section, I briefly describe some of these options, even though they really don't involve the use of Perl.

Embedding AVI, QuickTime, WAV, and GIF89a within HTML

QuickTime, which is a movie format developed by Apple Computer, Inc., is available on many platforms. AVI is a similar format available on the Windows platform. WAV is an audio format available for use on the Windows platform. Most of these formats are registered MIME types known by browsers. The Web author's role is to simply include these types of files within the HTML file. Some browsers can view or play these files directly within the browser window. Netscape's plug-in technology for Netscape Navigator and Microsoft's ActiveX technology in the Internet Explorer are examples. Using these technologies, you can directly embed these multimedia formats within Web pages. Refer to Chapter 4, "HTML Forms--The Foundation of an Interactive Web," for a discussion of the <EMBED> HTML tag.

Previously in this chapter, you learned how to implement a "poor man's animation" using the Netscape server push technique. You also can author a GIF file in such a way that server push is not required for an animation effect. A new developing standard called GIF89a is now available; it defines an extension to the GIF format, allowing for multiple frames to be stored and played back in a single GIF file. Netscape Navigator 2.0 and higher support GIF89a to some extent. To find more information about the GIF89a standard, you can visit http://www.reiworld.com/royalef/gifabout.htm.

Summary

Hopefully, this chapter has given you a few ideas on how to implement some multimedia techniques on your Web page. Again, I cannot stress enough the importance of minimizing the size of your media files. Some techniques on how to reduce the size of your image are discussed in Chapter 7, "Simple Pleasures--Examples." You may also find some good examples of GIF animation in Web Page Wizardry by Dick Oliver (Sams.net Publishing, ISBN 1-57521-094-4).