That’s really all there is to
content handlers. As one final example, I’ll show you how to
write a content handler for image files. These differ from the
text-based content handlers you’ve already seen in that they
generally produce an object that implements the
java.awt.ImageProducer
interface rather than an
Input Stream
object. The specific example
we’ll choose is the Flexible Image Transport System (FITS) format in common use among
astronomers. FITS files are grayscale, bitmapped images with headers
that determine the bit depth of the picture, the width and the height
of the picture, and the number of pictures in the file. Although FITS
files commonly contain several images (typically pictures of the same
thing taken at different times), in this example we look at only the
first image in a file.[29]
There are a few key things you need to know to process FITS files. First, FITS files are broken up into blocks of exactly 2,880 bytes. If there isn’t enough data to fill a block, it is padded with spaces at the end. Each FITS file has two parts, the header and the primary data unit. The header occupies an integral number of blocks, as does the primary data unit. If the FITS file contains extensions, there may be additional data after the primary data unit, but we ignore that here. Any extensions that are present will not change the image contained in the primary data unit.
The header begins in the
first block of the FITS file. It may occupy one or more blocks; the
last block may be padded with spaces at the end. The header is ASCII
text. Each line of the header is exactly 80 bytes wide. The first
eight characters of each header line contain a keyword, which is
followed by an equals sign (character 9), followed by a space (10).
The keyword is padded on the right with spaces to make it eight
characters long. Columns 11 through 30 contain a value; the value may
be right-justified and padded on the left with spaces if necessary.
The value may be an integer, a floating point number, a
T
or an F
signifying the
boolean values true and false, or a string delimited with single
quotes. A comment may appear in columns 31 through 80; comments are
separated from the value of a field by a slash (/). Here’s a
simple header taken from a FITS image produced by K. S.
Balasubramaniam at the Vacuum Tower Telescope at the National Solar
Observatory in Sunspot, New Mexico (http://www.sunspot.noao.edu/ ):
SIMPLE = T / BITPIX = 16 / NAXIS = 2 / NAXIS1 = 242 / NAXIS2 = 252 / DATE = '19 Aug 1996' / TELESC = 'NSO/SP - VTT' / IMAGE = 'Continuum' / COORDS = 'N29.1W34.2' / OBSTIME = '13:59:00 UT' / END
Every
FITS file begins with the keyword SIMPLE. This keyword always has the
value T
. If this isn’t the case, the file is
not valid. The second line of a FITS file always has the keyword
BITPIX, which tells you how the data is stored. There are five
possible values for BITPIX, four of which correspond exactly to Java
primitive data types. The most common value of BITPIX is 16, meaning
that there are 16 bits per pixel, which is equivalent to a Java
short
. A BITPIX of 32 is a Java
int
. A BITPIX of -32 means that each pixel is
represented by a 32-bit floating point number (equivalent to a Java
float
); a BITPIX of -64 is equivalent to a Java
double
. A BITPIX of 8 means that 8 bits are used
to represent each pixel; this is similar to a Java
byte
, except that FITS uses unsigned bytes ranging
from
to 255; Java’s byte
data type is signed,
taking values that range from -128 to 127.
The remaining keywords in a FITS file may appear in any order. They
are not necessarily in the order shown here. In
our FITS content handler, we first read all the keywords into a
Hashtable
and then extract the ones we want by
name.
The NAXIS header specifies the number of axes (that is, the dimension) of the primary data array. A NAXIS value of one identifies a one-dimensional image. A NAXIS value of two indicates a normal two-dimensional rectangular image. A NAXIS value of three is called a data cube and generally means the file contains a series of pictures of the same object taken at different moments in time. In other words, time is the third dimension. On rare occasions, the third dimension can represent depth: i.e., the file contains a true three-dimensional image. A NAXIS of four means the file contains a sequence of three-dimensional pictures taken at different moments in time. Higher values of NAXIS, while theoretically possible, are rarely seen in practice. Our example is going to look at only the first two-dimensional image in a file.
The NAXISn headers (where n is an integer ranging from 1 to NAXIS) give the length of the image in pixels along that dimension. In this example, NAXIS1 is 242, so the image is 242 pixels wide. NAXIS2 is 252, so this image is 252 pixels high. Since FITS images are normally pictures of astronomical bodies like the sun, it doesn’t really matter if you reverse width and height. All FITS images contain the SIMPLE, BITPIX, END, and NAXIS keywords, plus a series of NAXISn keywords. These keywords all provide information that is essential for displaying the image.
The next five keywords are specific to this file and may not be present in other FITS files. They give meaning to the image, though they are not needed to display it. The DATE keyword says this image was taken on August 19, 1996. The TELESC keyword says this image was taken by the Vacuum Tower Telescope (VTT) at the National Solar Observatory (NSO) on Sacramento Peak (SP). The IMAGE keyword says that this is a picture of the white light continuum; images taken through spectrographs might look at only a particular wavelength in the spectrum. The COORDS keyword gives the latitude and longitude of the telescope. Finally, the OBSTIME keyword says this image was taken at 1:59 P.M. Universal Time (essentially, Greenwich Mean Time). There are many more optional headers that don’t appear in this example. Like the five discussed here, the remaining keywords may help someone interpret an image, but they don’t provide the information needed to display it.
The keyword END terminates the header. Following the END keyword, the header is padded with spaces so that it fills a 2,880-byte block. A header may take up more than one 2,880-byte block, but it must always be padded to an integral number of blocks.
The image data follows the header. How the image is stored depends on
the value of BITPIX, as explained earlier. Fortunately, these data
types are stored in formats (big-endian, two’s complement) that
can be read directly with a DataInputStream
. The
exact meaning of each number in the image data is completely
file-dependent. More often than not, it’s the number of
electrons that were collected in a specific time interval by a
particular pixel in a charge coupled device (CCD); in older FITS
files, the numbers could represent the value read from photographic
film by a densitometer. However, the unifying theme is that larger
numbers represent brighter light. To interpret these numbers as a
grayscale image, you map the smallest value in the data to pure
black, the largest value in the data to pure white, and scale all
intermediate values appropriately. A general-purpose FITS reader
cannot interpret the numbers as anything except abstract brightness
levels. Without scaling, differences tend to get washed out. For
example, a dark spot on the Sun tends to be about 4,000K. That is
dark compared to the normal solar surface temperature of 6,000K, but
considerably brighter than anything you’re likely to see on the
surface of the Earth.
Example 17.9 is a FITS content hander. FITS files
should be served with the MIME type image/x-fits
.
This is almost certainly not included in your server’s default
MIME-type mappings, so make sure to add a mapping between files that
end in .fit
, .fts
, or
.fits
and the MIME type image/x-fits.
Example 17-9. An x-fits Content Handler
package com.macfaq.net.www.content.image; import java.net.*; import java.io.*; import java.awt.image.*; import java.util.*; public class x_fits extends ContentHandler { public Object getContent(URLConnection uc) throws IOException { int width = -1; int height = -1; int bitpix = 16; int[] data = null; int naxis = 2; Hashtable header = null; DataInputStream dis = new DataInputStream(uc.getInputStream( )); header = readHeader(dis); bitpix = getIntFromHeader("BITPIX ", -1, header); if (bitpix <= 0) return null; naxis = getIntFromHeader("NAXIS ", -1, header); if (naxis < 1) return null; width = getIntFromHeader("NAXIS1 ", -1, header); if (width <= 0) return null; if (naxis == 1) height = 1; else height = getIntFromHeader("NAXIS2 ", -1, header); if (height <= 0) return null; if (bitpix == 16) { short[] theInput = new short[height * width]; for (int i = 0; i < theInput.length; i++) { theInput[i] = dis.readShort( ); } data = scaleArray(theInput); } else if (bitpix == 32) { int[] theInput = new int[height * width]; for (int i = 0; i < theInput.length; i++) { theInput[i] = dis.readInt( ); } data = scaleArray(theInput); } else if (bitpix == 64) { long[] theInput = new long[height * width]; for (int i = 0; i < theInput.length; i++) { theInput[i] = dis.readLong( ); } data = scaleArray(theInput); } else if (bitpix == -32) { float[] theInput = new float[height * width]; for (int i = 0; i < theInput.length; i++) { theInput[i] = dis.readFloat( ); } data = scaleArray(theInput); } else if (bitpix == -64) { double[] theInput = new double[height * width]; for (int i = 0; i < theInput.length; i++) { theInput[i] = dis.readDouble( ); } data = scaleArray(theInput); } else { System.err.println("Invalid BITPIX"); return null; } // end if-else-if return new MemoryImageSource(width, height, data, 0, width); } // end getContent private Hashtable readHeader(DataInputStream dis) throws IOException { int blocksize = 2880; int fieldsize = 80; String key, value; int linesRead = 0; byte[] buffer = new byte[fieldsize]; Hashtable header = new Hashtable( ); while (true) { dis.readFully(buffer); key = new String(buffer, 0, 8, "ASCII"); linesRead++; if (key.substring(0, 3).equals("END")) break; if (buffer[8] != '=' || buffer[9] != ' ') continue; value = new String(buffer, 10, 20, "ASCII"); header.put(key, value); } int linesLeftToRead = (blocksize - ((linesRead * fieldsize) % blocksize))/fieldsize; for (int i = 0; i < linesLeftToRead; i++) dis.readFully(buffer); return header; } private int getIntFromHeader(String name, int defaultValue, Hashtable header) { String s = ""; int result = defaultValue; try { s = (String) header.get(name); } catch (NullPointerException e) { return defaultValue; } try { result = Integer.parseInt(s.trim( )); } catch (NumberFormatException e) { System.err.println(e); System.err.println(s); return defaultValue; } return result; } // parameterized types (templates) would help a lot here private int[] scaleArray(short[] theInput) { int data[] = new int[theInput.length]; int max = 0; int min = 0; for (int i = 0; i < theInput.length; i++) { if (theInput[i] > max) max = theInput[i]; if (theInput[i] < min) min = theInput[i]; } long r = max - min; double a = 255.0/r; double b = -a * min; int opaque = 255; for (int i = 0; i < data.length; i++) { int temp = (int) (theInput[i] * a + b); data[i] = (opaque << 24) | (temp << 16) | (temp << 8) | temp; } return data; } private int[] scaleArray(int[] theInput) { int data[] = new int[theInput.length]; int max = 0; int min = 0; for (int i = 0; i < theInput.length; i++) { if (theInput[i] > max) max = theInput[i]; if (theInput[i] < min) min = theInput[i]; } long r = max - min; double a = 255.0/r; double b = -a * min; int opaque = 255; for (int i = 0; i < data.length; i++) { int temp = (int) (theInput[i] * a + b); data[i] = (opaque << 24) | (temp << 16) | (temp << 8) | temp; } return data; } private int[] scaleArray(long[] theInput) { int data[] = new int[theInput.length]; long max = 0; long min = 0; for (int i = 0; i < theInput.length; i++) { if (theInput[i] > max) max = theInput[i]; if (theInput[i] < min) min = theInput[i]; } long r = max - min; double a = 255.0/r; double b = -a * min; int opaque = 255; for (int i = 0; i < data.length; i++) { int temp = (int) (theInput[i] * a + b); data[i] = (opaque << 24) | (temp << 16) | (temp << 8) | temp; } return data; } private int[] scaleArray(double[] theInput) { int data[] = new int[theInput.length]; double max = 0; double min = 0; for (int i = 0; i < theInput.length; i++) { if (theInput[i] > max) max = theInput[i]; if (theInput[i] < min) min = theInput[i]; } double r = max - min; double a = 255.0/r; double b = -a * min; int opaque = 255; for (int i = 0; i < data.length; i++) { int temp = (int) (theInput[i] * a + b); data[i] = (opaque << 24) | (temp << 16) | (temp << 8) | temp; } return data; } private int[] scaleArray(float[] theInput) { int data[] = new int[theInput.length]; float max = 0; float min = 0; for (int i = 0; i < theInput.length; i++) { if (theInput[i] > max) max = theInput[i]; if (theInput[i] < min) min = theInput[i]; } double r = max - min; double a = 255.0/r; double b = -a * min; int opaque = 255; for (int i = 0; i < data.length; i++) { int temp = (int) (theInput[i] * a + b); data[i] = (opaque << 24) | (temp << 16) | (temp << 8) | temp; } return data; } }
The key method of the x_fits
class is
getContent( )
; it is the one method that the
ContentHandler
class requires subclasses to
implement. The other methods in this class are all simply utility
methods that help to break up the program into easier-to-digest
chunks. getContent( )
is called by a
URLConnection
, which passes a reference to itself
in the argument uc
. The getContent( )
method reads data from that
URLConnection
and uses it to construct an object
that implements the ImageProducer
interface. To
simplify the task of creating an ImageProducer
, we
create an array of image data and use a
MemoryImageSource
object, which implements the
ImageProducer
interface, to convert that array
into an image. getContent( )
returns this
MemoryImageSource
.
MemoryImageSource
has several constructors. The
one we use here requires us to provide the width and height of the
image, an array of integer values containing the RGB data for each
pixel, the offset of the start of that data in the array, and the
number of pixels per line in the array:
public MemoryImageSource(int width, int height, int[] pixels, int offset, int scanlines);
The width, height, and pixel data can be read from the header of the FITS image. Since we are creating a new array to hold the pixel data, the offset is zero and the scanlines are the width of the image.
Our content handler has a utility method called readHeader( )
that reads the image header from
uc
’s InputStream
. This
method returns a Hashtable
containing the keywords
and their values as String
objects. Comments are
thrown away. readHeader( )
reads 80 bytes at a
time, since that’s the length of each field. The first eight
bytes are transformed into the String
key
. If there is no key, the line is a comment and
is ignored. If there is a key, then the eleventh through thirtieth
bytes are stored in a String
called
value
. The key-value pair is stored in the
Hashtable
. This continues until the END keyword is
spotted. At this point, we break out of the loop and read as many
lines as necessary to finish the block. (Recall that the header is
padded with spaces to make an integral multiple of 2,880). Finally,
readHeader( )
returns the Hashtable header
.
After the header has been read into the Hashtable
header
, the InputStream
is now
pointing at the first byte of data. However, before we’re ready
to read the data, we must extract the height, width, and bits per
pixel of the primary data unit from the header. These are all integer
values, so to simplify the code, we use the
getIntFromHeader(String name, int defaultValue, Hashtable header)
method. This method takes as arguments the name of
the header whose value we want (e.g., BITPIX), a default value for
that header, and the Hashtable
that contains the
header. This method retrieves the value associated with the string
name
from the Hashtable
and
casts the result to a String
object—we know
this cast is safe because we put only String
data
into the Hashtable
. This String
is then converted to an int
using
Integer.parseInt(s.trim( ))
; we then return the
resulting int
. If an exception is thrown,
getIntFromHeader( )
returns the
defaultValue
argument instead. In this content
handler, we use an impossible flag value (-1) as the default to
indicate that getIntFromHeader( )
failed.
getContent( )
uses getIntFromHeader( )
to retrieve four crucial values from the header: NAXIS,
NAXIS1, NAXIS2, and BITPIX. NAXIS is the number of dimensions in the
primary data array; if it is greater than or equal to two, we read
the width and height from NAXIS1 and NAXIS2. If there are more than
two dimensions, we still read a single two-dimensional frame from the
data. A more advanced FITS content handler might read subsequent
frames and include them below the original image, or display the
sequence of images as an animation. If NAXIS is one, the width is
read from NAXIS1, and the height is set to one.[30] If
NAXIS is less than one, there’s no image data at all, so we
return null
.
Now we are ready to read the image data. The data can be stored in
one of five formats, depending on the value of BITPIX: unsigned
bytes, short
s, int
s,
float
s, or double
s. This is
where the lack of parameterized types and templates in Java makes
coding painful: we need to repeat the algorithm for reading data five
times, once for each of the five possible data types. In each case,
the data is first read from the stream into an array of the
appropriate type called theInput
. Then this array
is passed to the scaleArray( )
method, which
returns a scaled array. scaleArray( )
is an
overloaded method that reads the data in theInput
and copies the data into the int
array
theData
, while scaling the data to fall from
to 255; there is a different version of scaleArray( )
for each of the five data types we might need to handle.
Thus, no matter what format the data starts in, it becomes an
int
array with values from
to 255. This data now needs to be converted into grayscale RGB
values. The standard 32-bit RGB color model allows 256 different
shades of gray ranging from pure black to pure white; 8 bits are used
to represent opacity, usually called “alpha”. To get a
particular shade of gray, the red, green, and blue bytes of an RGB
triple should all be set to the same value, and the alpha value
should be 255 (fully opaque). Thinking of these as four byte values,
you need colors like 255.127.127.127 (medium gray) or 255.255.255.255
(pure white). This is produced by the lines:
int temp = (int) (theInput[i] * a + b); theData[i] = (opaque << 24) | (temp << 16) | (temp << 8) | temp;
Once it has converted every pixel in theInput[]
into a 32-bit color value and stored the result in
theData[]
, scaleArray( )
returns theData
. The only thing left for
getContent( )
to do is feed this array, along with
the header values previously retrieved, into the
MemoryImageSource
constructor and return the
result.
This FITS content handler has one glaring problem. The image has to
be completely loaded before the method returns. Since FITS images are
quite literally astronomical in size, loading the image can take a
significant amount of time. It would be better to create a new class
for FITS images that implements the ImageProducer
interface and into which the data can be streamed asynchronously. The
ImageConsumer
that eventually displays the image
can use the methods of ImageProducer
to determine
when the height and width are available, when a new scanline has been
read, when the image is completely loaded or errored out, and so on.
getContent( )
would spawn a separate thread to
feed the data into the ImageProducer
and would
return almost immediately. However, a FITS
ImageProducer
would not be able to take
significant advantage of progressive loading, because the file format
doesn’t define unambiguously what each data value means; before
you can generate RGB pixels, you must read all of the data and find
the minimum and maximum values.
Example 17.10 is a simple
ContentHandlerFactory
that recognizes FITS images.
For all types other than image/x-fits, it returns null so that the
default locations will be searched for content handlers.
Example 17-10. The FITS ContentHandlerFactory
import java.net.*; public class FitsFactory implements ContentHandlerFactory { public ContentHandler createContentHandler(String mimeType) { if (mimeType.equalsIgnoreCase("image/x-fits")) { return new com.macfaq.net.www.content.image.x_fits( ); } return null; } }
Example 17.11 is a simple program that tests this
content handler by loading and displaying a FITS image from a URL. In
fact, it can display any image type for which a content handler is
installed. However, it does use the FitsFactory
to
recognize FITS images.
Example 17-11. The FITS Viewer
import java.awt.*; import javax.swing.*; import java.awt.image.*; import java.net.*; import java.io.*; public class FitsViewer extends JFrame { private URL url; private Image theImage; public FitsViewer(URL u) { super(u.getFile( )); this.url = u; } public void loadImage( ) throws IOException { Object content = this.url.getContent( ); ImageProducer producer; try { producer = (ImageProducer) content; } catch (ClassCastException e) { throw new IOException("Unexpected type " + content.getClass( )); } if (producer == null) theImage = null; else { theImage = this.createImage(producer); int width = theImage.getWidth(this); int height = theImage.getHeight(this); if (width > 0 && height > 0) this.setSize(width, height); } } public void paint(Graphics g) { if (theImage != null) g.drawImage(theImage, 0, 0, this); } public static void main(String[] args) { URLConnection.setContentHandlerFactory(new FitsFactory( )); for (int i = 0; i < args.length; i++) { try { FitsViewer f = new FitsViewer(new URL(args[i])); f.setSize(252, 252); f.loadImage( ); f.show( ); } catch (MalformedURLException e) { System.err.println(args[i] + " is not a URL I recognize."); } catch (IOException e) { e.printStackTrace( ); } } } }
The FitsViewer
program extends
JFrame
. The main( )
method
loops through all the command-line arguments creating a new window
for each one. Then it loads the image into the window and shows it.
The loadImage( )
method actually downloads the
requested picture by implicitly using the content handler of Example 17.9 to convert the FITS data into a
java.awt.Image
object stored in the field
theImage
. If the width and the height of the image
are available (as they will be for a FITS image using our content
handler but maybe not for some other image types that load the image
in a separate thread), then the window is resized to the exact size
of the image. The paint( )
method simply draws
this image on the screen. Most of the work is done inside the content
handler. In fact, this program can actually display images of any
type for which a content handler is installed and available. For
instance, it works equally well for GIF and JPEG images. Figure 17.2 shows this program displaying a picture of
part of solar granulation.
[29] For more details about the FITS format and how to handle FITS files, see The Encyclopedia of Graphics File Formats, 2nd ed., by James D. Murray and William vanRyper, pp. 392-400 (O’Reilly & Associates, Inc.)
[30] A FITS file with NAXIS as one would typically be produced from observations that used a one-dimensional CCD.
18.189.2.122