Полезная информация

Exploring Java

Previous: 17.4 Producing Image DataChapter 17
Working with Images
Next: 17.6 Filtering Image Data
 

17.5 Image Producers and Consumers

In this section we'll create an image producer that generates a stream of image frames rather than just a static image. Unfortunately, it would take too many lines of code to generate anything really interesting, so we'll stick with a simple modification of our ColorPan example. After all, figuring out what to display is your job; I'm primarily concerned with giving you the necessary tools. After this, you should have the needed tools to implement more interesting applications.

A word of advice: if you find yourself writing image producers, you're probably making your life excessively difficult. Most situations can be handled by the dynamic MemoryImageSource technique that we just demonstrated. Before going to the trouble of writing an image producer, convince yourself that there isn't a simpler solution. Even if you never write an image producer yourself, it's good (like Motherhood and Apple Pie) to understand how Java's image-rendering tools work.

17.5.1 Image Consumers

First, we have to know a little more about the image consumers we'll be feeding. An image consumer implements the seven methods that are defined in the ImageConsumer interface. Two of these methods are overloaded versions of the setPixels() method that accept the actual pixel data for a region of the image. They are identical except that one takes the pixel data as an array of integers, and the other uses an array of bytes. (An array of bytes is natural when you're using an indexed color model because each pixel is specified by an index into a color array.) A call to setPixels() looks something like:

setPixels(x, y, width, height, colorModel, pixels, offset, scanLength); 

pixels is the one-dimensional array of bytes or integers that holds the pixel data. Often, you deliver only part of the image with each call to setPixels(). The x, y, width, and height values define the rectangle of the image for which pixels are being delivered. x and y specify the upper left-hand corner of the chunk you're delivering, relative to the upper left-hand corner of the image as a whole. width specifies the width in pixels of the chunk; height specifies the number of scan lines in the chunk. offset specifies the point in pixels at which the data being delivered in this call to setPixels() starts. Finally, scanLength indicates the width of the entire image, which is not necessarily the same as width. The pixels array must be large enough to accommodate width*length+offset elements; if it's larger, any leftover data is ignored.

We haven't said anything yet about the colorModel argument to setPixels(). In our previous example, we drew our image using the default ARGB color model for pixel values; the version of the MemoryImageSource constructor that we used supplied the default color model for us. In this example, we also stick with the default model, but this time we have to specify it explicitly. The remaining five methods of the ImageConsumer interface accept general attributes and framing information about the image:

Before delivering any data to a consumer, the producer should call the consumer's setHints() method to pass it information about how pixels will be delivered. Hints are specified in the form of flags defined in the ImageConsumer interface. The flags are described in Table 17.2. The consumer uses these hints to optimize the way it builds the image; it's also free to ignore them.

Table 17.2: ImageConsumer setHints() Flags
FlagDescription
RANDOMPIXELORDERThe pixels are delivered in random order
TOPDOWNLEFTRIGHTThe pixels are delivered from top to bottom, left to right
COMPLETESCANLINES

Each call to setPixels() delivers one or more complete scan lines

SINGLEPASSEach pixel is delivered only once
SINGLEFRAMEThe pixels define a single, static image

setDimensions() is called to pass the width and height of the image when they are known.

setProperties() is used to pass a hashtable of image properties, stored by name. This method isn't particularly useful without some prior agreement between the producer and consumer about what properties are meaningful. For example, image formats such as GIF and TIFF can include additional information about the image. These image attributes could be delivered to the consumer in the hashtable.

setColorModel() is called to tell the consumer which color model will be used to process most of the pixel data. However, remember that each call to setPixels() also specifies a ColorModel for its group of pixels. The color model specified in setColorModel() is really only a hint that the consumer can use for optimization. You're not required to use this color model to deliver all (or for that matter, any) of the pixels in the image.

The producer calls the consumer's imageComplete() method when it has completely delivered the image or a frame of an image sequence. If the consumer doesn't wish to receive further frames of the image, it should unregister itself from the producer at this point. The producer passes a status flag formed from the flags shown in Table 17.3.

Table 17.3: ImageConsumer imageComplete() Flags
FlagDescription
STATICIMAGEDONE

A single static image is complete

SINGLEFRAMEDONE

One frame of an image sequence is complete

IMAGEERROR

An error occurred while generating the image

As you can see, the ImageProducer and ImageConsumer interfaces provide a very flexible mechanism for distributing image data. Now let's look at a simple producer.

17.5.2 A Sequence of Images

The following class, ImageSequence, shows how to implement an ImageProducer that generates a sequence of images. The images are a lot like the ColorPan image we generated a few pages back, except that the blue component of each pixel changes with every frame. This image producer doesn't do anything you couldn't do with a MemoryImageSource. It reads ARGB data from an array and consults the object that creates the array to give it an opportunity to update the data between each frame.

This is a complex example, so before diving into the code, let's take a broad look at the pieces. The ImageSequence class is an image producer; it generates data and sends it to image consumers to be displayed. To make our design more modular, we define an interface called FrameARGBData that describes how our rendering code provides each frame of ARGB pixel data to our producer. To do the computation and provide the raw bits, we create a class called ColorPanCycle that implements FrameARGBData. This means that ImageSequence doesn't care specifically where the data comes from; if we wanted to draw different images, we could just drop in another class, provided that the new class implements FrameARGBData. Finally, we create an applet called UpdatingImage that includes two image consumers to display the data.

Here's the ImageSequence class:

import java.awt.image.*;
import java.util.*;
public class ImageSequence extends Thread implements ImageProducer {
    int width, height, delay;
    ColorModel model = ColorModel.getRGBdefault();
    FrameARGBData frameData;
    private Vector consumers = new Vector();
    public void run() {
        while ( frameData != null ) {
            frameData.nextFrame();
            sendFrame();
            try {
                sleep( delay );
            } catch ( InterruptedException e ) {}
        }
    }
    public ImageSequence(FrameARGBData src, int maxFPS ) {
        frameData = src;
        width = frameData.size().width;
        height = frameData.size().height;
        delay = 1000/maxFPS;
        setPriority( MIN_PRIORITY + 1 );
    }
    public synchronized void addConsumer(ImageConsumer c) {
        if ( isConsumer( c ) ) 
            return;
        consumers.addElement( c );
        c.setHints(ImageConsumer.TOPDOWNLEFTRIGHT |
                 ImageConsumer.SINGLEPASS );
        c.setDimensions( width, height );
        c.setProperties( new Hashtable() );
        c.setColorModel( model );
    }
    public synchronized boolean isConsumer(ImageConsumer c) {
        return ( consumers.contains( c ) );
    }
    public synchronized void removeConsumer(ImageConsumer c) {
        consumers.removeElement( c );
    }
    public void startProduction(ImageConsumer ic) {
        addConsumer(ic);
    }
    public void requestTopDownLeftRightResend(ImageConsumer ic) { }
    private void sendFrame() {
        for ( Enumeration e = consumers.elements(); e.hasMoreElements();  ) {
            ImageConsumer c = (ImageConsumer)e.nextElement();
            c.setPixels(0, 0, width, height, model, frameData.getPixels(), 
                        0, width);
            c.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
        }
    }
}

The bulk of the code in ImageSequence creates the skeleton we need for implementing the ImageProducer interface. ImageSequence is actually a simple subclass of Thread whose run() method loops, generating and sending a frame of data on each iteration. The ImageSequence constructor takes two items: a FrameARGBData object that updates the array of pixel data for each frame, and an integer that specifies the maximum number of frames per second to generate. We give the thread a low priority (MIN_PRIORITY+1) so that it can't run away with all of our CPU time.

Our FrameARGBData object implements the following interface:

interface FrameARGBData { 
    java.awt.Dimension size(); 
    int [] getPixels(); 
    void nextFrame(); 
} 

In ImageSequence's run() method, we call nextFrame() to compute the array of pixels for each frame. After computing the pixels, we call our own sendFrame() method to deliver the data to the consumers. sendFrame() calls getPixels() to retrieve the updated array of pixel data from the FrameARGBData object. sendFrame() then sends the new data to all of the consumers by invoking each of their setPixels() methods and signaling the end of the frame with imageComplete(). Note that sendFrame() can handle multiple consumers; it iterates through a Vector of image consumers. In a more realistic implementation, we would also check for errors and notify the consumers if any occurred.

The business of managing the Vector of consumers is handled by addConsumer() and the other methods in the ImageProducer interface. addConsumer() adds an item to consumers. A Vector is a perfect tool for this task, since it's an automatically extendable array, with methods for finding out how many elements it has, whether or not a given element is already a member, and so on.

addConsumer() also gives the consumer hints about how the data will be delivered by calling setHints(). This image provider always works from top to bottom and left to right, and makes only one pass through the data. addConsumer() next gives the consumer an empty hashtable of image properties. Finally, it reports that most of the pixels will use the default ARGB color model (we initialized the variable model to ColorModel.getRGBDefault()). In this example, we always start sending image data on the next frame, so startProduction() simply calls addConsumer().

We've discussed the mechanism for communications between the consumer and producer, but I haven't yet told you where the data comes from. We have a FrameARGBData interface that defines how to retrieve the data, but we don't yet have an object that implements the interface. The following class, ColorPanCycle, implements FrameARGBData; we'll use it to generate our pixels:

import java.awt.*;
class ColorPanCycle implements FrameARGBData {
    int frame = 0, width, height;
    private int [] pixels;
    ColorPanCycle ( int w, int h ) {
        width = w;
        height = h;
        pixels = new int [ width * height ];
        nextFrame();
    }
    public synchronized int [] getPixels() {
        return pixels;
    }
    public synchronized void nextFrame() {
        int index = 0;
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int red = (y * 255) / (height - 1);
                int green = (x * 255) / (width - 1);
                int blue = (frame * 10) & 0xff;
                pixels[index++] = 
                    (255 << 24) | (red << 16) | (green << 8) | blue;
            }
        }
        frame++;
    }
    public Dimension size() {
        return new Dimension ( width, height );
    }
}

ColorPanCycle is like our previous ColorPan example, except that it adjusts each pixel's blue component each time nextFrame() is called. This should produce a color cycling effect; as time goes on, the image becomes more blue.

Now let's put the pieces together by writing an applet that displays a sequence of changing images: UpdatingImage. In fact, we'll do better than displaying one sequence. To prove that ImageSequence really can deal with multiple consumers, UpdatingImage creates two components that display different views of the image. Once the mechanism has been set up, it's surprising how little code you need to add additional displays.

import java.awt.*;
import java.awt.image.*;
public class UpdatingImage extends java.applet.Applet { 
    ImageSequence seq;
    public void init() {
        seq = new ImageSequence( new ColorPanCycle(100, 100), 10);
        setLayout( null );
        add( new ImageCanvas( seq, 50, 50 ) );
        add( new ImageCanvas( seq, 100, 100 ) );
        seq.start();
    }
    public void stop() {
        if ( seq != null ) {
            seq.stop();
            seq = null;
        }
    }
}
class ImageCanvas extends Canvas { 
    Image img;
    ImageProducer source;
    ImageCanvas ( ImageProducer p, int w, int h ) {
        source = p;
        setSize( w, h );
    }
    public void update( Graphics g ) {
        paint(g);
    }
    public void paint( Graphics g ) {
        if ( img == null )
            img = createImage( source );
        g.drawImage( img, 0, 0, getSize().width, getSize().height, this );
    }
}

UpdatingImage constructs a new ImageSequence producer with an instance of our ColorPanCycle object as its frame source. It then creates two ImageCanvas components that create and display the two views of our animation. ImageCanvas is a subclass of Canvas; it takes an ImageProducer and a width and height in its constructor and creates and displays an appropriately scaled version of the image in its paint() method. UpdatingImage places the smaller view on top of the larger one for a sort of "picture in picture" effect.

If you've followed the example to this point, you're probably wondering where in the heck is the image consumer. After all, we spent a lot of time writing methods in ImageSequence for the consumer to call. If you look back at the code, you'll see that an ImageSequence object gets passed to the ImageCanvas constructor, and that this object is used as an argument to createImage(). But nobody appears to call addConsumer(). And the image producer calls setPixels() and other consumer methods; but it always digs a consumer out of its Vector of registered consumers, so we never see where these consumers come from.

In UpdatingImage, the image consumer is behind the scenes, hidden deep inside the Canvas--in fact, inside the Canvas' peer. The call to createImage() tells its component (i.e., our canvas) to become an image consumer. Something deep inside the component is calling addConsumer() behind our backs and registering a mysterious consumer, and that consumer is the one the producer uses in calls to setPixels() and other methods. We haven't implemented any ImageConsumer objects in this book because, as you might imagine, most image consumers are implemented in native code, since they need to display things on the screen. There are others though; the java.awt.image.PixelGrabber class is a consumer that returns the pixel data as a byte array. You might use it to save an image. You can make your own consumer do anything you like with pixel data from a producer. But in reality, you rarely need to write an image consumer yourself. Let them stay hidden; take it on faith that they exist.

Now for the next question: How does the screen get updated? Even though we are updating the consumer with new data, the new image will not appear on the display unless the applet repaints it periodically. By now, this part of the machinery should be familiar: what we need is an image observer. Remember that all components are image observers (i.e., the class Component implements ImageObserver). The call to drawImage() specifies our ImageCanvas as its image observer. The default Component class-image-observer functionality then repaints our image whenever new pixel data arrives.

In this example, we haven't bothered to stop and start our applet properly; it continues running and wasting CPU time even when it's invisible. There are two strategies for stopping and restarting our thread. We can destroy the thread and create a new one, which would require recreating our ImageCanvas objects, or we could suspend and resume the active thread. Neither option is particularly difficult.


Previous: 17.4 Producing Image DataExploring JavaNext: 17.6 Filtering Image Data
17.4 Producing Image DataBook Index17.6 Filtering Image Data

Other Books in this LibraryJava in a NutshellJava Language ReferenceJava AWTJava Fundamental ClassesExploring Java