Push Imaging Model

It is a misconception that the push model was poorly designed and is no longer useful. On the contrary, this model was designed to provide a simple way to load images into Applets and applications. The main advantage of this model is that it can load and display images incrementally as they become available over the network. Another advantage is that by using the push model, your images can be viewed by almost all browsers without the need for plug-ins to replace the browser's Java virtual machine (JVM). The main disadvantage of the push model is that the image data isn't collected into an accessible location, making anything more than simple image processing difficult. A second disadvantage is that the programming interface can be confusing, especially when you encounter it for the first time.

Images

Conventionally, an image can be thought of as a formatted collection of pixel values. In Java programming, this type of thinking can cause confusion. It is better to think of the java.awt.Image class as a group of resources and methods that provide a means for transferring and inquiring about a collection of image data and not as the collection itself. For example, let's look at the two code lines typically used to load an image into an Applet:

Image anImage = getImage(url);  //java.awt.Applet method
						

and

drawImage(anImage, xlocation, ylocation, this);
//java.awt.Graphics method where "this" is an
							ImageObserver
						

Note that for an application, the first line can be replaced by

Image anImage = Toolkit.getDefaultToolkit().getImage(url);

or

Image anImage = Toolkit.getDefaultToolkit().getImage(filename);

In all cases, the initial step doesn't start loading the image data, but instead instantiates an Image object that creates the resources necessary to process the image data. The second method begins the image loading, although the method returns immediately regardless of how much of the image is available to display. The reason for this is so that the executing thread can move on to other tasks while the image data is loading. The actual loading continues in a separate thread, where an object implementing the java.awt.ImageProducer interface sends image data to an object implementing the java.awt.ImageConsumer interface.

It is of interest to note that the three classes explicitly used to load the image data—that is, Applet, Graphics, and Image—don't implement any of these interfaces. Thus, the separate thread connecting the ImageProducer to the ImageConsumer is a bit mysterious in this context, although a reference to the ImageProducer can be obtained through the getSource method of the Image class. It should be noted that the drawImage method of the Graphics class isn't the only method that will start the image data loading. This is true of any method that requires information about the image data, such as the Image methods public int getWidth(ImageObserver) and public int getHeight(ImageObserver).

An important question at this point is how does the applet know how much data has been transferred to the ImageConsumer, where it is available for drawing? This task is left up to an object implementing the java.awt.ImageObserver interface. As the flow of data between the ImageProducer and the ImageConsumer progresses the

public boolean imageUpdate(Image img, int infoflags,
                           int x, int y, int width, int height)

method of the ImageObserver is called. Each time it is called, information is passed to it through an integer value representing a set of flags. This information can describe such things as whether the image width or image height is known, whether additional data bits have been loaded, or whether all the image data has been loaded. The trick to getting all this to work is to note that the java.awt.Component class implements the ImageObserver interface and, thus, defines this imageUpdate method. Therefore, the Component class and any descendent of it, such as the Applet class, is an ImageObserver. So, when you specify this as the ImageObserver, you are actually specifying that the Applet's imageUpdate method gets called when the ImageProducer sends data to the ImageConsumer. The default behavior of the Applet's imageUpdate method is to repaint the Applet whenever new data bits are available.

Listing 4.1 demonstrates the use of the Applet class as an ImageObserver. We have simply replaced the default imageUpdate method with one that is functionally similar, but much more verbose.

Listing 4.1 ImageLoaderApplet.java
package ch4;

import java.applet.*;
import java.awt.*;
import java.net.*;
import java.awt.image.ImageObserver;

/**
 * ImageLoaderApplet.java -- load and display image specified by imageURL
 */
public class ImageLoaderApplet extends Applet {
    private Image img;
    private String imageURLString = "file:images/peppers.png";

    public void init() {
        URL url;
        try {
            // set imageURL here
            url = new URL(imageURLString);
            img = getImage(url);
        }
        catch (MalformedURLException me) {
            showStatus("Malformed URL: " + me.getMessage());
        }
    }

    /**
     * overloaded method to prevent clearing drawing area
     */
    public void update(Graphics g) {
        paint(g);
    }

    public void paint(Graphics g) {
        g.drawImage(img, 0, 0, this);
    }

    /**
     * Verbose version of ImageConsumer's imageUpdate method
     */
    public boolean imageUpdate(Image img, int flags,
                               int x, int y, int width, int height) {
        System.out.print("Flag(s): ");
        if ( (flags & ImageObserver.WIDTH) != 0) {
            System.out.print("WIDTH:("+width+") ");
        }

        if ( (flags & ImageObserver.HEIGHT) != 0) {
            System.out.print("HEIGHT:("+height+") ");
        }

        if ( (flags & ImageObserver.PROPERTIES) != 0) {
            System.out.print("PROPERTIES ");
        }

        if ( (flags & ImageObserver.SOMEBITS) != 0) {
            System.out.print("SOMEBITS("+x+","+y+")->(");
            System.out.print(width+","+height+") ");
            repaint();
        }

        if ( (flags & ImageObserver.FRAMEBITS) != 0) {
            System.out.print("FRAMEBITS("+x+","+y+")->(");
            System.out.print(width+","+height+") ");
            repaint();
        }

        if ( (flags & ImageObserver.ALLBITS) != 0) {
            System.out.print("ALLBITS("+x+","+y+")->(");
            System.out.println(width+","+height+") ");
            repaint();
            return false;
        }

        if ( (flags & ImageObserver.ABORT) != 0) {
            System.out.println("ABORT 
");
            return false;
        }

        if ( (flags & ImageObserver.ERROR) != 0) {
            System.out.println("ERROR ");
            return false;
        }

        System.out.println();
        return true;
    }
}

If you were to run this Applet using the appletviewer or another browser with a defined standard output, the output would be similar to the following:

Flag(s): WIDTH:(256) HEIGHT:(256)
Flag(s): PROPERTIES
Flag(s): SOMEBITS(0,0)->(256,1)
Flag(s): SOMEBITS(0,1)->(256,1)
Flag(s): SOMEBITS(0,2)->(256,1)
. . .
Flag(s): SOMEBITS(0,253)->(256,1)
Flag(s): SOMEBITS(0,254)->(256,1)
Flag(s): SOMEBITS(0,255)->(256,1)
Flag(s): ALLBITS(0,0)->(256,256)

Note that the meaning of the arguments width and height change according to the set flags. For the WIDTH, HEIGHT, FRAMEBITS, and ALLBITS flags, the width and height represent the image dimensions. For the SOMEBITS flag, the width and height represent the dimensions of the block of data received by the ImageConsumer.

Before moving on to Image filtering, we will take a quick look at the methods belonging to the ImageProducers and the ImageConsumers. When you look at the methods of the ImageProducer, you can see that the two most important methods involve registering ImageConsumers and starting production of Image data—that is, public void addConsumer(ImageConsumer ic) and public void startProduction(ImageConsumer ic). When you look at the methods of the ImageConsumer, you find that they are meant to be called by the ImageProducer and that they correspond closely to the flags in the ImageObserver's imageUpdate method (see Table 4.1).

Table 4.1. Correspondence Between ImageConsumer Methods and ImageObserver Flags
ImageConsumer Method ImageObserver Flag
setPixels SOMEBITS
imageComplete FRAMEBITS, ALLBITS, ABORT, ERROR
setDimensions WIDTH and HEIGHT
setProperties PROPERTIES

Thus, the communication between the ImageProducer, ImageConsumer, and ImageObserver is as follows: The ImageConsumer registers itself with the ImageProducer using the ImageProducer's addConsumer method. The ImageProducer then starts sending data to this ImageConsumer when its startProduction method is called. The ImageProducer communicates with the ImageConsumer by calling one of the ImageConsumer's methods such as setPixels or imageComplete. The status of the ImageConsumer arrives at the ImageObserver through the ImageObserver's imageUpdate method with the appropriate flags set so that the ImageObserver knows what information is available to it with respect to the loading image data.

Filtering

In this context, filtering is defined as an operation that changes the pixel values or the number of pixels represented by an Image. The most basic and most common type of filtering is simply scaling.

Image Scaling

The easiest way to perform Image scaling is with the Image method Image getScaledInstance(int width, int height, int hints), where width and height represent the dimensions of the new scaled Image. You can give one of them the value of -1 to ensure that the Image aspect ratio doesn't change. For example, if the original Image dimensions were 256X200 and a width and height value of 512, -1 were used in the getScaledInstance method, the new Image dimensions would be 512X400.

The hints parameter allows you to specify how pixel interpolation should be performed. For example, using the previously mentioned dimensions, the number of pixels in the Image increase from 51,200 to 204,800 and the hints parameter gives the programmer some options in specifying how these additional 153,600 pixel values are calculated. Basically, the choices refer to either pixel replication or pixel averaging. With the hints parameter set to Image.SCALE_REPLICATE the new pixel columns and rows introduced will just be copies of existing pixel columns and rows. Similarly, if the Image had decreased in size, pixel columns and rows would have just dropped out. The disadvantage of this method is that the resulting Image might appear coarse with neighboring pixels exhibiting large differences in values. The advantage to this method is that the Image scaling will be performed very quickly. On the other hand, if the hints parameter is set to Image.SCALE_AREA_AVERAGING, the new pixel values will be linearly related to their neighboring pixels with close pixels contributing more and far pixels contributing less. This method of Image scaling takes longer, but it produces smoother image data than simply replicating pixel values. Although the getScaledInstance method is a method of the Image class, it is the only such method that allows Image filtering. For more advanced push model filtering, a subclass of the java.awt.image.ImageFilter must be used.

ImageFilter

During simple image drawing, the ImageConsumer can be difficult to find, but there are situations in which it is well defined. For example, you can use an ImageFilter, which is an ImageConsumer, to process image data as it passes from the original ImageProducer to the final ImageConsumer.

In early versions of Java image processing, there wasn't much support for image manipulation because image data was never meant to be collected into an accessible area. Therefore, image manipulation was primarily developed for filtering single pixel values. In other words, as the ImageProducer sent image data to the ImageConsumer, this data could get filtered. But, this filtering was designed to be done asynchronously on a pixel by pixel basis. Thus, easily implemented ImageFilter subclasses were created, which could crop out certain regions of pixels or process individual pixel values (java.awt.image.CropImageFilter and java.awt.image.RGBImageFilter, respectively). But, if you wanted to write a push model filter that replaces each pixel value with the average of itself and its neighbors (simple smoothing filter), things became much more complex. For this latter task, you would have to subclass the ImageFilter directly, which involves fully implementing an ImageConsumer in order to handle the information sent from the ImageProducer. Because this procedure isn't really useful anymore, it won't be covered in this chapter.

The basic idea behind the ImageFilter class and its subclasses is that they are ImageConsumers, which allow them to receive data from an ImageProducer. These filters get wrapped in an ImageProducer (java.awt.Image.FilteredImageSource), which sends out the filtered data (see Figure 4.1). So the original ImageProducer sends out data and the final ImageConsumer receives data unaware that there was one (or more) ImageConsumer/ImageProducer pairs in the pipeline filtering the data. In Figure 4.1, note that ImageFilter is an ImageConsumer, and FilteredImageSource is an ImageProducer. The ImageObserver is told the status of the image loading in the final ImageConsumer through calls to its imageUpdate method.

Figure 4.1. Push model pixel pipeline showing how asynchronous rendering takes place using ImageFilter/FilteredImageSource pairs.


In summary, consider Figure 4.1. When a Graphics object calls a drawImage method to draw the created Image, the ImageProducer producing the original image data passes this data to the ImageFilter, which acts as an ImageConsumer. The ImageFilter then filters the data before the FilteredImageSource, acting as an ImageProducer, passes the data to the final ImageConsumer. The ImageConsumer then communicates this progress to the ImageObserver (the Applet) through the Applet's imageUpdate method. Although admittedly this seems a bit involved, there isn't much code required. The following code block takes care of most of it with the exception of defining the filter, which we will explore in the next several sections. The only thing that remains to be done is to start the production of the image data and this will occur when the filteredImage data is requested.

Image originalImage = getImage(url);
ImageFilter if = new ImageFilterSubclass(subclassParameters));
ImageProducer ip = new FilteredImageSource(originalImage.getSource(), if);
Image filteredImage = createImage(ip);   //Component method
							

CropImageFilter

The CropImageFilter is a subclass of the ImageFilter which allows you to crop the dimensions of an Image. To create a CropImageFilter, simply specify the x, y value of the top left corner depicting where you want the new Image to start and a width and height value. As the original image data passes through this filter, only the data within this rectangle will be passed through. Of course, the FilteredImageSource lets its ImageConsumer(s) know the new Image dimensions through their setDimensions method so that they will be expecting the correct number of pixels. The constructor for the CropImageFilter is as follows:

ImageFilter if = new CropImageFilter(int x, int y, int width, int height);

RGBImageFilter

An RGBImageFilter is a subclass of the ImageFilter which allows you to change individual pixel values. To create an RGBImageFilter, you must extend the RGBImageFilter class and overwrite the public int filter(int x, int y, int rgb) method, where x and y represent the pixel location and rgb is the pixel's original red, green, and blue color samples packed into a single integer. Likewise, the return value is the pixel's new red, green, and blue color samples packed into an integer. This type of color representation will be discussed further in the section “Pixel Storage and Conversion,” so for now it is enough to know that the filtering that takes place in an RGBImageFilter can only be performed one pixel at a time using that pixel's color samples and its location.

Because the public int filterRGB(int x, int y, int rgb) method is abstract in the RGBImageFilter, it must be defined. But, the parameters used to do this filtering are up to the programmer. Thus, the constructor for the RGBImageFilter subclass should be passed any necessary filtering parameters. In Listing 4.2, a subclass of the RGBImageFilter is defined, which linearly scales the red, green, and blue components of the image data. For example, the constructor parameters 1.2, 1.0, 1.0 will increase the red component by 20%, but leave the green and blue components unchanged.

Listing 4.2 ColorComponentScaler.java
package ch4;

import java.awt.*;
import java.awt.image.RGBImageFilter;

/**
 * ColorComponentScaler -- filters an image by multiplier its
 * red, green and blue color components by their given
 * scale factors
 */
public class ColorComponentScaler extends RGBImageFilter {
    private double redMultiplier, greenMultiplier, blueMultiplier;
    private int newRed, newGreen, newBlue;
    private Color color, newColor;

    /**
     * rm = red multiplier
     * gm = green multiplier
     * bm = blue multiplier
     */
    public ColorComponentScaler(double rm, double gm, double bm) {
        canFilterIndexColorModel = true;
        redMultiplier = rm;
        greenMultiplier = gm;
        blueMultiplier = bm;
    }

    private int multColor(int colorComponent, double multiplier) {
        colorComponent = (int)(colorComponent*multiplier);
        if (colorComponent < 0)
            colorComponent = 0;
        else if (colorComponent > 255)
            colorComponent = 255;

        return colorComponent;
    }

    /**
     * split the argb value into its color components,
     * multiply each color component by its corresponding scaler factor
     * and pack the components back into a single pixel
     */
    public int filterRGB(int x, int y, int argb) {
        color = new Color(argb);
        newBlue = multColor(color.getBlue(), blueMultiplier);
        newGreen = multColor(color.getGreen(), greenMultiplier);
        newRed = multColor(color.getRed(), redMultiplier);
        newColor = new Color(newRed, newGreen, newBlue);
        return (newColor.getRGB());
    }
}

One last point is that the instance variable canFilterIndexColorModel specifies whether this filter method can be applied to Images using an IndexColorModel. The IndexColorModel will be discussed in the later section “Creating and Using ColorModels,” but for now it is enough to know that the pixels in some Images don't correspond to color components, but instead to indices of an array (or arrays) where the color components are held. In these cases, you don't want to filter the pixel values, but instead, you should filter the array (or arrays) holding the color components. The canFilterIndexColorModel variable gives the RGBImageFilter permission to do this.

PixelGrabber/MemoryImageSource

Although you can directly subclass the ImageFilter in order to create more complex filters, it is usually simpler to just collect all the data into an array and process it in its entirety before sending it out again. This doesn't allow incremental image rendering, but that isn't always of importance. The java.awt.image.PixelGrabber class is used for just this purpose. There are a few different constructors for the PixelGrabber, but the one used throughout this book is the following:

PixelGrabber(Image img, int x, int y, int w, int h, boolean forceRGB)

where x, y, w, and h are provided in case you wanted to obtain some rectangular subset of the image data. If you are interested in the entire Image, make the origin of this rectangle (0, 0) and w and h equal to the width and height of the Image. If the image dimensions are not known, you can grab the entire image by using a value of -1 for both the width and the height along with an origin of 0,0. The last parameter forces the PixelGrabber to convert all pixels into the default ColorModel. (ColorModels will be discussed in the later section entitled “Creating and Using ColorModels.”)

The PixelGrabber is an ImageConsumer so it can receive image data, but it marks the end of the push model and the beginning of an immediate mode model because the image data is put into an array instead of being passed to another ImageConsumer.

Caution

It is important to realize that this immediate mode isn't the same as the formal immediate mode that originated as part of the Java2D package. They are similar in that the image data is collected into one accessible area. The Java2D immediate mode imaging model will be discussed in the later section “Immediate Mode Imaging Model.”


The way the PixelGrabber collects the image data is through the use of its public boolean grabPixels() method; for example,

PixelGrabber grabber = new PixelGrabber(originalImage, 0, 0, -1, -1, true);
try {
    if (grabber.grabPixels()) {
        int width = grabber.getWidth();
        int height = grabber.getHeight();
        int[] originalPixelArray = (int[])grabber.getPixels();
    }
    else {
        System.err.println("Grabbing Failed");
    }
}
catch (InterruptedException e) {
    System.err.println("PixelGrabbing interrupted");
}

When all the image data is together in an array, any type of filtering can be performed as long as it is written to handle an array of integers, where each integer typically represents red, green, and blue color components. Last, the post-filtered data, contained in the original array or a new array, can be given to an ImageProducer that will send it to an ImageConsumer, thus returning the imaging pipeline back to a push model. The class used to read the image array data and pass it to an ImageConsumer is the java.awt.image.MemoryImageSource class (see Figure 4.2).

Figure 4.2. The push model doesn't adequately describe this figure because the use of the PixelGrabber stops the asynchronous pixel delivery.


Because the MemoryImageSource is an ImageProducer, it is typically used with the following Component method:

Image createImage(ImageProducer producer)

as follows:

MemoryImageSource mis;
mis = new MemoryImageSource(width, height,
                            newPixelArray, arrayOffset, scanLength);
Image filteredImage = createImage(mis);

where newPixelArray is the filtered pixel array, arrayOffset is the number of bytes prior to the pixel data in the array, and scanlength is the number of pixels in each array column. Usually the scanlength is the same as the width. Once filteredImage is created, any use of this Image will cause the ImageProducer to start sending Image data to an ImageConsumer.

Tip

The data provided by the PixelGrabber and passed to the MemoryImageSource is unformatted pixel data. This means that if you were to write this data out to a file, it wouldn't be a gif or a jpeg image even if that is how the data originated. The ability to write formatted images to a file didn't originate until the Java Image I/O package, which will be discussed in Chapter 5, “Image I/O API.”


One last point is that the MemoryImageSource can be used for simple animations. The way this is done is by calling its public void setAnimation(boolean value) method immediately after it is instantiated. As an example, consider the Applet code in Listing 4.3 in which an image appears correctly, but then fades to black. This is done by starting with a data array filled with the original pixel values, and each time the paint method is called, these pixel values are brought closer to zero. Note that this code was provided for four purposes:

  • The main reason was the use of the PixelGrabber to collect the image data and the MemoryImageSource to start up the push model again.

  • A second reason was to introduce simple animations.

  • The third reason was to introduce the idea that a pixel isn't the smallest unit of interest and usually needs to be broken down into pixel samples representing the different color components. In this case, we are using the default ARGB ColorModel in which each pixel represents a transparency component and three color components: red, green, and blue. In order to separate the samples from the pixel values, you can do bitwise shifts and ands with the appropriate mask. In general, getting color components from pixels is much more involved, and it will be covered in detail in the later section “Pixel Storage and Conversion.”

  • The last reason was to introduce handling of 2D image data stored in a 1D image array. Generally, this conversion takes place as follows: pixel at location(x,y) = GrabbedImageArray[x + imageWidth*y]. This equation describes the situation in which the 2D image data is stored as a 1D array, where the first row of the image data is stored first, the second row next, and so on.

Listing 4.3 GrabandFade.java
package ch4;

import java.awt.*;
import java.applet.*;
import java.net.*;
import java.awt.image.PixelGrabber;
import java.awt.image.MemoryImageSource;

/**
 * GrabandFade.java -- displays provided image and then slowly
 * fades to black
 */
public class GrabandFade extends Applet {
    private Image originalImage;
    private Image newImage;
    private MemoryImageSource mis;
    private int width;
    private int height;
    private int index = 10;
    private int[] originalPixelArray;
    private boolean imageLoaded = false;
    private String imageURLString = "file:images/peppers.png";


    public void init() {
        URL url;
        try {
            // set imageURLString here
            url = new URL(imageURLString);
            originalImage = getImage(url);
        }
        catch (MalformedURLException me) {
            showStatus("Malformed URL: " + me.getMessage());
        }

        /*
         * Create PixelGrabber and use it to fill originalPixelArray with
         * image pixel data.  This array will then by used by the
         * MemoryImageSource.
         */
        try {
            PixelGrabber grabber = new PixelGrabber(originalImage,
                                                    0, 0, -1, -1, true);
            if (grabber.grabPixels()) {
                width = grabber.getWidth();
                height = grabber.getHeight();
                originalPixelArray = (int[])grabber.getPixels();

                mis = new MemoryImageSource(width, height,
                                            originalPixelArray,0, width);
                mis.setAnimated(true);
                newImage = createImage(mis);
            }
            else {
                System.err.println("Grabbing Failed");
            }
        }
        catch (InterruptedException ie) {
            System.err.println("Pixel Grabbing Interrupted");
        }
    }

    /**
     * overwrite update method to avoid clearing of drawing area
     */
    public void update(Graphics g) {
        paint(g);
    }

    /**
     * continually draw image, then decrease color components
     * of all pixels contained in the originalPixelArray
     * array until color components are all 0
     */
    public void paint(Graphics g) {
        int value;
        int alpha, sourceRed, sourceGreen, sourceBlue;
        if (newImage != null) {
            g.drawImage(newImage, 0, 0, this); // redraw image

            // if image isn't faded to black, continue
            if (imageLoaded == false) {
                imageLoaded = true;
                for (int x=0; x < width; x+=1)
                    for (int y=0; y < height; y+=1) {

                        // find the color components
                        value = originalPixelArray[x*height+y];
                        alpha = ( value >> 24) & 0x000000ff;
                        sourceRed =   ( value >> 16) & 0x000000ff;
                        sourceGreen = ( value >> 8) & 0x000000ff;
                        sourceBlue = value & 0x000000ff;

                        // subtract index from each red component
                        if (sourceRed > index) {
                            sourceRed-=index;
                            imageLoaded = false;
                        }
                        else
                            sourceRed = 0;

                        // subtract index from each green component
                        if (sourceGreen > index) {
                            sourceGreen-=index;
                            imageLoaded = false;
                        }
                        else
                            sourceGreen = 0;

                        // subtract index from each blue component
                        if (sourceBlue > index) {
                            sourceBlue-=index;
                            imageLoaded = false;
                        }
                        else
                            sourceBlue = 0;

                        /*
                           when we pack new color components into integer
                           we make sure the alpha (transparency) value
                           represents opaque
                        */
                        value = (alpha << 24);
                        value += (sourceRed << 16);
                        value += (sourceGreen << 8);
                        value += sourceBlue;

                        // fill pixel array
                        originalPixelArray[x*height+y] = value;
                    }
                mis.newPixels(); //send pixels to ImageConsumer
            }
        }
    }
}

Another interesting thing about Listing 4.3 is that if you use the imageUpdate method defined in Listing 4.1, the following output appears:

Flag(s): WIDTH:(256) HEIGHT:(256)
Flag(s): PROPERTIES
Flag(s): SOMEBITS(0,0)->(256,256)
Flag(s): FRAMEBITS(0,0)->(256,256)
Flag(s): FRAMEBITS(0,0)->(256,256)
Flag(s): FRAMEBITS(0,0)->(256,256)
. . .
Flag(s): FRAMEBITS(0,0)->(256,256)
Flag(s): FRAMEBITS(0,0)->(256,256)
Flag(s): FRAMEBITS(0,0)->(256,256)
Flag(s): ALLBITS(0,0)->(256,256)

In comparison with the output from Listing 4.1, there isn't a series of SOMEBITS flags because the PixelGrabber grabbed all the image data before it was needed. Also, because we are now sending a series of frames to the ImageConsumer, the FRAMEBITS flag appears multiple times.

One additional thing to notice in Listing 4.3 is the method called public void update (Graphics g). When repaint() is called (typically by the ImageObserver's imageUpdate method), the method that gets called isn't paint(Graphics g), but update(Graphics g). The default behavior of this method is to first clear the viewing area and then call paint(Graphics g). Often, this clearing of the viewing area results in the animation appearing choppy. To avoid this problem, it is common to override this update method and have it only call the paint method, that is,

public void update(Graphics g) {
       paint(g);
}

When doing animations, the only time this update method doesn't need to be overwritten is if you are using a swing component such as a javax.swing.JComponent or a javax.swing.JApplet. The designers of swing decided to overwrite the update method so that it no longer clears the viewing area before calling the paint method.

Double Buffering

As was mentioned in the beginning of this chapter, a java.awt.Image is unlike a conventional image in that it doesn't hold any pixel data. It is usually easier to think of an Image as a class with methods and resources to allow image data to be processed and displayed. With double buffering, an Image takes on another unconventional role: that of a drawing surface. You can obtain a Graphics object of a particular Image and use that object to draw on the Image. For example, using the following block of code:

Image dbimg = someComponent.createImage(512, 512);
Graphics dbgraphics = dbimg.getGraphics();

anything that gets drawn using the dbgraphics object will be drawn on the hidden drawing area of Image dbImg.

When this drawing process is completed, you can then draw the Image dbImg onto another drawing surface (such as an Applet's) using code similar to the following:

public void paint(Graphics g) {
       if (dbimg != null)
              g.drawImage(dbimg,0,0,null);
}

This technique can be very useful for tasks such as animation in which one frame is being displayed on an Applet while another frame is being invisibly built on an Image. When this hidden frame is completed, it is then displayed on the Applet while another frame can be invisibly built. This allows the transitions that occur during the frame building to be completely hidden from the user.

In Listing 4.4, a circle continually passes over an Image. This is an excellent application to appreciate the role of double buffering and of overloading the update method. If either one of these techniques isn't used, the circle won't appear to be traveling smoothly over the image.

Listing 4.4 DoubleBufferedImage
package ch4;

import java.awt.*;
import java.applet.*;
import java.net.*;

public class DoubleBufferedImage extends Applet {
    private Image dbImage;
    private Image originalImage;
    private int xLocation = 0;
    private int imageWidth, imageHeight;
    private Graphics dbImageGraphics;
    private String imageURLString = "file:images/peppers.png";

    public void init() {
        URL url = null;
        try {
            url = new URL(imageURLString);
        }
        catch (MalformedURLException me) {
            showStatus("Malformed URL: " + me.getMessage());
        }

        originalImage = getImage(url);

        MediaTracker mt = new MediaTracker(this);
        mt.addImage(originalImage, 0);
        try {
            mt.waitForID(0);
        }
        catch (InterruptedException ie) {
        }

        //don't need ImageObservers since the Image is already loaded
        imageWidth = originalImage.getWidth(null);
        imageHeight = originalImage.getHeight(null);

        dbImage = this.createImage(imageWidth, imageHeight);
        dbImageGraphics = dbImage.getGraphics();
    }

    public void update(Graphics g) {
        paint(g);
    }

    public void paint(Graphics g) {
        if (xLocation == imageWidth)
            xLocation = 0;

        //anything drawn using the dbImagGraphics object is hidden
        dbImageGraphics.clearRect(0,0,imageWidth, imageHeight);
        dbImageGraphics.drawImage(originalImage, 0, 0, this);
        dbImageGraphics.setColor(Color.red);
        dbImageGraphics.fillOval(xLocation, imageHeight/2, 20, 20);

        //now dbImage's drawing area appears
        g.drawImage(dbImage,0,0,this);

        xLocation ++;
        repaint(10);
    }
}

Tip

One very useful class used in this example is the java.awt.MediaTracker, which allows you to wait until a particular image or a group of images is loaded before proceeding. A typical usage of this class is as follows:

MediaTracker mt = new MediaTracker(someImageObserver);
mt.addImage(img, id);  //give each Image a possibly non-unique id value
try {
    //wait for all Images referred to by id to completely load
    mt.waitForID(id);
}
catch (InterruptedException ie) {
     //waiting was interrupted
}


..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset