How Java uses the producer/consumer model to handle images -- An insider's look

Learn more about Java's powerful image-handling technique, then follow these simple procedures for building your own producer and consumer components

Not content with the complete set of graphics primitives provided by the Graphics, many programmers still find that they need more flexibility; simply loading and displaying images within an applet or application often doesn't make the grade. In these cases, the only solution is to create images on the fly. Such a task is made possible by Java's powerful image-handling framework and the two interfaces that make it up.

Before we get too deep into our latest image endeavor, let's review what we learned about images in the preceeding How-To Java column.

A crash course in image handling

Recall that delay is an inherent property of the network-based environment in which Java applets and many Java applications live. Because Java applets and many Java applications depend heavily on resources (classes and media) that are initially available only remotely, the designers of the Java class library had to address the delay posed by network transactions. The result of their efforts was a system in which images are fetched asynchronously, possibly in another thread.

The most visible player in the image game is the Image class. An instance of the Image class represents an image, but it does not need to contain the data that makes up the image -- the Image class provides methods for obtaining information about the image represented by an instance of the class. Due to network delay, information about an image might not be immediately available. Therefore, each method requires, as a parameter, an instance of a class that implements the ImageObserver interface.

Whew! That's a whole lot of information crammed into a very small space. If you find that you're scratching your head wondering what this is all about, check out last month's column, for more information.

Uncovering the power of the the producer/consumer model

The developers of the Java class library used the producer/consumer model as the basis for the library's low-level image-handling API. This model, which has several advantages we'll examine in a minute, consists of two tightly coupled components: producers and consumers. You may have been able to guess the components' names, but understanding what they do requires some explanation. Let's take a look.

Image producers are objects that produce image data. An image producer may generate image data itself, or it may provide access to image data in a particular image format (GIF, JPEG). All image producers implement the ImageProducer interface. Image consumers, on the other hand, are objects that consume image data. Once the image data is consumed, the object is then free to use (or modify) it. All image consumers implement the ImageConsumer interface.

As I mentioned a moment ago, the producer/consumer model has several advantages: it's modular, which means that existing producers and consumers can be used interchangeably, and new producers and consumers fit seamlessly into the existing framework; and it lends itself to asynchronous interaction, which means that once a connection between a producer and a consumer is made, the producer notifies the consumer only when more information is available. Meanwhile, the applet or application is free to do other work.

In addition to these benefits, the producer/consumer model is also an intuitive description for two collaborating objects in a network-based environment. Producers represent resources located on the other side of the network; consumers represent the applet or application located on this side of the network. Together they provide the machinery that gets the resource data from there to here.

Observing the role of the producer and consumer components

Now that you've got the fundamentals of the producer/consumer model securely under your belt, take a look at Listing 1, which demonstrates the way images are typically created and used in an applet.

public class New extends Applet { private Image _im = null;

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

// because this is an applet, "this" refers to an // instance of the Applet class... which just // happens to be an image observer prepareImage(_im, this); // step 2 }

public void paint(Graphics g) { // see the comment about "this" above... g.drawImage(_im, 50, 50, this); // step 3 } }

Listing 1: Image handling in a typical applet

Let's examine the three steps that take place in this listing:

  1. An instance of the Image class is created
  2. An instance of the Image class is prepared
  3. An image is drawn

You may recall from last month's column that the two last steps cause image reconstruction to begin. The prepareImage and drawImage methods used in these two steps require an instance of an image observer, which is not really part of the producer/consumer model. Instead, image observers are a convenient appendage whose functionality is underwritten by the Component class. If a request can't be immediately satisfied because image reconstruction is not complete, the component notifies the image observer when reconstruction is complete. The image observer can then act upon this information. In order to learn what role image producers and image consumers play in the demonstration above, we must take a look behind the scenes.

First, the creation of an instance of the Image class requires the existence of an image producer. In our demonstration, code deep within the AWT creates the image producer; alternately, the programmer can supply the image producer when the Image object is created.

Second, attempting to prepare or to draw an instance of the Image class (in fact, attempting to invoke any of methods that require an image observer) causes an image consumer to be created deep within the AWT and reconstruction to begin.

Finally, as reconstruction occurs the image consumer stores the image data and ensures that the supplied image observer is notified.

If the applet itself is used as the image observer, as was the case in last month's demonstrations, we get a better magic show. The Component class (from which the Applet class is derived) supplies an updateImage() method that calls the Component object's repaint() method. This schedules another painting operation, during which the image (now containing additional image data) will be redrawn.

Creating your own custom image producer

Last month I described how an Image object could be created from image data fetched across the network. I also mentioned two other ways to create an Image object: from an Image object representing an off-screen image and directly from an ImageProducer object. The first technique has been discussed at length elsewhere (due primarily to its role in animation -- see the JavaWorld article "Animation in Java Applets" by Arthur van Hoff and Kathy Walrath for a detailed look at this technique), so we'll deal with the second. Take a look at the following method, which is provided by the Component class:

public Image createImage(ImageProducer ip)

The createImage method takes an instance of a class that implements the ImageProducer interface and creates an instance of the Image class. This Image object can then be manipulated in the same manner as any other Image object.

A class that implements the ImageProducer interface must provide definitions for five methods. The first three methods

public void addConsumer(ImageConsumer ic)

public void removeConsumer(ImageConsumer ic)

public boolean isConsumer(ImageConsumer ic)

are responsible for adding and removing image consumers from the list of those items that are interested in the state of the image producer. They require an instance of an ImageConsumer as a parameter.

The method

public void requestTopDownLeftRightResend(ImageConsumer ic)

allows an image consumer to request that the image producer resend the image data in top-down, left-to-right order. If the image producer cannot resend the image data in this order, it may ignore this request.

Finally, the method

public void startProduction(ImageConsumer ic)

allows an image consumer to request that the image producer start image production.

Listing 2 shows the implementations of the first three methods. Note that the _v variable is a reference to an instance of the Vector class. Listing 3 shows one possible definition of the startProduction() method. In this example, the image producer generates the image data within the body of its startProduction() method; however, we could have easily started another thread and let it generate the image data.

Note: The definitions in these listings are part of an image producer that generates a small 128-by-128 pixel image.

public void addConsumer(ImageConsumer ic) { if (isConsumer(ic) == false) _v.addElement(ic); }

public void removeConsumer(ImageConsumer ic) { if (isConsumer(ic) == true) _v.removeElement(ic); }

public boolean isConsumer(ImageConsumer ic) { return _v.indexOf(ic) > -1; }

Listing 2: Image consumer maintenance

public void startProduction(ImageConsumer ic) { addConsumer(ic);

// create a 128x128 element buffer

int x = 128; int y = 128;

int [] rgn = new int [x * y];

for (int i = 0; i < x * y; i++) rgn[i] = i * 2;

// create the properties hashtable

Hashtable ht = new Hashtable();

// create the default color model

ColorModel cm = new DirectColorModel(24, 0x0000FF, 0x00FF00, 0xFF0000);

// clone the vector containing consumers

Vector v = (Vector)_v.clone();

// loop over all consumers

Enumeration e = v.elements();

while (e.hasMoreElements()) { ic = (ImageConsumer)e.nextElement();

// send information

ic.setColorModel(cm); ic.setDimensions(x, y); ic.setProperties(ht); ic.setHints(ImageConsumer.RANDOMPIXELORDER); ic.setPixels(0, 0, x, y, cm, rgn, 0, x); ic.imageComplete(ImageConsumer.STATICIMAGEDONE); } }

Listing 3: Generating image data within the body of the startProduction() method

The complete source code is available here.

Creating your own custom image consumer

The other half of the producer/consumer model is the image consumer. Unfortunately, image consumers are nowhere near as glamorous as their counterparts; they simply provide front ends for image filters, image viewers, and a myriad of other tools. Once an image consumer has added itself to an image producer, as we discussed earlier, the image producer invokes methods implemented by the image consumer to route information about the image back to the image consumer.

As with the image producer, any class that implements the ImageConsumer interface must provide definitions for certain methods (in this case, seven methods). We'll look at each method individually, so that I can provide some insight as we go along:

public void setHints(int nHints)

Hints are bits of information that describe how an image producer intends to deliver the image data. A "smart" image consumer can use this information to enhance its handling of the image data. An image producer invokes the setHints() method with a bit mask composed of the following values: COMPLETESCANLINES, TOPDOWNLEFTRIGHT, RANDOMPIXELORDER, SINGLEFRAME, and SINGLEPASS. RANDOMPIXELORDER is the most general case.

public void setColorModel(ColorModel cm)

An image producer invokes the setColorModel() method to set the default color model for subsequent set-pixel operations. A color model provides methods for translating from pixel values to color components.

public void setDimensions(int w, int h)

An image producer invokes the setDimensions() method to set the width, height, and properties of the image. These values are then available via Image class methods.

public void setProperties(Hashtable htProps)

As with the setDimensions() method, the image producer invokes the setProperties() method to set the width, height, and properties of the image.

public void setPixels(int x, int y, int w, int h, ColorModel cm, byte rgb[], int nOffset, int nScansize)

public void setPixels(int x, int y, int w, int h, ColorModel cm, int rgn[], int nOffset, int nScansize)

An image producer invokes the setPixels() methods to deliver pixels to an image consumer. The array parameter contains the pixel values for a rectangular region within the image. The image producer can reinvoke the method as often as necessary.

public void imageComplete(int nStatus)

An image producer invokes the imageComplete() method to indicate that either the image reconstruction is complete or that something went wrong. The status parameter can be one of the following values: STATICIMAGEDONE, SINGLEFRAMEDONE, IMAGEERROR, or IMAGEABORTED.

Listing 4, which is part of a class that monitors the progress of an image producer, shows the implementation of the imageComplete() method along with three of its friends: addImage(), waitFor(), and run() The addImage() method adds the image consumer to the image's list of consumers. The waitFor() and run() methods begin image reconstruction within a new thread while the original thread sleeps. The imageComplete() method is called when reconstruction is complete. It wakes up the original thread, which is now free to use the image. The complete source code is available here.

1 2 Page 1
Page 1 of 2