CITT Group 3/4 Support - Pass Mode / 2D Change referencing in work

This commit is contained in:
Schmidor 2015-07-01 01:10:56 +02:00
parent 1a43958aeb
commit a2042e75bf
5 changed files with 687 additions and 448 deletions

View File

@ -139,6 +139,9 @@ public interface TIFF {
// "Old-style" JPEG (still used as EXIF thumbnail) // "Old-style" JPEG (still used as EXIF thumbnail)
int TAG_JPEG_INTERCHANGE_FORMAT = 513; int TAG_JPEG_INTERCHANGE_FORMAT = 513;
int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514; int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514;
int TAG_GROUP3OPTIONS = 292;
int TAG_GROUP4OPTIONS = 293;
/// C. Tags relating to image data characteristics /// C. Tags relating to image data characteristics

View File

@ -28,426 +28,623 @@
package com.twelvemonkeys.imageio.plugins.tiff; package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.lang.Validate;
import java.io.EOFException; import java.io.EOFException;
import java.io.FilterInputStream; import java.io.FilterInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import com.twelvemonkeys.lang.Validate;
/** /**
* CCITT Modified Huffman RLE<!--, and hopefully soon: Group 3 (T4) and Group 4 (T6) fax compression-->. * CCITT Modified Huffman RLE, Group 3 (T4) and Group 4 (T6) fax compression.
* *
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a> * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$ * @author last modified by $Author: haraldk$
* @version $Id: CCITTFaxDecoderStream.java,v 1.0 23.05.12 15:55 haraldk Exp$ * @version $Id: CCITTFaxDecoderStream.java,v 1.0 23.05.12 15:55 haraldk Exp$
*/ */
final class CCITTFaxDecoderStream extends FilterInputStream { final class CCITTFaxDecoderStream extends FilterInputStream {
// See TIFF 6.0 Specification, Section 10: "Modified Huffman Compression", page 43. // See TIFF 6.0 Specification, Section 10: "Modified Huffman Compression",
// page 43.
private final int columns; private final int columns;
private final byte[] decodedRow; private final byte[] decodedRow;
private int decodedLength; private int decodedLength;
private int decodedPos; private int decodedPos;
private int bitBuffer; // Need to take fill order into account (?) (use flip table?)
private int bitBufferLength; private final int fillOrder;
private final int type;
// Need to take fill order into account (?) (use flip table?) private final int[] changesReferenceRow;
private final int fillOrder; private final int[] changesCurrentRow;
private final int type; private int changesReferenceRowCount;
private int changesCurrentRowCount;
private final int[] changes; private static final int EOL_CODE = 0x01; // 12 bit
private int changesCount;
private static final int EOL_CODE = 0x01; // 12 bit private boolean optionG32D = false;
public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, final int fillOrder) { @SuppressWarnings("unused") // Leading zeros for aligning EOL
super(Validate.notNull(stream, "stream")); private boolean optionG3Fill = false;
this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0"); private boolean optionUncompressed = false;
// We know this is only used for b/w (1 bit)
this.decodedRow = new byte[(columns + 7) / 8];
this.type = Validate.isTrue(type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, type, "Only CCITT Modified Huffman RLE compression (2) supported: %s"); // TODO: Implement group 3 and 4
this.fillOrder = Validate.isTrue(fillOrder == 1, fillOrder, "Only fill order 1 supported: %s"); // TODO: Implement fillOrder == 2
this.changes = new int[columns]; public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, final int fillOrder,
} final long options) {
super(Validate.notNull(stream, "stream"));
// IDEA: Would it be faster to keep all bit combos of each length (>=2) that is NOT a code, to find bit length, then look up value in table? this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0");
// -- If white run, start at 4 bits to determine length, if black, start at 2 bits // We know this is only used for b/w (1 bit)
this.decodedRow = new byte[(columns + 7) / 8];
this.type = type;
this.fillOrder = fillOrder;// Validate.isTrue(fillOrder == 1, fillOrder,
// "Only fill order 1 supported: %s"); //
// TODO: Implement fillOrder == 2
private void fetch() throws IOException { this.changesReferenceRow = new int[columns];
if (decodedPos >= decodedLength) { this.changesCurrentRow = new int[columns];
decodedLength = 0;
try { switch (type) {
decodeRow(); case TIFFExtension.COMPRESSION_CCITT_T4:
} optionG32D = (options & TIFFExtension.GROUP3OPT_2DENCODING) != 0;
catch (EOFException e) { optionG3Fill = (options & TIFFExtension.GROUP3OPT_FILLBITS) != 0;
// TODO: Rewrite to avoid throw/catch for normal flow... optionUncompressed = (options & TIFFExtension.GROUP3OPT_UNCOMPRESSED) != 0;
if (decodedLength != 0) { break;
throw e; case TIFFExtension.COMPRESSION_CCITT_T6:
} optionUncompressed = (options & TIFFExtension.GROUP4OPT_UNCOMPRESSED) != 0;
break;
}
// ..otherwise, just client code trying to read past the end of stream Validate.isTrue(!optionUncompressed, optionUncompressed,
decodedLength = -1; "CCITT GROUP 3/4 OPTION UNCOMPRESSED is not supported");
} }
decodedPos = 0; private void fetch() throws IOException {
} if (decodedPos >= decodedLength) {
} decodedLength = 0;
private void decodeRow() throws IOException { try {
resetBuffer(); decodeRow();
} catch (EOFException e) {
// TODO: Rewrite to avoid throw/catch for normal flow...
if (decodedLength != 0) {
throw e;
}
boolean literalRun = true; // ..otherwise, just client code trying to read past the end of
// stream
decodedLength = -1;
}
/* decodedPos = 0;
if (type == TIFFExtension.COMPRESSION_CCITT_T4) { }
int eol = readBits(12); }
System.err.println("eol: " + eol);
while (eol != EOL_CODE) {
eol = readBits(1);
System.err.println("eol: " + eol);
// throw new IOException("Missing EOL");
}
literalRun = readBits(1) == 1; private void decode1D() throws IOException {
} int index = 0;
boolean white = true;
changesCurrentRowCount = 0;
do {
int completeRun = 0;
if (white) {
completeRun = decodeRun(whiteRunTree);
} else {
completeRun = decodeRun(blackRunTree);
}
System.err.println("literalRun: " + literalRun); index += completeRun;
*/ changesCurrentRow[changesCurrentRowCount++] = index;
int index = 0; // Flip color for next run
white = !white;
} while (index < columns);
}
if (literalRun) { private void decode2D() throws IOException {
changesCount = 0; boolean white = true;
boolean white = true; int index = 0;
changesCurrentRowCount = 0;
int ref = 0;
mode: while (index < columns) {
// read mode
N n = codeTree.root;
while (true) {
n = n.walk(readBit());
if (n == null) {
continue mode;
} else if (n.isLeaf) {
switch (n.value) {
case VALUE_HMODE:
System.out.print("|H=");
int runLength = 0;
runLength = decodeRun(white ? whiteRunTree : blackRunTree);
changesCurrentRow[changesCurrentRowCount++] = index;
index += runLength;
System.out.print(runLength + (white? "W" : "B"));
runLength = decodeRun(white ? blackRunTree : whiteRunTree);
changesCurrentRow[changesCurrentRowCount++] = index;
index += runLength;
System.out.print(runLength + (!white? "W" : "B"));
break;
case VALUE_PASSMODE:
System.out.print("|P");
ref++;
// TODO
break;
default:
System.out.print("|V" + n.value);
index = changesReferenceRow[ref] + n.value;
changesCurrentRow[ref] = index;
if(changesCurrentRow[ref] <= index) ref++; //TODO
changesCurrentRowCount++;
white = !white;
break;
}
continue mode;
}
}
}
}
do { private void decodeRowType2() throws IOException {
int completeRun = 0; resetBuffer();
decode1D();
}
int run; private void decodeRowType4() throws IOException {
do { eof: while (true) {
if (white) { // read till next EOL code
run = decodeRun(WHITE_CODES, WHITE_RUN_LENGTHS, 4); N n = eolOnlyTree.root;
} while (true) {
else { N tmp = n;
run = decodeRun(BLACK_CODES, BLACK_RUN_LENGTHS, 2); n = n.walk(readBit());
} if (n == null)
continue eof;
if (n.isLeaf) {
System.out.print("|EOL");
break eof;
}
if(tmp == n) System.out.print("F");
}
}
boolean k = optionG32D ? readBit() : true;
System.out.print("|k=" + k);
if (k) {
decode1D();
changesReferenceRowCount = changesCurrentRowCount;
System.arraycopy(changesCurrentRow, 0, changesReferenceRow, 0, changesCurrentRowCount);
} else {
decode2D();
}
}
completeRun += run; private void decodeRowType6() throws IOException {
} changesReferenceRowCount = 1;
while (run >= 64); // Additional makeup codes are packed into both b/w codes, terminating codes are < 64 bytes changesReferenceRow[0] = columns;
decode2D();
}
changes[changesCount++] = index + completeRun; private void decodeRow() throws IOException {
switch (type) {
case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE:
decodeRowType2();
break;
case TIFFExtension.COMPRESSION_CCITT_T4:
decodeRowType4();
break;
case TIFFExtension.COMPRESSION_CCITT_T6:
decodeRowType6();
break;
}
int index = 0;
boolean white = true;
for (int i = 0; i <= changesCurrentRowCount; i++) {
int nextChange = columns;
if (i != changesCurrentRowCount) {
nextChange = changesCurrentRow[i];
}
// System.err.printf("%s run: %d\n", white ? "white" : "black", run); while (index % 8 != 0 && (nextChange - index) > 0) {
decodedRow[index++ / 8] |= (white ? 1 << 8 - (index % 8) : 0);
}
// TODO: Optimize with lookup for 0-7 bits? if (index % 8 == 0) {
// Fill bits to byte boundary... final byte value = (byte) (white ? 0xff : 0x00);
while (index % 8 != 0 && completeRun-- > 0) {
decodedRow[index++ / 8] |= (white ? 1 << 8 - (index % 8) : 0);
}
// ...then fill complete bytes to either 0xff or 0x00... while ((nextChange - index) > 7) {
if (index % 8 == 0) { decodedRow[index / 8] = value;
final byte value = (byte) (white ? 0xff : 0x00); index += 8;
}
}
while ((nextChange - index) > 0) {
if (index % 8 == 0)
decodedRow[(index + 1) / 8] = 0;
while (completeRun > 7) { decodedRow[index++ / 8] |= (white ? 1 << 8 - (index % 8) : 0);
decodedRow[index / 8] = value; }
completeRun -= 8;
index += 8;
}
}
// ...finally fill any remaining bits white = !white;
while (completeRun-- > 0) { }
decodedRow[index++ / 8] |= (white ? 1 << 8 - (index % 8) : 0);
}
// Flip color for next run if (index != columns) {
white = !white; throw new IOException("Sum of run-lengths does not equal scan line width: " + index + " > " + columns);
} }
while (index < columns);
}
else {
// non-literal run
}
if (type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE && index != columns) { decodedLength = (index + 7) / 8;
throw new IOException("Sum of run-lengths does not equal scan line width: " + index + " > " + columns); }
}
decodedLength = (index + 7) / 8; private int decodeRun(Tree tree) throws IOException {
} int total = 0;
private int decodeRun(short[][] codes, short[][] runLengths, int minCodeSize) throws IOException { N n = tree.root;
// TODO: Optimize... while (true) {
// Looping and comparing is the most straight-forward, but probably not the most effective way... boolean bit = readBit();
int code = readBits(minCodeSize); n = n.walk(bit);
if (n == null)
throw new IOException("Unknown code in Huffman RLE stream");
for (int bits = 0; bits < codes.length; bits++) { if (n.isLeaf) {
short[] bitCodes = codes[bits]; total += n.value;
if (n.value < 64) {
return total;
} else {
n = tree.root;
continue;
}
}
}
}
for (int i = 0; i < bitCodes.length; i++) { private void resetBuffer() {
if (bitCodes[i] == code) { for (int i = 0; i < decodedRow.length; i++) {
// System.err.println("code: " + code); decodedRow[i] = 0;
}
while (true) {
if (bufferPos == -1) {
return;
}
// Code found, return matching run length try {
return runLengths[bits][i]; boolean skip = readBit();
} } catch (IOException e) {
} // TODO Auto-generated catch block
e.printStackTrace();
}
}
}
// No code found, read one more bit and try again int buffer = -1;
code = fillOrder == 1 ? (code << 1) | readBits(1) : readBits(1) << (bits + minCodeSize) | code; int bufferPos = -1;
}
throw new IOException("Unknown code in Huffman RLE stream"); private boolean readBit() throws IOException {
} if (bufferPos < 0 || bufferPos > 7) {
buffer = in.read();
if (buffer == -1) {
throw new EOFException("Unexpected end of Huffman RLE stream");
}
bufferPos = 0;
}
private void resetBuffer() { boolean isSet = ((buffer >> (7 - bufferPos)) & 1) == 1;
for (int i = 0; i < decodedRow.length; i++) { bufferPos++;
decodedRow[i] = 0; if (bufferPos > 7)
} bufferPos = -1;
return isSet;
}
bitBuffer = 0; @Override
bitBufferLength = 0; public int read() throws IOException {
} if (decodedLength < 0) {
return -1;
}
private int readBits(int bitCount) throws IOException { if (decodedPos >= decodedLength) {
while (bitBufferLength < bitCount) { fetch();
int read = in.read();
if (read == -1) {
throw new EOFException("Unexpected end of Huffman RLE stream");
}
int bits = read & 0xff; if (decodedLength < 0) {
bitBuffer = (bitBuffer << 8) | bits; return -1;
bitBufferLength += 8; }
} }
// TODO: Take fill order into account return decodedRow[decodedPos++] & 0xff;
bitBufferLength -= bitCount; }
int result = bitBuffer >> bitBufferLength;
bitBuffer &= (1 << bitBufferLength) - 1;
return result; @Override
} public int read(byte[] b, int off, int len) throws IOException {
if (decodedLength < 0) {
return -1;
}
@Override if (decodedPos >= decodedLength) {
public int read() throws IOException { fetch();
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) { if (decodedLength < 0) {
fetch(); return -1;
}
}
if (decodedLength < 0) { int read = Math.min(decodedLength - decodedPos, len);
return -1; System.arraycopy(decodedRow, decodedPos, b, off, read);
} decodedPos += read;
}
return decodedRow[decodedPos++] & 0xff; return read;
} }
@Override @Override
public int read(byte[] b, int off, int len) throws IOException { public long skip(long n) throws IOException {
if (decodedLength < 0) { if (decodedLength < 0) {
return -1; return -1;
} }
if (decodedPos >= decodedLength) { if (decodedPos >= decodedLength) {
fetch(); fetch();
if (decodedLength < 0) { if (decodedLength < 0) {
return -1; return -1;
} }
} }
int read = Math.min(decodedLength - decodedPos, len); int skipped = (int) Math.min(decodedLength - decodedPos, n);
System.arraycopy(decodedRow, decodedPos, b, off, read); decodedPos += skipped;
decodedPos += read;
return read; return skipped;
} }
@Override @Override
public long skip(long n) throws IOException { public boolean markSupported() {
if (decodedLength < 0) { return false;
return -1; }
}
if (decodedPos >= decodedLength) { @Override
fetch(); public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
if (decodedLength < 0) { static class N {
return -1; N left;
} N right;
}
int skipped = (int) Math.min(decodedLength - decodedPos, n); int value; // > 63 non term.
decodedPos += skipped; boolean canBeFill = false;
boolean isLeaf = false;
return skipped; void set(boolean next, N node) {
} if (!next) {
left = node;
} else {
right = node;
}
}
@Override N walk(boolean next) {
public boolean markSupported() { return next ? right : left;
return false; }
}
@Override @Override
public synchronized void reset() throws IOException { public String toString() {
throw new IOException("mark/reset not supported"); return "[leaf=" + isLeaf + ", value=" + value + ", canBeFill=" + canBeFill + "]";
} }
}
static final short[][] BLACK_CODES = { static class Tree {
{ // 2 bits N root = new N();
0x2, 0x3,
},
{ // 3 bits
0x2, 0x3,
},
{ // 4 bits
0x2, 0x3,
},
{ // 5 bits
0x3,
},
{ // 6 bits
0x4, 0x5,
},
{ // 7 bits
0x4, 0x5, 0x7,
},
{ // 8 bits
0x4, 0x7,
},
{ // 9 bits
0x18,
},
{ // 10 bits
0x17, 0x18, 0x37, 0x8, 0xf,
},
{ // 11 bits
0x17, 0x18, 0x28, 0x37, 0x67, 0x68, 0x6c, 0x8, 0xc, 0xd,
},
{ // 12 bits
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, 0x24, 0x27, 0x28, 0x2b, 0x2c, 0x33,
0x34, 0x35, 0x37, 0x38, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x64, 0x65,
0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xd2, 0xd3,
0xd4, 0xd5, 0xd6, 0xd7, 0xda, 0xdb,
},
{ // 13 bits
0x4a, 0x4b, 0x4c, 0x4d, 0x52, 0x53, 0x54, 0x55, 0x5a, 0x5b, 0x64, 0x65, 0x6c, 0x6d, 0x72, 0x73,
0x74, 0x75, 0x76, 0x77,
}
};
static final short[][] BLACK_RUN_LENGTHS = {
{ // 2 bits
3, 2,
},
{ // 3 bits
1, 4,
},
{ // 4 bits
6, 5,
},
{ // 5 bits
7,
},
{ // 6 bits
9, 8,
},
{ // 7 bits
10, 11, 12,
},
{ // 8 bits
13, 14,
},
{ // 9 bits
15,
},
{ // 10 bits
16, 17, 0, 18, 64,
},
{ // 11 bits
24, 25, 23, 22, 19, 20, 21, 1792, 1856, 1920,
},
{ // 12 bits
1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, 52, 55, 56, 59, 60, 320,
384, 448, 53, 54, 50, 51, 44, 45, 46, 47, 57, 58, 61, 256, 48, 49,
62, 63, 30, 31, 32, 33, 40, 41, 128, 192, 26, 27, 28, 29, 34, 35,
36, 37, 38, 39, 42, 43,
},
{ // 13 bits
640, 704, 768, 832, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 512, 576, 896, 960,
1024, 1088, 1152, 1216,
}
};
public static final short[][] WHITE_CODES = { void fill(int depth, int path, int value) throws IOException {
{ // 4 bits N current = root;
0x7, 0x8, 0xb, 0xc, 0xe, 0xf, for (int i = 0; i < depth; i++) {
}, int bitPos = depth - 1 - i;
{ // 5 bits boolean isSet = ((path >> bitPos) & 1) == 1;
0x12, 0x13, 0x14, 0x1b, 0x7, 0x8, N next = current.walk(isSet);
}, if (next == null) {
{ // 6 bits next = new N();
0x17, 0x18, 0x2a, 0x2b, 0x3, 0x34, 0x35, 0x7, 0x8, if (i == depth - 1) {
}, next.value = value;
{ // 7 bits next.isLeaf = true;
0x13, 0x17, 0x18, 0x24, 0x27, 0x28, 0x2b, 0x3, 0x37, 0x4, 0x8, 0xc, }
}, if (path == 0)
{ // 8 bits next.canBeFill = true;
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1a, 0x1b, 0x2, 0x24, 0x25, 0x28, 0x29, 0x2a, 0x2b, 0x2c, current.set(isSet, next);
0x2d, 0x3, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x4, 0x4a, 0x4b, 0x5, 0x52, 0x53, 0x54, 0x55, } else {
0x58, 0x59, 0x5a, 0x5b, 0x64, 0x65, 0x67, 0x68, 0xa, 0xb, if (next.isLeaf)
}, throw new IOException("node is leaf, no other following");
{ // 9 bits }
0x98, 0x99, 0x9a, 0x9b, 0xcc, 0xcd, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, current = next;
}, }
{ // 10 bits }
},
{ // 11 bits
0x8, 0xc, 0xd,
},
{ // 12 bits
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f,
}
};
public static final short[][] WHITE_RUN_LENGTHS = { void fill(int depth, int path, N node) throws IOException {
{ // 4 bits N current = root;
2, 3, 4, 5, 6, 7, for (int i = 0; i < depth; i++) {
}, int bitPos = depth - 1 - i;
{ // 5 bits boolean isSet = ((path >> bitPos) & 1) == 1;
128, 8, 9, 64, 10, 11, N next = current.walk(isSet);
}, if (next == null) {
{ // 6 bits if (i == depth - 1) {
192, 1664, 16, 17, 13, 14, 15, 1, 12, next = node;
}, } else {
{ // 7 bits next = new N();
26, 21, 28, 27, 18, 24, 25, 22, 256, 23, 20, 19, }
}, if (path == 0)
{ // 8 bits next.canBeFill = true;
33, 34, 35, 36, 37, 38, 31, 32, 29, 53, 54, 39, 40, 41, 42, 43, current.set(isSet, next);
44, 30, 61, 62, 63, 0, 320, 384, 45, 59, 60, 46, 49, 50, 51, } else {
52, 55, 56, 57, 58, 448, 512, 640, 576, 47, 48, if (next.isLeaf)
}, throw new IOException("node is leaf, no other following");
{ // 9 bits }
1472, 1536, 1600, 1728, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408, current = next;
}, }
{ // 10 bits }
}, }
{ // 11 bits
1792, 1856, 1920, static final short[][] BLACK_CODES = {
}, { // 2 bits
{ // 12 bits 0x2, 0x3, },
1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, { // 3 bits
} 0x2, 0x3, },
}; { // 4 bits
0x2, 0x3, },
{ // 5 bits
0x3, },
{ // 6 bits
0x4, 0x5, },
{ // 7 bits
0x4, 0x5, 0x7, },
{ // 8 bits
0x4, 0x7, },
{ // 9 bits
0x18, },
{ // 10 bits
0x17, 0x18, 0x37, 0x8, 0xf, },
{ // 11 bits
0x17, 0x18, 0x28, 0x37, 0x67, 0x68, 0x6c, 0x8, 0xc, 0xd, },
{ // 12 bits
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, 0x24, 0x27, 0x28, 0x2b, 0x2c, 0x33,
0x34, 0x35, 0x37, 0x38, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x64, 0x65,
0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xd2, 0xd3,
0xd4, 0xd5, 0xd6, 0xd7, 0xda, 0xdb, },
{ // 13 bits
0x4a, 0x4b, 0x4c, 0x4d, 0x52, 0x53, 0x54, 0x55, 0x5a, 0x5b, 0x64, 0x65, 0x6c, 0x6d, 0x72, 0x73,
0x74, 0x75, 0x76, 0x77, } };
static final short[][] BLACK_RUN_LENGTHS = {
{ // 2 bits
3, 2, },
{ // 3 bits
1, 4, },
{ // 4 bits
6, 5, },
{ // 5 bits
7, },
{ // 6 bits
9, 8, },
{ // 7 bits
10, 11, 12, },
{ // 8 bits
13, 14, },
{ // 9 bits
15, },
{ // 10 bits
16, 17, 0, 18, 64, },
{ // 11 bits
24, 25, 23, 22, 19, 20, 21, 1792, 1856, 1920, },
{ // 12 bits
1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, 52, 55, 56, 59, 60, 320, 384, 448, 53,
54, 50, 51, 44, 45, 46, 47, 57, 58, 61, 256, 48, 49, 62, 63, 30, 31, 32, 33, 40, 41, 128, 192, 26,
27, 28, 29, 34, 35, 36, 37, 38, 39, 42, 43, },
{ // 13 bits
640, 704, 768, 832, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 512, 576, 896, 960, 1024, 1088,
1152, 1216, } };
public static final short[][] WHITE_CODES = {
{ // 4 bits
0x7, 0x8, 0xb, 0xc, 0xe, 0xf, },
{ // 5 bits
0x12, 0x13, 0x14, 0x1b, 0x7, 0x8, },
{ // 6 bits
0x17, 0x18, 0x2a, 0x2b, 0x3, 0x34, 0x35, 0x7, 0x8, },
{ // 7 bits
0x13, 0x17, 0x18, 0x24, 0x27, 0x28, 0x2b, 0x3, 0x37, 0x4, 0x8, 0xc, },
{ // 8 bits
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1a, 0x1b, 0x2, 0x24, 0x25, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d,
0x3, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x4, 0x4a, 0x4b, 0x5, 0x52, 0x53, 0x54, 0x55, 0x58, 0x59,
0x5a, 0x5b, 0x64, 0x65, 0x67, 0x68, 0xa, 0xb, },
{ // 9 bits
0x98, 0x99, 0x9a, 0x9b, 0xcc, 0xcd, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, },
{ // 10 bits
},
{ // 11 bits
0x8, 0xc, 0xd, },
{ // 12 bits
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, } };
public static final short[][] WHITE_RUN_LENGTHS = {
{ // 4 bits
2, 3, 4, 5, 6, 7, },
{ // 5 bits
128, 8, 9, 64, 10, 11, },
{ // 6 bits
192, 1664, 16, 17, 13, 14, 15, 1, 12, },
{ // 7 bits
26, 21, 28, 27, 18, 24, 25, 22, 256, 23, 20, 19, },
{ // 8 bits
33, 34, 35, 36, 37, 38, 31, 32, 29, 53, 54, 39, 40, 41, 42, 43, 44, 30, 61, 62, 63, 0, 320, 384, 45,
59, 60, 46, 49, 50, 51, 52, 55, 56, 57, 58, 448, 512, 640, 576, 47, 48, },
{ // 9
// bits
1472, 1536, 1600, 1728, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408, },
{ // 10 bits
},
{ // 11 bits
1792, 1856, 1920, },
{ // 12 bits
1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, } };
final static N EOL;
final static N FILL;
final static Tree blackRunTree;
final static Tree whiteRunTree;
final static Tree eolOnlyTree;
final static Tree codeTree;
final static int VALUE_EOL = -2000;
final static int VALUE_FILL = -1000;
final static int VALUE_PASSMODE = -3000;
final static int VALUE_HMODE = -4000;
static {
EOL = new N();
EOL.isLeaf = true;
EOL.value = VALUE_EOL;
FILL = new N();
FILL.value = VALUE_FILL;
FILL.left = FILL;
FILL.right = EOL;
eolOnlyTree = new Tree();
try {
eolOnlyTree.fill(12, 0, FILL);
eolOnlyTree.fill(12, 1, EOL);
} catch (Exception e) {
e.printStackTrace();
}
blackRunTree = new Tree();
try {
for (int i = 0; i < BLACK_CODES.length; i++) {
for (int j = 0; j < BLACK_CODES[i].length; j++) {
blackRunTree.fill(i + 2, BLACK_CODES[i][j], BLACK_RUN_LENGTHS[i][j]);
}
}
blackRunTree.fill(12, 0, FILL);
blackRunTree.fill(12, 1, EOL);
} catch (Exception e) {
e.printStackTrace();
}
whiteRunTree = new Tree();
try {
for (int i = 0; i < WHITE_CODES.length; i++) {
for (int j = 0; j < WHITE_CODES[i].length; j++) {
whiteRunTree.fill(i + 4, WHITE_CODES[i][j], WHITE_RUN_LENGTHS[i][j]);
}
}
whiteRunTree.fill(12, 0, FILL);
whiteRunTree.fill(12, 1, EOL);
} catch (Exception e) {
e.printStackTrace();
}
codeTree = new Tree();
try {
codeTree.fill(4, 1, VALUE_PASSMODE); // pass mode
codeTree.fill(3, 1, VALUE_HMODE); // H mode
codeTree.fill(1, 1, 0); // V(0)
codeTree.fill(3, 3, 1); // V_R(1)
codeTree.fill(6, 3, 2); // V_R(2)
codeTree.fill(7, 3, 3); // V_R(3)
codeTree.fill(3, 2, -1); // V_L(1)
codeTree.fill(6, 2, -2); // V_L(2)
codeTree.fill(7, 2, -3); // V_L(3)
} catch (Exception e) {
e.printStackTrace();
}
}
} }

View File

@ -91,5 +91,9 @@ interface TIFFExtension {
int ORIENTATION_RIGHTTOP = 6; int ORIENTATION_RIGHTTOP = 6;
int ORIENTATION_RIGHTBOT = 7; int ORIENTATION_RIGHTBOT = 7;
int ORIENTATION_LEFTBOT = 8; int ORIENTATION_LEFTBOT = 8;
int GROUP3OPT_2DENCODING = 1;
int GROUP3OPT_UNCOMPRESSED = 2;
int GROUP3OPT_FILLBITS = 4;
int GROUP4OPT_UNCOMPRESSED = 2;
} }

View File

@ -604,9 +604,9 @@ public class TIFFImageReader extends ImageReaderBase {
case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE: case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE:
// CCITT modified Huffman // CCITT modified Huffman
// Additionally, the specification defines these values as part of the TIFF extensions: // Additionally, the specification defines these values as part of the TIFF extensions:
// case TIFFExtension.COMPRESSION_CCITT_T4: case TIFFExtension.COMPRESSION_CCITT_T4:
// CCITT Group 3 fax encoding // CCITT Group 3 fax encoding
// case TIFFExtension.COMPRESSION_CCITT_T6: case TIFFExtension.COMPRESSION_CCITT_T6:
// CCITT Group 4 fax encoding // CCITT Group 4 fax encoding
int[] yCbCrSubsampling = null; int[] yCbCrSubsampling = null;
@ -1028,10 +1028,6 @@ public class TIFFImageReader extends ImageReaderBase {
break; break;
// Additionally, the specification defines these values as part of the TIFF extensions: // Additionally, the specification defines these values as part of the TIFF extensions:
case TIFFExtension.COMPRESSION_CCITT_T4:
// CCITT Group 3 fax encoding
case TIFFExtension.COMPRESSION_CCITT_T6:
// CCITT Group 4 fax encoding
// Known, but unsupported compression types // Known, but unsupported compression types
case TIFFCustom.COMPRESSION_NEXT: case TIFFCustom.COMPRESSION_NEXT:
@ -1320,7 +1316,7 @@ public class TIFFImageReader extends ImageReaderBase {
} }
private InputStream createDecompressorStream(final int compression, final int width, final int bands, final InputStream stream) throws IOException { private InputStream createDecompressorStream(final int compression, final int width, final int bands, final InputStream stream) throws IOException {
switch (compression) { switch (compression) {
case TIFFBaseline.COMPRESSION_NONE: case TIFFBaseline.COMPRESSION_NONE:
return stream; return stream;
case TIFFBaseline.COMPRESSION_PACKBITS: case TIFFBaseline.COMPRESSION_PACKBITS:
@ -1332,9 +1328,11 @@ public class TIFFImageReader extends ImageReaderBase {
case TIFFExtension.COMPRESSION_DEFLATE: case TIFFExtension.COMPRESSION_DEFLATE:
return new InflaterInputStream(stream, new Inflater(), 1024); return new InflaterInputStream(stream, new Inflater(), 1024);
case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE: case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE:
return new CCITTFaxDecoderStream(stream, width, compression, getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, 1),0L);
case TIFFExtension.COMPRESSION_CCITT_T4: case TIFFExtension.COMPRESSION_CCITT_T4:
return new CCITTFaxDecoderStream(stream, width, compression, getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, 1),getValueAsLongWithDefault(TIFF.TAG_GROUP3OPTIONS, 0L));
case TIFFExtension.COMPRESSION_CCITT_T6: case TIFFExtension.COMPRESSION_CCITT_T6:
return new CCITTFaxDecoderStream(stream, width, compression, getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, 1)); return new CCITTFaxDecoderStream(stream, width, compression, getValueAsIntWithDefault(TIFF.TAG_FILL_ORDER, 1),getValueAsLongWithDefault(TIFF.TAG_GROUP4OPTIONS, 0L));
default: default:
throw new IllegalArgumentException("Unsupported TIFF compression: " + compression); throw new IllegalArgumentException("Unsupported TIFF compression: " + compression);
} }

View File

@ -45,122 +45,159 @@ import static org.junit.Assert.*;
* *
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a> * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$ * @author last modified by $Author: haraldk$
* @version $Id: CCITTFaxDecoderStreamTest.java,v 1.0 09.03.13 14:44 haraldk Exp$ * @version $Id: CCITTFaxDecoderStreamTest.java,v 1.0 09.03.13 14:44 haraldk
* Exp$
*/ */
public class CCITTFaxDecoderStreamTest { public class CCITTFaxDecoderStreamTest {
// TODO: Better tests (full A4 width scan lines?) static final byte[] DATA_G3_1D = { 0x00, 0x18, // 000000000001|1000| EOL|3W|
0x4E, 0x00, // 010|0111|000000000 1B|2W|EOL
0x30, (byte) 0x9C, // 001|1000|010|0111|00 |3W|1B|2W|EOL
0x00, 0x61, // 0000000001|1000|01 |3W|1B
0x38, 0x00, // 0|0111|00000000000 |2W|EOL
(byte) 0xBE, (byte) 0xE0 }; // 1|0111|11|0111|00000 |2W|2B|2W|5F
// From http://www.mikekohn.net/file_formats/tiff.php static final byte[] DATA_G3_1D_FILL = { 0x00, 0x01, (byte) 0x84, (byte) 0xE0, 0x01, (byte) 0x84, (byte) 0xE0, 0x01,
static final byte[] DATA_TYPE_2 = { (byte) 0x84, (byte) 0xE0, 0x1, 0x7D, (byte) 0xC0 };
(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 = { static final byte[] DATA_G3_2D = {
0x00, 0x01, (byte) 0xc2, 0x70, 0x00, 0x1C, // 000000000001|1|100 EOL|1|3W
0x00, 0x01, 0x70, 0x27, 0x00, // 0|010|0111|00000000 |1B|2W|EOL
0x01, 0x17, 0x00, // 0001|0|1|1|1|00000000 |0|V|V|V|EOL
0x1C, 0x27, // 0001|1|1000|010|0111| |1|3W|1B|2W|
0x00, 0x12, // 000000000001|0|010| EOL|0|V-1|
(byte) 0xC0 }; // 1|1|000000 V|V|6F
}; static final byte[] DATA_G3_2D_FILL = { 0x00, 0x01, (byte) 0xC2, 0x70, 0x01, 0x70, 0x01, (byte) 0xC2, 0x70, 0x01,
0x2C };
static final byte[] DATA_TYPE_4 = { // EOF exception, not sure
0x26, (byte) 0xb0, 95, (byte) 0xfa, (byte) 0xc0 static final byte[] DATA_G3_2D_lsb2msb = { 0x00, 0x38, (byte) 0xE4, 0x00, (byte) 0xE8, 0x00, 0x38, (byte) 0xE4,
}; 0x00, 0x48, 0x03 };
// Image should be (6 x 4): static final byte[] DATA_G4 = {
// 1 1 1 0 1 1 x x 0x04, 0x17, // 0000 0100 0001 01|11
// 1 1 1 0 1 1 x x (byte) 0xF5, (byte) 0x80, // 1|111| 0101 1000 0000
// 1 1 1 0 1 1 x x 0x08, 0x00, // 0000 1000 0000 0000
// 1 1 0 0 1 1 x x (byte) 0x80 }; // 1000 0000
BufferedImage image; // 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
@Before // TODO: Better tests (full A4 width scan lines?)
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, 0xff000000); // 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
};
@Test static final byte[] DATA_TYPE_3 = { 0x00, 0x01, (byte) 0xc2, 0x70, // 00000000
public void testReadCountType2() throws IOException { // 00000001
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_2), 6, TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1); // 11000010
// 01110000
0x00, 0x01, 0x78, // 00000000 00000001 01111000
0x00, 0x01, 0x78, // 00000000 00000001 01110000
0x00, 0x01, 0x56, // 00000000 00000001 01010110
// 0x01, // 00000001
int count = 0; };
int read;
while ((read = stream.read()) >= 0) {
count++;
}
// Just make sure we'll have 4 bytes // 001 00110101 10 000010 1 1 1 1 1 1 1 1 1 1 010 11 (000000 padding)
assertEquals(4, count); 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)
};
// Verify that we don't return arbitrary values // Image should be (6 x 4):
assertEquals(-1, read); // 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;
@Test @Before
public void testDecodeType2() throws IOException { public void init() {
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_2), 6, TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1); 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);
}
}
byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); image.setRGB(2, 3, 0xff000000);
byte[] bytes = new byte[imageData.length]; }
new DataInputStream(stream).readFully(bytes);
// JPanel panel = new JPanel(); @Test
// panel.add(new JLabel("Expected", new BufferedImageIcon(image, 300, 300, true), JLabel.CENTER)); public void testDecodeType2() throws IOException {
// panel.add(new JLabel("Actual", new BufferedImageIcon(new BufferedImage(image.getColorModel(), Raster.createPackedRaster(new DataBufferByte(bytes, bytes.length), 6, 4, 1, null), false, null), 300, 300, true), JLabel.CENTER)); InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_2), 6,
// JOptionPane.showConfirmDialog(null, panel); TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1, 0L);
assertArrayEquals(imageData, bytes); byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
} byte[] bytes = new byte[imageData.length];
new DataInputStream(stream).readFully(bytes);
assertArrayEquals(imageData, bytes);
}
@Test(expected = IllegalArgumentException.class) @Test
public void testDecodeType3() throws IOException { public void testDecodeType3_1D() throws IOException {
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_3), 6, TIFFExtension.COMPRESSION_CCITT_T4, 1); InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_G3_1D), 6,
TIFFExtension.COMPRESSION_CCITT_T4, 1, 0L);
byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
byte[] bytes = new byte[imageData.length]; byte[] bytes = new byte[imageData.length];
DataInputStream dataInput = new DataInputStream(stream); new DataInputStream(stream).readFully(bytes);
assertArrayEquals(imageData, bytes);
}
for (int y = 0; y < image.getHeight(); y++) { @Test
System.err.println("y: " + y); public void testDecodeType3_1D_FILL() throws IOException {
dataInput.readFully(bytes, y * image.getWidth(), image.getWidth()); InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_G3_1D_FILL), 6,
} TIFFExtension.COMPRESSION_CCITT_T4, 1, TIFFExtension.GROUP3OPT_FILLBITS);
// JPanel panel = new JPanel(); byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
// panel.add(new JLabel("Expected", new BufferedImageIcon(image, 300, 300, true), JLabel.CENTER)); byte[] bytes = new byte[imageData.length];
// panel.add(new JLabel("Actual", new BufferedImageIcon(new BufferedImage(image.getColorModel(), Raster.createPackedRaster(new DataBufferByte(bytes, bytes.length), 6, 4, 1, null), false, null), 300, 300, true), JLabel.CENTER)); new DataInputStream(stream).readFully(bytes);
// JOptionPane.showConfirmDialog(null, panel); assertArrayEquals(imageData, bytes);
}
assertArrayEquals(imageData, bytes); @Test
} public void testDecodeType3_2D() throws IOException {
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_G3_2D), 6,
TIFFExtension.COMPRESSION_CCITT_T4, 1, TIFFExtension.GROUP3OPT_2DENCODING);
@Test(expected = IllegalArgumentException.class) byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
public void testDecodeType4() throws IOException { byte[] bytes = new byte[imageData.length];
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_TYPE_4), 6, TIFFExtension.COMPRESSION_CCITT_T6, 1); new DataInputStream(stream).readFully(bytes);
assertArrayEquals(imageData, bytes);
}
byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); @Test
byte[] bytes = new byte[imageData.length]; public void testDecodeType3_2D_FILL() throws IOException {
DataInputStream dataInput = new DataInputStream(stream); InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(DATA_G3_2D_FILL), 6,
TIFFExtension.COMPRESSION_CCITT_T4, 1,
TIFFExtension.GROUP3OPT_2DENCODING | TIFFExtension.GROUP3OPT_FILLBITS);
for (int y = 0; y < image.getHeight(); y++) { byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
System.err.println("y: " + y); byte[] bytes = new byte[imageData.length];
dataInput.readFully(bytes, y * image.getWidth(), image.getWidth()); 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, 1,
0L);
// JPanel panel = new JPanel(); byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
// panel.add(new JLabel("Expected", new BufferedImageIcon(image, 300, 300, true), JLabel.CENTER)); byte[] bytes = new byte[imageData.length];
// panel.add(new JLabel("Actual", new BufferedImageIcon(new BufferedImage(image.getColorModel(), Raster.createPackedRaster(new DataBufferByte(bytes, bytes.length), 6, 4, 1, null), false, null), 300, 300, true), JLabel.CENTER)); new DataInputStream(stream).readFully(bytes);
// JOptionPane.showConfirmDialog(null, panel); assertArrayEquals(imageData, bytes);
}
assertArrayEquals(imageData, bytes);
}
} }