How to exclude specific TIFF reader from ImageIO?

2019-02-25 14:24发布

问题:

Stack:

  • Java - 1.8.0_91
  • Scala - 2.11.8
  • Library - it.geosolutions.imageio-ext imageio-ext-tiff 1.1.15

We are reading lots of old TIF images and for some reason read is highly inconsistent - for some reasons on a different run reading the same image can succeed or fail with exception -

javax.imageio.IIOException: Invalid component ID 3 in SOS
at com.sun.imageio.plugins.jpeg.JPEGImageReader.readImage(Native Method)
at com.sun.imageio.plugins.jpeg.JPEGImageReader.readInternal(JPEGImageReader.java:1236)
at com.sun.imageio.plugins.jpeg.JPEGImageReader.read(JPEGImageReader.java:1039)
at com.sun.media.imageioimpl.plugins.tiff.TIFFOldJPEGDecompressor.decodeRaw(TIFFOldJPEGDecompressor.java:654)
at com.sun.media.imageio.plugins.tiff.TIFFDecompressor.decode(TIFFDecompressor.java:2527)
at com.sun.media.imageioimpl.plugins.tiff.TIFFImageReader.decodeTile(TIFFImageReader.java:1137)
at com.sun.media.imageioimpl.plugins.tiff.TIFFImageReader.read(TIFFImageReader.java:1417)

The code is something like this:

import java.io.{ByteArrayInputStream, ByteArrayOutputStream}
import javax.imageio.ImageIO

def convertToPng(data: Array[Byte]): Array[Byte] = {
    val inputStream = new ByteArrayInputStream(data)
    val image = ImageIO.read(inputStream)
    val outputStream = new ByteArrayOutputStream(inputStream.available())
    ImageIO.write(image, "png", outputStream)
    outputStream.toByteArray
}

The problem is ImageIO initializes 2 TIFF readers at the same time

 com.sun.media.imageioimpl.plugins.tiff.TIFFImageReader & 
 it.geosolutions.imageioimpl.plugins.tiff.TIFFImageReader

OR

 it.geosolutions.imageioimpl.plugins.tiff.TIFFImageReader
 com.sun.media.imageioimpl.plugins.tiff.TIFFImageReader 

The first one fails, the second one works. How to exclude com.sun.media.imageioimpl.plugins.tiff.TIFFImageReader from ImageIO configuration?

回答1:

The issue here is that ImageIO uses a service provider interface (SPI) lookup to register plugins at runtime, and in your setup, multiple plugins that can read TIFF is found. By default, the plugins does not have any specific order, which is why you sometimes get the com.sun (JAI) TIFF plugin first and sometimes the it.geosolutions (Geosolutions) TIFF plugin first. ImageIO.read(...) will only try this first plugin and give up if it fails.

If you can, the easiest solution is just to remove one of the plugins from class path. But I assume you already thought of that. There are still multiple other ways to solve this (I give code examples in Java, as that's what I'm most familiar with, I'm sure you can write it more elegant in Scala ;-)).

The one that requires the least changes to your code, is to unregister the JAI provider at runtime, somewhere in your "bootstrap" code (exactly where this is, depends on the application, could be a static initializer block or a web context listener or similar). The IIORegistry has a deregisterServiceProvider method for this purpose, removing the provider from the registry, and making it unavailable for ImageIO.

Another option is to define an explicit order for the providers. This can be useful if you need to to have multiple providers for a single format for some reason (third-party requirements/inter-plugin dependencies etc). The IIORegistry has a setOrdering method for this purpose, that allows setting pairwise ordering of two service providers, making ImageIO always prefer one before the other.

The below code shows both of the above options:

// Get the global registry
IIORegistry registry = IIORegistry.getDefaultInstance();

// Lookup the known TIFF providers
ImageReaderSpi jaiProvider = lookupProviderByName(registry, "com.sun.media.imageioimpl.plugins.tiff.TIFFImageReaderSpi");
ImageReaderSpi geoProvider = lookupProviderByName(registry, "it.geosolutions.imageioimpl.plugins.tiff.TIFFImageReaderSpi");

if (jaiProvider != null && geoProvider != null) {
    // If both are found, EITHER
    // order the it.geosolutions provider BEFORE the com.sun (JAI) provider
    registry.setOrdering(ImageReaderSpi.class, geoProvider, jaiProvider);

    // OR
    // un-register the JAI provider
    registry.deregisterServiceProvider(jaiProvider);
}

// New and improved (shorter) version. :-)
private static <T> T lookupProviderByName(final ServiceRegistry registry, final String providerClassName) {
    try {
        return (T) registry.getServiceProviderByClass(Class.forName(providerClassName));
    }
    catch (ClassNotFoundException ignore) {
        return null;
    }
}

The above code will make sure the Geosolutions TIFF plugin will always be used by ImageIO.read(...), and your existing code should just work (but now be stable).

A completely different option, is to try reading the data using all registered TIFF plugins, and use the first one that succeeds. This is more explicit than the previous code, but requires rewriting the image reading code:

byte[] data;
BufferedImage image;

try (ImageInputStream inputStream = ImageIO.createImageInputStream(new ByteArrayInputStream(data))) {
    Iterator<ImageReader> readers = ImageIO.getImageReaders(inputStream);

    // Try reading the data, using each reader until we succeed (or have no more readers)
    while (readers.hasNext()) {
        ImageReader reader = readers.next();

        try {
            reader.setInput(inputStream);
            image = reader.read(0);
            break; // Image is now correctly decoded
        }
        catch (Exception e) {
            // TODO: Log exception?
            e.printStackTrace();

            // Reading failed, try the next Reader
            inputStream.seek(0);
        }
        finally {
            reader.dispose();
        }
    }
}

You can of course combine the options above, to have the best of both worlds (ie. stable order and fallback if one reader fails).