diff --git a/src/main/java/mil/nga/tiff/compression/LZWCompression.java b/src/main/java/mil/nga/tiff/compression/LZWCompression.java index a75e2ec..30bdd62 100644 --- a/src/main/java/mil/nga/tiff/compression/LZWCompression.java +++ b/src/main/java/mil/nga/tiff/compression/LZWCompression.java @@ -2,10 +2,6 @@ import java.io.ByteArrayOutputStream; import java.nio.ByteOrder; -import java.util.HashMap; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; import mil.nga.tiff.io.ByteReader; import mil.nga.tiff.util.TiffException; @@ -17,12 +13,6 @@ */ public class LZWCompression implements CompressionDecoder, CompressionEncoder { - /** - * Logger - */ - private static final Logger logger = Logger.getLogger(LZWCompression.class - .getName()); - /** * Clear code */ @@ -38,10 +28,15 @@ public class LZWCompression implements CompressionDecoder, CompressionEncoder { */ private static final int MIN_BITS = 9; + /** + * Max bits + */ + private static final int MAX_BITS = 12; + /** * Table entries */ - private Map table = new HashMap<>(); + private byte[][] table; /** * Current max table code @@ -52,11 +47,21 @@ public class LZWCompression implements CompressionDecoder, CompressionEncoder { * Current byte length */ private int byteLength; + + /** + * The next code to cause a bit size increase + */ + private int nextBitSizeIncrease; + + /** + * Any remaining code info from previous code reads. + */ + private int codeRemainder; /** - * Current byte compression position + * The number of bits stored in codeRemainder */ - private int position; + private int numBitsInCodeRemainer; /** * {@inheritDoc} @@ -70,8 +75,9 @@ public byte[] decode(byte[] bytes, ByteOrder byteOrder) { // Initialize the table, starting position, and old code initializeTable(); - position = 0; - int oldCode = 0; + codeRemainder = 0; + numBitsInCodeRemainer = 0; + byte[] oldValue = null; // Read codes until end of input int code = getNextCode(reader); @@ -97,35 +103,33 @@ public byte[] decode(byte[] bytes, ByteOrder byteOrder) { } // Write the code value - Integer[] value = table.get(code); - writeValue(decodedStream, value); - oldCode = code; + byte[] value = table[code]; + decodedStream.writeBytes(value); + oldValue = value; } else { // If already in the table - Integer[] value = table.get(code); + byte[] value = table[code]; if (value != null) { // Write the code value - writeValue(decodedStream, value); + decodedStream.writeBytes(value); // Create new value and add to table - Integer[] newValue = concat(table.get(oldCode), - table.get(code)[0]); + byte[] newValue = combine(oldValue, value); addToTable(newValue); - oldCode = code; + oldValue = value; } else { // Create and write new value from old value - Integer[] oldValue = table.get(oldCode); - Integer[] newValue = concat(oldValue, oldValue[0]); - writeValue(decodedStream, newValue); + byte[] newValue = combine(oldValue, oldValue); + decodedStream.writeBytes(newValue); // Write value to the table - addToTable(code, newValue); - oldCode = code; + addToTable(newValue); + oldValue = newValue; } } @@ -142,11 +146,12 @@ public byte[] decode(byte[] bytes, ByteOrder byteOrder) { * Initialize the table and byte length */ private void initializeTable() { - table.clear(); - for (int i = 0; i <= 257; i++) { - table.put(i, new Integer[] { i }); + table = new byte[ 2<<(MAX_BITS-1) ][];//size is 4096 + for (int i = 0; i < 256; i++) { + table[i] = new byte[]{ (byte)i }; } maxCode = 257; + nextBitSizeIncrease = (2<<(MIN_BITS-1)) - 2;//510 byteLength = MIN_BITS; } @@ -154,9 +159,14 @@ private void initializeTable() { * Check the byte length and increase if needed */ private void checkByteLength() { - if (maxCode >= Math.pow(2, byteLength) - 2) { - byteLength++; - } + if( maxCode == nextBitSizeIncrease ) + { + nextBitSizeIncrease = nextBitSizeIncrease*2 + 2; + if( byteLength < MAX_BITS ) + { + byteLength++; + } + } } /** @@ -165,26 +175,13 @@ private void checkByteLength() { * @param value * value */ - private void addToTable(Integer[] value) { - addToTable(maxCode + 1, value); - } - - /** - * Add the code and value to the table - * - * @param code - * code - * @param value - * value - */ - private void addToTable(int code, Integer[] value) { - table.put(code, value); - maxCode = Math.max(maxCode, code); + private void addToTable(byte[] value) { + table[++maxCode] = value; checkByteLength(); } /** - * Concatenate the two values + * Combines the two values such that the result is the concatenation of the first value and the first element in the second value. * * @param first * first value @@ -192,40 +189,13 @@ private void addToTable(int code, Integer[] value) { * second value * @return concatenated value */ - private Integer[] concat(Integer[] first, Integer second) { - return concat(first, new Integer[] { second }); - } - - /** - * Concatenate the two values - * - * @param first - * first value - * @param second - * second value - * @return concatenated value - */ - private Integer[] concat(Integer[] first, Integer[] second) { - Integer[] combined = new Integer[first.length + second.length]; + private byte[] combine(byte[] first, byte[] second) { + byte[] combined = new byte[first.length + 1]; System.arraycopy(first, 0, combined, 0, first.length); - System.arraycopy(second, 0, combined, first.length, second.length); + combined[first.length] = second[0]; return combined; } - /** - * Write the value to the decoded stream - * - * @param decodedStream - * decoded byte stream - * @param value - * value - */ - private void writeValue(ByteArrayOutputStream decodedStream, Integer[] value) { - for (int i = 0; i < value.length; i++) { - decodedStream.write(value[i]); - } - } - /** * Get the next code * @@ -234,47 +204,19 @@ private void writeValue(ByteArrayOutputStream decodedStream, Integer[] value) { * @return code */ private int getNextCode(ByteReader reader) { - int nextByte = getByte(reader); - position += byteLength; - return nextByte; - } - - /** - * Get the next byte - * - * @param reader - * byte reader - * @return byte - */ - private int getByte(ByteReader reader) { - - int d = position % 8; - int a = (int) Math.floor(position / 8.0); - int de = 8 - d; - int ef = (position + byteLength) - ((a + 1) * 8); - int fg = 8 * (a + 2) - (position + byteLength); - int dg = (a + 2) * 8 - position; - fg = Math.max(0, fg); - if (a >= reader.byteLength()) { - logger.log(Level.WARNING, - "End of data reached without an end of input code"); - return EOI_CODE; - } - int chunk1 = ((int) reader.readUnsignedByte(a)) - & ((int) (Math.pow(2, 8 - d) - 1)); - chunk1 = chunk1 << (byteLength - de); - int chunks = chunk1; - if (a + 1 < reader.byteLength()) { - int chunk2 = reader.readUnsignedByte(a + 1) >>> fg; - chunk2 = chunk2 << Math.max(0, byteLength - dg); - chunks += chunk2; - } - if (ef > 8 && a + 2 < reader.byteLength()) { - int hi = (a + 3) * 8 - (position + byteLength); - int chunk3 = reader.readUnsignedByte(a + 2) >>> hi; - chunks += chunk3; - } - return chunks; + while( numBitsInCodeRemainer < byteLength ) { + if( !reader.hasByte() ) { + return EOI_CODE; + } + codeRemainder = (codeRemainder<<8) | reader.readUnsignedByte(); + numBitsInCodeRemainer += 8; + } + + numBitsInCodeRemainer -= byteLength; + int code = codeRemainder >>> numBitsInCodeRemainer; + codeRemainder ^= code << numBitsInCodeRemainer; + + return code; } /** diff --git a/src/test/java/mil/nga/tiff/TiffReadTest.java b/src/test/java/mil/nga/tiff/TiffReadTest.java index 29ec115..2f1ccc5 100644 --- a/src/test/java/mil/nga/tiff/TiffReadTest.java +++ b/src/test/java/mil/nga/tiff/TiffReadTest.java @@ -1,5 +1,6 @@ package mil.nga.tiff; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import java.io.File; @@ -8,6 +9,7 @@ import org.junit.Test; import junit.framework.TestCase; +import mil.nga.tiff.util.TiffConstants; import mil.nga.tiff.util.TiffException; /** @@ -373,4 +375,25 @@ public void testFloat32VsLZWPredictorFloatingPoint() throws IOException { } + /** + * Test the TIFF file where the LZW bit size requires the cap of 12. + * + * @throws IOException + * upon error + */ + @Test + public void testLzw12BitMax() throws IOException { + + File lzw12BitMaxFile = TiffTestUtils + .getTestFile(TiffTestConstants.FILE_LZW_12_BIT_MAX); + TIFFImage image = TiffReader.readTiff(lzw12BitMaxFile); + assertEquals( 1, image.getFileDirectories().size() ); + FileDirectory dir = image.getFileDirectory(); + assertEquals( TiffConstants.COMPRESSION_LZW, dir.getCompression().intValue() ); + assertEquals( 555, dir.getImageWidth().intValue() ); + assertEquals( 555, dir.getImageHeight().intValue() ); + + dir.readRasters();//this will throw a parsing exception if invalid + } + } diff --git a/src/test/java/mil/nga/tiff/TiffTestConstants.java b/src/test/java/mil/nga/tiff/TiffTestConstants.java index 710c549..2684fad 100644 --- a/src/test/java/mil/nga/tiff/TiffTestConstants.java +++ b/src/test/java/mil/nga/tiff/TiffTestConstants.java @@ -92,4 +92,9 @@ public class TiffTestConstants { */ public static final String FILE_LZW_PREDICTOR_FLOATING = "lzw_predictor_floating.tiff"; + /** + * LZW 12 bit max TIFF test file + */ + public static final String FILE_LZW_12_BIT_MAX = "lzw12bitmax.tiff"; + } diff --git a/src/test/resources/lzw12bitmax.tiff b/src/test/resources/lzw12bitmax.tiff new file mode 100644 index 0000000..f51f2d1 Binary files /dev/null and b/src/test/resources/lzw12bitmax.tiff differ