Loading Images

One of the challenges in building software for networked apps is that data is not always instantly available. Since some of Java's roots are in Internet apps such as web browsers, its image-handling APIs were designed specifically to accommodate the fact that images might take some time to load over a slow network, providing for detailed information about image-loading progress. While many client apps do not require handling of image data in this way, it's still useful to understand this mechanism if for no other reason than it appears in the most basic image-related APIs. The Swing toolkit adds its own layer of image handling over this with components such as ImageIcon, which encapsulates an image source for you. After reading this chapter, you'll have an understanding of how the layers fit together.

ImageObserver

In the previous chapter, we mentioned that all operations on image data (e.g., loading, drawing, scaling) allow you to specify an "image observer" object as a participant. An image observer implements the ImageObserver interface, allowing it to receive notification as information about the image becomes available. The image observer is essentially a callback that is notified progressively as the image is loaded. For a static image, such as a GIF or JPEG data file, the observer is notified as chunks of image data arrive and also when the entire image is complete. For a video source or animation (e.g., GIF89), the image observer is notified at the end of each frame as the continuous stream of pixel data is generated. The image observer can do whatever it wants with this information. For example, in the last chapter we used the image observer built into the base Component class. Although you probably didn't see it happen in our examples, the Component image observer invoked repaint( ) for us each time a new section of the image became available so that the picture, if it had taken a long time to load, would have displayed progressively. A different kind of image observer might have waited for the entire image before telling the app to display it; yet another use for an observer might be to update a loading meter showing how far the image loading had progressed. To be an image observer, you have to implement the single method, imageUpdate( ), 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. The image parameter 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 flag values are defined as static variables in the ImageObserver interface, as illustrated in this example:

 //file: ObserveImageLoad.java
 import java.awt.*;
 import java.awt.image.*;
 public class ObserveImageLoad {
 public static void main( String [] args)
 {
 ImageObserver myObserver = new 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!");
 if ( (flags & ABORT) != 0 )
 System.out.println("Image load aborted...");
 return true;
 }
 };
 Toolkit toolkit = Toolkit.getDefaultToolkit( );
 Image img = toolkit.getImage( args[0] );
 toolkit.prepareImage( img, -1, -1, myObserver );
 }
 }


Run the example, supplying an image file as the command-line argument and observe the output. You'll see a number of incremental messages about loading the image. The flags integer determines 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 (using the & (AND) operator). The width and height parameters 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. Finally, imageUpdate( ) returns a boolean value indicating whether or not it's interested in future updates. In this example, after requesting the Image object with getImage( ), we kick-start the loading process with the Toolkit's prepareImage( ) method, which takes our image observer as an argument. Using an Image API method such as drawImage( ), scaleImage( ), or asking for image dimensions with getWidth( ) or getHeight( ) will also suffice to start the operation. Remember that although the getImage( ) method created the image object, it doesn't begin loading the data until one of the image operations requires it. The example shows the lowest-level general mechanism for starting and monitoring the process of loading image data. You should be able to see how we could implement all sorts of sophisticated image loading and tracking schemes with this. The two most important strategies (to draw an image progressively, as it's constructed, or to wait until it's complete and draw it in its entirety) are handled for us. 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 in the next section.

MediaTracker

java.awt.MediaTracker is a utility class that simplifies life if we have to wait for one or more images to be loaded completely before they're displayed. A MediaTracker monitors the loading of an image or a group of images and lets us check on them periodically or wait until they are finished. MediaTracker implements the ImageObserver interface that we just discussed, allowing it to receive image updates. The following code snippet illustrates using a MediaTracker to wait while an image is prepared:

 //file: StatusImage.java
 import java.awt.*;
 import javax.swing.*;
 public class StatusImage extends JComponent
 {
 boolean loaded = false;
 String message = "Loading...";
 Image image;
 public StatusImage( Image image ) { this.image = image; }
 public void paint(Graphics g) {
 if (loaded)
 g.drawImage(image, 0, 0, this);
 else {
 g.drawRect(0, 0, getSize( ).width - 1, getSize( ).height - 1);
 g.drawString(message, 20, 20);
 }
 }
 public void loaded( ) {
 loaded = true;
 repaint( );
 }
 public void setMessage( String msg ) {
 message = msg;
 repaint( );
 }
 public static void main( String [] args ) {
 JFrame frame = new JFrame("TrackImage");
 Image image = Toolkit.getDefaultToolkit( ).getImage( args[0] );
 StatusImage statusImage = new StatusImage( image );
 frame.getContentPane( ).add( statusImage );
 frame.setSize(300,300);
 frame.setVisible(true);
 MediaTracker tracker = new MediaTracker( statusImage );
 int MAIN_IMAGE = 0;
 tracker.addImage( image, MAIN_IMAGE );
 try {
 tracker.waitForID( MAIN_IMAGE ); }
 catch (InterruptedException e) {}
 if ( tracker.isErrorID( MAIN_IMAGE ) )
 statusImage.setMessage( "Error" );
 else
 statusImage.loaded( );
 }
 }


In this example we have created a trivial component called StatusImage that accepts an image and draws a text status message until it is told that the image is loaded. It then displays the image. The only interesting part here is that we use a MediaTracker to load the image data for us, simplifying our logic. First, we create a MediaTracker to manage the image. The MediaTracker constructor takes a Component as an argument; this is supposed to be the component onto which the image is later drawn. This argument is somewhat of a hold-over from earlier Java days with AWT. If you don't have the component reference handy, you can simply substitute a generic component reference like so:

 Component comp = new Component( );


After creating the MediaTracker, we assign it images to manage. Each image is associated with an integer identifier we can use later for checking on its status or to wait for its completion. Multiple images can be associated with the same identifier, letting us manage them as a group. The value of the identifier is also meant to prioritize loading when waiting on multiple sets of images; lower IDs have higher priority. In this case, we want to manage only a single image, so we created one identifier called MAIN_IMAGE and passed it as the ID for our image in the call to addImage( ). Next, we call the MediaTracker waitforID( ) routine, which blocks on the image, waiting for it to finish loading. If successful, we tell our example component to use the image and repaint. Another MediaTracker method, waitForAll( ), waits for all images to complete, not just a single ID. It is possible to be interrupted here by an InterruptedException. We should also test for errors during image preparation with isErrorID( ). In our example, we change the status message if we find one. The MediaTracker checkID( ) and checkAll( ) methods may be used to poll periodically the status of images loading, returning TRue or false indicating whether loading is finished. The checkAll( ) method does this for the union of all images being loaded. Additionally, the statusID( ) and statusAll( ) methods return a constant indicating the status or final condition of an image load. The value is one of the MediaTracker constant values: LOADING, ABORTED, ERROR, or COMPLETE. For statusAll( ), the value is the bitwise OR value of all of the various statuses. This may seem like a lot of work to go through just to put up a status message while loading a single image. MediaTracker is more valuable when you are working with many raw images that have to be available before you can begin parts of an app. It saves implementing a custom ImageObserver for every app. For general Swing app work, you can use yet another simplification by employing the ImageIcon component to use a MediaTracker; this is covered next.

ImageIcon

In , we discussed Swing components that can work with images using the Icon interface. In particular, the ImageIcon class accepts an image filename or URL and can render it into a component. Internally, ImageIcon uses a MediaTracker to fully load the image in the call to its constructor. It can also provide the Image reference back. So, a shortcut to what we did in the last few sectionsgetting an image loaded fully before using itwould be:

 ImageIcon icon = new ImageIcon("myimage.jpg");
 Image image = icon.getImage( );


This quirky but useful approach saves a few lines of typing but uses a component in an odd way and is not very clear. ImageIcon also gives you direct access to the MediaTracker it is using through the getMediaTracker( ) method or tells you the MediaTracker load status through the getImageLoadStatus( ) method. This returns one of the MediaTracker constants: ABORTED, ERROR, or COMPLETE.

ImageIO

As we mentioned in the introduction to , the javax.imageio package is a standard extension that deals with reading and writing many image formats. It is a part of the larger Java Advanced Imaging (JAI) API. This API supports advanced manipulation and display of images. Whereas the AWT has a relatively fixed set of functionality, JAI is an extensible framework that accepts plug-ins for new file formats and features. The imageio portion of JAI is bundled with Java 1.4 and later, so we can take advantage of it on all current Java releases. ImageIO effectively supercedes the APIs we've talked about here with new ones for loading and monitoring image data and although we won't cover it in detail, we will discuss it briefly here for several reasons. First, it is fairly easy to use. Second, ImageIO natively works with BufferedImages and not just plain AWT Images. As we'll discuss throughout the rest of this chapter, buffered images can expose their pixel data for you to read or manipulate. Finally, using ImageIO allows you both to load and save BufferedImages to files. The core AWT has no tools for encoding image data for saving to files. Previously, we showed how easy it is to load an image with the static read( ) methods of the ImageIO class, which accept either a File, URL, or InputStream:

 File file = new File("/Users/pat/images/boojum.gif");
 BufferedImage bi = ImageIO.read( file );


This time we have revealed that the type returned is actually a BufferedImage, which is a subtype of Image. The ImageIO.read( ) method, like the AWT getImage( ) method, automatically detects the image type and decodes it properly. Since ImageIO is extensible, it's useful to be able to list the types of images it can decode. You get this information with the ImageIO.getReaderFormatNames( ) method, which returns an array of strings corresponding roughly to file extensions for the image types it understands. (ImageIO does not rely on file extensions to detect image types but looks at the content of the file.) Images loaded by the ImageIO.read( ) methods are fully loaded before they are returned, so the method blocks until they are done. If you want more fine-grained information on the progress of image loading, you can use the IIOReadProgressListener interface of the javax.imageio.event package, which roughly corresponds to the AWT ImageObserver. To use it, you must delve a little deeper into the ImageIO API by first looking up an appropriate ImageReader object with which to register the listener:

 import javax.imageio.*;
 import javax.imageio.stream.*;
 import javax.imageio.event.*;
  
 File file = new File("image.jpg");
 ImageInputStream iis = ImageIO.createImageInputStream( file );
  
 Iterator readers = ImageIO.getImageReaders( iis );
 ImageReader reader = (ImageReader)readers.next( ); // choose first one
  
 reader.addIIOReadProgressListener( readProgressListener );
 reader.setInput( iis, true );
 BufferedImage bi = reader.read( 0/*index*/ );


This code is fairly straightforward. The ReadProgressListener is used like any of the AWT or Swing event interfaces we've seen before. You can refer to the Javadoc for the exact methods you must implement. Finally, in addition to the progress listener, two other listener APIs, IIOReadUpdateListener and IIOReadWarningListener, offer information on pixel changes (e.g., for progressive loading) and loading errors. There are also, of course, "write" versions of all of these tools that handle the flip side, saving image data. We'll return to that topic later in this chapter.

Comments