Add support for reading animated WebP files

This commit is contained in:
Jason Douglas 2017-09-26 02:58:54 -07:00
parent 6e4766155d
commit 482d803717
2 changed files with 344 additions and 62 deletions

View File

@ -28,24 +28,134 @@ class WebPImageFile(ImageFile.ImageFile):
format_description = "WebP image"
def _open(self):
data, width, height, self.mode, icc_profile, exif = \
_webp.WebPDecode(self.fp.read())
if not _webp.HAVE_WEBPMUX:
# Legacy mode
data, width, height, self.mode = _webp.WebPDecode(self.fp.read())
self.size = width, height
self.fp = BytesIO(data)
self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
return
# Use the newer AnimDecoder API to parse the (possibly) animated file,
# and access muxed chunks like ICC/EXIF/XMP.
self._decoder = _webp.WebPAnimDecoder(self.fp.read())
# Get info from decoder
width, height, loop_count, bgcolor, frame_count, mode = self._decoder.get_info()
self.size = width, height
self.info["loop"] = loop_count
bg_a, bg_r, bg_g, bg_b = \
(bgcolor >> 24) & 0xFF, \
(bgcolor >> 16) & 0xFF, \
(bgcolor >> 8) & 0xFF, \
bgcolor & 0xFF
self.info["background"] = (bg_r, bg_g, bg_b, bg_a)
self._n_frames = frame_count
self.mode = mode
self.tile = []
# Attempt to read ICC / EXIF / XMP chunks from file
icc_profile = self._decoder.get_chunk("ICCP")
exif = self._decoder.get_chunk("EXIF")
xmp = self._decoder.get_chunk("XMP ")
if icc_profile:
self.info["icc_profile"] = icc_profile
if exif:
self.info["exif"] = exif
if xmp:
self.info["xmp"] = xmp
self.size = width, height
self.fp = BytesIO(data)
self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
# Initialize seek state
self._reset(reset=False)
self.seek(0)
def _getexif(self):
from .JpegImagePlugin import _getexif
return _getexif(self)
@property
def n_frames(self):
if not _webp.HAVE_WEBPMUX:
return 1
return self._n_frames
@property
def is_animated(self):
if not _webp.HAVE_WEBPMUX:
return False
return self._n_frames > 1
def seek(self, frame):
# Perform some simple checks first
if frame >= self._n_frames:
raise EOFError("attempted to seek beyond end of sequence")
if frame < 0:
raise EOFError("negative frame index is not valid")
# Set logical frame to requested position
self.__logical_frame = frame
def _reset(self, reset=True):
if reset:
self._decoder.reset()
self.__physical_frame = 0
self.__loaded = -1
self.__timestamp = 0
def _get_next(self):
# Get next frame
ret = self._decoder.get_next()
self.__physical_frame += 1
# Check if an error occurred
if ret is None:
self._reset() # Reset just to be safe
self.seek(0)
raise EOFError("failed to decode next frame in WebP file")
# Compute duration
data, timestamp = ret
duration = timestamp - self.__timestamp
self.__timestamp = timestamp
timestamp -= duration # libwebp gives frame end, adjust to start of frame
return data, timestamp, duration
def _seek(self, frame):
if self.__physical_frame == frame:
return # Nothing to do
if frame < self.__physical_frame:
# Rewind to beginning
self._reset()
# Advance to the requested frame
while self.__physical_frame < frame:
self._get_next()
def load(self):
if self.__loaded != self.__logical_frame:
self._seek(self.__logical_frame)
# We need to load the image data for this frame
data, timestamp, duration = self._get_next()
self.info["timestamp"] = timestamp
self.info["duration"] = duration
self.__loaded = self.__logical_frame
# Set tile
self.fp = BytesIO(data)
self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
return super(WebPImageFile, self).load()
def tell(self):
return self.__logical_frame
def _save_all(im, fp, filename):
if not _webp.HAVE_WEBPMUX:
_save(im, fp, filename)
return
encoderinfo = im.encoderinfo.copy()
append_images = encoderinfo.get("append_images", [])
background = encoderinfo.get("background", (0, 0, 0, 0))
@ -105,7 +215,10 @@ def _save_all(im, fp, filename):
ims.load()
# Make sure image mode is supported
frame = ims if ims.mode in _VALID_WEBP_MODES else ims.convert("RGBA")
frame = ims
if not ims.mode in _VALID_WEBP_MODES:
alpha = 'A' in ims.im.getpalettemode()
frame = image.convert('RGBA' if alpha else 'RGB')
# Append the frame to the animation encoder
enc.add(
@ -139,11 +252,15 @@ def _save_all(im, fp, filename):
fp.write(data)
def _save(im, fp, filename):
if _webp.HAVE_WEBPMUX:
_save_all(im, fp, filename)
return
image_mode = im.mode
if im.mode not in _VALID_WEBP_MODES:
im = im.convert("RGBA")
alpha = 'A' in im.im.getpalettemode()
im = im.convert('RGBA' if alpha else 'RGB')
lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80)

273
_webp.c
View File

@ -8,13 +8,13 @@
#ifdef HAVE_WEBPMUX
#include <webp/mux.h>
#endif
#include <webp/demux.h>
/* -------------------------------------------------------------------- */
/* WebP Animation Support */
/* -------------------------------------------------------------------- */
#ifdef HAVE_WEBPMUX
// Encoder type
typedef struct {
PyObject_HEAD
WebPAnimEncoder* enc;
@ -23,6 +23,17 @@ typedef struct {
static PyTypeObject WebPAnimEncoder_Type;
// Decoder type
typedef struct {
PyObject_HEAD
WebPAnimDecoder* dec;
WebPAnimInfo info;
WebPData data;
} WebPAnimDecoderObject;
static PyTypeObject WebPAnimDecoder_Type;
// Encoder functions
PyObject* _anim_encoder_new(PyObject* self, PyObject* args)
{
int width, height;
@ -177,10 +188,207 @@ PyObject* _anim_encoder_assemble(PyObject* self, PyObject* args)
return ret;
}
#endif
// Decoder functions
PyObject* _anim_decoder_new(PyObject* self, PyObject* args)
{
PyBytesObject *webp_string;
const uint8_t *webp;
Py_ssize_t size;
if (!PyArg_ParseTuple(args, "S", &webp_string)) {
return NULL;
}
PyBytes_AsStringAndSize((PyObject *) webp_string, (char**)&webp, &size);
WebPData webp_src = {webp, size};
// Create the decoder (default mode is RGBA, if no options passed)
WebPAnimDecoderObject* decp;
decp = PyObject_New(WebPAnimDecoderObject, &WebPAnimDecoder_Type);
if (decp) {
if (WebPDataCopy(&webp_src, &(decp->data))) {
WebPAnimDecoder* dec = WebPAnimDecoderNew(&(decp->data), NULL);
if (dec) {
if (WebPAnimDecoderGetInfo(dec, &(decp->info))) {
decp->dec = dec;
return (PyObject*) decp;
}
}
}
PyObject_Del(decp);
}
fprintf(stderr, "Error! Could not create decoder object.\n");
Py_RETURN_NONE;
}
PyObject* _anim_decoder_dealloc(PyObject* self)
{
WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self;
WebPDataClear(&(decp->data));
WebPAnimDecoderDelete(decp->dec);
Py_RETURN_NONE;
}
PyObject* _anim_decoder_get_info(PyObject* self, PyObject* args)
{
WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self;
WebPAnimInfo* info = &(decp->info);
return Py_BuildValue("IIIIIs",
info->canvas_width, info->canvas_height,
info->loop_count,
info->bgcolor,
info->frame_count,
"RGBA" // WebPAnimDecoder defaults to RGBA if no mode is specified
);
}
PyObject* _anim_decoder_get_chunk(PyObject* self, PyObject* args)
{
char *mode;
PyObject *ret;
WebPChunkIterator iter;
if (!PyArg_ParseTuple(args, "s", &mode)) {
return NULL;
}
WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self;
const WebPDemuxer* demux = WebPAnimDecoderGetDemuxer(decp->dec);
if (!WebPDemuxGetChunk(demux, mode, 1, &iter)) {
Py_RETURN_NONE;
}
ret = PyBytes_FromStringAndSize((const char*)iter.chunk.bytes, iter.chunk.size);
WebPDemuxReleaseChunkIterator(&iter);
return ret;
}
PyObject* _anim_decoder_get_next(PyObject* self, PyObject* args)
{
uint8_t* buf;
int timestamp;
PyObject *bytes;
WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self;
if (!WebPAnimDecoderGetNext(decp->dec, &buf, &timestamp)) {
fprintf(stderr, "Error! Failed to read next frame.\n");
Py_RETURN_NONE;
}
bytes = PyBytes_FromStringAndSize((char *)buf,
decp->info.canvas_width * 4 * decp->info.canvas_height);
return Py_BuildValue("Si", bytes, timestamp);
}
PyObject* _anim_decoder_has_more_frames(PyObject* self, PyObject* args)
{
WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self;
return Py_BuildValue("i", WebPAnimDecoderHasMoreFrames(decp->dec));
}
PyObject* _anim_decoder_reset(PyObject* self, PyObject* args)
{
WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self;
WebPAnimDecoderReset(decp->dec);
Py_RETURN_NONE;
}
/* -------------------------------------------------------------------- */
/* WebP Single-Frame Support */
/* Type Definitions */
/* -------------------------------------------------------------------- */
// WebPAnimEncoder methods
static struct PyMethodDef _anim_encoder_methods[] = {
{"add", (PyCFunction)_anim_encoder_add, METH_VARARGS, "add"},
{"assemble", (PyCFunction)_anim_encoder_assemble, METH_VARARGS, "assemble"},
{NULL, NULL} /* sentinel */
};
// WebPAnimDecoder type definition
static PyTypeObject WebPAnimEncoder_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
"WebPAnimEncoder", /*tp_name */
sizeof(WebPAnimEncoderObject), /*tp_size */
0, /*tp_itemsize */
/* methods */
(destructor)_anim_encoder_dealloc, /*tp_dealloc*/
0, /*tp_print*/
0, /*tp_getattr*/
0, /*tp_setattr*/
0, /*tp_compare*/
0, /*tp_repr*/
0, /*tp_as_number */
0, /*tp_as_sequence */
0, /*tp_as_mapping */
0, /*tp_hash*/
0, /*tp_call*/
0, /*tp_str*/
0, /*tp_getattro*/
0, /*tp_setattro*/
0, /*tp_as_buffer*/
Py_TPFLAGS_DEFAULT, /*tp_flags*/
0, /*tp_doc*/
0, /*tp_traverse*/
0, /*tp_clear*/
0, /*tp_richcompare*/
0, /*tp_weaklistoffset*/
0, /*tp_iter*/
0, /*tp_iternext*/
_anim_encoder_methods, /*tp_methods*/
0, /*tp_members*/
0, /*tp_getset*/
};
// WebPAnimDecoder methods
static struct PyMethodDef _anim_decoder_methods[] = {
{"get_info", (PyCFunction)_anim_decoder_get_info, METH_VARARGS, "get_info"},
{"get_chunk", (PyCFunction)_anim_decoder_get_chunk, METH_VARARGS, "get_chunk"},
{"get_next", (PyCFunction)_anim_decoder_get_next, METH_VARARGS, "get_next"},
{"has_more_frames", (PyCFunction)_anim_decoder_has_more_frames, METH_VARARGS, "has_more_frames"},
{"reset", (PyCFunction)_anim_decoder_reset, METH_VARARGS, "reset"},
{NULL, NULL} /* sentinel */
};
// WebPAnimDecoder type definition
static PyTypeObject WebPAnimDecoder_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
"WebPAnimDecoder", /*tp_name */
sizeof(WebPAnimDecoderObject), /*tp_size */
0, /*tp_itemsize */
/* methods */
(destructor)_anim_decoder_dealloc, /*tp_dealloc*/
0, /*tp_print*/
0, /*tp_getattr*/
0, /*tp_setattr*/
0, /*tp_compare*/
0, /*tp_repr*/
0, /*tp_as_number */
0, /*tp_as_sequence */
0, /*tp_as_mapping */
0, /*tp_hash*/
0, /*tp_call*/
0, /*tp_str*/
0, /*tp_getattro*/
0, /*tp_setattro*/
0, /*tp_as_buffer*/
Py_TPFLAGS_DEFAULT, /*tp_flags*/
0, /*tp_doc*/
0, /*tp_traverse*/
0, /*tp_clear*/
0, /*tp_richcompare*/
0, /*tp_weaklistoffset*/
0, /*tp_iter*/
0, /*tp_iternext*/
_anim_decoder_methods, /*tp_methods*/
0, /*tp_members*/
0, /*tp_getset*/
};
#endif
#ifdef HAVE_WEBPMUX
/* -------------------------------------------------------------------- */
/* Legacy WebP Support */
/* -------------------------------------------------------------------- */
PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
@ -410,6 +618,8 @@ end:
return ret;
}
#endif
// Return the decoder's version number, packed in hexadecimal using 8bits for
// each of major/minor/revision. E.g: v2.5.7 is 0x020507.
PyObject* WebPDecoderVersion_wrapper(PyObject* self, PyObject* args){
@ -428,54 +638,6 @@ PyObject* WebPDecoderBuggyAlpha_wrapper(PyObject* self, PyObject* args){
return Py_BuildValue("i", WebPDecoderBuggyAlpha());
}
/* -------------------------------------------------------------------- */
/* WebPAnimEncoder Type */
/* -------------------------------------------------------------------- */
#ifdef HAVE_WEBPMUX
static struct PyMethodDef _anim_encoder_methods[] = {
{"add", (PyCFunction)_anim_encoder_add, 1},
{"assemble", (PyCFunction)_anim_encoder_assemble, 1},
{NULL, NULL} /* sentinel */
};
/* type description */
static PyTypeObject WebPAnimEncoder_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
"WebPAnimEncoder", /*tp_name */
sizeof(WebPAnimEncoderObject), /*tp_size */
0, /*tp_itemsize */
/* methods */
(destructor)_anim_encoder_dealloc, /*tp_dealloc*/
0, /*tp_print*/
0, /*tp_getattr*/
0, /*tp_setattr*/
0, /*tp_compare*/
0, /*tp_repr*/
0, /*tp_as_number */
0, /*tp_as_sequence */
0, /*tp_as_mapping */
0, /*tp_hash*/
0, /*tp_call*/
0, /*tp_str*/
0, /*tp_getattro*/
0, /*tp_setattro*/
0, /*tp_as_buffer*/
Py_TPFLAGS_DEFAULT, /*tp_flags*/
0, /*tp_doc*/
0, /*tp_traverse*/
0, /*tp_clear*/
0, /*tp_richcompare*/
0, /*tp_weaklistoffset*/
0, /*tp_iter*/
0, /*tp_iternext*/
_anim_encoder_methods, /*tp_methods*/
0, /*tp_members*/
0, /*tp_getset*/
};
#endif
/* -------------------------------------------------------------------- */
/* Module Setup */
/* -------------------------------------------------------------------- */
@ -483,10 +645,12 @@ static PyTypeObject WebPAnimEncoder_Type = {
static PyMethodDef webpMethods[] =
{
#ifdef HAVE_WEBPMUX
{"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"},
{"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"},
#endif
#else
{"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"},
{"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"},
#endif
{"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_VARARGS, "WebPVersion"},
{"WebPDecoderBuggyAlpha", WebPDecoderBuggyAlpha_wrapper, METH_VARARGS, "WebPDecoderBuggyAlpha"},
{NULL, NULL}
@ -511,7 +675,8 @@ static int setup_module(PyObject* m) {
#ifdef HAVE_WEBPMUX
/* Ready object types */
if (PyType_Ready(&WebPAnimEncoder_Type) < 0)
if (PyType_Ready(&WebPAnimDecoder_Type) < 0 ||
PyType_Ready(&WebPAnimEncoder_Type) < 0)
return -1;
#endif
return 0;