| 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 JDK 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;
}
}