ImageProducer
The ImageProducer
interface defines the methods that ImageProducer
objects must implement. Image producers serve as sources for pixel data; they may compute the data themselves or interpret data from some external source, like a GIF file. No matter how it generates the data, an image producer's job is to hand that data to an image consumer, which usually renders the data on the screen. The methods in the ImageProducer
interface let ImageConsumer
objects register their interest in an image. The business end of an ImageProducer
--that is, the methods it uses to deliver pixel data to an image consumer--are defined by the ImageConsumer
interface. Therefore, we can summarize the way an image producer works as follows:
- It waits for image consumers to register their interest in an image.
- As image consumers register, it stores them in a
Hashtable
,Vector
, or some other collection mechanism. - As image data becomes available, it loops through all the registered consumers and calls their methods to transfer the data.
There's a sense in which you have to take this process on faith; image consumers are usually well hidden. If you call createImage()
, an image consumer will eventually show up.
Every Image
has an ImageProducer
associated with it; to acquire a reference to the producer, use the getSource()
method of Image
.
Because an ImageProducer
must call methods in the ImageConsumer
interface, we won't show an example of a full-fledged producer until we have discussed ImageConsumer
.
ImageProducer Interface
Methods- public void addConsumer (ImageConsumer ic)
- The
addConsumer()
method registersic
as anImageConsumer
interested in theImage
information. Once anImageConsumer
is registered, theImageProducer
can deliverImage
pixels immediately or wait untilstartProduction()
has been called.Note that one image may have many consumers; therefore,
addConsumer()
usually stores image consumers in a collection like aVector
orHashtable
. There is one notable exception: if the producer has the image data in memory,addConsumer()
can deliver the image to the consumer immediately. WhenaddConsumer()
returns, it has finished with the consumer. In this case, you don't need to manage a list of consumers, because there is only one image consumer at a time. (In this case,addConsumer()
should be implemented as a synchronized method.) - public boolean isConsumer (ImageConsumer ic)
- The
isConsumer()
method checks to see ific
is a registeredImageConsumer
for thisImageProducer
. Ific
is registered,true
is returned. Ific
is not registered,false
is returned. - public void removeConsumer (ImageConsumer ic)
- The
removeConsumer()
method removesic
as a registeredImageConsumer
for thisImageProducer
. Ific
was not a registeredImageConsumer
, nothing should happen. This is not an error that should throw an exception. Onceic
has been removed from the registry, theImageProducer
should no longer send data to it. - public void startProduction (ImageConsumer ic)
- The
startProduction()
method registersic
as anImageConsumer
interested in theImage
information and tells theImageProducer
to start sending theImage
data immediately. TheImageProducer
sends the image data toic
and all other registeredImageConsumer
objects, throughaddConsumer()
. - public void requestTopDownLeftRightResend (ImageConsumer ic)
- The
requestTopDownLeftRightResend()
method is called by theImageConsumer
ic
requesting that theImageProducer
retransmit theImage
data in top-down, left-to-right order. If theImageProducer
is unable to send the data in that order or always sends the data in that order (like withMemoryImageSource
), it can ignore the call.
FilteredImageSource
The FilteredImageSource
class combines an ImageProducer
and an ImageFilter
to create a new Image
. The image producer generates pixel data for an original image. The FilteredImageSource
takes this data and uses an ImageFilter
to produce a modified version: the image may be scaled, clipped, or rotated, or the colors shifted, etc. The FilteredImageSource
is the image producer for the new image. The ImageFilter
object transforms the original image's data to yield the new image; it implements the ImageConsumer
interface. We cover the ImageConsumer
interface in ImageConsumer and the ImageFilter
class in ImageFilter. Figure 12.1 shows the relationship between an ImageProducer
, FilteredImageSource
, ImageFilter
, and the ImageConsumer
.
Figure 12.1: Image producers, filters, and consumers
Constructors
- public FilteredImageSource (ImageProducer original, ImageFilter filter)
- The
FilteredImageSource
constructor creates an image producer that combines an image,original
, and a filter,filter
, to create a new image. TheImageProducer
of the original image is the constructor's first parameter; given anImage
, you can acquire itsImageProducer
by using thegetSource()
method. The following code shows how to create a new image from an original. ImageFilter shows several extensive examples of image filters.
Image image = getImage (new URL ("http://www.ora.com/graphics/headers/homepage.gif")); Image newOne = createImage (new FilteredImageSource (image.getSource(), new SomeImageFilter()));ImageProducer interface methods
The ImageProducer
interface methods maintain an internal table for the image consumers. Since this is private, you do not have direct access to it.
- public synchronized void addConsumer (ImageConsumer ic)
- The
addConsumer()
method addsic
as anImageConsumer
interested in the pixels for this image. - public synchronized boolean isConsumer (ImageConsumer ic)
- The
isConsumer()
method checks to see ific
is a registeredImageConsumer
for thisImageProducer
. Ific
is registered,true
is returned. If not registered,false
is returned. - public synchronized void removeConsumer (ImageConsumer ic)
- The
removeConsumer()
method removesic
as a registeredImageConsumer
for thisImageProducer
. - public void startProduction (ImageConsumer ic)
- The
startProduction()
method registersic
as anImageConsumer
interested in theImage
information and tells theImageProducer
to start sending theImage
data immediately. - public void requestTopDownLeftRightResend (ImageConsumer ic)
- The
requestTopDownLeftRightResend()
method registersic
as anImageConsumer
interested in theImage
information and requests theImageProducer
to retransmit theImage
data in top-down, left-to-right order.
MemoryImageSource
The MemoryImageSource
class allows you to create images completely in memory; you generate pixel data, place it in an array, and hand that array and a ColorModel
to the MemoryImageSource
constructor. The MemoryImageSource
is an image producer that can be used with a consumer to display the image on the screen. For example, you might use a MemoryImageSource
to display a Mandelbrot image or some other image generated by your program. You could also use a MemoryImageSource
to modify a pre-existing image; use PixelGrabber
to get the image's pixel data, modify that data, and then use a MemoryImageSource
as the producer for the modified image. Finally, you can use MemoryImageSource
to simplify implementation of a new image type; you can develop a class that reads an image in some unsupported format from a local file or the network; interprets the image file and puts pixel data into an array; and uses a MemoryImageSource
to serve as an image producer. This is simpler than implementing an image producer yourself, but it isn't quite as flexible; you lose the ability to display partial images as the data becomes available.
In Java 1.1, MemoryImageSource
supports multiframe images to animate a sequence. In earlier versions, it was necessary to create a dynamic ImageFilter
to animate the image. Constructors
There are six constructors for MemoryImageSource
, each with slightly different parameters. They all create an image producer that delivers some array of data to an image consumer. The constructors are:
- public MemoryImageSource (int w, int h, ColorModel cm, byte pix[], int off, int scan)
public MemoryImageSource (int w, int h, ColorModel cm, byte pix[], int off, int scan, Hashtable props)
public MemoryImageSource (int w, int h, ColorModel cm, int pix[], int off, int scan)
public MemoryImageSource (int w, int h, ColorModel cm, int pix[], int off, int scan, Hashtable props)
public MemoryImageSource (int w, int h, int pix[], int off, int scan)
public MemoryImageSource (int w, int h, int pix[], int off, int scan, Hashtable props)
The parameters that might be present are:
w
- Width of the image being created, in pixels.
h
- Height of the image being created, in pixels.
cm
- The
ColorModel
that describes the color representation used in the pixel data. If this parameter is not present, theMemoryImageSource
uses the default RGB color model (ColorModel.getRGBDefault()
). pix[]
- The array of pixel information to be converted into an image. This may be either a
byte
array or anint
array, depending on the color model. If you're using a direct color model (including the default RGB color model),pix
is usually anint
array; if it isn't, it won't be able to represent all 16 million possible colors. If you're using an indexed color model, the array should be abyte
array. However, if you use anint
array with an indexed color model, theMemoryImageSource
ignores the three high-order bytes because an indexed color model has at most 256 entries in the color map. In general: if your color model requires more than 8 bits of data per pixel, use anint
array; if it requires 8 bits or less, use abyte
array. off
- The first pixel used in the array (usually 0); prior pixels are ignored.
scan
- The number of pixels per line in the array (usually equal to
w
). The number of pixels per scan line in the array may be larger than the number of pixels in the scan line. Extra pixels in the array are ignored. props
- A
Hashtable
of the properties associated with the image. If this argument isn't present, the constructor assumes there are no properties.
The pixel at location (x
, y
) in the image is located at pix[y * scan + x + off]
. ImageProducer interface methods
In Java 1.0, the ImageProducer
interface methods maintain a single internal variable for the image consumer because the image is delivered immediately and synchronously. There is no need to worry about multiple consumers; as soon as one registers, you give it the image, and you're done. These methods keep track of this single ImageConsumer
.
In Java 1.1, MemoryImageSource
supports animation. One consequence of this new feature is that it isn't always possible to deliver all the image's data immediately. Therefore, the class maintains a list of image consumers that are notified when each frame is generated. Since this list is private, you do not have direct access to it.
- public synchronized void addConsumer (ImageConsumer ic)
- The
addConsumer()
method addsic
as anImageConsumer
interested in the pixels for this image. - public synchronized boolean isConsumer (ImageConsumer ic)
- The
isConsumer()
method checks to see ific
is a registeredImageConsumer
for thisImageProducer
. Ific
is registered,true
is returned. Ific
is not registered,false
is returned. - public synchronized void removeConsumer (ImageConsumer ic)
- The
removeConsumer()
method removesic
as a registeredImageConsumer
for thisImageProducer
. - public void startProduction (ImageConsumer ic)
- The
startProduction()
method callsaddConsumer()
. - public void requestTopDownLeftRightResend (ImageConsumer ic)
- The
requestTopDownLeftRightResend()
method does nothing since in-memory images are already in this format or are multiframed, with each frame in this format.
In Java 1.1, MemoryImageSource
supports animation; it can now pass multiple frames to interested image consumers. This feature mimics GIF89a's multiframe functionality. (If you have GIF89a animations, you can display them using getImage()
and drawImage()
; you don't have to build a complicated creature using MemoryImageSource
.) . An animation example follows in Example 12.3 (later in this chapter).
- public synchronized void setAnimated(boolean animated)
- The
setAnimated()
method notifies theMemoryImageSource
if it will be in animation mode (animated
istrue
) or not (animated
isfalse
). By default, animation is disabled; you must call this method to generate an image sequence. To prevent losing data, call this method immediately after calling theMemoryImageSource
constructor. - public synchronized void setFullBufferUpdates(boolean fullBuffers)
- The
setFullBufferUpdates()
method controls how image updates are done during an animation. It is ignored if you are not creating an animation. IffullBuffers
istrue
, this method tells theMemoryImageSource
that it should always send all of an image's data to the consumers whenever it received new data (by a call tonewPixels()
). IffullBuffers
isfalse
, theMemoryImageSource
sends only the changed portion of the image and notifies consumers (by a call toImageConsumer.setHints()
) that frames sent will be complete.Like
setAnimated()
,setFullBufferUpdates()
should be called immediately after calling theMemoryImageSource
constructor, before the animation is started.
To do the actual animation, you update the image array pix[]
that was specified in the constructor and call one of the overloaded newPixels()
methods to tell the MemoryImageSource
that you have changed the image data. The parameters to newPixels()
determine whether you are animating the entire image or just a portion of the image. You can also supply a new array to take pixel data from, replacing pix[]
. In any case, pix[]
supplies the initial image data (i.e., the first frame of the animation).
If you have not called setAnimated(true)
, calls to any version of newPixels()
are ignored.
- public void newPixels()
- The version of
newPixels()
with no parameters tells theMemoryImageSource
to send the entire pixel data (frame) to all the registered image consumers again. Data is taken from the original arraypix[]
. After the data is sent, theMemoryImageSource
notifies consumers that a frame is complete by callingimageComplete(ImageConsumer.SINGLEFRAMEDONE)
, thus updating the display when the image is redisplayed. Remember that in many cases, you don't need to update the entire image; updating part of the image saves CPU time, which may be crucial for your application. To update part of the image, call one of the other versions ofnewPixels()
. - public synchronized void newPixels(int x, int y, int w, int h)
- This
newPixels()
method sends part of the image in the arraypix[]
to the consumers. The portion of the image sent has its upper left corner at the point (x
,y
), widthw
and heighth
, all in pixels. Changing part of the image rather than the whole thing saves considerably on system resources. Obviously, it is appropriate only if most of the image is still. For example, you could use this method to animate the steam rising from a cup of hot coffee, while leaving the cup itself static (an image that should be familiar to anyone reading JavaSoft's Web site). After the data is sent, consumers are notified that a frame is complete by a call toimageComplete(ImageConsumer.SINGLEFRAMEDONE)
, thus updating the display when the image is redisplayed.If
setFullBufferUpdates()
was called, the entire image is sent, and the dimensions of the bounding box are ignored. - public synchronized void newPixels(int x, int y, int w, int h, boolean frameNotify)
- This
newPixels()
method is identical to the last, with one exception: consumers are notified that new image data is available only whenframeNotify
istrue
. This method allows you to generate new image data in pieces, updating the consumers only once when you are finished.If
setFullBufferUpdates()
was called, the entire image is sent, and the dimensions of the bounding box are ignored. - public synchronized void newPixels(byte[] newpix, ColorModel newmodel, int offset, int scansize)
public synchronized void newPixels(int[] newpix, ColorModel newmodel, int offset, int scansize) - These
newPixels()
methods change the source of the animation to thebyte
orint
arraynewpix[]
, with aColorModel
ofnewmodel
.offset
marks the beginning of the data innewpix
to use, whilescansize
states the number of pixels innewpix
per line ofImage
data. Future calls to other versions ofnewPixels()
should modifynewpix[]
rather thanpix[]
.
You can create an image by generating an integer or byte array in memory and converting it to an image with MemoryImageSource
. The following MemoryImage
applet generates two identical images that display a series of color bars from left to right. Although the images look the same, they were generated differently: the image on the left uses the default DirectColorModel
; the image on the right uses an IndexColorModel
.
Because the image on the left uses a DirectColorModel
, it stores the actual color value of each pixel in an array of integers (rgbPixels[]
). The image on the right can use a byte array (indPixels[]
) because the IndexColorModel
puts the color information in its color map instead of the pixel array; elements of the pixel array need to be large enough only to address the entries in this map. Images that are based on IndexColorModel
are generally more efficient in their use of space (integer vs. byte arrays, although IndexColorModel
requires small support arrays) and in performance (if you filter the image).
The output from this example is shown in Figure 12.2. The source is shown in Example 12.2.
Figure 12.2: MemoryImage applet output
Example 12.2: MemoryImage Test Program
import java.applet.*; import java.awt.*; import java.awt.image.*; public class MemoryImage extends Applet { Image i, j; int width = 200; int height = 200; public void init () { int rgbPixels[] = new int [width*height]; byte indPixels[] = new byte [width*height]; int index = 0; Color colorArray[] = {Color.red, Color.orange, Color.yellow, Color.green, Color.blue, Color.magenta}; int rangeSize = width / colorArray.length; int colorRGB; byte colorIndex; byte reds[] = new byte[colorArray.length]; byte greens[] = new byte[colorArray.length]; byte blues[] = new byte[colorArray.length]; for (int i=0;i<colorArray.length;i++) { reds[i] = (byte)colorArray[i].getRed(); greens[i] = (byte)colorArray[i].getGreen(); blues[i] = (byte)colorArray[i].getBlue(); } for (int y=0;y<height;y++) { for (int x=0;x<width;x++) { if (x < rangeSize) { colorRGB = Color.red.getRGB(); colorIndex = 0; } else if (x < (rangeSize*2)) { colorRGB = Color.orange.getRGB(); colorIndex = 1; } else if (x < (rangeSize*3)) { colorRGB = Color.yellow.getRGB(); colorIndex = 2; } else if (x < (rangeSize*4)) { colorRGB = Color.green.getRGB(); colorIndex = 3; } else if (x < (rangeSize*5)) { colorRGB = Color.blue.getRGB(); colorIndex = 4; } else { colorRGB = Color.magenta.getRGB(); colorIndex = 5; } rgbPixels[index] = colorRGB; indPixels[index] = colorIndex; index++; } } i = createImage (new MemoryImageSource (width, height, rgbPixels, 0, width)); j = createImage (new MemoryImageSource (width, height, new IndexColorModel (8, colorArray.length, reds, greens, blues), indPixels, 0, width)); } public void paint (Graphics g) { g.drawImage (i, 0, 0, this); g.drawImage (j, width+5, 0, this); } }
Almost all of the work is done in init()
(which, in a real applet, isn't a terribly good idea; ideally init()
should be lightweight). Previously, we explained the color model's use for the images on the left and the right. Toward the end of init()
, we create the images i
and j
by calling createImage()
with a MemoryImageSource
as the image producer. For image i
, we used the simplest MemoryImageSource
constructor, which uses the default RGB color model. For j
, we called the IndexColorModel
constructor within the MemoryImageSource
constructor, to create a color map that has only six entries: one for each of the colors we use. Using MemoryImageSource for animation
As we've seen, Java 1.1 gives you the ability to create an animation using a MemoryImageSource
by updating the image data in memory; whenever you have finished an update, you can send the resulting frame to the consumers. This technique gives you a way to do animations that consume very little memory, since you keep overwriting the original image. The applet in Example 12.3 demonstrates MemoryImageSource
's animation capability by creating a Mandelbrot image in memory, updating the image as new points are added. Figure 12.3 shows the results, using four consumers to display the image four times.
Example 12.3: Mandelbrot Program
// Java 1.1 only import java.awt.*; import java.awt.image.*; import java.applet.*; public class Mandelbrot extends Applet implements Runnable { Thread animator; Image im1, im2, im3, im4; public void start() { animator = new Thread(this); animator.start(); } public synchronized void stop() { animator = null; } public void paint(Graphics g) { if (im1 != null) g.drawImage(im1, 0, 0, null); if (im2 != null) g.drawImage(im2, 0, getSize().height / 2, null); if (im3 != null) g.drawImage(im3, getSize().width / 2, 0, null); if (im4 != null) g.drawImage(im4, getSize().width / 2, getSize().height / 2, null); } public void update (Graphics g) { paint (g); } public synchronized void run() { Thread.currentThread().setPriority(Thread.MIN_PRIORITY); int width = getSize().width / 2; int height = getSize().height / 2; byte[] pixels = new byte[width * height]; int index = 0; int iteration=0; double a, b, p, q, psq, qsq, pnew, qnew; byte[] colorMap = {(byte)255, (byte)255, (byte)255, // white (byte)0, (byte)0, (byte)0}; // black MemoryImageSource mis = new MemoryImageSource( width, height, new IndexColorModel (8, 2, colorMap, 0, false, -1), pixels, 0, width); mis.setAnimated(true); im1 = createImage(mis); im2 = createImage(mis); im3 = createImage(mis); im4 = createImage(mis); // Generate Mandelbrot final int ITERATIONS = 16; for (int y=0; y<height; y++) { b = ((double)(y-64))/32; for (int x=0; x<width; x++) { a = ((double)(x-64))/32; p=q=0; iteration = 0; while (iteration < ITERATIONS) { psq = p*p; qsq = q*q; if ((psq + qsq) >= 4.0) break; pnew = psq - qsq + a; qnew = 2*p*q+b; p = pnew; q = qnew; iteration++; } if (iteration == ITERATIONS) { pixels[index] = 1; mis.newPixels(x, y, 1, 1); repaint(); } index++; } } } }
Figure 12.3: Mandelbrot output
Most of the applet in Example 12.3 should be self-explanatory. The init()
method starts the thread in which we do our computation. paint()
just displays the four images we create. All the work, including the computation, is done in the thread's run()
method. run()
starts by setting up a color map, creating a MemoryImageSource
with animation enabled and creating four images using that source as the producer. It then does the computation, which I won't explain; for our purposes, the interesting part is what happens when we've computed a pixel. We set the appropriate byte in our data array, pixels[]
, and then call newPixels()
, giving the location of the new pixel and its size (1 by 1) as arguments. Thus, we redraw the images for every new pixel. In a real application, you would probably compute a somewhat larger chunk of new data before updating the screen, but the same principles apply.