Chapter 7

Java and Images


CONTENTS


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():

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:

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:

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:

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.

NameMeaning
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:

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:

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:

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.

NameMeaning
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:

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:

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:

The mask values for Java's default RGB model are the following:

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:

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:

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
                                          Buff eredInputStream(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
                                          Buff eredInputStream(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:

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(),
                                          &nbs p;          "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.