TIFFImageWriterTest.java

/*
 * Copyright (c) 2014, Harald Kuhr
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.twelvemonkeys.imageio.plugins.tiff;

import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.tiff.Rational;
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ImageWriterAbstractTest;
import com.twelvemonkeys.io.FastByteArrayOutputStream;

import org.w3c.dom.NodeList;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.event.IIOWriteProgressListener;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.FileCacheImageOutputStream;
import javax.imageio.stream.FileImageOutputStream;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.ImageOutputStreamImpl;
import java.awt.*;
import java.awt.color.*;
import java.awt.image.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static com.twelvemonkeys.imageio.metadata.tiff.TIFF.TAG_X_RESOLUTION;
import static com.twelvemonkeys.imageio.metadata.tiff.TIFF.TAG_Y_RESOLUTION;
import static com.twelvemonkeys.imageio.plugins.tiff.TIFFImageMetadataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME;
import static com.twelvemonkeys.imageio.plugins.tiff.TIFFImageMetadataTest.createTIFFFieldNode;
import static com.twelvemonkeys.imageio.util.ImageReaderAbstractTest.assertRGBEquals;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.mockito.Mockito.*;

/**
 * TIFFImageWriterTest
 *
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @author last modified by $Author: haraldk$
 * @version $Id: TIFFImageWriterTest.java,v 1.0 19.09.13 13:22 haraldk Exp$
 */
public class TIFFImageWriterTest extends ImageWriterAbstractTest<TIFFImageWriter> {
    @Override
    protected ImageWriterSpi createProvider() {
        return new TIFFImageWriterSpi();
    }

    @Override
    protected List<? extends RenderedImage> getTestData() {
        return Arrays.asList(
                new BufferedImage(300, 200, BufferedImage.TYPE_INT_RGB),
                new BufferedImage(301, 199, BufferedImage.TYPE_INT_ARGB),
                new BufferedImage(299, 201, BufferedImage.TYPE_3BYTE_BGR),
                new BufferedImage(160, 90, BufferedImage.TYPE_4BYTE_ABGR),
                new BufferedImage(90, 160, BufferedImage.TYPE_BYTE_GRAY),
                new BufferedImage(30, 20, BufferedImage.TYPE_USHORT_GRAY),
                new BufferedImage(30, 20, BufferedImage.TYPE_BYTE_BINARY),
                new BufferedImage(30, 20, BufferedImage.TYPE_BYTE_INDEXED)
        );
    }

    // TODO: Test write bilevel stays bilevel
    // TODO: Test write indexed stays indexed

    @Test
    public void testWriteWithCustomResolutionNative() throws IOException {
        // Issue 139 Writing TIFF files with custom resolution value
        Rational resolutionValue = new Rational(1200);
        int resolutionUnitValue = TIFFBaseline.RESOLUTION_UNIT_CENTIMETER;

        RenderedImage image = getTestData(0);

        ImageWriter writer = createWriter();
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();

        try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
            writer.setOutput(stream);

            String nativeFormat = SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME;
            IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), null);

            IIOMetadataNode customMeta = new IIOMetadataNode(nativeFormat);

            IIOMetadataNode ifd = new IIOMetadataNode("TIFFIFD");
            customMeta.appendChild(ifd);

            createTIFFFieldNode(ifd, TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, resolutionUnitValue);
            createTIFFFieldNode(ifd, TAG_X_RESOLUTION, TIFF.TYPE_RATIONAL, resolutionValue);
            createTIFFFieldNode(ifd, TAG_Y_RESOLUTION, TIFF.TYPE_RATIONAL, resolutionValue);

            metadata.mergeTree(nativeFormat, customMeta);

            writer.write(null, new IIOImage(image, null, metadata), null);
        }
        catch (IOException e) {
            e.printStackTrace();
            fail(e.getMessage());
        }

        assertTrue(buffer.size() > 0, "No image data written");

        Directory ifds = new TIFFReader().read(new ByteArrayImageInputStream(buffer.toByteArray()));

        Entry resolutionUnit = ifds.getEntryById(TIFF.TAG_RESOLUTION_UNIT);
        assertNotNull(resolutionUnit);
        assertEquals(resolutionUnitValue, ((Number) resolutionUnit.getValue()).intValue());

        Entry xResolution = ifds.getEntryById(TAG_X_RESOLUTION);
        assertNotNull(xResolution);
        assertEquals(resolutionValue, xResolution.getValue());

        Entry yResolution = ifds.getEntryById(TAG_Y_RESOLUTION);
        assertNotNull(yResolution);
        assertEquals(resolutionValue, yResolution.getValue());
    }

    @Test
    public void testByteCountsForUncompressed() throws IOException {
        // See issue #863
        BufferedImage image = ImageTypeSpecifiers.createGrayscale(2, DataBuffer.TYPE_BYTE)
                                                 .createBufferedImage(43, 2); // 43 + 2 = 86 bits/line;

        ImageWriter writer = createWriter();
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();

        try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
            writer.setOutput(stream);
            writer.write(image);
        }

        try (ImageInputStream stream = new ByteArrayImageInputStream(buffer.toByteArray())) {
            Directory entries = new TIFFReader().read(stream);
            Entry byteCountsEntry = entries.getEntryById(TIFF.TAG_STRIP_BYTE_COUNTS);
            assertNotNull(byteCountsEntry);

            assertEquals(22, ((Number) byteCountsEntry.getValue()).intValue()); // 86 bits/line, needs 88 bits * 2 => 22 bytes
        }
    }

    @Test
    public void testWriteWithCustomSoftwareNative() throws IOException {
        String softwareString = "12M TIFF Test 1.0 (build $foo$)";

        RenderedImage image = getTestData(0);

        ImageWriter writer = createWriter();
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();

        try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
            writer.setOutput(stream);

            String nativeFormat = SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME;
            IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), null);

            IIOMetadataNode customMeta = new IIOMetadataNode(nativeFormat);

            IIOMetadataNode ifd = new IIOMetadataNode("TIFFIFD");
            customMeta.appendChild(ifd);

            createTIFFFieldNode(ifd, TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, softwareString);

            metadata.mergeTree(nativeFormat, customMeta);

            writer.write(null, new IIOImage(image, null, metadata), null);
        }
        catch (IOException e) {
            e.printStackTrace();
            fail(e.getMessage());
        }

        assertTrue(buffer.size() > 0, "No image data written");

        Directory ifds = new TIFFReader().read(new ByteArrayImageInputStream(buffer.toByteArray()));
        Entry software = ifds.getEntryById(TIFF.TAG_SOFTWARE);
        assertNotNull(software);
        assertEquals(softwareString, software.getValueAsString());
    }

    @Test
    public void testWriteWithCustomResolutionStandard() throws IOException {
        // Issue 139 Writing TIFF files with custom resolution value
        double resolutionValue = 300 / 25.4; // 300 dpi, 1 inch = 2.54 cm or 25.4 mm
        int resolutionUnitValue = TIFFBaseline.RESOLUTION_UNIT_CENTIMETER;
        Rational expectedResolutionValue = new Rational(Math.round(resolutionValue * 10 * TIFFImageMetadata.RATIONAL_SCALE_FACTOR), TIFFImageMetadata.RATIONAL_SCALE_FACTOR);

        RenderedImage image = getTestData(0);

        ImageWriter writer = createWriter();
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();

        try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
            writer.setOutput(stream);

            String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName;
            IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), null);

            IIOMetadataNode customMeta = new IIOMetadataNode(standardFormat);

            IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
            customMeta.appendChild(dimension);

            IIOMetadataNode xSize = new IIOMetadataNode("HorizontalPixelSize");
            dimension.appendChild(xSize);
            xSize.setAttribute("value", String.valueOf(resolutionValue));

            IIOMetadataNode ySize = new IIOMetadataNode("VerticalPixelSize");
            dimension.appendChild(ySize);
            ySize.setAttribute("value", String.valueOf(resolutionValue));

            metadata.mergeTree(standardFormat, customMeta);

            writer.write(null, new IIOImage(image, null, metadata), null);
        }
        catch (IOException e) {
            e.printStackTrace();
            fail(e.getMessage());
        }

        assertTrue(buffer.size() > 0, "No image data written");

        Directory ifds = new TIFFReader().read(new ByteArrayImageInputStream(buffer.toByteArray()));

        Entry resolutionUnit = ifds.getEntryById(TIFF.TAG_RESOLUTION_UNIT);
        assertNotNull(resolutionUnit);
        assertEquals(resolutionUnitValue, ((Number) resolutionUnit.getValue()).intValue());

        Entry xResolution = ifds.getEntryById(TAG_X_RESOLUTION);
        assertNotNull(xResolution);
        assertEquals(expectedResolutionValue, xResolution.getValue());

        Entry yResolution = ifds.getEntryById(TAG_Y_RESOLUTION);
        assertNotNull(yResolution);
        assertEquals(expectedResolutionValue, yResolution.getValue());
    }

    @Test
    public void testWriteWithCustomSoftwareStandard() throws IOException {
        String softwareString = "12M TIFF Test 1.0 (build $foo$)";

        RenderedImage image = getTestData(0);

        ImageWriter writer = createWriter();
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();

        try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) {
            writer.setOutput(stream);

            String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName;
            IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), null);

            IIOMetadataNode customMeta = new IIOMetadataNode(standardFormat);

            IIOMetadataNode dimension = new IIOMetadataNode("Text");
            customMeta.appendChild(dimension);

            IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry");
            dimension.appendChild(textEntry);
            textEntry.setAttribute("keyword", "Software");
            textEntry.setAttribute("value", softwareString);

            metadata.mergeTree(standardFormat, customMeta);

            writer.write(null, new IIOImage(image, null, metadata), null);
        }
        catch (IOException e) {
            e.printStackTrace();
            fail(e.getMessage());
        }

        assertTrue(buffer.size() > 0, "No image data written");

        Directory ifds = new TIFFReader().read(new ByteArrayImageInputStream(buffer.toByteArray()));
        Entry software = ifds.getEntryById(TIFF.TAG_SOFTWARE);
        assertNotNull(software);
        assertEquals(softwareString, software.getValueAsString());
    }

    @Test
    public void testWriteIncompatibleCompression() throws IOException {
        ImageWriter writer = createWriter();
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();

        try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {
            writer.setOutput(output);

            ImageWriteParam param = writer.getDefaultWriteParam();
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionType("CCITT T.6");

            // Use assertThrows to check for IOException
            assertThrows(IllegalArgumentException.class, () -> {
                writer.write(null, new IIOImage(new BufferedImage(8, 8, BufferedImage.TYPE_INT_RGB), null, null), param);
            });
        }
    }

    @Test
    public void testWriterCanWriteSequence() throws IOException {
        ImageWriter writer = createWriter();
        assertTrue(writer.canWriteSequence(), "Writer should support sequence writing");
    }

    @Test
    public void testWriteSequenceWithoutPrepare() throws IOException {
        ImageWriter writer = createWriter();
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();

        try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {
            writer.setOutput(output);
            assertThrows(IllegalStateException.class, () -> writer.writeToSequence(new IIOImage(new BufferedImage(10, 10, BufferedImage.TYPE_3BYTE_BGR), null, null), null));
        }
    }

    @Test
    public void testEndSequenceWithoutPrepare() throws IOException {
        ImageWriter writer = createWriter();
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();

        try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {
            writer.setOutput(output);
            assertThrows(IllegalStateException.class, () -> writer.endWriteSequence());
        }
    }

    private void assertWriteSequence(Class<? extends ImageOutputStream> iosClass, String... compression) throws IOException {
        BufferedImage image = new BufferedImage(13, 13, BufferedImage.TYPE_BYTE_GRAY);

        Graphics2D g2d = image.createGraphics();
        try {
            g2d.setColor(Color.WHITE);
            g2d.fillRect(image.getWidth() / 4, image.getHeight() / 4, image.getWidth() / 2, image.getHeight() / 2);
        }
        finally {
            g2d.dispose();
        }

        boolean isFileDirect = iosClass == FileImageOutputStream.class;
        Object destination = isFileDirect
                             ? File.createTempFile("temp-", ".tif")
                             : new ByteArrayOutputStream(1024);

        ImageWriter writer = createWriter();
        try (ImageOutputStream output = isFileDirect
                                        ? new FileImageOutputStream((File) destination)
                                        : new FileCacheImageOutputStream((OutputStream) destination, ImageIO.getCacheDirectory())) {
            writer.setOutput(output);

            ImageWriteParam params = writer.getDefaultWriteParam();
            params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);

            try {
                writer.prepareWriteSequence(null);

                for (String compressionType : compression) {
                    params.setCompressionType(compressionType);
                    writer.writeToSequence(new IIOImage(image, null, null), params);
                }

                writer.endWriteSequence();
            }
            catch (IOException e) {
                fail(e.getMessage());
            }
        }

        try (ImageInputStream input = ImageIO.createImageInputStream(isFileDirect
                                                                     ? destination
                                                                     : new ByteArrayInputStream(((ByteArrayOutputStream) destination).toByteArray()))) {
            ImageReader reader = ImageIO.getImageReaders(input).next();
            reader.setInput(input);

            assertEquals(compression.length, reader.getNumImages(true), "wrong image count");

            for (int i = 0; i < reader.getNumImages(true); i++) {
                assertImageEquals("image " + i + " differs", image, reader.read(i), 5); // Allow room for JPEG compression
            }
        }
    }

    @Test
    public void testWriteSequenceFileImageOutputStreamUncompressed() throws IOException {
        assertWriteSequence(FileImageOutputStream.class, "None", "None");
    }

    @Test
    public void testWriteSequenceFileImageOutputCompressed() throws IOException {
        assertWriteSequence(FileImageOutputStream.class, "LZW", "Deflate");
    }

    @Test
    public void testWriteSequenceFileImageOutputStreamUncompressedCompressed() throws IOException {
        assertWriteSequence(FileImageOutputStream.class, "None", "LZW", "None");
    }

    @Test
    public void testWriteSequenceFileImageOutputStreamCompressedUncompressed() throws IOException {
        assertWriteSequence(FileImageOutputStream.class, "Deflate", "None", "Deflate");
    }

    @Test
    public void testWriteSequenceFileCacheImageOutputStreamUncompressed() throws IOException {
        assertWriteSequence(FileCacheImageOutputStream.class, "None", "None");
    }

    @Test
    public void testWriteSequenceFileCacheImageOutputStreamCompressed() throws IOException {
        assertWriteSequence(FileCacheImageOutputStream.class, "Deflate", "LZW");
    }

    @Test
    public void testWriteSequenceFileCacheImageOutputStreamCompressedUncompressed() throws IOException {
        assertWriteSequence(FileCacheImageOutputStream.class, "LZW", "None", "LZW");
    }

    @Test
    public void testWriteSequenceFileCacheImageOutputStreamUncompressedCompressed() throws IOException {
        assertWriteSequence(FileCacheImageOutputStream.class, "None", "Deflate", "None");
    }

    @Test
    public void testWriteSequence() throws IOException {
        BufferedImage[] images = new BufferedImage[] {
                new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB),
                new BufferedImage(110, 100, BufferedImage.TYPE_3BYTE_BGR),
                new BufferedImage(120, 100, BufferedImage.TYPE_INT_RGB),
                new BufferedImage(140, 100, BufferedImage.TYPE_INT_ARGB),
                new BufferedImage(130, 100, BufferedImage.TYPE_BYTE_GRAY),
                new BufferedImage(150, 100, BufferedImage.TYPE_BYTE_BINARY),
                new BufferedImage(160, 100, BufferedImage.TYPE_BYTE_BINARY)
        };

        Color[] colors = new Color[] {Color.RED, Color.GREEN, Color.BLUE, Color.ORANGE, Color.PINK, Color.WHITE, Color.GRAY};

        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, image.getWidth(), image.getHeight());
            }
            finally {
                g2d.dispose();
            }
        }

        ImageWriter writer = createWriter();
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {
            writer.setOutput(output);

            ImageWriteParam params = writer.getDefaultWriteParam();
            params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);

            try {
                writer.prepareWriteSequence(null);

                params.setCompressionType("LZW");
                writer.writeToSequence(new IIOImage(images[0], null, null), params);

                params.setCompressionType("None");
                writer.writeToSequence(new IIOImage(images[1], null, null), params);

                params.setCompressionType("JPEG");
                writer.writeToSequence(new IIOImage(images[2], null, null), params);

                params.setCompressionType("PackBits");
                writer.writeToSequence(new IIOImage(images[3], null, null), params);

                params.setCompressionType("Deflate");
                writer.writeToSequence(new IIOImage(images[4], null, null), params);

                params.setCompressionType("CCITT T.4");
                writer.writeToSequence(new IIOImage(images[5], null, null), params);

                params.setCompressionType("CCITT T.6");
                writer.writeToSequence(new IIOImage(images[6], null, null), params);

                writer.endWriteSequence();
            }
            catch (IOException e) {
                fail(e.getMessage());
            }
        }

        try (ImageInputStream input = ImageIO.createImageInputStream(new ByteArrayInputStream(buffer.toByteArray()))) {
            ImageReader reader = ImageIO.getImageReaders(input).next();
            reader.setInput(input);

            assertEquals(images.length, reader.getNumImages(true), "wrong image count");

            for (int i = 0; i < reader.getNumImages(true); i++) {
                assertImageEquals("image " + i + " differs", images[i], reader.read(i), 5); // Allow room for JPEG compression
            }
        }
    }

    @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 = createWriter();
        IIOWriteProgressListener progress = mock(IIOWriteProgressListener.class, "progress");
        writer.addIIOWriteProgressListener(progress);

        try (ImageOutputStream output = new NullImageOutputStream()) {
            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 testWriteGrayNoProfile() throws IOException {
        ImageWriter writer = createWriter();

        FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(512);

        try (ImageOutputStream output = ImageIO.createImageOutputStream(bytes)) {
            writer.setOutput(output);
            writer.write(new BufferedImage(10, 10, BufferedImage.TYPE_BYTE_GRAY));
        }

        try (ImageInputStream input = ImageIO.createImageInputStream(bytes.createInputStream())) {
            ImageReader reader = ImageIO.getImageReaders(input).next();
            reader.setInput(input);

            TIFFImageMetadata metadata = (TIFFImageMetadata) reader.getImageMetadata(0);
            Directory ifd = metadata.getIFD();

            assertNull(ifd.getEntryById(TIFF.TAG_ICC_PROFILE), "Unexpected ICC profile for default gray");
        }
    }

    @Test
    public void testWriteParamJPEGQuality() throws IOException {
        ImageWriter writer = createWriter();

        try (ImageOutputStream output = new NullImageOutputStream()) {
            writer.setOutput(output);

            try {
                ImageWriteParam param = writer.getDefaultWriteParam();
                // Make sure that the JPEG delegation outputs the correct indexes
                param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                param.setCompressionType("JPEG");
                param.setCompressionQuality(.1f);

                writer.write(null, new IIOImage(new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB), null, null), param);

                // In a perfect world, we should verify that the parameter was passed to the JPEG delegate...
            }
            catch (IOException e) {
                fail(e.getMessage());
            }
        }
    }

    @Test
    public void testReadWriteRead1BitLZW() throws IOException {
        // Read original LZW compressed TIFF
        IIOImage original;

        try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource("/tiff/a33.tif"))) {
            ImageReader reader = ImageIO.getImageReaders(input).next();
            reader.setInput(input);

            original = reader.readAll(0, null);
            reader.dispose();
        }
        assumeTrue(original != null);

        // Write it back, using same compression (copied from metadata)
        FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768);

        try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {
            ImageWriter writer = createWriter();
            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++) {
                    try {
                        assertRGBEquals("", orig.getRGB(x, y), image.getRGB(x, y), 0);
                    }
                    catch (AssertionError err) {
                        fail(String.format("Pixel differ: @%d,%d %s", x, y, err.getMessage()));
                    }
                }
            }

            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(softwareFound, "Software metadata not found");
        }
    }

    @Test
    public void testReadWriteRead1BitDeflate() throws IOException {
        // Read original LZW compressed TIFF
        IIOImage original;

        try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource("/tiff/a33.tif"))) {
            ImageReader reader = ImageIO.getImageReaders(input).next();
            reader.setInput(input);

            original = reader.readAll(0, null);
            reader.dispose();
        }

        assumeTrue(original != null);

        // Write it back, using deflate compression
        FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768);

        try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {
            ImageWriter writer = createWriter();
            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(softwareFound, "Software metadata not found");
        }
    }

    @Test
    public void testReadWriteRead1BitNone() throws IOException {
        // Read original LZW compressed TIFF
        IIOImage original;

        try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource("/tiff/a33.tif"))) {
            ImageReader reader = ImageIO.getImageReaders(input).next();
            reader.setInput(input);

            original = reader.readAll(0, null);
            reader.dispose();
        }

        assumeTrue(original!= null);


        // Write it back, no compression
        FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768);

        try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {
            ImageWriter writer = createWriter();
            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(softwareFound, "Software metadata not found");
        }
    }

    @Test
    public void testReadWriteRead24BitLZW() throws IOException {
        // Read original LZW compressed TIFF
        IIOImage original;

        try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource("/tiff/quad-lzw.tif"))) {
            ImageReader reader = ImageIO.getImageReaders(input).next();
            reader.setInput(input);

            original = reader.readAll(0, null);
            reader.dispose();
        }

        assumeTrue(original != null);

        // Write it back, using same compression (copied from metadata)
        FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768);

        try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {
            ImageWriter writer = createWriter();
            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(softwareFound, "Software metadata not found");
        }
    }

    @Test
    public void testReadWriteRead24BitDeflate() throws IOException {
        // Read original LZW compressed TIFF
        IIOImage original;

        try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource("/tiff/quad-lzw.tif"))) {
            ImageReader reader = ImageIO.getImageReaders(input).next();
            reader.setInput(input);

            original = reader.readAll(0, null);
            reader.dispose();
        }

        assumeTrue(original != null);

        // Write it back, using same compression (copied from metadata)
        FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768);

        try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {
            ImageWriter writer = createWriter();
            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(softwareFound, "Software metadata not found");
        }
    }

    @Test
    public void testReadWriteRead24BitNone() throws IOException {
        // Read original LZW compressed TIFF
        IIOImage original;

        try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource("/tiff/quad-lzw.tif"))) {
            ImageReader reader = ImageIO.getImageReaders(input).next();
            reader.setInput(input);

            original = reader.readAll(0, null);
            reader.dispose();
        }

        assumeTrue(original != null);

        // Write it back, using same compression (copied from metadata)
        FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768);

        try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {
            ImageWriter writer = createWriter();
            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(softwareFound, "Software metadata not found");
        }
    }

    @Test
    public void testWriteCropped() throws IOException {
        List<URL> testData = Arrays.asList(
                getClassLoaderResource("/tiff/quad-lzw.tif"),
                getClassLoaderResource("/tiff/grayscale-alpha.tiff"),
                getClassLoaderResource("/tiff/ccitt/group3_1d.tif"),
                getClassLoaderResource("/tiff/depth/flower-palette-02.tif"),
                getClassLoaderResource("/tiff/depth/flower-palette-04.tif"),
                getClassLoaderResource("/tiff/depth/flower-minisblack-16.tif"),
                getClassLoaderResource("/tiff/depth/flower-minisblack-32.tif")
        );

        for (URL resource : testData) {
            // Read it
            BufferedImage original = ImageIO.read(resource);

            // Crop it
            BufferedImage subimage = original.getSubimage(original.getWidth() / 4, original.getHeight() / 4, original.getWidth() / 2, original.getHeight() / 2);

            // Store cropped
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            try (ImageOutputStream output = ImageIO.createImageOutputStream(bytes)) {
                ImageWriter imageWriter = createWriter();
                imageWriter.setOutput(output);
                imageWriter.write(subimage);
            }

            // Re-read cropped
            BufferedImage cropped = ImageIO.read(new ByteArrayImageInputStream(bytes.toByteArray()));

            // Compare
            assertImageEquals(String.format("Cropped output differs: %s", resource.getFile()), subimage, cropped, 0);
        }
    }

    private void assertImageEquals(final String message, final BufferedImage expected, final BufferedImage actual, final int tolerance) {
        assertNotNull(expected, message);
        assertNotNull(actual, message);
        assertEquals(expected.getWidth(), actual.getWidth(), message + ", widths differ");
        assertEquals(expected.getHeight(), actual.getHeight(), message + ", heights differ");

        for (int y = 0; y < expected.getHeight(); y++) {
            for (int x = 0; x < expected.getWidth(); x++) {
                assertRGBEquals(String.format("%s, ARGB differs at (%s,%s)", message, x, y), expected.getRGB(x, y), actual.getRGB(x, y), tolerance);
            }
        }
    }

    @Test
    public void testWriteStreamMetadataDefaultMM() throws IOException {
        ImageWriter writer = createWriter();

        ByteArrayOutputStream output = new ByteArrayOutputStream();
        try (ImageOutputStream stream = ImageIO.createImageOutputStream(output)) {
            stream.setByteOrder(ByteOrder.BIG_ENDIAN); // Should pass through
            writer.setOutput(stream);

            writer.write(null, new IIOImage(getTestData(0), null, null), null);
        }

        byte[] bytes = output.toByteArray();
        assertArrayEquals(new byte[] {'M', 'M', 0, 42}, Arrays.copyOf(bytes, 4));
    }

    @Test
    public void testWriteStreamMetadataDefaultII() throws IOException {
        ImageWriter writer = createWriter();

        ByteArrayOutputStream output = new ByteArrayOutputStream();
        try (ImageOutputStream stream = ImageIO.createImageOutputStream(output)) {
            stream.setByteOrder(ByteOrder.LITTLE_ENDIAN); // Should pass through
            writer.setOutput(stream);

            writer.write(null, new IIOImage(getTestData(0), null, null), null);
        }

        byte[] bytes = output.toByteArray();
        assertArrayEquals(new byte[] {'I', 'I', 42, 0}, Arrays.copyOf(bytes, 4));
    }

    @Test
    public void testWriteStreamMetadataMM() throws IOException {
        ImageWriter writer = createWriter();

        ByteArrayOutputStream output = new ByteArrayOutputStream();
        try (ImageOutputStream stream = ImageIO.createImageOutputStream(output)) {
            stream.setByteOrder(ByteOrder.LITTLE_ENDIAN); // Should be overridden by stream metadata
            writer.setOutput(stream);

            writer.write(new TIFFStreamMetadata(ByteOrder.BIG_ENDIAN), new IIOImage(getTestData(0), null, null), null);
        }

        byte[] bytes = output.toByteArray();
        assertArrayEquals(new byte[] {'M', 'M', 0, 42}, Arrays.copyOf(bytes, 4));
    }

    @Test
    public void testWriteStreamMetadataII() throws IOException {
        ImageWriter writer = createWriter();

        ByteArrayOutputStream output = new ByteArrayOutputStream();
        try (ImageOutputStream stream = ImageIO.createImageOutputStream(output)) {
            stream.setByteOrder(ByteOrder.BIG_ENDIAN); // Should be overridden by stream metadata
            writer.setOutput(stream);

            writer.write(new TIFFStreamMetadata(ByteOrder.LITTLE_ENDIAN), new IIOImage(getTestData(0), null, null), null);
        }

        byte[] bytes = output.toByteArray();
        assertArrayEquals(new byte[] {'I', 'I', 42, 0}, Arrays.copyOf(bytes, 4));
    }

    @Test
    public void testMergeTreeARGB() throws IOException {
        ImageWriter writer = createWriter();
        ImageWriteParam writeParam = writer.getDefaultWriteParam();
        writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        writeParam.setCompressionType("LZW");

        IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR), writeParam);

        IIOMetadataNode tiffTree = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());
        metadata.setFromTree(metadata.getNativeMetadataFormatName(), tiffTree);
    }

    @Test
    public void testMergeTreeGray() throws IOException {
        ImageWriter writer = createWriter();
        ImageWriteParam writeParam = writer.getDefaultWriteParam();
        writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        writeParam.setCompressionType("LZW");

        IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY), writeParam);

        IIOMetadataNode tiffTree = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());
        metadata.setFromTree(metadata.getNativeMetadataFormatName(), tiffTree);
    }

    @Test
    public void testMergeTreeBW() throws IOException {
        ImageWriter writer = createWriter();
        ImageWriteParam writeParam = writer.getDefaultWriteParam();
        writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        writeParam.setCompressionType("CCITT T.6");

        IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_BINARY), writeParam);

        IIOMetadataNode tiffTree = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());
        metadata.setFromTree(metadata.getNativeMetadataFormatName(), tiffTree);
    }

    @Test
    public void testRewrite() throws IOException {
        ImageWriter writer = createWriter();
        ImageReader reader = ImageIO.getImageReader(writer);

        List<URL> testData = Arrays.asList(
                getClassLoaderResource("/tiff/pixtiff/17-tiff-binary-ccitt-group3.tif"),
                getClassLoaderResource("/tiff/pixtiff/36-tiff-8-bit-gray-jpeg.tif"),
                getClassLoaderResource("/tiff/pixtiff/51-tiff-24-bit-color-jpeg.tif"),
                getClassLoaderResource("/tiff/pixtiff/58-plexustiff-binary-ccitt-group4.tif"),
                getClassLoaderResource("/tiff/balloons.tif"),
                getClassLoaderResource("/tiff/ColorCheckerCalculator.tif"),
                getClassLoaderResource("/tiff/quad-jpeg.tif"),
                getClassLoaderResource("/tiff/quad-lzw.tif"),
                getClassLoaderResource("/tiff/bali.tif"),
                getClassLoaderResource("/tiff/lzw-colormap-iiobe.tif"),
                // TODO: FixMe for ColorMap + ExtraSamples (custom ColorModel)
//                getClassLoaderResource("/tiff/colormap-with-extrasamples.tif"),

                getClassLoaderResource("/tiff/depth/flower-minisblack-02.tif"),
                getClassLoaderResource("/tiff/depth/flower-minisblack-04.tif"),
                getClassLoaderResource("/tiff/depth/flower-minisblack-06.tif"),
                getClassLoaderResource("/tiff/depth/flower-minisblack-08.tif"),
                getClassLoaderResource("/tiff/depth/flower-minisblack-10.tif"),
                getClassLoaderResource("/tiff/depth/flower-minisblack-12.tif"),
                getClassLoaderResource("/tiff/depth/flower-minisblack-14.tif"),
                getClassLoaderResource("/tiff/depth/flower-minisblack-16.tif"),
                getClassLoaderResource("/tiff/depth/flower-minisblack-24.tif"),
                getClassLoaderResource("/tiff/depth/flower-minisblack-32.tif"),

                getClassLoaderResource("/tiff/depth/flower-palette-02.tif"),
                getClassLoaderResource("/tiff/depth/flower-palette-04.tif"),
                getClassLoaderResource("/tiff/depth/flower-palette-08.tif"),
                getClassLoaderResource("/tiff/depth/flower-palette-16.tif"),

                getClassLoaderResource("/tiff/depth/flower-rgb-contig-08.tif"),
                // TODO: FixMe for RGB > 8 bits / sample
//                getClassLoaderResource("/tiff/depth/flower-rgb-contig-10.tif"),
//                getClassLoaderResource("/tiff/depth/flower-rgb-contig-12.tif"),
//                getClassLoaderResource("/tiff/depth/flower-rgb-contig-14.tif"),
//                getClassLoaderResource("/tiff/depth/flower-rgb-contig-16.tif"),
//                getClassLoaderResource("/tiff/depth/flower-rgb-contig-24.tif"),
//                getClassLoaderResource("/tiff/depth/flower-rgb-contig-32.tif"),

                getClassLoaderResource("/tiff/depth/flower-rgb-planar-08.tif"),
                // TODO: FixMe for planar RGB > 8 bits / sample
//                getClassLoaderResource("/tiff/depth/flower-rgb-planar-10.tif"),
//                getClassLoaderResource("/tiff/depth/flower-rgb-planar-12.tif"),
//                getClassLoaderResource("/tiff/depth/flower-rgb-planar-14.tif"),
//                getClassLoaderResource("/tiff/depth/flower-rgb-planar-16.tif"),
//                getClassLoaderResource("/tiff/depth/flower-rgb-planar-24.tif"),

                getClassLoaderResource("/tiff/scan-mono-iccgray.tif"),
                getClassLoaderResource("/tiff/old-style-jpeg-inconsistent-metadata.tif"),
                getClassLoaderResource("/tiff/ccitt/group3_1d.tif"),
                getClassLoaderResource("/tiff/ccitt/group3_2d.tif"),
                getClassLoaderResource("/tiff/ccitt/group3_1d_fill.tif"),
                getClassLoaderResource("/tiff/ccitt/group3_2d_fill.tif"),
                getClassLoaderResource("/tiff/ccitt/group4.tif")
        );

        for (URL url : testData) {
            ByteArrayOutputStream output = new ByteArrayOutputStream();

            try (ImageInputStream input = ImageIO.createImageInputStream(url);
                 ImageOutputStream stream = ImageIO.createImageOutputStream(output)) {
                reader.setInput(input);
                writer.setOutput(stream);

                List<ImageInfo> infos = new ArrayList<>(20);

                writer.prepareWriteSequence(null);

                for (int i = 0; i < reader.getNumImages(true); i++) {
                    IIOImage image = reader.readAll(i, null);

                    // If compression is Old JPEG, rewrite as JPEG
                    // Normally, use the getAsTree method, but we don't care here if we are tied to our impl
                    TIFFImageMetadata metadata = (TIFFImageMetadata) image.getMetadata();
                    Directory ifd = metadata.getIFD();
                    Entry compressionEntry = ifd.getEntryById(TIFF.TAG_COMPRESSION);

                    int compression = compressionEntry != null ? ((Number) compressionEntry.getValue()).intValue() : TIFFBaseline.COMPRESSION_NONE;

                    infos.add(new ImageInfo(image.getRenderedImage().getWidth(), image.getRenderedImage().getHeight(), compression));

                    ImageWriteParam param = writer.getDefaultWriteParam();

                    if (compression == TIFFExtension.COMPRESSION_OLD_JPEG) {
                        param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); // Override the copy from metadata
                        param.setCompressionType("JPEG");
                    }

                    writer.writeToSequence(image, param);
                }

                writer.endWriteSequence();

//                File tempFile = File.createTempFile("foo-", ".tif");
//                System.err.println("open " + tempFile.getAbsolutePath());
//                FileUtil.write(tempFile, output.toByteArray());

                try (ImageInputStream inputAfter = new ByteArrayImageInputStream(output.toByteArray())) {
                    reader.setInput(inputAfter);

                    int numImages = reader.getNumImages(true);

                    assertEquals(infos.size(), numImages, "Number of pages differs from original");

                    for (int i = 0; i < numImages; i++) {
                        IIOImage after = reader.readAll(i, null);
                        ImageInfo info = infos.get(i);

                        TIFFImageMetadata afterMetadata = (TIFFImageMetadata) after.getMetadata();
                        Directory afterIfd = afterMetadata.getIFD();
                        Entry afterCompressionEntry = afterIfd.getEntryById(TIFF.TAG_COMPRESSION);

                        if (info.compression == TIFFExtension.COMPRESSION_OLD_JPEG) {
                            // Should rewrite this from old-style to new style
                            assertEquals(TIFFExtension.COMPRESSION_JPEG, ((Number) afterCompressionEntry.getValue()).intValue(), "Old JPEG compression not rewritten as JPEG");
                        }
                        else {
                            assertEquals(info.compression, ((Number) afterCompressionEntry.getValue()).intValue(), "Compression differs from original");
                        }

                        assertEquals(info.width, after.getRenderedImage().getWidth(), "Image width differs from original");
                        assertEquals(info.height, after.getRenderedImage().getHeight(), "Image height differs from original");
                    }
                }
            }
        }
    }

    @Test
    public void testWriteBinaryWhiteIsZero() throws IOException {
        IndexColorModel whiteIsZero = new IndexColorModel(1, 2, new int[] {-1, 0}, 0, false, -1, DataBuffer.TYPE_BYTE);
        BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_BYTE_BINARY, whiteIsZero);

        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        try (ImageOutputStream output = ImageIO.createImageOutputStream(bytes)) {
            ImageWriter imageWriter = createWriter();
            imageWriter.setOutput(output);
            imageWriter.write(image);
        }

        Directory directory = new TIFFReader().read(new ByteArrayImageInputStream(bytes.toByteArray()));

        assertNotNull(directory.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION));
        assertEquals(TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO, directory.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION).getValue());
    }

    @Test
    public void testWriteBinaryBlackIsZero() throws IOException {
        IndexColorModel blackIsZero = new IndexColorModel(1, 2, new int[] {0, -1}, 0, false, -1, DataBuffer.TYPE_BYTE);
        BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_BYTE_BINARY, blackIsZero);

        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        try (ImageOutputStream output = ImageIO.createImageOutputStream(bytes)) {
            ImageWriter imageWriter = createWriter();
            imageWriter.setOutput(output);
            imageWriter.write(image);
        }

        Directory directory = new TIFFReader().read(new ByteArrayImageInputStream(bytes.toByteArray()));

        assertNotNull(directory.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION));
        assertEquals(TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO, directory.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION).getValue());
    }

    @Test
    public void testWriteBinaryPalette() throws IOException {
        IndexColorModel redAndBluePalette = new IndexColorModel(1, 2, new int[] {0xFF00FF00, 0xFF0000FF}, 0, false, -1, DataBuffer.TYPE_BYTE);
        BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_BYTE_BINARY, redAndBluePalette);

        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        try (ImageOutputStream output = ImageIO.createImageOutputStream(bytes)) {
            ImageWriter imageWriter = createWriter();
            imageWriter.setOutput(output);
            imageWriter.write(image);
        }

        Directory directory = new TIFFReader().read(new ByteArrayImageInputStream(bytes.toByteArray()));

        assertNotNull(directory.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION));
        assertEquals(TIFFBaseline.PHOTOMETRIC_PALETTE, directory.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION).getValue());
    }

    @Test
    public void testWriteJPEGCompressedShouldNotPassMetadata() throws IOException {
        BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_3BYTE_BGR);

        try (ImageOutputStream output = new NullImageOutputStream()) {
            ImageWriter imageWriter = createWriter();
            imageWriter.setOutput(output);

            ImageWriteParam param = imageWriter.getDefaultWriteParam();
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionType("JPEG");

            // From #815
            // Prior to fix, this would throw IIOException: Metadata components != number of destination bands
            // (empty metadata defaults to 1 channel gray, while image data is 3 channel BGR)
            imageWriter.write(null, new IIOImage(image, null, new TIFFImageMetadata()), param);
        }
    }

    @Test
    public void testShortOverflowHuge() throws IOException {
        int width = 34769;
        int height = 33769;

        // Create a huge image without actually allocating memory...
        DataBuffer buffer = new NullDataBuffer(DataBuffer.TYPE_USHORT, width * height);
        WritableRaster raster = Raster.createWritableRaster(new ComponentSampleModel(buffer.getDataType(), width, height, 1, width, new int[] {0}), buffer, null);
        ColorModel colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false, Transparency.OPAQUE, buffer.getDataType());
        BufferedImage image = new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), null);

        // Write image without any exception
        TIFFImageWriter writer = createWriter();
        try (ImageOutputStream stream = new NullImageOutputStream()) {
            writer.setOutput(stream);
            writer.write(image);
        }
        finally {
            writer.dispose();
        }
    }

    @Test
    public void testIntOverflowHuge() throws IOException {
        int width = 34769;
        int height = 33769;

        // Create a huge image without actually allocating memory...
        DataBuffer buffer = new NullDataBuffer(DataBuffer.TYPE_INT, width * height);
        WritableRaster raster = Raster.createWritableRaster(new ComponentSampleModel(buffer.getDataType(), width, height, 1, width, new int[] {0}), buffer, null);
        ColorModel colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false, Transparency.OPAQUE, buffer.getDataType());
        BufferedImage image = new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), null);

        // Write image without any exception
        TIFFImageWriter writer = createWriter();
        try (ImageOutputStream stream = new NullImageOutputStream()) {
            writer.setOutput(stream);
            writer.write(image);
        }
        finally {
            writer.dispose();
        }
    }

    private static class ImageInfo {
        final int width;
        final int height;

        final int compression;

        private ImageInfo(int width, int height, int compression) {
            this.width = width;
            this.height = height;
            this.compression = compression;
        }
    }

    // Special purpose output stream that acts as a sink
    private static final class NullImageOutputStream extends ImageOutputStreamImpl {
        @Override
        public void write(int b) {
            streamPos++;
        }

        @Override
        public void write(byte[] b, int off, int len) {
            streamPos += len;
        }

        @Override
        public int read() {
            streamPos++;
            return 0;
        }

        @Override
        public int read(byte[] b, int off, int len) {
            streamPos += len;
            return 0;
        }

        @Override
        public long length() {
            return streamPos;
        }
    }

    // Special purpose data buffer that does not require memory, to allow very large images
    private static final class NullDataBuffer extends DataBuffer {
        public NullDataBuffer(int type, int size) {
            super(type, size);
        }

        @Override
        public int getElem(int bank, int i) {
            return 0;
        }

        @Override
        public void setElem(int bank, int i, int val) {
        }
    }
}