CONTENTS | PREV | NEXT | JavaTM Image I/O API Guide |
A minimal reader plug-in consists of anImageReaderSpi
subclass, and anImageReader
subclass. Optionally, it may contain implementations of theIIOMetadata
interface representing the stream and image metadata, and anIIOMetadataFormat
object describing the structure of the metadata.In the following sections, we will sketch out the implementation of a simple reader plug-in for a hypothetical format called "MyFormat". It will consist of the classes
MyFormatImageReaderSpi
,MyFormatImageReader
, andMyFormatMetadata
.The format itself is defined to begin with the characters `myformat\n', followed by two four-byte integers representing the width, height, and a single byte indicating the color type of the image, which may be either gray or RGB. Next, after a newline character, metadata values may stored as alternating lines containing a keyword and a value, terminated by the special keyword `END'. The string values are stored using UTF8 encoding followed by a newline. Finally, the image samples are stored in left-to-right, top-to-bottom order as either byte grayscale values, or three bytes representing red, green, and blue.
MyFormatImageReaderSpi
TheMyFormatImageReaderSpi
class provides information about the plug-in, including the vendor name, plug-in version string and description, format name, file suffixes associated with the format, MIME types associated with the format, input source classes that the plug-in can handle, and theImageWriterSpi
s of plug-ins that are able to interoperate specially with the reader. It also must provide an implementation of thecanDecodeInput
method, which is used to locate plug-ins based on the contents of a source image file.The
ImageReaderSpi
class provides implementations of most of its methods. These methods mainly return the value of various protected instance variables, which theMyFormatImageReaderSpi
may set directly or via the superclass constructor, as in the example below:
package com.mycompany.imageio; import java.io.IOException; import java.util.Locale; import javax.imageio.ImageReader; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; public class MyFormatImageReaderSpi extends ImageReaderSpi { static final String vendorName = "My Company"; static final String version = "1.0_beta33_build9467"; static final String readerClassName = "com.mycompany.imageio.MyFormatImageReader"; static final String[] names = { "myformat" }; static final String[] suffixes = { "myf" }; static final String[] MIMETypes = { "image/x-myformat" }; static final String[] writerSpiNames = { "com.mycompany.imageio.MyFormatImageWriterSpi" }; // Metadata formats, more information below static final boolean supportsStandardStreamMetadataFormat = false; static final String nativeStreamMetadataFormatName = null static final String nativeStreamMetadataFormatClassName = null; static final String[] extraStreamMetadataFormatNames = null; static final String[] extraStreamMetadataFormatClassNames = null; static final boolean supportsStandardImageMetadataFormat = false; static final String nativeImageMetadataFormatName = "com.mycompany.imageio.MyFormatMetadata_1.0"; static final String nativeImageMetadataFormatClassName = "com.mycompany.imageio.MyFormatMetadata"; static final String[] extraImageMetadataFormatNames = null; static final String[] extraImageMetadataFormatClassNames = null; public MyFormatImageReaderSpi() { super(vendorName, version, names, suffixes, MIMETypes, readerClassName, STANDARD_INPUT_TYPE, // Accept ImageInputStreams writerSpiNames, supportsStandardStreamMetadataFormat, nativeStreamMetadataFormatName, nativeStreamMetadataFormatClassName, extraStreamMetadataFormatNames, extraStreamMetadataFormatClassNames, supportsStandardImageMetadataFormat, nativeImageMetadataFormatName, extraImageMetadataFormatNames, extraImageMetadataFormatClassNames); } public String getDescription(Locale locale) { // Localize as appropriate return "Description goes here"; } public boolean canDecodeInput(Object input) throws IOException { // see below } public ImageReader createReaderInstance(Object extension) { return new MyFormatImageReader(this); } }
Most plug-ins need read only fromImageInputStream
sources, since it is possible to "wrap" most other types of input with an appropriateImageInputStream
. However, it is possible for a plug-in to work directly with otherObject
s, for example anObject
that provides an interface to a digital camera or scanner. This interface need not provide a "stream" view of the device at all. Rather, a plug-in that is aware of the interface may use it to drive the device directly.The plug-in advertises which input classes it can handle via its
getInputTypes
method, which returns an array ofClass
objects. An implementation ofgetInputTypes
is provided in the superclass, which returns the value of theinputTypes
instance variable, which in turn is set by the seventh argument to the superclass constructor. The value used in the example above,STANDARD_INPUT_TYPE
, is shorthand for an array containing the single elementjavax.imageio.stream.ImageInputStream.class
, indicating that the plug-in accepts onlyImageInputStream
s.The
canDecodeInput
method is responsible for determining two things: first, whether the input parameter is an instance of a class that the plug-in can understand, and second, whether the file contents appear to be in the format handled by the plug-in. It must leave its input in the same state as it was when it was passed in. For anImageInputStream
input source, the mark and reset methods may be used. For example, since files in the "MyFormat" format all begin with the characters `myformat',canDecodeInput
may be implemented as:
public boolean canDecodeInput(Object input) { if (!(input instanceof ImageInputStream)) { return false; } ImageInputStream stream = (ImageInputStream)input; byte[] b = new byte[8]; try { stream.mark(); stream.readFully(b); stream.reset(); } catch (IOException e) { return false; } // Cast unsigned character constants prior to comparison return (b[0] == (byte)'m' && b[1] == (byte)'y' && b[2] == (byte)'f' && b[3] == (byte)'o' && b[4] == (byte)'r' && b[5] == (byte)'m' && b[6] == (byte)'a' && b[7] == (byte)'t'); }
MyFormatImageReader
The heart of a reader plug-in is its extension of theImageReader
class. This class is responsible for responding to queries about the images actually stored in an input file or stream, as well as the actual reading of images, thumbnails, and metadata. For simplicity, we will ignore thumbnail images in this example.A sketch of some of the methods of a hypothetical MyFormatImageReader class is shown below:
package com.mycompany.imageio; public class MyFormatImageReader extends ImageReader { ImageInputStream stream = null; int width, height; int colorType; // Constants enumerating the values of colorType static final int COLOR_TYPE_GRAY = 0; static final int COLOR_TYPE_RGB = 1; boolean gotHeader = false; public MyFormatImageReader(ImageReaderSpi originatingProvider) { super(originatingProvider); } public void setInput(Object input, boolean isStreamable) { super.setInput(input, isStreamable); if (input == null) { this.stream = null; return; } if (input instanceof ImageInputStream) { this.stream = (ImageInputStream)input; } else { throw new IllegalArgumentException("bad input"); } } public int getNumImages(boolean allowSearch) throws IIOException { return 1; // format can only encode a single image } private void checkIndex(int imageIndex) { if (imageIndex != 0) { throw new IndexOutOfBoundsException("bad index"); } } public int getWidth(int imageIndex) throws IIOException { checkIndex(imageIndex); // must throw an exception if != 0 readHeader(); return width; } public int getHeight(int imageIndex) throws IIOException { checkIndex(imageIndex); readHeader(); return height; }
The getImageTypes Method
The reader is responsible for indicating what sorts of images may be used to hold the decoded output. TheImageTypeSpecifier
class is used to hold aSampleModel
andColorModel
indicating a legal image layout. ThegetImageTypes
method returns anIterator
ofImageTypeSpecifier
s:
public Iterator getImageTypes(int imageIndex) throws IIOException { checkIndex(imageIndex); readHeader(); ImageTypeSpecifier imageType = null; int datatype = DataBuffer.TYPE_BYTE; java.util.List l = new ArrayList(); switch (colorType) { case COLOR_TYPE_GRAY: imageType = ImageTypeSpecifier.createGrayscale(8, datatype, false); break; case COLOR_TYPE_RGB: ColorSpace rgb = ColorSpace.getInstance(ColorSpace.CS_sRGB); int[] bandOffsets = new int[3]; bandOffsets[0] = 0; bandOffsets[1] = 1; bandOffsets[2] = 2; imageType = ImageTypeSpecifier.createInterleaved(rgb, bandOffsets, datatype, false, false); break; } l.add(imageType); return l.iterator(); }
Parsing the Image Header
Several of the methods above depend on areadHeader
method, which is responsible for reading enough of the input stream to determine the width, height, and layout of the image.readHeader
is defined so it is safe to be called multiple times (note that we are not concerned with multi-threaded access):
public void readHeader() { if (gotHeader) { return; } gotHeader = true; if (stream == null) { throw new IllegalStateException("No input stream"); } // Read `myformat\n' from the stream byte[] signature = new byte[9]; try { stream.readFully(signature); } catch (IOException e) { throw new IIOException("Error reading signature", e); } if (signature[0] != (byte)'m' || ...) { // etc. throw new IIOException("Bad file signature!"); } // Read width, height, color type, newline try { this.width = stream.readInt(); this.height = stream.readInt(); this.colorType = stream.readUnsignedByte(); stream.readUnsignedByte(); // skip newline character } catch (IOException e) { throw new IIOException("Error reading header", e) } }
The actual reading of the image is handled by theread
method:
public BufferedImage read(int imageIndex, ImageReadParam param) throws IIOException { readMetadata(); // Stream is positioned at start of image data
Handling the ImageReadParam
The first section of the method is concerned with using a supplied ImageReadParam object to determine what region of the source image is to be read, what sort of subsampling is to be applied, the selection and rearrangement of bands, and the offset in the destination:
// Compute initial source region, clip against destination later Rectangle sourceRegion = getSourceRegion(param, width, height); // Set everything to default values int sourceXSubsampling = 1; int sourceYSubsampling = 1; int[] sourceBands = null; int[] destinationBands = null; Point destinationOffset = new Point(0, 0); // Get values from the ImageReadParam, if any if (param != null) { sourceXSubsampling = param.getSourceXSubsampling(); sourceYSubsampling = param.getSourceYSubsampling(); sourceBands = param.getSourceBands(); destinationBands = param.getDestinationBands(); destinationOffset = param.getDestinationOffset(); }
At this point, the region of interest, subsampling, band selection, and destination offset have been initialized. The next step is to create a suitable destination image. TheImageReader.getDestination
method will return any image that was specified usingImageReadParam.setDestination
, or else will create a suitable destination image using a suppliedImageTypeSpecifier
, in this case determined by callinggetImageTypes(0)
:
// Get the specified detination image or create a new one BufferedImage dst = getDestination(param, getImageTypes(0), width, height); // Enure band settings from param are compatible with images int inputBands = (colorType == COLOR_TYPE_RGB) ? 3 : 1; checkReadParamBandSettings(param, inputBands, dst.getSampleModel().getNumBands());
To reduce the amount of code we have to write, we create aRaster
to hold a row's worth of data, and copy the pixels from thatRaster
into the actual image. In this way, band selection and the details of pixel formatting are taken care of, at the expense of an additional copy.
int[] bandOffsets = new int[inputBands]; for (int i = 0; i < inputBands; i++) { bandOffsets[i] = i; } int bytesPerRow = width*inputBands; DataBufferByte rowDB = new DataBufferByte(bytesPerRow); WritableRaster rowRas = Raster.createInterleavedRaster(rowDB, width, 1, bytesPerRow, inputBands, bandOffsets, new Point(0, 0)); byte[] rowBuf = rowDB.getData(); // Create an int[] that can a single pixel int[] pixel = rowRas.getPixel(0, 0, (int[])null);
Now we have a byte array,rowBuf
, which can be filled in from the input data, and which is also the source of pixel data for theRaster
rowRaster
. We extract the (single) tile of the destination image, and determine its extent. Then we create child rasters of both the source and destination that select and order their bands according to the settings previously extracted from theImageReadParam
:
WritableRaster imRas = dst.getWritableTile(0, 0); int dstMinX = imRas.getMinX(); int dstMaxX = dstMinX + imRas.getWidth() - 1; int dstMinY = imRas.getMinY(); int dstMaxY = dstMinY + imRas.getHeight() - 1; // Create a child raster exposing only the desired source bands if (sourceBands != null) { rowRas = rowRas.createWritableChild(0, 0, width, 1, 0, 0, sourceBands); } // Create a child raster exposing only the desired dest bands if (destinationBands != null) { imRas = imRas.createWritableChild(0, 0, imRas.getWidth(), imRas.getHeight(), 0, 0, destinationBands); }
Reading the Pixel Data
Now we are ready to begin read pixel data from the image. We will read whole rows, and perform subsampling and destination clipping as we proceed. The horizontal clipping is complicated by the need to take subsampling into account. Here we perform per-pixel clipping; a more sophisticated reader could perform horizontal clipping once:
for (int srcY = 0; srcY < height; srcY++) { // Read the row try { stream.readFully(rowBuf); } catch (IOException e) { throw new IIOException("Error reading line " + srcY, e); } // Reject rows that lie outside the source region, // or which aren't part of the subsampling if ((srcY < sourceRegion.y) || (srcY >= sourceRegion.y + sourceRegion.height) || (((srcY - sourceRegion.y) % sourceYSubsampling) != 0)) { continue; } // Determine where the row will go in the destination int dstY = destinationOffset.y + (srcY - sourceRegion.y)/sourceYSubsampling; if (dstY < dstMinY) { continue; // The row is above imRas } if (dstY > dstMaxY) { break; // We're done with the image } // Copy each (subsampled) source pixel into imRas for (int srcX = sourceRegion.x; srcX < sourceRegion.x + sourceRegion.width; srcX++) { if (((srcX - sourceRegion.x) % sourceXSubsampling) != 0) { continue; } int dstX = destinationOffset.x + (srcX - sourceRegion.x)/sourceXSubsampling; if (dstX < dstMinX) { continue; // The pixel is to the left of imRas } if (dstX > dstMaxX) { break; // We're done with the row } // Copy the pixel, sub-banding is done automatically rowRas.getPixel(srcX, 0, pixel); imRas.setPixel(dstX, dstY, pixel); } } return dst;
For performance, the case wheresourceXSubsampling
is equal to 1 may be broken out separately, since it is possible to copy multiple pixels at once:
// Create an int[] that can hold a row's worth of pixels int[] pixels = rowRas.getPixels(0, 0, width, 1, (int[])null); // Clip against the left and right edges of the destination image int srcMinX = Math.max(sourceRegion.x, dstMinX - destinationOffset.x + sourceRegion.x); int srcMaxX = Math.min(sourceRegion.x + sourceRegion.width - 1, dstMaxX - destinationOffset.x + sourceRegion.x); int dstX = destinationOffset.x + (srcMinX - sourceRegion.x); int w = srcMaxX - srcMinX + 1; rowRas.getPixels(srcMinX, 0, w, 1, pixels); imRas.setPixels(dstX, dstY, w, 1, pixels);
There are several additional features that readers should implement, namely informing listeners of the progress of the read, and allowing the read process to be aborted from another thread.
Listeners
There are three types of listeners that may be attached to a reader: IIOReadProgressListener, IIOReadUpdateListener, and IIOReadWarningListener. Any number of each type may be attached to a reader by means of various add and remove methods that are implemented in the ImageReader superclass. ImageReader also contains various process methods that broadcast information to all of the attached listeners of a given type. For example, when the image read begins, the method processImageStarted(imageIndex) should be called to inform all attached IIOReadProgressListeners of the event.A reader plug-in is normally responsible for calling processImageStarted and processImageComplete at the beginning and end of its read method, respectively. processImageProgress should be called at least every few scanlines with an estimate of the percentage completion of the read. It is important that this percentage never decrease during the read of a single image. If the reader supports thumbnails, the corresponsing thumbnail progress methods should be called as well. The processSequenceStarted and processSequenceComplete methods of IIOReadProgressListener only need to be called if the plug-in overrides the superclass implementation of readAll.
More advanced readers that process incoming data in multiple passes may choose to support IIOReadUpdateListeners, which receive more detauled information about which pixels have been read so far. Applications may use this information to perform selective updates of an on-screen image, for example, or to re-encode image data in a streaming fashion.
Aborting the Read Process
While one thread performs an image read, another thread may call the reader's abort method asynchronously. The reading thread should poll the reader's status periodically using the abortRequested method, and attempt to cut the decoding short. The partially decoded image should still be returned, although the reader need not make any guarantees about its contents. For example, it could contain compressed or encrypted data in its DataBuffer that does not make sense visually.
IIOReadProgressListener Example
A typical set of IIOReadProgressListener calls might look like this:
public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { // Clear any previous abort request boolean aborted = false; clearAbortRequested(); // Inform IIOReadProgressListeners of the start of the image processImageStarted(imageIndex); // Compute xMin, yMin, xSkip, ySkip from the ImageReadParam // ... // Create a suitable image BufferedImage theImage = new BufferedImage(...); // Compute factors for use in reporting percentages int pixelsPerRow = (width - xMin + xSkip - 1)/xSkip; int rows = (height - yMin + ySkip - 1)/ySkip; long pixelsDecoded = 0L; long totalPixels = rows*pixelsPerRow; for (int y = yMin; y < height; y += yskip) { // Decode a (subsampled) scanline of the image // ... // Update the percentage estimate // This may be done only every few rows if desired pixelsDecoded += pixelsPerRow; processImageProgress(100.0F*pixelsDecoded/totalPixels); // Check for an asynchronous abort request if (abortRequested()) { aborted = true; break; } } // Handle the end of decoding if (aborted) { processImageAborted(); } else { processImageComplete(imageIndex); } // If the read was aborted, we still return a partially decoded image return theImage; }
Metadata
The next set of methods inMyFormatImageReader
deal with metadata. Because our hypothetical format only encodes a single image, we may ignore the concept of "stream" metadata, and use "image" metadata only:
MyFormatMetadata metadata = null; // class defined below public IIOMetadata getStreamMetadata() throws IIOException { return null; } public IIOMetadata getImageMetadata(int imageIndex) throws IIOException { if (imageIndex != 0) { throw new IndexOutOfBoundsException("imageIndex != 0!"); } readMetadata(); return metadata; }
The actual work is done by a format-specific methodreadMetadata
, which for this format fills in the keyword/value pairs of the metadata object,
public void readMetadata() throws IIOException { if (metadata != null) { return; } readHeader(); this.metadata = new MyFormatMetadata(); try { while (true) { String keyword = stream.readUTF(); stream.readUnsignedByte(); if (keyword.equals("END")) { break; } String value = stream.readUTF(); stream.readUnsignedByte(); metadata.keywords.add(keyword); metadata.values.add(value); } catch (IIOException e) { throw new IIOException("Exception reading metadata", e); } } }
MyFormatMetadata
Finally, the various interfaces for extracting and editing metadata must be defined. We define a class calledMyFormatMetadata
that extends theIIOMetadata
class, and additionally can store the keyword/value pairs that are allowed in the file format:
package com.mycompany.imageio; import org.w3c.dom.*; import javax.xml.parsers.*; // Package name may change in J2SDK 1.4 import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.imageio.metadata.IIOInvalidTreeException; import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataFormat; import javax.imageio.metadata.IIOMetadataNode; public class MyFormatMetadata extends IIOMetadata { static final boolean standardMetadataFormatSupported = false; static final String nativeMetadataFormatName = "com.mycompany.imageio.MyFormatMetadata_1.0"; static final String nativeMetadataFormatClassName = "com.mycompany.imageio.MyFormatMetadata"; static final String[] extraMetadataFormatNames = null; static final String[] extraMetadataFormatClassNames = null; // Keyword/value pairs List keywords = new ArrayList(); List values = new ArrayList();
The first set of methods are common to most IIOMetadata implementations:
public MyFormatMetadata() { super(standardMetadataFormatSupported, nativeMetadataFormatName, nativeMetadataFormatClassName, extraMetadataFormatNames, extraMetadataFormatClassNames); } public IIOMetadataFormat getMetadataFormat(String formatName) { if (!formatName.equals(nativeMetadataFormatName)) { throw new IllegalArgumentException("Bad format name!"); } return MyFormatMetadataFormat.getDefaultInstance(); }
The most important method for reader plug-ins isgetAsTree
:
public Node getAsTree(String formatName) { if (!formatName.equals(nativeMetadataFormatName)) { throw new IllegalArgumentException("Bad format name!"); } // Create a root node IIOMetadataNode root = new IIOMetadataNode(nativeMetadataFormatName); // Add a child to the root node for each keyword/value pair Iterator keywordIter = keywords.iterator(); Iterator valueIter = values.iterator(); while (keywordIter.hasNext()) { IIOMetadataNode node = new IIOMetadataNode("KeywordValuePair"); node.setAttribute("keyword", (String)keywordIter.next()); node.setAttribute("value", (String)valueIter.next()); root.appendChild(node); } return root; }
For writer plug-ins, the ability to edit metadata values is obtained by implementing theisReadOnly
,reset
, andmergeTree
methods:
public boolean isReadOnly() { return false; } public void reset() { this.keywords = new ArrayList(); this.values = new ArrayList(); } public void mergeTree(String formatName, Node root) throws IIOInvalidTreeException { if (!formatName.equals(nativeMetadataFormatName)) { throw new IllegalArgumentException("Bad format name!"); } Node node = root; if (!node.getNodeName().equals(nativeMetadataFormatName)) { fatal(node, "Root must be " + nativeMetadataFormatName); } node = node.getFirstChild(); while (node != null) { if (!node.getNodeName().equals("KeywordValuePair")) { fatal(node, "Node name not KeywordValuePair!"); } NamedNodeMap attributes = node.getAttributes(); Node keywordNode = attributes.getNamedItem("keyword"); Node valueNode = attributes.getNamedItem("value"); if (keywordNode == null || valueNode == null) { fatal(node, "Keyword or value missing!"); } // Store keyword and value keywords.add((String)keywordNode.getNodeValue()); values.add((String)valueNode.getNodeValue()); // Move to the next sibling node = node.getNextSibling(); } } private void fatal(Node node, String reason) throws IIOInvalidTreeException { throw new IIOInvalidTreeException(reason, node); } }
MyFormatMetadataFormat
The tree structure of the metadata may be described using the IIOMetadataFormat interface. An implementation class, IIOMetadataFormatImpl, takes care of maintaining the "database" of information about elements, their attributes, and the parent-child relationships between them:
package com.mycompany.imageio; import javax.imageio.ImageTypeSpecifier; import javax.imageio.metadata.IIOMetadataFormatImpl; public class MyFormatMetadataFormat extends IIOMetadataFormatImpl { // Create a single instance of this class (singleton pattern) private static MyFormatMetadataFormat defaultInstance = new MyFormatMetadataFormat(); // Make constructor private to enforce the singleton pattern private MyFormatMetadataFormat() { // Set the name of the root node // The root node has a single child node type that may repeat super("com.mycompany.imageio.MyFormatMetadata_1.0", CHILD_POLICY_REPEAT); // Set up the "KeywordValuePair" node, which has no children addElement("KeywordValuePair", "com.mycompany.imageio.MyFormatMetadata_1.0", CHILD_POLICY_EMPTY); // Set up attribute "keyword" which is a String that is required // and has no default value addAttribute("KeywordValuePair", "keyword", DATATYPE_STRING, true, null); // Set up attribute "value" which is a String that is required // and has no default value addAttribute("KeywordValuePair", "value", DATATYPE_STRING, true, null); } // Check for legal element name public boolean canNodeAppear(String elementName, ImageTypeSpecifier imageType) { return elementName.equals("KeywordValuePair"); } // Return the singleton instance public static MyFormatMetadataFormat getDefaultInstance() { return defaultInstance; } }