Add support for writing animated webp files

This commit is contained in:
Jason Douglas 2017-09-25 18:53:31 -07:00
parent 04c96f6030
commit 6e4766155d
3 changed files with 393 additions and 9 deletions

View File

@ -45,10 +45,105 @@ class WebPImageFile(ImageFile.ImageFile):
return _getexif(self) return _getexif(self)
def _save_all(im, fp, filename):
encoderinfo = im.encoderinfo.copy()
append_images = encoderinfo.get("append_images", [])
background = encoderinfo.get("background", (0, 0, 0, 0))
duration = im.encoderinfo.get("duration", 0)
loop = im.encoderinfo.get("loop", 0)
minimize_size = im.encoderinfo.get("minimize_size", False)
kmin = im.encoderinfo.get("kmin", None)
kmax = im.encoderinfo.get("kmax", None)
allow_mixed = im.encoderinfo.get("allow_mixed", False)
verbose = False
lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80)
method = im.encoderinfo.get("method", 0)
icc_profile = im.encoderinfo.get("icc_profile", "")
exif = im.encoderinfo.get("exif", "")
if allow_mixed:
lossless = False
# Sensible keyframe defaults are from gif2webp.c script
if kmin is None:
kmin = 9 if lossless else 3
if kmax is None:
kmax = 17 if lossless else 5
# Validate background color
if (not isinstance(background, (list, tuple)) or len(background) != 4 or
not all(v >= 0 and v < 256 for v in background)):
raise IOError("Background color is not an RGBA tuple clamped to (0-255): %s" % str(background))
bg_r, bg_g, bg_b, bg_a = background
background = (bg_a << 24) | (bg_r << 16) | (bg_g << 8) | (bg_b << 0) # Convert to packed uint
# Setup the WebP animation encoder
enc = _webp.WebPAnimEncoder(
im.size[0], im.size[1],
background,
loop,
minimize_size,
kmin, kmax,
allow_mixed,
verbose
)
# Add each frame
frame_idx = 0
timestamp = 0
cur_idx = im.tell()
try:
for ims in [im]+append_images:
# Get # of frames in this image
if not hasattr(ims, "n_frames"):
nfr = 1
else:
nfr = ims.n_frames
for idx in range(nfr):
ims.seek(idx)
ims.load()
# Make sure image mode is supported
frame = ims if ims.mode in _VALID_WEBP_MODES else ims.convert("RGBA")
# Append the frame to the animation encoder
enc.add(
frame.tobytes(),
timestamp,
frame.size[0], frame.size[1],
frame.mode,
lossless,
quality,
method
)
# Update timestamp and frame index
timestamp += duration[frame_idx] if isinstance(duration, (list, tuple)) else duration
frame_idx += 1
finally:
im.seek(cur_idx)
# Force encoder to flush frames
enc.add(
None,
timestamp,
0, 0, "", lossless, quality, 0
)
# Get the final output from the encoder
data = enc.assemble(icc_profile, exif)
if data is None:
raise IOError("cannot write file as WEBP (encoder returned None)")
fp.write(data)
def _save(im, fp, filename): def _save(im, fp, filename):
image_mode = im.mode image_mode = im.mode
if im.mode not in _VALID_WEBP_MODES: if im.mode not in _VALID_WEBP_MODES:
raise IOError("cannot write mode %s as WEBP" % image_mode) im = im.convert("RGBA")
lossless = im.encoderinfo.get("lossless", False) lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80) quality = im.encoderinfo.get("quality", 80)
@ -73,6 +168,6 @@ def _save(im, fp, filename):
Image.register_open(WebPImageFile.format, WebPImageFile, _accept) Image.register_open(WebPImageFile.format, WebPImageFile, _accept)
Image.register_save(WebPImageFile.format, _save) Image.register_save(WebPImageFile.format, _save)
Image.register_save_all(WebPImageFile.format, _save_all)
Image.register_extension(WebPImageFile.format, ".webp") Image.register_extension(WebPImageFile.format, ".webp")
Image.register_mime(WebPImageFile.format, "image/webp") Image.register_mime(WebPImageFile.format, "image/webp")

247
_webp.c
View File

@ -10,6 +10,179 @@
#include <webp/mux.h> #include <webp/mux.h>
#endif #endif
/* -------------------------------------------------------------------- */
/* WebP Animation Support */
/* -------------------------------------------------------------------- */
#ifdef HAVE_WEBPMUX
typedef struct {
PyObject_HEAD
WebPAnimEncoder* enc;
WebPPicture frame;
} WebPAnimEncoderObject;
static PyTypeObject WebPAnimEncoder_Type;
PyObject* _anim_encoder_new(PyObject* self, PyObject* args)
{
int width, height;
uint32_t bgcolor;
int loop_count;
int minimize_size;
int kmin, kmax;
int allow_mixed;
int verbose;
if (!PyArg_ParseTuple(args, "iiIiiiiii",
&width, &height, &bgcolor, &loop_count, &minimize_size,
&kmin, &kmax, &allow_mixed, &verbose)) {
return NULL;
}
// Setup and configure the encoder's options (these are animation-specific)
WebPAnimEncoderOptions enc_options;
if (!WebPAnimEncoderOptionsInit(&enc_options)) {
fprintf(stderr, "Error! Failed to initialize encoder options\n");
Py_RETURN_NONE;
}
enc_options.anim_params.bgcolor = bgcolor;
enc_options.anim_params.loop_count = loop_count;
enc_options.minimize_size = minimize_size;
enc_options.kmin = kmin;
enc_options.kmax = kmax;
enc_options.allow_mixed = allow_mixed;
enc_options.verbose = verbose;
// Validate canvas dimensions
if (width <= 0 || height <= 0) {
fprintf(stderr, "Error! Invalid canvas dimensions: width=%d, height=%d\n", width, height);
Py_RETURN_NONE;
}
// Create a new animation encoder and picture frame
WebPAnimEncoderObject* encp;
encp = PyObject_New(WebPAnimEncoderObject, &WebPAnimEncoder_Type);
if (encp) {
if (WebPPictureInit(&(encp->frame))) {
WebPAnimEncoder* enc = WebPAnimEncoderNew(width, height, &enc_options);
if (enc) {
encp->enc = enc;
return (PyObject*) encp;
}
}
PyObject_Del(encp);
}
fprintf(stderr, "Error! Could not create encoder object.\n");
Py_RETURN_NONE;
}
PyObject* _anim_encoder_dealloc(PyObject* self)
{
WebPAnimEncoderObject* encp = (WebPAnimEncoderObject *)self;
WebPPictureFree(&(encp->frame));
WebPAnimEncoderDelete(encp->enc);
Py_RETURN_NONE;
}
PyObject* _anim_encoder_add(PyObject* self, PyObject* args)
{
uint8_t *rgb;
Py_ssize_t size;
int timestamp;
int width;
int height;
char *mode;
int lossless;
float quality_factor;
int method;
if (!PyArg_ParseTuple(args, "z#iiisifi",
(char**)&rgb, &size, &timestamp, &width, &height, &mode,
&lossless, &quality_factor, &method)) {
return NULL;
}
WebPAnimEncoderObject* encp = (WebPAnimEncoderObject *)self;
WebPAnimEncoder* enc = encp->enc;
WebPPicture* frame = &(encp->frame);
// Check for NULL frame, which sets duration of final frame
if (!rgb) {
WebPAnimEncoderAdd(enc, NULL, timestamp, NULL);
Py_RETURN_NONE;
}
// Setup config for this frame
WebPConfig config;
if (!WebPConfigInit(&config)) {
fprintf(stderr, "Error! Failed to initialize config!\n");
Py_RETURN_NONE;
}
config.lossless = lossless;
config.quality = quality_factor;
config.method = method;
// Validate the config
if (!WebPValidateConfig(&config)) {
fprintf(stderr, "Error! Invalid configuration\n");
Py_RETURN_NONE;
}
// Populate the frame with raw bytes passed to us
frame->width = width;
frame->height = height;
frame->use_argb = 1; // Don't convert RGB pixels to YUV
if (strcmp(mode, "RGBA")==0) {
WebPPictureImportRGBA(frame, rgb, 4 * width);
} else if (strcmp(mode, "RGB")==0) {
WebPPictureImportRGB(frame, rgb, 3 * width);
}
// Add the frame to the encoder
if (!WebPAnimEncoderAdd(enc, frame, timestamp, &config)) {
fprintf(stderr, "Error! Could not add frame: %s\n", WebPAnimEncoderGetError(enc));
Py_RETURN_NONE;
}
Py_RETURN_NONE;
}
PyObject* _anim_encoder_assemble(PyObject* self, PyObject* args)
{
uint8_t *icc_bytes;
uint8_t *exif_bytes;
Py_ssize_t icc_size;
Py_ssize_t exif_size;
if (!PyArg_ParseTuple(args, "s#s#",
&icc_bytes, &icc_size, &exif_bytes, &exif_size)) {
return NULL;
}
// Init the output buffer
WebPData webp_data;
WebPDataInit(&webp_data);
// Assemble everything into the output buffer
WebPAnimEncoderObject* encp = (WebPAnimEncoderObject *)self;
WebPAnimEncoder* enc = encp->enc;
if (!WebPAnimEncoderAssemble(enc, &webp_data)) {
fprintf(stderr, "%s\n", WebPAnimEncoderGetError(enc));
Py_RETURN_NONE;
}
// Convert to Python bytes and return
PyObject *ret = PyBytes_FromStringAndSize((char*)webp_data.bytes, webp_data.size);
WebPDataClear(&webp_data);
return ret;
}
#endif
/* -------------------------------------------------------------------- */
/* WebP Single-Frame Support */
/* -------------------------------------------------------------------- */
PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
{ {
int width; int width;
@ -255,8 +428,63 @@ PyObject* WebPDecoderBuggyAlpha_wrapper(PyObject* self, PyObject* args){
return Py_BuildValue("i", WebPDecoderBuggyAlpha()); 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 */
/* -------------------------------------------------------------------- */
static PyMethodDef webpMethods[] = static PyMethodDef webpMethods[] =
{ {
#ifdef HAVE_WEBPMUX
{"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"},
#endif
{"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"}, {"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"},
{"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"}, {"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"},
{"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_VARARGS, "WebPVersion"}, {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_VARARGS, "WebPVersion"},
@ -277,6 +505,17 @@ void addTransparencyFlagToModule(PyObject* m) {
PyBool_FromLong(!WebPDecoderBuggyAlpha())); PyBool_FromLong(!WebPDecoderBuggyAlpha()));
} }
static int setup_module(PyObject* m) {
addMuxFlagToModule(m);
addTransparencyFlagToModule(m);
#ifdef HAVE_WEBPMUX
/* Ready object types */
if (PyType_Ready(&WebPAnimEncoder_Type) < 0)
return -1;
#endif
return 0;
}
#if PY_VERSION_HEX >= 0x03000000 #if PY_VERSION_HEX >= 0x03000000
PyMODINIT_FUNC PyMODINIT_FUNC
@ -292,8 +531,9 @@ PyInit__webp(void) {
}; };
m = PyModule_Create(&module_def); m = PyModule_Create(&module_def);
addMuxFlagToModule(m); if (setup_module(m) < 0)
addTransparencyFlagToModule(m); return NULL;
return m; return m;
} }
#else #else
@ -301,7 +541,6 @@ PyMODINIT_FUNC
init_webp(void) init_webp(void)
{ {
PyObject* m = Py_InitModule("_webp", webpMethods); PyObject* m = Py_InitModule("_webp", webpMethods);
addMuxFlagToModule(m); setup_module(m);
addTransparencyFlagToModule(m);
} }
#endif #endif

View File

@ -110,7 +110,7 @@ are available::
**append_images** **append_images**
A list of images to append as additional frames. Each of the A list of images to append as additional frames. Each of the
images in the list can be single or multiframe images. images in the list can be single or multiframe images.
This is currently only supported for GIF, PDF and TIFF. This is currently only supported for GIF, PDF, TIFF, and WebP.
**duration** **duration**
The display duration of each frame of the multiframe gif, in The display duration of each frame of the multiframe gif, in
@ -661,8 +661,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
If present and true, instructs the WEBP writer to use lossless compression. If present and true, instructs the WEBP writer to use lossless compression.
**quality** **quality**
Integer, 1-100, Defaults to 80. Sets the quality level for Integer, 1-100, Defaults to 80. For lossy, 0 gives the smallest
lossy compression. size and 100 the largest. For lossless, this parameter is the amount
of effort put into the compression: 0 is the fastest, but gives larger
files compared to the slowest, but best, 100.
**method**
Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 0.
**icc_procfile** **icc_procfile**
The ICC Profile to include in the saved file. Only supported if The ICC Profile to include in the saved file. Only supported if
@ -672,6 +677,51 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
The exif data to include in the saved file. Only supported if The exif data to include in the saved file. Only supported if
the system webp library was built with webpmux support. the system webp library was built with webpmux support.
Saving sequences
~~~~~~~~~~~~~~~~~
.. note::
Support for animated WebP files will only be enabled if the system webp
library was built with webpmux support. You can check webpmux support
at runtime by inspecting the `_webp.HAVE_WEBPMUX` module flag.
When calling :py:meth:`~PIL.Image.Image.save`, the following options
are available when the save_all argument is present and true.
**append_images**
A list of images to append as additional frames. Each of the
images in the list can be single or multiframe images.
**duration**
The display duration of each frame, in milliseconds. Pass a single
integer for a constant duration, or a list or tuple to set the
duration for each frame separately.
**loop**
Number of times to repeat the animation. Defaults to [0 = infinite].
**background**
Background color of the canvas, as an RGBA tuple with values in
the range of (0-255).
**minimize_size**
If true, minimize the output size (slow). Implicitly disables
key-frame insertion.
**kmin, kmax**
Minimum and maximum distance between consecutive key frames in
the output. The library may insert some key frames as needed
to satisfy this criteria. Note that these conditions should
hold: kmax > kmin and kmin >= kmax / 2 + 1. Also, if kmax <= 0,
then key-frame insertion is disabled; and if kmax == 1, then all
frames will be key-frames (kmin value does not matter for these
special cases).
**allow_mixed**
If true, use mixed compression mode; the encoder heuristically
chooses between lossy and lossless for each frame.
XBM XBM
^^^ ^^^