It's been a few years now since I wrote my first image-processing program in Java, due to a need to read a file format that wasn't supported by default in the ImageIO library, and which I couldn't find a suitable reader for online. Java Advanced Imaging exists, but its documentation is spotty at best, and it doesn't cover every format. So I rolled my own, and over time added the ability to read a few more formats as a coding exercise.
Recently, I decided I should see how I could make that code fit into the ImageIO service-provider interface. The basic premise is that if you write an ImageReader that conforms to certain interfaces, and it's on the classpath of a program, ImageIO will be able to automatically detect it and use it to read images of the format(s) it supports, with a simple:
ImageIO.read(new File(fileName));
statement. Which is pretty cool, really. It's designed to be pretty convenient - Sun wrote an extensible interface, and programmers all around the world can add to it, taking it far beyond the initial handful of formats that Sun wrote support for.
However, I had difficulty finding a high-level overview of how to write your own ImageIO reader. All the classes do have proper JavaDocs - Sun's always been pretty good about those - but there isn't a high-level overview of how it all ties together on the official site, nor did I find a great explanation through search engines. This contributed to my putting off implementing ImageIO for several years. But I've now looked up how to do it, tied it together, and got it all working, and this post aims to explain how it all works in a what-you-need-to-know-to-get-up-and-running manner.
There are two different, but related, interfaces in ImageIO that work with each other to provide the seamless integration that ImageIO allows. These are the Service Provider Interface (SPI), and ImageReaders. ImageReaders are the classes that do the heavy lifting of image processing. Your ImageReaders will all extend ImageReader. ImageReader provides access to an underlying object called input (more on how that is wired up later), which you can cast to the ImageInputStream class, and will act as the input to your ImageIO-compatible reader.
So what's this mean for the design of your ImageReader? I'll take a few snippets of the MSP (Microsoft Paint format, from Windows 1.0 and 2.0) reader that I've written to help explain it. Your class will begin like this:
public class MSPReader extends ImageReader
And it will have a method similar to this:
@Override
public BufferedImage read(int imageIndex, ImageReadParam ignored) {
try {
ImageInputStream iis = (ImageInputStream)input;
readFile(iis);
...
The read method in ImageReader takes an integer for the imageIndex, which you may or may not care about depending on the image format. For example, a TIFF file may contain multiple images - our sample Microsoft Paint format cannot, so we ignore it. The ImageReadParam class is another one that for purposes of our initial demo we'll gloss over.
The key is inside the try block. We cast the input object to an ImageInputStream, and then pass it to our method that does processing on the input stream. This will then do whatever particulars we need for this image format. This leads to the following recommendation:
If you plan to allow invoking your ImageReader outside of ImageIO, it should be through a read(ImageInputStream iis) method, or a helper method that invokes that.
In my case, I'd already used the image reader outside of ImageIO, so I refactored my existing code so that it could go through the same code path as the ImageIO code path - the processing code is exactly the same once it hits that read method.
The ImageInputStream class is pretty flexible. It includes your standard readByte, readShort, etc. methods, including variants such as readUnsignedShort that you are more likely to need when dealing with image formats than in general Java code. One particular method deserves calling out:
iis.setByteOrder(ByteOrder.LITTLE_ENDIAN);
The setByteOrder method lets you set the byte order to whichever byte order works most conveniently with the image format you are working with. This is both much better than trying to remember to manually flip when necessary, and less maintenance than rolling your own class for this, both of which I've done. You can also easily create an ImageInputStream method from a file with the FileImageInputStream class and its constructors - great for creating a non-ImageIO entry point.
There are some additional classes your ImageReader will need to implement, but we'll skip to the SPI side for now and revisit those later.
The SPI class is key to getting your ImageReader picked up by ImageIO. In essence, ImageIO looks for classes on the classpath which extend its various SPI abstract classes, and use the information exposed by those classes to determine which, if any, readers can read a particular file. We'll focus for now on extending the ImageReaderSpi class, although there are additional Spi classes such as ImageWriterSpi as well.
The createReaderInstance method can be pretty straightforward, passing this to a constructor on your ImageReader class which will do the processing for the image type in question; for example:
@Override
public ImageReader createReaderInstance(Object object) {
return new MSPReader(this);
}
And the corresponding constructor in our MSPImageReader implementation:
public MSPReader(final ImageReaderSpi originatingProvider) {
super(originatingProvider);
}
Another key method is getInputTypes. While not actually abstract in ImageReaderSpi, you will either need to overwrite the protected inputTypes object in ImageReaderSpi, or override this method, to be able to accept anything in your reader. This method returns the classes that the reader can accept as input. So, for my reader, I've allowed ImageInputStream:
@Override
public Class[] getInputTypes() {
Class[] retVal = new Class[1];
retVal[0] = ImageInputStream.class;
return retVal;
}
Finally, the canDecodeInput method is essential. It returns a boolean as to whether the reader believes it can read an image, based on evaluating the data in the image, not evaluating the file extension or something like that. Thus, this method will typically cast its source Object parameter to an ImageInputStream, and then evaluate it to determine if it can read it or not. There are two pieces of best practice associated with this:
In the case of my MSP reader, I read the header (26 bytes), verify the magic numbers match, and verify the checksum of the header matches its specified calculated value. My reader supports all variants of the MSP format, but if it did not, it would also be a good idea to check if the format were not one of the formats this reader supported, so another potential MSP reader on the classpath that did support those variants could read those files instead.
In my case, I put the implementation of canReadImage method as a static method on my MSPImageReader class, to keep the format-related logic together. However, you would not have to do this, I simply found it convenient to keep the SPI class as simple as possible, and the similar functions together.
It's also worth noting that ImageIO essentially works with ImageReaders by following this process:
So it's essential that your SPI class accurately indicate which types of input streams it supports in both of the above-mentioned methods.
There are a few other methods you can optionally override, such as getDescription(). However, for a basic implementation, this is what you need on your SPI class. We'll now briefly revisit the ImageReader, before talking about the classpath.
There are a couple other methods we need to implement on the ImageReader. They are:
Creating the metadata is left for another post; if you only really care about displaying the image and not its metadata, you can simply return null for these methods and be okay. The other methods should hopefully be pretty trivial to implement.
Since there are a lot of build tools, I'll focus on Maven in this example, as I'm most familiar with it. Maven makes it pretty easy to get on the classpath. You'll need two things:
A quick note on the first one - I believe it should also work if the ImageReader and its SPI are in your main JAR, or if they are in separate JARs that where one the SPI JAR depends on the ImageReader JAR, but I have not attempted these configurations yet. This is standard Maven dependency stuff, so I'll focus on the latter, which isn't something you see every day with Maven.
While it is possible to manually put folders and files in a JAR, thankfully Maven makes it quite easy to get this set up in the Maven build itself. You'll need a src/main/resources folder within your Maven project, and within that, META-INF/services subdirectories. Finally, within that, create a file whose name is the fully-qualified name of the abstract class which you are implementing - not the implementations themselves! This should look like the below screenshot from my NetBeans workspace:
This file, then, simply contains the fully-qualified names of all the ServiceProviderInterface (SPI) classes you wish to register, one per line, as seen below:
com.ajtjp.jImageReader.spi.MSPImageSPI
That's it. Now rebuild your Maven projects, and ImageIO will magically pickup your SPI classes, the associated ImageReaders, and be able to read your newly-supported image formats. You can simply call ImageIO.read(myNewlySupportedFile), and get a BufferedImage just like you would with the types with build-in support.
All in all, it is a decent amount of work to support ImageIO, but it has a pretty solid design, and it will become increasingly quick to implement it after you've done it the first time. It will also make your code easy to consume for anyone who's familiar with ImageIO.
Along with the official documentation, the TwelveMonkeys library, a BSD-license ImageIO library, was a key resource for me in understanding how ImageIO worked. They support a wide range of formats, and if you are considering writing an ImageIO reader for a semi-common format, you should probably check there first and see if they support the one you're looking for.
Java Advanced Imaging is the official advanced imaging framework, which also extends ImageIO. However, as far as I can tell it's been dormant for about a decade, and the documentation is not the greatest. From what I've read, you need the jai-codec.jar file for ImageIO to interact with it. It may provide what you need, but could really use a boost into the current decade. It also has some dependencies on native implementations, which can provide cross-platform challenges; TwelveMonkeys is pure Java and thus does not run into this problem.
I am not aware of a site that has a compendium of open-source ImageIO implementations. It seems like that would be, in a way, the missing rope that would tie together the ImageIO vision - the framework itself is nicely extensible once you understand how it works, but there's no central place to find plugins.
A future post will cover metadata in ImageIO, as well as the MSPReader used in this example (and perhaps a couple other readers as well).
Return to Blog Index