Java Network Programming, Third Edition

     

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 / 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 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 NAXIS n 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 NAXIS n 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; 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; } }

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 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, 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 , 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, short s, int s, float s, or double s. 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); 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 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.*; 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 ex) { System.err.println(args[i] + " is not a URL I recognize."); } catch (IOException ex) { ex.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.

Figure 17-2. The FitsViewer application displaying a FITS image of solar granulation

Категории