#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,19 +90,15 @@ 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) {
// 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,20 +107,22 @@ public final class LSBBitReader {
if (!peek) {
bitOffset += bits;
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
imageInput.seek(streamPosition + 8);
for (; bitOffset >= 8; bitOffset -= 8) {
@ -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;

View File

@ -96,8 +96,10 @@ final class WebPImageReader extends ImageReaderBase {
public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) {
super.setInput(input, seekForwardOnly, ignoreMetadata);
if (imageInput != null) {
lsbBitReader = new LSBBitReader(imageInput);
}
}
private void readHeader(int imageIndex) throws IOException {
checkBounds(imageIndex);
@ -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

@ -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
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
int[] lengthsAndSymbols = new int[numPosCodeLens];
int index = 0;
@ -179,11 +171,9 @@ final class HuffmanTable {
// 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
@ -191,7 +181,6 @@ final class HuffmanTable {
int[] currentTable = null;
for (int i = 0; i < lengthsAndSymbols.length; i++) {
int lengthAndSymbol = lengthsAndSymbols[i];
int length = lengthAndSymbol >>> 16;
@ -204,14 +193,13 @@ final class HuffmanTable {
else {
// 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--) {
@ -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,7 +331,6 @@ 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];

View File

@ -51,7 +51,6 @@ final class PredictorTransform implements Transform {
@Override
public void applyInverse(WritableRaster raster) {
int width = raster.getWidth();
int height = raster.getHeight();
@ -64,7 +63,6 @@ final class PredictorTransform implements Transform {
rgba[3] += 0xff;
raster.setDataElements(0, 0, rgba);
byte[] predictor = new byte[4];
byte[] predictor2 = new byte[4];
byte[] predictor3 = new byte[4];
@ -89,13 +87,11 @@ 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 tY = y - 1; // y for top
// top right is not (x+1, tY) if last pixel in line instead (0, y)

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