I am attempting to merge a number of input TIFF files into a single multi-page output TIFF file using the open JDK 11 imageIO ImageReader and ImageWriter classes. My routine works correctly for almost all of my sample input files created from a number of different brands of scanning devices. These devices produce a variety of TIFF files using old and new JPEG compression. However, the TIFF files from one particular device result in an incorrect output that has a pink background. Even stranger, the TIFF produced with a portrait scan creates the correct output, while the TIFF produced with a landscape scan from the same device produces the incorrect output with a pink background. I can see no obvious difference between the 2 input files that would cause the difference in behaviour when processed by the ImageIO library.
I know that a pink background in the output usually indicates that there is a problem with transparency interpretation. I found a number of references to this issue when reading and writing JEPG images. However, I have not found any references to similar issues with TIFF images. When I walk through the ImageReader and ImageWriter in the debugger I can find no obvious difference between the input TIFF file that works and the file that produces the bad pink coloured output. Neither file has transparency. Both have the same YCbCr photometric interpretation, bands, and sub-sampling. The problematic TIFF file uses old JPEG compression, so the image write parameters explicitly specify new JPEG compression for ImageWriter. However, this is true for the similar portrait TIFF file that works correctly, so the problem must be more subtle than just the output compression.
Below is a simple command-line application that reproduces my issue.
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;
}
}
I expect the output of the similar landscape and portrait TIFFs to have the correct white background. I am obviously doing something wrong with the setup of the reading or writing procedure. However, there are not many options to try. The ImageReader only supports one image destination type for the TIFF files. The problem happens with the latest open JDK 11.0.4_11 version.