diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java index 7446611b..bb794554 100644 --- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java +++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java @@ -565,7 +565,7 @@ final class WebPImageReader extends ImageReaderBase { readVP8Lossless(tempRaster, null, width, height); // Copy from green (band 1) in temp to alpha in destination - WritableRaster alphaChannel = tempRaster.createWritableChild(0, 0, tempRaster.getWidth(), tempRaster.getHeight(), 0, 0, new int[]{1}); + WritableRaster alphaChannel = tempRaster.createWritableChild(0, 0, width, height, 0, 0, new int[]{1}); alphaFilter(alphaChannel, filtering); copyIntoRasterWithParams(alphaChannel, alphaRaster, param); break; diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/HuffmanTable.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/HuffmanTable.java index a1909278..21a81276 100644 --- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/HuffmanTable.java +++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/HuffmanTable.java @@ -166,73 +166,81 @@ final class HuffmanTable { if (numPosCodeLens == 1) { // Length is 0 so mask to clear length bits Arrays.fill(level1, lengthsAndSymbols[0] & 0xffff); + return; } // Due to the layout of the elements this effectively first sorts by length and then symbol. Arrays.sort(lengthsAndSymbols); + int[] count = new int[16]; + for (int lengthAndSymbol : lengthsAndSymbols) { + count[lengthAndSymbol >>> 16]++; + } + // 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; + int step = 2; + index = 0; - // Used for level2 lookup + for (int length = 1; length <= LEVEL1_BITS; length++, step <<= 1) { + for (; count[length] > 0; count[length]--) { + int lengthAndSymbol = lengthsAndSymbols[index++]; + + for (int j = code; j < level1.length; j += step) { + level1[j] = lengthAndSymbol; + } + + code = nextCode(code, length); + } + } + + int rootMask = (1 << LEVEL1_BITS) - 1; int rootEntry = -1; int[] currentTable = null; - for (int i = 0; i < lengthsAndSymbols.length; i++) { - int lengthAndSymbol = lengthsAndSymbols[i]; + step = 2; + for (int length = LEVEL1_BITS + 1; length <= 15; length++, step <<= 1) { + for (; count[length] > 0; count[length]--) { + int lengthAndSymbol = lengthsAndSymbols[index++]; - int length = lengthAndSymbol >>> 16; + if ((code & rootMask) != rootEntry) { + int level2Bits = nextTableBitSize(count, length, LEVEL1_BITS); + int level2Size = 1 << level2Bits; - 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); + currentTable = new int[level2Size]; + rootEntry = code & rootMask; level2.add(currentTable); // Set root table indirection - level1[rootEntry] = (LEVEL1_BITS + level2Size) << 16 | (level2.size() - 1); + level1[rootEntry] = (LEVEL1_BITS + level2Bits) << 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); + int value = (length - LEVEL1_BITS) << 16 | (lengthAndSymbol & 0xffff); + for (int j = (code >>> LEVEL1_BITS); j < currentTable.length; j += step) { + currentTable[j] = value; } + + code = nextCode(code, length); } - - code = nextCode(code, length); - } } + private static int nextTableBitSize(int[] count, int length, int rootBits) { + int left = 1 << (length - rootBits); + while (length < 15) { + left -= count[length]; + if (left <= 0) { + break; + } + length++; + left <<= 1; + } + + return length - rootBits; + } + /** * Computes the next code * diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/VP8LDecoder.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/VP8LDecoder.java index 73a8695d..97adfb72 100644 --- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/VP8LDecoder.java +++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/VP8LDecoder.java @@ -276,6 +276,12 @@ public final class VP8LDecoder { private int decodeBwRef(WritableRaster raster, ColorCache colorCache, int width, HuffmanCodeGroup curCodeGroup, byte[] rgba, short code, int x, int y) throws IOException { int length = lz77decode(code - 256); + int remaining = width * raster.getHeight() - (y * width + x); + if (length > remaining) { + throw new IIOException("Corrupt WebP stream, backward reference exceeds image bounds: length=" + length + + ", remaining=" + remaining + ", x=" + x + ", y=" + y); + } + short distancePrefix = curCodeGroup.distanceCode.readSymbol(lsbBitReader); int distanceCode = lz77decode(distancePrefix); @@ -302,6 +308,11 @@ public final class VP8LDecoder { ySrc++; } + if (ySrc < 0 || ySrc >= raster.getHeight()) { + throw new IIOException("Corrupt WebP stream, backward reference outside image: distance=" + distanceCode + + ", x=" + x + ", y=" + y + ", xSrc=" + xSrc + ", ySrc=" + ySrc); + } + for (int l = length; l > 0; x++, l--) { // Check length and xSrc, ySrc not falling outside raster? (Should not occur if image is correct) if (x == width) { 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 a27951ce..3c920a1c 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 @@ -1,3 +1,4 @@ + package com.twelvemonkeys.imageio.plugins.webp; import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; @@ -11,6 +12,8 @@ import javax.imageio.stream.MemoryCacheImageInputStream; import java.awt.*; import java.awt.image.*; import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.List; import static java.util.Arrays.asList; @@ -266,4 +269,76 @@ public class WebPImageReaderTest extends ImageReaderAbstractTest{@code + * dwebp imageio/imageio-webp/src/test/resources/webp/lossless.transparent.webp -o /tmp/lossless.transparent.png + * magick /tmp/lossless.transparent.png -alpha extract -depth 8 gray:/tmp/lossless.transparent-alpha.raw + * shasum -a 256 /tmp/lossless.transparent-alpha.raw + * } + * + * @throws IOException + */ + @Test + public void testReadWriteTransparentWebP() throws IOException { + WebPImageReader reader = createReader(); + + try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource("/webp/lossless.transparent.webp"))) { + reader.setInput(stream); + + // Read dimensions + int width = reader.getWidth(0); + int height = reader.getHeight(0); + assertEquals(1920, width, "Expected width of 1920"); + assertEquals(1477, height, "Expected height of 1477"); + + // Read the full image and validate alpha output (exercises long LZ77 back-references). + BufferedImage image = reader.read(0); + assertNotNull(image, "Image should not be null"); + assertEquals(width, image.getWidth(), "Image width should match"); + assertEquals(height, image.getHeight(), "Image height should match"); + assertTrue(image.getColorModel().hasAlpha(), "Image should have alpha channel"); + assertEquals("79ffff20392a9cef308b317cbac9d3e57f78e26a4f49fb38b3f3b4dbc4e63c50", + sha256Alpha(image), "Alpha plane hash mismatch"); + } + finally { + reader.dispose(); + } + } + + private static String sha256Alpha(BufferedImage image) { + WritableRaster alphaRaster = image.getAlphaRaster(); + assertNotNull(alphaRaster, "Image should have alpha raster"); + + int width = alphaRaster.getWidth(); + int height = alphaRaster.getHeight(); + int[] samples = alphaRaster.getSamples(0, 0, width, height, 0, (int[]) null); + + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-256"); + } + catch (NoSuchAlgorithmException e) { + throw new AssertionError("SHA-256 not available", e); + } + + for (int sample : samples) { + digest.update((byte) sample); + } + + return toHex(digest.digest()); + } + + private static String toHex(byte[] bytes) { + StringBuilder builder = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + builder.append(Character.forDigit((b >>> 4) & 0x0f, 16)); + builder.append(Character.forDigit(b & 0x0f, 16)); + } + return builder.toString(); + } } diff --git a/imageio/imageio-webp/src/test/resources/webp/lossless.transparent.webp b/imageio/imageio-webp/src/test/resources/webp/lossless.transparent.webp new file mode 100644 index 00000000..218d996e Binary files /dev/null and b/imageio/imageio-webp/src/test/resources/webp/lossless.transparent.webp differ