All Categories :
Java
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.
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.
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.
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.
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.
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. |
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.
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.
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.
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
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.
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.
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.
|
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.
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.
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.
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.
|
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.
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);
}
}
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.