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.
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:
setHints()
setDimensions()
setProperties()
setColorModel()
imageComplete()
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.
| Flag | Description |
|---|---|
RANDOMPIXELORDER | The pixels are delivered in random order |
TOPDOWNLEFTRIGHT | The pixels are delivered from top to bottom, left to right |
COMPLETESCANLINES | Each call to |
SINGLEPASS | Each pixel is delivered only once |
SINGLEFRAME | The 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.
| Flag | Description |
|---|---|
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.
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.