One of the most compelling features of Java is its wide support for the presentation of graphical information. Along with providing a simple means of displaying static images, Java enables developers to manipulate and animate images in ways previously impossible in Web content. At the heart of Java graphics and imaging are Java color models. This chapter takes a close look at what a color model is, along with how color models impact image handling and Java graphics in general.
In this chapter, you also learn about Java image filters and how they are used to manipulate graphical images. Image filtering is a powerful feature of Java that is tightly linked to color models. Java provides a variety of image filter classes that interact together to form a framework for easily filtering graphical images. You can extend the standard Java image filtering classes and build your own image filters to perform just about any type of image processing you can imagine. You learn how to implement your own image filters near the end of this chapter.
Together, color models and image filters form an integral part of the advanced Java graphics API. By the end of this chapter, you will be well on your way to becoming a Java graphics wizard!
Before jumping into the specifics of what a color model is and how it works in Java, it's important to understand how color is represented on a computer in general. Although most operating systems have some degree of platform-dependent handling of color, they all share a common approach to the general representation of colors. Knowing that all data in a computer is ultimately stored in a binary form, it stands to reason that physical colors are somehow mapped to binary values (numbers) in the computer domain. The question is, how are colors mapped to numbers?
One way to come up with numeric representations of colors would be to start at one end of the color spectrum and assign numbers to each color until you reach the other end. This approach solves the problem of representing a color as a number, but it doesn't provide any way to handle the mixing of colors. As anyone who has experienced the joy of Play-Doh can tell you, colors react in different ways when combined with each other. The way colors mix to form other colors goes back to physics, which is a little beyond this discussion. A computer color system needs to be able to handle mixing colors with accurate, predictable results.
The best place to look for a solution to the color problem is a color computer monitor. A color monitor has three electron guns: red, green, and blue. The output from these three guns converge on each pixel of the screen, exciting phosphors to produce the appropriate color (see Figure 12.1). The combined intensities of each gun determine the resulting pixel color. This convergence of different colors from the monitor guns is very similar to the convergence of different colored Play-Doh The primary difference is that monitors use only these three colors (red, green, and blue) to come up with every possible color that can be represented on a computer. (Actually, the biggest difference is that Play-Doh can't display high-resolution computer graphics, but that's another discussion.)
Figure 12.1 : Electron guns in a color monitor converging to create a unique color.
Knowing that monitors form unique colors by using varying intensities of the colors red, green, and blue, you might be thinking that a good solution to the color problem would be to provide an intensity value for each of these primary colors. This is exactly how computers model color. Computers represent different colors by combining the numeric intensities of the primary colors red, green, and blue. This color system is known as RGB (Red Green Blue) and is fully supported by Java.
Although RGB is the most popular computer color system in use, there are others. Another popular color system is HSB, which stands for Hue Saturation Brightness. In this system, colors are defined by varying degrees of hue, saturation, and brightness. The HSB color system is also supported by Java.
Bitmapped computer images are composed of pixels that describe the colors at each location of an image. Each pixel in an image has a unique color that is usually described using the RGB color system. Java provides support for working with 32-bit images, which means that each pixel in an image is described as using 32 bits. The red, green, and blue components of a pixel's color are stored in these 32 bits, along with an alpha component. The alpha component of a pixel refers to the transparency or opaqueness of the pixel.
A 32-bit Java image pixel is therefore composed of red, green, blue, and alpha components. By default, these four components are packed into a 32-bit pixel value, as shown in Figure 12.2. Notice that each component is described by 8 bits, yielding possible values between 0 and 255 for each. These components are packed into the 32-bit pixel value from high-order bits to low-order bits in the following order: alpha, red, green, and blue. It is possible for the pixel components to be packed differently, but this is the default pixel storage method used in Java.
Figure 12.2 : The four components of a pixel in a 32-bit Java image.
A color component value of 0 means the component is absent, and a value of 255 means it is maxed out. If all three color components are 0, the resulting pixel color is black. Likewise, if all three components are 255, the color is white. If the red component is 255 and the others are 0, the resulting color is pure red.
The alpha component describes the transparency of a pixel, independent of the color components. An alpha value of 0 means a pixel is completely transparent (invisible), and an alpha value of 255 means a pixel is completely opaque. Values between 0 and 255 enable the background color to show through a pixel in varying degrees.
The color components of a Java image are encapsulated in a simple class called Color. The Color class is a member of the AWT package and represents the three primary color components: red, green, and blue. This class is useful because it provides a clean abstraction for representing color, along with useful methods for extracting and modifying the primary components. The Color class also contains predefined constant members representing many popular colors.
In Java, pixel colors are managed through color models. Java color models provide an important abstraction that enables Java to work with images of different formats in a similar fashion. More specifically, a color model is a Java object that provides methods for translating from pixel values to the corresponding red, green, and blue color components of an image. At first, this may seem like a trivial chore, knowing that pixel color components are packed neatly into a 32-bit value. However, there are different types of color models reflecting different methods of maintaining pixel colors. The two types of color models supported by Java are direct color models and index color models.
Direct color models are based on the earlier description of pixels, where each pixel contains specific color and alpha components. Direct color models provide methods for translating these types of pixels into their corresponding color and alpha components. Typically, direct color models extract the appropriate components from the 32-bit pixel value using bit masks.
Index color models work differently from direct color models. In fact, index color models work with pixels containing completely different information than you've learned thus far. Pixels in an image using an index color model don't contain the alpha and RGB components like the pixels used in a direct color model. An index color model pixel contains an index into an array of fixed colors (see Figure 12.3). This array of colors is called a color map.
Figure 12.3 : An index color model pixel and its associated color map.
An example of an image that uses an index color model is a 256-color image. 256-color images use 8 bits to describe each pixel, which doesn't leave much room for RGB components. Rather than try to cram these components into 8 bits, 256-color pixels store an 8-bit index in a color map. The color map has 256 color entries that each contain RGB and alpha values describing a particular color.
Index color models provide methods for resolving pixels containing color map indices into alpha, red, green, and blue components. Index color models handle looking up the index of a pixel in the color map and extracting the appropriate components from the color entry.
Index color models provide an additional feature not available in direct color models: support for a transparent pixel color. Using an index color model, you can specify a color in the color map as the transparent color for the image. When the image is drawn, pixels having the transparent color are left out. The background shows through these pixels, effectively resulting in the pixels being completely transparent.
The transparency feature is very useful when working with images that have an irregular shape. All images are stored as rectangles and typically are drawn in a rectangular region. By using a transparent color to define the region around the irregular shape, the image can be drawn on a background without erasing a rectangular area of the background. Figure 12.4 shows the difference between images drawn with and without a transparent color.
Figure 12.4 : The effects of using a transparency color to draw an image.
Java provides standard classes for working with color models. At the top of the hierarchy is the ColorModel class, which defines the core functionality required of all color models. Two other classes are derived from ColorModel, representing the two types of color models supported by Java: DirectColorModel and IndexColorModel.
The ColorModel class is an abstract class containing the basic support required to translate pixel values into alpha and color components. ColorModel contains the following methods:
The ColorModel method is the only creation method defined for the ColorModel class. It takes a single integer parameter that specifies the pixel width of the color model in bits.
The getRGBdefault method is a class method that returns a ColorModel object based on the default RGB pixel component storage, as described earlier in this chapter (0xAARRGGBB).
The getPixelSize method returns the current pixel width of the color model. For example, the default color model would return 32 as the number of bits used to represent each pixel. The following piece of code shows how you can check this yourself:
System.out.println(ColorModel.getRGBdefault().getPixelSize());
The four methods that get each different pixel component are all defined as abstract. This means that a derived color model class must provide the specific implementation for these methods. The reason for this goes back to the issue of supporting different types of color models. Getting the color components of a pixel is completely dependent on how each pixel represents colors in an image, which is determined by the color model. For direct color models, you can extract the components by simply masking out the correct 8-bit values. For an index color model, however, you have to use each pixel's value as an index into a color map and then retrieve the components from there. In keeping with the object-oriented structure of Java, the ColorModel class provides the method descriptions but leaves the specific implementations to more specific color model classes.
The last method in the ColorModel class is getRGB, which returns the color of a pixel using the default color model. You can use this method to get a pixel value in the default RGB color model format.
The DirectColorModel class is derived from ColorModel and provides specific support for direct color models. If you recall, pixels in a direct color model directly contain the alpha and color components in each pixel value. DirectColorModel provides the following methods:
The first two methods are the creation methods for DirectColorModel.
The first creation method takes the pixel width of the color model,
along with the masks used to specify how each color component
is packed into the pixel bits. You may have noticed that there
is no mask parameter for the alpha component. Using this creation
method, the alpha component is forced to a value of 255, or fully
opaque. This is useful if you don't want any alpha information
to be represented. The second creation method is just like the
first, with the exception that it lets you specify an alpha mask.
Note |
A mask is used to extract specific bits out of an integer value. The bits are extracted by bitwise ANDing the mask with the value. Masks themselves are integers and are typically specified in hexadecimal. For example, to mask out the high-order word of a 32-bit value, you use the mask 0xFFFF0000. |
If you're a little shaky with masks, think about the masks for
the default pixel component packing. Remember, the components
are packed from high-order to low-order in the following order:
alpha, red, green, and blue. Each component is eight bits, so
the mask for each component extracts the appropriate byte out
of the 32-bit pixel value. Table 12.1 shows the default RGB pixel
component masks.
With DirectColorModel, there are four methods that simply return the pixel component masks: getRedMask, getGreenMask, getBlueMask, and getAlphaMask. Notice that these methods are defined as final, meaning that they cannot be overridden in a derived class. The reason for this is that the underlying native Java graphics code is dependent on this specific implementation of these methods. You'll notice that this is a common theme in the color model classes.
The four abstract methods defined in ColorModel are implemented in DirectColorModel: getRed, getGreen, getBlue, and getAlpha. These methods return the appropriate component values of a pixel in the range 0 to 255. Like the mask methods, these methods are also defined as final, so that no derived classes can override their behavior.
The getRGB method returns the color of a pixel in the default color model format. This method is no different than the one implemented in ColorModel.
The IndexColorModel class is also derived from ColorModel and provides support for index color models. Recall from the earlier discussion of color models that pixels in an index color model contain indices into a fixed array of colors known as a color map. The IndexColorModel class provides the following methods:
The first five methods are the creation methods for the IndexColorModel class. They look kind of messy with all those parameters, but they really aren't that bad. First, all the creation methods take as their first parameter the width of each pixel in bits. They all also take as their second parameter the size of the color map array to be used by the color model.
In addition to the pixel width and color map array size, the first three creation methods also take three byte arrays containing the red, green, and blue components of each entry in the color map. These arrays should all be the same length, which should match the color map size passed in as the second parameter. The second creation method enables you to specify the array index of the transparent color. The third creation method enables you to specify an array of alpha values to go along with the color component arrays.
Rather than using parallel arrays of individual component values, the last two creation methods take a single array of "packed" pixel component values-the color components are stored sequentially in a single array instead of in separate parallel arrays. The start parameter specifies the index to begin including colors from the array. The hasalpha parameter specifies whether the colors in the array include alpha information. The only difference between these two methods is that the second version enables you to specify the array index for the transparent color.
The getMapSize method returns the size of the color map used by the color model.
The getTransparentPixel method returns the array index of the transparent pixel color, if it is defined. Otherwise, getTransparentPixel returns -1.
There are four methods for getting the color values from the color map: getReds, getGreens, getBlues, and getAlphas. Each method takes an array of bytes as the only parameter and fills it with the color map values for the appropriate pixel component. These methods are final, so you can't override them in a derived class.
IndexColorModel provides implementations for the four abstract methods defined in ColorModel: getRed, getGreen, getBlue, and getAlpha. These methods return the appropriate component values of a pixel in the range 0-255. These methods are also defined as final, so derived classes aren't allowed to modify their behavior.
The getRGB method returns the color of a pixel in the default RGB color model format. Because the default color model is a direct color model, this method effectively converts an index color to a direct color.
Okay, so you know all about color models and the Java classes that bring them to life. Now what? Most of the time they act behind the scenes. It is fairly rare that you will need to create or manipulate a color model directly.
Color models are used extensively in the internal implementations of the various image processing classes, however. What does this mean to you, the ever-practical Java programmer? It means that you now know a great deal about the internal workings of color in the Java graphics system. Without fully understanding color models and how they work, you would no doubt run into difficulties when trying to work with the advanced graphics and image processing classes provided by Java.
Take a look at the Gradient sample program in Figure 12.5. The Gradient sample program uses an IndexColorModel object with 32 varying shades of green. It creates an image based on this color model and sets the image pixels to a horizontal gradient pattern. The complete source code for this program is shown in Listing 12.1. It is also included on the CD-ROM in the file Gradient.java.
Figure 12.5 : The Gradient sample program.
Listing 12.1. The Gradient sample program.
// Gradient Class
// Gradient.java
// Imports
import java.applet.Applet;
import java.awt.*;
import java.awt.image.*;
public class Gradient extends Applet {
final int colors = 32;
final int width = 200;
final int height = 200;
Image img;
public void init() {
// Create the color map
byte[] rbmap = new byte[colors];
byte[] gmap = new byte[colors];
for (int i = 0; i < colors; i++)
gmap[i] = (byte)((i * 255) / (colors - 1));
// Create the color model
int bits = (int)Math.ceil(Math.log(colors) / Math.log(2));
IndexColorModel model = new IndexColorModel(bits, colors,
rbmap, gmap, rbmap);
// Create the pixels
int pixels[] = new int[width * height];
int index = 0;
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++)
pixels[index++] = (x * colors) / width;
// Create the image
img = createImage(new MemoryImageSource(width, height, model,
pixels, 0, width));
}
public void paint(Graphics g) {
g.drawImage(img, 0, 0, this);
}
}
The Gradient program starts off by declaring a few final member variables for determining the number of colors used in the index color model, along with the size of the image.
It then creates a color map by building two arrays for the color components. The first array, rbmap, is used for both the red and blue color components. The second array, gmap, is used for the green color component. The rbmap array is left in its initial state. Remember, Java automatically initializes all member variables to zero. The gmap array is initialized to equally spaced values between 0 and 255, dependent on the number of colors in the color map (as specified by colors). The arrays are set up this way because you want the color map to contain shades of green. This is accomplished by specifying non-zero values for the green component of the color map and leaving the red and blue components set to 0.
With the color map created, you are ready to create the index color model. The creation method for IndexColorModel requires you to specify the pixel width of the color model. The pixel width is simply how many bits are required for a pixel to store an index in the color map. Calculating the pixel width is a matter of determining how many bits (b) are necessary to index an array of (n) colors. I won't go into the details of where the equation comes from, but the following equation yields the desired result:
b = log(n) / log(2)
To understand the implications of this equation, think about the number of colors used in the program. The final member variable colors is set to 32, meaning that the color model contains 32 color entries. Each pixel needs to be able to distinguish between (or index) these 32 different entries. Using the previous equation, you'll find that 5 bits per pixel are enough to index an array of 32 colors. Likewise, 8 bits per pixel are required to index an array of 256 colors.
You may notice that the equation used in Gradient is a little different; it calls the ceil method as well as the log method, like this:
int bits = (int)Math.ceil(Math.log(colors) / Math.log(2));
The call to ceil is there to make sure there are enough bits in case the number of colors is set to a value that is not a power of 2. For example, what if you change colors to 45 instead of 32? The result of the original equation would be 5.49, but the .49 would be lost in the cast to a byte. The resulting 5 would not be enough bits per pixel to keep up with 45 colors. The trick is always to use the smallest whole number greater than or equal to the floating-point result before casting. This is exactly what the ceil method does.
With the IndexColorModel creation method, you pass in the newly calculated pixel width, the number of colors, and the three color component arrays. The zero-filled rbmap array is used for both the red and blue component arrays.
Now the color model is created and ready to go, but there is still some work to be done. A bitmap image based on an index color model is composed of pixels that reference the colors in the color map. To create an image, you simply build up an array of pixels with the length equal to the width times the height of the image. The pixel array for the new image is created and each pixel initialized using nested for loops. Each pixel is initialized using the following equation:
pixels[index++] = (x * colors) / width;
This equation results in an equal distribution of colors (gradient) horizontally across the image.
The image is actually created with a call to the createImage method. This method takes a MemoryImageSource object as its only parameter. The MemoryImageSource class uses an array of pixel values to build an image in memory. The creation method for MemoryImageSource takes the width, height, color model, pixel array, pixel array offset, and scan line width for the image. It's simply a matter of plugging in the information you've already created.
At this point, you have an image made up of gradient pixels that contain indices into a color model with 32 shades of green. Now the fun part-drawing the image! A simple call to drawImage in the paint method is all it takes.
A thriving area of software research and development is image processing. Most popular paint programs contain image processing features, such as sharpening or softening an image. Typically, image processing developers have to build complex libraries of routines for manipulating images. Java provides a simple, yet powerful framework for manipulating images. In Java, image processing objects are called image filters, and they serve as a way to abstract the filtering of an image without worrying about the details associated with the source or destination of the image data.
A Java image filter can be thought of quite literally as a filter into which all the data for an image must enter and exit on its way from a source to a destination. Take a look at Figure 12.6 to see how image data passes through an image filter.
Figure 12.6 : Image data passing through an image filter.
While passing through a filter, the individual pixels of an image can be altered in any way as determined by that filter. By design, image filters are structured to be self-contained components. The image filter model supported by Java is based on three logical components: an image producer, an image filter, and an image consumer. The image producer makes the raw pixel data for an image available. The image filter in turn filters this data. The resulting filtered image data is then passed on to the image consumer where it has usually been requested. Figure 12.7 shows how these three components interact with each other.
Figure 12.7 : The relationship between an image producer, an image filter, and an image consumer.
Breaking down the process of filtering images into these three components provides a very powerful object-oriented solution to a complex problem. Different types of image producers can be derived that are able to retrieve image data from a variety of sources. Likewise, filters can ignore the complexities associated with different image sources and focus on the details of manipulating the individual pixels of an image.
Java support for image filters is scattered across several classes and interfaces. You don't necessarily have to understand all these classes in detail to work with image filters, but it is important that you understand what functionality they provide and where they fit into the scheme of things. Here are the Java classes and interfaces that provide support for image filtering:
The ImageProducer interface provides the method descriptions necessary to extract image pixel data from Image objects. Classes implementing the ImageProducer interface provide implementations for these methods specific to the image sources they represent. For example, the MemoryImageSource class implements the ImageProducer interface and produces image pixels from an array of pixel values in memory.
The FilteredImageSource class implements the ImageProducer interface and produces filtered image data. The filtered image data produced is based on the image and the filter object passed in FilteredImageSource's creation method. FilteredImageSource provides a very easy way to apply image filters to Image objects.
The MemoryImageSource class implements the ImageProducer interface and produces image data based on an array of pixels in memory. This is very useful in cases where you need to build an Image object directly from data in memory. You used the MemoryImageSource class earlier in this chapter in the Gradient sample program.
The ImageConsumer interface provides method prototypes necessary for an object to retrieve image data from an image producer. Instantiated classes implementing the ImageConsumer interface are attached to an image producer object when they are interested in its image data. The image producer object delivers the image data by calling methods defined by the ImageConsumer interface.
The PixelGrabber class implements the ImageConsumer interface and provides a way of retrieving a subset of the pixels in an image. A PixelGrabber object can be created based on either an Image object or an object implementing the ImageProducer interface. The creation method for PixelGrabber enables you to specify a rectangular section of the image data to be grabbed. This image data is then delivered by the image producer to the PixelGrabber object.
The ImageFilter class provides the basic functionality of an image filter that operates on image data being delivered from an image producer to an image consumer. ImageFilter objects are specifically designed to be used in conjunction with FilteredImageSource objects.
The FilterImage class is implemented as a null filter, which means that it passes image data through unmodified. Nevertheless, it implements the overhead for processing the data in an image. The only thing missing is the actual modification of the pixel data, which is left up to derived filter classes. This is actually a very nice design, because it enables you to create new image filters by deriving from ImageFilter and overriding a few methods.
The ImageFilter class operates on an image using the color model defined by the image producer. The RGBImageFilter class, on the other hand, derives from ImageFilter and implements an image filter specific to the default RGB color model. RGBImageFilter provides the overhead necessary to process image data in a single method that converts pixels one at a time in the default RGB color model. This processing takes place in the default RGB color model regardless of the color model used by the image producer. Like ImageFilter, RGBImageFilter is meant to be used in conjunction with the FilteredImageSource image producer.
The seemingly strange thing about RGBImageFilter is that it is an abstract class, so you can't instantiate objects from it. It is abstract because of a single abstract method, filterRGB. The filterRGB method is used to convert a single input pixel to a single output pixel in the default RGB color model. filterRGB is the workhorse method that handles filtering the image data; each pixel in the image is sent through this method for processing. To create your own RGB image filters, all you must do is derive from RGBImageFilter and implement the filterRGB method. This is the technique you use later in this chapter to implement your own image filters.
The RGBImageFilter class contains a member variable that is very important in determining how it processes image data: canFilterIndexColorModel. The canFilterIndexColorModel member variable is a boolean that specifies whether the filterRGB method can be used to filter the color map entries of an image using an index color model. If this member variable is false, each pixel in the image is processed.
The CropImageFilter class is derived from ImageFilter and provides a means of extracting a rectangular region within an image. Like ImageFilter, the CropImageFilter class is designed to be used with the FilteredImageSource image producer.
You may be a little confused by CropImageFilter, because the PixelGrabber class mentioned earlier sounds very similar. It is important to understand the differences between these two classes because they perform very different functions. First, remember that PixelGrabber implements the ImageConsumer interface, so it functions as an image consumer. CropImageFilter, on the other hand, is an image filter. This means that PixelGrabber is used as a destination for image data, where CropImageFilter is applied to image data in transit. You use PixelGrabber to extract a region of an image to store in an array of pixels (the destination). You use CropImageFilter to extract a region of an image that is sent along to its destination (usually another Image object).
Although the standard Java image filter classes are powerful as a framework, they aren't that exciting to work with by themselves. Image filters don't really get interesting until you start implementing your own. Fortunately, the Java classes make it painfully simple to write your own image filters.
All the image filters you develop in this chapter are derived from RGBImageFilter, which enables you to filter images through a single method, filterRGB. It really is as easy as deriving your class from RGBImageFilter and implementing the filterRGB method.
Probably the simplest image filter imaginable is one that filters out the individual color components (red, green, and blue) of an image. The ColorFilter class does exactly that. Listing 12.2 contains the source code for the ColorFilter class. It is located on the CD-ROM in the file ColorFilter.java.
Listing 12.2. The ColorFilter class.
// Color Filter Class
// ColorFilter.java
// Imports
import java.awt.image.*;
class ColorFilter extends RGBImageFilter {
boolean red, green, blue;
public ColorFilter(boolean r, boolean g, boolean b) {
red = r;
green = g;
blue = b;
canFilterIndexColorModel = true;
}
public int filterRGB(int x, int y, int rgb) {
// Filter the colors
int r = red ? 0 : ((rgb >> 16) & 0xff);
int g = green ? 0 : ((rgb >> 8) & 0xff);
int b = blue ? 0 : ((rgb >> 0) & 0xff);
// Return the result
return (rgb & 0xff000000) | (r << 16) | (g << 8) | (b << 0);
}
}
The ColorFilter class is derived from RGBImageFilter and contains three boolean member variables that determine which colors are to be filtered out of the image. These member variables are set by the parameters passed into the creation method. The member variable inherited from RGBImageFilter, canFilterIndexColorModel, is set to true to indicate that the color map entries can be filtered using filterRGB if the incoming image is using an index color model.
Beyond the creation method, ColorFilter implements only one method, filterRGB, which is the abstract method inherited from RGBImageFilter. filterRGB takes three parameters: the x and y position of the pixel within the image, and the 32-bit (integer) color value. The only parameter you are concerned with is the color value (rgb).
Recalling that the default RGB color model places the red, green, and blue components in the lower 24 bits of the 32-bit color value, it is easy to extract each one by shifting out of the rgb parameter. These individual components are stored in the local variables r, g, and b. Notice, however, that each color component is shifted only if it is not being filtered. For filtered colors, the color component is set to zero.
The new color components are shifted back into a 32-bit color value and returned from filterRGB. Notice that care is taken to ensure that the alpha component of the color value is not altered. The 0xff000000 mask takes care of this, because the alpha component resides in the upper byte of the color value.
Congratulations, you've written your first image filter! You have two more to go before you plug them all into a test program.
It isn't always apparent to programmers how the alpha value stored in the color value for each pixel impacts an image. Remember, the alpha component specifies the transparency or opaqueness of a pixel. By altering the alpha values for an entire image, you can make it appear to fade in and out. This works because the alpha values range from totally transparent (invisible) to totally opaque.
The AlphaFilter class filters the alpha components of an image based on the alpha level you supply in its creation method. Listing 12.3 contains the source code for the AlphaFilter class. It is located on the CD-ROM in the file AlphaFilter.java.
Listing 12.3. The AlphaFilter class.
// Alpha Filter Class
// AlphaFilter.java
// Imports
import java.awt.image.*;
class AlphaFilter extends RGBImageFilter {
int alphaLevel;
public AlphaFilter(int alpha) {
alphaLevel = alpha;
canFilterIndexColorModel = true;
}
public int filterRGB(int x, int y, int rgb) {
// Adjust the alpha value
int alpha = (rgb >> 24) & 0xff;
alpha = (alpha * alphaLevel) / 255;
// Return the result
return ((rgb & 0x00ffffff) | (alpha << 24));
}
}
The AlphaFilter class contains a single member variable, alphaLevel, that keeps up with the alpha level to be applied to the image. This member variable is initialized in the creation method, as is the canFilterIndexModel member variable.
Similar to the ColorFilter class, the filterRGB method is the only other method implemented by AlphaFilter. The alpha component of the pixel is first extracted by shifting it into a local variable, alpha. This value is then scaled according to the alphaLevel member variable initialized in the creation method. The purpose of the scaling is to alter the alpha value based on its current value. If you were just to set the alpha component to the alpha level, you wouldn't be taking into account the original alpha component value.
The new alpha component is shifted back into the pixel color value and the result returned from filterRGB. Notice that the red, green, and blue components are preserved by using the 0x00ffffff mask.
So far, the image filters you've seen have been pretty simple. The last one you create is a little more complex, but it acts as a more interesting filter. The BrightnessFilter class implements an image filter that brightens or darkens an image based on a brightness percentage you provide in the creation method. Listing 12.4 contains the source code for the BrightnessFilter class. It is located on the CD-ROM in the file BrightnessFilter.java.
Listing 12.4. The BrightnessFilter class.
// Brightness Filter Class
// BrightnessFilter.java
// Imports
import java.awt.image.*;
class BrightnessFilter extends RGBImageFilter {
int brightness;
public BrightnessFilter(int b) {
brightness = b;
canFilterIndexColorModel = true;
}
public int filterRGB(int x, int y, int rgb) {
// Get the individual colors
int r = (rgb >> 16) & 0xff;
int g = (rgb >> 8) & 0xff;
int b = (rgb >> 0) & 0xff;
// Calculate the brightness
r += (brightness * r) / 100;
g += (brightness * g) / 100;
b += (brightness * b) / 100;
// Check the boundaries
r = Math.min(Math.max(0, r), 255);
g = Math.min(Math.max(0, g), 255);
b = Math.min(Math.max(0, b), 255);
// Return the result
return (rgb & 0xff000000) | (r << 16) | (g << 8) | (b << 0);
}
}
The BrightnessFilter class contains one member variable, brightness, that keeps track of the percentage to alter the brightness of the image. This member variable is set via the creation method, along with the canFilterIndexModel member variable. The brightness member variable can contain values in the range -100 to 100. A value of -100 means the image is darkened by 100 percent, and a value of 100 means the image is brightened by 100 percent. A value of 0 doesn't modify the image at all.
It should come as no surprise by now that filterRGB is the only other method implemented by BrightnessFilter. In filterRGB, the individual color components are first extracted into the local variables r, g, and b. The brightness effects are then calculated based on the brightness member variable. The new color components are then checked against the 0 and 255 boundaries and modified if necessary.
Finally, the new color components are shifted back into the pixel color value and returned from filterRGB. Hey, it's not that complicated after all!
You put in the time writing some of your own image filters, but you have yet to enjoy the fruit of your labors. It's time to plug the filters into a real Java applet and see how they work. Figure 12.8 shows the FilterTest applet busily at work filtering an image of a train.
Figure 12.8 : The Filter Test applet.
The FilterTest applet uses all three filters you've written to enable you to filter an image of a train. The R, G, and B keys on the keyboard change the different colors filtered by the color filter. The left and right arrow keys modify the alpha level for the alpha filter. The up and down arrow keys alter the brightness percentage used by the brightness filter. Finally, the Home key restores the image to its unfiltered state.
Listing 12.5 contains the source code for the FilterTest applet. The source code is located on the CD-ROM in the file FilterTest.java, along with an HTML file containing a link to the applet, Example1.html.
Listing 12.5. The FilterTest applet.
// Filter Test Class
// FilterTest.java
// Imports
import java.applet.Applet;
import java.awt.*;
import java.awt.image.*;
public class FilterTest extends Applet {
Image src, dst;
boolean red, green, blue;
final int alphaMax = 9;
int alphaLevel = alphaMax;
int brightness;
public void init() {
src = getImage(getDocumentBase(), "Res/ChooChoo.gif");
dst = src;
}
public void paint(Graphics g) {
g.drawImage(dst, 0, 0, this);
}
public boolean keyDown(Event evt, int key) {
switch (key) {
case Event.HOME:
red = false;
green = false;
blue = false;
alphaLevel = alphaMax;
brightness = 0;
break;
case Event.LEFT:
if (--alphaLevel < 0)
alphaLevel = 0;
break;
case Event.RIGHT:
if (++alphaLevel > alphaMax)
alphaLevel = alphaMax;
break;
case Event.UP:
brightness = Math.min(brightness + 10, 100);
break;
case Event.DOWN:
brightness = Math.max(-100, brightness - 10);
break;
case (int)'r':
case (int)'R':
red = !red;
break;
case (int)'g':
case (int)'G':
green = !green;
break;
case (int)'b':
case (int)'B':
blue = !blue;
break;
default:
return false;
}
filterImage();
return true;
}
void filterImage() {
dst = src;
// Apply the color filter
dst = createImage(new FilteredImageSource(dst.getSource(),
new ColorFilter(red, green, blue)));
// Apply the alpha filter
dst = createImage(new FilteredImageSource(dst.getSource(),
new AlphaFilter((alphaLevel * 255) / alphaMax)));
// Apply the brightness filter
dst = createImage(new FilteredImageSource(dst.getSource(),
new BrightnessFilter(brightness)));
// Redraw the image
repaint();
}
}
The FilterTest applet class contains member variables for keeping up with the source and destination images, along with member variables for maintaining the various filter parameters.
The first method implemented by FilterTest is init, which loads the image ChooChoo.gif into the src member variable. It also initializes the dst member variable to the same image.
The paint method is implemented next, and it simply consists of one call to the drawImage method, which draws the destination (filtered) Image object.
The keyDown method is implemented to handle keyboard events generated by the user. In this case, the keys used to control the image filters are handled in the switch statement. The corresponding member variables are altered according to the keys pressed. Notice the call to the filterImage method at the bottom of keyDown.
The filterImage method is where the actual filtering takes place; it applies each image filter to the image. The dst member variable is first initialized with the src member variable to restore the destination image to its original state. Each filter is then applied using a messy looking call to createImage. The only parameter to createImage is an ImageProducer object. In this case, you create a FilteredImageSource object to pass into createImage. The creation method for FilteredImageSource takes two parameters: an image producer and an image filter. The first parameter is an ImageProducer object for the source image, which is obtained using the getSource method for the image. The second parameter is an ImageFilter-derived object.
The color filter is first applied to the image by creating a ColorFilter object using the three boolean color value member variables. The alpha filter is applied by creating an AlphaFilter object using the alphaLevel member variable. Rather than allowing 255 different alpha levels, the alpha level is normalized to provide only 10 different alpha levels. This is evident in the equation using alphaMax, which is set to 9. Finally, the brightness filter is applied by creating a BrightnessFilter object and passing in the brightness member variable.
You covered a lot of territory in this chapter. You first learned about colors in general and then about the heart of advanced Java graphics, color models. After taking a good dose of color model theory, you saw color models in action in the Gradient sample program.
With color models under your belt, you moved on to image filters. The Java image filter classes provide a powerful framework for working with images without worrying about unnecessary details. You learned about the different classes that comprise Java's support for image filters. You then topped it off by writing three of your own image filters, along with an applet that put them to the test.
Above all, you learned in this chapter that Java is no slouch when it comes to advanced graphics and image processing. You also saw first hand how Java's support for color models and image filters is very useful and easy to work with. In the next chapter, you continue building your portfolio of Java graphics tricks by learning about the Java media tracker.