12.4 ImageConsumerThe ImageConsumer interface specifies the methods that must be implemented to receive data from an ImageProducer. For the most part, that is the only context in which you need to know about the ImageConsumer interface. If you write an image producer, it will be handed a number of obscure objects, about which you know nothing except that they implement ImageConsumer, and that you can therefore call the methods discussed in this section to deliver your data. The chances that you will ever implement an image consumer are rather remote, unless you are porting Java to a new environment. It is more likely that you will want to subclass ImageFilter, in which case you may need to implement some of these methods. But most of the time, you will just need to know how to hand your data off to the next element in the chain. The java.awt.image package includes two classes that implement ImageConsumer: PixelGrabber and ImageFilter (and its subclasses). These classes are unique in that they don't display anything on the screen. PixelGrabber takes the image data and stores it in a pixel array; you can use this array to save the image in a file, generate a new image, etc. ImageFilter, which is used in conjunction with FilteredImageSource, modifies the image data; the FilteredImageSource sends the modified image to another consumer, which can further modify or display the new image. When you draw an image on the screen, the JDK's ImageRepresentation class is probably doing the real work. This class is part of the sun.awt.image package. You really don't need to know anything about it, although you may see ImageRepresentation mentioned in a stack trace if you try to filter beyond the end of a pixel array. ImageConsumer InterfaceConstantsThere are two sets of constants for ImageConsumer. One set represents those that can be used for the imageComplete() method. The other is used with the setHints() method. See the descriptions of those methods on how to use them. The first set of flags is for the imageComplete() method:
The following set of flags can be ORed together to form the single parameter to the setHints() method. Certain flags do not make sense set together, but it is the responsibility of the concrete ImageConsumer to enforce this.
The interface methods are presented in the order in which they are normally called by an ImageProducer.
Now that we have discussed the ImageConsumer interface, we're finally ready to give an example of a full-fledged ImageProducer. This producer uses the methods of the ImageConsumer interface to communicate with image consumers; image consumers use the ImageProducer interface to register themselves with this producer. Our image producer will interpret images in the PPM format.[1] PPM is a simple image format developed by Jef Poskanzer as part of the pbmplus image conversion package. A PPM file starts with a header consisting of the image type, the image's width and height in pixels, and the maximum value of any RGB component. The header is entirely in ASCII. The pixel data follows the header; it is either in binary (if the image type is P6) or ASCII (if the image type is P3). The pixel data is simply a series of bytes describing the color of each pixel, moving left to right and top to bottom. In binary format, each pixel is represented by three bytes: one for red, one for green, and one for blue. In ASCII format, each pixel is represented by three numeric values, separated by white space (space, tab, or newline). A comment may occur anywhere in the file, but it would be surprising to see one outside of the header. Comments start with # and continue to the end of the line. ASCII format files are obviously much larger than binary files. There is no compression on either file type.
The PPMImageDecoder source is listed in Example 12--4. The applet that uses this class is shown in Example 12.5. You can reuse a lot of the code in the PPMImageDecoder when you implement your own image producers. Example 12.4: PPMImageDecoder Source
import java.awt.*; import java.awt.image.*; import java.util.*; import java.io.*; public class PPMImageDecoder implements ImageProducer { /* Since done in-memory, only one consumer */ private ImageConsumer consumer; boolean loadError = false; int width; int height; int store[][]; Hashtable props = new Hashtable(); /* Format of Ppm file is single pass/frame, w/ complete scan lines in order */ private static int PpmHints = (ImageConsumer.TOPDOWNLEFTRIGHT | ImageConsumer.COMPLETESCANLINES | ImageConsumer.SINGLEPASS | ImageConsumer.SINGLEFRAME); The class starts by declaring class variables and constants. We will use the variable PpmHints when we call setHints(). Here, we set this variable to a collection of "hint" constants that indicate we will produce pixel data in top-down, left-right order; we will always send complete scan lines; we will make only one pass over the pixel data (we will send each pixel once); and there is one frame per image (i.e., we aren't producing a multiframe sequence). The next chunk of code implements the ImageProducer interface; consumers use it to request image data:
/* There is only a single consumer. When it registers, produce image. */ /* On error, notify consumer. */ public synchronized void addConsumer (ImageConsumer ic) { consumer = ic; try { produce(); }catch (Exception e) { if (consumer != null) consumer.imageComplete (ImageConsumer.IMAGEERROR); } consumer = null; } /* If consumer passed to routine is single consumer, return true, else false. */ public synchronized boolean isConsumer (ImageConsumer ic) { return (ic == consumer); } /* Disables consumer if currently consuming. */ public synchronized void removeConsumer (ImageConsumer ic) { if (consumer == ic) consumer = null; } /* Production is done by adding consumer. */ public void startProduction (ImageConsumer ic) { addConsumer (ic); } public void requestTopDownLeftRightResend (ImageConsumer ic) { // Not needed. The data is always in this format. } The previous group of methods implements the ImageProducer interface. They are quite simple, largely because of the way this ImageProducer generates images. It builds the image in memory before delivering it to the consumer; you must call the readImage() method (discussed shortly) before you can create an image with this consumer. Because the image is in memory before any consumers can register their interest, we can write an addConsumer() method that registers a consumer and delivers all the data to that consumer before returning. Therefore, we don't need to manage a list of consumers in a Hashtable or some other collection object. We can store the current consumer in an instance variable ic and forget about any others: only one consumer exists at a time. To make sure that only one consumer exists at a time, we synchronize the addConsumer(), isConsumer(), and removeConsumer() methods. Synchronization prevents another consumer from registering itself before the current consumer has finished. If you write an ImageProducer that builds the image in memory before delivering it, you can probably use this code verbatim. addConsumer() is little more than a call to the method produce(), which handles "consumer relations": it delivers the pixels to the consumer using the methods in the ImageConsumer interface. If produce() throws an exception, addConsumer() calls imageComplete() with an IMAGEERROR status code. Here's the code for the produce() method:
/* Production Process: Prerequisite: Image already read into store array. (readImage) props / width / height already set (readImage) Assumes RGB Color Model - would need to filter to change. Sends Ppm Image data to consumer. Pixels sent one row at a time. */ private void produce () { ColorModel cm = ColorModel.getRGBdefault(); if (consumer != null) { if (loadError) { consumer.imageComplete (ImageConsumer.IMAGEERROR); } else { consumer.setDimensions (width, height); consumer.setProperties (props); consumer.setColorModel (cm); consumer.setHints (PpmHints); for (int j=0;j<height;j++) consumer.setPixels (0, j, width, 1, cm, store[j], 0, width); consumer.imageComplete (ImageConsumer.STATICIMAGEDONE); } } } produce() just calls the ImageConsumer methods in order: it sets the image's dimensions, hands off an empty Hashtable of properties, sets the color model (the default RGB model) and the hints, and then calls setPixels() once for each row of pixel data. The data is in the integer array store[][], which has already been loaded by the readImage() method (defined in the following code). When the data is delivered, the method setPixels() calls imageComplete() to indicate that the image has been finished successfully.
/* Allows reading to be from internal byte array, in addition to disk/socket */ public void readImage (byte b[]) { readImage (new ByteArrayInputStream (b)); } /* readImage reads image data from Stream */ /* parses data for PPM format */ /* closes inputstream when done */ public void readImage (InputStream is) { long tm = System.currentTimeMillis(); boolean raw=false; DataInputStream dis = null; BufferedInputStream bis = null; try { bis = new BufferedInputStream (is); dis = new DataInputStream (bis); String word; word = readWord (dis); if ("P6".equals (word)) { raw = true; } else if ("P3".equals (word)) { raw = false; } else { throw (new AWTException ("Invalid Format " + word)); } width = Integer.parseInt (readWord (dis)); height = Integer.parseInt (readWord (dis)); // Could put comments in props - makes readWord more complex int maxColors = Integer.parseInt (readWord (dis)); if ((maxColors < 0) || (maxColors > 255)) { throw (new AWTException ("Invalid Colors " + maxColors)); } store = new int[height][width]; if (raw) { // binary format (raw) pixel data byte row[] = new byte [width*3]; for (int i=0;i<height;i++){ dis.readFully (row); for (int j=0,k=0;j<width;j++,k+=3) { int red = row[k]; int green = row[k+1]; int blue = row[k+2]; if (red < 0) red +=256; if (green < 0) green +=256; if (blue < 0) blue +=256; store[i][j] = (0xff<< 24) | (red << 16) | (green << 8) | blue; } } } else { // ASCII pixel data for (int i=0;i<height;i++) { for (int j=0;j<width;j++) { int red = Integer.parseInt (readWord (dis)); int green = Integer.parseInt (readWord (dis)); int blue = Integer.parseInt (readWord (dis)); store[i][j] = (0xff<< 24) | (red << 16) | (green << 8) | blue; } } } } catch (IOException io) { loadError = true; System.out.println ("IO Exception " + io.getMessage()); } catch (AWTException awt) { loadError = true; System.out.println ("AWT Exception " + awt.getMessage()); } catch (NoSuchElementException nse) { loadError = true; System.out.println ("No Such Element Exception " + nse.getMessage()); } finally { try { if (dis != null) dis.close(); if (bis != null) bis.close(); if (is != null) is.close(); } catch (IOException io) { System.out.println ("IO Exception " + io.getMessage()); } } System.out.println ("Done in " + (System.currentTimeMillis() - tm) + " ms"); } readImage() reads the image data from an InputStream and converts it into the array of pixel data that produce() transfers to the consumer. Code using this class must call readImage() to process the data before calling createImage(); we'll see how this works shortly. Although there is a lot of code in readImage(), it's fairly simple. (It would be much more complex if we were dealing with an image format that compressed the data.) It makes heavy use of readWord(), a utility method that we'll discuss next; readWord() returns a word of ASCII text as a string. readImage() starts by converting the InputStream into a DataInputStream. It uses readWord() to get the first word from the stream. This should be either "P6" or "P3", depending on whether the data is in binary or ASCII. It then uses readWord() to save the image's width and height and the maximum value of any color component. Next, it reads the color data into the store[][] array. The ASCII case is simple because we can use readWord() to read ASCII words conveniently; we read red, green, and blue words, convert them into ints, and pack the three into one element (one pixel) of store[][]. For binary data, we read an entire scan line into the byte array row[], using readFully(); then we start a loop that packs this scan line into one row of store[][]. A little additional complexity is in the inner loop because we must keep track of two arrays (row[] and store[][]). We read red, green, and blue components from row[], converting Java's signed bytes to unsigned data by adding 256 to any negative values; finally, we pack these components into one element of store[][].
/* readWord returns a word of text from stream */ /* Ignores PPM comment lines. */ /* word defined to be something wrapped by whitespace */ private String readWord (InputStream is) throws IOException { StringBuffer buf = new StringBuffer(); int b; do {// get rid of leading whitespace if ((b=is.read()) == -1) throw new EOFException(); if ((char)b == '#') { // read to end of line - ppm comment DataInputStream dis = new DataInputStream (is); dis.readLine(); b = ' '; // ensure more reading } }while (Character.isSpace ((char)b)); do { buf.append ((char)(b)); if ((b=is.read()) == -1) throw new EOFException(); } while (!Character.isSpace ((char)b)); // reads first space return buf.toString(); } } readWord() is a utility method that reads one ASCII word from an InputStream. A word is a sequence of characters that aren't spaces; space characters include newlines and tabs in addition to spaces. This method also throws out any comments (anything between # and the end of the line). It collects the characters into a StringBuffer, converting the StringBuffer into a String when it returns. Example 12.5: PPMImageDecoder Test Program
import java.awt.Graphics; import java.awt.Color; import java.awt.image.ImageConsumer; import java.awt.Image; import java.awt.MediaTracker; import java.net.URL; import java.net.MalformedURLException; import java.io.InputStream; import java.io.IOException; import java.applet.Applet; public class ppmViewer extends Applet { Image image = null; public void init () { try { String file = getParameter ("file"); if (file != null) { URL imageurl = new URL (getDocumentBase(), file); InputStream is = imageurl.openStream(); PPMImageDecoder ppm = new PPMImageDecoder (); ppm.readImage (is); image = createImage (ppm); repaint(); } } catch (MalformedURLException me) { System.out.println ("Bad URL"); } catch (IOException io) { System.out.println ("Bad File"); } } public void paint (Graphics g) { g.drawImage (image, 0, 0, this); } } The applet we use to test our ImageProducer is very simple. It creates a URL that points to an appropriate PPM file and gets an InputStream from that URL. It then creates an instance of our PPMImageDecoder; calls readImage() to load the image and generate pixel data; and finally, calls createImage() with our ImageProducer as an argument to create an Image object, which we draw in paint(). PixelGrabberThe PixelGrabber class is a utility for converting an image into an array of pixels. This is useful in many situations. If you are writing a drawing utility that lets users create their own graphics, you probably want some way to save a drawing to a file. Likewise, if you're implementing a shared whiteboard, you'll want some way to transmit images across the Net. If you're doing some kind of image processing, you may want to read and alter individual pixels in an image. The PixelGrabber class is an ImageConsumer that can capture a subset of the current pixels of an Image. Once you have the pixels, you can easily save the image in a file, send it across the Net, or work with individual points in the array. To recreate the Image (or a modified version), you can pass the pixel array to a MemoryImageSource. Prior to Java 1.1, PixelGrabber saves an array of pixels but doesn't save the image's width and height--that's your responsibility. You may want to put the width and height in the first two elements of the pixel array and use an offset of 2 when you store (or reproduce) the image. Starting with Java 1.1, the grabbing process changes in several ways. You can ask the PixelGrabber for the image's size or color model. You can grab pixels asynchronously and abort the grabbing process before it is completed. Finally, you don't have to preallocate the pixel data array. Constructors
You can modify images by combining a PixelGrabber with MemoryImageSource. Use getImage() to load an image from the Net; then use PixelGrabber to convert the image into an array. Modify the data in the array any way you please; then use MemoryImageSource as an image producer to display the new image. Example 12.6 demonstrates the use of the PixelGrabber and MemoryImageSource to rotate, flip, and mirror an image. (We could also do the rotations with a subclass of ImageFilter, which we will discuss next.) The output is shown in Figure 12.6. When working with an image that is loaded from a local disk or the network, remember to wait until the image is loaded before grabbing its pixels. In this example, we use a MediaTracker to wait for the image to load. Example 12.6: Flip Source
import java.applet.*; import java.awt.*; import java.awt.image.*; public class flip extends Applet { Image i, j, k, l; public void init () { MediaTracker mt = new MediaTracker (this); i = getImage (getDocumentBase(), "ora-icon.gif"); mt.addImage (i, 0); try { mt.waitForAll(); int width = i.getWidth(this); int height = i.getHeight(this); int pixels[] = new int [width * height]; PixelGrabber pg = new PixelGrabber (i, 0, 0, width, height, pixels, 0, width); if (pg.grabPixels() && ((pg.status() & ImageObserver.ALLBITS) !=0)) { j = createImage (new MemoryImageSource (width, height, rowFlipPixels (pixels, width, height), 0, width)); k = createImage (new MemoryImageSource (width, height, colFlipPixels (pixels, width, height), 0, width)); l = createImage (new MemoryImageSource (height, width, rot90Pixels (pixels, width, height), 0, height)); } } catch (InterruptedException e) { e.printStackTrace(); } } The try block in Example 12.6 does all the interesting work. It uses a PixelGrabber to grab the entire image into the array pixels[]. After calling grabPixels(), it checks the PixelGrabber status to make sure that the image was stored correctly. It then generates three new images based on the first by calling createImage() with a MemoryImageSource object as an argument. Instead of using the original array, the MemoryImageSource objects call several utility methods to manipulate the array: rowFlipPixels(), colFlipPixels(), and rot90Pixels(). These methods all return integer arrays.
public void paint (Graphics g) { g.drawImage (i, 10, 10, this); // regular if (j != null) g.drawImage (j, 150, 10, this); // rowFlip if (k != null) g.drawImage (k, 10, 60, this); // colFlip if (l != null) g.drawImage (l, 150, 60, this); // rot90 } private int[] rowFlipPixels (int pixels[], int width, int height) { int newPixels[] = null; if ((width*height) == pixels.length) { newPixels = new int [width*height]; int newIndex=0; for (int y=height-1;y>=0;y--) for (int x=width-1;x>=0;x--) newPixels[newIndex++]=pixels[y*width+x]; } return newPixels; } rowFlipPixels() creates a mirror image of the original, flipped horizontally. It is nothing more than a nested loop that copies the original array into a new array.
private int[] colFlipPixels (int pixels[], int width, int height) { ... } private int[] rot90Pixels (int pixels[], int width, int height) { ... } } colFlipPixels() and rot90Pixels() are fundamentally similar to rowFlipPixels(); they just copy the original pixel array into another array, and return the result. colFlipPixels() generates a vertical mirror image; rot90Pixels() rotates the image by 90 degrees counterclockwise. Grabbing data asynchronously To demonstrate the new methods introduced by Java 1.1 for PixelGrabber, the following program grabs the pixels and reports information about the original image on mouse clicks. It takes its data from the image used in Figure 12.6.
// Java 1.1 only import java.applet.*; import java.awt.*; import java.awt.image.*; import java.awt.event.*; public class grab extends Applet { Image i; PixelGrabber pg; public void init () { i = getImage (getDocumentBase(), "ora-icon.gif"); pg = new PixelGrabber (i, 0, 0, -1, -1, false); pg.startGrabbing(); enableEvents (AWTEvent.MOUSE_EVENT_MASK); } public void paint (Graphics g) { g.drawImage (i, 10, 10, this); } protected void processMouseEvent(MouseEvent e) { if (e.getID() == MouseEvent.MOUSE_CLICKED) { System.out.println ("Status: " + pg.getStatus()); System.out.println ("Width: " + pg.getWidth()); System.out.println ("Height: " + pg.getHeight()); System.out.println ("Pixels: " + (pg.getPixels() instanceof byte[] ? "bytes" : "ints")); System.out.println ("Model: " + pg.getColorModel()); } super.processMouseEvent (e); } } This applet creates a PixelGrabber without specifying an array, then starts grabbing pixels. The grabber allocates its own array, but we never bother to ask for it since we don't do anything with the data itself: we only report the grabber's status. (If we wanted the data, we'd call getPixels().) Sample output from a single mouse click, after the image loaded, would appear something like the following:
Status: 27 Width: 120 Height: 38 Pixels: bytes Model: java.awt.image.IndexColorModel@1ed34 You need to convert the status value manually to the corresponding meaning by looking up the status codes in ImageObserver. The value 27 indicates that the 1, 2, 8, and 16 flags are set, which translates to the WIDTH, HEIGHT, SOMEBITS, and FRAMEBITS flags, respectively. |
|