CONTENTS | PREV | NEXT |
ImageReaderSpi
subclass, and an
ImageReader
subclass. Optionally, it may contain
implementations of the IIOMetadata
interface
representing the stream and image metadata, and an
IIOMetadataFormat
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
, and
MyFormatMetadata
.
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
The MyFormatImageReaderSpi
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 the
ImageWriterSpi
s of plug-ins that are able to
interoperate specially with the reader. It also must provide an
implementation of the canDecodeInput
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 the MyFormatImageReaderSpi
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 from
ImageInputStream
sources, since it is possible to
"wrap" most other types of input with an appropriate
ImageInputStream
. However, it is possible for a
plug-in to work directly with other Object
s, for
example an Object
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 of Class
objects. An
implementation of getInputTypes
is provided in the
superclass, which returns the value of the inputTypes
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 element
javax.imageio.stream.ImageInputStream.class
,
indicating that the plug-in accepts only
ImageInputStream
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 an
ImageInputStream
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 the
ImageReader
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. The
ImageTypeSpecifier
class is
used to hold a SampleModel
and ColorModel
indicating a legal image layout. The getImageTypes
method returns an Iterator
of
ImageTypeSpecifier
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 a
readHeader
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 the
read
method:
public BufferedImage read(int imageIndex, ImageReadParam param) throws IIOException { readMetadata(); // Stream is positioned at start of image dataHandling 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. The
ImageReader.getDestination
method will
return any image that was specified using
ImageReadParam.setDestination
, or else will create a
suitable destination image using a supplied
ImageTypeSpecifier
, in this case determined by calling
getImageTypes(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 a
Raster
to hold a row's worth
of data, and copy the pixels from that Raster
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 the
Raster
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 the ImageReadParam
:
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 where
sourceXSubsampling
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 in
MyFormatImageReader
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 method
readMetadata
, 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 called
MyFormatMetadata
that extends the
IIOMetadata
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 is
getAsTree
:
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 the
isReadOnly
, reset
, and
mergeTree
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; } }