diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/LSBBitReader.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/LSBBitReader.java index 162be55e..8eb7e101 100644 --- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/LSBBitReader.java +++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/LSBBitReader.java @@ -35,6 +35,8 @@ import javax.imageio.stream.ImageInputStream; import java.io.EOFException; import java.io.IOException; +import static com.twelvemonkeys.lang.Validate.notNull; + /** * LSBBitReader * @@ -45,18 +47,17 @@ public final class LSBBitReader { // TODO: Consider creating an ImageInputStream wrapper with the WebP implementation of readBit(s)? private final ImageInputStream imageInput; - private int bitOffset = 64; - private long streamPosition = -1; + int bitOffset = 64; + long streamPosition = -1; /** - * Pre buffers up to the next 8 Bytes in input. + * Pre-buffers up to the next 8 Bytes in input. * Contains valid bits in bits 63 to {@code bitOffset} (inclusive). - * Should always be refilled to have at least 56 valid bits (if possible) */ private long buffer; public LSBBitReader(ImageInputStream imageInput) { - this.imageInput = imageInput; + this.imageInput = notNull(imageInput); } /** @@ -89,20 +90,16 @@ public final class LSBBitReader { if (bits > 56) { throw new IllegalArgumentException("Tried peeking over 56"); } + return readBits(bits, true); } - //Driver private long readBits(int bits, boolean peek) throws IOException { if (bits <= 56) { - - /* - Could eliminate if we never read from the underlying InputStream outside this class after the object is - created - */ - long inputStreamPosition = imageInput.getStreamPosition(); - if (streamPosition != inputStreamPosition) { - //Need to reset buffer as stream was read in the meantime + // Could eliminate if we never read from the underlying InputStream + // outside this class after the object is created + if (streamPosition != imageInput.getStreamPosition()) { + // Need to reset buffer as stream was read in the meantime resetBuffer(); } @@ -110,21 +107,23 @@ public final class LSBBitReader { if (!peek) { bitOffset += bits; - refillBuffer(); + + if (bitOffset >= 8) { + refillBuffer(); + } } return ret; } else { - //FIXME Untested + // Peek always false in this case long lower = readBits(56); return (readBits(bits - 56) << (56)) | lower; } } private void refillBuffer() throws IOException { - - //Set to stream position consistent with buffered bytes + // Set to stream position consistent with buffered bytes imageInput.seek(streamPosition + 8); for (; bitOffset >= 8; bitOffset -= 8) { try { @@ -138,17 +137,16 @@ public final class LSBBitReader { return; } } - /* - Reset to guarantee stream position consistent with returned bytes - Would not need to do this seeking around when the underlying ImageInputStream is never read from outside - this class after the object is created. - */ + + // Reset to guarantee stream position consistent with returned bytes + // Would not need to do this seeking around when the underlying ImageInputStream is never read from outside + // this class after the object is created. imageInput.seek(streamPosition); } private void resetBuffer() throws IOException { - long inputStreamPosition = imageInput.getStreamPosition(); + try { buffer = imageInput.readLong(); bitOffset = 0; @@ -156,7 +154,7 @@ public final class LSBBitReader { imageInput.seek(inputStreamPosition); } catch (EOFException e) { - //Retry byte by byte + // Retry byte by byte streamPosition = inputStreamPosition - 8; bitOffset = 64; refillBuffer(); @@ -164,7 +162,7 @@ public final class LSBBitReader { } - //Left for backwards compatibility / Compatibility with ImageInputStream interface + // Left for backwards compatibility / Compatibility with ImageInputStream interface public int readBit() throws IOException { return (int) readBits(1); } diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java index dca166b5..fb4ab4c2 100644 --- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java +++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java @@ -96,7 +96,9 @@ final class WebPImageReader extends ImageReaderBase { public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { super.setInput(input, seekForwardOnly, ignoreMetadata); - lsbBitReader = new LSBBitReader(imageInput); + if (imageInput != null) { + lsbBitReader = new LSBBitReader(imageInput); + } } private void readHeader(int imageIndex) throws IOException { @@ -272,19 +274,19 @@ final class WebPImageReader extends ImageReaderBase { } // RsV|I|L|E|X|A|R - int reserved = (int) imageInput.readBits(2); + int reserved = lsbBitReader.readBit(); if (reserved != 0) { // Spec says SHOULD be 0 throw new IIOException(String.format("Unexpected 'VP8X' chunk reserved value, expected 0: %d", reserved)); } - header.containsICCP = imageInput.readBit() == 1; - header.containsALPH = imageInput.readBit() == 1; // L -> aLpha - header.containsEXIF = imageInput.readBit() == 1; - header.containsXMP_ = imageInput.readBit() == 1; - header.containsANIM = imageInput.readBit() == 1; // A -> Anim + header.containsANIM = lsbBitReader.readBit() == 1; // A -> Anim + header.containsXMP_ = lsbBitReader.readBit() == 1; + header.containsEXIF = lsbBitReader.readBit() == 1; + header.containsALPH = lsbBitReader.readBit() == 1; // L -> aLpha + header.containsICCP = lsbBitReader.readBit() == 1; - reserved = (int) imageInput.readBits(25); // 1 + 24 bits reserved + reserved = (int) lsbBitReader.readBits(26); // 2 + 24 bits reserved if (reserved != 0) { // Spec says SHOULD be 0 throw new IIOException(String.format("Unexpected 'VP8X' chunk reserved value, expected 0: %d", reserved)); @@ -509,15 +511,15 @@ final class WebPImageReader extends ImageReaderBase { } private void readAlpha(BufferedImage destination, ImageReadParam param, final int width, final int height) throws IOException { - int reserved = (int) imageInput.readBits(2); + int reserved = (int) lsbBitReader.readBits(2); if (reserved != 0) { // Spec says SHOULD be 0 processWarningOccurred(String.format("Unexpected 'ALPH' chunk reserved value, expected 0: %d", reserved)); } - int preProcessing = (int) imageInput.readBits(2); - int filtering = (int) imageInput.readBits(2); - int compression = (int) imageInput.readBits(2); + int preProcessing = (int) lsbBitReader.readBits(2); + int filtering = (int) lsbBitReader.readBits(2); + int compression = (int) lsbBitReader.readBits(2); if (DEBUG) { System.out.println("preProcessing: " + preProcessing); diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/ColorIndexingTransform.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/ColorIndexingTransform.java index e92af39f..4d062b6c 100644 --- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/ColorIndexingTransform.java +++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/ColorIndexingTransform.java @@ -55,7 +55,7 @@ final class ColorIndexingTransform implements Transform { byte[] rgba = new byte[4]; for (int y = 0; y < height; y++) { - //Reversed so no used elements are overridden (in case of packing) + // Reversed so no used elements are overridden (in case of packing) for (int x = width - 1; x >= 0; x--) { int componentSize = 8 >> bits; @@ -67,7 +67,7 @@ final class ColorIndexingTransform implements Transform { int index = sample >> componentOffset & ((1 << componentSize) - 1); - //Arraycopy for 4 elements might not be beneficial + // Arraycopy for 4 elements might not be beneficial System.arraycopy(colorTable, index * 4, rgba, 0, 4); raster.setDataElements(x, y, rgba); diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/HuffmanInfo.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/HuffmanInfo.java index b0b2b7d9..24f14428 100644 --- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/HuffmanInfo.java +++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/HuffmanInfo.java @@ -37,7 +37,7 @@ import java.awt.image.*; * @author Simon Kammermeier */ final class HuffmanInfo { - public Raster huffmanMetaCodes; //Raster allows intuitive lookup by x and y + public Raster huffmanMetaCodes; // Raster allows intuitive lookup by x and y public int metaCodeBits; diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/HuffmanTable.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/HuffmanTable.java index 590b8758..a1909278 100644 --- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/HuffmanTable.java +++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/HuffmanTable.java @@ -83,7 +83,6 @@ final class HuffmanTable { * @throws IOException when reading produces an exception */ public HuffmanTable(LSBBitReader lsbBitReader, int alphabetSize) throws IOException { - boolean simpleLengthCode = lsbBitReader.readBit() == 1; if (simpleLengthCode) { @@ -104,11 +103,9 @@ final class HuffmanTable { } } else { - /* - code lengths also huffman coded - first read the "first stage" code lengths - In the following this is called the L-Code (for length code) - */ + // code lengths also huffman coded + // first read the "first stage" code lengths + // In the following this is called the L-Code (for length code) int numLCodeLengths = (int) (lsbBitReader.readBits(4) + 4); short[] lCodeLengths = new short[L_CODE_ORDER.length]; int numPosCodeLens = 0; @@ -116,16 +113,15 @@ final class HuffmanTable { for (int i = 0; i < numLCodeLengths; i++) { short len = (short) lsbBitReader.readBits(3); lCodeLengths[L_CODE_ORDER[i]] = len; + if (len > 0) { numPosCodeLens++; } - } - //Use L-Code to read the actual code lengths + // Use L-Code to read the actual code lengths short[] codeLengths = readCodeLengths(lsbBitReader, lCodeLengths, alphabetSize, numPosCodeLens); - buildFromLengths(codeLengths); } } @@ -142,25 +138,21 @@ final class HuffmanTable { buildFromLengths(codeLengths, numPosCodeLens); } - - /* - Helper methods to allow reusing in different constructors - */ - + // Helper methods to allow reusing in different constructors private void buildFromLengths(short[] codeLengths) { int numPosCodeLens = 0; + for (short codeLength : codeLengths) { if (codeLength != 0) { numPosCodeLens++; } } + buildFromLengths(codeLengths, numPosCodeLens); } private void buildFromLengths(short[] codeLengths, int numPosCodeLens) { - - //Pack code length and corresponding symbols as described above - + // Pack code length and corresponding symbols as described above int[] lengthsAndSymbols = new int[numPosCodeLens]; int index = 0; @@ -170,28 +162,25 @@ final class HuffmanTable { } } - //Special case: Only 1 code value + // Special case: Only 1 code value if (numPosCodeLens == 1) { - //Length is 0 so mask to clear length bits + // Length is 0 so mask to clear length bits Arrays.fill(level1, lengthsAndSymbols[0] & 0xffff); } - //Due to the layout of the elements this effectively first sorts by length and then symbol. + // Due to the layout of the elements this effectively first sorts by length and then symbol. Arrays.sort(lengthsAndSymbols); - /* - The next code, in the bit order it would appear on the input stream, i.e. it is reversed. - Only the lowest bits (corresponding to the bit length of the code) are considered. - Example: code 0..010 (length 2) would appear as 0..001. - */ + // The next code, in the bit order it would appear on the input stream, i.e. it is reversed. + // Only the lowest bits (corresponding to the bit length of the code) are considered. + // Example: code 0..010 (length 2) would appear as 0..001. int code = 0; - //Used for level2 lookup + // Used for level2 lookup int rootEntry = -1; int[] currentTable = null; for (int i = 0; i < lengthsAndSymbols.length; i++) { - int lengthAndSymbol = lengthsAndSymbols[i]; int length = lengthAndSymbol >>> 16; @@ -202,16 +191,15 @@ final class HuffmanTable { } } else { - //Existing level2 table not fitting + // Existing level2 table not fitting if ((code & ((1 << LEVEL1_BITS) - 1)) != rootEntry) { - /* - Figure out needed table size. - Start at current symbol and length. - Every symbol uses 1 slot at the current bit length. - Going up 1 bit in length multiplies the slots by 2. - No more open slots indicate the table size to be big enough. - */ + // Figure out needed table size. + // Start at current symbol and length. + // Every symbol uses 1 slot at the current bit length. + // Going up 1 bit in length multiplies the slots by 2. + // No more open slots indicate the table size to be big enough. int maxLength = length; + for (int j = i, openSlots = 1 << (length - LEVEL1_BITS); j < lengthsAndSymbols.length && openSlots > 0; j++, openSlots--) { @@ -230,11 +218,11 @@ final class HuffmanTable { rootEntry = code & ((1 << LEVEL1_BITS) - 1); level2.add(currentTable); - //Set root table indirection + // Set root table indirection level1[rootEntry] = (LEVEL1_BITS + level2Size) << 16 | (level2.size() - 1); } - //Add to existing (or newly generated) 2nd level table + // Add to existing (or newly generated) 2nd level table for (int j = (code >>> LEVEL1_BITS); j < currentTable.length; j += 1 << (length - LEVEL1_BITS)) { currentTable[j] = (length - LEVEL1_BITS) << 16 | (lengthAndSymbol & 0xffff); } @@ -256,12 +244,12 @@ final class HuffmanTable { private int nextCode(int code, int length) { int a = (~code) & ((1 << length) - 1); - //This will result in the highest 0-bit in the lower length bits of code set (by construction of a) - //I.e. the lowest 0-bit in the value code represents + // This will result in the highest 0-bit in the lower length bits of code set (by construction of a) + // I.e. the lowest 0-bit in the value code represents int step = Integer.highestOneBit(a); - //In the represented value this clears the consecutive 1-bits starting at bit 0 and then sets the lowest 0 bit - //This corresponds to adding 1 to the value + // In the represented value this clears the consecutive 1-bits starting at bit 0 and then sets the lowest 0 bit + // This corresponds to adding 1 to the value return (code & (step - 1)) | step; } @@ -270,7 +258,7 @@ final class HuffmanTable { HuffmanTable huffmanTable = new HuffmanTable(aCodeLengths, numPosCodeLens); - //Not sure where this comes from. Just adapted from the libwebp implementation + // Not sure where this comes from. Just adapted from the libwebp implementation int codedSymbols; if (lsbBitReader.readBit() == 1) { int maxSymbolBitLength = (int) (2 + 2 * lsbBitReader.readBits(3)); @@ -282,13 +270,13 @@ final class HuffmanTable { short[] codeLengths = new short[alphabetSize]; - //Default code for repeating + // Default code for repeating short prevLength = 8; for (int i = 0; i < alphabetSize && codedSymbols > 0; i++, codedSymbols--) { short len = huffmanTable.readSymbol(lsbBitReader); - if (len < 16) { //Literal length + if (len < 16) { // Literal length codeLengths[i] = len; if (len != 0) { prevLength = len; @@ -300,16 +288,16 @@ final class HuffmanTable { int repeatOffset; switch (len) { - case 16: //Repeat previous + case 16: // Repeat previous repeatSymbol = prevLength; extraBits = 2; repeatOffset = 3; break; - case 17: //Repeat 0 short + case 17: // Repeat 0 short extraBits = 3; repeatOffset = 3; break; - case 18: //Repeat 0 long + case 18: // Repeat 0 long extraBits = 7; repeatOffset = 11; break; @@ -319,7 +307,6 @@ final class HuffmanTable { int repeatCount = (int) (lsbBitReader.readBits(extraBits) + repeatOffset); - if (i + repeatCount > alphabetSize) { throw new IIOException( String.format( @@ -330,11 +317,9 @@ final class HuffmanTable { Arrays.fill(codeLengths, i, i + repeatCount, repeatSymbol); i += repeatCount - 1; - } } - return codeLengths; } @@ -346,21 +331,20 @@ final class HuffmanTable { * @throws IOException when the reader throws one reading a symbol */ public short readSymbol(LSBBitReader lsbBitReader) throws IOException { - int index = (int) lsbBitReader.peekBits(LEVEL1_BITS); int lengthAndSymbol = level1[index]; int length = lengthAndSymbol >>> 16; if (length > LEVEL1_BITS) { - //Lvl2 lookup - lsbBitReader.readBits(LEVEL1_BITS); //Consume bits of first level - int level2Index = (int) lsbBitReader.peekBits(length - LEVEL1_BITS); //Peek remaining required bits + // Lvl2 lookup + lsbBitReader.readBits(LEVEL1_BITS); // Consume bits of first level + int level2Index = (int) lsbBitReader.peekBits(length - LEVEL1_BITS); // Peek remaining required bits lengthAndSymbol = level2.get(lengthAndSymbol & 0xffff)[level2Index]; length = lengthAndSymbol >>> 16; } - lsbBitReader.readBits(length); //Consume bits + lsbBitReader.readBits(length); // Consume bits return (short) (lengthAndSymbol & 0xffff); } diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/PredictorTransform.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/PredictorTransform.java index 6b023b40..042042a2 100644 --- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/PredictorTransform.java +++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/PredictorTransform.java @@ -51,25 +51,23 @@ final class PredictorTransform implements Transform { @Override public void applyInverse(WritableRaster raster) { - int width = raster.getWidth(); int height = raster.getHeight(); byte[] rgba = new byte[4]; - //Handle top and left border separately + // Handle top and left border separately - //(0,0) Black (0x000000ff) predict + // (0,0) Black (0x000000ff) predict raster.getDataElements(0, 0, rgba); rgba[3] += 0xff; raster.setDataElements(0, 0, rgba); - byte[] predictor = new byte[4]; byte[] predictor2 = new byte[4]; byte[] predictor3 = new byte[4]; - //(x,0) L predict + // (x,0) L predict for (int x = 1; x < width; x++) { raster.getDataElements(x, 0, rgba); raster.getDataElements(x - 1, 0, predictor); @@ -78,7 +76,7 @@ final class PredictorTransform implements Transform { raster.setDataElements(x, 0, rgba); } - //(0,y) T predict + // (0,y) T predict for (int y = 1; y < height; y++) { raster.getDataElements(0, y, rgba); raster.getDataElements(0, y - 1, predictor); @@ -89,16 +87,14 @@ final class PredictorTransform implements Transform { for (int y = 1; y < height; y++) { for (int x = 1; x < width; x++) { - int transformType = data.getSample(x >> bits, y >> bits, 1); raster.getDataElements(x, y, rgba); - int lX = x - 1; //x for left + int lX = x - 1; // x for left + int tY = y - 1; // y for top - int tY = y - 1; //y for top - - //top right is not (x+1, tY) if last pixel in line instead (0, y) + // top right is not (x+1, tY) if last pixel in line instead (0, y) int trX = x == width - 1 ? 0 : x + 1; int trY = x == width - 1 ? y : tY; diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/TransformType.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/TransformType.java index 525a6fa8..a2d613be 100644 --- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/TransformType.java +++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/TransformType.java @@ -36,7 +36,7 @@ package com.twelvemonkeys.imageio.plugins.webp.lossless; * * @author Harald Kuhr */ -// Hmm.. Why doesn't SUBTRACT_GREEN follow the convention? +// Hmm... Why doesn't SUBTRACT_GREEN follow the convention? interface TransformType { int PREDICTOR_TRANSFORM = 0; int COLOR_TRANSFORM = 1; diff --git a/imageio/imageio-webp/src/test/java/com/twelvemonkeys/imageio/plugins/webp/LSBBitReaderTest.java b/imageio/imageio-webp/src/test/java/com/twelvemonkeys/imageio/plugins/webp/LSBBitReaderTest.java new file mode 100644 index 00000000..56191bbe --- /dev/null +++ b/imageio/imageio-webp/src/test/java/com/twelvemonkeys/imageio/plugins/webp/LSBBitReaderTest.java @@ -0,0 +1,270 @@ +package com.twelvemonkeys.imageio.plugins.webp; + +import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; + +import org.junit.Test; + +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.nio.ByteOrder; + +import static org.junit.Assert.assertEquals; + +/** + * LSBBitReaderTest. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: LSBBitReaderTest.java,v 1.0 16/10/2022 haraldk Exp$ + */ +public class LSBBitReaderTest { + @Test + public void testReadBit() throws IOException { + final LSBBitReader bitReader = createBitReader(new byte[] { + 0b00010010, 0b00100001, 0b00001000, 0b00000100, + /*TODO: Remove these, should not be needed... */ 0, 0, 0, 0 + }); + + assertEquals(0, bitReader.readBit()); + assertEquals(1, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + + assertEquals(1, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + + assertEquals(1, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + + assertEquals(0, bitReader.readBit()); + assertEquals(1, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + + assertEquals(0, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + assertEquals(1, bitReader.readBit()); + + assertEquals(0, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + + assertEquals(0, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + assertEquals(1, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + + assertEquals(0, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + assertEquals(0, bitReader.readBit()); + +// assertThrows(EOFException.class, new ThrowingRunnable() { +// @Override +// public void run() throws Throwable { +// bitReader.readBits(1); +// } +// }); + } + + @Test + public void testReadBits() throws IOException { + final LSBBitReader bitReader = createBitReader(new byte[] { + 0b00100101, 0b01000010, 0b00010000, 0b00001000, + 0b00001000, 0b00010000, 0b01000000, 0b00000000, + 0b00000010, 0b00100000, 0b00000000, 0b00000100, + 0b00000000, 0b00000001, (byte) 0b10000000, + }); + + assertEquals(1, bitReader.readBits(1)); + assertEquals(2, bitReader.readBits(2)); + assertEquals(4, bitReader.readBits(3)); + assertEquals(8, bitReader.readBits(4)); + assertEquals(16, bitReader.readBits(5)); + assertEquals(32, bitReader.readBits(6)); + assertEquals(64, bitReader.readBits(7)); + assertEquals(128, bitReader.readBits(8)); + assertEquals(256, bitReader.readBits(9)); + assertEquals(512, bitReader.readBits(10)); + assertEquals(1024, bitReader.readBits(11)); + assertEquals(2048, bitReader.readBits(12)); + assertEquals(4096, bitReader.readBits(13)); + assertEquals(8192, bitReader.readBits(14)); + assertEquals(16384, bitReader.readBits(15)); + +// assertThrows(EOFException.class, new ThrowingRunnable() { +// @Override +// public void run() throws Throwable { +// bitReader.readBits(1); +// } +// }); + } + + @Test + public void testPeekBits() throws IOException { + final LSBBitReader bitReader = createBitReader(new byte[] { + 0b00100101, 0b01000010, 0b00010000, 0b00001000, + 0b00001000, 0b00010000, 0b01000000, 0b00000000, + 0b00000010, 0b00100000, 0b00000000, 0b00000100, + 0b00000000, 0b00000001, (byte) 0b10000000 + }); + + assertEquals(1, bitReader.peekBits(1)); + assertEquals(1, bitReader.peekBits(1)); + assertEquals(1, bitReader.readBits(1)); + + assertEquals(2, bitReader.peekBits(2)); + assertEquals(2, bitReader.readBits(2)); + + assertEquals(4, bitReader.readBits(3)); + + assertEquals(8, bitReader.peekBits(4)); + assertEquals(8, bitReader.readBits(4)); + + assertEquals(16, bitReader.peekBits(5)); + assertEquals(16, bitReader.peekBits(5)); + assertEquals(16, bitReader.readBits(5)); + + assertEquals(32, bitReader.peekBits(6)); + assertEquals(32, bitReader.readBits(6)); + + assertEquals(64, bitReader.peekBits(7)); + assertEquals(64, bitReader.peekBits(7)); + assertEquals(64, bitReader.peekBits(7)); + assertEquals(64, bitReader.peekBits(7)); + assertEquals(64, bitReader.readBits(7)); + + assertEquals(128, bitReader.peekBits(8)); + assertEquals(128, bitReader.readBits(8)); + + assertEquals(256, bitReader.readBits(9)); + + assertEquals(512, bitReader.peekBits(10)); + assertEquals(512, bitReader.readBits(10)); + + assertEquals(1024, bitReader.peekBits(11)); + assertEquals(1024, bitReader.readBits(11)); + + assertEquals(2048, bitReader.peekBits(12)); + assertEquals(2048, bitReader.peekBits(12)); + assertEquals(2048, bitReader.peekBits(12)); + assertEquals(2048, bitReader.readBits(12)); + + assertEquals(4096, bitReader.peekBits(13)); + assertEquals(4096, bitReader.readBits(13)); + + assertEquals(8192, bitReader.readBits(14)); + + assertEquals(16384, bitReader.peekBits(15)); + assertEquals(16384, bitReader.peekBits(15)); + assertEquals(16384, bitReader.readBits(15)); + +// assertThrows(EOFException.class, new ThrowingRunnable() { +// @Override +// public void run() throws Throwable { +// bitReader.readBits(1); +// } +// }); + } + + @Test + public void testReadBetweenBits() throws IOException { + ImageInputStream stream = createStream(new byte[] { + 0b00100101, 0b01000010, 0b00010000, 0b00001000, + 0b00001000, 0b00010000, 0b01000000, 0b00000000, + 0b00000010, 0b00100000, 0b00000000, 0b00000100, + 0b00000000, 0b00000001, (byte) 0b10000000 + }); + final LSBBitReader bitReader = new LSBBitReader(stream); + + assertEquals(1, bitReader.peekBits(1)); + assertEquals(1, bitReader.peekBits(1)); + assertEquals(1, bitReader.readBits(1)); + + assertEquals(2, bitReader.peekBits(2)); + assertEquals(2, bitReader.readBits(2)); + + assertEquals(4, bitReader.readBits(3)); + + // We've read 6 bits, but still on the 1st byte + assertEquals(0b00100101, stream.readByte()); + + // Start reading from the second byte (10 == 2) + assertEquals(2, bitReader.readBits(2)); + + assertEquals(16, bitReader.peekBits(5)); + assertEquals(16, bitReader.peekBits(5)); + assertEquals(16, bitReader.readBits(5)); + + // We've now read 7 bits, but still on the second byte + assertEquals(1, stream.getStreamPosition()); + assertEquals(0b01000010, stream.readByte()); + assertEquals(2, stream.getStreamPosition()); + + assertEquals(16, bitReader.peekBits(11)); + + assertEquals(0b00010000, stream.readByte()); + assertEquals(3, stream.getStreamPosition()); + stream.seek(2); + assertEquals(2, stream.getStreamPosition()); + + // Start reading from the third byte (10000 == 16) + assertEquals(16, bitReader.peekBits(5)); + assertEquals(16, bitReader.readBits(5)); + + assertEquals(64, bitReader.peekBits(7)); + assertEquals(64, bitReader.peekBits(7)); + assertEquals(64, bitReader.peekBits(7)); + assertEquals(64, bitReader.peekBits(7)); + assertEquals(64, bitReader.readBits(7)); + + assertEquals(128, bitReader.peekBits(8)); + assertEquals(128, bitReader.readBits(8)); + + assertEquals(256, bitReader.readBits(9)); + + assertEquals(512, bitReader.peekBits(10)); + assertEquals(512, bitReader.readBits(10)); + + assertEquals(1024, bitReader.peekBits(11)); + assertEquals(1024, bitReader.readBits(11)); + + assertEquals(2048, bitReader.peekBits(12)); + assertEquals(2048, bitReader.peekBits(12)); + assertEquals(2048, bitReader.peekBits(12)); + assertEquals(2048, bitReader.readBits(12)); + + assertEquals(4096, bitReader.peekBits(13)); + assertEquals(4096, bitReader.readBits(13)); + + assertEquals(8192, bitReader.readBits(14)); + + assertEquals(16384, bitReader.peekBits(15)); + assertEquals(16384, bitReader.peekBits(15)); + assertEquals(16384, bitReader.readBits(15)); + +// assertThrows(EOFException.class, new ThrowingRunnable() { +// @Override +// public void run() throws Throwable { +// bitReader.readBits(1); +// } +// }); + } + + private static LSBBitReader createBitReader(final byte[] data) { + ImageInputStream stream = createStream(data); + return new LSBBitReader(stream); + } + + private static ImageInputStream createStream(byte[] data) { + ByteArrayImageInputStream stream = new ByteArrayImageInputStream(data); + stream.setByteOrder(ByteOrder.LITTLE_ENDIAN); + return stream; + } +} \ No newline at end of file