diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java index e559406a..bccf599c 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java @@ -1605,7 +1605,7 @@ public abstract class ImageReaderAbstractTest { /** * Slightly fuzzy RGB equals method. Variable tolerance. */ - protected void assertRGBEquals(String message, int expectedRGB, int actualRGB, int tolerance) { + public static void assertRGBEquals(String message, int expectedRGB, int actualRGB, int tolerance) { assertEquals(message, (expectedRGB >>> 24) & 0xff, (actualRGB >>> 24) & 0xff, 0); assertEquals(message, (expectedRGB >> 16) & 0xff, (actualRGB >> 16) & 0xff, tolerance); assertEquals(message, (expectedRGB >> 8) & 0xff, (actualRGB >> 8) & 0xff, tolerance); 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 15d934fa..17e75864 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 @@ -196,39 +196,30 @@ public final class TIFFImageWriter extends ImageWriterBase { @Override public void write(final IIOMetadata streamMetadata, final IIOImage image, final ImageWriteParam param) throws IOException { // TODO: Validate input - assertOutput(); // TODO: streamMetadata? - // TODO: Consider writing TIFF header, offset to IFD0 (leave blank), write image data with correct - // tiling/compression/etc, then write IFD0, go back and update IFD0 offset? - - // Write minimal TIFF header (required "Baseline" fields) - // Use EXIFWriter to write leading metadata (TODO: consider rename to TTIFFWriter, again...) // TODO: Make TIFFEntry and possibly TIFFDirectory? public EXIFWriter exifWriter = new EXIFWriter(); exifWriter.writeTIFFHeader(imageOutput); - long IFD0Pos = imageOutput.getStreamPosition(); - writePage(image, param, exifWriter, IFD0Pos); - imageOutput.writeInt(0); // EOF + + writePage(image, param, exifWriter, imageOutput.getStreamPosition()); + imageOutput.flush(); } - private long writePage(IIOImage image, ImageWriteParam param, EXIFWriter exifWriter, long lastIFDPointer) + private long writePage(IIOImage image, ImageWriteParam param, EXIFWriter exifWriter, long lastIFDPointerOffset) throws IOException { RenderedImage renderedImage = image.getRenderedImage(); + + TIFFImageMetadata metadata = image.getMetadata() != null + ? convertImageMetadata(image.getMetadata(), ImageTypeSpecifier.createFromRenderedImage(renderedImage), param) + : getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(renderedImage), param); + ColorModel colorModel = renderedImage.getColorModel(); - int numComponents = colorModel.getNumComponents(); - - TIFFImageMetadata metadata; - if (image.getMetadata() != null) { - metadata = convertImageMetadata(image.getMetadata(), ImageTypeSpecifier.createFromRenderedImage(renderedImage), param); - } - else { - metadata = initMeta(null, ImageTypeSpecifier.createFromRenderedImage(renderedImage), param); - } - SampleModel sampleModel = renderedImage.getSampleModel(); + int numBands = sampleModel.getNumBands(); + int pixelSize = computePixelSize(sampleModel); int[] bandOffsets; int[] bitOffsets; @@ -248,18 +239,19 @@ public final class TIFFImageWriter extends ImageWriterBase { throw new IllegalArgumentException("Unknown bit/bandOffsets for sample model: " + sampleModel); } + // TODO: There shouldn't be necessary to create a separate map here, this should be handled in the + // convertImageMetadata/getDefaultImageMetadata methods.... Map entries = new LinkedHashMap<>(); entries.put(TIFF.TAG_IMAGE_WIDTH, new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, renderedImage.getWidth())); entries.put(TIFF.TAG_IMAGE_HEIGHT, new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, renderedImage.getHeight())); - // entries.add(new TIFFEntry(TIFF.TAG_ORIENTATION, 1)); // (optional) + entries.put(TIFF.TAG_ORIENTATION, new TIFFEntry(TIFF.TAG_ORIENTATION, 1)); // (optional) entries.put(TIFF.TAG_BITS_PER_SAMPLE, new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, asShortArray(sampleModel.getSampleSize()))); + // If numComponents > numColorComponents, write ExtraSamples - if (numComponents > colorModel.getNumColorComponents()) { + if (numBands > colorModel.getNumColorComponents()) { // TODO: Write per component > numColorComponents if (colorModel.hasAlpha()) { - entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, colorModel.isAlphaPremultiplied() - ? TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA - : TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA)); + entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, colorModel.isAlphaPremultiplied() ? TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA : TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA)); } else { entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED)); @@ -275,6 +267,7 @@ public final class TIFFImageWriter extends ImageWriterBase { else { compression = TIFFImageWriteParam.getCompressionType(param); } + entries.put(TIFF.TAG_COMPRESSION, new TIFFEntry(TIFF.TAG_COMPRESSION, compression)); // TODO: Let param/metadata control predictor @@ -283,29 +276,39 @@ public final class TIFFImageWriter extends ImageWriterBase { case TIFFExtension.COMPRESSION_ZLIB: case TIFFExtension.COMPRESSION_DEFLATE: case TIFFExtension.COMPRESSION_LZW: - entries.put(TIFF.TAG_PREDICTOR, new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING)); + if (pixelSize >= 8) { + entries.put(TIFF.TAG_PREDICTOR, new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING)); + } + break; + case TIFFExtension.COMPRESSION_CCITT_T4: Entry group3options = metadata.getIFD().getEntryById(TIFF.TAG_GROUP3OPTIONS); + if (group3options == null) { group3options = new TIFFEntry(TIFF.TAG_GROUP3OPTIONS, (long) TIFFExtension.GROUP3OPT_2DENCODING); } + entries.put(TIFF.TAG_GROUP3OPTIONS, group3options); + break; + case TIFFExtension.COMPRESSION_CCITT_T6: Entry group4options = metadata.getIFD().getEntryById(TIFF.TAG_GROUP4OPTIONS); + if (group4options == null) { group4options = new TIFFEntry(TIFF.TAG_GROUP4OPTIONS, 0L); } + entries.put(TIFF.TAG_GROUP4OPTIONS, group4options); + break; + default: } // TODO: We might want to support CMYK in JPEG as well... Pending JPEG CMYK write support. - int photometric = compression == TIFFExtension.COMPRESSION_JPEG ? - TIFFExtension.PHOTOMETRIC_YCBCR : - getPhotometricInterpretation(colorModel); + int photometric = compression == TIFFExtension.COMPRESSION_JPEG ? TIFFExtension.PHOTOMETRIC_YCBCR : getPhotometricInterpretation(colorModel); entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, photometric)); if (photometric == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) { @@ -313,11 +316,7 @@ public final class TIFFImageWriter extends ImageWriterBase { entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1)); } else { - if (colorModel.getPixelSize() == 1) { - numComponents = 1; - } - - entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numComponents)); + entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numBands)); // Note: Assuming sRGB to be the default RGB interpretation ColorSpace colorSpace = colorModel.getColorSpace(); @@ -332,11 +331,10 @@ public final class TIFFImageWriter extends ImageWriterBase { } // TODO: Float values! + // TODO: Again, this should be handled in the metadata conversion.... // Get Software from metadata, or use default Entry software = metadata.getIFD().getEntryById(TIFF.TAG_SOFTWARE); - entries.put(TIFF.TAG_SOFTWARE, software != null - ? software - : new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer " + originatingProvider.getVersion())); + entries.put(TIFF.TAG_SOFTWARE, software != null ? software : new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer " + originatingProvider.getVersion())); // Copy metadata to output int[] copyTags = { @@ -366,36 +364,50 @@ public final class TIFFImageWriter extends ImageWriterBase { Entry yRes = metadata.getIFD().getEntryById(TIFF.TAG_Y_RESOLUTION); entries.put(TIFF.TAG_Y_RESOLUTION, yRes != null ? yRes : new TIFFEntry(TIFF.TAG_Y_RESOLUTION, STANDARD_DPI)); Entry resUnit = metadata.getIFD().getEntryById(TIFF.TAG_RESOLUTION_UNIT); - entries.put(TIFF.TAG_RESOLUTION_UNIT, - resUnit != null ? resUnit : new TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFFBaseline.RESOLUTION_UNIT_DPI)); + entries.put(TIFF.TAG_RESOLUTION_UNIT, resUnit != null ? resUnit : new TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFFBaseline.RESOLUTION_UNIT_DPI)); // TODO: RowsPerStrip - can be entire image (or even 2^32 -1), but it's recommended to write "about 8K bytes" per strip - entries.put(TIFF.TAG_ROWS_PER_STRIP, new TIFFEntry(TIFF.TAG_ROWS_PER_STRIP, Integer.MAX_VALUE)); // TODO: Allowed but not recommended + entries.put(TIFF.TAG_ROWS_PER_STRIP, new TIFFEntry(TIFF.TAG_ROWS_PER_STRIP, renderedImage.getHeight())); // - StripByteCounts - for no compression, entire image data... (TODO: How to know the byte counts prior to writing data?) - TIFFEntry dummyStripByteCounts = new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, -1); - entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, dummyStripByteCounts); // Updated later + entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, -1)); // Updated later // - StripOffsets - can be offset to single strip only (TODO: but how large is the IFD data...???) - TIFFEntry dummyStripOffsets = new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, -1); - entries.put(TIFF.TAG_STRIP_OFFSETS, dummyStripOffsets); // Updated later + entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, -1)); // Updated later // TODO: If tiled, write tile indexes etc // Depending on param.getTilingMode - long nextIFDPointer = -1; - long stripOffset = -1; - long stripByteCount = 0; + long nextIFDPointerOffset = -1; if (compression == TIFFBaseline.COMPRESSION_NONE) { - long ifdOffset = exifWriter.computeIFDOffsetSize(entries.values()); - long dataLength = renderedImage.getWidth() * renderedImage.getHeight() * numComponents; - long pointerPos = imageOutput.getStreamPosition() + dataLength + 4 + ifdOffset; - imageOutput.writeInt((int) pointerPos); + // This implementation, allows semi-streaming-compatible uncompressed TIFFs + long streamPosition = imageOutput.getStreamPosition(); + + long ifdSize = exifWriter.computeIFDSize(entries.values()); + long stripOffset = streamPosition + 4 + ifdSize + 4; + long stripByteCount = (renderedImage.getWidth() * renderedImage.getHeight() * pixelSize + 7) / 8; + + entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, stripOffset)); + entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, stripByteCount)); + + long ifdPointer = exifWriter.writeIFD(entries.values(), imageOutput); // NOTE: Writer takes case of ordering tags + nextIFDPointerOffset = imageOutput.getStreamPosition(); + + // If we have a previous IFD, update pointer + if (streamPosition > lastIFDPointerOffset) { + imageOutput.seek(lastIFDPointerOffset); + imageOutput.writeInt((int) ifdPointer); + imageOutput.seek(nextIFDPointerOffset); + } + + imageOutput.writeInt(0); // Update next IFD pointer later } else { - imageOutput.writeInt(0); // Update IFD Pointer later + imageOutput.writeInt(0); // Update current IFD pointer later } - stripOffset = imageOutput.getStreamPosition(); + long stripOffset = imageOutput.getStreamPosition(); + // TODO: Create compressor stream per Tile/Strip + // TODO: Cache JPEGImageWriter, dispose in dispose() method if (compression == TIFFExtension.COMPRESSION_JPEG) { Iterator writers = ImageIO.getImageWritersByFormatName("JPEG"); @@ -415,40 +427,45 @@ public final class TIFFImageWriter extends ImageWriterBase { } else { // Write image data - writeImageData(createCompressorStream(renderedImage, param, entries), renderedImage, numComponents, bandOffsets, - bitOffsets); + writeImageData(createCompressorStream(renderedImage, param, entries), renderedImage, numBands, bandOffsets, bitOffsets); } - stripByteCount = imageOutput.getStreamPosition() - stripOffset; + + long stripByteCount = imageOutput.getStreamPosition() - stripOffset; // Update IFD0-pointer, and write IFD if (compression != TIFFBaseline.COMPRESSION_NONE) { - entries.remove(dummyStripOffsets); entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, stripOffset)); - entries.remove(dummyStripByteCounts); entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, stripByteCount)); - long idfOffset = exifWriter.writeIFD(entries.values(), imageOutput); // NOTE: Writer takes case of ordering tags - nextIFDPointer = imageOutput.getStreamPosition(); - imageOutput.seek(lastIFDPointer); - imageOutput.writeInt((int) idfOffset); - imageOutput.seek(nextIFDPointer); - imageOutput.flush(); - } - else { - entries.remove(dummyStripOffsets); - entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, stripOffset)); - entries.remove(dummyStripByteCounts); - entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, stripByteCount)); + long ifdPointer = exifWriter.writeIFD(entries.values(), imageOutput); // NOTE: Writer takes case of ordering tags - exifWriter.writeIFD(entries.values(), imageOutput); // NOTE: Writer takes case of ordering tags - nextIFDPointer = imageOutput.getStreamPosition(); - imageOutput.flush(); + nextIFDPointerOffset = imageOutput.getStreamPosition(); + + // TODO: This is slightly duped.... + // However, need to update here, because to the writeIFD method writes the pointer, but at the incorrect offset + // TODO: Refactor writeIFD to take an offset + imageOutput.seek(lastIFDPointerOffset); + imageOutput.writeInt((int) ifdPointer); + imageOutput.seek(nextIFDPointerOffset); + + imageOutput.writeInt(0); // Next IFD pointer updated later } - return nextIFDPointer; + return nextIFDPointerOffset; } - private DataOutput createCompressorStream(RenderedImage image, ImageWriteParam param, Map entries) { + // TODO: Candidate util method + private int computePixelSize(final SampleModel sampleModel) { + int size = 0; + + for (int i = 0; i < sampleModel.getNumBands(); i++) { + size += sampleModel.getSampleSize(i); + } + + return size; + } + + private DataOutput createCompressorStream(final RenderedImage image, final ImageWriteParam param, final Map entries) { /* 36 MB test data: @@ -513,57 +530,56 @@ public final class TIFFImageWriter extends ImageWriterBase { stream = new EncoderStream(stream, new PackBitsEncoder(), true); // NOTE: PackBits + Predictor is possible, but not generally supported, disable it by default // (and probably not even allow it, see http://stackoverflow.com/questions/20337400/tiff-packbits-compression-with-predictor-step) -// stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder()); return new DataOutputStream(stream); case TIFFExtension.COMPRESSION_ZLIB: case TIFFExtension.COMPRESSION_DEFLATE: + // NOTE: This interpretation does the opposite of the JAI TIFFImageWriter, but seems more correct. + // API Docs says: + // A compression quality setting of 0.0 is most generically interpreted as "high compression is important," + // while a setting of 1.0 is most generically interpreted as "high image quality is important." + // However, the JAI TIFFImageWriter uses: + // if (param & compression etc...) { + // float quality = param.getCompressionQuality(); + // deflateLevel = (int)(1 + 8*quality); + // } else { + // deflateLevel = Deflater.DEFAULT_COMPRESSION; + // } + // (in other words, 0.0 means 1 == BEST_SPEED, 1.0 means 9 == BEST_COMPRESSION) + // PS: PNGImageWriter just uses hardcoded BEST_COMPRESSION... :-P int deflateSetting = Deflater.BEST_SPEED; // This is consistent with default compression quality being 1.0 and 0 meaning max compression... if (param.getCompressionMode() == ImageWriteParam.MODE_EXPLICIT) { - // TODO: Determine how to interpret compression quality... - // Docs says: - // A compression quality setting of 0.0 is most generically interpreted as "high compression is important," - // while a setting of 1.0 is most generically interpreted as "high image quality is important." - // Is this what JAI TIFFImageWriter (TIFFDeflater) does? No, it does: - /* - if (param & compression etc...) { - float quality = param.getCompressionQuality(); - deflateLevel = (int)(1 + 8*quality); - } else { - deflateLevel = Deflater.DEFAULT_COMPRESSION; - } - */ - // PS: PNGImageWriter just uses hardcoded BEST_COMPRESSION... :-P - deflateSetting = 9 - Math.round(8 * (param.getCompressionQuality())); // This seems more correct + deflateSetting = Deflater.BEST_COMPRESSION - Math.round((Deflater.BEST_COMPRESSION - 1) * param.getCompressionQuality()); } stream = IIOUtil.createStreamAdapter(imageOutput); stream = new DeflaterOutputStream(stream, new Deflater(deflateSetting), 1024); - stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder()); + 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()); + } return new DataOutputStream(stream); case TIFFExtension.COMPRESSION_LZW: stream = IIOUtil.createStreamAdapter(imageOutput); - stream = new EncoderStream(stream, new LZWEncoder((image.getTileWidth() * image.getTileHeight() - * image.getTile(0, 0).getNumBands() * image.getColorModel().getComponentSize(0) + 7) / 8)); - stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), - image.getColorModel().getComponentSize(0), imageOutput.getByteOrder()); + stream = new EncoderStream(stream, new LZWEncoder((image.getTileWidth() * image.getTileHeight() * image.getColorModel().getPixelSize() + 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()); + } return new DataOutputStream(stream); + case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE: case TIFFExtension.COMPRESSION_CCITT_T4: case TIFFExtension.COMPRESSION_CCITT_T6: long option = 0L; + if (compression != TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE) { - option = (long) entries.get(compression == TIFFExtension.COMPRESSION_CCITT_T4 - ? TIFF.TAG_GROUP3OPTIONS - : TIFF.TAG_GROUP4OPTIONS).getValue(); + option = (long) entries.get(compression == TIFFExtension.COMPRESSION_CCITT_T4 ? TIFF.TAG_GROUP3OPTIONS : TIFF.TAG_GROUP4OPTIONS).getValue(); } + Entry fillOrderEntry = entries.get(TIFF.TAG_FILL_ORDER); - int fillOrder = (int) (fillOrderEntry != null - ? fillOrderEntry.getValue() - : TIFFBaseline.FILL_LEFT_TO_RIGHT); + int fillOrder = (int) (fillOrderEntry != null ? fillOrderEntry.getValue() : TIFFBaseline.FILL_LEFT_TO_RIGHT); stream = IIOUtil.createStreamAdapter(imageOutput); stream = new CCITTFaxEncoderStream(stream, image.getTileWidth(), image.getTileHeight(), compression, fillOrder, option); @@ -908,6 +924,11 @@ public final class TIFFImageWriter extends ImageWriterBase { if (!isWritingSequence) { throw new IllegalStateException("prepareWriteSequence() must be called before writeToSequence()!"); } + + if (sequenceLastIFDPos > 0) { + imageOutput.flushBefore(sequenceLastIFDPos); + } + sequenceLastIFDPos = writePage(image, param, sequenceExifWriter, sequenceLastIFDPos); } @@ -916,7 +937,7 @@ public final class TIFFImageWriter extends ImageWriterBase { if (!isWritingSequence) { throw new IllegalStateException("prepareWriteSequence() must be called before endWriteSequence()!"); } - imageOutput.writeInt(0); // EOF + isWritingSequence = false; sequenceExifWriter = null; sequenceLastIFDPos = -1; 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 86d6e135..fdd925a0 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 @@ -34,8 +34,12 @@ import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; import com.twelvemonkeys.imageio.metadata.exif.Rational; import com.twelvemonkeys.imageio.metadata.exif.TIFF; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import com.twelvemonkeys.imageio.util.ImageWriterAbstractTestCase; +import com.twelvemonkeys.io.FastByteArrayOutputStream; import org.junit.Test; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import javax.imageio.*; import javax.imageio.metadata.IIOMetadata; @@ -46,12 +50,18 @@ import javax.imageio.stream.ImageOutputStream; import java.awt.*; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; import static com.twelvemonkeys.imageio.plugins.tiff.TIFFImageMetadataTest.createTIFFFieldNode; +import static com.twelvemonkeys.imageio.util.ImageReaderAbstractTest.assertRGBEquals; import static org.junit.Assert.*; +import static org.junit.Assume.assumeNotNull; /** * TIFFImageWriterTest @@ -277,62 +287,461 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase { } @Test - public void testSequenceWriter() throws IOException { + public void testWriterCanWriteSequence() { + ImageWriter writer = createImageWriter(); + assertTrue("Writer should support sequence writing", writer.canWriteSequence()); + } + + // TODO: Test Sequence writing without prepare/end sequence + + @Test + public void testWriteSequence() 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), + new BufferedImage(130, 100, BufferedImage.TYPE_INT_RGB) + }; + + Color[] colors = new Color[] {Color.RED, Color.GREEN, Color.BLUE, Color.ORANGE}; + + for (int i = 0; i < images.length; i++) { + BufferedImage image = images[i]; + Graphics2D g2d = image.createGraphics(); + try { + g2d.setColor(colors[i]); + g2d.fillRect(0, 0, 100, 100); + } + finally { + g2d.dispose(); + } + } + ImageWriter writer = createImageWriter(); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - ImageOutputStream stream = ImageIO.createImageOutputStream(buffer); - writer.setOutput(stream); + try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(output); - Graphics2D g2d = null; - BufferedImage image[] = new BufferedImage[] { - new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB), - new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB), - new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB) - }; - g2d = image[0].createGraphics(); - g2d.setColor(Color.red); - g2d.fillRect(0,0,100,100); - g2d.dispose(); - g2d = image[1].createGraphics(); - g2d.setColor(Color.green); - g2d.fillRect(0,0,100,100); - g2d.dispose(); - g2d = image[2].createGraphics(); - g2d.setColor(Color.blue); - g2d.fillRect(0,0,100,100); - g2d.dispose(); + ImageWriteParam params = writer.getDefaultWriteParam(); + params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + try { + writer.prepareWriteSequence(null); - ImageWriteParam params = writer.getDefaultWriteParam(); - params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + params.setCompressionType("JPEG"); + writer.writeToSequence(new IIOImage(images[0], null, null), params); - assertTrue("", writer.canWriteSequence()); + params.setCompressionType("None"); + writer.writeToSequence(new IIOImage(images[1], null, null), params); - try { - writer.prepareWriteSequence(null); + params.setCompressionType("None"); + writer.writeToSequence(new IIOImage(images[2], null, null), params); - params.setCompressionType("JPEG"); - writer.writeToSequence(new IIOImage(image[0], null, null), params); - params.setCompressionType("None"); - writer.writeToSequence(new IIOImage(image[1], null, null), params); - params.setCompressionType("JPEG"); - writer.writeToSequence(new IIOImage(image[2], null, null), params); - g2d.dispose(); - writer.endWriteSequence(); - } - catch (IOException e) { - fail(e.getMessage()); - } - finally { - stream.close(); // Force data to be written + params.setCompressionType("PackBits"); + writer.writeToSequence(new IIOImage(images[3], null, null), params); + + writer.endWriteSequence(); + } + catch (IOException e) { + fail(e.getMessage()); + } } - ImageInputStream input = ImageIO.createImageInputStream(new ByteArrayInputStream(buffer.toByteArray())); - ImageReader reader = ImageIO.getImageReaders(input).next(); - reader.setInput(input); - assertEquals("wrong image count", 3, reader.getNumImages(true)); - for(int i = 0; i < reader.getNumImages(true); i++){ - reader.read(i); + try (ImageInputStream input = ImageIO.createImageInputStream(new ByteArrayInputStream(buffer.toByteArray()))) { + ImageReader reader = ImageIO.getImageReaders(input).next(); + reader.setInput(input); + + assertEquals("wrong image count", images.length, reader.getNumImages(true)); + + for (int i = 0; i < reader.getNumImages(true); i++) { + BufferedImage image = reader.read(i); + + assertEquals(images[i].getWidth(), image.getWidth()); + assertEquals(images[i].getHeight(), image.getHeight()); + + assertRGBEquals("RGB differ", images[i].getRGB(0, 0), image.getRGB(0, 0), 5); // Allow room for JPEG compression + } + } + } + + @Test + public void testReadWriteRead1BitLZW() throws IOException { + // Read original LZW compressed TIFF + IIOImage original; + + try (ImageInputStream input = ImageIO.createImageInputStream(getClass().getResource("/tiff/a33.tif"))) { + ImageReader reader = ImageIO.getImageReaders(input).next(); + reader.setInput(input); + + original = reader.readAll(0, null); + reader.dispose(); + } + + assumeNotNull(original); + + // Write it back, using same compression (copied from metadata) + FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768); + + try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) { + ImageWriter writer = createImageWriter(); + writer.setOutput(output); + + writer.write(original); + writer.dispose(); + } + + // Try re-reading the same TIFF + try (ImageInputStream input = ImageIO.createImageInputStream(buffer.createInputStream())) { + ImageReader reader = ImageIO.getImageReaders(input).next(); + reader.setInput(input); + BufferedImage image = reader.read(0); + + BufferedImage orig = (BufferedImage) original.getRenderedImage(); + + int maxH = Math.min(300, image.getHeight()); + for (int y = 0; y < maxH; y++) { + for (int x = 0; x < image.getWidth(); x++) { + assertRGBEquals("Pixel differ: ", orig.getRGB(x, y), image.getRGB(x, y), 0); + } + } + + IIOMetadata metadata = reader.getImageMetadata(0); + IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + IIOMetadataNode compression = (IIOMetadataNode) tree.getElementsByTagName("CompressionTypeName").item(0); + assertEquals("LZW", compression.getAttribute("value")); + + boolean softwareFound = false; + NodeList textEntries = tree.getElementsByTagName("TextEntry"); + for (int i = 0; i < textEntries.getLength(); i++) { + IIOMetadataNode textEntry = (IIOMetadataNode) textEntries.item(i); + if ("Software".equals(textEntry.getAttribute("keyword"))) { + softwareFound = true; + assertEquals("IrfanView", textEntry.getAttribute("value")); + } + } + + assertTrue("Software metadata not found", softwareFound); + } + } + + @Test + public void testReadWriteRead1BitDeflate() throws IOException { + // Read original LZW compressed TIFF + IIOImage original; + + try (ImageInputStream input = ImageIO.createImageInputStream(getClass().getResource("/tiff/a33.tif"))) { + ImageReader reader = ImageIO.getImageReaders(input).next(); + reader.setInput(input); + + original = reader.readAll(0, null); + reader.dispose(); + } + + assumeNotNull(original); + + // Write it back, using same compression (copied from metadata) + FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768); + + try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) { + ImageWriter writer = createImageWriter(); + writer.setOutput(output); + + ImageWriteParam param = writer.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType("Deflate"); + + writer.write(null, original, param); + writer.dispose(); + } + + // Try re-reading the same TIFF + try (ImageInputStream input = ImageIO.createImageInputStream(buffer.createInputStream())) { + ImageReader reader = ImageIO.getImageReaders(input).next(); + reader.setInput(input); + BufferedImage image = reader.read(0); + + BufferedImage orig = (BufferedImage) original.getRenderedImage(); + + int maxH = Math.min(300, image.getHeight()); + for (int y = 0; y < maxH; y++) { + for (int x = 0; x < image.getWidth(); x++) { + assertRGBEquals("Pixel differ: ", orig.getRGB(x, y), image.getRGB(x, y), 0); + } + } + + IIOMetadata metadata = reader.getImageMetadata(0); + IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + IIOMetadataNode compression = (IIOMetadataNode) tree.getElementsByTagName("CompressionTypeName").item(0); + assertEquals("Deflate", compression.getAttribute("value")); + + boolean softwareFound = false; + NodeList textEntries = tree.getElementsByTagName("TextEntry"); + for (int i = 0; i < textEntries.getLength(); i++) { + IIOMetadataNode textEntry = (IIOMetadataNode) textEntries.item(i); + if ("Software".equals(textEntry.getAttribute("keyword"))) { + softwareFound = true; + assertEquals("IrfanView", textEntry.getAttribute("value")); + } + } + + assertTrue("Software metadata not found", softwareFound); + } + } + + @Test + public void testReadWriteRead1BitNone() throws IOException { + // Read original LZW compressed TIFF + IIOImage original; + + try (ImageInputStream input = ImageIO.createImageInputStream(getClass().getResource("/tiff/a33.tif"))) { + ImageReader reader = ImageIO.getImageReaders(input).next(); + reader.setInput(input); + + original = reader.readAll(0, null); + reader.dispose(); + } + + assumeNotNull(original); + + // Write it back, using same compression (copied from metadata) + FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768); + + try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) { + ImageWriter writer = createImageWriter(); + writer.setOutput(output); + + ImageWriteParam param = writer.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType("None"); + + writer.write(null, original, param); + writer.dispose(); + } + + // Try re-reading the same TIFF + try (ImageInputStream input = ImageIO.createImageInputStream(buffer.createInputStream())) { + ImageReader reader = ImageIO.getImageReaders(input).next(); + reader.setInput(input); + BufferedImage image = reader.read(0); + + BufferedImage orig = (BufferedImage) original.getRenderedImage(); + + int maxH = Math.min(300, image.getHeight()); + for (int y = 0; y < maxH; y++) { + for (int x = 0; x < image.getWidth(); x++) { + assertRGBEquals("Pixel differ: ", orig.getRGB(x, y), image.getRGB(x, y), 0); + } + } + + IIOMetadata metadata = reader.getImageMetadata(0); + IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + NodeList compressions = tree.getElementsByTagName("CompressionTypeName"); + IIOMetadataNode compression = (IIOMetadataNode) compressions.item(0); + assertEquals("None", compression.getAttribute("value")); + + boolean softwareFound = false; + NodeList textEntries = tree.getElementsByTagName("TextEntry"); + for (int i = 0; i < textEntries.getLength(); i++) { + IIOMetadataNode textEntry = (IIOMetadataNode) textEntries.item(i); + if ("Software".equals(textEntry.getAttribute("keyword"))) { + softwareFound = true; + assertEquals("IrfanView", textEntry.getAttribute("value")); + } + } + + assertTrue("Software metadata not found", softwareFound); + } + } + + @Test + public void testReadWriteRead24BitLZW() throws IOException { + // Read original LZW compressed TIFF + IIOImage original; + + try (ImageInputStream input = ImageIO.createImageInputStream(getClass().getResource("/tiff/quad-lzw.tif"))) { + ImageReader reader = ImageIO.getImageReaders(input).next(); + reader.setInput(input); + + original = reader.readAll(0, null); + reader.dispose(); + } + + assumeNotNull(original); + + // Write it back, using same compression (copied from metadata) + FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768); + + try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) { + ImageWriter writer = createImageWriter(); + writer.setOutput(output); + + writer.write(original); + writer.dispose(); + } + + // Try re-reading the same TIFF + try (ImageInputStream input = ImageIO.createImageInputStream(buffer.createInputStream())) { + ImageReader reader = ImageIO.getImageReaders(input).next(); + reader.setInput(input); + BufferedImage image = reader.read(0); + + BufferedImage orig = (BufferedImage) original.getRenderedImage(); + + int maxH = Math.min(300, image.getHeight()); + for (int y = 0; y < maxH; y++) { + for (int x = 0; x < image.getWidth(); x++) { + assertRGBEquals("Pixel differ: ", orig.getRGB(x, y), image.getRGB(x, y), 0); + } + } + + IIOMetadata metadata = reader.getImageMetadata(0); + IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + IIOMetadataNode compression = (IIOMetadataNode) tree.getElementsByTagName("CompressionTypeName").item(0); + assertEquals("LZW", compression.getAttribute("value")); + + boolean softwareFound = false; + NodeList textEntries = tree.getElementsByTagName("TextEntry"); + for (int i = 0; i < textEntries.getLength(); i++) { + IIOMetadataNode textEntry = (IIOMetadataNode) textEntries.item(i); + if ("Software".equals(textEntry.getAttribute("keyword"))) { + softwareFound = true; + assertTrue(textEntry.getAttribute("value").startsWith("TwelveMonkeys ImageIO TIFF")); + } + } + + assertTrue("Software metadata not found", softwareFound); + } + } + + @Test + public void testReadWriteRead24BitDeflate() throws IOException { + // Read original LZW compressed TIFF + IIOImage original; + + try (ImageInputStream input = ImageIO.createImageInputStream(getClass().getResource("/tiff/quad-lzw.tif"))) { + ImageReader reader = ImageIO.getImageReaders(input).next(); + reader.setInput(input); + + original = reader.readAll(0, null); + reader.dispose(); + } + + assumeNotNull(original); + + // Write it back, using same compression (copied from metadata) + FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768); + + try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) { + ImageWriter writer = createImageWriter(); + writer.setOutput(output); + + ImageWriteParam param = writer.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType("Deflate"); + + writer.write(null, original, param); + writer.dispose(); + } + + // Try re-reading the same TIFF + try (ImageInputStream input = ImageIO.createImageInputStream(buffer.createInputStream())) { + ImageReader reader = ImageIO.getImageReaders(input).next(); + reader.setInput(input); + BufferedImage image = reader.read(0); + + BufferedImage orig = (BufferedImage) original.getRenderedImage(); + + int maxH = Math.min(300, image.getHeight()); + for (int y = 0; y < maxH; y++) { + for (int x = 0; x < image.getWidth(); x++) { + assertRGBEquals("Pixel differ: ", orig.getRGB(x, y), image.getRGB(x, y), 0); + } + } + + IIOMetadata metadata = reader.getImageMetadata(0); + IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + IIOMetadataNode compression = (IIOMetadataNode) tree.getElementsByTagName("CompressionTypeName").item(0); + assertEquals("Deflate", compression.getAttribute("value")); + + boolean softwareFound = false; + NodeList textEntries = tree.getElementsByTagName("TextEntry"); + for (int i = 0; i < textEntries.getLength(); i++) { + IIOMetadataNode textEntry = (IIOMetadataNode) textEntries.item(i); + if ("Software".equals(textEntry.getAttribute("keyword"))) { + softwareFound = true; + assertTrue(textEntry.getAttribute("value").startsWith("TwelveMonkeys ImageIO TIFF")); + } + } + + assertTrue("Software metadata not found", softwareFound); + } + } + + @Test + public void testReadWriteRead24BitNone() throws IOException { + // Read original LZW compressed TIFF + IIOImage original; + + try (ImageInputStream input = ImageIO.createImageInputStream(getClass().getResource("/tiff/quad-lzw.tif"))) { + ImageReader reader = ImageIO.getImageReaders(input).next(); + reader.setInput(input); + + original = reader.readAll(0, null); + reader.dispose(); + } + + assumeNotNull(original); + + // Write it back, using same compression (copied from metadata) + FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768); + + try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) { + ImageWriter writer = createImageWriter(); + writer.setOutput(output); + + ImageWriteParam param = writer.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType("None"); + + writer.write(null, original, param); + writer.dispose(); + } + +// Path tempFile = Files.createTempFile("test-", ".tif"); +// Files.write(tempFile, buffer.toByteArray()); +// System.out.println("open " + tempFile.toAbsolutePath()); + + // Try re-reading the same TIFF + try (ImageInputStream input = ImageIO.createImageInputStream(buffer.createInputStream())) { + ImageReader reader = ImageIO.getImageReaders(input).next(); + reader.setInput(input); + BufferedImage image = reader.read(0); + + BufferedImage orig = (BufferedImage) original.getRenderedImage(); + + int maxH = Math.min(300, image.getHeight()); + for (int y = 0; y < maxH; y++) { + for (int x = 0; x < image.getWidth(); x++) { + assertRGBEquals("Pixel differ: ", orig.getRGB(x, y), image.getRGB(x, y), 0); + } + } + + IIOMetadata metadata = reader.getImageMetadata(0); + IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + IIOMetadataNode compression = (IIOMetadataNode) tree.getElementsByTagName("CompressionTypeName").item(0); + assertEquals("None", compression.getAttribute("value")); + + boolean softwareFound = false; + NodeList textEntries = tree.getElementsByTagName("TextEntry"); + for (int i = 0; i < textEntries.getLength(); i++) { + IIOMetadataNode textEntry = (IIOMetadataNode) textEntries.item(i); + if ("Software".equals(textEntry.getAttribute("keyword"))) { + softwareFound = true; + assertTrue(textEntry.getAttribute("value").startsWith("TwelveMonkeys ImageIO TIFF")); + } + } + + assertTrue("Software metadata not found", softwareFound); } } } diff --git a/imageio/imageio-tiff/src/test/resources/tiff/a33.tif b/imageio/imageio-tiff/src/test/resources/tiff/a33.tif new file mode 100644 index 00000000..ffda08c7 Binary files /dev/null and b/imageio/imageio-tiff/src/test/resources/tiff/a33.tif differ