diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index d6cc39fd877..1947978586f 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -119,11 +119,74 @@ def test_write_method(self, tmp_path: Path) -> None: hopper().save(buffer_method, format="WEBP", method=6) assert buffer_no_args.getbuffer() != buffer_method.getbuffer() - def test_save_all(self, tmp_path: Path) -> None: + @pytest.mark.parametrize( + "options", + ( + {"alpha_compression": 0}, + {"alpha_filtering": 2}, + {"autofilter": True}, + {"emulate_jpeg_size": True}, + {"filter_sharpness": 7}, + {"filter_strength": 0}, + {"filter_type": 0}, + {"low_memory": True}, + {"partition_limit": 100}, + {"partitions": 3}, + {"preprocessing": 1}, + {"segments": 1}, + {"sns_strength": 0}, + {"thread_level": 1}, + # Options that do nothing without `pass` == 1 + {"target_psnr": 40.0, "pass": 10}, + {"target_size": 2000, "pass": 10}, + ), + ) + def test_advanced_options(self, options: dict[str, Any]) -> None: + buffer = io.BytesIO() + hopper().save(buffer, format="WEBP", **options) + + with Image.open(buffer) as reloaded: + assert reloaded.size == (128, 128) + reloaded.load() + + def test_advanced_options_change_output(self) -> None: + buffer_no_args = io.BytesIO() + hopper().save(buffer_no_args, format="WEBP") + + buffer_args = io.BytesIO() + hopper().save(buffer_args, format="WEBP", sns_strength=0, filter_strength=0) + + assert buffer_no_args.getbuffer() != buffer_args.getbuffer() + + @pytest.mark.parametrize("preset", sorted(WebPImagePlugin._PRESETS)) + def test_preset(self, preset: str) -> None: + buffer = io.BytesIO() + hopper().save(buffer, format="WEBP", preset=preset) + + with Image.open(buffer) as reloaded: + assert reloaded.size == (128, 128) + reloaded.load() + + def test_invalid(self) -> None: + with pytest.raises(ValueError, match="Unknown preset"): + hopper().save(io.BytesIO(), format="WEBP", preset="invalid") + + with pytest.raises(ValueError, match="WebP configuration validation failed"): + hopper().save(io.BytesIO(), format="WEBP", segments=99) + + with pytest.raises(TypeError): + hopper().save(io.BytesIO(), format="WEBP", sns_strength="invalid") + + @pytest.mark.parametrize( + "options", + [{}, {"preset": "photo", "sns_strength": 0}], + ids=["no-options", "advanced-options"], + ) + def test_save_all(self, tmp_path: Path, options: dict[str, Any]) -> None: temp_file = tmp_path / "temp.webp" im = Image.new("RGB", (1, 1)) im2 = Image.new("RGB", (1, 1), "#f00") - im.save(temp_file, save_all=True, append_images=[im2]) + im.save(temp_file, save_all=True, append_images=[im2], **options) with Image.open(temp_file) as reloaded: assert_image_equal(im, reloaded) @@ -134,7 +197,7 @@ def test_save_all(self, tmp_path: Path) -> None: def test_unsupported_image_mode(self) -> None: im = Image.new("1", (1, 1)) with pytest.raises(ValueError): - _webp.WebPEncode(im.getim(), False, 0, 0, "", 4, 0, b"", "") + _webp.WebPEncode(im.getim(), "", b"", "", {}) def test_icc_profile(self, tmp_path: Path) -> None: self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 60e0b483d24..53cd02f3ee0 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1374,7 +1374,7 @@ using the general tags available through tiffinfo. WebP ^^^^ -Pillow reads and writes WebP files. Requires libwebp v0.5.0 or later. +Pillow reads and writes WebP files. Requires libwebp v1.2.0 or later. .. _webp-saving: @@ -1383,6 +1383,17 @@ Saving The :py:meth:`~PIL.Image.Image.save` method supports the following options: +**preset** + A predefined set of encoding parameters for a type of source picture. + It is applied before any of the other options here. One of: + + * ``"default"`` - the default preset + * ``"picture"`` - digital picture, like portrait, inner shot + * ``"photo"`` - outdoor photograph, with natural lighting + * ``"drawing"`` - hand or line drawing, with high-contrast details + * ``"icon"`` - small-sized colorful images + * ``"text"`` - text-like + **lossless** If present and true, instructs the WebP writer to use lossless compression. @@ -1397,7 +1408,8 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: smallest size and 100 is lossless. **method** - Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4. + Quality/speed trade-off (0=fast, 6=slower-better). + Defaults to 4 for single images, 0 for animated WebPs. **exact** If true, preserve the transparent RGB values. Otherwise, discard @@ -1412,6 +1424,97 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **xmp** The XMP data to include in the saved file. +.. _webp-advanced-saving: + +Advanced saving options +~~~~~~~~~~~~~~~~~~~~~~~ + +The following options correspond to `libwebp's encoder +configuration `_. +The names, ranges and defaults below follow its ``WebPConfig`` struct, +which is the authoritative description of what these do. +Values outside the range accepted by libwebp raise :py:exc:`ValueError`. + +**target_size** + If non-zero, the desired target size in bytes. Takes precedence over ``quality``. + This has no effect unless ``pass`` is greater than 1. + +**target_psnr** + If non-zero, the minimal distortion to try to achieve, in dB. + Takes precedence over ``target_size``. Defaults to 0. + This has no effect unless ``pass`` is greater than 1. + +**segments** + Maximum number of segments to use, in [1..4]. Defaults to 4. + +**sns_strength** + Spatial noise shaping: 0 is off, 100 is maximum. Defaults to 50. + +**filter_strength** + Range 0 (off) to 100 (strongest). Defaults to 60. + +**filter_sharpness** + Range 0 (off) to 7 (least sharp). Defaults to 0. + +**filter_type** + Filtering type: 0 for simple, 1 for strong. + Only used if ``filter_strength`` is greater than 0 or ``autofilter`` is on. + Defaults to 1. + +**autofilter** + If true, automatically adjust the filter's strength. Defaults to false. + +**alpha_compression** + Algorithm for encoding the alpha plane: + 0 for none, 1 for compressed with WebP lossless. Defaults to 1. + +**alpha_filtering** + Predictive filtering method for the alpha plane: + 0 for none, 1 for fast, 2 for best. Defaults to 1. + +**pass** + Number of entropy-analysis passes, in [1..10]. Defaults to 1. + Note that some options only have an effect when this is set to a value greater than 1. + +**preprocessing** + Preprocessing filter: + 0 for none, 1 for segment-smooth, 2 for pseudo-random dithering. + Defaults to 0. + +**partitions** + Log2 of the number of token partitions, in [0..3]. + Defaults to 0, for easier progressive decoding. + +**partition_limit** + Quality degradation allowed to fit the 512k limit on prediction modes coding: + 0 for no degradation, 100 for maximum possible degradation. + Defaults to 0. + +**emulate_jpeg_size** + If true, compression parameters will be remapped to better match the + expected output size from JPEG compression. + Generally the output size will be similar, but the degradation will be lower. + Defaults to false. + +**thread_level** + If non-zero, try and use multi-threaded encoding. Defaults to 0. + +**low_memory** + If true, reduce memory usage, at the cost of higher CPU use. + Defaults to false. + +**near_lossless** + Near-lossless encoding, from 0 (maximum loss) to 100 (off). + Only used for lossless encoding. Defaults to 100. + +**use_sharp_yuv** + If true, use sharp (and slow) RGB to YUV conversion, + which only affects lossy encoding. Defaults to false. + +**qmin**, **qmax** + The minimum and maximum permissible quality factor, clamping ``quality``. + Default to 0 and 100 respectively. + Saving sequences ~~~~~~~~~~~~~~~~ diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index a0923066afd..8a363ac6e67 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -55,6 +55,8 @@ Many of Pillow's features require external libraries: * **libwebp** provides the WebP format. + * Pillow requires libwebp **1.2.0** or later. + * **openjpeg** provides JPEG 2000 functionality. * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 63a48169182..9f2a4e25c52 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -21,6 +21,41 @@ b"VP8L": "RGBA", # lossless } +# Advanced encoder settings passed through to WebPConfig +_ADVANCED_OPTIONS = ( + "alpha_compression", + "alpha_filtering", + "autofilter", + "emulate_jpeg_size", + "filter_sharpness", + "filter_strength", + "filter_type", + "low_memory", + "near_lossless", + "partition_limit", + "partitions", + "pass", + "preprocessing", + "qmax", + "qmin", + "segments", + "sns_strength", + "target_psnr", + "target_size", + "thread_level", + "use_sharp_yuv", +) + +# Per https://github.com/webmproject/libwebp/blob/9c4a699e5/src/webp/encode.h#L158-L167 +_PRESETS = { + "default": 0, + "picture": 1, + "photo": 2, + "drawing": 3, + "icon": 4, + "text": 5, +} + def _accept(prefix: bytes) -> bool | str: is_riff_file_format = prefix.startswith(b"RIFF") @@ -156,6 +191,31 @@ def _convert_frame(im: Image.Image) -> Image.Image: return im +def _get_encoder_options( + encoderinfo: dict[str, Any], *, default_method: int +) -> dict[str, Any]: + options: dict[str, Any] = { + "lossless": encoderinfo.get("lossless", False), + "quality": float(encoderinfo.get("quality", 80)), + "alpha_quality": int(encoderinfo.get("alpha_quality", 100)), + "method": encoderinfo.get("method", default_method), + "exact": 1 if encoderinfo.get("exact") else 0, + } + options.update( + {name: encoderinfo[name] for name in _ADVANCED_OPTIONS if name in encoderinfo} + ) + + preset = encoderinfo.get("preset") + if preset is not None: + try: + options["preset"] = _PRESETS[preset] + except KeyError: + msg = f"Unknown preset {preset!r}, expected one of {sorted(_PRESETS)}" + raise ValueError(msg) from None + + return options + + def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: encoderinfo = im.encoderinfo.copy() append_images = list(encoderinfo.get("append_images", [])) @@ -191,17 +251,15 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> 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) - alpha_quality = im.encoderinfo.get("alpha_quality", 100) - method = im.encoderinfo.get("method", 0) + options = _get_encoder_options(im.encoderinfo, default_method=0) icc_profile = im.encoderinfo.get("icc_profile") or "" exif = im.encoderinfo.get("exif", "") if isinstance(exif, Image.Exif): exif = exif.tobytes() xmp = im.encoderinfo.get("xmp", "") if allow_mixed: - lossless = False + options["lossless"] = False + lossless = options["lossless"] # Sensible keyframe defaults are from gif2webp.c script if kmin is None: @@ -249,14 +307,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: frame = _convert_frame(ims) # Append the frame to the animation encoder - enc.add( - frame.getim(), - round(timestamp), - lossless, - quality, - alpha_quality, - method, - ) + enc.add(frame.getim(), round(timestamp), options) # Update timestamp and frame index if isinstance(duration, (list, tuple)): @@ -269,7 +320,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: im.seek(cur_idx) # Force encoder to flush frames - enc.add(None, round(timestamp), lossless, quality, alpha_quality, 0) + enc.add(None, round(timestamp), options) # Get the final output from the encoder data = enc.assemble(icc_profile, exif, xmp) @@ -281,9 +332,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - lossless = im.encoderinfo.get("lossless", False) - quality = im.encoderinfo.get("quality", 80) - alpha_quality = im.encoderinfo.get("alpha_quality", 100) + options = _get_encoder_options(im.encoderinfo, default_method=4) icc_profile = im.encoderinfo.get("icc_profile") or "" exif = im.encoderinfo.get("exif", b"") if isinstance(exif, Image.Exif): @@ -291,22 +340,10 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if exif.startswith(b"Exif\x00\x00"): exif = exif[6:] xmp = im.encoderinfo.get("xmp", "") - method = im.encoderinfo.get("method", 4) - exact = 1 if im.encoderinfo.get("exact") else 0 im = _convert_frame(im) - data = _webp.WebPEncode( - im.getim(), - lossless, - float(quality), - float(alpha_quality), - icc_profile, - method, - exact, - exif, - xmp, - ) + data = _webp.WebPEncode(im.getim(), icc_profile, exif, xmp, options) if data is None: msg = "cannot write file as WebP (encoder returned None)" raise OSError(msg) diff --git a/src/_webp.c b/src/_webp.c index 9f9fbf3978a..a44851460cb 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -8,13 +8,12 @@ #include /* - * Check the versions from mux.h and demux.h, to ensure the WebPAnimEncoder and - * WebPAnimDecoder APIs are present (initial support was added in 0.5.0). The - * very early versions had some significant differences, so we require later - * versions. + * Check the ABI versions to ensure the APIs and config options used here are are + * present */ -#if WEBP_MUX_ABI_VERSION < 0x0106 || WEBP_DEMUX_ABI_VERSION < 0x0107 -#error libwebp 0.5.0 and above is required. Upgrade libwebp or build Pillow with --disable-webp flag +#if WEBP_ENCODER_ABI_VERSION < 0x020f || WEBP_MUX_ABI_VERSION < 0x0108 || \ + WEBP_DEMUX_ABI_VERSION < 0x0107 +#error libwebp 1.2.0 and above is required. Upgrade libwebp or build Pillow with --disable-webp flag #endif void @@ -73,10 +72,146 @@ HandleMuxError(WebPMuxError err, char *chunk) { return NULL; } +/* -------------------------------------------------------------------- */ +/* Encoder configuration */ +/* -------------------------------------------------------------------- */ + +typedef struct { + const char *name; + size_t offset; + int is_float; +} WebPConfigOption; + +static const WebPConfigOption webp_config_options[] = { + // clang-format off + {.name = "alpha_compression", .offset = offsetof(WebPConfig, alpha_compression), .is_float = 0}, + {.name = "alpha_filtering", .offset = offsetof(WebPConfig, alpha_filtering), .is_float = 0}, + {.name = "alpha_quality", .offset = offsetof(WebPConfig, alpha_quality), .is_float = 0}, + {.name = "autofilter", .offset = offsetof(WebPConfig, autofilter), .is_float = 0}, + {.name = "emulate_jpeg_size", .offset = offsetof(WebPConfig, emulate_jpeg_size), .is_float = 0}, + {.name = "exact", .offset = offsetof(WebPConfig, exact), .is_float = 0}, + {.name = "filter_sharpness", .offset = offsetof(WebPConfig, filter_sharpness), .is_float = 0}, + {.name = "filter_strength", .offset = offsetof(WebPConfig, filter_strength), .is_float = 0}, + {.name = "filter_type", .offset = offsetof(WebPConfig, filter_type), .is_float = 0}, + {.name = "lossless", .offset = offsetof(WebPConfig, lossless), .is_float = 0}, + {.name = "low_memory", .offset = offsetof(WebPConfig, low_memory), .is_float = 0}, + {.name = "method", .offset = offsetof(WebPConfig, method), .is_float = 0}, + {.name = "near_lossless", .offset = offsetof(WebPConfig, near_lossless), .is_float = 0}, + {.name = "partition_limit", .offset = offsetof(WebPConfig, partition_limit), .is_float = 0}, + {.name = "partitions", .offset = offsetof(WebPConfig, partitions), .is_float = 0}, + {.name = "pass", .offset = offsetof(WebPConfig, pass), .is_float = 0}, + {.name = "preprocessing", .offset = offsetof(WebPConfig, preprocessing), .is_float = 0}, + {.name = "qmax", .offset = offsetof(WebPConfig, qmax), .is_float = 0}, + {.name = "qmin", .offset = offsetof(WebPConfig, qmin), .is_float = 0}, + {.name = "quality", .offset = offsetof(WebPConfig, quality), .is_float = 1}, + {.name = "segments", .offset = offsetof(WebPConfig, segments), .is_float = 0}, + {.name = "sns_strength", .offset = offsetof(WebPConfig, sns_strength), .is_float = 0}, + {.name = "target_psnr", .offset = offsetof(WebPConfig, target_PSNR), .is_float = 1}, + {.name = "target_size", .offset = offsetof(WebPConfig, target_size), .is_float = 0}, + {.name = "thread_level", .offset = offsetof(WebPConfig, thread_level), .is_float = 0}, + {.name = "use_sharp_yuv", .offset = offsetof(WebPConfig, use_sharp_yuv), .is_float = 0}, + // clang-format on + + // show_compressed, use_delta_palette, and image_hint, while defined in WebPConfig, + // are deliberately not exposed since they're either useless or reserved-internal. +}; + +/** + * Cast a float value from a dict of encoder options. + * @param options Options dict. + * @param name Name of option. + * @param out Pointer to float to write to. MUST be non-NULL. + * @return 0 on success, -1 on failure (with a Python exception set). + */ +static int +config_get_float(PyObject *options, const char *name, float *out) { + PyObject *value = PyDict_GetItemString(options, name); + if (value == NULL) { + return 0; + } + double f = PyFloat_AsDouble(value); + if (f == -1.0 && PyErr_Occurred()) { + return -1; + } + *out = (float)f; + return 0; +} + +/** + * Initialize and validate a WebPConfig from a dict of encoder options. + * @param config Pointer to WebPConfig to initialize. + * @param options Dict of encoder options to read from. + * @return 0 on success, -1 on failure (with a Python exception set). + */ +static int +config_setup(WebPConfig *config, PyObject *options) { + if (!WebPConfigInit(config)) { + PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!"); + return -1; + } + + // Preset + quality needs to be set first, as it will override other options. + PyObject *preset = PyDict_GetItemString(options, "preset"); + if (preset != NULL) { + long preset_value = PyLong_AsLong(preset); + if (preset_value == -1 && PyErr_Occurred()) { + return -1; + } + float quality = config->quality; + if (config_get_float(options, "quality", &quality)) { + return -1; + } + if (!WebPConfigPreset(config, (WebPPreset)preset_value, quality)) { + PyErr_SetString(PyExc_ValueError, "invalid preset"); + return -1; + } + } + + for (size_t i = 0; i < sizeof(webp_config_options) / sizeof(webp_config_options[0]); + i++) { + const WebPConfigOption *option = &webp_config_options[i]; + char *field = (char *)config + option->offset; + if (option->is_float) { + if (config_get_float(options, option->name, (float *)field)) { + return -1; + } + } else { + PyObject *value = PyDict_GetItemString(options, option->name); + if (value == NULL) { + continue; + } + long i_value = PyLong_AsLong(value); + if (i_value == -1 && PyErr_Occurred()) { + return -1; + } + *(int *)field = (int)i_value; + } + } + + if (!WebPValidateConfig(config)) { + PyErr_SetString(PyExc_ValueError, "WebP configuration validation failed"); + return -1; + } + + return 0; +} + /* -------------------------------------------------------------------- */ /* Frame import */ /* -------------------------------------------------------------------- */ +#ifdef WORDS_BIGENDIAN +#define ARGB_A 0 +#define ARGB_R 1 +#define ARGB_G 2 +#define ARGB_B 3 +#else +#define ARGB_B 0 +#define ARGB_G 1 +#define ARGB_R 2 +#define ARGB_A 3 +#endif + static int import_frame_libwebp(WebPPicture *frame, Imaging im) { if (im->mode != IMAGING_MODE_RGBA && im->mode != IMAGING_MODE_RGB && @@ -85,8 +220,10 @@ import_frame_libwebp(WebPPicture *frame, Imaging im) { return -1; } - frame->width = im->xsize; - frame->height = im->ysize; + int xsize = im->xsize, ysize = im->ysize; + + frame->width = xsize; + frame->height = ysize; frame->use_argb = 1; // Don't convert RGB pixels to YUV if (!WebPPictureAlloc(frame)) { @@ -94,21 +231,25 @@ import_frame_libwebp(WebPPicture *frame, Imaging im) { return -2; } + // restrict safe: imIn is read-only, + // frame is a fresh allocation from libwebp. int ignore_fourth_channel = im->mode != IMAGING_MODE_RGBA; - for (int y = 0; y < im->ysize; ++y) { - UINT8 *src = (UINT8 *)im->image32[y]; - UINT32 *dst = frame->argb + frame->argb_stride * y; + for (int y = 0; y < ysize; ++y) { + const UINT8 *restrict src = (const UINT8 *)im->image32[y]; + UINT8 *restrict dst = (UINT8 *)(frame->argb + frame->argb_stride * y); if (ignore_fourth_channel) { - for (int x = 0; x < im->xsize; ++x) { - dst[x] = - ((UINT32)(src[x * 4 + 2]) | ((UINT32)(src[x * 4 + 1]) << 8) | - ((UINT32)(src[x * 4]) << 16) | (0xff << 24)); + for (int x = 0; x < xsize; x++, src += 4, dst += 4) { + dst[ARGB_R] = src[0]; + dst[ARGB_G] = src[1]; + dst[ARGB_B] = src[2]; + dst[ARGB_A] = 0xff; } } else { - for (int x = 0; x < im->xsize; ++x) { - dst[x] = - ((UINT32)(src[x * 4 + 2]) | ((UINT32)(src[x * 4 + 1]) << 8) | - ((UINT32)(src[x * 4]) << 16) | ((UINT32)(src[x * 4 + 3]) << 24)); + for (int x = 0; x < xsize; x++, src += 4, dst += 4) { + dst[ARGB_R] = src[0]; + dst[ARGB_G] = src[1]; + dst[ARGB_B] = src[2]; + dst[ARGB_A] = src[3]; } } } @@ -217,26 +358,14 @@ _anim_encoder_add(PyObject *self, PyObject *args) { PyObject *i0; Imaging im; int timestamp; - int lossless; - float quality_factor; - float alpha_quality_factor; - int method; + PyObject *options; ImagingSectionCookie cookie; WebPConfig config; WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; WebPAnimEncoder *enc = encp->enc; WebPPicture *frame = &(encp->frame); - if (!PyArg_ParseTuple( - args, - "Oiiffi", - &i0, - ×tamp, - &lossless, - &quality_factor, - &alpha_quality_factor, - &method - )) { + if (!PyArg_ParseTuple(args, "OiO!", &i0, ×tamp, &PyDict_Type, &options)) { return NULL; } @@ -256,19 +385,7 @@ _anim_encoder_add(PyObject *self, PyObject *args) { return NULL; } - // Setup config for this frame - if (!WebPConfigInit(&config)) { - PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!"); - return NULL; - } - config.lossless = lossless; - config.quality = quality_factor; - config.alpha_quality = alpha_quality_factor; - config.method = method; - - // Validate the config - if (!WebPValidateConfig(&config)) { - PyErr_SetString(PyExc_ValueError, "invalid configuration"); + if (config_setup(&config, options)) { return NULL; } @@ -557,13 +674,9 @@ static PyTypeObject WebPAnimDecoder_Type = { PyObject * WebPEncode_wrapper(PyObject *self, PyObject *args) { - int lossless; - float quality_factor; - float alpha_quality_factor; - int method; - int exact; Imaging im; PyObject *i0; + PyObject *options; uint8_t *icc_bytes; uint8_t *exif_bytes; uint8_t *xmp_bytes; @@ -580,19 +693,16 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "Oiffs#iis#s#", + "Os#s#s#O!", &i0, - &lossless, - &quality_factor, - &alpha_quality_factor, &icc_bytes, &icc_size, - &method, - &exact, &exif_bytes, &exif_size, &xmp_bytes, - &xmp_size + &xmp_size, + &PyDict_Type, + &options )) { return NULL; } @@ -607,20 +717,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { return NULL; } - // Setup config for this frame - if (!WebPConfigInit(&config)) { - PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!"); - return NULL; - } - config.lossless = lossless; - config.quality = quality_factor; - config.alpha_quality = alpha_quality_factor; - config.method = method; - config.exact = exact; - - // Validate the config - if (!WebPValidateConfig(&config)) { - PyErr_SetString(PyExc_ValueError, "invalid configuration"); + if (config_setup(&config, options)) { return NULL; }