#704 Tiny performance improvement + code clean-up

(cherry picked from commit 61424f33b646a31ab49364d06c5ac630c3417ee1)
This commit is contained in:
Harald Kuhr 2022-10-19 20:46:24 +02:00
parent 38192ae835
commit 6ed858a4ca
8 changed files with 358 additions and 108 deletions

View File

@ -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);
}

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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);
}

View File

@ -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;

View File

@ -36,7 +36,7 @@ package com.twelvemonkeys.imageio.plugins.webp.lossless;
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/
// 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;

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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;
}
}