TMI-TIFF: Now uses subclasses instead of if-branching for LZW compatibility decoding.

This commit is contained in:
Harald Kuhr 2013-01-29 21:24:51 +01:00
parent 59b91918e0
commit dd849aeea6
3 changed files with 112 additions and 82 deletions

View File

@ -42,9 +42,7 @@ import java.util.Arrays;
* @author last modified by $Author: haraldk$ * @author last modified by $Author: haraldk$
* @version $Id: LZWDecoder.java,v 1.0 08.05.12 21:11 haraldk Exp$ * @version $Id: LZWDecoder.java,v 1.0 08.05.12 21:11 haraldk Exp$
*/ */
final class LZWDecoder implements Decoder { abstract class LZWDecoder implements Decoder {
// TODO: Break out compatibility handling to subclass, to avoid code branching?
/** Clear: Re-initialize tables. */ /** Clear: Re-initialize tables. */
static final int CLEAR_CODE = 256; static final int CLEAR_CODE = 256;
/** End of Information. */ /** End of Information. */
@ -53,23 +51,25 @@ final class LZWDecoder implements Decoder {
private static final int MIN_BITS = 9; private static final int MIN_BITS = 9;
private static final int MAX_BITS = 12; private static final int MAX_BITS = 12;
private final boolean reverseBitOrder; private final boolean compatibilityMode;
// TODO: Consider speeding things up with a "string" type (instead of the inner byte[]), private final byte[][] table;
// that uses variable size/dynamic allocation, to avoid the excessive array copying?
// private final byte[][] table = new byte[4096][0]; // libTiff adds another 1024 "for compatibility"...
private final byte[][] table = new byte[4096 + 1024][0]; // libTiff adds another 1024 "for compatibility"...
// private final Entry[] tableToo = new Entry[4096 + 1024]; // private final Entry[] tableToo = new Entry[4096 + 1024];
private int tableLength; private int tableLength;
private int bitsPerCode; int bitsPerCode;
private int oldCode = CLEAR_CODE; private int oldCode = CLEAR_CODE;
private int maxCode; private int maxCode;
private int bitMask; int bitMask;
private int maxString; private int maxString;
private boolean eofReached; boolean eofReached;
int nextData;
int nextBits;
LZWDecoder(final boolean reverseBitOrder) {
this.reverseBitOrder = reverseBitOrder; protected LZWDecoder(final boolean compatibilityMode) {
this.compatibilityMode = compatibilityMode;
table = new byte[compatibilityMode ? 4096 + 1024 : 4096][0]; // libTiff adds another 1024 "for compatibility"...
for (int i = 0; i < 256; i++) { for (int i = 0; i < 256; i++) {
table[i] = new byte[] {(byte) i}; table[i] = new byte[] {(byte) i};
@ -82,19 +82,15 @@ final class LZWDecoder implements Decoder {
init(); init();
} }
LZWDecoder() { private static int bitmaskFor(final int bits) {
this(false);
}
private static int maxCodeFor(final int bits) {
return (1 << bits) - 1; return (1 << bits) - 1;
} }
private void init() { private void init() {
tableLength = 258; tableLength = 258;
bitsPerCode = MIN_BITS; bitsPerCode = MIN_BITS;
bitMask = maxCodeFor(bitsPerCode); bitMask = bitmaskFor(bitsPerCode);
maxCode = reverseBitOrder ? bitMask : bitMask - 1; maxCode = maxCode();
maxString = 1; maxString = 1;
} }
@ -116,14 +112,6 @@ final class LZWDecoder implements Decoder {
bufferPos += writeString(table[code], buffer, bufferPos); bufferPos += writeString(table[code], buffer, bufferPos);
} }
else { else {
if (code > tableLength + 1 || oldCode >= tableLength) {
// TODO: FixMe for old, borked streams
System.err.println("code: " + code);
System.err.println("oldCode: " + oldCode);
System.err.println("tableLength: " + tableLength);
throw new DecodeException("Corrupted LZW table");
}
if (isInTable(code)) { if (isInTable(code)) {
bufferPos += writeString(table[code], buffer, bufferPos); bufferPos += writeString(table[code], buffer, bufferPos);
addStringToTable(concatenate(table[oldCode], table[code][0])); addStringToTable(concatenate(table[oldCode], table[code][0]));
@ -161,7 +149,7 @@ final class LZWDecoder implements Decoder {
bitsPerCode++; bitsPerCode++;
if (bitsPerCode > MAX_BITS) { if (bitsPerCode > MAX_BITS) {
if (reverseBitOrder) { if (compatibilityMode) {
bitsPerCode--; bitsPerCode--;
} }
else { else {
@ -169,8 +157,8 @@ final class LZWDecoder implements Decoder {
} }
} }
bitMask = maxCodeFor(bitsPerCode); bitMask = bitmaskFor(bitsPerCode);
maxCode = reverseBitOrder ? bitMask : bitMask - 1; maxCode = maxCode();
} }
if (string.length > maxString) { if (string.length > maxString) {
@ -178,6 +166,8 @@ final class LZWDecoder implements Decoder {
} }
} }
protected abstract int maxCode();
private static int writeString(final byte[] string, final byte[] buffer, final int bufferPos) { private static int writeString(final byte[] string, final byte[] buffer, final int bufferPos) {
if (string.length == 0) { if (string.length == 0) {
return 0; return 0;
@ -198,10 +188,38 @@ final class LZWDecoder implements Decoder {
return code < tableLength; return code < tableLength;
} }
protected abstract int getNextCode(final InputStream stream) throws IOException;
int nextData, nextBits;
private int getNextCode(final InputStream stream) throws IOException { static boolean isOldBitReversedStream(final InputStream stream) throws IOException {
stream.mark(2);
try {
int one = stream.read();
int two = stream.read();
return one == 0 && (two & 0x1) == 1; // => (reversed) 1 00000000 == 256 (CLEAR_CODE)
}
finally {
stream.reset();
}
}
public static LZWDecoder create(boolean oldBitReversedStream) {
return oldBitReversedStream ? new LZWCompatibilityDecoder() : new LZWSpecDecoder();
}
private static final class LZWSpecDecoder extends LZWDecoder {
protected LZWSpecDecoder() {
super(false);
}
@Override
protected int maxCode() {
return bitMask - 1;
}
protected final int getNextCode(final InputStream stream) throws IOException {
if (eofReached) { if (eofReached) {
return EOI_CODE; return EOI_CODE;
} }
@ -213,12 +231,55 @@ final class LZWDecoder implements Decoder {
return EOI_CODE; return EOI_CODE;
} }
if (reverseBitOrder) { nextData = (nextData << 8) | read;
nextBits += 8;
if (nextBits < bitsPerCode) {
read = stream.read();
if (read < 0) {
eofReached = true;
return EOI_CODE;
}
nextData = (nextData << 8) | read;
nextBits += 8;
}
code = ((nextData >> (nextBits - bitsPerCode)) & bitMask);
nextBits -= bitsPerCode;
return code;
}
}
private static final class LZWCompatibilityDecoder extends LZWDecoder {
// NOTE: This is a spec violation. However, libTiff reads such files. // NOTE: This is a spec violation. However, libTiff reads such files.
// TIFF 6.0 Specification, Section 13: "LZW Compression"/"The Algorithm", page 61, says: // TIFF 6.0 Specification, Section 13: "LZW Compression"/"The Algorithm", page 61, says:
// "LZW compression codes are stored into bytes in high-to-low-order fashion, i.e., FillOrder // "LZW compression codes are stored into bytes in high-to-low-order fashion, i.e., FillOrder
// is assumed to be 1. The compressed codes are written as bytes (not words) so that the // is assumed to be 1. The compressed codes are written as bytes (not words) so that the
// compressed data will be identical whether it is an II or MM file." // compressed data will be identical whether it is an II or MM file."
protected LZWCompatibilityDecoder() {
super(true);
}
@Override
protected int maxCode() {
return bitMask;
}
protected final int getNextCode(final InputStream stream) throws IOException {
if (eofReached) {
return EOI_CODE;
}
int code;
int read = stream.read();
if (read < 0) {
eofReached = true;
return EOI_CODE;
}
nextData |= read << nextBits; nextData |= read << nextBits;
nextBits += 8; nextBits += 8;
@ -236,40 +297,9 @@ final class LZWDecoder implements Decoder {
code = (nextData & bitMask); code = (nextData & bitMask);
nextData >>= bitsPerCode; nextData >>= bitsPerCode;
nextBits -= bitsPerCode; nextBits -= bitsPerCode;
}
else {
nextData = (nextData << 8) | read;
nextBits += 8;
if (nextBits < bitsPerCode) {
read = stream.read();
if (read < 0) {
eofReached = true;
return EOI_CODE;
}
nextData = (nextData << 8) | read;
nextBits += 8;
}
code = ((nextData >> (nextBits - bitsPerCode)) & bitMask);
nextBits -= bitsPerCode;
}
return code; return code;
} }
static boolean isOldBitReversedStream(final InputStream stream) throws IOException {
stream.mark(2);
try {
int one = stream.read();
int two = stream.read();
return one == 0 && (two & 0x1) == 1; // => (reversed) 1 00000000 == 256 (CLEAR_CODE)
}
finally {
stream.reset();
}
} }
private class Entry { private class Entry {

View File

@ -864,7 +864,7 @@ public class TIFFImageReader extends ImageReaderBase {
case TIFFBaseline.COMPRESSION_PACKBITS: case TIFFBaseline.COMPRESSION_PACKBITS:
return new DecoderStream(stream, new PackBitsDecoder(), 1024); return new DecoderStream(stream, new PackBitsDecoder(), 1024);
case TIFFExtension.COMPRESSION_LZW: case TIFFExtension.COMPRESSION_LZW:
return new DecoderStream(stream, new LZWDecoder(LZWDecoder.isOldBitReversedStream(stream)), 1024); return new DecoderStream(stream, LZWDecoder.create(LZWDecoder.isOldBitReversedStream(stream)), 1024);
case TIFFExtension.COMPRESSION_ZLIB: case TIFFExtension.COMPRESSION_ZLIB:
case TIFFExtension.COMPRESSION_DEFLATE: case TIFFExtension.COMPRESSION_DEFLATE:
// TIFFphotoshop.pdf (aka TIFF specification, supplement 2) says ZLIB (8) and DEFLATE (32946) algorithms are identical // TIFFphotoshop.pdf (aka TIFF specification, supplement 2) says ZLIB (8) and DEFLATE (32946) algorithms are identical

View File

@ -59,7 +59,7 @@ public class LZWDecoderTest extends DecoderAbstractTestCase {
@Test @Test
public void testShortBitReversedStream() throws IOException { public void testShortBitReversedStream() throws IOException {
InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-short.bin"), new LZWDecoder(true), 128); InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-short.bin"), LZWDecoder.create(true), 128);
InputStream unpacked = new ByteArrayInputStream(new byte[512 * 3 * 5]); // Should be all 0's InputStream unpacked = new ByteArrayInputStream(new byte[512 * 3 * 5]); // Should be all 0's
assertSameStreamContents(unpacked, stream); assertSameStreamContents(unpacked, stream);
@ -67,7 +67,7 @@ public class LZWDecoderTest extends DecoderAbstractTestCase {
@Test @Test
public void testLongStream() throws IOException { public void testLongStream() throws IOException {
InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-long.bin"), new LZWDecoder(), 1024); InputStream stream = new DecoderStream(getClass().getResourceAsStream("/lzw/lzw-long.bin"), LZWDecoder.create(false), 1024);
InputStream unpacked = getClass().getResourceAsStream("/lzw/unpacked-long.bin"); InputStream unpacked = getClass().getResourceAsStream("/lzw/unpacked-long.bin");
assertSameStreamContents(unpacked, stream); assertSameStreamContents(unpacked, stream);
@ -101,7 +101,7 @@ public class LZWDecoderTest extends DecoderAbstractTestCase {
@Override @Override
public Decoder createDecoder() { public Decoder createDecoder() {
return new LZWDecoder(); return LZWDecoder.create(false);
} }
@Override @Override