From 0160fb70f8c2bf78b85d38d5690f5f248f6c3478 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 6 Oct 2022 15:24:23 +0200 Subject: [PATCH] #702 Fix NPE while reading an WebP animation without alpha + bonus cleanup --- .../imageio/plugins/webp/WebPImageReader.java | 91 ++++++-------- .../plugins/webp/lossless/VP8LDecoder.java | 112 ++++++++---------- 2 files changed, 83 insertions(+), 120 deletions(-) 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 953ce1ca..5f35eb27 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 @@ -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"); 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 5930faa5..49c52017 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 @@ -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 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];