Implement buffering in LSBBitReader

This optimizes away the constant re-reading of bytes.
Also allows peeking at coming bits without consuming them.
This commit is contained in:
Simon Kammermeier 2022-08-29 18:11:32 +02:00
parent 7ab627a754
commit b3004a1227

View File

@ -11,46 +11,127 @@ public final class LSBBitReader {
// TODO: Consider creating an ImageInputStream wrapper with the WebP implementation of readBit(s)?
private final ImageInputStream imageInput;
int bitOffset = 0;
private int bitOffset = 64;
private long streamPosition = -1;
/**
* 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;
}
// TODO: Optimize this... Read many bits at once!
/**
* Reads the specified number of bits from the stream in an LSB-first way and advances the bitOffset.
* The underlying ImageInputStream will be advanced to the first not (completely) read byte.
* Requesting more than 64 bits will advance the reader by the correct amount and return the lowest 64 bits of
* the read number
*
* @param bits the number of bits to read
* @return a signed long built from the requested bits (truncated to the low 64 bits)
* @throws IOException if an I/O error occurs
* @see LSBBitReader#peekBits
*/
public long readBits(int bits) throws IOException {
long result = 0;
for (int i = 0; i < bits; i++) {
result |= (long) readBit() << i;
}
return result;
return readBits(bits, false);
}
// TODO: Optimize this...
// TODO: Consider not reading value over and over....
/**
* Reads the specified number of bits from the buffer in an LSB-first way.
* Does not advance the bitOffset or the underlying input stream.
* As only 56 bits are buffered (in the worst case) peeking more is not possible without advancing the reader and
* as such disallowed.
*
* @param bits the number of bits to peek (max 56)
* @return a signed long built from the requested bits
* @throws IOException if an I/O error occurs
* @see LSBBitReader#readBits
*/
public long peekBits(int bits) throws IOException {
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
resetBuffer();
}
long ret = (buffer >>> bitOffset) & ((1L << bits) - 1);
if (!peek) {
bitOffset += bits;
refillBuffer();
}
return ret;
}
else {
//FIXME Untested
long lower = readBits(56);
return (readBits(bits - 56) << (56)) | lower;
}
}
private void refillBuffer() throws IOException {
//Set to stream position consistent with buffered bytes
imageInput.seek(streamPosition + 8);
for (; bitOffset >= 8; bitOffset -= 8) {
try {
byte b = imageInput.readByte();
buffer >>>= 8;
streamPosition++;
buffer |= ((long) b << 56);
}
catch (EOFException e) {
imageInput.seek(streamPosition);
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.
*/
imageInput.seek(streamPosition);
}
private void resetBuffer() throws IOException {
long inputStreamPosition = imageInput.getStreamPosition();
try {
buffer = imageInput.readLong();
bitOffset = 0;
streamPosition = inputStreamPosition;
imageInput.seek(inputStreamPosition);
}
catch (EOFException e) {
//Retry byte by byte
streamPosition = inputStreamPosition - 8;
bitOffset = 64;
refillBuffer();
}
}
//Left for backwards compatibility / Compatibility with ImageInputStream interface
public int readBit() throws IOException {
int bit = 7 - bitOffset;
imageInput.setBitOffset(bit);
// Compute final bit offset before we call read() and seek()
int newBitOffset = (bitOffset + 1) & 0x7;
int val = imageInput.read();
if (val == -1) {
throw new EOFException();
}
if (newBitOffset != 0) {
// Move byte position back if in the middle of a byte
// NOTE: RESETS bit offset!
imageInput.seek(imageInput.getStreamPosition() - 1);
}
bitOffset = newBitOffset;
// Shift the bit to be read to the rightmost position
return (val >> (7 - bit)) & 0x1;
return (int) readBits(1);
}
}