mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2026-01-24 00:00:05 -05:00
Fixes Lossless Alpha Channel WebP (#1243)
* Fix alpha channel dimensions in WebP lossless decoding Use the expected width and height values instead of tempRaster.getWidth()/getHeight() when creating the alpha channel's writable child raster. This ensures the alpha channel is correctly sized when the temp raster dimensions differ from the expected dimensions. * Fixes Lossless Huffman table based on libwebp * Remove redundant flush call in image reader test Removed unnecessary image.flush() call in WebPImageReaderTest. * adding code to generate good hash for reproducability
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<WebPImageReader
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This test compares alpha channel information that is decoded by the WebPImageReader with the known "good" alpha
|
||||
* channel information. To generate the known "good" alpha channel information, we use the command line and libwebp,
|
||||
* e.g.
|
||||
*
|
||||
* <pre>{@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
|
||||
* }</pre>
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Reference in New Issue
Block a user