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)
|
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
247
_webp.c
|
@ -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, ×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)
|
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
|
||||||
|
|
|
@ -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
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user