17.4 A Content Handler for the FITS Image Format
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. This kind of
handler differs 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
InputStream 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 often 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. For more details
about the FITS format and how to handle FITS files, see The
Encyclopedia of Graphics File Formats by James D. Murray
and William vanRyper (O'Reilly).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 using the Dunn Solar Telescope at
the National Solar Observatory in Sunspot, New Mexico (http://www.sunspot.noao.edu/):
SIMPLE = T /Every FITS file
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
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 0 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, although
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, we 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 handler. 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;The key method of the x_fits class is
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 ex) {
return defaultValue;
}
try {
result = Integer.parseInt(s.trim( ));
}
catch (NumberFormatException ex) {
System.err.println(ex);
System.err.println(s);
return defaultValue;
}
return result;
}
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;
}
}
getContent( ); it is the one method that the
ContentHandler class requires subclasses to
implement. The other methods in this class are all 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 invoked 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,The width, height, and pixel data can be read from the header of the
int offset, int scanlines);
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,
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 objectwe 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. (A FITS file with
NAXIS as one would typically be produced from observations that used
a one-dimensional CCD.) 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, shorts, ints,
floats, or doubles. This is
where the lack of generics that can handle primitive types 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 0 to
255; there is a different version of scaleArray( )
for each of the five data types it might need to handle. Thus, no
matter what format the data starts in, it becomes an
int array with values from 0 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,
we need colors like 255.127.127.127 (medium gray) or 255.255.255.255
(pure white). These colors are produced by the lines:
int temp = (int) (theInput[i] * a + b);Once it has converted every pixel in theInput[]
theData[i] = (opaque << 24) | (temp << 16) | (temp << 8) | temp;
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 unambiguously define what each data value
means; before we can generate RGB pixels, we 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.*;Example 17-11 is a simple program that tests this content handler by
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;
}
}
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.*;The FitsViewer program extends
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 ex) {
System.err.println(args[i] + " is not a URL I recognize.");
}
catch (IOException ex) {
ex.printStackTrace( );
}
}
}
}
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.
Figure 17-2. The FitsViewer application displaying a FITS image of solar granulation
