#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.EOFException;
import java.io.IOException; import java.io.IOException;
import static com.twelvemonkeys.lang.Validate.notNull;
/** /**
* LSBBitReader * LSBBitReader
* *
@ -45,18 +47,17 @@ public final class LSBBitReader {
// TODO: Consider creating an ImageInputStream wrapper with the WebP implementation of readBit(s)? // TODO: Consider creating an ImageInputStream wrapper with the WebP implementation of readBit(s)?
private final ImageInputStream imageInput; private final ImageInputStream imageInput;
private int bitOffset = 64; int bitOffset = 64;
private long streamPosition = -1; 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). * 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; private long buffer;
public LSBBitReader(ImageInputStream imageInput) { public LSBBitReader(ImageInputStream imageInput) {
this.imageInput = imageInput; this.imageInput = notNull(imageInput);
} }
/** /**
@ -89,20 +90,16 @@ public final class LSBBitReader {
if (bits > 56) { if (bits > 56) {
throw new IllegalArgumentException("Tried peeking over 56"); throw new IllegalArgumentException("Tried peeking over 56");
} }
return readBits(bits, true); return readBits(bits, true);
} }
//Driver
private long readBits(int bits, boolean peek) throws IOException { private long readBits(int bits, boolean peek) throws IOException {
if (bits <= 56) { if (bits <= 56) {
// Could eliminate if we never read from the underlying InputStream
/* // outside this class after the object is created
Could eliminate if we never read from the underlying InputStream outside this class after the object is if (streamPosition != imageInput.getStreamPosition()) {
created // Need to reset buffer as stream was read in the meantime
*/
long inputStreamPosition = imageInput.getStreamPosition();
if (streamPosition != inputStreamPosition) {
//Need to reset buffer as stream was read in the meantime
resetBuffer(); resetBuffer();
} }
@ -110,21 +107,23 @@ public final class LSBBitReader {
if (!peek) { if (!peek) {
bitOffset += bits; bitOffset += bits;
refillBuffer();
if (bitOffset >= 8) {
refillBuffer();
}
} }
return ret; return ret;
} }
else { else {
//FIXME Untested // Peek always false in this case
long lower = readBits(56); long lower = readBits(56);
return (readBits(bits - 56) << (56)) | lower; return (readBits(bits - 56) << (56)) | lower;
} }
} }
private void refillBuffer() throws IOException { 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); imageInput.seek(streamPosition + 8);
for (; bitOffset >= 8; bitOffset -= 8) { for (; bitOffset >= 8; bitOffset -= 8) {
try { try {
@ -138,17 +137,16 @@ public final class LSBBitReader {
return; return;
} }
} }
/*
Reset to guarantee stream position consistent with returned bytes // 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 // Would not need to do this seeking around when the underlying ImageInputStream is never read from outside
this class after the object is created. // this class after the object is created.
*/
imageInput.seek(streamPosition); imageInput.seek(streamPosition);
} }
private void resetBuffer() throws IOException { private void resetBuffer() throws IOException {
long inputStreamPosition = imageInput.getStreamPosition(); long inputStreamPosition = imageInput.getStreamPosition();
try { try {
buffer = imageInput.readLong(); buffer = imageInput.readLong();
bitOffset = 0; bitOffset = 0;
@ -156,7 +154,7 @@ public final class LSBBitReader {
imageInput.seek(inputStreamPosition); imageInput.seek(inputStreamPosition);
} }
catch (EOFException e) { catch (EOFException e) {
//Retry byte by byte // Retry byte by byte
streamPosition = inputStreamPosition - 8; streamPosition = inputStreamPosition - 8;
bitOffset = 64; bitOffset = 64;
refillBuffer(); 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 { public int readBit() throws IOException {
return (int) readBits(1); return (int) readBits(1);
} }

View File

@ -96,7 +96,9 @@ final class WebPImageReader extends ImageReaderBase {
public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) {
super.setInput(input, seekForwardOnly, ignoreMetadata); super.setInput(input, seekForwardOnly, ignoreMetadata);
lsbBitReader = new LSBBitReader(imageInput); if (imageInput != null) {
lsbBitReader = new LSBBitReader(imageInput);
}
} }
private void readHeader(int imageIndex) throws IOException { private void readHeader(int imageIndex) throws IOException {
@ -272,19 +274,19 @@ final class WebPImageReader extends ImageReaderBase {
} }
// RsV|I|L|E|X|A|R // RsV|I|L|E|X|A|R
int reserved = (int) imageInput.readBits(2); int reserved = lsbBitReader.readBit();
if (reserved != 0) { if (reserved != 0) {
// Spec says SHOULD be 0 // Spec says SHOULD be 0
throw new IIOException(String.format("Unexpected 'VP8X' chunk reserved value, expected 0: %d", reserved)); throw new IIOException(String.format("Unexpected 'VP8X' chunk reserved value, expected 0: %d", reserved));
} }
header.containsICCP = imageInput.readBit() == 1; header.containsANIM = lsbBitReader.readBit() == 1; // A -> Anim
header.containsALPH = imageInput.readBit() == 1; // L -> aLpha header.containsXMP_ = lsbBitReader.readBit() == 1;
header.containsEXIF = imageInput.readBit() == 1; header.containsEXIF = lsbBitReader.readBit() == 1;
header.containsXMP_ = imageInput.readBit() == 1; header.containsALPH = lsbBitReader.readBit() == 1; // L -> aLpha
header.containsANIM = imageInput.readBit() == 1; // A -> Anim 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) { if (reserved != 0) {
// Spec says SHOULD be 0 // Spec says SHOULD be 0
throw new IIOException(String.format("Unexpected 'VP8X' chunk reserved value, expected 0: %d", reserved)); 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 { 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) { if (reserved != 0) {
// Spec says SHOULD be 0 // Spec says SHOULD be 0
processWarningOccurred(String.format("Unexpected 'ALPH' chunk reserved value, expected 0: %d", reserved)); processWarningOccurred(String.format("Unexpected 'ALPH' chunk reserved value, expected 0: %d", reserved));
} }
int preProcessing = (int) imageInput.readBits(2); int preProcessing = (int) lsbBitReader.readBits(2);
int filtering = (int) imageInput.readBits(2); int filtering = (int) lsbBitReader.readBits(2);
int compression = (int) imageInput.readBits(2); int compression = (int) lsbBitReader.readBits(2);
if (DEBUG) { if (DEBUG) {
System.out.println("preProcessing: " + preProcessing); System.out.println("preProcessing: " + preProcessing);

View File

@ -55,7 +55,7 @@ final class ColorIndexingTransform implements Transform {
byte[] rgba = new byte[4]; byte[] rgba = new byte[4];
for (int y = 0; y < height; y++) { 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--) { for (int x = width - 1; x >= 0; x--) {
int componentSize = 8 >> bits; int componentSize = 8 >> bits;
@ -67,7 +67,7 @@ final class ColorIndexingTransform implements Transform {
int index = sample >> componentOffset & ((1 << componentSize) - 1); 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); System.arraycopy(colorTable, index * 4, rgba, 0, 4);
raster.setDataElements(x, y, rgba); raster.setDataElements(x, y, rgba);

View File

@ -37,7 +37,7 @@ import java.awt.image.*;
* @author Simon Kammermeier * @author Simon Kammermeier
*/ */
final class HuffmanInfo { 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; public int metaCodeBits;

View File

@ -83,7 +83,6 @@ final class HuffmanTable {
* @throws IOException when reading produces an exception * @throws IOException when reading produces an exception
*/ */
public HuffmanTable(LSBBitReader lsbBitReader, int alphabetSize) throws IOException { public HuffmanTable(LSBBitReader lsbBitReader, int alphabetSize) throws IOException {
boolean simpleLengthCode = lsbBitReader.readBit() == 1; boolean simpleLengthCode = lsbBitReader.readBit() == 1;
if (simpleLengthCode) { if (simpleLengthCode) {
@ -104,11 +103,9 @@ final class HuffmanTable {
} }
} }
else { else {
/* // code lengths also huffman coded
code lengths also huffman coded // first read the "first stage" code lengths
first read the "first stage" code lengths // In the following this is called the L-Code (for length code)
In the following this is called the L-Code (for length code)
*/
int numLCodeLengths = (int) (lsbBitReader.readBits(4) + 4); int numLCodeLengths = (int) (lsbBitReader.readBits(4) + 4);
short[] lCodeLengths = new short[L_CODE_ORDER.length]; short[] lCodeLengths = new short[L_CODE_ORDER.length];
int numPosCodeLens = 0; int numPosCodeLens = 0;
@ -116,16 +113,15 @@ final class HuffmanTable {
for (int i = 0; i < numLCodeLengths; i++) { for (int i = 0; i < numLCodeLengths; i++) {
short len = (short) lsbBitReader.readBits(3); short len = (short) lsbBitReader.readBits(3);
lCodeLengths[L_CODE_ORDER[i]] = len; lCodeLengths[L_CODE_ORDER[i]] = len;
if (len > 0) { if (len > 0) {
numPosCodeLens++; 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); short[] codeLengths = readCodeLengths(lsbBitReader, lCodeLengths, alphabetSize, numPosCodeLens);
buildFromLengths(codeLengths); buildFromLengths(codeLengths);
} }
} }
@ -142,25 +138,21 @@ final class HuffmanTable {
buildFromLengths(codeLengths, numPosCodeLens); buildFromLengths(codeLengths, numPosCodeLens);
} }
// Helper methods to allow reusing in different constructors
/*
Helper methods to allow reusing in different constructors
*/
private void buildFromLengths(short[] codeLengths) { private void buildFromLengths(short[] codeLengths) {
int numPosCodeLens = 0; int numPosCodeLens = 0;
for (short codeLength : codeLengths) { for (short codeLength : codeLengths) {
if (codeLength != 0) { if (codeLength != 0) {
numPosCodeLens++; numPosCodeLens++;
} }
} }
buildFromLengths(codeLengths, numPosCodeLens); buildFromLengths(codeLengths, numPosCodeLens);
} }
private void buildFromLengths(short[] codeLengths, int 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[] lengthsAndSymbols = new int[numPosCodeLens];
int index = 0; 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) { 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); 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); Arrays.sort(lengthsAndSymbols);
/* // The next code, in the bit order it would appear on the input stream, i.e. it is reversed.
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.
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.
Example: code 0..010 (length 2) would appear as 0..001.
*/
int code = 0; int code = 0;
//Used for level2 lookup // Used for level2 lookup
int rootEntry = -1; int rootEntry = -1;
int[] currentTable = null; int[] currentTable = null;
for (int i = 0; i < lengthsAndSymbols.length; i++) { for (int i = 0; i < lengthsAndSymbols.length; i++) {
int lengthAndSymbol = lengthsAndSymbols[i]; int lengthAndSymbol = lengthsAndSymbols[i];
int length = lengthAndSymbol >>> 16; int length = lengthAndSymbol >>> 16;
@ -202,16 +191,15 @@ final class HuffmanTable {
} }
} }
else { else {
//Existing level2 table not fitting // Existing level2 table not fitting
if ((code & ((1 << LEVEL1_BITS) - 1)) != rootEntry) { if ((code & ((1 << LEVEL1_BITS) - 1)) != rootEntry) {
/* // Figure out needed table size.
Figure out needed table size. // Start at current symbol and length.
Start at current symbol and length. // Every symbol uses 1 slot at the current bit length.
Every symbol uses 1 slot at the current bit length. // Going up 1 bit in length multiplies the slots by 2.
Going up 1 bit in length multiplies the slots by 2. // No more open slots indicate the table size to be big enough.
No more open slots indicate the table size to be big enough.
*/
int maxLength = length; int maxLength = length;
for (int j = i, openSlots = 1 << (length - LEVEL1_BITS); for (int j = i, openSlots = 1 << (length - LEVEL1_BITS);
j < lengthsAndSymbols.length && openSlots > 0; j < lengthsAndSymbols.length && openSlots > 0;
j++, openSlots--) { j++, openSlots--) {
@ -230,11 +218,11 @@ final class HuffmanTable {
rootEntry = code & ((1 << LEVEL1_BITS) - 1); rootEntry = code & ((1 << LEVEL1_BITS) - 1);
level2.add(currentTable); level2.add(currentTable);
//Set root table indirection // Set root table indirection
level1[rootEntry] = (LEVEL1_BITS + level2Size) << 16 | (level2.size() - 1); 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)) { for (int j = (code >>> LEVEL1_BITS); j < currentTable.length; j += 1 << (length - LEVEL1_BITS)) {
currentTable[j] = (length - LEVEL1_BITS) << 16 | (lengthAndSymbol & 0xffff); currentTable[j] = (length - LEVEL1_BITS) << 16 | (lengthAndSymbol & 0xffff);
} }
@ -256,12 +244,12 @@ final class HuffmanTable {
private int nextCode(int code, int length) { private int nextCode(int code, int length) {
int a = (~code) & ((1 << length) - 1); 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) // 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 // I.e. the lowest 0-bit in the value code represents
int step = Integer.highestOneBit(a); 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 // 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 // This corresponds to adding 1 to the value
return (code & (step - 1)) | step; return (code & (step - 1)) | step;
} }
@ -270,7 +258,7 @@ final class HuffmanTable {
HuffmanTable huffmanTable = new HuffmanTable(aCodeLengths, numPosCodeLens); 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; int codedSymbols;
if (lsbBitReader.readBit() == 1) { if (lsbBitReader.readBit() == 1) {
int maxSymbolBitLength = (int) (2 + 2 * lsbBitReader.readBits(3)); int maxSymbolBitLength = (int) (2 + 2 * lsbBitReader.readBits(3));
@ -282,13 +270,13 @@ final class HuffmanTable {
short[] codeLengths = new short[alphabetSize]; short[] codeLengths = new short[alphabetSize];
//Default code for repeating // Default code for repeating
short prevLength = 8; short prevLength = 8;
for (int i = 0; i < alphabetSize && codedSymbols > 0; i++, codedSymbols--) { for (int i = 0; i < alphabetSize && codedSymbols > 0; i++, codedSymbols--) {
short len = huffmanTable.readSymbol(lsbBitReader); short len = huffmanTable.readSymbol(lsbBitReader);
if (len < 16) { //Literal length if (len < 16) { // Literal length
codeLengths[i] = len; codeLengths[i] = len;
if (len != 0) { if (len != 0) {
prevLength = len; prevLength = len;
@ -300,16 +288,16 @@ final class HuffmanTable {
int repeatOffset; int repeatOffset;
switch (len) { switch (len) {
case 16: //Repeat previous case 16: // Repeat previous
repeatSymbol = prevLength; repeatSymbol = prevLength;
extraBits = 2; extraBits = 2;
repeatOffset = 3; repeatOffset = 3;
break; break;
case 17: //Repeat 0 short case 17: // Repeat 0 short
extraBits = 3; extraBits = 3;
repeatOffset = 3; repeatOffset = 3;
break; break;
case 18: //Repeat 0 long case 18: // Repeat 0 long
extraBits = 7; extraBits = 7;
repeatOffset = 11; repeatOffset = 11;
break; break;
@ -319,7 +307,6 @@ final class HuffmanTable {
int repeatCount = (int) (lsbBitReader.readBits(extraBits) + repeatOffset); int repeatCount = (int) (lsbBitReader.readBits(extraBits) + repeatOffset);
if (i + repeatCount > alphabetSize) { if (i + repeatCount > alphabetSize) {
throw new IIOException( throw new IIOException(
String.format( String.format(
@ -330,11 +317,9 @@ final class HuffmanTable {
Arrays.fill(codeLengths, i, i + repeatCount, repeatSymbol); Arrays.fill(codeLengths, i, i + repeatCount, repeatSymbol);
i += repeatCount - 1; i += repeatCount - 1;
} }
} }
return codeLengths; return codeLengths;
} }
@ -346,21 +331,20 @@ final class HuffmanTable {
* @throws IOException when the reader throws one reading a symbol * @throws IOException when the reader throws one reading a symbol
*/ */
public short readSymbol(LSBBitReader lsbBitReader) throws IOException { public short readSymbol(LSBBitReader lsbBitReader) throws IOException {
int index = (int) lsbBitReader.peekBits(LEVEL1_BITS); int index = (int) lsbBitReader.peekBits(LEVEL1_BITS);
int lengthAndSymbol = level1[index]; int lengthAndSymbol = level1[index];
int length = lengthAndSymbol >>> 16; int length = lengthAndSymbol >>> 16;
if (length > LEVEL1_BITS) { if (length > LEVEL1_BITS) {
//Lvl2 lookup // Lvl2 lookup
lsbBitReader.readBits(LEVEL1_BITS); //Consume bits of first level lsbBitReader.readBits(LEVEL1_BITS); // Consume bits of first level
int level2Index = (int) lsbBitReader.peekBits(length - LEVEL1_BITS); //Peek remaining required bits int level2Index = (int) lsbBitReader.peekBits(length - LEVEL1_BITS); // Peek remaining required bits
lengthAndSymbol = level2.get(lengthAndSymbol & 0xffff)[level2Index]; lengthAndSymbol = level2.get(lengthAndSymbol & 0xffff)[level2Index];
length = lengthAndSymbol >>> 16; length = lengthAndSymbol >>> 16;
} }
lsbBitReader.readBits(length); //Consume bits lsbBitReader.readBits(length); // Consume bits
return (short) (lengthAndSymbol & 0xffff); return (short) (lengthAndSymbol & 0xffff);
} }

View File

@ -51,25 +51,23 @@ final class PredictorTransform implements Transform {
@Override @Override
public void applyInverse(WritableRaster raster) { public void applyInverse(WritableRaster raster) {
int width = raster.getWidth(); int width = raster.getWidth();
int height = raster.getHeight(); int height = raster.getHeight();
byte[] rgba = new byte[4]; 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); raster.getDataElements(0, 0, rgba);
rgba[3] += 0xff; rgba[3] += 0xff;
raster.setDataElements(0, 0, rgba); raster.setDataElements(0, 0, rgba);
byte[] predictor = new byte[4]; byte[] predictor = new byte[4];
byte[] predictor2 = new byte[4]; byte[] predictor2 = new byte[4];
byte[] predictor3 = new byte[4]; byte[] predictor3 = new byte[4];
//(x,0) L predict // (x,0) L predict
for (int x = 1; x < width; x++) { for (int x = 1; x < width; x++) {
raster.getDataElements(x, 0, rgba); raster.getDataElements(x, 0, rgba);
raster.getDataElements(x - 1, 0, predictor); raster.getDataElements(x - 1, 0, predictor);
@ -78,7 +76,7 @@ final class PredictorTransform implements Transform {
raster.setDataElements(x, 0, rgba); raster.setDataElements(x, 0, rgba);
} }
//(0,y) T predict // (0,y) T predict
for (int y = 1; y < height; y++) { for (int y = 1; y < height; y++) {
raster.getDataElements(0, y, rgba); raster.getDataElements(0, y, rgba);
raster.getDataElements(0, y - 1, predictor); raster.getDataElements(0, y - 1, predictor);
@ -89,16 +87,14 @@ final class PredictorTransform implements Transform {
for (int y = 1; y < height; y++) { for (int y = 1; y < height; y++) {
for (int x = 1; x < width; x++) { for (int x = 1; x < width; x++) {
int transformType = data.getSample(x >> bits, y >> bits, 1); int transformType = data.getSample(x >> bits, y >> bits, 1);
raster.getDataElements(x, y, rgba); 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 trX = x == width - 1 ? 0 : x + 1;
int trY = x == width - 1 ? y : tY; 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> * @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 { interface TransformType {
int PREDICTOR_TRANSFORM = 0; int PREDICTOR_TRANSFORM = 0;
int COLOR_TRANSFORM = 1; 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;
}
}