Learning Java 2D, Part 2

   
By Robert Eckstein, October 27, 2005  
Contents
 
Working With Images
Buffered Images
Volatile Images
Creating a Custom Button
Summary
For More Information
 
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 <CODE>BufferedImage</CODE> Class
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 data buffer, which contains the raw image data
  • A sample model, which describes how the data is organized in the buffer

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.

Table 1. BufferedImage Color Models
 
 
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
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
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.

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.

Rate and Review
Tell us what you think of the content of this page.
Excellent   Good   Fair   Poor  
Comments:
Your email address (no reply is possible without an address):
Sun Privacy Policy

Note: We are not able to respond to all submitted comments.