mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-04 12:05:29 -04:00
#629: Preliminary WebP animation (ANIM/ANMF) support
(cherry picked from commit 74927d5396a52cfa489d097896adbecd142ed6b9)
This commit is contained in:
parent
abbbca9be3
commit
0ac8011053
@ -0,0 +1,24 @@
|
||||
package com.twelvemonkeys.imageio.plugins.webp;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* Represents one animation frame (ANMF) chunk.
|
||||
*/
|
||||
final class AnimationFrame extends RIFFChunk {
|
||||
|
||||
final Rectangle bounds;
|
||||
final int duration;
|
||||
final boolean blend;
|
||||
final boolean dispose;
|
||||
|
||||
AnimationFrame(long length, long offset, Rectangle rectangle, int duration, int flags) {
|
||||
super(WebP.CHUNK_ANMF, length, offset);
|
||||
|
||||
this.bounds = rectangle.getBounds();
|
||||
this.duration = duration; // Duration in ms
|
||||
|
||||
blend = (flags & 2) == 0; // 0: Use alpha blending (SrcOver), 1: Do not blend (Src)
|
||||
dispose = (flags & 1) != 0; // 0: Do not dispose, 1: Dispose to (fill bounds with) background color
|
||||
}
|
||||
}
|
@ -31,6 +31,28 @@
|
||||
|
||||
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.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.ColorSpaces;
|
||||
import com.twelvemonkeys.imageio.metadata.Directory;
|
||||
@ -44,22 +66,6 @@ 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.color.ICC_ColorSpace;
|
||||
import java.awt.color.ICC_Profile;
|
||||
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;
|
||||
|
||||
/**
|
||||
* WebPImageReader
|
||||
*/
|
||||
@ -72,6 +78,7 @@ final class WebPImageReader extends ImageReaderBase {
|
||||
// Either VP8_, VP8L or VP8X chunk
|
||||
private VP8xChunk header;
|
||||
private ICC_Profile iccProfile;
|
||||
private final List<AnimationFrame> frames = new ArrayList<>();
|
||||
|
||||
WebPImageReader(ImageReaderSpi provider) {
|
||||
super(provider);
|
||||
@ -82,6 +89,7 @@ final class WebPImageReader extends ImageReaderBase {
|
||||
header = null;
|
||||
iccProfile = null;
|
||||
lsbBitReader = null;
|
||||
frames.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -94,12 +102,82 @@ final class WebPImageReader extends ImageReaderBase {
|
||||
private void readHeader(int imageIndex) throws IOException {
|
||||
checkBounds(imageIndex);
|
||||
|
||||
// TODO: Consider just storing the chunks, parse until VP8, VP8L or VP8X chunk
|
||||
readHeader();
|
||||
|
||||
if (header.containsANIM) {
|
||||
readFrame(imageIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void readFrame(int frameIndex) throws IOException {
|
||||
if (!header.containsANIM) {
|
||||
throw new IndexOutOfBoundsException("imageIndex >= 1 for non-animated WebP: " + frameIndex);
|
||||
}
|
||||
|
||||
if (frameIndex < frames.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: Always extended format if we have animation
|
||||
// Seek to last frame, or end of header if no frames...
|
||||
RIFFChunk frame = frames.isEmpty() ? header : frames.get(frames.size() - 1);
|
||||
imageInput.seek(frame.offset + frame.length);
|
||||
|
||||
while (imageInput.getStreamPosition() < imageInput.length()) {
|
||||
int nextChunk = imageInput.readInt();
|
||||
long chunkLength = imageInput.readUnsignedInt();
|
||||
long chunkStart = imageInput.getStreamPosition();
|
||||
|
||||
if (DEBUG) {
|
||||
System.out.printf("chunk: '%s'\n", fourCC(nextChunk));
|
||||
System.out.println("chunkLength: " + chunkLength);
|
||||
System.out.println("chunkStart: " + chunkStart);
|
||||
}
|
||||
|
||||
switch (nextChunk) {
|
||||
case WebP.CHUNK_ANIM:
|
||||
// TODO: 32 bit bg color (hint!) + 16 bit loop count
|
||||
// + expose bg color in std image metadata...
|
||||
break;
|
||||
|
||||
case WebP.CHUNK_ANMF:
|
||||
// TODO: Expose x/y offset in std image metadata
|
||||
int x = 2 * (int) lsbBitReader.readBits(24); // Might be more efficient to read as 3 bytes...
|
||||
int y = 2 * (int) lsbBitReader.readBits(24);
|
||||
int w = 1 + (int) lsbBitReader.readBits(24);
|
||||
int h = 1 + (int) lsbBitReader.readBits(24);
|
||||
|
||||
Rectangle bounds = new Rectangle(x, y, w, h);
|
||||
|
||||
// TODO: Expose duration/flags in image metadata
|
||||
int duration = (int) imageInput.readBits(24);
|
||||
int flags = imageInput.readUnsignedByte(); // 6 bit reserved + blend mode + disposal mode
|
||||
|
||||
frames.add(new AnimationFrame(chunkLength, chunkStart, bounds, duration, flags));
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
// Skip
|
||||
break;
|
||||
}
|
||||
|
||||
if (frameIndex < frames.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
imageInput.seek(chunkStart + chunkLength + (chunkLength & 1)); // Padded to even length
|
||||
}
|
||||
|
||||
throw new IndexOutOfBoundsException(String.format("imageIndex > %d: %d", frames.size(), frameIndex));
|
||||
}
|
||||
|
||||
private void readHeader() throws IOException {
|
||||
if (header != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Generalize RIFF chunk parsing!
|
||||
// TODO: Generalize RIFF chunk parsing! Visitor?
|
||||
|
||||
// RIFF native order is Little Endian
|
||||
imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
|
||||
@ -125,10 +203,10 @@ final class WebPImageReader extends ImageReaderBase {
|
||||
switch (chunk) {
|
||||
case WebP.CHUNK_VP8_:
|
||||
//https://tools.ietf.org/html/rfc6386#section-9.1
|
||||
int frameType = lsbBitReader.readBit(); // 0 = key frame, 1 = interframe (not used in WebP)
|
||||
int frameType = lsbBitReader.readBit(); // 0 = key frame, 1 = inter frame (not used in WebP)
|
||||
|
||||
if (frameType != 0) {
|
||||
throw new IIOException("Unexpected WebP frame type (expected 0): " + frameType);
|
||||
throw new IIOException("Unexpected WebP frame type, expected key frame (0): " + frameType);
|
||||
}
|
||||
|
||||
int versionNumber = (int) lsbBitReader.readBits(3); // 0 - 3 = different profiles (see spec)
|
||||
@ -214,7 +292,7 @@ final class WebPImageReader extends ImageReaderBase {
|
||||
long chunkStart = imageInput.getStreamPosition();
|
||||
|
||||
if (nextChunk == WebP.CHUNK_ICCP) {
|
||||
iccProfile = ICC_Profile.getInstance(IIOUtil.createStreamAdapter(imageInput, chunkLength));
|
||||
iccProfile = ColorSpaces.readProfile(IIOUtil.createStreamAdapter(imageInput, chunkLength));
|
||||
}
|
||||
else {
|
||||
processWarningOccurred(String.format("Expected 'ICCP' chunk, '%s' chunk encountered", fourCC(nextChunk)));
|
||||
@ -250,20 +328,42 @@ final class WebPImageReader extends ImageReaderBase {
|
||||
|
||||
@Override
|
||||
public int getNumImages(boolean allowSearch) throws IOException {
|
||||
// TODO: Support ANIM/ANMF
|
||||
return super.getNumImages(allowSearch);
|
||||
assertInput();
|
||||
readHeader();
|
||||
|
||||
if (header.containsANIM && allowSearch) {
|
||||
if (isSeekForwardOnly()) {
|
||||
throw new IllegalStateException("Illegal combination of allowSearch with seekForwardOnly");
|
||||
}
|
||||
|
||||
readAllFrames();
|
||||
return frames.size();
|
||||
}
|
||||
|
||||
return header.containsANIM ? -1 : 1;
|
||||
}
|
||||
|
||||
private void readAllFrames() throws IOException {
|
||||
try {
|
||||
readFrame(Integer.MAX_VALUE);
|
||||
}
|
||||
catch (IndexOutOfBoundsException ignore) {}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth(int imageIndex) throws IOException {
|
||||
readHeader(imageIndex);
|
||||
return header.width;
|
||||
|
||||
return header.containsANIM ? frames.get(imageIndex).bounds.width
|
||||
: header.width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight(int imageIndex) throws IOException {
|
||||
readHeader(imageIndex);
|
||||
return header.height;
|
||||
|
||||
return header.containsANIM ? frames.get(imageIndex).bounds.height
|
||||
: header.height;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -318,80 +418,15 @@ final class WebPImageReader extends ImageReaderBase {
|
||||
break;
|
||||
|
||||
case WebP.CHUNK_VP8X:
|
||||
imageInput.seek(header.offset + header.length);
|
||||
|
||||
while (imageInput.getStreamPosition() < imageInput.length()) {
|
||||
int nextChunk = imageInput.readInt();
|
||||
long chunkLength = imageInput.readUnsignedInt();
|
||||
long chunkStart = imageInput.getStreamPosition();
|
||||
|
||||
if (DEBUG) {
|
||||
System.out.printf("chunk: '%s'\n", fourCC(nextChunk));
|
||||
System.out.println("chunkLength: " + chunkLength);
|
||||
System.out.println("chunkStart: " + chunkStart);
|
||||
}
|
||||
|
||||
switch (nextChunk) {
|
||||
case WebP.CHUNK_ALPH:
|
||||
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));
|
||||
}
|
||||
|
||||
int preProcessing = (int) imageInput.readBits(2);
|
||||
int filtering = (int) imageInput.readBits(2);
|
||||
int compression = (int) imageInput.readBits(2);
|
||||
|
||||
if (DEBUG) {
|
||||
System.out.println("preProcessing: " + preProcessing);
|
||||
System.out.println("filtering: " + filtering);
|
||||
System.out.println("compression: " + compression);
|
||||
}
|
||||
|
||||
switch (compression) {
|
||||
case 0:
|
||||
readUncompressedAlpha(destination.getAlphaRaster());
|
||||
break;
|
||||
case 1:
|
||||
opaqueAlpha(destination.getAlphaRaster()); // TODO: Remove when correctly implemented!
|
||||
readVP8Lossless(destination.getAlphaRaster(), param);
|
||||
break;
|
||||
default:
|
||||
processWarningOccurred("Unknown WebP alpha compression: " + compression);
|
||||
opaqueAlpha(destination.getAlphaRaster());
|
||||
break;
|
||||
}
|
||||
|
||||
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);
|
||||
break;
|
||||
|
||||
case WebP.CHUNK_VP8L:
|
||||
readVP8Lossless(RasterUtils.asByteRaster(destination.getRaster()), param);
|
||||
break;
|
||||
|
||||
case WebP.CHUNK_ICCP:
|
||||
// Ignore, we already read this
|
||||
case WebP.CHUNK_EXIF:
|
||||
case WebP.CHUNK_XMP_:
|
||||
// Ignore, we'll read this later
|
||||
break;
|
||||
|
||||
case WebP.CHUNK_ANIM:
|
||||
case WebP.CHUNK_ANMF:
|
||||
processWarningOccurred("Ignoring unsupported chunk: " + fourCC(nextChunk));
|
||||
break;
|
||||
|
||||
default:
|
||||
processWarningOccurred("Ignoring unexpected chunk: " + fourCC(nextChunk));
|
||||
break;
|
||||
}
|
||||
|
||||
imageInput.seek(chunkStart + chunkLength + (chunkLength & 1)); // Padded to even length
|
||||
if (header.containsANIM) {
|
||||
AnimationFrame frame = frames.get(imageIndex);
|
||||
imageInput.seek(frame.offset + 16);
|
||||
opaqueAlpha(destination.getAlphaRaster());
|
||||
readVP8Extended(destination, param, frame.offset + frame.length);
|
||||
}
|
||||
else {
|
||||
imageInput.seek(header.offset + header.length);
|
||||
readVP8Extended(destination, param, imageInput.length());
|
||||
}
|
||||
|
||||
break;
|
||||
@ -404,13 +439,90 @@ final class WebPImageReader extends ImageReaderBase {
|
||||
|
||||
if (abortRequested()) {
|
||||
processReadAborted();
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
processImageComplete();
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
private void readVP8Extended(BufferedImage destination, ImageReadParam param, long streamEnd) throws IOException {
|
||||
while (imageInput.getStreamPosition() < streamEnd) {
|
||||
int nextChunk = imageInput.readInt();
|
||||
long chunkLength = imageInput.readUnsignedInt();
|
||||
long chunkStart = imageInput.getStreamPosition();
|
||||
|
||||
if (DEBUG) {
|
||||
System.out.printf("chunk: '%s'\n", fourCC(nextChunk));
|
||||
System.out.println("chunkLength: " + chunkLength);
|
||||
System.out.println("chunkStart: " + chunkStart);
|
||||
}
|
||||
|
||||
switch (nextChunk) {
|
||||
case WebP.CHUNK_ALPH:
|
||||
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));
|
||||
}
|
||||
|
||||
int preProcessing = (int) imageInput.readBits(2);
|
||||
int filtering = (int) imageInput.readBits(2);
|
||||
int compression = (int) imageInput.readBits(2);
|
||||
|
||||
if (DEBUG) {
|
||||
System.out.println("preProcessing: " + preProcessing);
|
||||
System.out.println("filtering: " + filtering);
|
||||
System.out.println("compression: " + compression);
|
||||
}
|
||||
|
||||
switch (compression) {
|
||||
case 0:
|
||||
readUncompressedAlpha(destination.getAlphaRaster());
|
||||
break;
|
||||
case 1:
|
||||
opaqueAlpha(destination.getAlphaRaster()); // TODO: Remove when correctly implemented!
|
||||
// readVP8Lossless(destination.getAlphaRaster(), param);
|
||||
break;
|
||||
default:
|
||||
processWarningOccurred("Unknown WebP alpha compression: " + compression);
|
||||
opaqueAlpha(destination.getAlphaRaster());
|
||||
break;
|
||||
}
|
||||
|
||||
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);
|
||||
break;
|
||||
|
||||
case WebP.CHUNK_VP8L:
|
||||
readVP8Lossless(RasterUtils.asByteRaster(destination.getRaster()), param);
|
||||
break;
|
||||
|
||||
case WebP.CHUNK_ANIM:
|
||||
case WebP.CHUNK_ANMF:
|
||||
if (!header.containsANIM) {
|
||||
processWarningOccurred("Ignoring unsupported chunk: " + fourCC(nextChunk));
|
||||
}
|
||||
case WebP.CHUNK_ICCP:
|
||||
// Ignore, we already read this
|
||||
case WebP.CHUNK_EXIF:
|
||||
case WebP.CHUNK_XMP_:
|
||||
// Ignore, we'll read these later
|
||||
break;
|
||||
|
||||
default:
|
||||
processWarningOccurred("Ignoring unexpected chunk: " + fourCC(nextChunk));
|
||||
break;
|
||||
}
|
||||
|
||||
imageInput.seek(chunkStart + chunkLength + (chunkLength & 1)); // Padded to even length
|
||||
}
|
||||
}
|
||||
|
||||
private void applyICCProfileIfNeeded(final BufferedImage destination) {
|
||||
if (iccProfile != null) {
|
||||
ColorModel colorModel = destination.getColorModel();
|
||||
@ -543,8 +655,4 @@ final class WebPImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static void showIt(BufferedImage image, String title) {
|
||||
ImageReaderBase.showIt(image, title);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user