One of the challenges in building software for networked
applications is that data is not always instantly available. Since some of
Java’s roots are in Internet applications 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
applications 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.
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
application 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, implement the imageUpdate()
method,
which is 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.Media
Tracker
, 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.
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 how to use 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
.
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 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 holdover 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 that 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’s possible to be
interrupted here by an Interrupted
Exception
. 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
to indicate 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 application. It saves implementing a custom ImageObserver
for every application. For
general Swing application work, you can use yet another simplification
by employing the ImageIcon
component
to use a MediaTracker
. This is
covered next.
In Chapter 17, 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 sections—getting an image loaded fully before
using it—would be:
ImageIcon
icon
=
new
ImageIcon
(
"myimage.jpg"
);
Image
image
=
icon
.
getImage
();
This quirky approach saves a few lines of typing, but uses an icon
in an odd way and is not very clear. ImageIcon
also gives you direct access to the
MediaTracker
it’s 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
.
As we mentioned in the introduction to Chapter 1, 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. While the
AWT has a relatively fixed set of functionality, JAI is an extensible
framework that accepts plug-ins for new image 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 BufferedImage
s and not
just plain AWT Image
s. 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
BufferedImage
s 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
);
In this example, we 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. Because 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; rather, it 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.
Get Learning Java, 4th Edition 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.