#702 Fix NPE while reading an WebP animation without alpha

+ bonus cleanup
This commit is contained in:
Harald Kuhr 2022-10-06 15:24:23 +02:00
parent 29dca0f124
commit 0160fb70f8
2 changed files with 83 additions and 120 deletions

View File

@ -31,29 +31,6 @@
package com.twelvemonkeys.imageio.plugins.webp;
import java.awt.*;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.imageio.IIOException;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.color.ColorProfiles;
import com.twelvemonkeys.imageio.color.ColorSpaces;
@ -68,6 +45,25 @@ import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.imageio.util.RasterUtils;
import javax.imageio.IIOException;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import java.awt.*;
import java.awt.color.*;
import java.awt.image.*;
import java.io.IOException;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import static java.lang.Math.max;
import static java.lang.Math.min;
/**
* WebPImageReader
*/
@ -436,7 +432,6 @@ final class WebPImageReader extends ImageReaderBase {
if (header.containsANIM) {
AnimationFrame frame = frames.get(imageIndex);
imageInput.seek(frame.offset + 16);
opaqueAlpha(destination.getAlphaRaster());
readVP8Extended(destination, param, frame.offset + frame.length, frame.bounds.width, frame.bounds.height);
}
else {
@ -466,8 +461,7 @@ final class WebPImageReader extends ImageReaderBase {
readVP8Extended(destination, param, streamEnd, header.width, header.height);
}
private void readVP8Extended(BufferedImage destination, ImageReadParam param, long streamEnd, final int width,
final int height) throws IOException {
private void readVP8Extended(BufferedImage destination, ImageReadParam param, long streamEnd, final int width, final int height) throws IOException {
while (imageInput.getStreamPosition() < streamEnd) {
int nextChunk = imageInput.readInt();
long chunkLength = imageInput.readUnsignedInt();
@ -482,7 +476,6 @@ final class WebPImageReader extends ImageReaderBase {
switch (nextChunk) {
case WebP.CHUNK_ALPH:
readAlpha(destination, param, width, height);
break;
case WebP.CHUNK_VP8_:
@ -519,8 +512,7 @@ final class WebPImageReader extends ImageReaderBase {
int reserved = (int) imageInput.readBits(2);
if (reserved != 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);
@ -539,15 +531,14 @@ final class WebPImageReader extends ImageReaderBase {
readUncompressedAlpha(alphaRaster);
break;
case 1:
WritableRaster tempRaster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE,
destination.getWidth(), destination.getHeight(), 4,
destination.getRaster().getBounds().getLocation());
// Simulate header
imageInput.seek(imageInput.getStreamPosition() - 5);
WritableRaster tempRaster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, destination.getWidth(), destination.getHeight(), 4, null);
readVP8Lossless(tempRaster, param, width, height);
// Copy from green (band 1) in temp to alpha in destination
alphaRaster.setRect(tempRaster.createChild(0, 0, tempRaster.getWidth(),
tempRaster.getHeight(), 0, 0, new int[] {1}));
alphaRaster.setRect(tempRaster.createChild(0, 0, tempRaster.getWidth(), tempRaster.getHeight(), 0, 0, new int[] {1}));
break;
default:
processWarningOccurred("Unknown WebP alpha compression: " + compression);
@ -571,34 +562,21 @@ final class WebPImageReader extends ImageReaderBase {
return 0;
case AlphaFiltering.HORIZONTAL:
if (x == 0) {
if (y == 0) {
return 0;
}
else {
return alphaRaster.getSample(0, y - 1, 0);
}
return y == 0 ? 0 : alphaRaster.getSample(0, y - 1, 0);
}
else {
return alphaRaster.getSample(x - 1, y, 0);
}
case AlphaFiltering.VERTICAL:
if (y == 0) {
if (x == 0) {
return 0;
}
else {
return alphaRaster.getSample(x - 1, 0, 0);
}
return x == 0 ? 0 : alphaRaster.getSample(x - 1, 0, 0);
}
else {
return alphaRaster.getSample(x, y - 1, 0);
}
case AlphaFiltering.GRADIENT:
if (x == 0 && y == 0) {
return 0;
}
else if (x == 0) {
return alphaRaster.getSample(0, y - 1, 0);
if (x == 0) {
return y == 0 ? 0 : alphaRaster.getSample(0, y - 1, 0);
}
else if (y == 0) {
return alphaRaster.getSample(x - 1, 0, 0);
@ -608,7 +586,7 @@ final class WebPImageReader extends ImageReaderBase {
int top = alphaRaster.getSample(x, y - 1, 0);
int topLeft = alphaRaster.getSample(x - 1, y - 1, 0);
return Math.max(0, Math.min(left + top - topLeft, 255));
return max(0, min(left + top - topLeft, 255));
}
default:
processWarningOccurred("Unknown WebP alpha filtering: " + filtering);
@ -647,6 +625,7 @@ final class WebPImageReader extends ImageReaderBase {
}
}
@SuppressWarnings("RedundantThrows")
private void readUncompressedAlpha(final WritableRaster alphaRaster) throws IOException {
// Hardly used in practice, need to find a sample file
processWarningOccurred("Uncompressed WebP alpha not implemented");

View File

@ -74,15 +74,12 @@ public final class VP8LDecoder {
private final ImageInputStream imageInput;
private final LSBBitReader lsbBitReader;
public VP8LDecoder(final ImageInputStream imageInput, final boolean debug) {
public VP8LDecoder(final ImageInputStream imageInput, @SuppressWarnings("unused") final boolean debug) {
this.imageInput = imageInput;
lsbBitReader = new LSBBitReader(imageInput);
this.lsbBitReader = new LSBBitReader(imageInput);
}
public void readVP8Lossless(final WritableRaster raster, final boolean topLevel, ImageReadParam param, int width,
int height) throws IOException {
//https://github.com/webmproject/libwebp/blob/666bd6c65483a512fe4c2eb63fbc198b6fb4fae4/src/dec/vp8l_dec.c#L1114
public void readVP8Lossless(final WritableRaster raster, final boolean topLevel, ImageReadParam param, int width, int height) throws IOException {
// Skip past already read parts of header (signature, width, height, alpha, version) 5 Bytes in total
if (topLevel) {
imageInput.seek(imageInput.getStreamPosition() + 5);
@ -100,6 +97,7 @@ public final class VP8LDecoder {
int colorCacheBits = 0;
if (lsbBitReader.readBit() == 1) {
colorCacheBits = (int) lsbBitReader.readBits(4);
if (colorCacheBits < 1 || colorCacheBits > 11) {
throw new IIOException("Corrupt WebP stream, colorCacheBits < 1 || > 11: " + colorCacheBits);
}
@ -116,8 +114,8 @@ public final class VP8LDecoder {
WritableRaster fullSizeRaster;
WritableRaster decodeRaster;
if (topLevel) {
if (topLevel) {
Rectangle bounds = new Rectangle(width, height);
fullSizeRaster = getRasterForDecoding(raster, param, bounds);
@ -158,10 +156,9 @@ public final class VP8LDecoder {
byte[] rgba = new byte[4];
int xEnd = raster.getWidth() + raster.getMinX();
int yEnd = raster.getHeight() + raster.getMinY();
for (int xDst = destinationOffset.x, xSrc = sourceRegion.x + subsamplingXOffset;
xDst < xEnd; xDst++, xSrc += sourceXSubsampling) {
for (int yDst = destinationOffset.y, ySrc = sourceRegion.y + subsamplingYOffset;
yDst < yEnd; yDst++, ySrc += sourceYSubsampling) {
for (int xDst = destinationOffset.x, xSrc = sourceRegion.x + subsamplingXOffset; xDst < xEnd; xDst++, xSrc += sourceXSubsampling) {
for (int yDst = destinationOffset.y, ySrc = sourceRegion.y + subsamplingYOffset; yDst < yEnd; yDst++, ySrc += sourceYSubsampling) {
fullSizeRaster.getDataElements(xSrc, ySrc, rgba);
raster.setDataElements(xDst, yDst, rgba);
}
@ -173,8 +170,8 @@ public final class VP8LDecoder {
private WritableRaster getRasterForDecoding(WritableRaster raster, ImageReadParam param, Rectangle bounds) {
// If the ImageReadParam requires only a subregion of the image, and if the whole image does not fit into the
// Raster or subsampling is requested, we need a temporary Raster as we can only decode the whole image at once
boolean originSet = false;
if (param != null) {
if (param.getSourceRegion() != null && !param.getSourceRegion().contains(bounds) ||
param.getSourceXSubsampling() != 1 || param.getSourceYSubsampling() != 1) {
@ -193,6 +190,7 @@ public final class VP8LDecoder {
return Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, bounds.width, bounds.height, 4 * bounds.width,
4, new int[] {0, 1, 2, 3}, null);
}
return originSet ?
// Recenter to (0, 0)
raster.createWritableChild(bounds.x, bounds.y, bounds.width, bounds.height, 0, 0, null) :
@ -210,7 +208,6 @@ public final class VP8LDecoder {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if ((x & huffmanMask) == 0 && huffmanInfo.huffmanMetaCodes != null) {
// Crossed border into new metaGroup
int index = huffmanInfo.huffmanMetaCodes.getSample(x >> huffmanInfo.metaCodeBits, y >> huffmanInfo.metaCodeBits, 0);
@ -221,10 +218,8 @@ public final class VP8LDecoder {
if (code < 256) { // Literal
decodeLiteral(raster, colorCache, curCodeGroup, rgba, y, x, code);
}
else if (code < 256 + 24) { // backward reference
int length = decodeBwRef(raster, colorCache, width, curCodeGroup, rgba, code, x, y);
// Decrement one because for loop already increments by one
@ -232,25 +227,20 @@ public final class VP8LDecoder {
y = y + ((x + length) / width);
x = (x + length) % width;
// Reset Huffman meta group
if (y < height && x < width && huffmanInfo.huffmanMetaCodes != null) {
int index = huffmanInfo.huffmanMetaCodes.getSample(x >> huffmanInfo.metaCodeBits, y >> huffmanInfo.metaCodeBits, 0);
curCodeGroup = huffmanInfo.huffmanGroups[index];
}
}
else { // colorCache
decodeCached(raster, colorCache, rgba, y, x, code);
}
}
}
}
private void decodeCached(WritableRaster raster, ColorCache colorCache, byte[] rgba, int y, int x, short code) {
int argb = colorCache.lookup(code - 256 - 24);
rgba[0] = (byte) ((argb >> 16) & 0xff);
@ -265,11 +255,13 @@ public final class VP8LDecoder {
byte red = (byte) curCodeGroup.redCode.readSymbol(lsbBitReader);
byte blue = (byte) curCodeGroup.blueCode.readSymbol(lsbBitReader);
byte alpha = (byte) curCodeGroup.alphaCode.readSymbol(lsbBitReader);
rgba[0] = red;
rgba[1] = (byte) code;
rgba[2] = blue;
rgba[3] = alpha;
raster.setDataElements(x, y, rgba);
if (colorCache != null) {
colorCache.insert((alpha & 0xff) << 24 | (red & 0xff) << 16 | (code & 0xff) << 8 | (blue & 0xff));
}
@ -293,7 +285,6 @@ public final class VP8LDecoder {
// See comment of distances array
xSrc = x - (8 - (DISTANCES[distanceCode - 1] & 0xf));
ySrc = y - (DISTANCES[distanceCode - 1] >> 4);
}
if (xSrc < 0) {
@ -307,13 +298,14 @@ public final class VP8LDecoder {
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) {
x = 0;
y++;
}
raster.getDataElements(xSrc++, ySrc, rgba);
raster.setDataElements(x, y, rgba);
if (xSrc == width) {
xSrc = 0;
ySrc++;
@ -322,22 +314,21 @@ public final class VP8LDecoder {
colorCache.insert((rgba[3] & 0xff) << 24 | (rgba[0] & 0xff) << 16 | (rgba[1] & 0xff) << 8 | (rgba[2] & 0xff));
}
}
return length;
}
private int lz77decode(int prefixCode) throws IOException {
// According to specification
if (prefixCode < 4) {
return prefixCode + 1;
}
else {
int extraBits = (prefixCode - 2) >> 1;
int offset = (2 + (prefixCode & 1)) << extraBits;
return offset + (int) lsbBitReader.readBits(extraBits) + 1;
}
}
private int readTransform(int xSize, int ySize, List<Transform> transforms) throws IOException {
@ -376,7 +367,6 @@ public final class VP8LDecoder {
break;
}
case TransformType.COLOR_INDEXING_TRANSFORM: {
// 8 bit value for color table size
int colorTableSize = ((int) lsbBitReader.readBits(8)) + 1; // 1-256
@ -387,7 +377,6 @@ public final class VP8LDecoder {
colorTableSize > 4 ? 16 :
colorTableSize > 2 ? 4 : 2;
byte[] colorTable = new byte[safeColorTableSize * 4];
// The color table can be obtained by reading an image,
@ -398,10 +387,8 @@ public final class VP8LDecoder {
readVP8Lossless(
Raster.createInterleavedRaster(
new DataBufferByte(colorTable, colorTableSize * 4),
colorTableSize, 1, colorTableSize * 4,
4, new int[] {0, 1, 2, 3}, null)
, false, null, colorTableSize, 1);
colorTableSize, 1, colorTableSize * 4, 4, new int[] {0, 1, 2, 3}, null),
false, null, colorTableSize, 1);
// resolve subtraction code
for (int i = 4; i < colorTable.length; i++) {
@ -428,8 +415,7 @@ public final class VP8LDecoder {
return xSize;
}
private HuffmanInfo readHuffmanCodes(int xSize, int ySize, int colorCacheBits, boolean readMetaCodes)
throws IOException {
private HuffmanInfo readHuffmanCodes(int xSize, int ySize, int colorCacheBits, boolean readMetaCodes) throws IOException {
int huffmanGroupNum = 1;
int huffmanXSize;
int huffmanYSize;
@ -437,6 +423,7 @@ public final class VP8LDecoder {
int metaCodeBits = 0;
WritableRaster huffmanMetaCodes = null;
if (readMetaCodes && lsbBitReader.readBit() == 1) {
// read in meta codes
metaCodeBits = (int) lsbBitReader.readBits(3) + 2;
@ -456,13 +443,10 @@ public final class VP8LDecoder {
}
huffmanGroupNum = maxCode + 1;
/*
New Raster with just RG components exposed as single band allowing simple access of metaGroupIndex with
x,y lookup
*/
// 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];