mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-25 17:36:18 +03:00
Add support for writing animated webp files
This commit is contained in:
parent
04c96f6030
commit
6e4766155d
|
@ -45,10 +45,105 @@ class WebPImageFile(ImageFile.ImageFile):
|
|||
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):
|
||||
image_mode = im.mode
|
||||
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)
|
||||
quality = im.encoderinfo.get("quality", 80)
|
||||
|
@ -73,6 +168,6 @@ def _save(im, fp, filename):
|
|||
|
||||
Image.register_open(WebPImageFile.format, WebPImageFile, _accept)
|
||||
Image.register_save(WebPImageFile.format, _save)
|
||||
|
||||
Image.register_save_all(WebPImageFile.format, _save_all)
|
||||
Image.register_extension(WebPImageFile.format, ".webp")
|
||||
Image.register_mime(WebPImageFile.format, "image/webp")
|
||||
|
|
247
_webp.c
247
_webp.c
|
@ -10,6 +10,179 @@
|
|||
#include <webp/mux.h>
|
||||
#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, ×tamp, &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)
|
||||
{
|
||||
int width;
|
||||
|
@ -255,8 +428,63 @@ 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 */
|
||||
/* -------------------------------------------------------------------- */
|
||||
|
||||
static PyMethodDef webpMethods[] =
|
||||
{
|
||||
#ifdef HAVE_WEBPMUX
|
||||
{"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"},
|
||||
#endif
|
||||
{"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"},
|
||||
{"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"},
|
||||
{"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_VARARGS, "WebPVersion"},
|
||||
|
@ -277,6 +505,17 @@ void addTransparencyFlagToModule(PyObject* m) {
|
|||
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
|
||||
PyMODINIT_FUNC
|
||||
|
@ -292,8 +531,9 @@ PyInit__webp(void) {
|
|||
};
|
||||
|
||||
m = PyModule_Create(&module_def);
|
||||
addMuxFlagToModule(m);
|
||||
addTransparencyFlagToModule(m);
|
||||
if (setup_module(m) < 0)
|
||||
return NULL;
|
||||
|
||||
return m;
|
||||
}
|
||||
#else
|
||||
|
@ -301,7 +541,6 @@ PyMODINIT_FUNC
|
|||
init_webp(void)
|
||||
{
|
||||
PyObject* m = Py_InitModule("_webp", webpMethods);
|
||||
addMuxFlagToModule(m);
|
||||
addTransparencyFlagToModule(m);
|
||||
setup_module(m);
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -110,7 +110,7 @@ are available::
|
|||
**append_images**
|
||||
A list of images to append as additional frames. Each of the
|
||||
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**
|
||||
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.
|
||||
|
||||
**quality**
|
||||
Integer, 1-100, Defaults to 80. Sets the quality level for
|
||||
lossy compression.
|
||||
Integer, 1-100, Defaults to 80. For lossy, 0 gives the smallest
|
||||
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**
|
||||
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 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
|
||||
^^^
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user