CCITTFaxDecoderStreamTest.java

/*
 * Copyright (c) 2013, 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 java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.*;
import java.util.Arrays;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;


/**
 * CCITTFaxDecoderStreamTest
 *
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @author last modified by $Author: haraldk$
 * @version $Id: CCITTFaxDecoderStreamTest.java,v 1.0 09.03.13 14:44 haraldk
 *          Exp$
 */
public class CCITTFaxDecoderStreamTest {

    // group3_1d.tif: EOL|3W|1B|2W|EOL|3W|1B|2W|EOL|3W|1B|2W|EOL|2W|2B|2W|5*F
    static final byte[] DATA_G3_1D = { 0x00, 0x18, 0x4E, 0x00, 0x30, (byte) 0x9C, 0x00, 0x61, 0x38, 0x00, (byte) 0xBE,
            (byte) 0xE0 };

    // group3_1d_fill.tif
    static final byte[] DATA_G3_1D_FILL = { 0x00, 0x01, (byte) 0x84, (byte) 0xE0, 0x01, (byte) 0x84, (byte) 0xE0, 0x01,
            (byte) 0x84, (byte) 0xE0, 0x1, 0x7D, (byte) 0xC0 };

    // group3_1d_premature_eol.tif
    // 0011 0101 | 0000 0010 1011 | 0110 0111 | 0010 1001 | 0100
    // 0W | 59B | 640W | 40W
    static final byte[] DATA_G3_1D_PREMATURE_EOL = {
            0x35, 0x02, (byte) 0xB6, 0x72, (byte) 0x94, (byte) 0xE8, 0x74, 0x38, 0x1C, (byte) 0x81, 0x64, (byte) 0xD4,
            0x0A, (byte) 0xD9, (byte) 0xD2, 0x27, 0x50, (byte) 0x90, (byte) 0xA6, (byte) 0x87, 0x43, (byte) 0xE3
    };

    // group3_2d.tif: EOL|k=1|3W|1B|2W|EOL|k=0|V|V|V|EOL|k=1|3W|1B|2W|EOL|k=0|V-1|V|V|6*F
    static final byte[] DATA_G3_2D = {0x00, 0x1C, 0x27, 0x00, 0x17, 0x00, 0x1C, 0x27, 0x00, 0x12, (byte) 0xC0};

    // group3_2d_fill.tif
    static final byte[] DATA_G3_2D_FILL = {0x00, 0x01, (byte) 0xC2, 0x70, 0x01, 0x70, 0x01, (byte) 0xC2,
                                           0x70, 0x01, 0x2C};

    static final byte[] DATA_G3_2D_lsb2msb = {0x00, 0x38, (byte) 0xE4, 0x00, (byte) 0xE8, 0x00, 0x38, (byte) 0xE4,
                                              0x00, 0x48, 0x03};

    static final byte[] DATA_G3_LONG = {0x00, 0x68, 0x0A, (byte) 0xC9, 0x3A, 0x3A, 0x00, 0x68,
                                        (byte) 0x8A, (byte) 0xD8, 0x3A, 0x35, 0x00, 0x68, 0x0A, 0x06,
                                        (byte) 0xDD, 0x3A, 0x19, 0x00, 0x68, (byte) 0x8A, (byte) 0x9E, 0x75,
                                        0x08, 0x00, 0x68};

    // group4.tif:
    // Line 1: V-3, V-2, V0
    // Line 2: V0 V0 V0
    // Line 3: V0 V0 V0
    // Line 4: V-1, V0, V0 EOL EOL
    static final byte[] DATA_G4 = { 0x04, 0x17, (byte) 0xF5, (byte) 0x80, 0x08, 0x00, (byte) 0x80 };

    static final byte[] DATA_G4_ALIGNED = {
            0x04, 0x14, // 00000100 000101(00)
            (byte) 0xE0,    // 111 (00000)
            (byte) 0xE0,   // 111 (00000)
            0x58 // 01011 (000)
    };

    // TODO: Better tests (full A4 width scan lines?)

    // From http://www.mikekohn.net/file_formats/tiff.php
    static final byte[] DATA_TYPE_2 = { (byte) 0x84, (byte) 0xe0, // 10000100
                                                                  // 11100000
            (byte) 0x84, (byte) 0xe0, // 10000100 11100000
            (byte) 0x84, (byte) 0xe0, // 10000100 11100000
            (byte) 0x7d, (byte) 0xc0, // 01111101 11000000
    };

    static final byte[] DATA_TYPE_3 = { 0x00, 0x01, (byte) 0xc2, 0x70, // 00000000
                                                                       // 00000001
                                                                       // 11000010
                                                                       // 01110000
            0x00, 0x01, 0x78, // 00000000 00000001 01111000
            0x00, 0x01, 0x78, // 00000000 00000001 01110000
            0x00, 0x01, 0x56, // 00000000 00000001 01010110
            // 0x01, // 00000001

    };

    // 001 00110101 10 000010 1 1 1 1 1 1 1 1 1 1 010 11 (000000 padding)
    static final byte[] DATA_TYPE_4 = { 0x26, // 001 00110
            (byte) 0xb0, // 101 10 000
            0x5f, // 010 1 1 1 1 1
            (byte) 0xfa, // 1 1 1 1 1 010
            (byte) 0xc0 // 11 (000000 padding)
    };

    // 3W|1B|2W| 3W|1B|2W| 3W|1B|2W| 2W|2B|2W
    // 1000|010|0111| 1000|010|0111| 1000|010|0111| 0111|11|0111
    static final byte[] DATA_RLE_UNALIGNED = {
            (byte)0x84, (byte)0xF0, (byte)0x9E,0x13, (byte)0xBE,(byte) 0xE0
    };

    // Image should be (6 x 4):
    // 1 1 1 0 1 1 x x
    // 1 1 1 0 1 1 x x
    // 1 1 1 0 1 1 x x
    // 1 1 0 0 1 1 x x
    final BufferedImage image = new BufferedImage(6, 4, BufferedImage.TYPE_BYTE_BINARY);

    @BeforeEach
    public void init() {

        for (int y = 0; y < 4; y++) {
            for (int x = 0; x < 6; x++) {
                image.setRGB(x, y, x != 3 ? 0xff000000 : 0xffffffff);
            }
        }

        image.setRGB(2, 3, 0xffffffff);
    }

    @Test
    public void testDecodeType2() throws IOException {
        InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_2), 6,
                TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 0L);

        byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
        byte[] bytes = new byte[imageData.length];

        DataInputStream dataInputStream = new DataInputStream(stream);
        dataInputStream.readFully(bytes);
        assertArrayEquals(imageData, bytes);

        assertEquals(-1, dataInputStream.read());
        assertEquals(-1, dataInputStream.read(new byte[1]));
    }

    @Test
    public void testDecodeType3_1D() throws IOException {
        InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_G3_1D), 6,
                TIFFExtension.COMPRESSION_CCITT_T4, 0L);

        byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
        byte[] bytes = new byte[imageData.length];

        DataInputStream dataInputStream = new DataInputStream(stream);
        dataInputStream.readFully(bytes);
        assertArrayEquals(imageData, bytes);

        assertEquals(-1, dataInputStream.read());
        assertEquals(-1, dataInputStream.read(new byte[1]));
    }

    @Test
    public void testDecodeType3_1D_FILL() throws IOException {
        InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_G3_1D_FILL), 6,
                TIFFExtension.COMPRESSION_CCITT_T4, TIFFExtension.GROUP3OPT_FILLBITS);

        byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
        byte[] bytes = new byte[imageData.length];

        DataInputStream dataInputStream = new DataInputStream(stream);
        dataInputStream.readFully(bytes);
        assertArrayEquals(imageData, bytes);

        assertEquals(-1, dataInputStream.read());
        assertEquals(-1, dataInputStream.read(new byte[1]));
    }

    @Test
    public void testFindCompressionType() throws IOException {
        // RLE
        assertEquals(TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, CCITTFaxDecoderStream.findCompressionType(TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, new ByteArrayInputStream(DATA_RLE_UNALIGNED)));

        // Group 3/CCITT_T4
        assertEquals(TIFFExtension.COMPRESSION_CCITT_T4, CCITTFaxDecoderStream.findCompressionType(TIFFExtension.COMPRESSION_CCITT_T4, new ByteArrayInputStream(DATA_G3_1D)));
        assertEquals(TIFFExtension.COMPRESSION_CCITT_T4, CCITTFaxDecoderStream.findCompressionType(TIFFExtension.COMPRESSION_CCITT_T4, new ByteArrayInputStream(DATA_G3_1D_FILL)));
        assertEquals(TIFFExtension.COMPRESSION_CCITT_T4, CCITTFaxDecoderStream.findCompressionType(TIFFExtension.COMPRESSION_CCITT_T4, new ByteArrayInputStream(DATA_G3_2D)));
        assertEquals(TIFFExtension.COMPRESSION_CCITT_T4, CCITTFaxDecoderStream.findCompressionType(TIFFExtension.COMPRESSION_CCITT_T4, new ByteArrayInputStream(DATA_G3_2D_FILL)));
        assertEquals(TIFFExtension.COMPRESSION_CCITT_T4, CCITTFaxDecoderStream.findCompressionType(TIFFExtension.COMPRESSION_CCITT_T4, new ReverseInputStream(new ByteArrayInputStream(DATA_G3_2D_lsb2msb))));
        assertEquals(TIFFExtension.COMPRESSION_CCITT_T4, CCITTFaxDecoderStream.findCompressionType(TIFFExtension.COMPRESSION_CCITT_T4, new ReverseInputStream(new ByteArrayInputStream(DATA_G3_LONG))));

        // Group 4/CCITT_T6
        assertEquals(TIFFExtension.COMPRESSION_CCITT_T6, CCITTFaxDecoderStream.findCompressionType(TIFFExtension.COMPRESSION_CCITT_T6, new ByteArrayInputStream(DATA_G4)));
        assertEquals(TIFFExtension.COMPRESSION_CCITT_T6, CCITTFaxDecoderStream.findCompressionType(TIFFExtension.COMPRESSION_CCITT_T6, new ByteArrayInputStream(DATA_G4_ALIGNED)));

        // From sample file encoded with RLE, but with CCITT_T4 compression tag
        assertEquals(TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, CCITTFaxDecoderStream.findCompressionType(TIFFExtension.COMPRESSION_CCITT_T4, new ByteArrayInputStream(DATA_G3_1D_PREMATURE_EOL)));
        assertEquals(TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, CCITTFaxDecoderStream.findCompressionType(TIFFExtension.COMPRESSION_CCITT_T4, new ByteArrayInputStream(DATA_RLE_UNALIGNED)));
    }

    @Test
    public void testDecodeType3_2D() throws IOException {
        InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_G3_2D), 6,
                TIFFExtension.COMPRESSION_CCITT_T4, TIFFExtension.GROUP3OPT_2DENCODING);

        byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
        byte[] bytes = new byte[imageData.length];
        new DataInputStream(stream).readFully(bytes);
        assertArrayEquals(imageData, bytes);
    }

    @Test
    public void testDecodeType3_2D_FILL() throws IOException {
        InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_G3_2D_FILL), 6,
                TIFFExtension.COMPRESSION_CCITT_T4,
                TIFFExtension.GROUP3OPT_2DENCODING | TIFFExtension.GROUP3OPT_FILLBITS);

        byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
        byte[] bytes = new byte[imageData.length];
        new DataInputStream(stream).readFully(bytes);
        assertArrayEquals(imageData, bytes);
    }

    @Test
    public void testDecodeType3_2D_REVERSED() throws IOException {
        InputStream stream = new CCITTFaxDecoderStream(new ReverseInputStream(new ByteArrayInputStream(DATA_G3_2D_lsb2msb)), 6,
                TIFFExtension.COMPRESSION_CCITT_T4, TIFFExtension.GROUP3OPT_2DENCODING);

        byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
        byte[] bytes = new byte[imageData.length];
        new DataInputStream(stream).readFully(bytes);
        assertArrayEquals(imageData, bytes);
    }

    @Test
    public void testDecodeType4() throws IOException {
        InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_G4), 6,
                TIFFExtension.COMPRESSION_CCITT_T6, 0L);

        byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
        byte[] bytes = new byte[imageData.length];
        new DataInputStream(stream).readFully(bytes);
        assertArrayEquals(imageData, bytes);
    }

    @Test
    public void testDecodeMissingRows() throws IOException {
        // See https://github.com/haraldk/TwelveMonkeys/pull/225 and https://github.com/haraldk/TwelveMonkeys/issues/232
        InputStream inputStream = getResourceAsStream("/tiff/ccitt_tolessrows.tif");

        // Skip until StripOffsets: 8
        for (int i = 0; i < 8; i++) {
            inputStream.read();
        }

        // Read until StripByteCounts: 7
        byte[] data = new byte[7];
        new DataInputStream(inputStream).readFully(data);

        InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(data),
                6, 6, TIFFExtension.COMPRESSION_CCITT_T6, 0L, false);

        byte[] bytes = new byte[6]; // 6 x 6 pixel, 1 bpp => 6 bytes
        new DataInputStream(stream).readFully(bytes);

        // Pad image data with 0s
        byte[] imageData = Arrays.copyOf(((DataBufferByte) image.getData().getDataBuffer()).getData(), 6);
        assertArrayEquals(imageData, bytes);

         assertEquals(-1, stream.read(), "Should contain no more data");
         assertEquals(-1, stream.read(new byte[1]), "Should contain no more data");
    }

    @Test
    public void testMoreChangesThanColumns() throws IOException {
        // Produces an CCITT Stream with 9 changes on 8 columns.
        byte[] data = new byte[] {(byte) 0b10101010};
        ByteArrayOutputStream imageOutput = new ByteArrayOutputStream();
        OutputStream outputSteam = new CCITTFaxEncoderStream(imageOutput,
                8, 1, TIFFExtension.COMPRESSION_CCITT_T6, 1, 0L);
        outputSteam.write(data);
        outputSteam.close();

        byte[] encoded = imageOutput.toByteArray();
        InputStream inputStream = new CCITTFaxDecoderStream(new ByteArrayInputStream(encoded), 8,
                TIFFExtension.COMPRESSION_CCITT_T6, 0L);
        byte decoded = (byte) inputStream.read();
        assertEquals(data[0], decoded);
    }

    @Test
    public void testMoreChangesThanColumnsFile() throws IOException {
        // See https://github.com/haraldk/TwelveMonkeys/issues/328
        // 26 changes on 24 columns: H0w1b, H1w1b, ..., H1w0b
        InputStream stream = getResourceAsStream("/tiff/ccitt-too-many-changes.tif");

        // Skip bytes before StripOffsets: 86
        for (int i = 0; i < 86; i++) {
            stream.read();
        }

        InputStream inputStream = new CCITTFaxDecoderStream(stream,
                24, TIFFExtension.COMPRESSION_CCITT_T6, 0L);
        byte decoded = (byte) inputStream.read();
        assertEquals((byte) 0b10101010, decoded);
    }

    @Test
    public void testDecodeType4ByteAligned() throws IOException {
        CCITTFaxDecoderStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_G4_ALIGNED), 6,
                TIFFExtension.COMPRESSION_CCITT_T6, 0L, true);

        byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
        byte[] bytes = new byte[imageData.length];
        new DataInputStream(stream).readFully(bytes);
        assertArrayEquals(imageData, bytes);
    }

    @Test
    public void testDecodeType2NotByteAligned() throws IOException {
        CCITTFaxDecoderStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_RLE_UNALIGNED), 6,
                TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 0L, false);

        byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
        byte[] bytes = new byte[imageData.length];
        new DataInputStream(stream).readFully(bytes);
        assertArrayEquals(imageData, bytes);
    }

    @Test
    public void testG3AOE() throws IOException {
        InputStream inputStream = getResourceAsStream("/tiff/ccitt/g3aoe.tif");

        // Skip until StripOffsets: 8
        for (int i = 0; i < 8; i++) {
            inputStream.read();
        }

        // Read until StripByteCounts: 20050
        byte[] data = new byte[20050];
        new DataInputStream(inputStream).readFully(data);

        InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(data),
                1728, TIFFExtension.COMPRESSION_CCITT_T4, TIFFExtension.GROUP3OPT_FILLBITS);

        byte[] bytes = new byte[216 * 1168]; // 1728 x 1168 pixel, 1 bpp => 216 bytes * 1168
        new DataInputStream(stream).readFully(bytes);
    }

    @SuppressWarnings("StatementWithEmptyBody")
    @Test
    public void testAIOBEInCorruptStreamShouldThrowIOException() throws IOException {
        // From #645
        assertThrows(IOException.class, () -> {
            try (InputStream ccittFaxDecoderStream = new CCITTFaxDecoderStream(getResourceAsStream("/ccitt/645.ccitt"), 7, 4, 0, false)) {
                while (ccittFaxDecoderStream.read() != -1) ; // Just read until the end
            }
        });
    }

    @Test
    public void testFindCompressionTypeForMissingStartEOL() throws IOException {
        // Type 4, missing leading EOL code
        // starts with 1728px long white lines
        byte[] data = new byte[]{
                0x4d, (byte) 0x9a, (byte) 0x80, 0x01, 0x4d, (byte) 0x9a, (byte) 0x80, 0x01, 0x4d, (byte) 0x9a, (byte) 0x80, 0x01, 0x4d, (byte) 0x9a, (byte) 0x80, 0x01,
                (byte) 0x91, 0x3c, 0x17, 0x6d, 0x02, (byte) 0xf2, (byte) 0xb0, 0x20, (byte) 0x01, (byte) 0xda, (byte) 0xa8, (byte) 0xb3, 0x17, 0x4e, 0x62, (byte) 0xcd, (byte) 0xa7
        };
        try (ByteArrayInputStream is = new ByteArrayInputStream(data)) {
            int detectedType = CCITTFaxDecoderStream.findCompressionType(TIFFExtension.COMPRESSION_CCITT_T4, is);
            assertEquals(TIFFExtension.COMPRESSION_CCITT_T4, detectedType);
        }
    }

    private InputStream getResourceAsStream(String name) {
        return getClass().getResourceAsStream(name);
    }
}