blob: a1fd78cf5e11bf8a58d7187c0f21fbb96498bae7 [file] [log] [blame]
import '../animation.dart';
import '../color.dart';
import '../image.dart';
import '../transform/copy_into.dart';
import '../util/input_buffer.dart';
import 'decoder.dart';
import 'webp/vp8.dart';
import 'webp/vp8l.dart';
import 'webp/webp_frame.dart';
import 'webp/webp_info.dart';
/// Decode a WebP formatted image. This supports lossless (vp8l), lossy (vp8),
/// lossy+alpha, and animated WebP images.
class WebPDecoder extends Decoder {
InternalWebPInfo _info;
WebPDecoder([List<int> bytes]) {
if (bytes != null) {
startDecode(bytes);
}
}
WebPInfo get info => _info;
/// Is the given file a valid WebP image?
bool isValidFile(List<int> bytes) {
_input = InputBuffer(bytes);
if (!_getHeader(_input)) {
return false;
}
return true;
}
/// How many frames are available to decode?
///
/// You should have prepared the decoder by either passing the file bytes
/// to the constructor, or calling getInfo.
int numFrames() => (_info != null) ? _info.numFrames : 0;
/// Validate the file is a WebP image and get information about it.
/// If the file is not a valid WebP image, null is returned.
WebPInfo startDecode(List<int> bytes) {
_input = InputBuffer(bytes);
if (!_getHeader(_input)) {
return null;
}
_info = InternalWebPInfo();
if (!_getInfo(_input, _info)) {
return null;
}
switch (_info.format) {
case WebPInfo.FORMAT_ANIMATED:
return _info;
case WebPInfo.FORMAT_LOSSLESS:
_input.offset = _info.vp8Position;
VP8L vp8l = VP8L(_input, _info);
if (!vp8l.decodeHeader()) {
return null;
}
return _info;
case WebPInfo.FORMAT_LOSSY:
_input.offset = _info.vp8Position;
VP8 vp8 = VP8(_input, _info);
if (!vp8.decodeHeader()) {
return null;
}
return _info;
}
return null;
}
Image decodeFrame(int frame) {
if (_input == null || _info == null) {
return null;
}
if (_info.hasAnimation) {
if (frame >= _info.frames.length || frame < 0) {
return null;
}
InternalWebPFrame f = _info.frames[frame] as InternalWebPFrame;
InputBuffer frameData =
_input.subset(f.frameSize, position: f.framePosition);
return _decodeFrame(frameData, frame: frame);
}
if (_info.format == WebPInfo.FORMAT_LOSSLESS) {
InputBuffer data =
_input.subset(_info.vp8Size, position: _info.vp8Position);
return new VP8L(data, _info).decode();
} else if (_info.format == WebPInfo.FORMAT_LOSSY) {
InputBuffer data =
_input.subset(_info.vp8Size, position: _info.vp8Position);
return new VP8(data, _info).decode();
}
return null;
}
/// Decode a WebP formatted file stored in [bytes] into an Image.
/// If it's not a valid webp file, null is returned.
/// If the webp file stores animated frames, only the first image will
/// be returned. Use [decodeAnimation] to decode the full animation.
Image decodeImage(List<int> bytes, {int frame = 0}) {
startDecode(bytes);
_info.frame = 0;
_info.numFrames = 1;
return decodeFrame(frame);
}
/// Decode all of the frames of an animated webp. For single image webps,
/// this will return an animation with a single frame.
Animation decodeAnimation(List<int> bytes) {
if (startDecode(bytes) == null) {
return null;
}
_info.numFrames = _info.numFrames;
Animation anim = Animation();
anim.width = _info.width;
anim.height = _info.height;
anim.loopCount = _info.animLoopCount;
if (_info.hasAnimation) {
Image lastImage = Image(_info.width, _info.height);
for (int i = 0; i < _info.numFrames; ++i) {
_info.frame = i;
if (lastImage == null) {
lastImage = Image(_info.width, _info.height);
} else {
lastImage = Image.from(lastImage);
}
WebPFrame frame = _info.frames[i];
Image image = decodeFrame(i);
if (image == null) {
return null;
}
if (lastImage != null) {
if (frame.clearFrame) {
lastImage.fill(_info.backgroundColor);
}
copyInto(lastImage, image, dstX: frame.x, dstY: frame.y);
} else {
lastImage = image;
}
lastImage.duration = frame.duration;
anim.addFrame(lastImage);
}
} else {
Image image = decodeImage(bytes);
if (image == null) {
return null;
}
anim.addFrame(image);
}
return anim;
}
Image _decodeFrame(InputBuffer input, {int frame = 0}) {
InternalWebPInfo webp = InternalWebPInfo();
if (!_getInfo(input, webp)) {
return null;
}
if (webp.format == 0) {
return null;
}
webp.frame = _info.frame;
webp.numFrames = _info.numFrames;
if (webp.hasAnimation) {
if (frame >= webp.frames.length || frame < 0) {
return null;
}
InternalWebPFrame f = webp.frames[frame] as InternalWebPFrame;
InputBuffer frameData =
input.subset(f.frameSize, position: f.framePosition);
return _decodeFrame(frameData, frame: frame);
} else {
InputBuffer data = input.subset(webp.vp8Size, position: webp.vp8Position);
if (webp.format == WebPInfo.FORMAT_LOSSLESS) {
return new VP8L(data, webp).decode();
} else if (webp.format == WebPInfo.FORMAT_LOSSY) {
return new VP8(data, webp).decode();
}
}
return null;
}
bool _getHeader(InputBuffer input) {
// Validate the webp format header
String tag = input.readString(4);
if (tag != 'RIFF') {
return false;
}
/*int fileSize =*/ input.readUint32();
tag = input.readString(4);
if (tag != 'WEBP') {
return false;
}
return true;
}
bool _getInfo(InputBuffer input, InternalWebPInfo webp) {
bool found = false;
while (!input.isEOS && !found) {
String tag = input.readString(4);
int size = input.readUint32();
// For odd sized chunks, there's a 1 byte padding at the end.
int diskSize = ((size + 1) >> 1) << 1;
int p = input.position;
switch (tag) {
case 'VP8X':
if (!_getVp8xInfo(input, webp)) {
return false;
}
break;
case 'VP8 ':
webp.vp8Position = input.position;
webp.vp8Size = size;
webp.format = WebPInfo.FORMAT_LOSSY;
found = true;
break;
case 'VP8L':
webp.vp8Position = input.position;
webp.vp8Size = size;
webp.format = WebPInfo.FORMAT_LOSSLESS;
found = true;
break;
case 'ALPH':
webp.alphaData =
InputBuffer(input.buffer, bigEndian: input.bigEndian);
webp.alphaData.offset = input.offset;
webp.alphaSize = size;
input.skip(diskSize);
break;
case 'ANIM':
webp.format = WebPInfo.FORMAT_ANIMATED;
if (!_getAnimInfo(input, webp)) {
return false;
}
break;
case 'ANMF':
if (!_getAnimFrameInfo(input, webp, size)) {
return false;
}
break;
case 'ICCP':
webp.iccp = input.readString(size);
break;
case 'EXIF':
webp.exif = input.readString(size);
break;
case 'XMP ':
webp.xmp = input.readString(size);
break;
default:
print('UNKNOWN WEBP TAG: $tag');
input.skip(diskSize);
break;
}
int remainder = diskSize - (input.position - p);
if (remainder > 0) {
input.skip(remainder);
}
}
/**
* The alpha flag might not have been set, but it does in fact have alpha
* if there is an ALPH chunk.
*/
if (!webp.hasAlpha) {
webp.hasAlpha = webp.alphaData != null;
}
return webp.format != 0;
}
bool _getVp8xInfo(InputBuffer input, WebPInfo webp) {
int b = input.readByte();
if ((b & 0xc0) != 0) {
return false;
}
//int icc = (b >> 5) & 0x1;
int alpha = (b >> 4) & 0x1;
//int exif = (b >> 3) & 0x1;
//int xmp = (b >> 2) & 0x1;
int a = (b >> 1) & 0x1;
if (b & 0x1 != 0) {
return false;
}
if (input.readUint24() != 0) {
return false;
}
int w = input.readUint24() + 1;
int h = input.readUint24() + 1;
webp.width = w;
webp.height = h;
webp.hasAnimation = a != 0;
webp.hasAlpha = alpha != 0;
return true;
}
bool _getAnimInfo(InputBuffer input, WebPInfo webp) {
int c = input.readUint32();
webp.animLoopCount = input.readUint16();
// Color is stored in blue,green,red,alpha order.
int a = getRed(c);
int r = getGreen(c);
int g = getBlue(c);
int b = getAlpha(c);
webp.backgroundColor = getColor(r, g, b, a);
return true;
}
bool _getAnimFrameInfo(InputBuffer input, WebPInfo webp, int size) {
InternalWebPFrame frame = InternalWebPFrame(input, size);
if (!frame.isValid) {
return false;
}
webp.frames.add(frame);
return true;
}
InputBuffer _input;
}