#306 TIFF LZW IndexColorModel issue + sequence index

This commit is contained in:
Harald Kuhr 2017-01-18 18:08:20 +01:00
parent 10b8c11a8e
commit 762b59674b
2 changed files with 142 additions and 35 deletions

View File

@ -38,11 +38,13 @@ import com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFWriter; import com.twelvemonkeys.imageio.metadata.tiff.TIFFWriter;
import com.twelvemonkeys.imageio.stream.SubImageOutputStream; import com.twelvemonkeys.imageio.stream.SubImageOutputStream;
import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.io.enc.EncoderStream; import com.twelvemonkeys.io.enc.EncoderStream;
import com.twelvemonkeys.io.enc.PackBitsEncoder; import com.twelvemonkeys.io.enc.PackBitsEncoder;
import com.twelvemonkeys.lang.Validate; import com.twelvemonkeys.lang.Validate;
import javax.imageio.*; import javax.imageio.*;
import javax.imageio.event.IIOWriteWarningListener;
import javax.imageio.metadata.IIOInvalidTreeException; import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl; import javax.imageio.metadata.IIOMetadataFormatImpl;
@ -69,19 +71,12 @@ import static com.twelvemonkeys.imageio.plugins.tiff.TIFFStreamMetadata.configur
* @version $Id: TIFFImageWriter.java,v 1.0 18.09.13 12:46 haraldk Exp$ * @version $Id: TIFFImageWriter.java,v 1.0 18.09.13 12:46 haraldk Exp$
*/ */
public final class TIFFImageWriter extends ImageWriterBase { public final class TIFFImageWriter extends ImageWriterBase {
// Short term
// TODO: Support more of the ImageIO metadata (ie. compression from metadata, etc)
// Long term // Long term
// TODO: Support tiling // TODO: Support tiling
// TODO: Support thumbnails // TODO: Support thumbnails
// TODO: Support CCITT Modified Huffman compression (2)
// TODO: Full "Baseline TIFF" support (pending CCITT compression 2)
// TODO: CCITT compressions T.4 and T.6
// TODO: Support JPEG compression of CMYK data (pending JPEGImageWriter CMYK write support) // TODO: Support JPEG compression of CMYK data (pending JPEGImageWriter CMYK write support)
// ---- // ----
// TODO: Support storing multiple images in one stream (multi-page TIFF) // TODO: Support use-case: Transcode multi-layer PSD to multi-page TIFF with metadata (hard, as Photoshop don't store layers as multi-page TIFF...)
// TODO: Support use-case: Transcode multi-layer PSD to multi-page TIFF with metadata
// TODO: Support use-case: Transcode multi-page TIFF to multiple single-page TIFFs with metadata // TODO: Support use-case: Transcode multi-page TIFF to multiple single-page TIFFs with metadata
// TODO: Support use-case: Losslessly transcode JPEG to JPEG-in-TIFF with (EXIF) metadata (and back) // TODO: Support use-case: Losslessly transcode JPEG to JPEG-in-TIFF with (EXIF) metadata (and back)
@ -99,18 +94,25 @@ public final class TIFFImageWriter extends ImageWriterBase {
// Support JPEG compression (7) - might need extra input to allow multiple images with single DQT // Support JPEG compression (7) - might need extra input to allow multiple images with single DQT
// Use sensible defaults for compression based on input? None is sensible... :-) // Use sensible defaults for compression based on input? None is sensible... :-)
// Support resolution, resolution unit and software tags from ImageIO metadata // Support resolution, resolution unit and software tags from ImageIO metadata
// Support CCITT Modified Huffman compression (2)
// Full "Baseline TIFF" support (pending CCITT compression 2)
// CCITT compressions T.4 and T.6
// Support storing multiple images in one stream (multi-page TIFF)
// Support more of the ImageIO metadata (ie. compression from metadata, etc)
private static final Rational STANDARD_DPI = new Rational(72); private static final Rational STANDARD_DPI = new Rational(72);
/** /**
* Flag for active sequence writing * Flag for active sequence writing
*/ */
private boolean isWritingSequence = false; private boolean writingSequence = false;
private int sequenceIndex = 0;
/** /**
* Metadata writer for sequence writing * Metadata writer for sequence writing
*/ */
private TIFFWriter sequenceTiffWriter = null; private TIFFWriter sequenceTIFFWriter = null;
/** /**
* Position of last IFD Pointer on active sequence writing * Position of last IFD Pointer on active sequence writing
@ -137,12 +139,12 @@ public final class TIFFImageWriter extends ImageWriterBase {
TIFFWriter tiffWriter = new TIFFWriter(); TIFFWriter tiffWriter = new TIFFWriter();
tiffWriter.writeTIFFHeader(imageOutput); tiffWriter.writeTIFFHeader(imageOutput);
writePage(image, param, tiffWriter, imageOutput.getStreamPosition()); writePage(0, image, param, tiffWriter, imageOutput.getStreamPosition());
imageOutput.flush(); imageOutput.flush();
} }
private long writePage(IIOImage image, ImageWriteParam param, TIFFWriter tiffWriter, long lastIFDPointerOffset) private long writePage(int imageIndex, IIOImage image, ImageWriteParam param, TIFFWriter tiffWriter, long lastIFDPointerOffset)
throws IOException { throws IOException {
RenderedImage renderedImage = image.getRenderedImage(); RenderedImage renderedImage = image.getRenderedImage();
@ -246,7 +248,9 @@ public final class TIFFImageWriter extends ImageWriterBase {
entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, photometric)); entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, photometric));
if (photometric == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) { if (photometric == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) {
entries.put(TIFF.TAG_COLOR_MAP, new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel))); // TODO: Fix consistency between sampleModel.getSampleSize() and colorModel.getPixelSize()...
// We should be able to support 1, 2, 4 and 8 bits per sample at least, and probably 3, 5, 6 and 7 too
entries.put(TIFF.TAG_COLOR_MAP, new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel, sampleModel.getSampleSize(0))));
entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1)); entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1));
} }
else { else {
@ -353,6 +357,9 @@ public final class TIFFImageWriter extends ImageWriterBase {
ImageWriter jpegWriter = writers.next(); ImageWriter jpegWriter = writers.next();
try { try {
jpegWriter.setOutput(new SubImageOutputStream(imageOutput)); jpegWriter.setOutput(new SubImageOutputStream(imageOutput));
ListenerDelegate listener = new ListenerDelegate(imageIndex);
jpegWriter.addIIOWriteProgressListener(listener);
jpegWriter.addIIOWriteWarningListener(listener);
jpegWriter.write(renderedImage); jpegWriter.write(renderedImage);
} }
finally { finally {
@ -361,7 +368,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
} }
else { else {
// Write image data // Write image data
writeImageData(createCompressorStream(renderedImage, param, entries), renderedImage, numBands, bandOffsets, bitOffsets); writeImageData(createCompressorStream(renderedImage, param, entries), imageIndex, renderedImage, numBands, bandOffsets, bitOffsets);
} }
long stripByteCount = imageOutput.getStreamPosition() - stripOffset; long stripByteCount = imageOutput.getStreamPosition() - stripOffset;
@ -451,6 +458,9 @@ public final class TIFFImageWriter extends ImageWriterBase {
output.length: 12600399 output.length: 12600399
*/ */
int samplesPerPixel = (Integer) entries.get(TIFF.TAG_SAMPLES_PER_PIXEL).getValue();
int bitPerSample = ((short[]) entries.get(TIFF.TAG_BITS_PER_SAMPLE).getValue())[0];
// Use predictor by default for LZW and ZLib/Deflate // Use predictor by default for LZW and ZLib/Deflate
// TODO: Unless explicitly disabled in TIFFImageWriteParam // TODO: Unless explicitly disabled in TIFFImageWriteParam
int compression = (int) entries.get(TIFF.TAG_COMPRESSION).getValue(); int compression = (int) entries.get(TIFF.TAG_COMPRESSION).getValue();
@ -489,16 +499,16 @@ public final class TIFFImageWriter extends ImageWriterBase {
stream = IIOUtil.createStreamAdapter(imageOutput); stream = IIOUtil.createStreamAdapter(imageOutput);
stream = new DeflaterOutputStream(stream, new Deflater(deflateSetting), 1024); stream = new DeflaterOutputStream(stream, new Deflater(deflateSetting), 1024);
if (entries.containsKey(TIFF.TAG_PREDICTOR) && entries.get(TIFF.TAG_PREDICTOR).getValue().equals(TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING)) { if (entries.containsKey(TIFF.TAG_PREDICTOR) && entries.get(TIFF.TAG_PREDICTOR).getValue().equals(TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING)) {
stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder()); stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), samplesPerPixel, bitPerSample, imageOutput.getByteOrder());
} }
return new DataOutputStream(stream); return new DataOutputStream(stream);
case TIFFExtension.COMPRESSION_LZW: case TIFFExtension.COMPRESSION_LZW:
stream = IIOUtil.createStreamAdapter(imageOutput); stream = IIOUtil.createStreamAdapter(imageOutput);
stream = new EncoderStream(stream, new LZWEncoder((image.getTileWidth() * image.getTileHeight() * image.getColorModel().getPixelSize() + 7) / 8)); stream = new EncoderStream(stream, new LZWEncoder((image.getTileWidth() * image.getTileHeight() * samplesPerPixel * bitPerSample + 7) / 8));
if (entries.containsKey(TIFF.TAG_PREDICTOR) && entries.get(TIFF.TAG_PREDICTOR).getValue().equals(TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING)) { if (entries.containsKey(TIFF.TAG_PREDICTOR) && entries.get(TIFF.TAG_PREDICTOR).getValue().equals(TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING)) {
stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder()); stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), samplesPerPixel, bitPerSample, imageOutput.getByteOrder());
} }
return new DataOutputStream(stream); return new DataOutputStream(stream);
@ -553,12 +563,12 @@ public final class TIFFImageWriter extends ImageWriterBase {
throw new IllegalArgumentException("Can't determine PhotometricInterpretation for color model: " + colorModel); throw new IllegalArgumentException("Can't determine PhotometricInterpretation for color model: " + colorModel);
} }
private short[] createColorMap(final IndexColorModel colorModel) { private short[] createColorMap(final IndexColorModel colorModel, final int sampleSize) {
// TIFF6.pdf p. 23: // TIFF6.pdf p. 23:
// A TIFF color map is stored as type SHORT, count = 3 * (2^BitsPerSample) // A TIFF color map is stored as type SHORT, count = 3 * (2^BitsPerSample)
// "In a TIFF ColorMap, all the Red values come first, followed by the Green values, then the Blue values. // "In a TIFF ColorMap, all the Red values come first, followed by the Green values, then the Blue values.
// In the ColorMap, black is represented by 0,0,0 and white is represented by 65535, 65535, 65535." // In the ColorMap, black is represented by 0,0,0 and white is represented by 65535, 65535, 65535."
short[] colorMap = new short[(int) (3 * Math.pow(2, colorModel.getPixelSize()))]; short[] colorMap = new short[(int) (3 * Math.pow(2, sampleSize))];
for (int i = 0; i < colorModel.getMapSize(); i++) { for (int i = 0; i < colorModel.getMapSize(); i++) {
int color = colorModel.getRGB(i); int color = colorModel.getRGB(i);
@ -584,14 +594,14 @@ public final class TIFFImageWriter extends ImageWriterBase {
return shorts; return shorts;
} }
private void writeImageData(DataOutput stream, RenderedImage renderedImage, int numComponents, int[] bandOffsets, int[] bitOffsets) throws IOException { private void writeImageData(DataOutput stream, int imageIndex, RenderedImage renderedImage, int numComponents, int[] bandOffsets, int[] bitOffsets) throws IOException {
// Store 3BYTE, 4BYTE as is (possibly need to re-arrange to RGB order) // Store 3BYTE, 4BYTE as is (possibly need to re-arrange to RGB order)
// Store INT_RGB as 3BYTE, INT_ARGB as 4BYTE?, INT_ABGR must be re-arranged // Store INT_RGB as 3BYTE, INT_ARGB as 4BYTE?, INT_ABGR must be re-arranged
// Store IndexColorModel as is // Store IndexColorModel as is
// Store BYTE_GRAY as is // Store BYTE_GRAY as is
// Store USHORT_GRAY as is // Store USHORT_GRAY as is
processImageStarted(0); processImageStarted(imageIndex);
final int minTileY = renderedImage.getMinTileY(); final int minTileY = renderedImage.getMinTileY();
final int maxYTiles = minTileY + renderedImage.getNumYTiles(); final int maxYTiles = minTileY + renderedImage.getNumYTiles();
@ -777,7 +787,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
} }
// TODO: Report better progress // TODO: Report better progress
processImageProgress((100f * yTile) / maxYTiles); processImageProgress((100f * (yTile + 1)) / maxYTiles);
} }
if (stream instanceof DataOutputStream) { if (stream instanceof DataOutputStream) {
@ -884,21 +894,22 @@ public final class TIFFImageWriter extends ImageWriterBase {
@Override @Override
public void prepareWriteSequence(IIOMetadata streamMetadata) throws IOException { public void prepareWriteSequence(IIOMetadata streamMetadata) throws IOException {
if (isWritingSequence) { if (writingSequence) {
throw new IllegalStateException("sequence writing has already been started!"); throw new IllegalStateException("sequence writing has already been started!");
} }
// Ignore streamMetadata. ByteOrder is determined from OutputStream
assertOutput(); assertOutput();
isWritingSequence = true; configureStreamByteOrder(streamMetadata, imageOutput);
sequenceTiffWriter = new TIFFWriter();
sequenceTiffWriter.writeTIFFHeader(imageOutput); writingSequence = true;
sequenceTIFFWriter = new TIFFWriter();
sequenceTIFFWriter.writeTIFFHeader(imageOutput);
sequenceLastIFDPos = imageOutput.getStreamPosition(); sequenceLastIFDPos = imageOutput.getStreamPosition();
} }
@Override @Override
public void writeToSequence(IIOImage image, ImageWriteParam param) throws IOException { public void writeToSequence(IIOImage image, ImageWriteParam param) throws IOException {
if (!isWritingSequence) { if (!writingSequence) {
throw new IllegalStateException("prepareWriteSequence() must be called before writeToSequence()!"); throw new IllegalStateException("prepareWriteSequence() must be called before writeToSequence()!");
} }
@ -906,17 +917,18 @@ public final class TIFFImageWriter extends ImageWriterBase {
imageOutput.flushBefore(sequenceLastIFDPos); imageOutput.flushBefore(sequenceLastIFDPos);
} }
sequenceLastIFDPos = writePage(image, param, sequenceTiffWriter, sequenceLastIFDPos); sequenceLastIFDPos = writePage(sequenceIndex++, image, param, sequenceTIFFWriter, sequenceLastIFDPos);
} }
@Override @Override
public void endWriteSequence() throws IOException { public void endWriteSequence() throws IOException {
if (!isWritingSequence) { if (!writingSequence) {
throw new IllegalStateException("prepareWriteSequence() must be called before endWriteSequence()!"); throw new IllegalStateException("prepareWriteSequence() must be called before endWriteSequence()!");
} }
isWritingSequence = false; writingSequence = false;
sequenceTiffWriter = null; sequenceIndex = 0;
sequenceTIFFWriter = null;
sequenceLastIFDPos = -1; sequenceLastIFDPos = -1;
imageOutput.flush(); imageOutput.flush();
} }
@ -925,8 +937,9 @@ public final class TIFFImageWriter extends ImageWriterBase {
protected void resetMembers() { protected void resetMembers() {
super.resetMembers(); super.resetMembers();
isWritingSequence = false; writingSequence = false;
sequenceTiffWriter = null; sequenceIndex = 0;
sequenceTIFFWriter = null;
sequenceLastIFDPos = -1; sequenceLastIFDPos = -1;
} }
@ -1046,7 +1059,6 @@ public final class TIFFImageWriter extends ImageWriterBase {
System.err.println("output.length: " + output.length()); System.err.println("output.length: " + output.length());
// TODO: Support writing multipage TIFF
// ImageOutputStream stream = ImageIO.createImageOutputStream(output); // ImageOutputStream stream = ImageIO.createImageOutputStream(output);
// try { // try {
// writer.setOutput(stream); // writer.setOutput(stream);
@ -1068,4 +1080,52 @@ public final class TIFFImageWriter extends ImageWriterBase {
TIFFImageReader.showIt(read, output.getName()); TIFFImageReader.showIt(read, output.getName());
} }
private class ListenerDelegate extends ProgressListenerBase implements IIOWriteWarningListener {
private final int imageIndex;
public ListenerDelegate(final int imageIndex) {
this.imageIndex = imageIndex;
}
@Override
public void imageComplete(ImageWriter source) {
processImageComplete();
}
@Override
public void imageProgress(ImageWriter source, float percentageDone) {
processImageProgress(percentageDone);
}
@Override
public void imageStarted(ImageWriter source, int imageIndex) {
processImageStarted(this.imageIndex);
}
@Override
public void thumbnailComplete(ImageWriter source) {
processThumbnailComplete();
}
@Override
public void thumbnailProgress(ImageWriter source, float percentageDone) {
processThumbnailProgress(percentageDone);
}
@Override
public void thumbnailStarted(ImageWriter source, int imageIndex, int thumbnailIndex) {
processThumbnailStarted(this.imageIndex, thumbnailIndex);
}
@Override
public void writeAborted(ImageWriter source) {
processWriteAborted();
}
@Override
public void warningOccurred(ImageWriter source, int imageIndex, String warning) {
processWarningOccurred(this.imageIndex, warning);
}
}
} }

View File

@ -36,10 +36,12 @@ import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.util.ImageWriterAbstractTestCase; import com.twelvemonkeys.imageio.util.ImageWriterAbstractTestCase;
import com.twelvemonkeys.io.FastByteArrayOutputStream; import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.NullOutputStream;
import org.junit.Test; import org.junit.Test;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import javax.imageio.*; import javax.imageio.*;
import javax.imageio.event.IIOWriteProgressListener;
import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl; import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.metadata.IIOMetadataNode;
@ -60,6 +62,7 @@ import static com.twelvemonkeys.imageio.plugins.tiff.TIFFImageMetadataTest.creat
import static com.twelvemonkeys.imageio.util.ImageReaderAbstractTest.assertRGBEquals; import static com.twelvemonkeys.imageio.util.ImageReaderAbstractTest.assertRGBEquals;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.junit.Assume.assumeNotNull; import static org.junit.Assume.assumeNotNull;
import static org.mockito.Mockito.*;
/** /**
* TIFFImageWriterTest * TIFFImageWriterTest
@ -382,6 +385,50 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase {
} }
} }
@Test
public void testWriteSequenceProgress() throws IOException {
BufferedImage[] images = new BufferedImage[] {
new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB),
new BufferedImage(110, 100, BufferedImage.TYPE_INT_RGB),
new BufferedImage(120, 100, BufferedImage.TYPE_INT_RGB)
};
ImageWriter writer = createImageWriter();
IIOWriteProgressListener progress = mock(IIOWriteProgressListener.class, "progress");
writer.addIIOWriteProgressListener(progress);
try (ImageOutputStream output = ImageIO.createImageOutputStream(new NullOutputStream())) {
writer.setOutput(output);
try {
writer.prepareWriteSequence(null);
for (int i = 0; i < images.length; i++) {
reset(progress);
ImageWriteParam param = writer.getDefaultWriteParam();
if (i == images.length - 1) {
// Make sure that the JPEG delegation outputs the correct indexes
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionType("JPEG");
}
writer.writeToSequence(new IIOImage(images[i], null, null), param);
verify(progress, times(1)).imageStarted(writer, i);
verify(progress, atLeastOnce()).imageProgress(eq(writer), anyFloat());
verify(progress, times(1)).imageComplete(writer);
}
writer.endWriteSequence();
}
catch (IOException e) {
fail(e.getMessage());
}
}
}
@Test @Test
public void testReadWriteRead1BitLZW() throws IOException { public void testReadWriteRead1BitLZW() throws IOException {
// Read original LZW compressed TIFF // Read original LZW compressed TIFF