如何从 ImageIO 中排除特定的 TIFF reader?

How to exclude specific TIFF reader from ImageIO?

堆栈:

我们正在读取大量旧的 TIF 图像,由于某些原因,读取高度不一致 - 由于某些原因,在不同的 运行 上读取相同的图像可能成功或失败,但有异常 -

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)

代码是这样的:

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
}

问题是ImageIO同时初始化了2个TIFF阅读器

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

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

第一个失败,第二个成功。 如何从 ImageIO 配置中排除 com.sun.media.imageioimpl.plugins.tiff.TIFFImageReader?

这里的问题是 ImageIO 在运行时使用服务提供者接口 (SPI) 查找来注册插件,并且在您的设置中,发现了多个可以读取 TIFF 的插件。默认情况下,插件没有任何特定顺序,这就是为什么您有时会先获得 com.sun (JAI) TIFF 插件,有时会先获得 it.geosolutions (Geosolutions) TIFF 插件。 ImageIO.read(...)只会尝试第一个插件,如果失败就放弃。

如果可以,最简单的解决方案就是从 class 路径中删除其中一个插件。但我想你已经想到了。还有多种其他方法可以解决这个问题(我在 Java 中给出了代码示例,因为这是我最熟悉的,我相信你可以用 Scala 编写得更优雅;-))。

需要对您的代码进行最少更改的方法是在运行时注销 JAI 提供程序,在您的 "bootstrap" 代码中的某处(具体位置取决于应用程序,可能是静态初始化程序块或网络上下文侦听器或类似的)。 IIORegistry 有一个用于此目的的 deregisterServiceProvider 方法,从注册表中删除提供程序,并使其对 ImageIO.

不可用

另一种选择是为提供商定义明确的顺序。如果您出于某种原因(第三方 requirements/inter-plugin 依赖项等)需要为一种格式提供多个提供程序,这将很有用。 IIORegistry 有一个用于此目的的 setOrdering 方法,允许设置 成对 两个服务提供者的顺序,使 ImageIO 总是优先选择一个.

下面的代码显示了以上两个选项:

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

以上代码将确保 ImageIO.read(...) 始终使用 Geosolutions TIFF 插件,并且您现有的代码应该可以正常工作(但现在是稳定的)。

一个完全不同的选择,是尝试使用所有已注册的 TIFF 插件读取数据,并使用第一个成功的。这个比之前的代码更明确,但是需要重写图片读取代码:

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();
        }
    }
}

您当然可以结合上述选项,以实现两全其美(即稳定的顺序和在 reader 失败时回退)。