transforms) throws IOException {
int transformType = (int) lsbBitReader.readBits(2);
// TODO: Each transform type can only be present once in the stream.
switch (transformType) {
- case TransformType.PREDICTOR_TRANSFORM: {
- System.err.println("transformType: PREDICTOR_TRANSFORM");
-// int sizeBits = (int) readBits(3) + 2;
- int sizeBits = (int) lsbBitReader.readBits(3) + 2;
- int size = 1 << sizeBits;
-
- int blockWidth = size;
- int blockHeight = size;
-
-// int blockSize = divRoundUp(width, size);
- int blockSize = divRoundUp(xSize, size);
-
- for (int y = 0; y < ySize; y++) {
- for (int x = 0; x < xSize; x++) {
- int blockIndex = (y >> sizeBits) * blockSize + (x >> sizeBits);
- }
- }
-
- // Special rules:
- // Top-left pixel of image is predicted BLACK
- // Rest of top pixels is predicted L
- // Rest of leftmost pixels are predicted T
- // Rightmost pixels using TR, uses LEFTMOST pixel on SAME ROW (same distance as TR in memory!)
-
-// WritableRaster data = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, blockWidth, blockHeight, blockWidth, 1, new int[] {0}, null);
-// readVP8Lossless(data, false);
-//
- break;
- }
+ case TransformType.PREDICTOR_TRANSFORM:
+ //Intentional Fallthrough
case TransformType.COLOR_TRANSFORM: {
// The two first transforms contains the exact same data, can be combined
- System.err.println("transformType: COLOR_TRANSFORM");
- int sizeBits = (int) lsbBitReader.readBits(3) + 2;
-// int size = 1 << sizeBits;
-
- // TODO: Understand difference between spec divRoundUp and impl VP8LSubSampleSize
+ byte sizeBits = (byte) (lsbBitReader.readBits(3) + 2);
int blockWidth = subSampleSize(xSize, sizeBits);
int blockHeight = subSampleSize(ySize, sizeBits);
- WritableRaster data = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, blockWidth, blockHeight, blockWidth, 1, new int[] {0}, null);
- readVP8Lossless(data, false);
+ WritableRaster raster =
+ Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, blockWidth, blockHeight, 4 * blockWidth, 4,
+ new int[] {0, 1, 2, 3}, null);
+ readVP8Lossless(raster, false, null, blockWidth, blockHeight);
- transforms.add(new Transform(transformType, ((DataBufferByte) data.getDataBuffer()).getData()));
+ //Keep data as raster for convenient (x,y) indexing
+ if (transformType == TransformType.PREDICTOR_TRANSFORM) {
+ transforms.add(0, new PredictorTransform(raster, sizeBits));
+ }
+ else {
+ transforms.add(0, new ColorTransform(raster, sizeBits));
+ }
break;
}
case TransformType.SUBTRACT_GREEN: {
- System.err.println("transformType: SUBTRACT_GREEN");
// No data here
-
-// addGreenToBlueAndRed();
+ transforms.add(0, new SubtractGreenTransform());
break;
}
case TransformType.COLOR_INDEXING_TRANSFORM: {
- System.err.println("transformType: COLOR_INDEXING_TRANSFORM");
// 8 bit value for color table size
- int colorTableSize = imageInput.readUnsignedByte() + 1; // 1-256
- System.err.println("colorTableSize: " + colorTableSize);
+ int colorTableSize = ((int) lsbBitReader.readBits(8)) + 1; // 1-256
// If the index is equal or larger than color_table_size,
// the argb color value should be set to 0x00000000
@@ -165,43 +394,37 @@ public final class VP8LDecoder {
colorTableSize > 4 ? 16 :
colorTableSize > 2 ? 4 : 2;
- System.err.println("safeColorTableSize: " + safeColorTableSize);
- int[] colorTable = new int[safeColorTableSize];
+ byte[] colorTable = new byte[safeColorTableSize * 4];
// The color table can be obtained by reading an image,
// without the RIFF header, image size, and transforms,
// assuming a height of one pixel and a width of
// color_table_size. The color table is always
// subtraction-coded to reduce image entropy.
- // TODO: Read *without transforms*, using SUBTRACT_GREEN only!
- readVP8Lossless(asByteRaster(
- Raster.createPackedRaster(
- new DataBufferInt(colorTable, colorTableSize),
- colorTableSize, 1, colorTableSize,
- new int[] {0}, null
- )
- ), false);
+ readVP8Lossless(
+ Raster.createInterleavedRaster(
+ new DataBufferByte(colorTable, colorTableSize * 4),
+ colorTableSize, 1, colorTableSize * 4,
+ 4, new int[] {0, 1, 2, 3}, null)
+ , false, null, colorTableSize, 1);
- // TODO: We may not really need this value...
- // What we need is the number of pixels packed into each green sample (byte)
- int widthBits = colorTableSize > 16 ? 0 :
- colorTableSize > 4 ? 1 :
- colorTableSize > 2 ? 2 : 3;
+
+ //resolve subtraction code
+ for (int i = 4; i < colorTable.length; i++) {
+ colorTable[i] += colorTable[i - 4];
+ }
+
+ // The number of pixels packed into each green sample (byte)
+ byte widthBits = (byte) (colorTableSize > 16 ? 0 :
+ colorTableSize > 4 ? 1 :
+ colorTableSize > 2 ? 2 : 3);
xSize = subSampleSize(xSize, widthBits);
- /*
- // TODO: read ARGB
- int argb = 0;
-
- // Inverse transform
- // TODO: Expand to mutliple pixels?
- argb = colorTable[GREEN(argb)];
- */
-
+ // The colors components are stored in ARGB order at 4*index, 4*index + 1, 4*index + 2, 4*index + 3
// TODO: Can we use this to produce an image with IndexColorModel instead of expanding the values in-memory?
- transforms.add(new Transform(transformType, colorTable));
+ transforms.add(0, new ColorIndexingTransform(colorTable, widthBits));
break;
}
@@ -212,147 +435,53 @@ public final class VP8LDecoder {
return xSize;
}
- private void readHuffmanCodes(int colorCacheBits, boolean allowRecursion) {
+ private HuffmanInfo readHuffmanCodes(int xSize, int ySize, int colorCacheBits, boolean readMetaCodes)
+ throws IOException {
+ int huffmanGroupNum = 1;
+ int huffmanXSize;
+ int huffmanYSize;
- }
+ int metaCodeBits = 0;
- ////
+ WritableRaster huffmanMetaCodes = null;
+ if (readMetaCodes && lsbBitReader.readBit() == 1) {
+ //read in meta codes
+ metaCodeBits = (int) lsbBitReader.readBits(3) + 2;
+ huffmanXSize = subSampleSize(xSize, metaCodeBits);
+ huffmanYSize = subSampleSize(ySize, metaCodeBits);
- // FROM the spec
- private static int divRoundUp(final int numerator, final int denominator) {
- return (numerator + denominator - 1) / denominator;
+ //Raster with elements as BARG (only the RG components encode the meta group)
+ WritableRaster packedRaster = Raster.createPackedRaster(DataBuffer.TYPE_INT, huffmanXSize, huffmanYSize,
+ new int[] {0x0000ff00, 0x000000ff, 0xff000000, 0x00ff0000}, null);
+ readVP8Lossless(asByteRaster(packedRaster), false, null, huffmanXSize, huffmanYSize);
+
+ int[] data = ((DataBufferInt) packedRaster.getDataBuffer()).getData();
+ //Max metaGroup is number of meta groups
+ int maxCode = Integer.MIN_VALUE;
+ for (int code : data) {
+ maxCode = max(maxCode, code & 0xffff);
+ }
+ huffmanGroupNum = maxCode + 1;
+
+ /*
+ New Raster with just RG components exposed as single band allowing simple access of metaGroupIndex with
+ x,y lookup
+ */
+ huffmanMetaCodes = Raster.createPackedRaster(packedRaster.getDataBuffer(), huffmanXSize, huffmanYSize,
+ huffmanXSize, new int[] {0xffff}, null);
+
+ }
+
+ HuffmanCodeGroup[] huffmanGroups = new HuffmanCodeGroup[huffmanGroupNum];
+
+ for (int i = 0; i < huffmanGroups.length; i++) {
+ huffmanGroups[i] = new HuffmanCodeGroup(lsbBitReader, colorCacheBits);
+ }
+
+ return new HuffmanInfo(huffmanMetaCodes, metaCodeBits, huffmanGroups);
}
private static int subSampleSize(final int size, final int samplingBits) {
return (size + (1 << samplingBits) - 1) >> samplingBits;
}
-
- private static int ALPHA(final int ARGB) {
- return ARGB >>> 24;
- }
-
- private static int RED(final int ARGB) {
- return (ARGB >> 16) & 0xff;
- }
-
- private static int GREEN(final int ARGB) {
- return (ARGB >> 8) & 0xff;
- }
-
- private static int BLUE(final int ARGB) {
- return ARGB & 0xff;
- }
-
- private static int select(final int L, final int T, final int TL) {
- // L = left pixel, T = top pixel, TL = top left pixel.
-
- // ARGB component estimates for prediction.
- int pAlpha = ALPHA(L) + ALPHA(T) - ALPHA(TL);
- int pRed = RED(L) + RED(T) - RED(TL);
- int pGreen = GREEN(L) + GREEN(T) - GREEN(TL);
- int pBlue = BLUE(L) + BLUE(T) - BLUE(TL);
-
- // Manhattan distances to estimates for left and top pixels.
- int pL = abs(pAlpha - ALPHA(L)) + abs(pRed - RED(L)) +
- abs(pGreen - GREEN(L)) + abs(pBlue - BLUE(L));
- int pT = abs(pAlpha - ALPHA(T)) + abs(pRed - RED(T)) +
- abs(pGreen - GREEN(T)) + abs(pBlue - BLUE(T));
-
- // Return either left or top, the one closer to the prediction.
- return pL < pT ? L : T;
- }
-
- private static int average2(final int a, final int b) {
- return (a + b) / 2;
- }
-
- // Clamp the input value between 0 and 255.
- private static int clamp(final int a) {
- return max(0, min(a, 255));
- }
-
- private static int clampAddSubtractFull(final int a, final int b, final int c) {
- return clamp(a + b - c);
- }
-
- private static int clampAddSubtractHalf(final int a, final int b) {
- return clamp(a + (a - b) / 2);
- }
-
- static final class ColorTransformElement {
- final int green_to_red;
- final int green_to_blue;
- final int red_to_blue;
-
- ColorTransformElement(final int green_to_red, final int green_to_blue, final int red_to_blue) {
- this.green_to_red = green_to_red;
- this.green_to_blue = green_to_blue;
- this.red_to_blue = red_to_blue;
- }
- }
-
- // NOTE: For encoding!
- private static void colorTransform(final int red, final int blue, final int green,
- final ColorTransformElement trans,
- final int[] newRedBlue) {
- // Transformed values of red and blue components
- int tmp_red = red;
- int tmp_blue = blue;
-
- // Applying transform is just adding the transform deltas
- tmp_red += colorTransformDelta((byte) trans.green_to_red, (byte) green);
- tmp_blue += colorTransformDelta((byte) trans.green_to_blue, (byte) green);
- tmp_blue += colorTransformDelta((byte) trans.red_to_blue, (byte) red);
-
- // No pointer dereferences in Java...
- // TODO: Consider passing an offset too, so we can modify in-place
- newRedBlue[0] = tmp_red & 0xff;
- newRedBlue[1] = tmp_blue & 0xff;
- }
-
- // A conversion from the 8-bit unsigned representation (uint8) to the 8-bit
- // signed one (int8) is required before calling ColorTransformDelta(). It
- // should be performed using 8-bit two's complement (that is: uint8 range
- // [128-255] is mapped to the [-128, -1] range of its converted int8
- // value).
- private static byte colorTransformDelta(final byte t, final byte c) {
- return (byte) ((t * c) >> 5);
- }
-
- private static void inverseTransform(final byte red, final byte green, final byte blue,
- final ColorTransformElement trans,
- final int[] newRedBlue) {
- // Applying inverse transform is just subtracting the
- // color transform deltas
- // Transformed values of red and blue components
- int tmp_red = red;
- int tmp_blue = blue;
-
- tmp_red -= colorTransformDelta((byte) trans.green_to_red, green);
- tmp_blue -= colorTransformDelta((byte) trans.green_to_blue, green);
- tmp_blue -= colorTransformDelta((byte) trans.red_to_blue, red); // Spec has red & 0xff
-
- newRedBlue[0] = tmp_red & 0xff;
- newRedBlue[1] = tmp_blue & 0xff;
- }
-
- private static void inverseTransform(final byte[] rgb, final ColorTransformElement trans) {
- // Applying inverse transform is just subtracting the
- // color transform deltas
- // Transformed values of red and blue components
- int tmp_red = rgb[0];
- int tmp_blue = rgb[2];
-
- tmp_red -= colorTransformDelta((byte) trans.green_to_red, rgb[1]);
- tmp_blue -= colorTransformDelta((byte) trans.green_to_blue, rgb[1]);
- tmp_blue -= colorTransformDelta((byte) trans.red_to_blue, rgb[0]); // Spec has red & 0xff
-
- rgb[0] = (byte) (tmp_red & 0xff);
- rgb[2] = (byte) (tmp_blue & 0xff);
- }
-
- private static void addGreenToBlueAndRed(byte[] rgb) {
- rgb[0] = (byte) ((rgb[0] + rgb[1]) & 0xff);
- rgb[2] = (byte) ((rgb[2] + rgb[1]) & 0xff);
- }
}
diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/huffman/HuffmanCodeGroup.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/huffman/HuffmanCodeGroup.java
new file mode 100644
index 00000000..bc1a629a
--- /dev/null
+++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/huffman/HuffmanCodeGroup.java
@@ -0,0 +1,25 @@
+package com.twelvemonkeys.imageio.plugins.webp.lossless.huffman;
+
+import com.twelvemonkeys.imageio.plugins.webp.LSBBitReader;
+
+import java.io.IOException;
+
+public class HuffmanCodeGroup {
+ /**
+ * Used for green, backward reference length and color cache
+ */
+ public final HuffmanTable mainCode;
+
+ public final HuffmanTable redCode;
+ public final HuffmanTable blueCode;
+ public final HuffmanTable alphaCode;
+ public final HuffmanTable distanceCode;
+
+ public HuffmanCodeGroup(LSBBitReader lsbBitReader, int colorCacheBits) throws IOException {
+ mainCode = new HuffmanTable(lsbBitReader, 256 + 24 + (colorCacheBits > 0 ? 1 << colorCacheBits : 0));
+ redCode = new HuffmanTable(lsbBitReader, 256);
+ blueCode = new HuffmanTable(lsbBitReader, 256);
+ alphaCode = new HuffmanTable(lsbBitReader, 256);
+ distanceCode = new HuffmanTable(lsbBitReader, 40);
+ }
+}
diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/huffman/HuffmanInfo.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/huffman/HuffmanInfo.java
new file mode 100644
index 00000000..c06bc8ed
--- /dev/null
+++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/huffman/HuffmanInfo.java
@@ -0,0 +1,17 @@
+package com.twelvemonkeys.imageio.plugins.webp.lossless.huffman;
+
+import java.awt.image.*;
+
+public class HuffmanInfo {
+ public Raster huffmanMetaCodes; //Raster allows intuitive lookup by x and y
+
+ public int metaCodeBits;
+
+ public HuffmanCodeGroup[] huffmanGroups;
+
+ public HuffmanInfo(Raster huffmanMetaCodes, int metaCodeBits, HuffmanCodeGroup[] huffmanGroups) {
+ this.huffmanMetaCodes = huffmanMetaCodes;
+ this.metaCodeBits = metaCodeBits;
+ this.huffmanGroups = huffmanGroups;
+ }
+}
\ No newline at end of file
diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/huffman/HuffmanTable.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/huffman/HuffmanTable.java
new file mode 100644
index 00000000..f0130357
--- /dev/null
+++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/huffman/HuffmanTable.java
@@ -0,0 +1,334 @@
+package com.twelvemonkeys.imageio.plugins.webp.lossless.huffman;
+
+import com.twelvemonkeys.imageio.plugins.webp.LSBBitReader;
+
+import javax.imageio.IIOException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Represents a single huffman tree as a table.
+ *
+ * Decoding a symbol just involves reading bits from the input stream and using that read value to index into the
+ * lookup table.
+ *
+ * Code length and the corresponding symbol are packed into one array element (int).
+ * This is done to avoid the overhead and the fragmentation over the whole heap involved with creating objects
+ * of a custom class. The upper 16 bits of each element are the code length and lower 16 bits are the symbol.
+ *
+ * The max allowed code length by the WEBP specification is 15, therefore this would mean the table needs to have
+ * 2^15 elements. To keep a reasonable memory usage, instead the lookup table only directly holds symbols with code
+ * length up to {@code LEVEL1_BITS} (Currently 8 bits). For longer codes the lookup table stores a reference to a
+ * second level lookup table. This reference consists of an element with length as the max length of the level 2
+ * table and value as the index of the table in the list of level 2 tables.
+ *
+ * Reading bits from the input is done in a least significant bit first way (LSB) way, therefore the prefix of the
+ * read value of length i is the lowest i bits in inverse order.
+ * The lookup table is directly indexed by the {@code LEVEL1_BITS} next bits read from the input (i.e. the bits
+ * corresponding to next code are inverse suffix of the read value/index).
+ * So for a code length of l all values with the lowest l bits the same need to decode to the same symbol
+ * regardless of the {@code (LEVEL1_BITS - l)} higher bits. So the lookup table needs to have the entry of this symbol
+ * repeated every 2^(l + 1) spots starting from the bitwise inverse of the code.
+ */
+public class HuffmanTable {
+
+ private static final int LEVEL1_BITS = 8;
+ /**
+ * Symbols of the L-code in the order they need to be read
+ */
+ private static final int[] L_CODE_ORDER = {17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
+ private final int[] level1 = new int[1 << LEVEL1_BITS];
+ private final List level2 = new ArrayList<>();
+
+ /**
+ * Build a Huffman table by reading the encoded symbol lengths from the reader
+ *
+ * @param lsbBitReader the reader to read from
+ * @param alphabetSize the number of symbols in the alphabet to be decoded by this huffman table
+ * @throws IOException when reading produces an exception
+ */
+ public HuffmanTable(LSBBitReader lsbBitReader, int alphabetSize) throws IOException {
+
+ boolean simpleLengthCode = lsbBitReader.readBit() == 1;
+
+ if (simpleLengthCode) {
+ int symbolNum = lsbBitReader.readBit() + 1;
+ boolean first8Bits = lsbBitReader.readBit() == 1;
+ short symbol1 = (short) lsbBitReader.readBits(first8Bits ? 8 : 1);
+
+ if (symbolNum == 2) {
+ short symbol2 = (short) lsbBitReader.readBits(8);
+
+ for (int i = 0; i < (1 << LEVEL1_BITS); i += 2) {
+ level1[i] = 1 << 16 | symbol1;
+ level1[i + 1] = 1 << 16 | symbol2;
+ }
+ }
+ else {
+ Arrays.fill(level1, symbol1);
+ }
+ }
+ 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)
+ */
+ int numLCodeLengths = (int) (lsbBitReader.readBits(4) + 4);
+ short[] lCodeLengths = new short[L_CODE_ORDER.length];
+ int numPosCodeLens = 0;
+
+ 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);
+ }
+ }
+
+ /**
+ * Builds a Huffman table by using already given code lengths to generate the codes from
+ *
+ * @param codeLengths the array specifying the bit length of the code for a symbol (i.e. {@code codeLengths[i]}
+ * is the bit length of the code for the symbol i)
+ * @param numPosCodeLens the number of positive (i.e. non-zero) codeLengths in the array (allows more efficient
+ * table generation)
+ */
+ private HuffmanTable(short[] codeLengths, int numPosCodeLens) {
+ buildFromLengths(codeLengths, numPosCodeLens);
+ }
+
+
+ /*
+ 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;
+ for (int i = 0; i < codeLengths.length; i++) {
+ if (codeLengths[i] != 0) {
+ lengthsAndSymbols[index++] = codeLengths[i] << 16 | i;
+ }
+ }
+
+ //Special case: Only 1 code value
+ if (numPosCodeLens == 1) {
+ //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.
+ 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.
+ */
+ int code = 0;
+
+ //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;
+
+ if (length <= LEVEL1_BITS) {
+ for (int j = code; j < level1.length; j += 1 << length) {
+ level1[j] = lengthAndSymbol;
+ }
+ }
+ 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.
+ */
+ int maxLength = length;
+ for (int j = i, openSlots = 1 << (length - LEVEL1_BITS);
+ j < lengthsAndSymbols.length && openSlots > 0;
+ j++, openSlots--) {
+
+ int innerLength = lengthsAndSymbols[j] >>> 16;
+
+ while (innerLength != maxLength) {
+ maxLength++;
+ openSlots <<= 1;
+ }
+ }
+
+ int level2Size = maxLength - LEVEL1_BITS;
+
+ currentTable = new int[1 << level2Size];
+ rootEntry = code & ((1 << LEVEL1_BITS) - 1);
+ level2.add(currentTable);
+
+ //Set root table indirection
+ level1[rootEntry] = (LEVEL1_BITS + level2Size) << 16 | (level2.size() - 1);
+ }
+
+ //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);
+ }
+ }
+
+ code = nextCode(code, length);
+
+ }
+ }
+
+ /**
+ * Computes the next code
+ *
+ * @param code the current code
+ * @param length the currently valid length
+ * @return {@code reverse(reverse(code, length) + 1, length)} where {@code reverse(a, b)} is the lowest b bits of
+ * a in inverted order
+ */
+ 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
+ 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
+ return (code & (step - 1)) | step;
+ }
+
+ private static short[] readCodeLengths(LSBBitReader lsbBitReader, short[] aCodeLengths, int alphabetSize,
+ int numPosCodeLens) throws IOException {
+
+ HuffmanTable huffmanTable = new HuffmanTable(aCodeLengths, numPosCodeLens);
+
+ //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));
+ codedSymbols = (int) (2 + lsbBitReader.readBits(maxSymbolBitLength));
+ }
+ else {
+ codedSymbols = alphabetSize;
+ }
+
+ short[] codeLengths = new short[alphabetSize];
+
+ //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
+ codeLengths[i] = len;
+ if (len != 0) {
+ prevLength = len;
+ }
+ }
+ else {
+ short repeatSymbol = 0;
+ int extraBits;
+ int repeatOffset;
+
+ switch (len) {
+ case 16: //Repeat previous
+ repeatSymbol = prevLength;
+ extraBits = 2;
+ repeatOffset = 3;
+ break;
+ case 17: //Repeat 0 short
+ extraBits = 3;
+ repeatOffset = 3;
+ break;
+ case 18: //Repeat 0 long
+ extraBits = 7;
+ repeatOffset = 11;
+ break;
+ default:
+ throw new IIOException("Huffman: Unreachable: Decoded Code Length > 18.");
+ }
+
+ int repeatCount = (int) (lsbBitReader.readBits(extraBits) + repeatOffset);
+
+
+ if (i + repeatCount > alphabetSize) {
+ throw new IIOException(
+ String.format(
+ "Huffman: Code length repeat count overflows alphabet: Start index: %d, count: " +
+ "%d, alphabet size: %d", i, repeatCount, alphabetSize)
+ );
+ }
+
+ Arrays.fill(codeLengths, i, i + repeatCount, repeatSymbol);
+ i += repeatCount - 1;
+
+ }
+ }
+
+
+ return codeLengths;
+ }
+
+ /**
+ * Reads the next code symbol from the streaming and decode it using the Huffman table
+ *
+ * @param lsbBitReader the reader to read a symbol from (will be advanced accordingly)
+ * @return the decoded symbol
+ * @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
+ lengthAndSymbol = level2.get(lengthAndSymbol & 0xffff)[level2Index];
+ length = lengthAndSymbol >>> 16;
+ }
+
+ lsbBitReader.readBits(length); //Consume bits
+
+ return (short) (lengthAndSymbol & 0xffff);
+ }
+}
diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/ColorIndexingTransform.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/ColorIndexingTransform.java
new file mode 100644
index 00000000..5d75e947
--- /dev/null
+++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/ColorIndexingTransform.java
@@ -0,0 +1,43 @@
+package com.twelvemonkeys.imageio.plugins.webp.lossless.transform;
+
+import java.awt.image.*;
+
+public class ColorIndexingTransform implements Transform {
+
+ private final byte[] colorTable;
+ private final byte bits;
+
+ public ColorIndexingTransform(byte[] colorTable, byte bits) {
+ this.colorTable = colorTable;
+ this.bits = bits;
+ }
+
+ @Override
+ public void applyInverse(WritableRaster raster) {
+
+ int width = raster.getWidth();
+ int height = raster.getHeight();
+
+ byte[] rgba = new byte[4];
+
+ for (int y = 0; y < height; y++) {
+ //Reversed so no used elements are overridden (in case of packing)
+ for (int x = width - 1; x >= 0; x--) {
+
+ int componentSize = 8 >> bits;
+ int packed = 1 << bits;
+ int xC = x / packed;
+ int componentOffset = componentSize * (x % packed);
+
+ int sample = raster.getSample(xC, y, 1);
+
+ int index = sample >> componentOffset & ((1 << componentSize) - 1);
+
+ //Arraycopy for 4 elements might not be beneficial
+ System.arraycopy(colorTable, index * 4, rgba, 0, 4);
+ raster.setDataElements(x, y, rgba);
+
+ }
+ }
+ }
+}
diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/ColorTransform.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/ColorTransform.java
new file mode 100644
index 00000000..9e90e2b6
--- /dev/null
+++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/ColorTransform.java
@@ -0,0 +1,93 @@
+package com.twelvemonkeys.imageio.plugins.webp.lossless.transform;
+
+import java.awt.image.*;
+
+public class ColorTransform implements Transform {
+ private final Raster data;
+ private final byte bits;
+
+ public ColorTransform(Raster raster, byte bits) {
+ this.data = raster;
+ this.bits = bits;
+ }
+
+ @Override
+ public void applyInverse(WritableRaster raster) {
+ int width = raster.getWidth();
+ int height = raster.getHeight();
+
+ byte[] rgba = new byte[4];
+
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+
+ data.getDataElements(x >> bits, y >> bits, rgba);
+ ColorTransformElement trans = new ColorTransformElement(rgba);
+
+ raster.getDataElements(x, y, rgba);
+
+ trans.inverseTransform(rgba);
+
+ raster.setDataElements(x, y, rgba);
+ }
+ }
+ }
+
+ // NOTE: For encoding!
+ private static void colorTransform(final int red, final int blue, final int green,
+ final ColorTransformElement trans,
+ final int[] newRedBlue) {
+ // Transformed values of red and blue components
+ int tmp_red = red;
+ int tmp_blue = blue;
+
+ // Applying transform is just adding the transform deltas
+ tmp_red += colorTransformDelta((byte) trans.green_to_red, (byte) green);
+ tmp_blue += colorTransformDelta((byte) trans.green_to_blue, (byte) green);
+ tmp_blue += colorTransformDelta((byte) trans.red_to_blue, (byte) red);
+
+ // No pointer dereferences in Java...
+ // TODO: Consider passing an offset too, so we can modify in-place
+ newRedBlue[0] = tmp_red & 0xff;
+ newRedBlue[1] = tmp_blue & 0xff;
+ }
+
+ // A conversion from the 8-bit unsigned representation (uint8) to the 8-bit
+ // signed one (int8) is required before calling ColorTransformDelta(). It
+ // should be performed using 8-bit two's complement (that is: uint8 range
+ // [128-255] is mapped to the [-128, -1] range of its converted int8
+ // value).
+ private static byte colorTransformDelta(final byte t, final byte c) {
+ return (byte) ((t * c) >> 5);
+ }
+
+ private static final class ColorTransformElement {
+
+ final int green_to_red;
+ final int green_to_blue;
+ final int red_to_blue;
+
+ ColorTransformElement(final byte[] rgba) {
+ this.green_to_red = rgba[2];
+ this.green_to_blue = rgba[1];
+ this.red_to_blue = rgba[0];
+ }
+
+ private void inverseTransform(final byte[] rgb) {
+ // Applying inverse transform is just adding (!, different from specification) the
+ // color transform deltas 3
+
+ // Transformed values of red and blue components
+ int tmp_red = rgb[0];
+ int tmp_blue = rgb[2];
+
+ tmp_red += colorTransformDelta((byte) this.green_to_red, rgb[1]);
+ tmp_blue += colorTransformDelta((byte) this.green_to_blue, rgb[1]);
+ tmp_blue += colorTransformDelta((byte) this.red_to_blue, (byte) tmp_red); // Spec has red & 0xff
+
+ rgb[0] = (byte) (tmp_red & 0xff);
+ rgb[2] = (byte) (tmp_blue & 0xff);
+ }
+ }
+
+}
diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/PredictorMode.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/PredictorMode.java
similarity index 97%
rename from imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/PredictorMode.java
rename to imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/PredictorMode.java
index 907c4ed2..d3e0bca1 100644
--- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/PredictorMode.java
+++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/PredictorMode.java
@@ -29,7 +29,7 @@
* POSSIBILITY OF SUCH DAMAGE.
*/
-package com.twelvemonkeys.imageio.plugins.webp.lossless;
+package com.twelvemonkeys.imageio.plugins.webp.lossless.transform;
/**
* PredictorMode.
diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/PredictorTransform.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/PredictorTransform.java
new file mode 100644
index 00000000..e5d7fe70
--- /dev/null
+++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/PredictorTransform.java
@@ -0,0 +1,238 @@
+package com.twelvemonkeys.imageio.plugins.webp.lossless.transform;
+
+import java.awt.image.*;
+
+import static java.lang.Math.*;
+
+public class PredictorTransform implements Transform {
+ private final Raster data;
+ private final byte bits;
+
+ public PredictorTransform(Raster raster, byte bits) {
+ this.data = raster;
+ this.bits = bits;
+ }
+
+ @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
+
+ //(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
+ for (int x = 1; x < width; x++) {
+ raster.getDataElements(x, 0, rgba);
+ raster.getDataElements(x - 1, 0, predictor);
+ addPixels(rgba, predictor);
+
+ raster.setDataElements(x, 0, rgba);
+ }
+
+ //(0,y) T predict
+ for (int y = 1; y < height; y++) {
+ raster.getDataElements(0, y, rgba);
+ raster.getDataElements(0, y - 1, predictor);
+ addPixels(rgba, predictor);
+
+ raster.setDataElements(0, y, rgba);
+ }
+
+ 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)
+ int trX = x == width - 1 ? 0 : x + 1;
+ int trY = x == width - 1 ? y : tY;
+
+ switch (transformType) {
+ case PredictorMode.BLACK:
+ rgba[3] += 0xff;
+ break;
+ case PredictorMode.L:
+ raster.getDataElements(lX, y, predictor);
+ addPixels(rgba, predictor);
+ break;
+ case PredictorMode.T:
+ raster.getDataElements(x, tY, predictor);
+ addPixels(rgba, predictor);
+ break;
+ case PredictorMode.TR:
+ raster.getDataElements(trX, trY, predictor);
+ addPixels(rgba, predictor);
+ break;
+ case PredictorMode.TL:
+ raster.getDataElements(lX, tY, predictor);
+ addPixels(rgba, predictor);
+ break;
+ case PredictorMode.AVG_L_TR_T:
+ raster.getDataElements(lX, y, predictor);
+ raster.getDataElements(trX, trY, predictor2);
+ average2(predictor, predictor2);
+
+ raster.getDataElements(x, tY, predictor2);
+ average2(predictor, predictor2);
+
+ addPixels(rgba, predictor);
+ break;
+ case PredictorMode.AVG_L_TL:
+ raster.getDataElements(lX, y, predictor);
+ raster.getDataElements(lX, tY, predictor2);
+ average2(predictor, predictor2);
+
+ addPixels(rgba, predictor);
+ break;
+ case PredictorMode.AVG_L_T:
+ raster.getDataElements(lX, y, predictor);
+ raster.getDataElements(x, tY, predictor2);
+ average2(predictor, predictor2);
+
+ addPixels(rgba, predictor);
+ break;
+ case PredictorMode.AVG_TL_T:
+ raster.getDataElements(lX, tY, predictor);
+ raster.getDataElements(x, tY, predictor2);
+ average2(predictor, predictor2);
+
+ addPixels(rgba, predictor);
+ break;
+ case PredictorMode.AVG_T_TR:
+ raster.getDataElements(x, tY, predictor);
+ raster.getDataElements(trX, trY, predictor2);
+ average2(predictor, predictor2);
+
+ addPixels(rgba, predictor);
+ break;
+ case PredictorMode.AVG_L_TL_T_TR:
+ raster.getDataElements(lX, y, predictor);
+ raster.getDataElements(lX, tY, predictor2);
+ average2(predictor, predictor2);
+
+ raster.getDataElements(x, tY, predictor2);
+ raster.getDataElements(trX, trY, predictor3);
+ average2(predictor2, predictor3);
+
+ average2(predictor, predictor2);
+
+ addPixels(rgba, predictor);
+ break;
+ case PredictorMode.SELECT:
+ raster.getDataElements(lX, y, predictor);
+ raster.getDataElements(x, tY, predictor2);
+ raster.getDataElements(lX, tY, predictor3);
+
+
+ addPixels(rgba, select(predictor, predictor2, predictor3));
+ break;
+ case PredictorMode.CLAMP_ADD_SUB_FULL:
+ raster.getDataElements(lX, y, predictor);
+ raster.getDataElements(x, tY, predictor2);
+ raster.getDataElements(lX, tY, predictor3);
+ clampAddSubtractFull(predictor, predictor2, predictor3);
+
+ addPixels(rgba, predictor);
+ break;
+ case PredictorMode.CLAMP_ADD_SUB_HALF:
+ raster.getDataElements(lX, y, predictor);
+ raster.getDataElements(x, tY, predictor2);
+ average2(predictor, predictor2);
+
+ raster.getDataElements(lX, tY, predictor2);
+ clampAddSubtractHalf(predictor, predictor2);
+
+ addPixels(rgba, predictor);
+ break;
+
+ }
+
+ raster.setDataElements(x, y, rgba);
+ }
+ }
+ }
+
+ private static byte[] select(final byte[] l, final byte[] t, final byte[] tl) {
+ // l = left pixel, t = top pixel, tl = top left pixel.
+
+ // ARGB component estimates for prediction.
+
+ int pAlpha = addSubtractFull(l[3], t[3], tl[3]);
+ int pRed = addSubtractFull(l[0], t[0], tl[0]);
+ int pGreen = addSubtractFull(l[1], t[1], tl[1]);
+ int pBlue = addSubtractFull(l[2], t[2], tl[2]);
+
+ // Manhattan distances to estimates for left and top pixels.
+ int pL = manhattanDistance(l, pAlpha, pRed, pGreen, pBlue);
+ int pT = manhattanDistance(t, pAlpha, pRed, pGreen, pBlue);
+
+ // Return either left or top, the one closer to the prediction.
+ return pL < pT ? l : t;
+ }
+
+ private static int manhattanDistance(byte[] rgba, int pAlpha, int pRed, int pGreen, int pBlue) {
+ return abs(pAlpha - (rgba[3] & 0xff)) + abs(pRed - (rgba[0] & 0xff)) +
+ abs(pGreen - (rgba[1] & 0xff)) + abs(pBlue - (rgba[2] & 0xff));
+ }
+
+ private static void average2(final byte[] rgba1, final byte[] rgba2) {
+ rgba1[0] = (byte) (((rgba1[0] & 0xff) + (rgba2[0] & 0xff)) / 2);
+ rgba1[1] = (byte) (((rgba1[1] & 0xff) + (rgba2[1] & 0xff)) / 2);
+ rgba1[2] = (byte) (((rgba1[2] & 0xff) + (rgba2[2] & 0xff)) / 2);
+ rgba1[3] = (byte) (((rgba1[3] & 0xff) + (rgba2[3] & 0xff)) / 2);
+ }
+
+ // Clamp the input value between 0 and 255.
+ private static int clamp(final int a) {
+ return max(0, min(a, 255));
+ }
+
+ private static void clampAddSubtractFull(final byte[] a, final byte[] b, final byte[] c) {
+ a[0] = (byte) clamp(addSubtractFull(a[0], b[0], c[0]));
+ a[1] = (byte) clamp(addSubtractFull(a[1], b[1], c[1]));
+ a[2] = (byte) clamp(addSubtractFull(a[2], b[2], c[2]));
+ a[3] = (byte) clamp(addSubtractFull(a[3], b[3], c[3]));
+ }
+
+ private static void clampAddSubtractHalf(final byte[] a, final byte[] b) {
+ a[0] = (byte) clamp(addSubtractHalf(a[0], b[0]));
+ a[1] = (byte) clamp(addSubtractHalf(a[1], b[1]));
+ a[2] = (byte) clamp(addSubtractHalf(a[2], b[2]));
+ a[3] = (byte) clamp(addSubtractHalf(a[3], b[3]));
+ }
+
+ private static int addSubtractFull(byte a, byte b, byte c) {
+ return (a & 0xff) + (b & 0xff) - (c & 0xff);
+ }
+
+ private static int addSubtractHalf(byte a, byte b) {
+ return (a & 0xff) + ((a & 0xff) - (b & 0xff)) / 2;
+ }
+
+ private static void addPixels(byte[] rgba, byte[] predictor) {
+ rgba[0] += predictor[0];
+ rgba[1] += predictor[1];
+ rgba[2] += predictor[2];
+ rgba[3] += predictor[3];
+ }
+
+}
diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/SubtractGreenTransform.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/SubtractGreenTransform.java
new file mode 100644
index 00000000..0de60314
--- /dev/null
+++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/SubtractGreenTransform.java
@@ -0,0 +1,29 @@
+package com.twelvemonkeys.imageio.plugins.webp.lossless.transform;
+
+import java.awt.image.*;
+
+public class SubtractGreenTransform implements Transform {
+
+
+ private static void addGreenToBlueAndRed(byte[] rgb) {
+ rgb[0] = (byte) ((rgb[0] + rgb[1]) & 0xff);
+ rgb[2] = (byte) ((rgb[2] + rgb[1]) & 0xff);
+ }
+
+ @Override
+ public void applyInverse(WritableRaster raster) {
+
+ int width = raster.getWidth();
+ int height = raster.getHeight();
+
+ byte[] rgba = new byte[4];
+
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ raster.getDataElements(x, y, rgba);
+ addGreenToBlueAndRed(rgba);
+ raster.setDataElements(x, y, rgba);
+ }
+ }
+ }
+}
diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/Transform.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/Transform.java
similarity index 82%
rename from imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/Transform.java
rename to imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/Transform.java
index d4aafa74..98394f1e 100644
--- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/Transform.java
+++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/Transform.java
@@ -29,27 +29,16 @@
* POSSIBILITY OF SUCH DAMAGE.
*/
-package com.twelvemonkeys.imageio.plugins.webp.lossless;
+package com.twelvemonkeys.imageio.plugins.webp.lossless.transform;
+
+import java.awt.image.WritableRaster;
/**
* Transform.
*
* @author Harald Kuhr
*/
-final class Transform {
- final int type;
- final Object data;
+public interface Transform {
- Transform(final int type, final Object data) {
- this.type = type;
- this.data = data;
- }
-
- byte[] getData() {
- return (byte[]) data;
- }
-
- int[] getColorMap() {
- return (int[]) data;
- }
+ void applyInverse(WritableRaster raster);
}
diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/TransformType.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/TransformType.java
similarity index 94%
rename from imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/TransformType.java
rename to imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/TransformType.java
index 525a6fa8..18ba23e6 100644
--- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/TransformType.java
+++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/transform/TransformType.java
@@ -29,7 +29,7 @@
* POSSIBILITY OF SUCH DAMAGE.
*/
-package com.twelvemonkeys.imageio.plugins.webp.lossless;
+package com.twelvemonkeys.imageio.plugins.webp.lossless.transform;
/**
* TransformType.
@@ -37,7 +37,7 @@ package com.twelvemonkeys.imageio.plugins.webp.lossless;
* @author Harald Kuhr
*/
// Hmm.. Why doesn't SUBTRACT_GREEN follow the convention?
-interface TransformType {
+public interface TransformType {
int PREDICTOR_TRANSFORM = 0;
int COLOR_TRANSFORM = 1;
int SUBTRACT_GREEN = 2;
diff --git a/imageio/imageio-webp/src/test/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReaderTest.java b/imageio/imageio-webp/src/test/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReaderTest.java
index 8d3b6dc6..244b89a6 100644
--- a/imageio/imageio-webp/src/test/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReaderTest.java
+++ b/imageio/imageio-webp/src/test/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReaderTest.java
@@ -39,14 +39,26 @@ public class WebPImageReaderTest extends ImageReaderAbstractTest