From b3004a12279e48180bd46fa12d4bd5c103c88d59 Mon Sep 17 00:00:00 2001 From: Simon Kammermeier Date: Mon, 29 Aug 2022 18:11:32 +0200 Subject: [PATCH] Implement buffering in LSBBitReader This optimizes away the constant re-reading of bytes. Also allows peeking at coming bits without consuming them. --- .../imageio/plugins/webp/LSBBitReader.java | 145 ++++++++++++++---- 1 file changed, 113 insertions(+), 32 deletions(-) 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 ab19f722..c8eff70c 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 @@ -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); } }