我正在尝试使用打开的JDK 11 imageIO ImageReader和ImageWriter类将多个输入TIFF文件合并为单个多页输出TIFF文件。我的例程对于从许多不同品牌的扫描设备创建的几乎所有示例输入文件均能正常运行。这些设备使用旧的和新的JPEG压缩生成各种TIFF文件。但是,来自一台特定设备的TIFF文件会导致背景为粉红色的错误输出。甚至更陌生的是,使用纵向扫描生成的TIFF会产生正确的输出,而使用同一设备进行横向扫描所生成的TIFF会产生带有粉红色背景的错误输出。我看不到两个输入文件之间的明显差异,当由ImageIO库处理时,这会导致行为上的差异。

我知道输出中的粉红色背景通常表示透明度解释存在问题。在读取和写入JEPG图像时,我发现了许多有关此问题的引用。但是,我没有找到与TIFF图像类似问题的任何引用。当我在调试器中浏览ImageReader和ImageWriter时,无法发现有效的输入TIFF文件与产生不良的粉红色输出的文件之间没有明显的区别。这两个文件都没有透明度。两者具有相同的YCbCr光度学解释,波段和子采样。有问题的TIFF文件使用旧的JPEG压缩,因此图像写入参数为ImageWriter显式指定新的JPEG压缩。但是,对于正常工作的类似纵向TIFF文件来说,这是正确的,因此问题不仅仅在于输出压缩,还必须更加细微。

下面是一个简单的命令行应用程序,它再现了我的问题。

package com.example;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;

public class Main {

    private static final String TIFF_FORMAT = "tiff";
    private static final String IMAGEIO_PLUGIN_PACKAGE = "com.sun.imageio.plugins.tiff";
    //private static final String IMAGEIO_PLUGIN_PACKAGE = "com.github.jaiimageio.impl.plugins.tiff";

    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println("You must specify an input directory and output filename");
            return;
        }

        File sourceDirectory = new File(args[0]);
        if (!sourceDirectory.exists() || !sourceDirectory.isDirectory()) {
            System.out.println(String.format("Source directory '%s' is invalid", args[0]));
        }
        File outputFile = new File(args[1]);
        if (outputFile.exists()) {
            outputFile.delete();
        }
        File inputFiles[] = sourceDirectory.listFiles();

        mergeTiffFiles(inputFiles, outputFile);
    }

    /**
     * Merge a list of TIFF files into a single output TIFF file using the Java ImageIO utilities.
     *
     * @param inputFilePaths list of input file paths to merge
     * @param mergedFilePath destination path for the merged output file
     */
    private static void mergeTiffFiles(
            final File[] inputFilePaths,
            final File mergedFilePath) {
        ImageReader reader = null;
        ImageWriter writer = null;
        File inputFilePath = null;
        try (
                OutputStream outputStream = new FileOutputStream(mergedFilePath);
                ImageOutputStream ios = ImageIO.createImageOutputStream(outputStream)
        ) {
            // Initialise the output writer
            writer = getTiffWriter();
            writer.setOutput(ios);
            writer.prepareWriteSequence(null);

            // Iterate through the source files appending the pages in order within and across files
            reader = getTiffReader();
            for (final File filePath : inputFilePaths) {
                inputFilePath = filePath;
                try (
                        FileInputStream inputFile = new FileInputStream(filePath);
                        ImageInputStream inputStream = ImageIO.createImageInputStream(inputFile)
                ) {
                    reader.setInput(inputStream);
                    int numImages = reader.getNumImages(true);
                    for (int j = 0; j < numImages; j++) {
                        IIOMetadata imageMetadata = reader.getImageMetadata(j); // 0, first image
                        ImageWriteParam writeParams = getTiffWriteParams(writer, imageMetadata);
                        BufferedImage image = reader.read(j);
                        writer.writeToSequence(new IIOImage(image, null, imageMetadata), writeParams);
                    }
                }
            }
            inputFilePath = null;

            // Finalize the output file
            writer.endWriteSequence();
        } catch (Exception e) {
            if (inputFilePath != null) {
                throw new IllegalStateException(String.format("Error while merging TIFF file: %s", inputFilePath), e);
            } else {
                throw new IllegalStateException("Failed to merge TIFFs files", e);
            }
        } finally {
            // Cleanup the reader and writer
            if (writer != null) {
                writer.dispose();
            }
            if (reader != null) {
                reader.dispose();
            }
        }
    }

    /**
     * Get an TIFF reader used to read the source pages - ensure we use the imageIO plugin.
     *
     * @return an TIFF image reader.
     * @throws IOException if an reader plugin cannot be found
     */
    private static ImageReader getTiffReader() throws IOException {
        ImageReader reader = null;
        Iterator readers = ImageIO.getImageReadersByFormatName(TIFF_FORMAT);
        if (readers.hasNext()) {
            do {
                reader = (ImageReader) readers.next();
            } while (!reader.getClass().getPackage().getName().equals(IMAGEIO_PLUGIN_PACKAGE) && readers.hasNext());
        }
        if (reader == null) {
            throw new IOException("No imageio readers for format: " + TIFF_FORMAT);
        }
        return reader;
    }

    /**
     * Get a TIFF writer used to create the merged page - ensure we use the imageIO plugin
     *
     * @return a TIFF image writer
     * @throws IOException if an writer plugin cannot be found
     */
    private static ImageWriter getTiffWriter() throws IOException {
        ImageWriter writer = null;
        Iterator writers = ImageIO.getImageWritersByFormatName(TIFF_FORMAT);
        if (writers.hasNext()) {
            do {
                writer = (ImageWriter) writers.next();
            } while (!writer.getClass().getPackage().getName().equals(IMAGEIO_PLUGIN_PACKAGE) && writers.hasNext());
        }
        if (writer == null) {
            throw new IOException("No imageio writers for format: " + TIFF_FORMAT);
        }
        return writer;
    }

    /**
     * Get the appropriate TIFF write parameters to apply for an input with the given image meta-data.
     * Check the source image compression. If possible use the same compression settings as those from the
     * input image.  However, the ImageIO library doesn't support the legacy JPEG compression format for TIFF
     * images.  Unfortunately, there are a number of devices that create scanned TIFF images of this type
     * (Xerox, HP OXP).  To support the merge operation explicitly force the new JPEG compression with a high
     * quality value.
     *
     * @param writer        TIFF image writer that will use the returned image parameters
     * @param imageMetadata meta-data associated with the image to write
     * @return the adjusted image write parameters
     */
    private static ImageWriteParam getTiffWriteParams(ImageWriter writer, IIOMetadata imageMetadata) {
        // Determine the source compression type
        IIOMetadataNode root =
                (IIOMetadataNode) imageMetadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
        IIOMetadataNode compression =
                (IIOMetadataNode) root.getElementsByTagName("CompressionTypeName").item(0);
        String compressionName = compression.getAttribute("value");
        ImageWriteParam writeParams = writer.getDefaultWriteParam();
        if (compressionName.equalsIgnoreCase("Old JPEG")) {
            // Convert to modern JPEG encoding if the source uses old JPEG compression.
            writeParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            writeParams.setCompressionType("JPEG");
            double quality = 0.95;
            quality = Math.max(0, Math.min(1, quality));
            writeParams.setCompressionQuality((float) quality);
        } else {
            // Otherwise use the source image compression if possible
            writeParams.setCompressionMode(ImageWriteParam.MODE_COPY_FROM_METADATA);
        }
        writeParams.setTilingMode(ImageWriteParam.MODE_COPY_FROM_METADATA);
        return writeParams;
    }
}

我希望类似的横向和纵向TIFF的输出具有正确的白色背景。我显然在读取或写入过程的设置上做错了。但是,没有太多选择可以尝试。 ImageReader仅支持TIFF文件的一种图像目标类型。最新的开放式JDK 11.0.4_11版本会发生此问题。

最佳答案

好的,通过检查示例文件,我认为我已经找到了问题。而且它不在您的代码中*。
使用JPEG压缩读取和写入TIFF时,TIFF插件会将嵌入式JPEG流的解码/编码委托(delegate)给JPEG插件。从理论上讲,这很简单,因为JPEG不包含颜色信息,而TIFF容器在262/PhotometricInterpretation标记中包含正确的颜色信息。
在现实生活中,这要复杂得多,因为有时TIFF标签会丢失或不正确(尤其是结合使用值259/Compression(“旧JPEG”)的6标签(或者JPEG编码器/解码器将对颜色空间做出自己的假设) (基于独立JPEG的约定,通常是JFIF或Exif),我相信这里就是这种情况。与JRE捆绑在一起的JPEG插件使用conventions documented here,并且颜色空间是根据SOFn标记中的组件ID推断出来的。
对于您的文件,我们可以看到组件ID有所不同。
人像文件:

SOF0[ffc0, precision: 8, lines: 3520, samples/line: 2496,
     components: [id: 1, sub: 1/1, sel: 0, id: 2, sub: 1/1, sel: 1, id: 3, sub: 1/1, sel: 1]]
景观文件:
SOF0[ffc0, precision: 8, lines: 2496, samples/line: 3520,
    components: [id: 0, sub: 1/1, sel: 0, id: 1, sub: 1/1, sel: 1, id: 2, sub: 1/1, sel: 1]]
纵向文件中的组件ID为常规的1、2和3,而横向文件的ID为0、1和2。两个文件都没有子采样(即1:1)。
从约定:

因此,风景图像将被视为已经存在RGB(并且不正确地未从YCbCr转换),从而导致了粉红色。即使TIFF容器中的所有其他内容都清楚地表明它是YCbCr。
为了解决此问题(以及许多其他问题),我创建了my own JPEG plugin,可以将其用作JRE插件的直接替代品。它遵循IJG的libJPEG中的约定(简单得多),从而与其他应用程序具有更好的色彩空间一致性。结合使用来自同一项目的TIFF插件,可以正确读取两个输入(白色背景)。我尚未使用JRE TIFF插件对其进行测试,但从理论上讲,它也应该/也可以工作。不幸的是,TwelveMonkeys TIFF插件还没有(还)
具有您使用(平铺)的写入功能,并且对要写入的元数据有一些限制。

PS:由于您似乎主要处理的是在重新编码时质量下降的JPEG,因此您可能希望查看合并TIFF而不解码图像数据。您可以在Oliver Schmidtmer编写的 TIFFUtilities 中找到一个示例。
*)从技术上讲,可以在代码中解决问题,但是正确处理所有情况是很复杂的。如果您想自己实现此功能,或者只是好奇,建议您看一下TwelveMonkeys ImageIO JPEG plugin的源代码。

10-05 19:04
查看更多