Chapter  22

Faster Image Downloads

by Mark Wutka


CONTENTS

Images often account for a large amount of an applet's down-load time. Even though both the GIF and JPEG formats used by Java involve data compression, the images can still be rather large.

Since these images are already compressed, you won't realize much benefit from additional data compression, either. You need to find ways of either reducing the original image size or getting the images in a more efficient fashion.

Reducing Image Size

One of the easiest techniques for quicker downloads isn't part of Java at all. If you are willing to accept a reduction in image quality, you can often drastically reduce the size of an image.

The two formats used by Java are GIF (Graphics Interchange Format) and JPEG (Joint Photographic Experts Group-the creators of the format). Each has its own advantages and disadvantages, and frequently, one or the other provides a distinct size advantage.

The GIF format is geared toward 8-bit images or at least images with fewer colors. The GIF format works better when storing noncomplex images.

When an image is complex, as in a photograph, GIF does not store images as well as other formats. It works very well for storing patterns like a checkerboard or your Web page background.

The JPEG format was created with photography in mind. It allows 24-bit color and can store images with varying image quality.

JPEG has a number of optimizations that assume that it is storing photograph-type images. So when it goes to store something as mundane as a checkerboard, the resulting file can be several times as large as the equivalent GIF file.

Since JPEG allows you to store images with varying quality, you can reduce the size of a JPEG just by storing it as a lower-quality image. The quality of an image is actually determined by the amount of image compression.

The compression is given as a percentage ranging from 0 to 100. The higher the amount of compression, the lower the quality. You don't have to use 0 percent compression all the time when storing high-quality images.

Many images can be stored with 20 percent or even 40 percent compression with no loss of quality. On the other side, 100 percent compression does not reduce a file to nothing.

The variations in file size are not always in direct proportion to the change in compression factor. Figure 22.1 shows a 24-bit image stored with 40 percent compression, whereas Figure 22.2 shows an image stored with 80 percent compression.

Figure 22.1 : JPEG can compress many images with no loss of quality.

Figure 22.2 : JPEG images tend to get grainier the more they are compressed.

Even though the compression of the second figure is twice the amount of the first figure, the reduction in file size is much greater. The file size for the first figure is approximately 96K, whereas the file size for the second figure is about 16K.

If you are really concerned with image size, try storing the image in both GIF and JPEG formats and see which is smaller. If JPEG is the way you want to go, try varying amounts of compression to see how much quality you are willing to lose in exchange for a smaller file.

Image Strips

Every time you download a file over the network, a certain amount of overhead is involved in setting up the network connection, no matter how small the file. If you have to download a large number of files, you lose a lot of time in the connection overhead alone.

To compound the problem, if you download a large number of files simultaneously, you can't predict which file will be loaded first. This may not be so bad when you are downloading data files but if you are doing animation, it can be a pain.

You want to present something to the user as quickly as possible, preferably immediately, even if it's just a "Please wait…" message. Depending on the animation, you might want to grab the first frame and display it while waiting for the rest.

You might also build up the animation gradually, showing the frames in order as you get them. For instance, if you have the first two frames, loop through them, adding the third frame to the loop when it is loaded.

You can save yourself a lot of time if you just combine your images onto a single, larger image and download it. Although it's true it takes longer than downloading a single frame, the overall time to download the single image is less than the time it would take to download 12 frames individually.

The image containing all your animation frames is called an image strip.

Tip
You don't have to be doing animation to use this technique. It works any time you need to load several images into your applet.

Figure 22.3 shows an image strip consisting of multiple views of a person with a real head and cartoon body.

Figure 22.3 : Combining multiple images onto one image strip can save you time when downloading.

You can create an image strip with almost any paint program, as long as it works with a GIF or JPEG format. One thing to look for, however, is the ability to determine pixel coordinates. You need to know exactly where on the image strip each image is located.

Many paint programs show you the current cursor location somewhere on the screen, which helps tremendously. If the program has a zoom feature, it really takes the guesswork out of finding the images.

The trick to displaying images from an image strip is that you make use of clipping. Although you could use the CropImageFilter class to view just a portion of the image, the class adds a lot of unnecessary overhead. The clipping functions built into the AWT do the same function only much, much faster.

When you draw an image from an image strip, you are essentially viewing the image strip through a lens the size of the image you want to draw. You move the image strip around underneath the lens to view a different image.

It's like using a microscope. The microscope lens is in a fixed position. If you want to see a different part of the microscope slide, you have to move the slide around.

Since the lens is fixed and you must move the image, you have to move the image in the opposite direction from the direction you would move the lens. In other words, if you normally move the lens 50 pixels to the right and 20 pixels down, you must move the image 50 pixels to the left and 20 pixels up.

Figure 22.4 shows the relative positioning of the lens and the image.

Figure 22.4 : You must draw an image strip relative to the lens.

In Java lingo, the lens you use to view the image strip is called the clipping area. A clipping area is the area in which you can draw.

You may have noticed that Java doesn't give you an error when you try to draw images that are way outside the bounds of your applet but it also doesn't draw outside the bounds. This is because all your drawing is done within a clipping region.

The default clipping region for your applet is the entire area of the applet. You can change the clipping area, however, with the Graphics.clipRect method.

Using the Graphics.clipRect Method

The clipRect method in the Graphics class allows you to change the clipping region of your drawing area. The only restriction is that you can never enlarge the clipping area.

To draw an image from an image strip, you first set the clipping area to the location and size of the image you are drawing. For example, suppose you want to draw an 80¥60 image at location 40,30.

The corresponding clipRect call, assuming the variable g is an instance of Graphics, would be:

g.clipRect(40, 30, 80, 60);

Once you have created the clipping region, you still draw the image relative to the whole graphics area. In other words, the clipping region creates something like a graphics stencil that protects the rest of the graphics area from being painted, but you act like you are still painting the entire graphics area.

Remember that when you use image strips, you really draw the entire image strip every time; you just create a small window on top of the image strip so you see only one image at a time. Once you create a clipping region, you still have to figure out the x and y coordinates where the image strip should be drawn. The formula for the image strip's x and y coordinates is:

int imageStripX = clippingRegionX - imageX;
int imageStripY = clippingRegionY - imageY;

The imageX and imageY variables are the x and y coordinates of the image you want to draw relative to the image strip. In other words, if you want to draw an image from an image strip that is at location 50,10 on the strip, you would use 50 for the imageX and 10 for the imageY variables.

For the example of a clipping region at 40,30 and an image location of 50,10, you would draw the image strip at -10, 20 (that's 40-50, 30-10). To see why this is so, think of what would happen if you drew the image strip at 0,0. The image you really want is over at 50,10. Now, shift the image 50 pixels to the left and 10 pixels up (draw it at -50,-10). Now the image you want to draw is at location 0,0 on the screen. You really want it at 40,30, however, so you add 40 to the x coordinate and 30 to the y coordinate, moving the image you want over to location 40,30. Now if you look at what you did to the actual x,y of the full image strip, you moved the x coordinate left 50 and right 40, for a total movement of left 10, making its x coordinate -10. You shifted the y coordinate up 10 spaces and down 30 spaces, making a total movement of 20 pixels down, giving a y coordinate of 20.

Note
Since you can't enlarge the clipping area, once you reduce the clipping area to the size of the image, you can't draw anything outside of that boundary. If you need to draw multiple things in your paint method, do the image strip drawing last. This becomes a real problem if you do offscreen drawing. Normally, when you do off-screen drawing, you create an offscreen graphics context one time, just after you create the offscreen image. Once you change the clipping region on the offscreen graphics context, it stays changed. If you want to reset it, you have to create another offscreen graphics context by calling the getGraphics method in the offscreen image. You should probably also call the dispose method in the old graphics context first to free up its resources.

Creating Another Graphics Context

As you now know, the clipRect method has a serious drawback in that it can only shrink, effectively limiting you to drawing one image-strip image in a paint method. This may be acceptable in many cases, but you need an alternative.

Instead of changing the clipping region, you can create a new graphics drawing area that is a portion of the current drawing area. Then, instead of calling the drawImage method in your current drawing area, you call the same method in the subarea.

The create method in the Graphics class creates a subarea within the main drawing area. If you change the clipping region in the subarea, it doesn't affect the main area.

You don't need to clip the subarea, however, because you can just create it to be the size of the clipping area you want. To create a subarea at location 40,30 that is 80¥60 pixels, you would do this:

Graphics subArea = g.create(40, 30, 80, 60);

You could then draw your image in this subarea:

subArea.drawImage(imageStrip, -75, -25, this);

Once you are done with the subarea, you should free up its resources by calling the dispose method:

subArea.dispose();

Note
When you draw images on a subarea, you do not add the x and y locations of the subarea to the coordinates for the image strip. In other words, if you use a subarea to draw an image that is at location 75,25 on an image strip, you always draw the image strip at location -75,-25, no matter where you create the subarea. This is different from the method where you just create a clipping region. The coordinates of the upper-left corner of a clipping region are the coordinates relative to the drawing area. The coordinates of the upper-left corner of a subarea are 0,0.

You can use the following method in your programs to draw images from an image strip without doing all the clipping yourself:

public void drawStripImage(Graphics g, Image imageStrip,
     int drawX, int drawY, int stripX, int stripY,
     int imageWidth, imageHeight)
{
     Graphics subArea = g.create(drawX, drawY, imageWidth,
          imageHeight);
     subArea.drawImage(imageStrip, -stripX, -stripY, this);
     subArea.dispose();
}

In the preceding method, g is the original Graphics object from your paint method, imageStrip is the image strip you are drawing from, drawX,drawY are the coordinates where you want to draw the image, stripX,stripY is the location of the image on the image strip, and imageHeight and imageWidth are the width and height of the image.

Storing Only Parts on an Image Strip

You can save a great deal of additional time in downloading if you put whole frames in an image strip. The animation you are doing often has pieces that don't change from one frame to the next.

When that is the case, you can save space by storing the common parts only once. When you draw each animation frame, you put the parts together to make a whole image.

The images in Figure 22.3 have many common pieces. In fact, they were designed by using some common pieces. Every image has an identical head, and there are only three different body positions.

The only things that change consistently from frame to frame are the legs. This kind of partitioning takes a little bit of work up front, although it actually makes it easier to create multiple frames on your image strip since you don't have to redraw the entire frame each time.

Figure 22.5 shows a reduced image strip that contains only pieces of the images.

Figure 22.5 : Storing only pieces of images that are put together later can save even more time.

The file holding the image pieces is about one-fourth the size of the full set of images. When you piece together images, it is a good idea to use the GIF format and use a transparent pixel.

The image in Figure 22.5 uses black as the transparent pixel. This means that whenever pieces from the image strip are drawn, any area that is black is not drawn. This allows you to overlay pieces on top of one another.

Figure 22.6 shows an applet that puts these pieces together to form an animated sequence of a silly person walking back and forth. Notice that you don't see any of the black area from the image strip, even against the white background. This is because of the transparency.

Figure 22.6 : By using transparent pixels, you can piece images together seamlessly.

Note
Of the two image formats currently understood by Java, only GIF images may be transparent. The JPEG format does not support transparent images.

There are several tools available on the Internet for creating transparent GIFs. On the Windows platform, one of the most popular tools is PaintShop Pro, available as a shareware program from http://www.jasc.com/pspdl.html. Remember that shareware programs are not free. If you use it, you should pay for it.

The GIFTOOL program, from http://www.homepages.com/tools/giftool, is available on a wide variety of platforms and is also a shareware program. GIFTOOL is a little tougher to use since it is a command-line tool, but its availability on many platforms is appealing.

The idea behind transparent GIFs is that you mark one of the colors in the GIF color table as being a transparent pixel. Obviously, you must be using an indexed color model to create a transparent image. This is why JPEG cannot support transparent pixels-JPEG always uses 24-bit color, which never needs a color index.

When you draw images based on pieces, you must pay special attention to the relative positions of the different parts. When you draw a multipart figure at a particular location, how do you decide exactly where to draw the pieces?

For the animated figure in Figure 22.6, you might say that the figure's location is determined by the upper left corner of the head. In other words, if you want to draw the figure at location 40,20, you draw the head so that the head part of the image is drawn at 40,20. You must then determine the position of the other parts relative to the head.

For example, in Figure 22.6, the body portion is drawn 48 pixels down and 6 to the right from the upper left corner of the head. These locations are determined by using a paint program or other tools. Once you determine the relative positions of the pieces, you can store them in the following class:


Listing 22.1  Source Code for ImageStripImage.java
public class ImageStripImage
{

// distFromXOrigin and distFromYOrigin give the position where
// this image should be drawn relative to the location, or origin,
// of the multi-part image. 

     public int distFromXOrigin;
     public int distFromYOrigin;

// stripX,StripY give the location of this image on the image strip
     public int stripX;
     public int stripY;

     public int width;     // the width of this image
     public int height;     // the height of this image

     public ImageStripImage(int distX, int distY, int stripX,
          int stripY, int width, int height)
     {
          this.distFromXOrigin = distX;
          this.distFromYOrigin = distY;

          this.stripX = stripX;
          this.stripY = stripY;

          this.width = width;
          this.height = height;
     }
}

Once you have a piece of an image defined by this structure, you can draw it using this variation of the drawStripImage method:

public void drawStripImage(Graphics g, Image imageStrip,
     int drawX, int drawY, ImageStripImage imageInfo)
{
     Graphics subArea = g.create(drawX + imageInfo.distFromXOrigin,
drawY + imageInfo.distFromYOrigin, imageInfo.width,
          imageInfo.height);
     subArea.drawImage(imageStrip, -imageInfo.stripX, -imageInfo.stripY, this);
     subArea.dispose();
}

This variation of the drawStripImage method adjusts the location of the image piece by that piece's relative position to the overall position of the image.

There is a full example of an image strip animation available on the CD that comes with this book. It is called ImageStripApplet.java.