Chapter 18. Working with Images and Other Media

Up to this point, we’ve confined ourselves to working with the high-level drawing commands of the Graphics2D class, using images in a hands-off mode. In this section, we’ll clear up some of the mystery surrounding images and see how they are created and used. The classes in the java.awt.image package handle images and their insides; Figure 18.1 shows the important classes in this package.[47]

The java.awt.image package

Figure 18-1. The java.awt.image package

First, we’ll return to our discussion of image observers and see how we can get more control over image data as it’s processed asynchronously by GUI components. Then we’ll open the hood and have a look at the inside of a BufferedImage. If you’re interested in creating sophisticated graphics, such as rendered images or video streams, this will teach you about the foundations of image construction in Java.

Implementing an ImageObserver

The architects of Java realized that images might take some time to load over a slow network. Image observers implement the ImageObserver interface. They are effectively nosy neighbors of images that watch as the image data arrives.

An image is simply a rectangle of pixels. A pixel has both a color and a transparency; the transparency specifies how pixels underneath the image show through. For a static image, such as a GIF or JPEG data file, the observer is notified when the entire image is complete, and production is finished. For a video source or animation, the image observer would be notified repeatedly (at the end of each frame) as a continuous stream of pixel data was received.

The observer is notified as new portions of the image and new attributes are ready. Its job is to track this information and let another part of the application know its status. The image observer is essentially a callback that is notified asynchronously as the image is built. The default Component class image observer that we used in our previous examples called repaint( ) for us each time a new section of the image was available, so that the screen was updated more or less continuously as the data arrived. A different kind of image observer might wait for the entire image before telling the application to display it; yet another observer might update a loading meter showing how far the image loading had progressed.

To be an image observer, you have to implement the single method, image-Update( ) , defined by the java.awt.image.ImageObserver interface:

public boolean imageUpdate(Image image, int flags, int x, int y,
                           int width, int height)

imageUpdate( ) is called by the graphics system, as needed, to pass the observer information about the construction of its view of the image. Essentially, any time the image changes, the observer is notified so it can perform any necessary actions, like repainting. image holds a reference to the Image object in question. flags is an integer whose bits specify what information about the image is now available. The values of the flags are defined as static variables in the ImageObserver interface, as shown in Table 18.1.

Table 18-1. ImageObserver Information Flags

Flag

Description

HEIGHT

The height of the image is ready.

WIDTH

The width of the image is ready.

FRAMEBITS

A frame is complete.

SOMEBITS

Some new pixels have arrived.

ALLBITS

The image is complete.

ABORT

The image loading has been aborted.

ERROR

An error occurred during image processing; attempts to display the image will fail.

The flags determine which of the other parameters, x, y, width, and height, hold valid data and what that data means. To test whether a particular flag in the flags integer is set, we have to resort to some binary shenanigans. The following class, MyObserver, implements the ImageObserver interface; it reports on the information it receives:

//file: MyObserver.java
import java.awt.*;  
import java.awt.image.*;  

class MyObserver implements ImageObserver {  

  public boolean imageUpdate( Image image, int flags, int x, int y,  
                              int width, int height) {  

    if ( (flags & HEIGHT) !=0 )  
      System.out.println("Image height = " + height );  

    if ( (flags & WIDTH ) !=0 )  
      System.out.println("Image width = " + width );  

    if ( (flags & FRAMEBITS) != 0 )  
      System.out.println("Another frame finished.");  

    if ( (flags & SOMEBITS) != 0 )  
      System.out.println("Image section :"  
             + new Rectangle( x, y, width, height ) );
  
    if ( (flags & ALLBITS) != 0 ) {  
      System.out.println("Image finished!");  
      return false;   
    }  
  
    if ( (flags & ABORT) != 0 ) {  
      System.out.println("Image load aborted...");  
      return false;   
    } 
    return true;   
  }  
}

The imageUpdate( ) method of MyObserver is called by the consumer periodically, and prints simple status messages about the construction of the image. Notice that width and height play a dual role. If SOMEBITS is set, they represent the size of the chunk of the image that has just been delivered. If HEIGHT or WIDTH is set, however, they represent the overall image dimensions. Just for amusement, we have used the java.awt.Rectangle class to help us print the bounds of a rectangular region. (You may not want to create a new object each time you just need to report some coordinates.)

imageUpdate( ) returns a boolean value indicating whether or not it’s interested in future updates. If the image is finished or aborted, imageUpdate( ) returns false to indicate it isn’t interested in further updates. Otherwise, it returns true.

The following example uses a MyObserver object to generate information about an image as it is loaded. (To see the messages, enable your browser’s Java console.)

//file: ObserveImage.java
import java.awt.*;  
  
public class ObserveImage extends java.applet.Applet {   
  Image img;  

  public void init( ) {  
    img = getImage( getClass( ).getResource(getParameter("img")) );  
    MyObserver mo = new MyObserver( );  
    img.getWidth( mo );  
    img.getHeight( mo );  
    prepareImage( img, mo );  
  }  

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

After requesting the Image object with getImage( ) , we perform three operations on it to kick-start the loading process. getWidth( ) and getHeight( ) ask for the image’s width and height. If the image hasn’t been loaded yet, or its size can’t be determined until loading is finished, our observer will be called when the data is ready. prepareImage( ) asks that the image be readied for display on the component. It’s a general mechanism for starting the process of loading, converting, and possibly scaling the image. If the image hasn’t been otherwise prepared or displayed, this happens asynchronously, and our image observer will be notified as the data is constructed.

You should be able to see how we could implement all sorts of sophisticated image loading and tracking schemes. The two most obvious strategies, however, are to draw an image progressively, as it’s constructed, or to wait until it’s complete and draw it in its entirety. We have already seen that the Component class implements the first scheme. Another class, java.awt.MediaTracker, is a general utility that tracks the loading of a number of images or other media types for us. We’ll look at it next.



[47] Before Java 2, creating and modifying images was the domain of image producers and consumers. We won’t be covering these topics in this chapter; instead, we’ll stick to the “new stuff,” which is more capable and easier to use in some cases.

Get Learning Java now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.