From bad9aebdeeea9a2fdd74586cf4c92ea037da0e3f Mon Sep 17 00:00:00 2001 From: Schmidor Date: Sun, 5 Jul 2015 18:15:43 +0200 Subject: [PATCH 1/9] Work in progress: CCITT Encoder --- .../plugins/tiff/CCITTFaxEncoderStream.java | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java new file mode 100644 index 00000000..0e912e4b --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java @@ -0,0 +1,213 @@ +package com.twelvemonkeys.imageio.plugins.tiff; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import com.twelvemonkeys.lang.Validate; + +public class CCITTFaxEncoderStream extends OutputStream { + + private int currentBufferLength = 0; + private final byte[] inputBuffer; + private final int inputBufferLength; + private int columns; + + private int[] changesCurrentRow; + private int[] changesReferenceRow; + private int changesCurrentRowLength = 0; + private int changesReferenceRowLength = 0; + private byte outputBuffer = 0; + private byte outputBufferBitLength = 0; + private int type; + private int fillOrder; + private boolean optionG32D; + private boolean optionG3Fill; + private boolean optionUncompressed; + private OutputStream stream; + + public CCITTFaxEncoderStream(final OutputStream stream, final int columns, final int type, final int fillOrder, + final long options) { + + this.stream = stream; + this.type = type; + this.columns = columns; + this.fillOrder = fillOrder; + + this.changesReferenceRow = new int[columns]; + this.changesCurrentRow = new int[columns]; + + switch (type) { + case TIFFExtension.COMPRESSION_CCITT_T4: + optionG32D = (options & TIFFExtension.GROUP3OPT_2DENCODING) != 0; + optionG3Fill = (options & TIFFExtension.GROUP3OPT_FILLBITS) != 0; + optionUncompressed = (options & TIFFExtension.GROUP3OPT_UNCOMPRESSED) != 0; + break; + case TIFFExtension.COMPRESSION_CCITT_T6: + optionUncompressed = (options & TIFFExtension.GROUP4OPT_UNCOMPRESSED) != 0; + break; + } + + inputBufferLength = (columns + 7) / 8; + inputBuffer = new byte[inputBufferLength]; + + Validate.isTrue(!optionUncompressed, optionUncompressed, + "CCITT GROUP 3/4 OPTION UNCOMPRESSED is not supported"); + } + + @Override + public void write(int b) throws IOException { + inputBuffer[currentBufferLength] = (byte) b; + currentBufferLength++; + + if (currentBufferLength == inputBufferLength) { + encodeRow(); + } + } + + // TODO: when to write end EOLs, half filled buffer bytes etc. on end? + + private void encodeRow() throws IOException { + changesReferenceRow = changesCurrentRow; + changesReferenceRowLength = changesCurrentRowLength; + changesCurrentRowLength = 0; + + int index = 0; + boolean white = true; + while (index < columns) { + int byteIndex = index / 8; + int bit = index % 8; + if ((((inputBuffer[byteIndex] >> (7 - bit)) & 1) == 1) != (!white)) { + changesCurrentRow[changesCurrentRowLength] = index; + changesCurrentRowLength++; + white = !white; + } + index++; + } + + switch (type) { + case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE: + encodeRowType2(); + break; + case TIFFExtension.COMPRESSION_CCITT_T4: + encodeRowType4(); + break; + case TIFFExtension.COMPRESSION_CCITT_T6: + encodeRowType6(); + break; + } + } + + private void encodeRowType2() throws IOException { + encode1D(); + fill(); + } + + private void encodeRowType4() throws IOException { + writeEOL(); + if (optionG32D) { + // TODO decide whether 1d or 2d row, write k, encode + } else { + // TODO encode1d + } + } + + private void encodeRowType6() { + encode2D(); + } + + private void encode1D() throws IOException { + int index = 0; + boolean white = true; + while (index < columns) { + int nextChange = columns; + for (int i = 0; i < changesCurrentRowLength; i++) { + if (index < changesCurrentRow[i]) { + nextChange = changesCurrentRow[i]; + } + } + int runLength = nextChange - index; + + int nonterm = runLength / 64; + Code[] codes = white ? WHITE_NONTERMINATING_CODES : BLACK_NONTERMINATING_CODES; + while (nonterm > 0) { + if (nonterm >= codes.length) { + write(codes[codes.length-1].code,codes[codes.length-1].length); + nonterm -= codes.length - 1; + } else { + write(codes[nonterm - 1].code,codes[nonterm - 1].length); + nonterm = 0; + } + } + + Code c = white ? WHITE_TERMINATING_CODES[runLength % 64] : BLACK_TERMINATING_CODES[runLength % 64]; + write(c.code, c.length); + } + } + + private void encode2D() { + + } + + private void write(int code, int codeLength) throws IOException { + + for (int i = 0; i < codeLength; i++) { + boolean codeBit = ((code >> (i)) & 1) == 1; + + if (fillOrder == TIFFBaseline.FILL_LEFT_TO_RIGHT) { + outputBuffer |= (codeBit ? 1 << (7 - ((outputBufferBitLength) % 8)) : 0); + } else { + outputBuffer |= (codeBit ? 1 << (((outputBufferBitLength) % 8)) : 0); + } + outputBufferBitLength++; + + if (outputBufferBitLength == 8) { + stream.write(outputBuffer); + clearOutputBuffer(); + } + } + } + + private void writeEOL() throws IOException { + if (optionG3Fill) { + // Fill up so EOL ends on a byte-boundary + while (outputBufferBitLength != 4) { + write(0, 1); + } + } + write(1, 12); + } + + private void fill() throws IOException { + stream.write(outputBuffer); + clearOutputBuffer(); + } + + private void clearOutputBuffer() { + outputBuffer = 0; + outputBufferBitLength = 0; + } + + private static class Code { + public static Code create(int code, int length) { + Code c = new Code(code, length); + return c; + } + + private Code(int code, int length) { + this.code = code; + this.length = length; + } + + final int code; + final int length; + } + + private Code[] WHITE_TERMINATING_CODES = {}; + + private Code[] WHITE_NONTERMINATING_CODES = {}; + + private Code[] BLACK_TERMINATING_CODES = {}; + + private Code[] BLACK_NONTERMINATING_CODES = {}; +} From 8e11d95fd6dee1e78a99f4c6ca822bd7cb5e722f Mon Sep 17 00:00:00 2001 From: Schmidor Date: Sun, 4 Oct 2015 00:40:36 +0200 Subject: [PATCH 2/9] Work in progress: CCITT Encoder --- .../plugins/tiff/CCITTFaxEncoderStream.java | 167 ++++++++++++++++-- .../tiff/CCITTFaxEncoderStreamTest.java | 66 +++++++ 2 files changed, 217 insertions(+), 16 deletions(-) create mode 100644 imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java index 0e912e4b..29823188 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java @@ -1,11 +1,45 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import com.twelvemonkeys.lang.Validate; +/** + * CCITT Modified Huffman RLE, Group 3 (T4) and Group 4 (T6) fax compression. + * + * @author Harald Kuhr + * @author last modified by $Author$ + * @version $Id$ + */ public class CCITTFaxEncoderStream extends OutputStream { private int currentBufferLength = 0; @@ -65,6 +99,17 @@ public class CCITTFaxEncoderStream extends OutputStream { } } + @Override + public void flush() throws IOException { + stream.flush(); + } + + @Override + public void close() throws IOException { + fill(); + stream.close(); + } + // TODO: when to write end EOLs, half filled buffer bytes etc. on end? private void encodeRow() throws IOException { @@ -108,11 +153,14 @@ public class CCITTFaxEncoderStream extends OutputStream { if (optionG32D) { // TODO decide whether 1d or 2d row, write k, encode } else { - // TODO encode1d + encode1D(); + } + if (optionG3Fill) { + fill(); } } - private void encodeRowType6() { + private void encodeRowType6() throws IOException { encode2D(); } @@ -132,10 +180,10 @@ public class CCITTFaxEncoderStream extends OutputStream { Code[] codes = white ? WHITE_NONTERMINATING_CODES : BLACK_NONTERMINATING_CODES; while (nonterm > 0) { if (nonterm >= codes.length) { - write(codes[codes.length-1].code,codes[codes.length-1].length); + write(codes[codes.length - 1].code, codes[codes.length - 1].length); nonterm -= codes.length - 1; } else { - write(codes[nonterm - 1].code,codes[nonterm - 1].length); + write(codes[nonterm - 1].code, codes[nonterm - 1].length); nonterm = 0; } } @@ -145,8 +193,65 @@ public class CCITTFaxEncoderStream extends OutputStream { } } - private void encode2D() { + private void encode2D() throws IOException { + boolean white = true; + int lastChange = -1; + for (int i = 0; i < changesCurrentRowLength; i++) { // TODO + // columns-Basiert + // statt references + int nextChange = changesCurrentRow[i]; + int nextRef = getNextRefChange(lastChange); + + int difference = nextRef - nextChange; + if (difference < -3) { + // next change nearer than nextRef, PMODE + write(1, 4); + lastChange = nextRef; + } else if (difference > 3) { + // next change farer than nextRef, hmode + write(1, 3); + // write runLength(white) + // write runLength(!white) + // i++; + // lastChange = ... + } else { + // VMODE + switch (difference) { + case 0: + write(1, 1); + break; + case 1: + write(3, 3); + break; + case 2: + write(3, 6); + break; + case 3: + write(3, 7); + break; + case -1: + write(2, 3); + break; + case -2: + write(2, 6); + break; + case -3: + write(2, 7); + break; + } + white = !white; + } + } + } + + private int getNextRefChange(int currentRowChange) { + for (int i = 0; i < changesReferenceRowLength; i++) { + if (changesReferenceRow[i] > currentRowChange) { + return changesReferenceRow[i]; + } + } + return columns; } private void write(int code, int codeLength) throws IOException { @@ -188,12 +293,7 @@ public class CCITTFaxEncoderStream extends OutputStream { outputBufferBitLength = 0; } - private static class Code { - public static Code create(int code, int length) { - Code c = new Code(code, length); - return c; - } - + public static class Code { private Code(int code, int length) { this.code = code; this.length = length; @@ -203,11 +303,46 @@ public class CCITTFaxEncoderStream extends OutputStream { final int length; } - private Code[] WHITE_TERMINATING_CODES = {}; + public static final Code[] WHITE_TERMINATING_CODES; - private Code[] WHITE_NONTERMINATING_CODES = {}; + public static final Code[] WHITE_NONTERMINATING_CODES; - private Code[] BLACK_TERMINATING_CODES = {}; + public static final Code[] BLACK_TERMINATING_CODES; - private Code[] BLACK_NONTERMINATING_CODES = {}; + public static final Code[] BLACK_NONTERMINATING_CODES; + + static { + // Setup HUFFMAN Codes + WHITE_TERMINATING_CODES = new Code[64]; + WHITE_NONTERMINATING_CODES = new Code[40]; + for (int i = 0; i < CCITTFaxDecoderStream.WHITE_CODES.length; i++) { + int bitLength = i + 4; + for (int j = 0; j < CCITTFaxDecoderStream.WHITE_CODES[i].length; j++) { + int value = CCITTFaxDecoderStream.WHITE_RUN_LENGTHS[i][j]; + int code = CCITTFaxDecoderStream.WHITE_CODES[i][j]; + + if (value < 64) { + WHITE_TERMINATING_CODES[value] = new Code(code, bitLength); + } else { + WHITE_NONTERMINATING_CODES[(value / 64) - 1] = new Code(code, bitLength); + } + } + } + + BLACK_TERMINATING_CODES = new Code[64]; + BLACK_NONTERMINATING_CODES = new Code[40]; + for (int i = 0; i < CCITTFaxDecoderStream.BLACK_CODES.length; i++) { + int bitLength = i + 2; + for (int j = 0; j < CCITTFaxDecoderStream.BLACK_CODES[i].length; j++) { + int value = CCITTFaxDecoderStream.BLACK_RUN_LENGTHS[i][j]; + int code = CCITTFaxDecoderStream.BLACK_CODES[i][j]; + + if (value < 64) { + BLACK_TERMINATING_CODES[value] = new Code(code, bitLength); + } else { + BLACK_NONTERMINATING_CODES[(value / 64) - 1] = new Code(code, bitLength); + } + } + } + } } diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java new file mode 100644 index 00000000..b609b765 --- /dev/null +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java @@ -0,0 +1,66 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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 org.junit.Test; + +import com.twelvemonkeys.imageio.plugins.tiff.CCITTFaxEncoderStream.Code; + +import java.io.IOException; +import static org.junit.Assert.*; + +/** + * CCITTFaxEncoderStreamTest + * + * @author Harald Kuhr + * @author last modified by $Author$ + * @version $Id$ + */ +public class CCITTFaxEncoderStreamTest { + + @Test + public void testBuildCodes() throws IOException { + assertTrue(CCITTFaxEncoderStream.WHITE_TERMINATING_CODES.length == 64); + for (Code code : CCITTFaxEncoderStream.WHITE_TERMINATING_CODES) { + assertNotNull(code); + } + assertTrue(CCITTFaxEncoderStream.WHITE_NONTERMINATING_CODES.length == 40); + for (Code code : CCITTFaxEncoderStream.WHITE_NONTERMINATING_CODES) { + assertNotNull(code); + } + assertTrue(CCITTFaxEncoderStream.BLACK_TERMINATING_CODES.length == 64); + for (Code code : CCITTFaxEncoderStream.BLACK_TERMINATING_CODES) { + assertNotNull(code); + } + assertTrue(CCITTFaxEncoderStream.BLACK_NONTERMINATING_CODES.length == 40); + for (Code code : CCITTFaxEncoderStream.BLACK_NONTERMINATING_CODES) { + assertNotNull(code); + } + } +} From 39b92ab19f93be4824974878ab268568e4f1579a Mon Sep 17 00:00:00 2001 From: Schmidor Date: Thu, 8 Oct 2015 00:38:31 +0200 Subject: [PATCH 3/9] Work in progress: CCITT Encoder --- .../plugins/tiff/CCITTFaxEncoderStream.java | 130 +++++++++++------- .../tiff/CCITTFaxEncoderStreamTest.java | 79 ++++++++++- 2 files changed, 156 insertions(+), 53 deletions(-) diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java index 29823188..02c5eafc 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java @@ -36,7 +36,7 @@ import com.twelvemonkeys.lang.Validate; /** * CCITT Modified Huffman RLE, Group 3 (T4) and Group 4 (T6) fax compression. * - * @author Harald Kuhr + * @author Oliver Schmidtmer * @author last modified by $Author$ * @version $Id$ */ @@ -96,6 +96,7 @@ public class CCITTFaxEncoderStream extends OutputStream { if (currentBufferLength == inputBufferLength) { encodeRow(); + currentBufferLength = 0; } } @@ -106,14 +107,18 @@ public class CCITTFaxEncoderStream extends OutputStream { @Override public void close() throws IOException { + if (type == TIFFExtension.COMPRESSION_CCITT_T6) { + writeEOL(); + writeEOL(); + } fill(); stream.close(); } - // TODO: when to write end EOLs, half filled buffer bytes etc. on end? - private void encodeRow() throws IOException { + int[] tmp = changesReferenceRow; changesReferenceRow = changesCurrentRow; + changesCurrentRow = tmp; changesReferenceRowLength = changesCurrentRowLength; changesCurrentRowLength = 0; @@ -151,7 +156,15 @@ public class CCITTFaxEncoderStream extends OutputStream { private void encodeRowType4() throws IOException { writeEOL(); if (optionG32D) { - // TODO decide whether 1d or 2d row, write k, encode + // do k=1 only on first line. Detect first line by missing reference + // line. + if (changesReferenceRowLength == 0) { + write(1, 1); + encode1D(); + } else { + write(0, 1); + encode2D(); + } } else { encode1D(); } @@ -168,53 +181,66 @@ public class CCITTFaxEncoderStream extends OutputStream { int index = 0; boolean white = true; while (index < columns) { - int nextChange = columns; - for (int i = 0; i < changesCurrentRowLength; i++) { - if (index < changesCurrentRow[i]) { - nextChange = changesCurrentRow[i]; - } - } - int runLength = nextChange - index; - - int nonterm = runLength / 64; - Code[] codes = white ? WHITE_NONTERMINATING_CODES : BLACK_NONTERMINATING_CODES; - while (nonterm > 0) { - if (nonterm >= codes.length) { - write(codes[codes.length - 1].code, codes[codes.length - 1].length); - nonterm -= codes.length - 1; - } else { - write(codes[nonterm - 1].code, codes[nonterm - 1].length); - nonterm = 0; - } - } - - Code c = white ? WHITE_TERMINATING_CODES[runLength % 64] : BLACK_TERMINATING_CODES[runLength % 64]; - write(c.code, c.length); + int[] nextChanges = getNextChanges(index); + int runLength = nextChanges[0] - index; + writeRun(runLength, white); + index += runLength; + white = !white; } } + private int[] getNextChanges(int pos) { + int[] result = new int[] { columns, columns }; + for (int i = 0; i < changesCurrentRowLength; i++) { + if (pos < changesCurrentRow[i]) { + result[0] = changesCurrentRow[i]; + if ((i + 1) < changesCurrentRowLength) { + result[1] = changesCurrentRow[i + 1]; + } + break; + } + } + + return result; + } + + private void writeRun(int runLength, boolean white) throws IOException { + int nonterm = runLength / 64; + Code[] codes = white ? WHITE_NONTERMINATING_CODES : BLACK_NONTERMINATING_CODES; + while (nonterm > 0) { + if (nonterm >= codes.length) { + write(codes[codes.length - 1].code, codes[codes.length - 1].length); + nonterm -= codes.length - 1; + } else { + write(codes[nonterm - 1].code, codes[nonterm - 1].length); + nonterm = 0; + } + } + + Code c = white ? WHITE_TERMINATING_CODES[runLength % 64] : BLACK_TERMINATING_CODES[runLength % 64]; + write(c.code, c.length); + } + private void encode2D() throws IOException { boolean white = true; - int lastChange = -1; - for (int i = 0; i < changesCurrentRowLength; i++) { // TODO - // columns-Basiert - // statt references - int nextChange = changesCurrentRow[i]; + int index = 0; // a0 + while (index < columns) { + int[] nextChanges = getNextChanges(index); // a1, a2 - int nextRef = getNextRefChange(lastChange); + int[] nextRefs = getNextRefChanges(index, white); // b1, b2 - int difference = nextRef - nextChange; - if (difference < -3) { - // next change nearer than nextRef, PMODE + int difference = nextChanges[0] - nextRefs[0]; + if (nextChanges[0] > nextRefs[1]) { + // PMODE write(1, 4); - lastChange = nextRef; - } else if (difference > 3) { - // next change farer than nextRef, hmode + index = nextRefs[1]; + } else if (difference > 3 || difference < -3) { + // HMODE write(1, 3); - // write runLength(white) - // write runLength(!white) - // i++; - // lastChange = ... + writeRun(nextChanges[0] - index, white); + writeRun(nextChanges[1] - nextChanges[0], !white); + index = nextChanges[1]; + } else { // VMODE switch (difference) { @@ -241,24 +267,29 @@ public class CCITTFaxEncoderStream extends OutputStream { break; } white = !white; + index = nextRefs[0] + difference; } } } - private int getNextRefChange(int currentRowChange) { - for (int i = 0; i < changesReferenceRowLength; i++) { - if (changesReferenceRow[i] > currentRowChange) { - return changesReferenceRow[i]; + private int[] getNextRefChanges(int a0, boolean white) { + int[] result = new int[] { columns, columns }; + for (int i = (white ? 0 : 1); i < changesReferenceRowLength; i += 2) { + if (changesReferenceRow[i] > a0) { + result[0] = changesReferenceRow[i]; + if ((i + 1) < changesReferenceRowLength) { + result[1] = changesReferenceRow[i + 1]; + } + break; } } - return columns; + return result; } private void write(int code, int codeLength) throws IOException { for (int i = 0; i < codeLength; i++) { - boolean codeBit = ((code >> (i)) & 1) == 1; - + boolean codeBit = ((code >> (codeLength - i - 1)) & 1) == 1; if (fillOrder == TIFFBaseline.FILL_LEFT_TO_RIGHT) { outputBuffer |= (codeBit ? 1 << (7 - ((outputBufferBitLength) % 8)) : 0); } else { @@ -271,6 +302,7 @@ public class CCITTFaxEncoderStream extends OutputStream { clearOutputBuffer(); } } + System.err.println(""); } private void writeEOL() throws IOException { diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java index b609b765..63ea4c99 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java @@ -28,22 +28,51 @@ package com.twelvemonkeys.imageio.plugins.tiff; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.junit.Before; import org.junit.Test; import com.twelvemonkeys.imageio.plugins.tiff.CCITTFaxEncoderStream.Code; -import java.io.IOException; -import static org.junit.Assert.*; - /** * CCITTFaxEncoderStreamTest * - * @author Harald Kuhr + * @author Oliver Schmidtmer * @author last modified by $Author$ * @version $Id$ */ public class CCITTFaxEncoderStreamTest { + // 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 + BufferedImage image; + + @Before + public void init() { + image = new BufferedImage(6, 4, BufferedImage.TYPE_BYTE_BINARY); + 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 testBuildCodes() throws IOException { assertTrue(CCITTFaxEncoderStream.WHITE_TERMINATING_CODES.length == 64); @@ -63,4 +92,46 @@ public class CCITTFaxEncoderStreamTest { assertNotNull(code); } } + + @Test + public void testType2() throws IOException { + testImage(TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1, 0L); + } + + @Test + public void testType4() throws IOException { + testImage(TIFFExtension.COMPRESSION_CCITT_T4, 1, 0L); + testImage(TIFFExtension.COMPRESSION_CCITT_T4, 1, TIFFExtension.GROUP3OPT_FILLBITS); + testImage(TIFFExtension.COMPRESSION_CCITT_T4, 1, TIFFExtension.GROUP3OPT_2DENCODING); + testImage(TIFFExtension.COMPRESSION_CCITT_T4, 1, + TIFFExtension.GROUP3OPT_FILLBITS | TIFFExtension.GROUP3OPT_2DENCODING); + } + + @Test + public void testType6() throws IOException { + testImage(TIFFExtension.COMPRESSION_CCITT_T6, 1, 0L); + } + + @Test + public void restReversedFillOrder() throws IOException { + testImage(TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 2, 0L); + testImage(TIFFExtension.COMPRESSION_CCITT_T6, 2, 0L); + } + + private void testImage(int type, int fillOrder, long options) throws IOException { + byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); + byte[] redecodedData = new byte[imageData.length]; + ByteArrayOutputStream imageOutput = new ByteArrayOutputStream(); + OutputStream outputSteam = new CCITTFaxEncoderStream(imageOutput, 6, type, fillOrder, options); + outputSteam.write(imageData); + outputSteam.close(); + byte[] encodedData = imageOutput.toByteArray(); + + CCITTFaxDecoderStream inputStream = new CCITTFaxDecoderStream(new ByteArrayInputStream(encodedData), 6, type, + fillOrder, options); + new DataInputStream(inputStream).readFully(redecodedData); + inputStream.close(); + + assertArrayEquals(imageData, redecodedData); + } } From c8621439c0c13c52eb4354be3c705d234cc78ca8 Mon Sep 17 00:00:00 2001 From: Schmidor Date: Fri, 9 Oct 2015 23:18:56 +0200 Subject: [PATCH 4/9] CCITT Fax writer: adjust formatting and write finishing bytes on last row instead of on stream closing --- .../plugins/tiff/CCITTFaxEncoderStream.java | 133 ++++++++++-------- .../tiff/CCITTFaxEncoderStreamTest.java | 21 +-- 2 files changed, 81 insertions(+), 73 deletions(-) diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java index 02c5eafc..d8c11198 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java @@ -28,14 +28,14 @@ package com.twelvemonkeys.imageio.plugins.tiff; +import com.twelvemonkeys.lang.Validate; + import java.io.IOException; import java.io.OutputStream; -import com.twelvemonkeys.lang.Validate; - /** * CCITT Modified Huffman RLE, Group 3 (T4) and Group 4 (T6) fax compression. - * + * * @author Oliver Schmidtmer * @author last modified by $Author$ * @version $Id$ @@ -46,9 +46,11 @@ public class CCITTFaxEncoderStream extends OutputStream { private final byte[] inputBuffer; private final int inputBufferLength; private int columns; + private int rows; private int[] changesCurrentRow; private int[] changesReferenceRow; + private int currentRow = 0; private int changesCurrentRowLength = 0; private int changesReferenceRowLength = 0; private byte outputBuffer = 0; @@ -60,26 +62,27 @@ public class CCITTFaxEncoderStream extends OutputStream { private boolean optionUncompressed; private OutputStream stream; - public CCITTFaxEncoderStream(final OutputStream stream, final int columns, final int type, final int fillOrder, - final long options) { + public CCITTFaxEncoderStream(final OutputStream stream, final int columns, final int rows, final int type, final int fillOrder, + final long options) { this.stream = stream; this.type = type; this.columns = columns; + this.rows = rows; this.fillOrder = fillOrder; this.changesReferenceRow = new int[columns]; this.changesCurrentRow = new int[columns]; switch (type) { - case TIFFExtension.COMPRESSION_CCITT_T4: - optionG32D = (options & TIFFExtension.GROUP3OPT_2DENCODING) != 0; - optionG3Fill = (options & TIFFExtension.GROUP3OPT_FILLBITS) != 0; - optionUncompressed = (options & TIFFExtension.GROUP3OPT_UNCOMPRESSED) != 0; - break; - case TIFFExtension.COMPRESSION_CCITT_T6: - optionUncompressed = (options & TIFFExtension.GROUP4OPT_UNCOMPRESSED) != 0; - break; + case TIFFExtension.COMPRESSION_CCITT_T4: + optionG32D = (options & TIFFExtension.GROUP3OPT_2DENCODING) != 0; + optionG3Fill = (options & TIFFExtension.GROUP3OPT_FILLBITS) != 0; + optionUncompressed = (options & TIFFExtension.GROUP3OPT_UNCOMPRESSED) != 0; + break; + case TIFFExtension.COMPRESSION_CCITT_T6: + optionUncompressed = (options & TIFFExtension.GROUP4OPT_UNCOMPRESSED) != 0; + break; } inputBufferLength = (columns + 7) / 8; @@ -107,15 +110,11 @@ public class CCITTFaxEncoderStream extends OutputStream { @Override public void close() throws IOException { - if (type == TIFFExtension.COMPRESSION_CCITT_T6) { - writeEOL(); - writeEOL(); - } - fill(); stream.close(); } private void encodeRow() throws IOException { + currentRow++; int[] tmp = changesReferenceRow; changesReferenceRow = changesCurrentRow; changesCurrentRow = tmp; @@ -127,7 +126,7 @@ public class CCITTFaxEncoderStream extends OutputStream { while (index < columns) { int byteIndex = index / 8; int bit = index % 8; - if ((((inputBuffer[byteIndex] >> (7 - bit)) & 1) == 1) != (!white)) { + if ((((inputBuffer[byteIndex] >> (7 - bit)) & 1) == 1) == (white)) { changesCurrentRow[changesCurrentRowLength] = index; changesCurrentRowLength++; white = !white; @@ -136,15 +135,23 @@ public class CCITTFaxEncoderStream extends OutputStream { } switch (type) { - case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE: - encodeRowType2(); - break; - case TIFFExtension.COMPRESSION_CCITT_T4: - encodeRowType4(); - break; - case TIFFExtension.COMPRESSION_CCITT_T6: - encodeRowType6(); - break; + case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE: + encodeRowType2(); + break; + case TIFFExtension.COMPRESSION_CCITT_T4: + encodeRowType4(); + break; + case TIFFExtension.COMPRESSION_CCITT_T6: + encodeRowType6(); + break; + } + + if (currentRow == rows) { + if (type == TIFFExtension.COMPRESSION_CCITT_T6) { + writeEOL(); + writeEOL(); + } + fill(); } } @@ -161,11 +168,13 @@ public class CCITTFaxEncoderStream extends OutputStream { if (changesReferenceRowLength == 0) { write(1, 1); encode1D(); - } else { + } + else { write(0, 1); encode2D(); } - } else { + } + else { encode1D(); } if (optionG3Fill) { @@ -190,7 +199,7 @@ public class CCITTFaxEncoderStream extends OutputStream { } private int[] getNextChanges(int pos) { - int[] result = new int[] { columns, columns }; + int[] result = new int[] {columns, columns}; for (int i = 0; i < changesCurrentRowLength; i++) { if (pos < changesCurrentRow[i]) { result[0] = changesCurrentRow[i]; @@ -211,7 +220,8 @@ public class CCITTFaxEncoderStream extends OutputStream { if (nonterm >= codes.length) { write(codes[codes.length - 1].code, codes[codes.length - 1].length); nonterm -= codes.length - 1; - } else { + } + else { write(codes[nonterm - 1].code, codes[nonterm - 1].length); nonterm = 0; } @@ -234,37 +244,39 @@ public class CCITTFaxEncoderStream extends OutputStream { // PMODE write(1, 4); index = nextRefs[1]; - } else if (difference > 3 || difference < -3) { + } + else if (difference > 3 || difference < -3) { // HMODE write(1, 3); writeRun(nextChanges[0] - index, white); writeRun(nextChanges[1] - nextChanges[0], !white); index = nextChanges[1]; - } else { + } + else { // VMODE switch (difference) { - case 0: - write(1, 1); - break; - case 1: - write(3, 3); - break; - case 2: - write(3, 6); - break; - case 3: - write(3, 7); - break; - case -1: - write(2, 3); - break; - case -2: - write(2, 6); - break; - case -3: - write(2, 7); - break; + case 0: + write(1, 1); + break; + case 1: + write(3, 3); + break; + case 2: + write(3, 6); + break; + case 3: + write(3, 7); + break; + case -1: + write(2, 3); + break; + case -2: + write(2, 6); + break; + case -3: + write(2, 7); + break; } white = !white; index = nextRefs[0] + difference; @@ -273,7 +285,7 @@ public class CCITTFaxEncoderStream extends OutputStream { } private int[] getNextRefChanges(int a0, boolean white) { - int[] result = new int[] { columns, columns }; + int[] result = new int[] {columns, columns}; for (int i = (white ? 0 : 1); i < changesReferenceRowLength; i += 2) { if (changesReferenceRow[i] > a0) { result[0] = changesReferenceRow[i]; @@ -292,7 +304,8 @@ public class CCITTFaxEncoderStream extends OutputStream { boolean codeBit = ((code >> (codeLength - i - 1)) & 1) == 1; if (fillOrder == TIFFBaseline.FILL_LEFT_TO_RIGHT) { outputBuffer |= (codeBit ? 1 << (7 - ((outputBufferBitLength) % 8)) : 0); - } else { + } + else { outputBuffer |= (codeBit ? 1 << (((outputBufferBitLength) % 8)) : 0); } outputBufferBitLength++; @@ -355,7 +368,8 @@ public class CCITTFaxEncoderStream extends OutputStream { if (value < 64) { WHITE_TERMINATING_CODES[value] = new Code(code, bitLength); - } else { + } + else { WHITE_NONTERMINATING_CODES[(value / 64) - 1] = new Code(code, bitLength); } } @@ -371,7 +385,8 @@ public class CCITTFaxEncoderStream extends OutputStream { if (value < 64) { BLACK_TERMINATING_CODES[value] = new Code(code, bitLength); - } else { + } + else { BLACK_NONTERMINATING_CODES[(value / 64) - 1] = new Code(code, bitLength); } } diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java index 63ea4c99..c251d687 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java @@ -28,22 +28,15 @@ package com.twelvemonkeys.imageio.plugins.tiff; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import java.awt.image.BufferedImage; -import java.awt.image.DataBufferByte; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.IOException; -import java.io.OutputStream; - +import com.twelvemonkeys.imageio.plugins.tiff.CCITTFaxEncoderStream.Code; import org.junit.Before; import org.junit.Test; -import com.twelvemonkeys.imageio.plugins.tiff.CCITTFaxEncoderStream.Code; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.io.*; + +import static org.junit.Assert.*; /** * CCITTFaxEncoderStreamTest @@ -122,7 +115,7 @@ public class CCITTFaxEncoderStreamTest { byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); byte[] redecodedData = new byte[imageData.length]; ByteArrayOutputStream imageOutput = new ByteArrayOutputStream(); - OutputStream outputSteam = new CCITTFaxEncoderStream(imageOutput, 6, type, fillOrder, options); + OutputStream outputSteam = new CCITTFaxEncoderStream(imageOutput, 6, 4, type, fillOrder, options); outputSteam.write(imageData); outputSteam.close(); byte[] encodedData = imageOutput.toByteArray(); From 07617b49ceade9ed8d53347045c7d58c8d4ac486 Mon Sep 17 00:00:00 2001 From: Schmidor Date: Sat, 10 Oct 2015 16:31:40 +0200 Subject: [PATCH 5/9] Support CCITT Fax Encoder in TiffImageWriter --- .../plugins/tiff/TIFFImageWriteParam.java | 11 +- .../imageio/plugins/tiff/TIFFImageWriter.java | 138 ++++++++++++------ 2 files changed, 106 insertions(+), 43 deletions(-) diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriteParam.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriteParam.java index 637ea6f3..4ed13ee6 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriteParam.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriteParam.java @@ -65,7 +65,7 @@ public final class TIFFImageWriteParam extends ImageWriteParam { // See: http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/TIFFImageWriteParam.html compressionTypes = new String[] { "None", - null, null, null,/* "CCITT RLE", "CCITT T.4", "CCITT T.6", */ + "CCITT RLE", "CCITT T.4", "CCITT T.6", "LZW", "JPEG", "ZLib", "PackBits", "Deflate", null/* "EXIF JPEG" */ // A well-defined form of "Old-style JPEG", no tables/process, only 513 (offset) and 514 (length) }; @@ -111,6 +111,15 @@ public final class TIFFImageWriteParam extends ImageWriteParam { else if (param.getCompressionType().equals("JPEG")) { return TIFFExtension.COMPRESSION_JPEG; } + else if (param.getCompressionType().equals("CCITT RLE")) { + return TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE; + } + else if (param.getCompressionType().equals("CCITT T.4")) { + return TIFFExtension.COMPRESSION_CCITT_T4; + } + else if (param.getCompressionType().equals("CCITT T.6")) { + return TIFFExtension.COMPRESSION_CCITT_T6; + } // else if (param.getCompressionType().equals("EXIF JPEG")) { // return TIFFExtension.COMPRESSION_OLD_JPEG; // } 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 b4012e0b..0370976e 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 @@ -194,6 +194,7 @@ public final class TIFFImageWriter extends ImageWriterBase { ColorModel colorModel = renderedImage.getColorModel(); int numComponents = colorModel.getNumComponents(); + //TODO: streamMetadata? TIFFImageMetadata metadata; if (image.getMetadata() != null) { metadata = convertImageMetadata(image.getMetadata(), ImageTypeSpecifier.createFromRenderedImage(renderedImage), param); @@ -208,7 +209,7 @@ public final class TIFFImageWriter extends ImageWriterBase { int[] bitOffsets; if (sampleModel instanceof ComponentSampleModel) { bandOffsets = ((ComponentSampleModel) sampleModel).getBandOffsets(); - bitOffsets = null; + bitOffsets = null; } else if (sampleModel instanceof SinglePixelPackedSampleModel) { bitOffsets = ((SinglePixelPackedSampleModel) sampleModel).getBitOffsets(); @@ -222,26 +223,34 @@ public final class TIFFImageWriter extends ImageWriterBase { throw new IllegalArgumentException("Unknown bit/bandOffsets for sample model: " + sampleModel); } - Set entries = new LinkedHashSet<>(); - entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, renderedImage.getWidth())); - entries.add(new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, renderedImage.getHeight())); + HashMap 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.add(new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, asShortArray(sampleModel.getSampleSize()))); + 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()) { // TODO: Write per component > numColorComponents if (colorModel.hasAlpha()) { - entries.add(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.add(new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED)); + entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED)); } } // Write compression field from param or metadata - // TODO: Support COPY_FROM_METADATA - int compression = TIFFImageWriteParam.getCompressionType(param); - entries.add(new TIFFEntry(TIFF.TAG_COMPRESSION, compression)); + int compression; + if ((param == null || param.getCompressionMode() == TIFFImageWriteParam.MODE_COPY_FROM_METADATA) + && image.getMetadata() != null) { + compression = (int) metadata.getIFD().getEntryById(TIFF.TAG_COMPRESSION).getValue(); + } + else { + compression = TIFFImageWriteParam.getCompressionType(param); + } + entries.put(TIFF.TAG_COMPRESSION, new TIFFEntry(TIFF.TAG_COMPRESSION, compression)); // TODO: Let param/metadata control predictor // TODO: Depending on param.getCompressionMode(): DISABLED/EXPLICIT/COPY_FROM_METADATA/DEFAULT @@ -249,7 +258,22 @@ public final class TIFFImageWriter extends ImageWriterBase { case TIFFExtension.COMPRESSION_ZLIB: case TIFFExtension.COMPRESSION_DEFLATE: case TIFFExtension.COMPRESSION_LZW: - entries.add(new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING)); + 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: } @@ -257,49 +281,52 @@ public final class TIFFImageWriter extends ImageWriterBase { int photometric = compression == TIFFExtension.COMPRESSION_JPEG ? TIFFExtension.PHOTOMETRIC_YCBCR : getPhotometricInterpretation(colorModel); - entries.add(new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, photometric)); + entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, photometric)); if (photometric == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) { - entries.add(new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel))); - entries.add(new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1)); + entries.put(TIFF.TAG_COLOR_MAP, new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel))); + entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1)); } else { - entries.add(new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numComponents)); + entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numComponents)); // Note: Assuming sRGB to be the default RGB interpretation ColorSpace colorSpace = colorModel.getColorSpace(); if (colorSpace instanceof ICC_ColorSpace && !colorSpace.isCS_sRGB()) { - entries.add(new TIFFEntry(TIFF.TAG_ICC_PROFILE, ((ICC_ColorSpace) colorSpace).getProfile().getData())); + entries.put(TIFF.TAG_ICC_PROFILE, new TIFFEntry(TIFF.TAG_ICC_PROFILE, ((ICC_ColorSpace) colorSpace).getProfile().getData())); } } // Default sample format SAMPLEFORMAT_UINT need not be written - if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT /* TODO: if (isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) { - entries.add(new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT)); + if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT/* TODO: if isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) { + entries.put(TIFF.TAG_SAMPLE_FORMAT, new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT)); } // TODO: Float values! // Get Software from metadata, or use default Entry software = metadata.getIFD().getEntryById(TIFF.TAG_SOFTWARE); - entries.add(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())); // Get X/YResolution and ResolutionUnit from metadata if set, otherwise use defaults // TODO: Add logic here OR in metadata merging, to make sure these 3 values are consistent. Entry xRes = metadata.getIFD().getEntryById(TIFF.TAG_X_RESOLUTION); - entries.add(xRes != null ? xRes : new TIFFEntry(TIFF.TAG_X_RESOLUTION, STANDARD_DPI)); + entries.put(TIFF.TAG_X_RESOLUTION, xRes != null ? xRes : new TIFFEntry(TIFF.TAG_X_RESOLUTION, STANDARD_DPI)); Entry yRes = metadata.getIFD().getEntryById(TIFF.TAG_Y_RESOLUTION); - entries.add(yRes != null ? yRes : new TIFFEntry(TIFF.TAG_Y_RESOLUTION, STANDARD_DPI)); + 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.add(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.add(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, Integer.MAX_VALUE)); // TODO: Allowed but not recommended // - 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.add(dummyStripByteCounts); // Updated later + entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, dummyStripByteCounts); // 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.add(dummyStripOffsets); // Updated later + entries.put(TIFF.TAG_STRIP_OFFSETS, dummyStripOffsets); // Updated later // TODO: If tiled, write tile indexes etc // Depending on param.getTilingMode @@ -308,14 +335,15 @@ public final class TIFFImageWriter extends ImageWriterBase { if (compression == TIFFBaseline.COMPRESSION_NONE) { // This implementation, allows semi-streaming-compatible uncompressed TIFFs - long streamOffset = exifWriter.computeIFDSize(entries) + 12; // 12 == 4 byte magic, 4 byte IDD 0 pointer, 4 byte EOF + long streamOffset = exifWriter.computeIFDSize(entries.values()) + 12; // 12 == 4 byte magic, 4 byte IDD 0 pointer, 4 byte EOF entries.remove(dummyStripByteCounts); - entries.add(new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, renderedImage.getWidth() * renderedImage.getHeight() * numComponents)); + entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, + renderedImage.getWidth() * renderedImage.getHeight() * numComponents)); entries.remove(dummyStripOffsets); - entries.add(new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, streamOffset)); + entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, streamOffset)); - exifWriter.write(entries, imageOutput); // NOTE: Writer takes case of ordering tags + exifWriter.write(entries.values(), imageOutput); // NOTE: Writer takes case of ordering tags imageOutput.flush(); } else { @@ -344,7 +372,8 @@ public final class TIFFImageWriter extends ImageWriterBase { } else { // Write image data - writeImageData(createCompressorStream(renderedImage, param), renderedImage, numComponents, bandOffsets, bitOffsets); + writeImageData(createCompressorStream(renderedImage, param, entries), renderedImage, numComponents, bandOffsets, + bitOffsets); } // Update IFD0-pointer, and write IFD @@ -352,11 +381,11 @@ public final class TIFFImageWriter extends ImageWriterBase { long streamPosition = imageOutput.getStreamPosition(); entries.remove(dummyStripOffsets); - entries.add(new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, 8)); + entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, 8)); entries.remove(dummyStripByteCounts); - entries.add(new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, streamPosition - 8)); + entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, streamPosition - 8)); - long ifdOffset = exifWriter.writeIFD(entries, imageOutput); + long ifdOffset = exifWriter.writeIFD(entries.values(), imageOutput); imageOutput.writeInt(0); // Next IFD (none) streamPosition = imageOutput.getStreamPosition(); @@ -368,7 +397,7 @@ public final class TIFFImageWriter extends ImageWriterBase { } } - private DataOutput createCompressorStream(RenderedImage image, ImageWriteParam param) { + private DataOutput createCompressorStream(RenderedImage image, ImageWriteParam param, HashMap entries) { /* 36 MB test data: @@ -422,7 +451,7 @@ public final class TIFFImageWriter extends ImageWriterBase { // Use predictor by default for LZW and ZLib/Deflate // TODO: Unless explicitly disabled in TIFFImageWriteParam - int compression = TIFFImageWriteParam.getCompressionType(param); + int compression = (int) entries.get(TIFF.TAG_COMPRESSION).getValue(); OutputStream stream; switch (compression) { @@ -465,8 +494,27 @@ public final class TIFFImageWriter extends ImageWriterBase { 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.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()); + + 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(); + } + Entry fillOrderEntry = entries.get(TIFF.TAG_FILL_ORDER); + 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); return new DataOutputStream(stream); } @@ -555,9 +603,14 @@ public final class TIFFImageWriter extends ImageWriterBase { // TODO: SampleSize may differ between bands/banks int sampleSize = renderedImage.getSampleModel().getSampleSize(0); - final ByteBuffer buffer = ByteBuffer.allocate(tileWidth * renderedImage.getSampleModel().getNumBands() * sampleSize / 8); - -// System.err.println("tileWidth: " + tileWidth); + final ByteBuffer buffer; + if (sampleSize == 1) { + buffer = ByteBuffer.allocate((tileWidth + 7) / 8); + } + else { + buffer = ByteBuffer.allocate(tileWidth * renderedImage.getSampleModel().getNumBands() * sampleSize / 8); + } + // System.err.println("tileWidth: " + tileWidth); for (int yTile = minTileY; yTile < maxYTiles; yTile++) { for (int xTile = minTileX; xTile < maxXTiles; xTile++) { @@ -572,9 +625,10 @@ public final class TIFFImageWriter extends ImageWriterBase { // System.err.println("Writing " + numBands + "BYTE -> " + numBands + "BYTE"); for (int b = 0; b < dataBuffer.getNumBanks(); b++) { for (int y = 0; y < tileHeight; y++) { - final int yOff = y * tileWidth * numBands; + int steps = sampleSize == 1 ? (tileWidth + 7) / 8 : tileWidth; + final int yOff = y * steps * numBands; - for (int x = 0; x < tileWidth; x++) { + for (int x = 0; x < steps; x++) { final int xOff = yOff + x * numBands; for (int s = 0; s < numBands; s++) { From 237079bcc9db9843466b8d614eac2ead506ae01b Mon Sep 17 00:00:00 2001 From: Schmidor Date: Sun, 11 Oct 2015 16:32:39 +0200 Subject: [PATCH 6/9] Normalize Black on byte Data --- .../imageio/plugins/tiff/TIFFImageWriter.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 0370976e..dc7b968a 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 @@ -373,7 +373,7 @@ public final class TIFFImageWriter extends ImageWriterBase { else { // Write image data writeImageData(createCompressorStream(renderedImage, param, entries), renderedImage, numComponents, bandOffsets, - bitOffsets); + bitOffsets, photometric); } // Update IFD0-pointer, and write IFD @@ -583,7 +583,7 @@ public final class TIFFImageWriter extends ImageWriterBase { return shorts; } - private void writeImageData(DataOutput stream, RenderedImage renderedImage, int numComponents, int[] bandOffsets, int[] bitOffsets) throws IOException { + private void writeImageData(DataOutput stream, RenderedImage renderedImage, int numComponents, int[] bandOffsets, int[] bitOffsets, int photometric) throws IOException { // Store 3BYTE, 4BYTE as is (possibly need to re-arrange to RGB order) // Store INT_RGB as 3BYTE, INT_ARGB as 4BYTE?, INT_ABGR must be re-arranged // Store IndexColorModel as is @@ -632,7 +632,7 @@ public final class TIFFImageWriter extends ImageWriterBase { final int xOff = yOff + x * numBands; for (int s = 0; s < numBands; s++) { - buffer.put((byte) (dataBuffer.getElem(b, xOff + bandOffsets[s]) & 0xff)); + buffer.put(normalizeBlack(photometric,(byte) (dataBuffer.getElem(b, xOff + bandOffsets[s]) & 0xff))); } } @@ -744,6 +744,14 @@ public final class TIFFImageWriter extends ImageWriterBase { processImageComplete(); } + + private byte normalizeBlack(int photometricInterpretation, byte data) { + if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO) { + // Inverse values + return (byte) (0xff - data & 0xff); + } + return data; + } // TODO: Would be better to solve this on stream level... But writers would then have to explicitly flush the buffer before done. private void flushBuffer(final ByteBuffer buffer, final DataOutput stream) throws IOException { From 6c702c447b3d4a658464b5c93c53af50d7215c46 Mon Sep 17 00:00:00 2001 From: Schmidor Date: Sun, 11 Oct 2015 16:34:37 +0200 Subject: [PATCH 7/9] CCITT Fax writer: fill should only add bits if byte is not empty --- .../imageio/plugins/tiff/CCITTFaxEncoderStream.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java index d8c11198..b8b3e096 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java @@ -315,7 +315,6 @@ public class CCITTFaxEncoderStream extends OutputStream { clearOutputBuffer(); } } - System.err.println(""); } private void writeEOL() throws IOException { @@ -329,7 +328,9 @@ public class CCITTFaxEncoderStream extends OutputStream { } private void fill() throws IOException { - stream.write(outputBuffer); + if (outputBufferBitLength != 0) { + stream.write(outputBuffer); + } clearOutputBuffer(); } From 306a8ae166c4bc89194f4b0ce56f84ddf051aa10 Mon Sep 17 00:00:00 2001 From: Schmidor Date: Sun, 11 Oct 2015 17:31:28 +0200 Subject: [PATCH 8/9] Change PhotometricInterpretation / SamplesPerPixel detection for one bit per pixel ColorSpaces --- .../imageio/plugins/tiff/TIFFImageWriter.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) 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 dc7b968a..9fdb3dd9 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 @@ -288,6 +288,10 @@ 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)); // Note: Assuming sRGB to be the default RGB interpretation @@ -298,7 +302,7 @@ public final class TIFFImageWriter extends ImageWriterBase { } // Default sample format SAMPLEFORMAT_UINT need not be written - if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT/* TODO: if isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) { + if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT/* TODO: if isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) { entries.put(TIFF.TAG_SAMPLE_FORMAT, new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT)); } // TODO: Float values! @@ -523,12 +527,12 @@ public final class TIFFImageWriter extends ImageWriterBase { } private int getPhotometricInterpretation(final ColorModel colorModel) { - if (colorModel.getNumComponents() == 1 && colorModel.getComponentSize(0) == 1) { + if (colorModel.getPixelSize() == 1) { if (colorModel instanceof IndexColorModel) { - if (colorModel.getRGB(0) == 0xFFFFFF && colorModel.getRGB(1) == 0x000000) { + if (colorModel.getRGB(0) == 0xFFFFFFFF && colorModel.getRGB(1) == 0xFF000000) { return TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO; } - else if (colorModel.getRGB(0) != 0x000000 || colorModel.getRGB(1) != 0xFFFFFF) { + else if (colorModel.getRGB(0) != 0xFF000000 || colorModel.getRGB(1) != 0xFFFFFFFF) { return TIFFBaseline.PHOTOMETRIC_PALETTE; } // Else, fall through to default, BLACK_IS_ZERO @@ -561,9 +565,9 @@ public final class TIFFImageWriter extends ImageWriterBase { for (int i = 0; i < colorModel.getMapSize(); i++) { int color = colorModel.getRGB(i); - colorMap[i ] = (short) upScale((color >> 16) & 0xff); - colorMap[i + colorMap.length / 3] = (short) upScale((color >> 8) & 0xff); - colorMap[i + 2 * colorMap.length / 3] = (short) upScale((color ) & 0xff); + colorMap[i] = (short) upScale((color >> 16) & 0xff); + colorMap[i + colorMap.length / 3] = (short) upScale((color >> 8) & 0xff); + colorMap[i + 2 * colorMap.length / 3] = (short) upScale((color) & 0xff); } return colorMap; @@ -617,7 +621,6 @@ public final class TIFFImageWriter extends ImageWriterBase { final Raster tile = renderedImage.getTile(xTile, yTile); final DataBuffer dataBuffer = tile.getDataBuffer(); final int numBands = tile.getNumBands(); -// final SampleModel sampleModel = tile.getSampleModel(); switch (dataBuffer.getDataType()) { case DataBuffer.TYPE_BYTE: @@ -632,7 +635,7 @@ public final class TIFFImageWriter extends ImageWriterBase { final int xOff = yOff + x * numBands; for (int s = 0; s < numBands; s++) { - buffer.put(normalizeBlack(photometric,(byte) (dataBuffer.getElem(b, xOff + bandOffsets[s]) & 0xff))); + buffer.put(normalizeBlack(photometric, (byte) (dataBuffer.getElem(b, xOff + bandOffsets[s]) & 0xff))); } } @@ -744,9 +747,9 @@ public final class TIFFImageWriter extends ImageWriterBase { processImageComplete(); } - + private byte normalizeBlack(int photometricInterpretation, byte data) { - if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO) { + if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO) { // Inverse values return (byte) (0xff - data & 0xff); } @@ -848,7 +851,7 @@ public final class TIFFImageWriter extends ImageWriterBase { int argIdx = 0; // TODO: Proper argument parsing: -t -c - int type = args.length > argIdx + 1 ? Integer.parseInt(args[argIdx++]) : -1; + int type = args.length > argIdx + 1 ? Integer.parseInt(args[argIdx++]) : -1; int compression = args.length > argIdx + 1 ? Integer.parseInt(args[argIdx++]) : 0; if (args.length <= argIdx) { @@ -973,7 +976,6 @@ public final class TIFFImageWriter extends ImageWriterBase { // } // writer.dispose(); - image = null; BufferedImage read = ImageIO.read(output); From e5c0fead384b18bd1f0751862d6c1a730ec5267d Mon Sep 17 00:00:00 2001 From: Schmidor Date: Sun, 11 Oct 2015 17:59:01 +0200 Subject: [PATCH 9/9] Fix BlackIsZero handling in CCITTFaxEncoderStream --- .../plugins/tiff/CCITTFaxEncoderStream.java | 10 +-- .../imageio/plugins/tiff/TIFFImageWriter.java | 14 +--- .../tiff/CCITTFaxEncoderStreamTest.java | 67 ++++++++++++++++--- 3 files changed, 65 insertions(+), 26 deletions(-) diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java index b8b3e096..05d40bc4 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java @@ -190,7 +190,7 @@ public class CCITTFaxEncoderStream extends OutputStream { int index = 0; boolean white = true; while (index < columns) { - int[] nextChanges = getNextChanges(index); + int[] nextChanges = getNextChanges(index, white); int runLength = nextChanges[0] - index; writeRun(runLength, white); index += runLength; @@ -198,10 +198,10 @@ public class CCITTFaxEncoderStream extends OutputStream { } } - private int[] getNextChanges(int pos) { + private int[] getNextChanges(int pos, boolean white) { int[] result = new int[] {columns, columns}; for (int i = 0; i < changesCurrentRowLength; i++) { - if (pos < changesCurrentRow[i]) { + if (pos < changesCurrentRow[i] || (pos == 0 && white)) { result[0] = changesCurrentRow[i]; if ((i + 1) < changesCurrentRowLength) { result[1] = changesCurrentRow[i + 1]; @@ -235,7 +235,7 @@ public class CCITTFaxEncoderStream extends OutputStream { boolean white = true; int index = 0; // a0 while (index < columns) { - int[] nextChanges = getNextChanges(index); // a1, a2 + int[] nextChanges = getNextChanges(index, white); // a1, a2 int[] nextRefs = getNextRefChanges(index, white); // b1, b2 @@ -287,7 +287,7 @@ public class CCITTFaxEncoderStream extends OutputStream { private int[] getNextRefChanges(int a0, boolean white) { int[] result = new int[] {columns, columns}; for (int i = (white ? 0 : 1); i < changesReferenceRowLength; i += 2) { - if (changesReferenceRow[i] > a0) { + if (changesReferenceRow[i] > a0 || (a0 == 0 && i == 0)) { result[0] = changesReferenceRow[i]; if ((i + 1) < changesReferenceRowLength) { result[1] = changesReferenceRow[i + 1]; 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 9fdb3dd9..a5cfa189 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 @@ -377,7 +377,7 @@ public final class TIFFImageWriter extends ImageWriterBase { else { // Write image data writeImageData(createCompressorStream(renderedImage, param, entries), renderedImage, numComponents, bandOffsets, - bitOffsets, photometric); + bitOffsets); } // Update IFD0-pointer, and write IFD @@ -587,7 +587,7 @@ public final class TIFFImageWriter extends ImageWriterBase { return shorts; } - private void writeImageData(DataOutput stream, RenderedImage renderedImage, int numComponents, int[] bandOffsets, int[] bitOffsets, int photometric) throws IOException { + private void writeImageData(DataOutput stream, RenderedImage renderedImage, int numComponents, int[] bandOffsets, int[] bitOffsets) throws IOException { // Store 3BYTE, 4BYTE as is (possibly need to re-arrange to RGB order) // Store INT_RGB as 3BYTE, INT_ARGB as 4BYTE?, INT_ABGR must be re-arranged // Store IndexColorModel as is @@ -635,7 +635,7 @@ public final class TIFFImageWriter extends ImageWriterBase { final int xOff = yOff + x * numBands; for (int s = 0; s < numBands; s++) { - buffer.put(normalizeBlack(photometric, (byte) (dataBuffer.getElem(b, xOff + bandOffsets[s]) & 0xff))); + buffer.put((byte) (dataBuffer.getElem(b, xOff + bandOffsets[s]) & 0xff)); } } @@ -748,14 +748,6 @@ public final class TIFFImageWriter extends ImageWriterBase { processImageComplete(); } - private byte normalizeBlack(int photometricInterpretation, byte data) { - if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO) { - // Inverse values - return (byte) (0xff - data & 0xff); - } - return data; - } - // TODO: Would be better to solve this on stream level... But writers would then have to explicitly flush the buffer before done. private void flushBuffer(final ByteBuffer buffer, final DataOutput stream) throws IOException { buffer.flip(); diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java index c251d687..9a6f12a2 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java @@ -29,12 +29,20 @@ package com.twelvemonkeys.imageio.plugins.tiff; import com.twelvemonkeys.imageio.plugins.tiff.CCITTFaxEncoderStream.Code; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; import java.awt.image.BufferedImage; import java.awt.image.DataBufferByte; import java.io.*; +import java.net.URL; import static org.junit.Assert.*; @@ -88,30 +96,40 @@ public class CCITTFaxEncoderStreamTest { @Test public void testType2() throws IOException { - testImage(TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1, 0L); + testStreamEncodeDecode(TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1, 0L); } @Test public void testType4() throws IOException { - testImage(TIFFExtension.COMPRESSION_CCITT_T4, 1, 0L); - testImage(TIFFExtension.COMPRESSION_CCITT_T4, 1, TIFFExtension.GROUP3OPT_FILLBITS); - testImage(TIFFExtension.COMPRESSION_CCITT_T4, 1, TIFFExtension.GROUP3OPT_2DENCODING); - testImage(TIFFExtension.COMPRESSION_CCITT_T4, 1, + testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T4, 1, 0L); + testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T4, 1, TIFFExtension.GROUP3OPT_FILLBITS); + testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T4, 1, TIFFExtension.GROUP3OPT_2DENCODING); + testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T4, 1, TIFFExtension.GROUP3OPT_FILLBITS | TIFFExtension.GROUP3OPT_2DENCODING); } @Test public void testType6() throws IOException { - testImage(TIFFExtension.COMPRESSION_CCITT_T6, 1, 0L); + testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T6, 1, 0L); } @Test - public void restReversedFillOrder() throws IOException { - testImage(TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 2, 0L); - testImage(TIFFExtension.COMPRESSION_CCITT_T6, 2, 0L); + public void testReversedFillOrder() throws IOException { + testStreamEncodeDecode(TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 2, 0L); + testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T6, 2, 0L); } - private void testImage(int type, int fillOrder, long options) throws IOException { + @Test + public void testReencodeImages() throws IOException { + testImage(getClassLoaderResource("/tiff/fivepages-scan-causingerrors.tif")); + // testImage(getClassLoaderResource("/tiff/test-single-gray-compression-type-2.tiff")); + } + + protected URL getClassLoaderResource(final String pName) { + return getClass().getResource(pName); + } + + private void testStreamEncodeDecode(int type, int fillOrder, long options) throws IOException { byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); byte[] redecodedData = new byte[imageData.length]; ByteArrayOutputStream imageOutput = new ByteArrayOutputStream(); @@ -127,4 +145,33 @@ public class CCITTFaxEncoderStreamTest { assertArrayEquals(imageData, redecodedData); } + + private void testImage(URL imageUrl) throws IOException { + ImageInputStream iis = ImageIO.createImageInputStream(imageUrl.openStream()); + ImageReader reader = ImageIO.getImageReadersByFormatName("TIFF").next(); + reader.setInput(iis, true); + + ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(); + ImageWriter writer = ImageIO.getImageWritersByFormatName("TIFF").next(); + ImageOutputStream output = ImageIO.createImageOutputStream(outputBuffer); + writer.setOutput(output); + BufferedImage originalImage = reader.read(0); + + IIOImage outputImage = new IIOImage(originalImage, null, reader.getImageMetadata(0)); + writer.write(outputImage); + + FileOutputStream stream = new FileOutputStream("H:\\tmp\\test.tif"); + try { + stream.write(outputBuffer.toByteArray()); + } + finally { + stream.close(); + } + + BufferedImage reencodedImage = ImageIO.read(new ByteArrayInputStream(outputBuffer.toByteArray())); + byte[] reencodedData = ((DataBufferByte) reencodedImage.getData().getDataBuffer()).getData(); + + Assert.assertArrayEquals(((DataBufferByte) originalImage.getData().getDataBuffer()).getData(), + reencodedData); + } }