Learn how applets load network-based images asynchronously

Here's a close look at the way Java applets handle images in a network-based environment

Please indulge me while I let you in on a little secret (well, it's not really so little): The Java applet is a different sort of beast from the typical application that executes on your PC or workstation. For one thing, it doesn't sit quietly on your hard drive waiting for your attention; a Java applet sits on the other side of a network connection waiting for you (and many others) to pull it across the network. Once the applet has made the journey, it comes to life and begins to download the resources it needs. Unfortunately, all of this network activity results in one thing: delay. In contrast to an application on your hard drive, an applet must expect, and deal constructively with network delay.

For this reason, the Java class library was designed to support asynchronous loading of media, such as images. Asynchronous loading means that loading occurs out of step with the rest of the application -- in another thread -- in the background. In fact, what might be thought of as one operation, the loading of an image from across the network, actually occurs in two distinct stages, which often occur asynchronously. These stages are: image definition and the actual downloading of the image.

In this column, I will take a close look at the mechanics of image loading from the perspective of a Java applet or similar network-based Java application. I will also provide a step-by-step demonstration of how to load and draw images within an applet.

But first, let's take a look at the principle players.

Getting the picture with the Image class

An instance of the Image class represents image data. The image data does not need to reside locally; it may exist on another computer. In fact, it may not exist at all. An instance of the Image class is more like a reference to image data, than a container for image data.

Consider an image on a computer across a network. The creation of an instance of the Image class on a local computer does not cause the image data to be pulled across the network. This is true because instantiation of the Image class does not cause reconstruction of the image data. The Image class doesn't even provide methods for initiating image reconstruction. (This class does, however, provide access to an ImageProducer object that does provide such methods -- I will talk about image producers next month.)

Although the Image class doesn't store image data, it does provide methods for obtaining information about an image. The following methods return such information:

int getHeight(ImageObserver obs);

int getWidth(ImageObserver obs);

Object getProperty(String nm, ImageObserver obs);

The height and width values returned from the first two methods indicate the size of the image, in pixels. Properties, on the other hand, are image-format-specific pieces of information about an image, and are retrieved by name. The only property mentioned specifically in the Image class API is the comment property. It should contain a description of the image, or something similar. These methods return invalid values (the getHeight() and getWidth() methods both return -1 and the getProperty() method returns null) if the desired information is currently unavailable.

All three methods require an instance of a class that implements the ImageObserver interface as a parameter. An image observer is an object that expects to be notified when information about an image becomes available (I will talk about image observers next month). The Applet class implements this interface and we'll be using it as the image observer throughout this column.

Image objects are not created by instantiating the Image class directly. Instead, other classes in the Abstract Windowing Toolkit (AWT) provide methods for creating images. For example, the Applet class provides methods for creating instances of the Image class from a URL:

Image getImage(URL url);

Image getImage(URL url, String nm);

You can also create an instance of the Image class with methods provided by the Component class:

Image createImage(ImageProducer prod);

Image createImage(int w, int h);

Drawing images with the Graphics class

As you might expect, the Graphics class plays a pivotal role in the use of images. It would be impossible for me to include a thorough discussion of the Graphics class and its role in the space I have available. Fortunately, my October column was entirely devoted to the Graphics class and is available here.

Along with methods for drawing text and simple geometric shapes, the Graphics class provides methods for drawing images. In fact, it provides four related methods:

boolean drawImage(Image i, int x, int y, ImageObserver obs);

boolean drawImage(Image i, int x, int y, Color c, ImageObserver obs);

boolean drawImage(Image i, int x, int y, int w, int h, Color c, ImageObserver obs);

boolean drawImage(Image i, int x, int y, int w, int h, ImageObserver obs);

These methods have several parameters in common. Each method requires an instance of the Image class and the x and y coordinates at which to draw the image. Each method also requires an instance of a class that implements the ImageObserver interface.

Stepping through the motions

I think its time to put our feet to the fire and construct an applet that loads and draws an image. To help you better understand the techniques required to draw images, we will work through the process in several stages, examining each in detail.

Creating an instance of Image

Our first step is to create an instance of the Image class. Remember, instantiating the Image class does not cause the image to be reconstructed locally; that is, it does not cause the applet to download the image data.

public void init()
{
   // getDocumentBase() returns the URL of the document
   //   that contains this applet
   im = getImage(getDocumentBase(), "fig.gif");
      .
      .
      .
}

This code demonstrates how to use the Applet class's getImage() method to create a reference to image data located somewhere else on the network. getImage() requires either an instance of the URL class identifying the location of the image data, or an instance of the URL class identifying the document base and a string containing the filename of the image data. I used the latter of the two.

Downloading an image

After creating an instance of the Image class, our next step is to reconstruct the image data itself -- on this side of the network. Image reconstruction is initiated via a call to the prepareImage() method of the instance of the Component class upon which the image is to be drawn. The following code demonstrates how to use the Component class's prepareImage() method to begin reconstruction of the image data:

public void init()
{
   // getDocumentBase() returns the URL of the document
   //   that contains this applet
   im = getImage(getDocumentBase(), "fig.gif");
   // since this is an applet, "this" refers to an instance
   //   of the Applet class... which just happens to be an
   //   image observer
   prepareImage(im, this);
      .
      .
      .
}

The prepareImage() method requires an instance of the Image class representing the image, and an instance of a class that implements the ImageObserver interface. The latter requirement is satisfied by the Applet class, which happens to be an image observer.

As it turns out, the call to the prepareImage() method is not strictly necessary in this example because the call to the drawImage() method that we are about to make will start the reconstruction by itself. Let's see what this is all about.

Drawing an Image

As I mentioned before, the loading of an image occurs asynchronously -- in the background, so to speak. This fact, however, doesn't prevent us from attempting to draw the incompletely loaded image. Drawing is accomplished via a call to the Graphics class's drawImage() method. Notice that I use the Graphics class's drawImage() method from within the context of a component's paint() method:

public void paint(Graphics g)
{
   // because this is an applet, "this" refers to an instance
   //   of the Applet class... which just happens to be an
   //   image observer
   g.drawImage(im, 50, 50, this);
      .
      .
      .
}

The drawImage() method requires an instance of the Image class representing the image, the x and y coordinates at which to draw the image, and an instance of a class that implements the ImageObserver interface.

If the applet attempts to draw an image before the image is fully reconstructed, a partial image will be drawn. In fact, if you have an extremely slow Internet connection, the applet, which is shown in Figure 1, will allow you to observe this effect. The applet will redraw the image every time more of the image arrives. In order to see what I'm talking about, you might need to have your browser reload the current document.

You need a Java-enabled browser to see this applet.
Figure 1: The incremental loading effect

The code demonstrating the previous three techniques is available here.

Preventing incomplete drawing

Generally speaking, a truly elegant applet should not draw an image until all the image data is available, so we need a way to ensure that this is the case. To get started, we'll use a technique that works best for applets displaying a single image. This technique, which is shown in the following code snippet, uses the Component class's checkImage() method to determine whether or not an image has finished loading. We'll discuss a more comprehensive mechanism to prevent incomplete drawing in the next section.

public void paint(Graphics g)
{
   // because this is an applet, "this" refers to an instance
   //   of the Applet class... which just happens to be an
   //   image observer
   int n = checkImage(im, this) & ImageObserver.ALLBITS;
   // the return value consists of a set of bits describing the
   //   state of the image reconstruction
   if (n == ImageObserver.ALLBITS) g.drawImage(im, 50, 50, this);
      .
      .
      .
}

Here we see how to use the Component class's checkImage() method in conjunction with the Graphics class's drawImage() method. checkImage() requires an instance of the Image class representing the image and an instance of a class that implements the ImageObserver interface. It returns an integer describing the state of the reconstruction.

In this example, the applet must determine whether or not all of the image data is available before attempting to draw the image. It does so by testing to see whether or not the ALLBITS bit is set in the returned data (other bits include the ABORT, ERROR, FRAMEBITS, HEIGHT, WIDTH, SOMEBITS, and PROPERTIES bits). If ALLBITS is set, the applet knows that all of the image data has been loaded.

The applet in Figure 2 shows this code in action. Once again, it might be necessary to have your browser reload the current document. The code for this applet is available here.

You need a Java-enabled browser to see this applet.
Figure: 2 Waiting for the image data to load

Monitoring image loading with the MediaTracker class

In an applet with more that a few images to download, ad hoc techniques like the one we just discussed come up short. The MediaTracker class provides an easier, more comprehensive solution for monitoring image loading.

The MediaTracker class provides three clear advantages over the previous approach:

  • Images may be organized into groups
  • Loading may occur synchronously or asynchronously
  • Groups may be loaded independent of any other groups

Let's take a look at the code:

public void init() { // in this case, "this" refers to a subclass of the // Component class, which the Applet class just happens // to be MediaTracker mt = new MediaTracker(this);

im = getImage(getDocumentBase(), "fig.gif");

// add the image to group #0 mt.addImage(im, 0);

try { // wait for all of the images to load mt.waitForAll(); } catch (InterruptedException ie) { ; } }

Let's take a closer look at what's going on here. First, an instance of the MediaTracker class is created. The constructor requires that the component in which the image will be drawn be passed as a parameter.

Each image to be tracked is added via the addImage() method. The second parameter indicates the group to which the image must be added. The waitForAll() method instructs the MediaTracker object to begin reconstructing all images, and to return when the process is finished.

A handy feature of the MediaTracker class is its ability to monitor individual image groups; it allows the applet to load them once the entire group's data is available. This technique is helpful when you have a lot of images to load but not all of the images have to be displayed up front.

The applet in Figure 3 shows this code in action. Again, it might be necessary to have your browser reload the current document. The code for this applet is available here.

You need a Java-enabled browser to see this applet.
Figure 3: MediaTracker

Conclusion

By now, you might suspect that there is more to this image business than meets the eye -- and you'd be correct. Next month I will stroll behind the scenes to take a look at the classes that make all of this work. In the meantime, try to incorporate some of the material from prior columns (events, for instance) and see if you can't make something really interesting. As always, keep the feedback coming.

Todd Sundsted has been writing programs since computers became available in desktop models. Though originally interested in building distributed-object applications in C++, Todd moved to the Java programming language when Java became the obvious choice for that sort of thing. Todd is co-author of the Java Language API SuperBible, now in bookstores everywhere. In addition to writing, Todd provides Internet and Web consulting services to companies in the southeastern United States.

Learn more about this topic