#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
*/
@ -216,7 +212,7 @@ final class WebPImageReader extends ImageReaderBase {
switch (chunk) {
case WebP.CHUNK_VP8_:
//https://tools.ietf.org/html/rfc6386#section-9.1
// https://tools.ietf.org/html/rfc6386#section-9.1
int frameType = lsbBitReader.readBit(); // 0 = key frame, 1 = inter frame (not used in WebP)
if (frameType != 0) {
@ -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,12 +476,11 @@ final class WebPImageReader extends ImageReaderBase {
switch (nextChunk) {
case WebP.CHUNK_ALPH:
readAlpha(destination, param, width, height);
break;
case WebP.CHUNK_VP8_:
readVP8(RasterUtils.asByteRaster(destination.getRaster())
.createWritableChild(0, 0, destination.getWidth(), destination.getHeight(), 0, 0, new int[]{ 0, 1, 2}), param);
.createWritableChild(0, 0, destination.getWidth(), destination.getHeight(), 0, 0, new int[] {0, 1, 2}), param);
break;
case WebP.CHUNK_VP8L:
@ -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
// 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}));
// 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}));
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,16 +74,13 @@ 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
//Skip past already read parts of header (signature, width, height, alpha, version) 5 Bytes in total
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,16 +114,16 @@ public final class VP8LDecoder {
WritableRaster fullSizeRaster;
WritableRaster decodeRaster;
if (topLevel) {
if (topLevel) {
Rectangle bounds = new Rectangle(width, height);
fullSizeRaster = getRasterForDecoding(raster, param, bounds);
//If multiple indices packed into one pixel xSize is different from raster width
// If multiple indices packed into one pixel xSize is different from raster width
decodeRaster = fullSizeRaster.createWritableChild(0, 0, xSize, height, 0, 0, null);
}
else {
//All recursive calls have Rasters of the correct sizes with origin (0, 0)
// All recursive calls have Rasters of the correct sizes with origin (0, 0)
decodeRaster = fullSizeRaster = raster;
}
@ -137,7 +135,7 @@ public final class VP8LDecoder {
}
if (fullSizeRaster != raster && param != null) {
//Copy into destination raster with settings applied
// Copy into destination raster with settings applied
Rectangle sourceRegion = param.getSourceRegion();
int sourceXSubsampling = param.getSourceXSubsampling();
int sourceYSubsampling = param.getSourceYSubsampling();
@ -150,18 +148,17 @@ public final class VP8LDecoder {
}
if (sourceXSubsampling == 1 && sourceYSubsampling == 1) {
//Only apply offset (and limit to requested region)
// Only apply offset (and limit to requested region)
raster.setRect(destinationOffset.x, destinationOffset.y, fullSizeRaster);
}
else {
//Manual copy, more efficient way might exist
// Manual copy, more efficient way might exist
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);
}
@ -171,14 +168,14 @@ 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
// 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) {
//Can't reuse existing
// Can't reuse existing
return Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, bounds.width, bounds.height,
4 * bounds.width, 4, new int[] {0, 1, 2, 3}, null);
}
@ -189,12 +186,13 @@ public final class VP8LDecoder {
}
}
if (!raster.getBounds().contains(bounds)) {
//Can't reuse existing
// Can't reuse existing
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)
// Recenter to (0, 0)
raster.createWritableChild(bounds.x, bounds.y, bounds.width, bounds.height, 0, 0, null) :
raster;
}
@ -210,47 +208,39 @@ 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
// Crossed border into new metaGroup
int index = huffmanInfo.huffmanMetaCodes.getSample(x >> huffmanInfo.metaCodeBits, y >> huffmanInfo.metaCodeBits, 0);
curCodeGroup = huffmanInfo.huffmanGroups[index];
}
short code = curCodeGroup.mainCode.readSymbol(lsbBitReader);
if (code < 256) { //Literal
if (code < 256) { // Literal
decodeLiteral(raster, colorCache, curCodeGroup, rgba, y, x, code);
}
else if (code < 256 + 24) { //backward reference
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
// Decrement one because for loop already increments by one
x--;
y = y + ((x + length) / width);
x = (x + length) % width;
//Reset Huffman meta group
// 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
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));
}
@ -284,16 +276,15 @@ public final class VP8LDecoder {
int xSrc, ySrc;
if (distanceCode > 120) {
//Linear distance
// Linear distance
int distance = distanceCode - 120;
ySrc = y - (distance / width);
xSrc = x - (distance % width);
}
else {
//See comment of distances array
// See comment of distances array
xSrc = x - (8 - (DISTANCES[distanceCode - 1] & 0xf));
ySrc = y - (DISTANCES[distanceCode - 1] >> 4);
}
if (xSrc < 0) {
@ -306,14 +297,15 @@ 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)
// 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
// 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 {
@ -347,7 +338,7 @@ public final class VP8LDecoder {
switch (transformType) {
case TransformType.PREDICTOR_TRANSFORM:
//Intentional Fallthrough
// Intentional Fallthrough
case TransformType.COLOR_TRANSFORM: {
// The two first transforms contains the exact same data, can be combined
@ -360,7 +351,7 @@ public final class VP8LDecoder {
new int[] {0, 1, 2, 3}, null);
readVP8Lossless(raster, false, null, blockWidth, blockHeight);
//Keep data as raster for convenient (x,y) indexing
// Keep data as raster for convenient (x,y) indexing
if (transformType == TransformType.PREDICTOR_TRANSFORM) {
transforms.add(0, new PredictorTransform(raster, sizeBits));
}
@ -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,12 +387,10 @@ 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
// resolve subtraction code
for (int i = 4; i < colorTable.length; i++) {
colorTable[i] += colorTable[i - 4];
}
@ -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,32 +423,30 @@ public final class VP8LDecoder {
int metaCodeBits = 0;
WritableRaster huffmanMetaCodes = null;
if (readMetaCodes && lsbBitReader.readBit() == 1) {
//read in meta codes
// read in meta codes
metaCodeBits = (int) lsbBitReader.readBits(3) + 2;
huffmanXSize = subSampleSize(xSize, metaCodeBits);
huffmanYSize = subSampleSize(ySize, metaCodeBits);
//Raster with elements as BARG (only the RG components encode the meta group)
// 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
// 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
*/
// 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];