20.7. Displaying Images
So far, we've worked with
methods
for drawing simple
shapes
and displaying text. For more complex graphics, we'll be working with images. In a typical Swing application, the simplest way to display an image in your application is to use an
ImageIcon
with a
JLabel
component. Here, we are talking about working with image data at a lower level, for painting. The 2D API has a powerful set of tools for generating and displaying image data. We'll start with the basics of the
java.awt.Image
class and see how to load an image into an application and draw it where you want it. The Java AWT toolkit will handle most of the details for us. In the
next
chapter, we'll go further to discuss how to manage image loading manually as well as how to create and manipulate raw pixel data, allowing you to create any kind of graphics you can dream up.
The
core
AWT supports images encoded in JPEG, PNG, and GIF. (This includes GIF89a animations so that you can work with simple animations as easily as static images.) If you need to work with other types of images, you can
turn
to the Java Advanced Imaging
javax.imageio
framework. We'll mention it
briefly
here
and again in the next chapter when we discuss the
BufferedImage
class.
The ImageIO framework is bundled with Java 1.4 and later. In many ways, it supercedes and replaces the image handling functionality of the core AWT just as Swing extends and
replaces
the old AWT
components
. The ImageIO framework is easily extensible for new image types through plug-ins. However, out of the box, all that it adds is the ability to read bitmap (BMP) and wireless bitmap (WBMP) images. Since most Java code can and does use the original AWT functionality, that is what we'll focus on.
20.7.1. The Image Class
The
java.awt.Image
class represents a view of an image. The view is created from an image source that produces pixel data. Images can be from a static source, such as a JPEG file, or a dynamic one, such as a video stream or a graphics engine.
AWT Images are created with the
getImage( )
and
createImage( )
methods of the
java.awt.Toolkit
class. There are two forms of each method, which accept a URL or plain filename, respectively.
createImage( )
can also accept a byte array of image data directly.
When bundling images with your application, you should use the
Class
class's
getresource( )
method (discussed in Chapter 1) to construct a URL reference to the file from the application classpath.
geTResource( )
allows you to bundle images along with your application, inside JAR files or
anywhere
else in the classpath. The following code fragment shows some examples of loading images with the
getImage( )
method:
Toolkit toolkit = Toolkit.getDefaultToolkit( );
// Application resource URL - Best method
URL daffyURL = getClass( ).getResource("/cartoons/images/daffy.gif");
Image daffyDuckImage = toolkit.getImage( daffyURL );
// Absolute URL -
URL monaURL = new URL( "http://myserver/images/mona_lisa.png");
Image monaImage = toolkit.getImage( monaURL );
// Local file -
Image elvisImage = toolkit.getImage("c:/elvis/lateryears/fatelvis1.jpg" );
The
createImage( )
method looks just like
getImage( )
; the difference is that
getImage( )
"interns" images and shares them when it receives multiple
requests
for the same data. The
createImage( )
method does not do this (it creates a new Image object every time) and relies on you to cache and share the image.
getImage( )
is
convenient
in an application that uses a limited number of images for the life of the application, but it may not ever release the image data. You should use
createImage( )
and cache the
Image
objects yourself when it's an issue.
The
javax.imageio.ImageIO
class, similarly, provides several static
read( )
methods that can load images from a
File
,
URL
, or
InputStream
:
URL daffyURL = getClass( ).getResource("/cartoons/images/daffy.gif");
Image daffyDuckImage = ImageIO.read( daffyURL );
We'll discuss image loading with AWT and the ImageIO framework in more detail in Chapter 1.
Once we have an
Image
object, we can draw it into a graphics context with the
drawImage( )
method of the
Graphics
class. The simplest form of the
drawImage( )
method takes four parameters: the
Image
object, the
x
,
y
coordinates at which to draw it, and a reference to a special
image observer
object. We'll show an example involving
drawImage( )
soon, but first, let's say a word about image observers.
20.7.2. Image Observers
Images are
processed
asynchronously, which means that Java
performs
image operations, such as loading and scaling in the background (allowing the
user
code to continue). In a typical client application, this might not be important; images may be small for things like
buttons
, and are probably bundled with the application for almost instant retrieval. However, Java was designed to work with image data over the Web as well as locally, and you will see this
expressed
in the APIs for working with image data.
For example, the
getImage( )
method always returns immediately, even if the image data has to be retrieved over the network from Mars and isn't available yet. In fact, if it's a new image, Java won't even begin to fetch the data until we try to display or manipulate it. The advantage of this technique is that Java can do the work of a powerful, multithreaded image-processing environment for us. However, it also introduces several problems. If Java is loading an image for us, how do we know when it's completely loaded? What if we want to work with the image as it arrives? What if we need to know properties of the image (like its dimensions) before we can start working with it? What if there's an error in loading the image?
These issues are handled by
image observers
, objects that implement the
ImageObserver
interface. All operations that draw or examine
Image
objects are asynchronous and take an image observer object as a parameter. The
ImageObserver
monitors
the image operation's status and can make that information available to the rest of the application. When image data is loaded from its source by the graphics system, your image observer is notified of its progress, including when new pixels are available, when a complete frame of the image is ready, and if there is an error during loading. The image observer also receives attribute information about the image, such as its dimensions and properties, as soon as they are known.
The
drawImage( )
method, like other image operations, takes a reference to an
ImageObserver
object as a parameter.
drawImage( )
returns a
boolean
value specifying whether or not the image was
painted
in its entirety. If the image data has not yet been loaded or is only partially available,
drawImage( )
paints
whatever fraction of the image it can and returns. In the background, the graphics system starts (or continues) loading the image data. The image observer object is registered as interested in information about the image. The observer is then called repeatedly as more pixel information is available and again when the entire image is complete. The image observer can do whatever it wants with this information. Most often the information is used to call
repaint( )
to prompt the application to draw the image again with the updated data. In this way, an application or applet can draw the image as it arrives for a progressive loading effect. Alternatively, it could wait until the entire image is loaded before displaying it.
Image observers are covered in Chapter 21. For now, let's avoid the issue by using a prefabricated image observer. The
Component
class implements the
ImageObserver
interface and provides some simple
repainting
behavior, which means every component can serve as its own default image observer. We can simply pass a reference to whatever component is doing the painting as the image observer parameter of a
drawImage( )
call:
public void paint( Graphics g ) {
g.drawImage( monaImage, x, y, this );
...
Our component serves as the image observer and calls
repaint( )
for us to
redraw
the image as necessary. If the image arrives slowly, our component is notified repeatedly as new
chunks
become available. As a result, the image appears gradually, as it's loaded.
20.7.2.1 Preloading images
We'll discuss image loading in more detail in the next chapter when we look at
MediaTracker
, which is a utility for monitoring the load progress of one or more images. However, we'll skip ahead a bit here and show you the easy shortcut for loading a single image and making sure it's complete and ready to draw. You can use the
javax.swing.ImageIcon
class to do the dirty work for you:
ImageIcon icon = new ImageIcon("myimage.jpg");
Image image = icon.getImage( );
Images loaded by the
ImageIO read( )
methods are returned fully loaded.
ImageIO
provides its own API for monitoring image loading progress. That API
follows
a more standard event source/listener pattern, but we won't get into it here.
20.7.3. Scaling and
Size
Another version of
drawImage( )
renders
a scaled version of the image:
g.drawImage( monaImage, x, y, x2, y2, this );
This draws the entire image within the rectangle
formed
by the points
x
,
y
and
x2
,
y2
, scaling as necessary.
drawImage( )
behaves the same as before; the image is processed by the component as it arrives, and the image observer is notified as more pixel data and the completed image are available. Several other overloaded versions of
drawImage( )
provide more complex options: you can scale, crop, and perform some simple transpositions.
Normally, however, for performance you want to make a scaled copy of an image (as opposed to simply painting one at draw-time), and you can use
getScaledInstance( )
for this purpose. Here's how:
Image scaledDaffy =
daffyImage.getScaledInstance( 100, 200, Image.SCALE_AREA_AVERAGING );
This method
scales
the original image to the given size; in this case, 100 by 200 pixels. It returns a new
Image
that you can draw like any other image.
SCALE_AREA_AVERAGING
is a constant that
tells
getScaledImage( )
what scaling algorithm to use. The algorithm used here
tries
to do a decent job of scaling at the expense of time. Some alternatives that take less time are
SCALE_REPLICATE
, which scales by replicating scan lines and
columns
(which is fast but probably not pretty). You can also specify either
SCALE_FAST
or
SCALE_SMOOTH
and let the implementation choose an appropriate algorithm that optimizes for time or quality. If you don't have specific requirements, you should use
SCALE_DEFAULT
, which,
ideally
, would be set by a preference in the user's environment.
If you are going to draw the image more than once (which you almost always will), creating a scaled copy of the image can improve performance dramatically. Otherwise, repeated calls to
drawImage( )
with scaling requirements cause the image to be scaled every time, wasting processing time.
The
Image getHeight( )
and
getWidth( )
methods retrieve the dimensions of an image. Since this information may not be available until the image data is completely loaded, both methods also take an
ImageObserver
object as a parameter. If the dimensions aren't yet available, they return values of
-1
and notify the observer when the actual value is known. We'll see how to deal with these and other problems a bit later. For now, we'll continue to use our
Component
as the image observer and move on to some general painting techniques.
|