diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java index 847f57e0..49eb041e 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java @@ -38,11 +38,13 @@ import com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry; import com.twelvemonkeys.imageio.metadata.tiff.TIFFWriter; import com.twelvemonkeys.imageio.stream.SubImageOutputStream; import com.twelvemonkeys.imageio.util.IIOUtil; +import com.twelvemonkeys.imageio.util.ProgressListenerBase; import com.twelvemonkeys.io.enc.EncoderStream; import com.twelvemonkeys.io.enc.PackBitsEncoder; import com.twelvemonkeys.lang.Validate; import javax.imageio.*; +import javax.imageio.event.IIOWriteWarningListener; import javax.imageio.metadata.IIOInvalidTreeException; import javax.imageio.metadata.IIOMetadata; 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$ */ public final class TIFFImageWriter extends ImageWriterBase { - // Short term - // TODO: Support more of the ImageIO metadata (ie. compression from metadata, etc) - // Long term // TODO: Support tiling // 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 storing multiple images in one stream (multi-page TIFF) - // TODO: Support use-case: Transcode multi-layer PSD to multi-page TIFF with metadata + // 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-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) @@ -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 // Use sensible defaults for compression based on input? None is sensible... :-) // 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); /** * Flag for active sequence writing */ - private boolean isWritingSequence = false; + private boolean writingSequence = false; + + private int sequenceIndex = 0; /** * Metadata writer for sequence writing */ - private TIFFWriter sequenceTiffWriter = null; + private TIFFWriter sequenceTIFFWriter = null; /** * Position of last IFD Pointer on active sequence writing @@ -137,12 +139,12 @@ public final class TIFFImageWriter extends ImageWriterBase { TIFFWriter tiffWriter = new TIFFWriter(); tiffWriter.writeTIFFHeader(imageOutput); - writePage(image, param, tiffWriter, imageOutput.getStreamPosition()); + writePage(0, image, param, tiffWriter, imageOutput.getStreamPosition()); 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 { 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)); 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)); } else { @@ -353,6 +357,9 @@ public final class TIFFImageWriter extends ImageWriterBase { ImageWriter jpegWriter = writers.next(); try { jpegWriter.setOutput(new SubImageOutputStream(imageOutput)); + ListenerDelegate listener = new ListenerDelegate(imageIndex); + jpegWriter.addIIOWriteProgressListener(listener); + jpegWriter.addIIOWriteWarningListener(listener); jpegWriter.write(renderedImage); } finally { @@ -361,7 +368,7 @@ public final class TIFFImageWriter extends ImageWriterBase { } else { // 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; @@ -451,6 +458,9 @@ public final class TIFFImageWriter extends ImageWriterBase { 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 // TODO: Unless explicitly disabled in TIFFImageWriteParam int compression = (int) entries.get(TIFF.TAG_COMPRESSION).getValue(); @@ -489,16 +499,16 @@ public final class TIFFImageWriter extends ImageWriterBase { stream = IIOUtil.createStreamAdapter(imageOutput); 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)) { - 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); case TIFFExtension.COMPRESSION_LZW: 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)) { - 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); @@ -553,12 +563,12 @@ public final class TIFFImageWriter extends ImageWriterBase { 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: // 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 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++) { int color = colorModel.getRGB(i); @@ -584,14 +594,14 @@ public final class TIFFImageWriter extends ImageWriterBase { 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 INT_RGB as 3BYTE, INT_ARGB as 4BYTE?, INT_ABGR must be re-arranged // Store IndexColorModel as is // Store BYTE_GRAY as is // Store USHORT_GRAY as is - processImageStarted(0); + processImageStarted(imageIndex); final int minTileY = renderedImage.getMinTileY(); final int maxYTiles = minTileY + renderedImage.getNumYTiles(); @@ -777,7 +787,7 @@ public final class TIFFImageWriter extends ImageWriterBase { } // TODO: Report better progress - processImageProgress((100f * yTile) / maxYTiles); + processImageProgress((100f * (yTile + 1)) / maxYTiles); } if (stream instanceof DataOutputStream) { @@ -884,21 +894,22 @@ public final class TIFFImageWriter extends ImageWriterBase { @Override public void prepareWriteSequence(IIOMetadata streamMetadata) throws IOException { - if (isWritingSequence) { + if (writingSequence) { throw new IllegalStateException("sequence writing has already been started!"); } - // Ignore streamMetadata. ByteOrder is determined from OutputStream assertOutput(); - isWritingSequence = true; - sequenceTiffWriter = new TIFFWriter(); - sequenceTiffWriter.writeTIFFHeader(imageOutput); + configureStreamByteOrder(streamMetadata, imageOutput); + + writingSequence = true; + sequenceTIFFWriter = new TIFFWriter(); + sequenceTIFFWriter.writeTIFFHeader(imageOutput); sequenceLastIFDPos = imageOutput.getStreamPosition(); } @Override public void writeToSequence(IIOImage image, ImageWriteParam param) throws IOException { - if (!isWritingSequence) { + if (!writingSequence) { throw new IllegalStateException("prepareWriteSequence() must be called before writeToSequence()!"); } @@ -906,17 +917,18 @@ public final class TIFFImageWriter extends ImageWriterBase { imageOutput.flushBefore(sequenceLastIFDPos); } - sequenceLastIFDPos = writePage(image, param, sequenceTiffWriter, sequenceLastIFDPos); + sequenceLastIFDPos = writePage(sequenceIndex++, image, param, sequenceTIFFWriter, sequenceLastIFDPos); } @Override public void endWriteSequence() throws IOException { - if (!isWritingSequence) { + if (!writingSequence) { throw new IllegalStateException("prepareWriteSequence() must be called before endWriteSequence()!"); } - isWritingSequence = false; - sequenceTiffWriter = null; + writingSequence = false; + sequenceIndex = 0; + sequenceTIFFWriter = null; sequenceLastIFDPos = -1; imageOutput.flush(); } @@ -925,8 +937,9 @@ public final class TIFFImageWriter extends ImageWriterBase { protected void resetMembers() { super.resetMembers(); - isWritingSequence = false; - sequenceTiffWriter = null; + writingSequence = false; + sequenceIndex = 0; + sequenceTIFFWriter = null; sequenceLastIFDPos = -1; } @@ -1046,7 +1059,6 @@ public final class TIFFImageWriter extends ImageWriterBase { System.err.println("output.length: " + output.length()); - // TODO: Support writing multipage TIFF // ImageOutputStream stream = ImageIO.createImageOutputStream(output); // try { // writer.setOutput(stream); @@ -1068,4 +1080,52 @@ public final class TIFFImageWriter extends ImageWriterBase { 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); + } + } } diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java index b57b0e0d..84b5da75 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java @@ -36,10 +36,12 @@ import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.util.ImageWriterAbstractTestCase; import com.twelvemonkeys.io.FastByteArrayOutputStream; +import com.twelvemonkeys.io.NullOutputStream; import org.junit.Test; import org.w3c.dom.NodeList; import javax.imageio.*; +import javax.imageio.event.IIOWriteProgressListener; import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataFormatImpl; 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 org.junit.Assert.*; import static org.junit.Assume.assumeNotNull; +import static org.mockito.Mockito.*; /** * 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 public void testReadWriteRead1BitLZW() throws IOException { // Read original LZW compressed TIFF