| Click here to support our advertisers

SHOPPING
JOB BANK
CLASSIFIEDS
DIRECTORIES
REFERENCE Online Library
LEARNING CENTER
JOURNAL
NEWS CENTRAL
DOWNLOADS
COMMUNITY
CALENDAR
ABOUT US
Journal:
Get the weekly email highlights from the most popular journal for developers! Current issue
> developer.com developerdirect.com htmlgoodies.com javagoodies.com jars.com intranetjournal.com javascripts.com | All Categories : Java # Chapter 7 # Java and Images ### CONTENTS Displaying Images Loading Java Images Image Display Image Observers Tracking Image Loading The Consumer/Producer Model Java Color Models Default RGB Direct Color Index Color Chapter Project: Displaying a Windows BMP Image Using Image Types Not Supported by Java Memory Images Loading Foreign Images BMP File Format Reading Unsigned Binary in Java Creating the Color Table Constructing the Image Summary Images offer the best way to work with Java graphics; as a matter of fact, everything in the AWT seems centered on the concept of images. This chapter shows you how to use Java for generating images. It leads off with rendering and tracking a simple image and continues by explaining the fundamental model behind Java images. The chapter ends by writing a class to display image formats not directly supported by Java. ## Displaying Images Images are nothing more than a collection of colors and their layout, but they are useful because, with an auxiliary paint program, you can create sophisticated visual effects that can be captured and displayed in your applets. Java arrives with built-in support for two types of images: GIF and JPEG. The GIF standard (Graphics Interchange Format) is maintained by CompuServe. It uses an excellent compression scheme (LZW) to represent a large image in a small file. JPEG (Joint Photographic Experts Group) is an international standard mainly used for photographic material. It uses a discrete cosine transform (DCT) to remove extraneous material your eye doesn't really notice, so a very efficient compression scheme can be used. The cosine transform is "lossy," meaning it loses some information when applied. LZW, on the other hand, is "lossless." It turns out that the information removed by a cosine transform is precisely the photographic detail that your eye does not see. ### Loading Java Images Both these formats can be easily loaded by your applets: `Image newImage = getImage(URL); Image newImage = Toolkit.getDefaultToolkit().getImage(filename or URL);` The first line may be used only from a subclass of Applet, but line two can be called by either an applet or application. Each `getImage()` method returns immediately, without actually loading the image. To retrieve the image, you must try to display it; this is done to keep memory consumption down. For example, sometimes an applet might refer to an image, but not actually make use of it. Therefore, until the image is really needed, it will remain on the server.
> Note
> The `getImage()` method does not cause your image to be loaded. The image remains on the server until you try to display it.
> The Applet class provides two versions of `getImage()`: `public Image getImage(URL imgLocation);` `public Image getImage(URL baseLocation, String filename);` The second call is the one most commonly used because applets can load only from the server they originated on. The methods to get either the URL of the page or the applet code's URL can then be combined with the filename of the image to construct a complete path: `Image newImage = getImage(getDocumentBase(), "image.gif");` or `Image newImage = getImage(getCodeBase(), "image.gif");` The call you use depends on whether your image is grouped with the class files or the HTML Web pages on your server.
> Note
> You must be aware of the organization of your various data files on the server. If your images reside with your class files (/htdocs/classes/images), then use `getCodeBase()`; however, if your images reside with your html files (/htdocs/images), use `getDocumentBase()`. Many times, class files are grouped together with html files. In this case, both methods will return the same URL.
> The Toolkit also provides two `getImage()` methods: `public Image getImage(URL imgLocation);` `public Image getImage(String filename);` Although the Toolkit can retrieve a filename, applets can't use `getImage()` to read local files because they would cause a security exception. Remember, applets can read only from the server they originated on. Allowing applets to read from a local drive is definitely a security no-no. ### Image Display Once an Image object is instantiated, it can be displayed in an applet's `paint()` method by using the Graphics object passed to it: `g.drawImage(newImage, x, y, this);` Variables `x` and `y` contain the coordinates of the image's upper-left corner, and the final parameter is an ImageObserver object. This interface is implemented in the Component class that the applet is derived from, which is why you can pass the `this` pointer. You'll learn more about the ImageObserver interface in the next section. There are four variations of `drawImage()` in the Graphics class: `public abstract boolean drawImage(Image img, int x, int y, ImageObserver observer);` `public abstract boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer);` `public abstract boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer);` `public abstract boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer);` The width and height parameters allow you to scale an image, which can be enlarged or reduced in either the x or y direction. The `bgcolor` parameter specifies which color to use for any transparent pixels in the image. Each `drawImage()` version returns `true` if the image was painted, `false` otherwise. The image will not paint if it hasn't been loaded yet. It will eventually display because the Component class will be notified of the load and will call your paint method when the image arrives. ### Image Observers The Component class accomplishes this because it implements the ImageObserver interface. Most of Java's image-manipulation routines are asynchronous, meaning they return immediately and notify you when they have completed their assignment. The notification, which flows through the ImageObserver interface, contains the following method: `public abstract boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height);` The Component class uses this method, but you can override it to get information about your image. The `infoflags` parameter is a bit flag; the settings for the bits are shown in Table 7.1. Table 7.1. Infoflags bit values for the ImageObserver interface.
> Name | Meaning
> `WIDTH=1` | Width is available and can be read from the width parameter.
> `HEIGHT=2` | Height is available and can be read from the height parameter.
> `PROPERTIES=4` | Image properties are now available. The `getProperty()` method can be used.
> `SOMEBITS=8` | Additional pixels for drawing a scaled image are available. The bounding box of the pixels can be read from the x, y, width, and height parameters.
> `FRAMEBITS=16` | A complete image frame has been built and can now be displayed.
> `ALLBITS=32` | A complete static image has been built and can now be displayed.
> `ERROR=64` | An error occurred. No further information will be available, and drawing will fail.
> `ABORT=128` | Image processing has been aborted. Set at the same time as ERROR. If ERROR is not also set, then you may try to paint the object again.
> The following routine is used to repaint the applet when a complete image arrives: `public boolean imageUpdate(Image whichOne, int flags, int x, int y, int w, int h) { if ( (flags & (ERROR | FRAMEBITS | ALLBITS)) != 0 ) { repaint(); return false; } return true; }` The return value specifies whether you would like to continue to get information on this image; returning `false` will stop future notifications. ## Tracking Image Loading Image loading can also be tracked by using the MediaTracker class. Unlike the ImageObserver interface, it will not call back when something completes. The client of a MediaTracker object must register images with the tracker, then ask for status. Registration involves passing an image and assigning a tracking number to it, which then is used to query for the image's status. The following methods are available for image registration: `public void addImage(Image image, int id);` `public void addImage(Image image, int id, int w, int h);` If a width and height are specified, the image will be scaled to these values. You can assign the same ID to multiple images. All the status-check routines can work on several images at once.
> Note
> If you assign the same ID to two or more images, then you can't check on the individual status of each image. Only group status as a whole can be checked.
> You can use the following routines to get status information: `public boolean checkAll();` `public boolean checkAll(boolean load);` `public void waitForAll();` `public boolean waitForAll(long timeout);` `public int statusAll(boolean load);` `public boolean checkID(int id);` `public boolean checkID(int id, boolean load);` `public void waitForID(int id);` `public boolean waitForID(int id, long timeout);` `public int statusID(int id, boolean load);` The MediaTracker class can be passed a load parameter. If this parameter is `true`, then the image (or images) will start to load. Remember, `getImage()` does not actually load the image. MediaTracker can be used to preload an image before it is displayed. The methods returning a Boolean value will indicate `false` unless all eligible images are complete. Images that encounter an error are considered to be complete, so you have to check for errors with these routines: `public boolean isErrorAny();` `public Object[] getErrorsAny();` `public boolean isErrorID(int id);` `public Object[] getErrorsID(int id);` The integer returned by `statusAll()` and `statusID()` uses a bit flag much like `imageUpdate()` does; the values for the bit flag are listed in Table 7.2. The wait methods will block until all images are complete. You can also specify a time-out in milliseconds that determines the maximum time to wait. Table 7.2. Status bit values for MediaTracker.
> Name | Meaning
> `LOADING=1` | Some (or all) images are still loading.
> `ABORTED=2` | Some (or all) images have aborted.
> `ERRORED=4` | Some (or all) images have encountered an error.
> `COMPLETE=8` | Some (or all) images have loaded.
> ## The Consumer/Producer Model In Java, the Image class is just the tip of the iceberg; beneath it stand the ImageConsumer and ImageProducer interfaces. Image data is originated in an object that adheres to the ImageProducer interface, which sends the data to an object using the ImageConsumer interface. Figure 7.1 illustrates this relationship. Figure 7.1 : The relationship between ImageProducer and ImageConsumer This model allows any type of object to both originate and receive image data. By creating the image subsystems as interfaces, Sun has freed image production from any specific object type. This is an important abstraction that you'll exploit in this chapter's project. ## Java Color Models As stated earlier, an image is a collection of colors and their layout. Much research has been done on how color is represented. Humans perceive color when combinations of wavelengths of visible light stimulate the retina. The number of wavelength combinations is infinite, but humans can see only a fixed subset as separate colors. Therefore, color models were invented to group human-visible colors into a working set. There are two predominant color models used to represent color information: The CMY (cyan-magenta-yellow) color model is used in subtractive color systems, such as printing. The RGB (red-green-blue) color model is used in additive color systems, such as television and computer screens. Printing is a subtractive system because the perceived color is contained in wavelengths of light reflected from the paper. The absorbed colors are said to be "subtracted" from the perceived color. Conversely, an additive color system creates the light source containing the color. Therefore, you can watch television in the dark, but you can't read a magazine.
> Note
> If you're really curious, cyan absorbs red light, magenta absorbs green light, and yellow absorbs blue light. CMY color systems subtract RGB light and thus control the appearance of RGB color on a printed page.
> Java encapsulates color information for an image in the ColorModel class. Using the model, pixel data is interpreted into a raw color component (red, green, blue, and alpha) for display. The ColorModel class has the following methods: `public static ColorModel getRGBdefault();` `public int getPixelSize();` `public int getRed(int pixel);` `public int getGreen(int pixel);` `public int getBlue(int pixel);` `public int getAlpha(int pixel);` `public int getRGB(int pixel);` The lone static method returns the system default ColorModel. ### Default RGB Java uses the RGB color model for all its painting; all other models are eventually translated into this format. It has 8 bits of red, 8 bits of green, 8 bits of blue, and 8 bits of alpha. The alpha channel supplies transparency-255 is opaque (visible), and 0 is transparent. These add up to 32 bits of color information, which just happens to be the size of a Java integer. The format of colors within an integer is `0xAARRGGBB`. To support images, Java supplies two other ColorModels: DirectColorModel and IndexColorModel. ### Direct Color The DirectColorModel is used when the underlying pixels in an image contain the RGB values directly. This is also known as "true color." There are two constructors-one with an alpha channel, one without. To create the model, you need to specify only the number of bits per pixel and which bits correspond to which color: `public DirectColorModel(int bits, int rmask, int gmask, int bmask);` `public DirectColorModel(int bits, int rmask, int gmask, int bmask, int amask);` The mask values for Java's default RGB model are the following: `r` = `0x00ff0000` `g` = `0x0000ff00` `b` = `0x000000ff` `a` = `0xff000000` ### Index Color The IndexColorModel is used when the underlying pixels in an image represent an index into a color table. Most bitmaps fall into this category because the actual colors are contained in a color map somewhere in the file. The actual pixel data represent indexes into the color map instead of complete RGB values. There are five constructors for this model: `public IndexColorModel(int bits, int size, byte r[], byte g[], byte b[]);` `public IndexColorModel(int bits, int size, byte r[], byte g[], byte b[], int trans);` `public IndexColorModel(int bits, int size, byte r[], byte g[], byte b[], byte a[]);` `public IndexColorModel(int bits, int size, byte cmap[], int start, boolean hasalpha);` `public IndexColorModel(int bits, int size, byte cmap[], int start, boolean hasalpha, int trans);` The parameter `bits` represents how many bits per pixel in the image, and `size` specifies the length of each color array. The colors themselves can be passed as individual arrays or packed into one large array (all reds, then all greens, and so forth). The parameter `hasalpha` signals the presence (or absence) of alpha information at the end of the packed array, and the `trans` parameter indicates which index is to be considered transparent, regardless of its alpha channel setting. ## Chapter Project: Displaying a Windows BMP Image Java has built-in support for GIF and JPEG format images, but what if you want to display an image using a different format? This chapter's project creates a class to display Windows BMP images. The principles involved in the display can be applied to almost any image format. ### Using Image Types Not Supported by Java The goal of this project is to create a class that accepts a URL and filename just as `getImage()` does. Although `getImage()` returns an image, the BmpImage class will return an ImageProducer. The caller of the class will have to use the producer to create an image: `producer = BmpImage.getImageProducer(getCodeBase(), "Forest.bmp"); myImage = createImage(producer);`
> Note
> BmpImage could return an image, but the class would have had to be a component subclass to create an image. I didn't want to apply any restrictions to using BmpImage.
> ### Memory Images External image formats are most easily displayed by using the Java class MemoryImageSource, which allows an arbitrary array of pixels to be stored and used as the source for an ImageProducer. Because MemoryImageSource uses the ImageProducer interface, it can be used as the source for an image in the same way a GIF or JPEG image is used. The class has six different constructors: `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 MemoryImage(int w, int h, int pix[], int off, int scan);` `public MemoryImage(int w, int h, int pix[], int off, int scan, Hashtable props);` The first four pass in a ColorModel, but the final two do not. No ColorModel indicates that the passed pixel array uses the default RGB model. Hashtable `props` will be passed in the ImageConsumer call `setProperties(Hashtable)`. Normally, the props constructors are not used unless your image consumer uses the `setProperties()` method. Now that you know how to create a suitable ImageProducer, the only remaining mystery is how to load and convert an arbitrary BMP image into the correct constructor arguments for MemoryImageSource. ### Loading Foreign Images Foreign images, such as BMP, are loaded by using the Java URL class. The following code snippet creates an input stream for a URL: `InputStream is = new URL(getCodeBase(), filename).openStream();` Once created, the input stream is read until all the information needed to create the image has been extracted. ### BMP File Format The Windows and OS/2 BMP formats are simple color map images; Figure 7.2 lays out the formats. All quantities are in Intel little-endian format. This means that all multibyte quantities, such as a 2-byte short, are stored as low byte, then high byte. Java uses big endian for all I/O reads (high byte, then low byte). You cannot use Java's `readShort()` or `readInt()` method to parse the file. Figure 7.2 : Layout of Windows and OS/2 BMP files
> Note
> The 2-byte quantity `0x1234` appears in memory differently depending on the system's endian order. In a little-endian system, the number would be stored in memory as 34, 12 (low byte first). Big-endian systems would store the number in memory as 12, 34 (high byte first).
> Windows color maps are stored as 4 bytes per index. Each index consists of blue, green, red, and reserved bytes, in that order. The number of indexes is determined from either the number of colors specified in the header or the number of bits per pixel. If the number of colors in the header is zero, than bits per pixel is converted into number of colors. Images having 1, 4, or 8 bits per pixel use 2, 16, or 256 colors, respectively. OS/2 BMP images store colors as 3 bytes per index. Each OS/2 index consists of blue, green, and red bytes, in that order. ### Reading Unsigned Binary in Java I created a single method to read in a multibyte little-endian sequence: `/** * A private method for extracting little endian` `* quantities from a data input stream. * @param is contains the input stream` `* @param len is the number of bytes in the quantity * @returns the result as an integer` `*/ private int pullVal(DataInputStream is, int len)` `throws IOException {` `int value = 0; int temp; for ( int x = 0; x < len; x++ )` `{ temp = is.readUnsignedByte();` `value += (temp << (x * 8)); }` `return value; }` Each byte is read as an unsigned quantity and shifted into the proper position before being added to the total. Little-endian values are stored in completely reversed format, so the routine shifts each byte in multiples of 8 bits.
> Note
> The Java method `readUnsignedByte()` returns an integer, not a byte. Java bytes are signed quantities, so a larger storage variable had to be used to contain the unsigned value.
### Creating the Color Table Since the colors are stored in RGB format, you will create separate arrays for each color. One large array could have been used, but managing it would have been more complex. Once the color arrays have been stored, they are used to create a ColorModel: `/** * A private method for extracting the color table from` `* a BMP type file. * @param is contains the input stream` `* @param numColors contains the biClrUsed (for Windows) or zero */ private void extractColorMap(DataInputStream is, int numColors) throws IOException, AWTException { byte blues[], reds[], greens[]; // if passed count is zero, then determine the` `// number of entries from bits per pixel. if ( numColors == 0 )` `{ switch ( biBitCount )` `{ case 1: numColors = 2; break;` `case 4: numColors = 16; break; case 8: numColors = 256; break;` `case 24: numColors = 0; break; default: numColors = -1; break;` `} } if ( numColors == -1 ) throw new AWTException("Invalid bits per pixel: " + biBitCount); else if ( numColors == 0 ) colorModel = new DirectColorModel(24, 255 * 3, 255 * 2, 255); else {` `reds = new byte[numColors]; blues = new byte[numColors];` `greens = new byte[numColors]; for ( int x = 0; x < numColors; x++ )` `{ blues[x] = is.readByte();` `greens[x] = is.readByte(); reds[x] = is.readByte();` `if ( windowsStyle ) is.skipBytes(1);` `} colorModel = new IndexColorModel( biBitCount, numColors,` `reds, greens, blues ); } }` DirectColorModel is used for true color BMP images; IndexColorModel, for all other representations. ### Constructing the Image The image data itself is stored differently depending on the number of bits per pixel and whether the data is compressed. BMPImage will only support uncompressed 4 and 8 bits per pixel, though it can easily be extended to support all other modes. All modes store the image in rows from the bottom of the image to the top. Yes, this means that the image is stored upside down. For 8 bits per pixel, each row is stored as single bytes and padded to a 4-byte boundary. The following code block extracts uncompressed, 8-bits-per-pixel images: `/** * A private method for extracting 8 bit per pixel * image data. * @param is contains the input stream */ private void extract8BitData( DataInputStream is ) throws IOException { int index; if ( biCompression == 0 ) { int padding = 0; int overage = biWidth % 4; if ( overage != 0 ) padding = 4 - overage; pix = new int[biHeight * biWidth]; for ( int y = biHeight - 1; y >= 0; y- ) { index = y * biWidth; for ( int x = 0; x < biWidth; x++ ) { pix[index++] = is.readUnsignedByte(); } if ( padding != 0 ) is.skipBytes(padding); } } else { } }` Storage for 4 bits per pixel is similar to 8 bits per pixel, except the data is stored two per byte. The next code block extracts 4 bits per pixel data: `private void extract4BitData( DataInputStream is ) throws IOException { int index, temp = 0; if ( biCompression == 0 ) { int padding = 0; int overage = ((biWidth + 1)/ 2) % 4; if ( overage != 0 ) padding = 4 - overage; pix = new int[biHeight * biWidth]; for ( int y = biHeight - 1; y >= 0; y-- ) { index = y * biWidth; for ( int x = 0; x < biWidth; x++ ) { // if on an even byte, read new 8 bit quantity // use low nibble of previous read for odd bytes if ( (x % 2) == 0 ) { temp = is.readUnsignedByte(); pix[index++] = temp >> 4; } else pix[index++] = temp & 0x0f; } if ( padding != 0 ) is.skipBytes(padding); } } else { throw new IOException("Compressed images not supported"); } }` The real complication occurs when figuring the padding bytes. If the rows have an odd number of columns, then the last pixel will take up an entire byte. To accommodate this, the width is bumped up by one before it is divided by two. This will force odd-numbered columns to yield the correct number of bytes; even-numbered columns are unaffected (see the following code lines for an example): `11 columns: 11 / 2 = 5 [incorrect], (11 + 1) / 2 = 6 [correct] 12 columns: 12 / 2 = 6 [correct], (12 + 1) / 2 = 6 [correct]` Listing 7.1 displays the entire BmpImage class. At the bottom of the listing, you will see a static main function; it was added to allow testing of the class. This function allows the class to be invoked from the command line as follows: `java BmpImage Forest.bmp` Although the image won't be rendered, the entire image will be processed, and all the header contents will be displayed to the screen. In addition, any exceptions thrown during image extraction will be displayed. Listing 7.1. The BMP display class. `import java.io.*; import java.net.*; import java.awt.*; import java.awt.image.*; public class BmpImage { String bfName; boolean imageProcessed; boolean windowsStyle; ColorModel colorModel = null; int pix[]; byte bfType[]; int bfSize; int bfOffset; int biSize; int biWidth; int biHeight; int biPlanes; int biBitCount; int biCompression; int biSizeImage; int biXPelsPerMeter; int biYPelsPerMeter; int biClrUsed; int biClrImportant; public BmpImage(String name) { bfName = name; bfType = new byte[2]; imageProcessed = false; } /** * A private method for extracting little endian * quantities from a input stream. * @param is contains the input stream * @param len is the number of bytes in the quantity * @returns the result as an integer */ private int pullVal(DataInputStream is, int len) throws IOException { int value = 0; int temp = 0; for ( int x = 0; x < len; x++ ) { temp = is.readUnsignedByte(); value += (temp << (x * 8)); } return value; } /** * A private method for extracting the file header * portion of a BMP file. * @param is contains the input stream */ private void extractFileHeader(DataInputStream is) throws IOException, AWTException { is.read(bfType); if ( bfType[0] != 'B' || bfType[1] != 'M' ) throw new AWTException("Not BMP format"); bfSize = pullVal(is, 4); is.skipBytes(4); bfOffset = pullVal(is, 4); } /** * A private method for extracting the color table from * a BMP type file. * @param is contains the input stream * @param numColors contains the biClrUsed (for Windows) or zero */ private void extractColorMap(DataInputStream is, int numColors) throws IOException, AWTException { byte blues[], reds[], greens[]; // if passed count is zero, then determine the // number of entries from bits per pixel. if ( numColors == 0 ) { switch ( biBitCount ) { case 1: numColors = 2; break; case 4: numColors = 16; break; case 8: numColors = 256; break; case 24: numColors = 0; break; default: numColors = -1; break; } } if ( numColors == -1 ) throw new AWTException("Invalid bits per pixel: " + biBitCount); else if ( numColors == 0 ) colorModel = new DirectColorModel(24, 255 * 3, 255 * 2, 255); else { reds = new byte[numColors]; blues = new byte[numColors]; greens = new byte[numColors]; for ( int x = 0; x < numColors; x++ ) { blues[x] = is.readByte(); greens[x] = is.readByte(); reds[x] = is.readByte(); if ( windowsStyle ) is.skipBytes(1); } colorModel = new IndexColorModel( biBitCount, numColors, reds, greens, blues ); } } /** * A private method for extracting an OS/2 style * bitmap header. * @param is contains the input stream */ private void extractOS2Style(DataInputStream is) throws IOException, AWTException { windowsStyle = false; biWidth = pullVal(is, 2); biHeight = pullVal(is, 2); biPlanes = pullVal(is, 2); biBitCount = pullVal(is, 2); extractColorMap(is, 0); } /** * A private method for extracting a Windows style * bitmap header. * @param is contains the input stream */ private void extractWindowsStyle(DataInputStream is) throws IOException, AWTException { windowsStyle = true; biWidth = pullVal(is, 4); biHeight = pullVal(is, 4); biPlanes = pullVal(is, 2); biBitCount = pullVal(is, 2); biCompression = pullVal(is, 4); biSizeImage = pullVal(is, 4); biXPelsPerMeter = pullVal(is, 4); biYPelsPerMeter = pullVal(is, 4); biClrUsed = pullVal(is, 4); biClrImportant = pullVal(is, 4); extractColorMap(is, biClrUsed); } /** * A private method for extracting the bitmap header. * This method determines the header type (OS/2 or Windows) * and calls the appropriate routine. * @param is contains the input stream */ private void extractBitmapHeader(DataInputStream is) throws IOException, AWTException { biSize = pullVal(is, 4); if ( biSize == 12 ) extractOS2Style(is); else extractWindowsStyle(is); } /** * A private method for extracting 4 bit per pixel * image data. * @param is contains the input stream */ private void extract4BitData( DataInputStream is ) throws IOException { int index, temp = 0; if ( biCompression == 0 ) { int padding = 0; int overage = ((biWidth + 1)/ 2) % 4; if ( overage != 0 ) padding = 4 - overage; pix = new int[biHeight * biWidth]; for ( int y = biHeight - 1; y >= 0; y-- ) { index = y * biWidth; for ( int x = 0; x < biWidth; x++ ) { // if on an even byte, read new 8 bit quantity // use low nibble of previous read for odd bytes if ( (x % 2) == 0 ) { temp = is.readUnsignedByte(); pix[index++] = temp >> 4; } else pix[index++] = temp & 0x0f; } if ( padding != 0 ) is.skipBytes(padding); } } else { throw new IOException("Compressed images not supported"); } } /** * A private method for extracting 8 bit per pixel * image data. * @param is contains the input stream */ private void extract8BitData( DataInputStream is ) throws IOException { int index; if ( biCompression == 0 ) { int padding = 0; int overage = biWidth % 4; if ( overage != 0 ) padding = 4 - overage; pix = new int[biHeight * biWidth]; for ( int y = biHeight - 1; y >= 0; y-- ) { index = y * biWidth; for ( int x = 0; x < biWidth; x++ ) { pix[index++] = is.readUnsignedByte(); } if ( padding != 0 ) is.skipBytes(padding); } } else { throw new IOException("Compressed images not supported"); } } /** * A private method for extracting the image data from * a input stream. * @param is contains the input stream */ private void extractImageData( DataInputStream is ) throws IOException, AWTException { switch ( biBitCount ) { case 1: throw new AWTException("Unhandled bits/pixel: " + biBitCount); case 4: extract4BitData(is); break; case 8: extract8BitData(is); break; case 24: throw new AWTException("Unhandled bits/pixel: " + biBitCount); default: throw new AWTException("Invalid bits per pixel: " + biBitCount); } } /** * Given an input stream, create an ImageProducer from * the BMP info contained in the stream. * @param is contains the input stream to use * @returns the ImageProducer */ public ImageProducer extractImage( DataInputStream is ) throws AWTException { MemoryImageSource img = null; try { extractFileHeader(is); extractBitmapHeader(is); extractImageData(is); img = new MemoryImageSource( biWidth, biHeight, colorModel, pix, 0, biWidth ); imageProcessed = true; } catch (IOException ioe ) { throw new AWTException(ioe.toString()); } return img; } /** * Describe the image as a string */ public String toString() { StringBuffer buf = new StringBuffer(""); if ( imageProcessed ) { buf.append(" name: " + bfName + "\n"); buf.append(" size: " + bfSize + "\n"); buf.append(" img offset: " + bfOffset + "\n"); buf.append("header size: " + biSize + "\n"); buf.append(" width: " + biWidth + "\n"); buf.append(" height: " + biHeight + "\n"); buf.append(" clr planes: " + biPlanes + "\n"); buf.append(" bits/pixel: " + biBitCount + "\n"); if ( windowsStyle ) { buf.append("compression: " + biCompression + "\n"); buf.append(" image size: " + biSizeImage + "\n"); buf.append("Xpels/meter: " + biXPelsPerMeter + "\n"); buf.append("Ypels/meter: " + biYPelsPerMeter + "\n"); buf.append("colors used: " + biClrUsed + "\n"); buf.append("primary clr: " + biClrImportant + "\n"); } } else buf.append("Image not read yet."); return buf.toString(); } /** * A method to retrieve an ImageProducer for a BMP URL. * @param context contains the base URL (from getCodeBase() or such) * @param name contains the file name. * @returns an ImageProducer * @exception AWTException on stream or bitmap data errors */ public static ImageProducer getImageProducer( URL context, String name ) throws AWTException { InputStream is = null; ImageProducer img = null; try { BmpImage im = new BmpImage(name); is = new URL(context, name).openStream(); DataInputStream input = new DataInputStream( new BufferedInputStream(is) ); img = im.extractImage(input); } catch (MalformedURLException me) { throw new AWTException(me.toString()); } catch (IOException ioe) { throw new AWTException(ioe.toString()); } return img; } /** * A method to retrieve an ImageProducer given just a BMP URL. * @param context contains the base URL (from getCodeBase() or such) * @returns an ImageProducer * @exception AWTException on stream or bitmap data errors */ public static ImageProducer getImageProducer( URL context) throws AWTException { InputStream is = null; ImageProducer img = null; String name = context.toString(); int index; // Make last part of URL the name if ((index = name.lastIndexOf('/')) >= 0) name = name.substring(index + 1); try { BmpImage im = new BmpImage(name); is = context.openStream(); DataInputStream input = new DataInputStream( new BufferedInputStream(is) ); img = im.extractImage(input); } catch (MalformedURLException me) { throw new AWTException(me.toString()); } catch (IOException ioe) { throw new AWTException(ioe.toString()); } return img; } /** * A public test routine (you must pass the filename as the 1st arg) */ public static void main( String args[] ) { try { FileInputStream inFile = new FileInputStream(args[0]); DataInputStream is = new DataInputStream( new BufferedInputStream(inFile) ); BmpImage im = new BmpImage(args[0]); ImageProducer img = im.extractImage(is); System.out.println("Output:\n" + im); } catch ( Exception e ) { System.out.println(e); } }` } This class supports both Windows and OS/2 format bitmaps. OS/2 image data is identical to Windows image data, though OS/2 supports only uncompressed formats. The concepts used to create the BMP image can also be applied to other image formats. The steps can be boiled down to the following list: Acquire the input stream for the image data. Parse the image data to extract and create a ColorModel. Extract the image data into an array that conforms to the ColorModel previously created. Use the model and data to create a MemoryImageSource object that acts as the ImageProducer for the image. Listing 7.2 shows a simple applet that uses BmpImage to display a bitmap. Listing 7.2. The SimpleBmp applet used to exercise the BmpImage class. `import java.applet.*; import java.awt.*; import java.awt.image.*; import BmpImage; public class SimpleBmp extends Applet { private boolean init = false; Image myImage = null; /** * Standard initialization method for an applet */ public void init() { ImageProducer producer; if ( init == false ) { init = true; try { producer = BmpImage.getImageProducer(getCodeBase(), "Forest.bmp"); myImage = createImage(producer); } catch (AWTException ae) { System.out.println(ae.toString()); } } } /** * Standard paint routine for an applet. * @param g contains the Graphics class to use for painting */ public void paint(Graphics g) { g.drawImage(myImage, 0, 0, this); } }` ## Summary This chapter covers Java image concepts, including loading and display. Remember the image producer/consumer model; you'll see it recur whenever you deal with images. The Java color models-direct and indexed-are also important concepts. The producer/consumer and color models combine to enable you to render an almost infinite number of image types and formats. The chapter ends by demonstrating a class for reading and displaying image formats that Java doesn't directly support. Chapter 8, "Adding Threads to Applets," will explore image loading and tracking in more depth, so you can make use of the material covered in this chapter. Contact reference@developer.com with questions or comments. Copyright 1998 EarthWeb Inc., All rights reserved. PLEASE READ THE ACCEPTABLE USAGE STATEMENT. Copyright 1998 Macmillan Computer Publishing. All rights reserved. |