Programming in Java Advanced Imaging
C H A P T E R14 |
Extending the API |
THIS chapter describes how the JAI API may be extended.
14.1 Introduction
No image processing API can hope to capture the enormous variety of operations that can be performed on a digital image. Although the JAI API supports a large number of imaging operations, it was designed from the beginning to encourage programmers to write extensions rather than manipulating image data directly. JAI allows virtually any image processing algorithm to be added to the API and used as if it were a native part of the API.The mechanism for adding functionality to the API can be presented at multiple levels of encapsulation and complexity. This allows programmers who wish to add simple things to the API to deal with simple concepts, while more complex extensions have complete control over their environment at the lowest levels of abstraction. The API also supports a variety of programming styles, including an immediate mode and a deferred mode of execution for different types of imaging applications.
14.2 Package Naming Convention
All extensions to JAI require the addition of new classes. All new classes are grouped into packages as a convenient means of organizing the new classes and separating the new classes from code libraries provided by others.All new packages are given a product name. A product name is the accepted Java method of using your company's reversed Internet address to name new packages. This product naming convention helps to guarantee the uniqueness of package names. Supposing that your company's Internet address is
WebStuff.COM
and you wish to create a new package namedPrewitt
. A good choice of package name would becom.webstuff.PrewittOr, evencom.webstuff.media.jai.PrewittTo uniquely identify the package as part of JAI.The above new
prewitt
class file must now be placed into a subdirectory that matches the product name, such as:The Java convention for class naming is to use initial caps for the name, as in the
com/webstuff/media/jai
for Solaris-based systems
- or
com\webstuff\media\jai
for Windows systemsPrewitt
example above. So called multi-word class names use initial caps for each word. For exampleAddOpImage
.Vendors are encouraged to use unique product names (by means of the Java programming language convention of reversed internet addresses) to maximize the likelihood of a clean installation.
14.3 Writing New Operators
To extend the JAI API by creating new operations, you will need to write a newOpImage
subclass. This may be done by subclassing one or more existing utility classes to automate some of the details of the operator you wish to implement. For most operators, you need only supply a routine that is capable of producing an arbitrary rectangle of output, given contiguous source data.Once created, new operators may be made available to users transparently and without user source code changes using the JAI registry mechanism. Existing applications may be tuned for new hardware platforms by strategic insertion of new implementations of existing operators.
To create a new operator, you need to create the following new classes:
- A class that extends the
OpImage
class or any of its subclasses. This new class does the actual processing. See Section 14.3.1, "Extending the OpImage Class."
- A class that extends the
OperationDescriptor
class. This new class describes the operation such as name, parameter list, and so on. See Section 14.3.2, "Extending the OperationDescriptor Interface."
- If the operator will function in the Rendered mode only, a class that implements
java.awt.image.renderable.RenderedImageFactory
.14.3.1 Extending the OpImage Class
Every new operator being written must be a subclass ofOpImage
or one of its subclasses. TheOpImage
class currently has the following subclasses:
All abstract methods defined in
OpImage
must be implemented by any newOpImage
subclass. Specifically, there are two fundamental methods that must be implemented:
First, you have to decide which of the
OpImage
subclasses to extend. If you are writing a new operator to manipulate two or more images, you would most likely extend theMultiSourceOpImage
class. To write a new statistics operation, you would most likely extend theStatisticsOpImage
class. Each subclass has a specific purpose, as described in Table 14-1.The following code sample extends
MultiSourceOpImage
by creating a newOpImage
class namedAddOpImage
. This new operator adds two images together. It uses the methods defined in its superclass,MultiSourceOpImage
, to determine the image size and pixel layout.
Listing 14-1 Example of Extending the OpImage Class
// AddOpImage.java package javax.jai.operator; import java.awt.* import java.util.* import javax.media.jai.* // // An OpImage class to add two source images, pixelwise. // public class AddOpImage extends MultiSourceOpImage { // A dummy constructor used by the class loader. public AddOpImage() {} // // Constructs an AddOpImage. The image dimensions are taken // from the source images. The tile layout, SampleModel, and // ColorModel may optionally be specified by an ImageLayout // object. // // @param source0 the first RenderedImage source. // @param source1 the second RenderedImage source. // @param layout an ImageLayout optionally containing // the tile grid layout, SampleModel, and // ColorModel, or null. public AddOpImage(RenderedImage source0, RenderedImage source1, ImageLayout layout) { super(vectorize(source0, source1), layout, true); } // Adds the pixel values of a rectangle from the // two source images. protected void computeRect(Raster[] sources, WritableRaster dest, Rectangle destRect) { Raster source0 = sources[0]; Raster source1 = sources[1]; // Code omitted: add the two Rasters pixel by pixel ... } }
14.3.2 Extending the OperationDescriptor Interface
All high-level operation names in JAI (such asRotate
,Convolve
, andAddConst
) are mapped to instances ofRenderedImageFactory
that are capable of instantiatingOpImage
chains to perform the named operation. This mapping is done within theOperationRegistry
class, as follows:1. Register the operation name.
The
2. Register the set of rendered image factory objects.
- The high-level operation name, called an operation descriptor, is registered by calling the
registerOperationByName()
method or theregisterOperationDescriptor()
method. The operation descriptor name must be unique.
- The rendered image factory (RIF) is registered using the
registerRIF
method. Each RIF is registered with a specific operation name, and furthermore is given a product name. Similar methods exist for registering a contextual image factory (CRIF).OperationDescriptor
interface provides a comprehensive description of a specific image operation. All of the information regarding the operation, such as the operation name, version, input, and property, should be listed. Any conditions placed on the operation, such as its input format and legal parameter range, should also be included, and the methods to enforce these conditions should be implemented. A set ofPropertyGenerator
s may be specified to be used as a basis for the operation's property management.Each family of the image operation in JAI must have a descriptor that implements this interface. The following basic resource data must be provided:
- GlobalName - a global operation name that is visible to all and is the same in all
Locale
s
- LocalName - a localized operation name that may be used as a synonym for the global operation name
- Vendor - the name of the vendor (company name) defining this operation
- Description - a brief description of this operation
- DocURL - a URL where additional documentation on this operation may be found (the javadoc for the operation)
- Version - the version of the operation
- arg0Desc, arg1Desc, etc. - descriptions of the arguments. There must be a property for each argument.
Additional information about the operation must be provided when appropriate. It is also good idea to provide a detailed description of the operation's functionality in the class comment. When all of the above data is provided, the operation can be added to an
- hint0Desc, hint1Desc, etc. - descriptions of the rendering hints. There must be a property for each hint.
OperationRegistry
.Listing 14-2 shows an example of an operation descriptor for the Clamp operation. Note that the descriptor also contains descriptions of the two required operation parameters, but no hints as these aren't required for the operation.
Listing 14-2 Operation Descriptor for Clamp Operation
public class ClampDescriptor extends OperationDescriptorImpl { /** * The resource strings that provide the general documentation * and specify the parameter list for this operation. */ private static final String[][] resources = { {"GlobalName", "Clamp"}, {"LocalName", "Clamp"}, {"Vendor", "com.sun.javax.media.jai"}, {"Description", "Clamps the pixel values of a rendered image"}, {"DocURL", "http://java.sun.com/products/java-media/jai/ forDevelopers/jaiapi/ javax.media.jai.operator.ClampDescriptor.html"}, {"Version", "Beta")}, {"arg0Desc", "The lower boundary for each band."}, {"arg1Desc", "The upper boundary for each band."} };
As described in Section 3.3, "Processing Graphs," JAI has two image modes: Rendered and Renderable. An operation supporting the Rendered mode takes
RenderedImages
as its sources, can only be used in a Rendered op chain, and produces aRenderedImage
. An operation supporting the Renderable mode takesRenderableImage
s as its sources, can only be used in a Renderable op chain, and produces aRenderableImage
. Therefore, the class types of the sources and the destination of an operation are different between the two modes, but the parameters must be the same for both modes.All operations must support the rendered mode and implement those methods that supply the information for this mode. Those operations that support the renderable mode must specify this feature using the
isRenderableSupported
method and implement those methods that supply the additional information for the Renderable mode.Table 14-2 lists the Rendered mode methods. Table 14-3 lists the Renderable mode methods. Table 14-4 lists the methods relative to operation parameters.
API:javax.media.jai.OperationRegistry
- void registerOperationDescriptor(OperationDescriptor odesc, String operationName)
- registers an
OperationDescriptor
with the registry. Each operation must have anOperationDescriptor
beforeregisterRIF()
may be called to add RIFs to the operation.
Parameter: odesc
An OperationDescriptor
containing information about the operation.
operationName
The operation name as a String
.
- void registerOperationByName(String odescClassName, String operationName)
- registers an
OperationDescriptor
by its class name.
Parameter: odescClassName
The fully-qualified class name of the OperationDescriptor
.
operationName
The operation name as a String
.
- void unregisterOperationDescriptor(String operationName)
- unregisters an
OperationDescriptor
from the registry.
- void registerRIF(String operationName, String productName, RenderedImageFactory RIF)
- registers a
RIF
with a particular product and operation.
Parameter: operationName
The operation name as a String
.
productName
The product name, as a String
.
RIF
The RenderedImageFactory
to be registered.
- void registerRIFByClassName(String operationName, String productName, String RIFClassName)
- registers a
RIF
with a particular product and operation, constructing an instance using its class name.
Parameter: operationName
The operation name as a String
.
productName
The product name, as a String
.
RIFClassName
The fully-qualified class name of a RenderedImageFactory
.
14.4 Iterators
Iterators are provided to help the programmer who writes extensions to the JAI API and does not want to use any of the existing API methods for traversing pixels. Iterators define the manner in which the source image pixels are traversed for processing. Iterators may be used both in the implementation ofcomputeRect
methods orgetTile
methods of OpImage subclasses, and for ad-hoc pixel-by-pixel image manipulation.Iterators provide a mechanism for avoiding the need to cobble sources, as well as to abstract away the details of source pixel formatting. An iterator is instantiated to iterate over a specified rectangular area of a source
RenderedImage
orRaster
. The iterator returns pixel values inint
,float
, ordouble
format, automatically promoting integral values smaller than 32 bits toint
when reading, and performing the corresponding packing when writing.JAI offers three different types of iterator, which should cover nearly all of a programmer's needs. However, extenders may wish to build others for more specialized needs.
The most basic iterator is
RectIter
, which provides the ability to move one line or pixel at a time to the right or downwards, and to step forward in the list of bands.RookIter
offers slightly more functionality thanRectIter
, allowing leftward and upward movement and backwards motion through the set of bands. BothRectIter
andRookIter
allow jumping to an arbitrary line or pixel, and reading and writing of a random band of the current pixel. TheRookIter
also allows jumping back to the first line or pixel, and to the last line or pixel.
RandomIter
allows an unrelated set of samples to be read by specifying their x and y coordinates and band offset. TheRandomIter
will generally be slower than either theRectIter
orRookIter
, but remains useful for its ability to hide pixel formats and tile boundaries.Figure 14-1 shows the Iterator package hierarchy. The classes are described in the following paragraphs.
14.4.1 RectIter
TheRectIter
interface represents an iterator for traversing a read-only image in top-to-bottom, left-to-right order (Figure 14-2). The RectIter traversal will generally be the fastest style of iterator, since it does not need to perform bounds checks against the top or left edges of tiles. TheWritableRectIter
interface traverses a read/write image in the same manner as the RectIter.The iterator is initialized with a particular rectangle as its bounds. The initialization takes place in a factory method (the
RectIterFactory
class) and is not a part of the iterator interface itself. Once initialized, the iterator may be reset to its initial state by means of thestartLines()
,startPixels()
, andstartBands()
methods. Its position may be advanced using thenextLine()
,jumpLines()
,nextPixel()
,jumpPixels()
, andnextBand()
methods.
Figure 14-1 Iterator Hierarchy
Figure 14-2 RectIter Traversal Pattern The
WritableRookIter
interface adds the ability to alter the source pixel values using the varioussetSample()
andsetPixel()
methods.An instance of
RectIter
may be obtained by means of theRectIterFactory.create()
method, which returns an opaque object implementing this interface.
API:javax.media.jai.iterator.RectIterFactory
- static RectIter create(RenderedImage im, Rectangle bounds)
- constructs and returns an instance of
RectIter
suitable for iterating over the given bounding rectangle within the givenRenderedImage
source. If thebounds
parameter is null, the entire image will be used.
Parameters: im
A read-only RenderedImage
source.
bounds
The bounding Rectangle
for the iterator, or null.
- static RectIter create(Raster ras, Rectangle bounds)
- constructs and returns an instance of
RectIter
suitable for iterating over the given bounding rectangle within the givenRaster
source. If thebounds
parameter is null, the entire Raster will be used.
Parameters: ras
A read-only Raster
source.
bounds
The bounding Rectangle
for the iterator, or null.
- static WritableRectIter createWritable(WritableRenderedImage im, Rectangle bounds)
- constructs and returns an instance of
WritableRectIter
suitable for iterating over the given bounding rectangle within the givenWritableRenderedImage
source. If thebounds
parameter is null, the entire image will be used.
Parameters: im
A WritableRenderedImage
source.
bounds
The bounding Rectangle
for the iterator, or null.
- static WritableRectIter createWritable(WritableRaster ras, Rectangle bounds)
- constructs and returns an instance of
WritableRectIter
suitable for iterating over the given bounding rectangle within the givenWritableRaster
source. If thebounds
parameter is null, the entireRaster
will be used.
Parameters: ras
A WritableRaster
source.
bounds
The bounding Rectangle
for the iterator, or null.
API:javax.media.jai.iterator.RectIter
- void startLines()
- sets the iterator to the first line of its bounding rectangle. The pixel and band offsets are unchanged.
- void startPixels()
- sets the iterator to the leftmost pixel of its bounding rectangle. The line and band offsets are unchanged.
- void startBands()
- sets the iterator to the first band of the image. The pixel column and line are unchanged.
- void nextLine()
- sets the iterator to the next line of the image. The pixel and band offsets are unchanged. If the iterator passes the bottom line of the rectangles, calls to
get()
methods are not valid.
- void jumpLines(int num)
- jumps downward
num
lines from the current position. Thenum
parameter may be negative. The pixel and band offsets are unchanged.
- void nextPixel()
- sets the iterator to the next pixel in the image (that is, move rightward). The line and band offsets are unchanged.
- void jumpPixels(int num)
- jumps rightward
num
pixels from the current position. Thenum
parameter may be negative. The line and band offsets are unchanged.
- void nextBand()
- sets the iterator to the next band in the image. The pixel column and line are unchanged.
14.4.2 RookIter
TheRookIter
interface represents an iterator for traversing a read-only image using arbitrary up-down and left-right moves (Figure 14-3 shows two of the possibilities for traversing the pixels). The RookIter traversal will generally be somewhat slower than a corresponding instance ofRectIter
, since it must perform bounds checks against the top and left edges of tiles in addition to their bottom and right edges. TheWritableRookIter
interface traverses a read/write image in the same manner as the RookIter.An instance of RookIter may be obtained by means of the
RookIterFactory.create()
orRookIterFactory.createWritable()
methods, which return an opaque object implementing this interface. The iterator is initialized with a particular rectangle as its bounds. This initialization takes place in a factory method (theRookIterFactory
class) and is not a part of the iterator interface itself.Once initialized, the iterator may be reset to its initial state by means of the
startLines()
,startPixels()
, andstartBands()
methods. As withRectIter
, its position may be advanced using thenextLine()
,jumpLines()
,nextPixel()
,jumpPixels()
, andnextBand()
methods.
Figure 14-3 RookIter Traversal Patterns
API:avax.media.jai.iterator.RookIterFactory
- static RookIter create(RenderedImage im, Rectangle bounds)
- constructs and returns an instance of
RookIter
suitable for iterating over the given bounding rectangle within the givenRenderedImage
source. If thebounds
parameter is null, the entire image will be used.
Parameters: im
A read-only RenderedImage
source.
bounds
The bounding Rectangle
for the iterator, or null.
- static RookIter create(Raster ras, Rectangle bounds)
- constructs and returns an instance of
RookIter
suitable for iterating over the given bounding rectangle within the givenRaster
source. If the bounds parameter is null, the entireRaster
will be used.
Parameters: ras
A read-only Raster
source.
bounds
The bounding Rectangle
for the iterator, or null.
- static WritableRookIter createWritable(WritableRenderedImage im, Rectangle bounds)
- constructs and returns an instance of
WritableRookIter
suitable for iterating over the given bounding rectangle within the givenWritableRenderedImage
source. If thebounds
parameter is null, the entire image will be used.
Parameters: im
A WritableRenderedImage
source.
bounds
The bounding Rectangle
for the iterator, or null.
- static WritableRookIter createWritable(WritableRaster ras, Rectangle bounds)
- constructs and returns an instance of
WritableRookIter
suitable for iterating over the given bounding rectangle within the givenWritableRaster
source. If thebounds
parameter is null, the entireRaster
will be used.
Parameters: ras
A WritableRaster
source.
bounds
The bounding Rectangle
for the iterator, or null.
14.4.3 RandomIter
TheRandomIter
interface represents an iterator that allows random access to any sample within its bounding rectangle. The flexibility afforded by this class will generally exact a corresponding price in speed and setup overhead.The iterator is initialized with a particular rectangle as its bounds. This initialization takes place in a factory method (the
RandomIterFactory
class) and is not a part of the iterator interface itself. An instance ofRandomIter
may be obtained by means of theRandomIterFactory.create()
method, which returns an opaque object implementing this interface.The
getSample()
,getSampleFloat()
, andgetSampleDouble()
methods are provided to allow read-only access to the source data. ThegetPixel()
methods allow retrieval of all bands simultaneously.
API:javax.media.jai.iterator.RandomIterFactory
- static RandomIter create(RenderedImage im, Rectangle bounds)
- constructs and returns an instance of
RandomIter
suitable for iterating over the given bounding rectangle within the givenRenderedImage
source. If thebounds
parameter is null, the entire image will be used.
Parameters: im
A read-only RenderedImage
source.
bounds
The bounding Rectangle
for the iterator, or null.
- static RandomIter create(Raster ras, Rectangle bounds)
- constructs and returns an instance of
RandomIter
suitable for iterating over the given bounding rectangle within the givenRaster
source. If thebounds
parameter is null, the entireRaster
will be used.
Parameters: ras
A read-only Raster
source.
bounds
The bounding Rectangle
for the iterator, or null.
- static WritableRandomIter createWritable(WritableRenderedImage im, Rectangle bounds)
- constructs and returns an instance of
WritableRandomIter
suitable for iterating over the given bounding rectangle within the givenWritableRenderedImage
source. If thebounds
parameter is null, the entire image will be used.
Parameters: im
A WritableRenderedImage
source.
bounds
The bounding Rectangle
for the iterator, or null.
- static WritableRandomIter createWritable(WritableRaster ras, Rectangle bounds)
- constructs and returns an instance of
WritableRandomIter
suitable for iterating over the given bounding rectangle within the givenWritableRaster
source. If thebounds
parameter is null, the entire Raster will be used.
Parameters: ras
A read-only Raster
source.
bounds
The bounding Rectangle
for the iterator, or null.
14.4.4 Example RectIter
Listing 14-3 shows an example of the construction of a newRectIter
.
Listing 14-3 Example RectIter (Sheet 1 of 4)
import java.awt.Rectangle; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.PixelInterleavedSampleModel; import java.awt.image.SampleModel; import java.util.Random; import javax.media.jai.*; import javax.media.jai.iterator.*; class RectIterTest { int width = 10; int height = 10; int tileWidth = 4; int tileHeight = 4; public static void main(String[] args) { new RectIterTest(); } public RectIterTest() { Random rand = new Random(1L); Rectangle rect = new Rectangle(); int[] bandOffsets = { 2, 1, 0 }; SampleModel sampleModel = new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, tileWidth, tileHeight, 3, 3*tileWidth, bandOffsets); ColorModel colorModel = null; TiledImage im = new TiledImage(0, 0, width, height, 0, 0, sampleModel, colorModel); int[][][] check = new int[width][height][3]; int x, y, b; for (int i = 0; i < 10; i++) { rect.x = rand.nextInt(width); rect.width = rand.nextInt(width - rect.x) + 1; rect.y = rand.nextInt(height); rect.height = rand.nextInt(height - rect.y) + 1; System.out.println("Filling rect " + rect + " with " + i); WritableRectIter witer = RectIterFactory.createWritable(im, rect); b = 0; witer.startBands(); while (!witer.finishedBands()) { y = rect.y; witer.startLines(); while (!witer.finishedLines()) { x = rect.x; witer.startPixels(); while (!witer.finishedPixels()) { witer.setSample(i); check[x][y][b] = i; ++x; witer.nextPixel(); } ++y; witer.nextLine(); } ++b; witer.nextBand(); } } rect.x = 0; rect.y = 0; rect.width = width; rect.height = height; RectIter iter = RectIterFactory.createWritable(im, rect); b = 0; iter.startBands(); while (!iter.finishedBands()) { System.out.println(); y = 0; iter.startLines(); while (!iter.finishedLines()) { x = 0; iter.startPixels(); while (!iter.finishedPixels()) { int val = iter.getSample(); System.out.print(val); if (val != check[x][y][b]) { System.out.print("(" + check[x][y][b] + ") "); } else { System.out.print(" "); } ++x; iter.nextPixel(); } ++y; iter.nextLine(); System.out.println(); } ++b; iter.nextBand(); } } }
14.5 Writing New Image Decoders and Encoders
Thesample
directory contains an example of how to create a new image codec. The example is of a PNM codec, but can be used as a basis for creating any codec. The PNM codec consists of three files:
Note: The codec classes are provided for the developer as a convenience for file IO. These classes are not part of the official Java Advanced Imaging API and are subject to change as a result of the near future File IO extension API. Until the File IO extension API is defined, these classes and functions will be supported for JAI use.
TheImageCodec
class allows the creation of image decoders and encoders. Instances ofImageCodec
may be registered by name. TheregisterCodec
method associates anImageCodec
with the given name. Any codec previously associated with the name is discarded. Once a codec has been registered, the name associated with it may be used as thename
parameter in thecreateImageEncoder
andcreateImageDecoder
methods.The
ImageCodec
class maintains a registry ofFormatRecognizer
objects that examine anInputStream
and determine whether it adheres to the format handled by a particularImageCodec
. AFormatRecognizer
is added to the registry with theregisterFormatRecognizer
method. The unregisterFormatRecognizer method removes a previously registeredFormatRecognizer
from the registry.The
getCodec
method returns theImageCodec
associated with a given name. If no codec is registered with the given name,null
is returned.
API:com.sun.media.jai.codec.ImageCodec
- static ImageEncoder createImageEncoder(String name, OutputStream dst, ImageEncodeParam param)
- returns an
ImageEncoder
object suitable for encoding to the suppliedOutputStream
, using the suppliedImageEncodeParam
object.
Parameter: name
The name associated with the codec.
dst
An OutputStream
to write to.
param
An instance of ImageEncodeParam suitable for use with the named codec, or null.
- static ImageEncoder createImageEncoder(String name, OutputStream dst)
- returns an
ImageEncoder
object suitable for encoding to the suppliedOutputStream
object. A nullImageEncodeParam
is used.
- static ImageDecoder createImageDecoder(String name, InputStream src, ImageDecodeParam param)
- returns an
ImageDecoder
object suitable for decoding from the suppliedInputStream
, using the suppliedImageDecodeParam
object.
Parameter: name
The name associated with the codec.
src
An InputStream
to read from.
param
An instance of ImageEncodeParam suitable for use with the named codec, or null.
- static ImageDecoder createImageDecoder(String name, InputStream src)
- returns an
ImageDecoder
object suitable for decoding from the suppliedInputStream
. A nullImageDecodeParam
is used.
- static void registerCodec(String name, ImageCodec codec)
- associates an
ImageCodec
with the given name. Case is not significant. Any codec previously associated with the name is discarded.
Parameter: name
The name associated with the codec.
codec
The ImageCodec
object to be associated with the given name.
- static void unregisterCodec(String name)
- removes the association between a given
name
and anImageCodec
object. Case is not significant.
- static ImageCodec getCodec(String name)
- returns the
ImageCodec
associated with the given name. If no codec is registered with the given name, null is returned. Case is not significant.
Parameter: name
The name associated with the codec.
Programming in Java Advanced Imaging
Copyright © 1999, Sun Microsystems, Inc. All rights reserved.
Casa de Bender