Support ImageReadParam Settings limiting Raster size

On animation frames dimension has to be passed as it is not guaranteed
the same as in the file header.
This commit is contained in:
Simon Kammermeier 2022-09-09 00:42:55 +02:00
parent 67b48ce1e3
commit c858454c5a
2 changed files with 107 additions and 21 deletions

View File

@ -427,7 +427,7 @@ final class WebPImageReader extends ImageReaderBase {
AnimationFrame frame = frames.get(imageIndex); AnimationFrame frame = frames.get(imageIndex);
imageInput.seek(frame.offset + 16); imageInput.seek(frame.offset + 16);
opaqueAlpha(destination.getAlphaRaster()); opaqueAlpha(destination.getAlphaRaster());
readVP8Extended(destination, param, frame.offset + frame.length); readVP8Extended(destination, param, frame.offset + frame.length, frame.bounds.width, frame.bounds.height);
} }
else { else {
imageInput.seek(header.offset + header.length); imageInput.seek(header.offset + header.length);
@ -453,6 +453,11 @@ final class WebPImageReader extends ImageReaderBase {
} }
private void readVP8Extended(BufferedImage destination, ImageReadParam param, long streamEnd) throws IOException { private void readVP8Extended(BufferedImage destination, ImageReadParam param, long streamEnd) throws IOException {
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 {
while (imageInput.getStreamPosition() < streamEnd) { while (imageInput.getStreamPosition() < streamEnd) {
int nextChunk = imageInput.readInt(); int nextChunk = imageInput.readInt();
long chunkLength = imageInput.readUnsignedInt(); long chunkLength = imageInput.readUnsignedInt();
@ -466,7 +471,7 @@ final class WebPImageReader extends ImageReaderBase {
switch (nextChunk) { switch (nextChunk) {
case WebP.CHUNK_ALPH: case WebP.CHUNK_ALPH:
readAlpha(destination, param); readAlpha(destination, param, width, height);
break; break;
@ -476,7 +481,7 @@ final class WebPImageReader extends ImageReaderBase {
break; break;
case WebP.CHUNK_VP8L: case WebP.CHUNK_VP8L:
readVP8Lossless(RasterUtils.asByteRaster(destination.getRaster()), param); readVP8Lossless(RasterUtils.asByteRaster(destination.getRaster()), param, width, height);
break; break;
case WebP.CHUNK_ANIM: case WebP.CHUNK_ANIM:
@ -500,7 +505,7 @@ final class WebPImageReader extends ImageReaderBase {
} }
} }
private void readAlpha(BufferedImage destination, ImageReadParam param) throws IOException { private void readAlpha(BufferedImage destination, ImageReadParam param, final int width, final int height) throws IOException {
int reserved = (int) imageInput.readBits(2); int reserved = (int) imageInput.readBits(2);
if (reserved != 0) { if (reserved != 0) {
// Spec says SHOULD be 0 // Spec says SHOULD be 0
@ -525,10 +530,11 @@ final class WebPImageReader extends ImageReaderBase {
break; break;
case 1: case 1:
WritableRaster tempRaster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, WritableRaster tempRaster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE,
destination.getWidth(), destination.getHeight(), 4, null); destination.getWidth(), destination.getHeight(), 4,
destination.getRaster().getBounds().getLocation());
//Simulate header //Simulate header
imageInput.seek(imageInput.getStreamPosition() - 5); imageInput.seek(imageInput.getStreamPosition() - 5);
readVP8Lossless(tempRaster, param); readVP8Lossless(tempRaster, param, width, height);
//Copy from green (band 1) in temp to alpha in destination //Copy from green (band 1) in temp to alpha in destination
alphaRaster.setRect(tempRaster.createChild(0, 0, tempRaster.getWidth(), alphaRaster.setRect(tempRaster.createChild(0, 0, tempRaster.getWidth(),
tempRaster.getHeight(), 0, 0, new int[] {1})); tempRaster.getHeight(), 0, 0, new int[] {1}));
@ -638,8 +644,13 @@ final class WebPImageReader extends ImageReaderBase {
} }
private void readVP8Lossless(final WritableRaster raster, final ImageReadParam param) throws IOException { private void readVP8Lossless(final WritableRaster raster, final ImageReadParam param) throws IOException {
readVP8Lossless(raster, param, header.width, header.height);
}
private void readVP8Lossless(final WritableRaster raster, final ImageReadParam param,
final int width, final int height) throws IOException {
VP8LDecoder decoder = new VP8LDecoder(imageInput, DEBUG); VP8LDecoder decoder = new VP8LDecoder(imageInput, DEBUG);
decoder.readVP8Lossless(raster, true); decoder.readVP8Lossless(raster, true, param, width, height);
} }
private void readVP8(final WritableRaster raster, final ImageReadParam param) throws IOException { private void readVP8(final WritableRaster raster, final ImageReadParam param) throws IOException {

View File

@ -42,7 +42,9 @@ import com.twelvemonkeys.imageio.plugins.webp.lossless.transform.Transform;
import com.twelvemonkeys.imageio.plugins.webp.lossless.transform.TransformType; import com.twelvemonkeys.imageio.plugins.webp.lossless.transform.TransformType;
import javax.imageio.IIOException; import javax.imageio.IIOException;
import javax.imageio.ImageReadParam;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.image.*; import java.awt.image.*;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -84,7 +86,8 @@ public final class VP8LDecoder {
lsbBitReader = new LSBBitReader(imageInput); lsbBitReader = new LSBBitReader(imageInput);
} }
public void readVP8Lossless(final WritableRaster raster, final boolean topLevel) throws IOException { 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 //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 //Skip past already read parts of header (signature, width, height, alpha, version) 5 Bytes in total
@ -92,13 +95,12 @@ public final class VP8LDecoder {
imageInput.seek(imageInput.getStreamPosition() + 5); imageInput.seek(imageInput.getStreamPosition() + 5);
} }
int xSize = raster.getWidth(); int xSize = width;
int ySize = raster.getHeight();
// Read transforms // Read transforms
ArrayList<Transform> transforms = new ArrayList<>(); ArrayList<Transform> transforms = new ArrayList<>();
while (topLevel && lsbBitReader.readBit() == 1) { while (topLevel && lsbBitReader.readBit() == 1) {
xSize = readTransform(xSize, ySize, transforms); xSize = readTransform(xSize, height, transforms);
} }
// Read color cache size // Read color cache size
@ -111,7 +113,7 @@ public final class VP8LDecoder {
} }
// Read Huffman codes // Read Huffman codes
HuffmanInfo huffmanInfo = readHuffmanCodes(xSize, ySize, colorCacheBits, topLevel); HuffmanInfo huffmanInfo = readHuffmanCodes(xSize, height, colorCacheBits, topLevel);
ColorCache colorCache = null; ColorCache colorCache = null;
@ -119,16 +121,89 @@ public final class VP8LDecoder {
colorCache = new ColorCache(colorCacheBits); colorCache = new ColorCache(colorCacheBits);
} }
//If multiple indices packed into one pixel xSize is different from raster width WritableRaster fullSizeRaster;
WritableRaster writableChild = raster.createWritableChild(0, 0, xSize, ySize, 0, 0, null); WritableRaster decodeRaster;
if (topLevel) {
// Use the Huffman trees to decode the LZ77 encoded data. Rectangle bounds = new Rectangle(width, height);
decodeImage(writableChild, huffmanInfo, colorCache); fullSizeRaster = getRasterForDecoding(raster, param, bounds);
for (Transform transform : transforms) { //If multiple indices packed into one pixel xSize is different from raster width
transform.applyInverse(raster); decodeRaster = fullSizeRaster.createWritableChild(0, 0, xSize, height, 0, 0, null);
}
else {
//All recursive calls have Rasters of the correct sizes with origin (0, 0)
decodeRaster = fullSizeRaster = raster;
} }
// Use the Huffman trees to decode the LZ77 encoded data.
decodeImage(decodeRaster, huffmanInfo, colorCache);
for (Transform transform : transforms) {
transform.applyInverse(fullSizeRaster);
}
if (fullSizeRaster != raster && param != null) {
//Copy into destination raster with settings applied
Rectangle sourceRegion = param.getSourceRegion();
int sourceXSubsampling = param.getSourceXSubsampling();
int sourceYSubsampling = param.getSourceYSubsampling();
int subsamplingXOffset = param.getSubsamplingXOffset();
int subsamplingYOffset = param.getSubsamplingYOffset();
Point destinationOffset = param.getDestinationOffset();
if (sourceRegion == null) {
sourceRegion = raster.getBounds();
}
if (sourceXSubsampling == 1 && sourceYSubsampling == 1) {
//Only apply offset (and limit to requested region)
raster.setRect(destinationOffset.x, destinationOffset.y, fullSizeRaster);
}
else {
//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) {
fullSizeRaster.getDataElements(xSrc, ySrc, rgba);
raster.setDataElements(xDst, yDst, rgba);
}
}
}
}
}
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) {
//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);
}
else {
bounds.setLocation(param.getDestinationOffset());
originSet = true;
}
}
if (!raster.getBounds().contains(bounds)) {
//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)
raster.createWritableChild(bounds.x, bounds.y, bounds.width, bounds.height, 0, 0, null) :
raster;
} }
private void decodeImage(WritableRaster raster, HuffmanInfo huffmanInfo, ColorCache colorCache) throws IOException { private void decodeImage(WritableRaster raster, HuffmanInfo huffmanInfo, ColorCache colorCache) throws IOException {
@ -296,7 +371,7 @@ public final class VP8LDecoder {
WritableRaster raster = WritableRaster raster =
Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, blockWidth, blockHeight, 4 * blockWidth, 4, Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, blockWidth, blockHeight, 4 * blockWidth, 4,
new int[] {0, 1, 2, 3}, null); new int[] {0, 1, 2, 3}, null);
readVP8Lossless(raster, false); 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) { if (transformType == TransformType.PREDICTOR_TRANSFORM) {
@ -342,7 +417,7 @@ public final class VP8LDecoder {
new DataBufferByte(colorTable, colorTableSize * 4), new DataBufferByte(colorTable, colorTableSize * 4),
colorTableSize, 1, colorTableSize * 4, colorTableSize, 1, colorTableSize * 4,
4, new int[] {0, 1, 2, 3}, null) 4, new int[] {0, 1, 2, 3}, null)
, false); , false, null, colorTableSize, 1);
//resolve subtraction code //resolve subtraction code
@ -388,7 +463,7 @@ public final class VP8LDecoder {
//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, WritableRaster packedRaster = Raster.createPackedRaster(DataBuffer.TYPE_INT, huffmanXSize, huffmanYSize,
new int[] {0x0000ff00, 0x000000ff, 0xff000000, 0x00ff0000}, null); new int[] {0x0000ff00, 0x000000ff, 0xff000000, 0x00ff0000}, null);
readVP8Lossless(asByteRaster(packedRaster), false); readVP8Lossless(asByteRaster(packedRaster), false, null, huffmanXSize, huffmanYSize);
int[] data = ((DataBufferInt) packedRaster.getDataBuffer()).getData(); int[] data = ((DataBufferInt) packedRaster.getDataBuffer()).getData();
//Max metaGroup is number of meta groups //Max metaGroup is number of meta groups