mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-25 09:26:16 +03:00
Add support for reading animated WebP files
This commit is contained in:
parent
6e4766155d
commit
482d803717
|
@ -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
273
_webp.c
|
@ -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, ×tamp)) {
|
||||
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;
|
||||
|
|
Loading…
Reference in New Issue
Block a user