diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index a95434624..c74452121 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -151,3 +151,15 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None: target = im.convert("RGBA") assert_image_similar(image, target, 25.0) + + +def test_alpha_quality(tmp_path: Path) -> None: + with Image.open("Tests/images/transparent.png") as im: + out = str(tmp_path / "temp.webp") + im.save(out) + + out_quality = str(tmp_path / "quality.webp") + im.save(out_quality, alpha_quality=50) + with Image.open(out) as reloaded: + with Image.open(out_quality) as reloaded_quality: + assert reloaded.tobytes() != reloaded_quality.tobytes() diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 9a730f1f9..6a9337fa5 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -188,3 +188,21 @@ def test_seek_errors() -> None: with pytest.raises(EOFError): im.seek(42) + + +def test_alpha_quality(tmp_path: Path) -> None: + with Image.open("Tests/images/transparent.png") as im: + first_frame = Image.new("L", im.size) + + out = str(tmp_path / "temp.webp") + first_frame.save(out, save_all=True, append_images=[im]) + + out_quality = str(tmp_path / "quality.webp") + first_frame.save( + out_quality, save_all=True, append_images=[im], alpha_quality=50 + ) + with Image.open(out) as reloaded: + reloaded.seek(1) + with Image.open(out_quality) as reloaded_quality: + reloaded_quality.seek(1) + assert reloaded.tobytes() != reloaded_quality.tobytes() diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 569ccb769..54f983b69 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1234,11 +1234,15 @@ 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, 0-100, Defaults to 80. For lossy, 0 gives the smallest + Integer, 0-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. +**alpha_quality** + Integer, 0-100, defaults to 100. For lossy compression only. 0 gives the + smallest size and 100 is lossless. + **method** Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4. diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 59556206a..c07abcaf9 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -217,6 +217,7 @@ def _save_all(im, fp, filename): 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) icc_profile = im.encoderinfo.get("icc_profile") or "" exif = im.encoderinfo.get("exif", "") @@ -296,6 +297,7 @@ def _save_all(im, fp, filename): rawmode, lossless, quality, + alpha_quality, method, ) @@ -310,7 +312,7 @@ def _save_all(im, fp, filename): im.seek(cur_idx) # Force encoder to flush frames - enc.add(None, round(timestamp), 0, 0, "", lossless, quality, 0) + enc.add(None, round(timestamp), 0, 0, "", lossless, quality, alpha_quality, 0) # Get the final output from the encoder data = enc.assemble(icc_profile, exif, xmp) @@ -324,6 +326,7 @@ def _save_all(im, fp, filename): def _save(im, fp, filename): lossless = im.encoderinfo.get("lossless", False) quality = im.encoderinfo.get("quality", 80) + alpha_quality = im.encoderinfo.get("alpha_quality", 100) icc_profile = im.encoderinfo.get("icc_profile") or "" exif = im.encoderinfo.get("exif", b"") if isinstance(exif, Image.Exif): @@ -343,6 +346,7 @@ def _save(im, fp, filename): im.size[1], lossless, float(quality), + float(alpha_quality), im.mode, icc_profile, method, diff --git a/src/_webp.c b/src/_webp.c index 47592547c..0a70e3357 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -195,6 +195,7 @@ _anim_encoder_add(PyObject *self, PyObject *args) { char *mode; int lossless; float quality_factor; + float alpha_quality_factor; int method; WebPConfig config; WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; @@ -203,7 +204,7 @@ _anim_encoder_add(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "z#iiisifi", + "z#iiisiffi", (char **)&rgb, &size, ×tamp, @@ -212,6 +213,7 @@ _anim_encoder_add(PyObject *self, PyObject *args) { &mode, &lossless, &quality_factor, + &alpha_quality_factor, &method)) { return NULL; } @@ -229,6 +231,7 @@ _anim_encoder_add(PyObject *self, PyObject *args) { } config.lossless = lossless; config.quality = quality_factor; + config.alpha_quality = alpha_quality_factor; config.method = method; // Validate the config @@ -578,6 +581,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { int height; int lossless; float quality_factor; + float alpha_quality_factor; int method; int exact; uint8_t *rgb; @@ -601,13 +605,14 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "y#iiifss#iis#s#", + "y#iiiffss#iis#s#", (char **)&rgb, &size, &width, &height, &lossless, &quality_factor, + &alpha_quality_factor, &mode, &icc_bytes, &icc_size, @@ -637,6 +642,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { } config.lossless = lossless; config.quality = quality_factor; + config.alpha_quality = alpha_quality_factor; config.method = method; #if WEBP_ENCODER_ABI_VERSION >= 0x0209 // the "exact" flag is only available in libwebp 0.5.0 and later