| 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
ImageWriterSpis 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 Objects, 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
ImageInputStreams.
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
ImageTypeSpecifiers:
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 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. 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;
}
}