|
Contents
Part
1 in this series on learning to use the Java 2D API discussed how
the Java 2D graphics engine -- represented by the java.awt.Graphics2D
class -- takes graphics primitive classes such as shapes, text, and
images and renders them to an output device, such as a screen
or a printer. This article discusses how to use the Java 2D API
libraries to manipulate and display images.
Working With Images
With the Abstract Window Toolkit (AWT) alone, the only way to
display an image is to use the java.awt.Image class.
However, this class does not allow you to access the image data directly.
In fact, the only methods that directly returned information about the
image in java.awt.Image were getHeight()
and getWidth(), but even then, there were limitations:
If the system had not yet loaded the image data, the values would be
erroneous, and you would have to use an instance of
java.awt.ImageObserver to be
notified when the data became available. If you wanted to manipulate
the image data in other ways, you were forced to use the inconvenient
producer-consumer model to inspect or manipulate the data as it was
decoded from its source.
Buffered Images
The java.awt.image.BufferedImage class, introduced as
part of the Java 2D API with the Java Development Kit (JDK) 1.2,
affords the programmer much more freedom to directly manipulate the
pixels inside an image. Compared to the producer-consumer model, this
class uses an immediate-mode imaging model from which you can inspect
and modify pixel data stored directly in memory. You can also access
image data in a variety of formats and use several types of filtering
operations to manipulate the data.
A BufferedImage object -- specifically the image
inside of it -- has two parts: a ColorModel object
and a Raster object that represents the image data. See
Figure 1.
 |
|
Figure 1. The BufferedImage Class
|
The ColorModel object provides an interpretation of
the image's pixel data within a color space. A color space is
essentially a collection of all the colors that can be shown on a
particular device. Computer monitors, for example, often define
their color space using the red-green-blue (RGB) color space. A
printer, on the other hand, may use a cyan-magenta-yellow-black
(CMYK, using the letter K for "black" rather than B for "blue")
color space. Images may use one of several subclasses of
ColorModel in the Java 2D API libraries:
- A
ComponentColorModel, in which a pixel is
represented by several discrete values, typically bytes, each
representing one component of color, such as the red component of an
RGB representation
- A
DirectColorModel,
in which all components of a color are packed together in separate
bits of the same single pixel value
- An
IndexColorModel, in which each pixel is a
single value representing an index into a palette of colors
The Raster object, on the other hand, stores the
actual pixel data for an image in a rectangular array addressed by
x-axis and y-axis (x and y) coordinates. It also provides a
mechanism for creating subimages from its image data buffer. The
Raster itself is composed of two parts:
A Raster also provides methods for accessing
specific pixels within the image.
Using a BufferedImage Object
To create a BufferedImage object, simply call one of
its constructors with the width, height, and an image-type constant.
BufferedImage image =
new BufferedImage(400, 400, BufferedImage.TYPE_INT_RGB);
|
For the image-type parameter, use one of the BufferedImage
constants shown in Table 1, which specifies how the image data is
stored for each of its pixels.
 |
TYPE_3BYTE_BGR
|
Blue, green, and red values stored, 1
byte each |
TYPE_4BYTE_ABGR
|
Alpha, blue, green, and
red values stored, 1 byte each |
TYPE_4BYTE_ABGR_PRE
|
Alpha and premultiplied blue, green, and red values stored, 1 byte
each |
TYPE_BYTE_BINARY
|
1 bit per pixel, 8 pixels to a
byte |
TYPE_BYTE_INDEXED
|
8-bit pixel value that
references a color index table |
TYPE_BYTE_GRAY
|
8-bit
gray value for each pixel |
TYPE_USHORT_555_RGB
|
5-bit
red, green, and blue values packed into 16 bits |
TYPE_USHORT_565_RGB
|
5-bit red and blue values, 6-bit green values packed into 16 bits |
TYPE_USHORT_GRAY
|
16-bit gray values for each pixel |
TYPE_INT_RGB
|
8-bit red, green, and blue values
stored in a 32-bit integer |
TYPE_INT_BGR
|
8-bit blue, green, and red pixel values stored in a 32-bit integer |
TYPE_INT_ARGB
|
8-bit alpha, red, green, and blue values stored in a 32-bit integer |
TYPE_INT_ARGB_PRE
|
8-bit alpha and premultiplied red,
green, and blue values stored in a 32-bit integer |
To draw into a BufferedImage, call the
createGraphics() method to obtain the Graphics2D
object that renders into the BufferedImage, then just
call the appropriate rendering methods on the Graphics2D
object. Note that you can use all of the Java 2D API rendering
features, including those discussed in the first article, when you're
rendering to a BufferedImage.
BufferedImage image =
new BufferedImage(400, 400, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = (Graphics2D)image.createGraphics();
g2.setFont(new Font("Serif", Font.PLAIN, 36));
g2.drawString("Hello BufferedImage", 50, 50);
|
Historically, you can create a BufferedImage from a jpeg
file using the com.sun.image.codec.jpeg.JPEGImageDecoder
class.
String filename = "myGraphic.jpg";
InputStream in = ClipImage.class.getResourceAsStream(filename);
JPEGImageDecoder decoder = JPEGDecoder.createJPEGDecoder(in);
final BufferedImage bufferedImage = decoder.decodeAsBufferedImage();
in.close();
|
However, if you're looking for a simpler route, you can use the
Image I/O libraries in
javax.imageio (JSR
15). The javax.imageio.ImageIO class provides a set
of static convenience methods that perform most simple Image I/O
operations. For example, to read an image that is in a standard
format (gif, png, or jpeg), do
the following:
File f = new File("c:\images\myimage.gif");
BufferedImage bufferedImage = ImageIO.read(f);
|
To write it back out, use the write() method of
javax.imageio.ImageIO. With this method, you can
convert one image type to another. In this case, we converted a
gif to a png.
File f = new File("c:\images\myimage.png");
ImageIO.write(bufferedImage, "png", f);
|
A BufferedImage can be rendered using the
drawImage()method of any Graphics or
Graphics2D objects. For example, you can render a
BufferedImage into a Component using the
Graphics object passed into its paint()
method.
public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D)g;
g2.drawImage(bufferedImage, 0, 0, null);
}
|
You may have noticed that there is an unnecessary line in the
previous code snippet. Isn't there already a
drawImage() method in the base Graphics
class? Yes, the drawImage() method invoked above also
exists on the base Graphics class, so it is really
unnecessary to cast the incoming Graphics object to a
Graphics2D. Because the object that is passed in is
really a Graphics2D object, the proper Java 2D method
will be called.
The easiest way to access specific pixel data of an image is to
use the getRGB() and setRGB() methods of
the BufferedImage class for the given x and y
coordinates:
int rgb = 3096;
int oldRGB = image.getRGB(250, 180);
image.setRGB(250, 180, rgb);
|
The setRGB() and getRGB() methods
accept and return a 32-bit color value in the same format and color
space as a non-premultiplied INT_RGB image.
int rgb = image.getRGB(x, y);
int alpha = ((rgb >> 24) & 0xff);
int red = ((rgb >> 16) & 0xff);
int green = ((rgb >> 8) & 0xff);
int blue = ((rgb ) & 0xff);
// Manipulate the r, g, b, and a values.
rgb = (a << 24) | (r << 16) | (g << 8) | b;
image.setRGB(x, y, rgb);
|
You can also directly manipulate the image data of the Raster
using its various accessor methods, but you must be familiar with the
operation of the ColorModel that it is associated with
since you are manipulating the pixel data directly. The lowest-level
and potentially most efficient way to access the image data would be
to use the methods on the DataBuffer of the Raster.
However, that requires knowledge of both the ColorModel
and SampleModel in use.
Filtering a BufferedImage Object
Often, a graphics programmer may wish to perform more complex
operations on BufferedImage objects than individually
manipulating pixel values. The Java 2D API defines several filtering
operations for BufferedImage objects that manipulate
large amounts of the image at the same time. Each of these
image-processing operations is represented by a class that implements
the BufferedImageOp interface. The image manipulation
itself is performed in this class's filter() method.
The Java 2D API supports the following implementations of the
BufferedImageOp interface:
- Affine transformation
- Amplitude scaling
- Modification of the look-up table
- Linear combination of bands
- Color conversion
- Convolution
Filtering a BufferedImage object using one of the
image operation classes is easy. First, construct an instance of one
of the BufferedImageOp classes:
AffineTransformOp, BandCombineOp,
ColorConvertOp, ConvolveOp,
LookupOp, or RescaleOp. Then, call the
image operation's filter() method, passing in the
BufferedImage object that you want to filter and the
BufferedImage where you want to store the results.
The following applet, Code Example 1, based on an example in the
Java 2D API documentation, illustrates the use of four
image-filtering operations:
- Convolution using a 3x3 blurring filter
- Convolution using a 3x3 sharpen filter
- A look-up operation
- A rescale operation
Code Example 1
import java.awt.*;
import java.io.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.geom.*;
import java.awt.font.*;
import javax.swing.*;
import javax.imageio.*;
public class ImageOps extends JApplet {
private BufferedImage bi[];
public static final float[] BLUR3x3 = {
0.1f, 0.1f, 0.1f,
0.1f, 0.2f, 0.1f,
0.1f, 0.1f, 0.1f };
public static final float[] SHARPEN3x3 = {
0.f, -1.f, 0.f,
-1.f, 5.f, -1.f,
0.f, -1.f, 0.f};
public void init() {
setBackground(Color.white);
// Load two images that we can use as examples for the
// image operations.
bi = new BufferedImage[4];
String s[] = { "bld.jpg", "bld.jpg", "boat.gif", "boat.gif"};
for ( int i = 0; i < bi.length; i++ ) {
File f = new File("C:/" + s[i]);
try {
// Read in a BufferedImage from a file.
BufferedImage bufferedImage = ImageIO.read(f);
// Convert the image to an RGB style normalized image.
bi[i] = new BufferedImage(bufferedImage.getWidth(),
bufferedImage.getHeight(), BufferedImage.TYPE_INT_RGB);
bi[i].getGraphics().drawImage(bufferedImage, 0, 0, this);
} catch (IOException e) {
System.err.println("Error reading file: " + f);
System.exit(1);
}
}
}
public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
int w = getSize().width;
int h = getSize().height;
// Set the color to black.
g2.setColor(Color.black);
// Create a low-pass filter and a sharpen filter.
float[][] data = {BLUR3x3, SHARPEN3x3};
String theDesc[] = { "Convolve LowPass",
"Convolve Sharpen",
"LookupOp",
"RescaleOp"};
// Cycle through each of the four BufferedImage objects.
for ( int i = 0; i < bi.length; i++ ) {
int iw = bi[i].getWidth(this);
int ih = bi[i].getHeight(this);
int x = 0, y = 0;
// Create a scaled transformation for the image.
AffineTransform at = new AffineTransform();
at.scale((w-14)/2.0/iw, (h-34)/2.0/ih);
BufferedImageOp biop = null;
BufferedImage bimg =
new BufferedImage(iw, ih, BufferedImage.TYPE_INT_RGB);
switch ( i ) {
// IMAGE 1 and 2: Create a convolution
// kernel that consists of either the low-pass filter
// or the sharpen filter. Set the x and y of the image
// so that it appears in the correct quadrant and has
// enough room for the descriptive text above.
case 0 :
case 1 : x = i==0?5:w/2+3; y = 15;
Kernel kernel = new Kernel(3, 3, data[i]);
ConvolveOp cop = new ConvolveOp(kernel,
ConvolveOp.EDGE_NO_OP,
null);
// Apply the convolution operation, placing the
// result in bimg.
cop.filter(bi[i], bimg);
// Create the appropriate AffineTransformation that
// will be used while drawing IMAGES 1 and 2
biop = new AffineTransformOp(at,
AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
break;
case 2 : x = 5; y = h/2+15;
// IMAGE 3:
// Create the parameters needed for a LookupOp, which
// process the color channels of an image using a
// look-up table. This will create a reverse brightness
// of the image, similar to a photographic negative.
byte chlut[] = new byte[256];
for ( int j=0;j<200 ;j++ )
chlut[j]=(byte)(256-j);
ByteLookupTable blut=new ByteLookupTable(0,chlut);
LookupOp lop = new LookupOp(blut, null);
lop.filter(bi[i], bimg);
// Create the appropriate AffineTransformation, which
// will be used while drawing the IMAGE 3.
biop = new AffineTransformOp(at,
AffineTransformOp.TYPE_BILINEAR);
break;
case 3 : x = w/2+3; y = h/2+15;
// IMAGE 4:
// Perform a rescaling operation, multiplying each
// pixel by a scaling factor (1.1), then adding an
// offset (20.0). Note that this has nothing to do
// with a geometric scaling of an image.
RescaleOp rop = new RescaleOp(1.1f,20.0f, null);
rop.filter(bi[i],bimg);
biop = new AffineTransformOp(at,
AffineTransformOp.TYPE_BILINEAR);
}
// Draw the image with the appropriate AffineTransform
// operation, as well as the text above it.
g2.drawImage(bimg,biop,x,y);
TextLayout tl = new TextLayout(theDesc[i],
g2.getFont(),g2.getFontRenderContext());
tl.draw(g2, (float) x, (float) y-4);
}
}
public static void main(String s[]) {
JFrame f = new JFrame("ImageOps");
f.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {System.exit(0);}
});
JApplet applet = new ImageOps();
f.getContentPane().add("Center", applet);
applet.init();
f.pack();
f.setSize(new Dimension(550,550));
f.setVisible(true);
}
}
|
 |
|
Figure 2. Image Operations
|
Note that both the blurring and sharpen filter operations are performed by using convolution. Convolution is the process of weighting or
averaging the value of each pixel in an image with the values of
neighboring pixels. Most spatial-filtering algorithms, including the
3x3 sharpening algorithm shown in Code Example 1, are based on
convolution operations.
Double Buffering
When a graphic is complex or is used repeatedly, you can reduce
the time it takes to display it by first rendering the image to an
offscreen buffer image and then copying the buffer image to the
screen. This technique, called double buffering, is often used
for animations. A BufferedImage can easily be used as an
offscreen buffer image. To create a BufferedImage whose
color space, depth, and pixel layout exactly match the window into
which you're drawing, call the Component createImage()
or the GraphicsConfiguration.createCompatibleImage()
method. If you need control over the offscreen image's type or
transparency, you can construct a BufferedImage object
directly.
When you're ready to copy the BufferedImage to
the screen, call the drawImage() method on your visible
component's Graphics object and pass in the
BufferedImage.
public void paint(Graphics g) {
g.drawImage(offscreenBuffer, 0, 0, null);
}
|
Volatile Images
Starting with Java 2 Platform, Standard Edition 1.4, the Java 2D
API provides access to hardware acceleration for offscreen images,
resulting in better performance when rendering to and copying from
these images. However, one problem with hardware-accelerated images
is that their contents can be lost at any time, often due to
circumstances beyond the application's control. The
java.awt.image.VolatileImage class helps to correct that
by allowing you to create a hardware-accelerated offscreen image and
to manage the contents of that image. For example, in many operating
systems, a VolatileImage object can be stored in VRAM
and can benefit from hardware acceleration.
Note that the memory where the image contents actually reside can
be lost or invalidated. Hence, the drawing surface needs to be
restored or recreated, and the contents of that surface need to be
rerendered. VolatileImage provides an interface for
allowing the user to detect these problems and fix them when they
occur.
Code Example 2 shows how to use a VolatileImage object.
Code Example 2
VolatileImage vImg = GraphicsConfiguration.
createCompatibleVolatileImage(w, h);
public void paint(Graphics gScreen) {
do {
int returnCode = vImg.validate(getGraphicsConfiguration());
if (returnCode == VolatileImage.IMAGE_RESTORED) {
// Contents need to be restored.
reRender();
} else if (returnCode==VolatileImage.IMAGE_INCOMPATIBLE) {
vImg = GraphicsConfiguration.
createCompatibleVolatileImage(w, h);
reRender();
}
gScreen.drawImage(vImg, 0, 0, this);
} while (vImg.contentsLost());
}
public void reRender() {
Graphics2D g2 = vImg.createGraphics();
// Miscellaneous rendering commands to restore
// the image
g2.dispose();
}
|
If you would like more information about using the VolatileImage class, check out Chet Haase's blog for an in-depth question-and-answer session.
Creating a Custom Button
At this point, we can apply what we've learned to create a user
interface (UI) button that blurs itself when it is not enabled. The
following code demonstrates how to override the BasicButtonUI
class to do just that. Note that you can also override the
component's paintComponent() method by subclassing a
custom JButton class to do the same thing. However, this
example also shows how to use the pluggable look and feel of Java
Foundation Classes/Swing (JFC/Swing), which is useful if you would
like to create more advanced effects, such as displaying
semitransparent menus and pop-ups.
First, create a class that overrides the BasicButtonUI
class in javax.swing.plaf.basic, which we will call
CustomButtonUI. The source code for this class appears
in Code Example 3. Note that it reuses the blurring convolution
filter from Code Example 1. We want to override two methods:
createUI(), which tells Swing to use our custom button
UI, and the paint() method, which Swing calls upon to
actually render our button.
Code Example 3
public class CustomButtonUI extends BasicButtonUI {
public static final float[] BLUR3x3 = {
0.1f, 0.1f, 0.1f,
0.1f, 0.2f, 0.1f,
0.1f, 0.1f, 0.1f };
public static ComponentUI createUI(JComponent c) {
return new CustomButtonUI();
}
public void paint(Graphics g, JComponent comp) {
Graphics2D panelG2 = (Graphics2D)g;
// Create a buffered image to hold the rendering
// of the component that is passed in.
BufferedImage image = new BufferedImage(
comp.getWidth(),
comp.getHeight(),
BufferedImage.TYPE_INT_ARGB);
// Draw the component onto the buffered image.
Graphics2D g2 = image.createGraphics();
g2.setColor(g.getColor());
super.paint(g2, comp);
// Draw the resulting buffered image onto the current
// Graphics context with the same blurring convolution
// kernel as in Code Example 1.
if (!comp.isEnabled()) {
Kernel kernel = new Kernel(3, 3, BLUR3x3);
ConvolveOp cop = new ConvolveOp(kernel,
ConvolveOp.EDGE_NO_OP,
null);
Image newImage = cop.filter(image, null);
panelG2.drawImage(newImage, 0, 0, null);
} else {
panelG2.drawImage(image, 0, 0, null);
}
}
}
|
Next, use the static UIManager.put() method in your
source code to indicate which class should be used for the button's
UI.
public class BlurredButton {
public static void main(String[] args) {
UIManager.put("ButtonUI", "CustomButtonUI");
JFrame frame = new JFrame("Button test");
frame.getContentPane().setBackground(Color.black);
JButton button = new JButton("Test Enabled");
frame.add(button, BorderLayout.NORTH);
JButton button2 = new JButton("Test Disabled");
button2.setEnabled(false);
frame.add(button2, BorderLayout.SOUTH);
frame.pack();
frame.setSize(200, 100);
frame.setVisible(true);
}
}
|
Figure 3 shows the result.
 |
|
Figure 3. The Blurring of a Disabled Button
|
Summary
This article helped you learn a little about how the Java 2D API
works with images using the BufferedImage class. You
learned how the BufferedImage class stores images and
how to manipulate images at both the pixel level and using filter
operations. You also learned how to use the VolatileImage
class to take advantage of hardware acceleration. Finally, we
discussed how to use these classes in a custom Swing component. In
the next article in this series, we will discuss how the Java 2D APIs
manipulate and render text.
For More Information
Read Learning
Java 2D, Part 1.
You can download
Java 2D API source code examples here.
See the Java
2D API home page.
Download the source code for the ImageOps and BlurredButton
examples as NetBeans IDE project files. If you're interested in only the source code,
you can obtain it from the src directory inside each project directory.
Be sure to follow Chet
Haase's blog for a number of Java 2D API hints.
Chris
Campbell's blog also has a number of interesting topics to glance
through.
The JavaDesktop
Community and the Java
Games Forums are great places to pick up tips and tricks on using
the Java 2D and other media APIs.
|
|