mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2026-05-01 00:00:02 -04: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:
+1
-1
@@ -565,7 +565,7 @@ final class WebPImageReader extends ImageReaderBase {
|
|||||||
readVP8Lossless(tempRaster, null, width, height);
|
readVP8Lossless(tempRaster, null, width, height);
|
||||||
|
|
||||||
// Copy from green (band 1) in temp to alpha in destination
|
// 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);
|
alphaFilter(alphaChannel, filtering);
|
||||||
copyIntoRasterWithParams(alphaChannel, alphaRaster, param);
|
copyIntoRasterWithParams(alphaChannel, alphaRaster, param);
|
||||||
break;
|
break;
|
||||||
|
|||||||
+50
-42
@@ -166,73 +166,81 @@ final class HuffmanTable {
|
|||||||
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);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
||||||
|
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.
|
// 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;
|
||||||
|
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 rootEntry = -1;
|
||||||
int[] currentTable = null;
|
int[] currentTable = null;
|
||||||
|
|
||||||
for (int i = 0; i < lengthsAndSymbols.length; i++) {
|
step = 2;
|
||||||
int lengthAndSymbol = lengthsAndSymbols[i];
|
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) {
|
currentTable = new int[level2Size];
|
||||||
for (int j = code; j < level1.length; j += 1 << length) {
|
rootEntry = code & rootMask;
|
||||||
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);
|
level2.add(currentTable);
|
||||||
|
|
||||||
// Set root table indirection
|
// 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
|
int value = (length - LEVEL1_BITS) << 16 | (lengthAndSymbol & 0xffff);
|
||||||
for (int j = (code >>> LEVEL1_BITS); j < currentTable.length; j += 1 << (length - LEVEL1_BITS)) {
|
for (int j = (code >>> LEVEL1_BITS); j < currentTable.length; j += step) {
|
||||||
currentTable[j] = (length - LEVEL1_BITS) << 16 | (lengthAndSymbol & 0xffff);
|
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
|
* Computes the next code
|
||||||
*
|
*
|
||||||
|
|||||||
+11
@@ -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 {
|
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 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);
|
short distancePrefix = curCodeGroup.distanceCode.readSymbol(lsbBitReader);
|
||||||
int distanceCode = lz77decode(distancePrefix);
|
int distanceCode = lz77decode(distancePrefix);
|
||||||
|
|
||||||
@@ -302,6 +308,11 @@ public final class VP8LDecoder {
|
|||||||
ySrc++;
|
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--) {
|
for (int l = length; l > 0; x++, l--) {
|
||||||
// Check length and xSrc, ySrc not falling outside raster? (Should not occur if image is correct)
|
// Check length and xSrc, ySrc not falling outside raster? (Should not occur if image is correct)
|
||||||
if (x == width) {
|
if (x == width) {
|
||||||
|
|||||||
+75
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
package com.twelvemonkeys.imageio.plugins.webp;
|
package com.twelvemonkeys.imageio.plugins.webp;
|
||||||
|
|
||||||
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest;
|
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest;
|
||||||
@@ -11,6 +12,8 @@ import javax.imageio.stream.MemoryCacheImageInputStream;
|
|||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.*;
|
import java.awt.image.*;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
@@ -266,4 +269,76 @@ public class WebPImageReaderTest extends ImageReaderAbstractTest<WebPImageReader
|
|||||||
reader.dispose();
|
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