From 0a8e6dbedb84b50ca2ca8762ae1279a66c385bad Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sat, 31 Aug 2024 20:41:37 +0400 Subject: [PATCH 01/10] Use im.has_transparency_data for webp._save_all Also: remove _VALID_WEBP_MODES and _VALID_WEBP_LEGACY_MODES consts RGBX is not faster RGB since demands more bandwidth Do not convert to str paths in tests --- Tests/test_file_webp.py | 27 ++++++++++++--------------- src/PIL/WebPImagePlugin.py | 25 +++++-------------------- 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index e75e3ddd272..719831db9fb 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -72,7 +72,7 @@ def test_read_rgb(self) -> None: def _roundtrip( self, tmp_path: Path, mode: str, epsilon: float, args: dict[str, Any] = {} ) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" hopper(mode).save(temp_file, **args) with Image.open(temp_file) as image: @@ -116,7 +116,7 @@ def test_write_method(self, tmp_path: Path) -> None: assert buffer_no_args.getbuffer() != buffer_method.getbuffer() def test_save_all(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") + 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]) @@ -151,18 +151,16 @@ def test_write_unsupported_mode_P(self, tmp_path: Path) -> None: @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_message(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (15000, 15000)) with pytest.raises(ValueError) as e: - im.save(temp_file, method=0) + im.save(tmp_path / "temp.webp", method=0) assert str(e.value) == "encoding error 6" @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") im = Image.new("L", (16384, 16384)) with pytest.raises(ValueError) as e: - im.save(temp_file) + im.save(tmp_path / "temp.webp") assert ( str(e.value) == "encoding error 5: Image size exceeds WebP limit of 16383 pixels" @@ -187,9 +185,8 @@ def test_WebPAnimDecoder_with_invalid_args(self) -> None: def test_no_resource_warning(self, tmp_path: Path) -> None: file_path = "Tests/images/hopper.webp" with Image.open(file_path) as image: - temp_file = str(tmp_path / "temp.webp") with warnings.catch_warnings(): - image.save(temp_file) + image.save(tmp_path / "temp.webp") def test_file_pointer_could_be_reused(self) -> None: file_path = "Tests/images/hopper.webp" @@ -204,15 +201,16 @@ def test_file_pointer_could_be_reused(self) -> None: def test_invalid_background( self, background: int | tuple[int, ...], tmp_path: Path ) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" im = hopper() with pytest.raises(OSError): im.save(temp_file, save_all=True, append_images=[im], background=background) def test_background_from_gif(self, tmp_path: Path) -> None: + out_webp = tmp_path / "temp.webp" + # Save L mode GIF with background with Image.open("Tests/images/no_palette_with_background.gif") as im: - out_webp = str(tmp_path / "temp.webp") im.save(out_webp, save_all=True) # Save P mode GIF with background @@ -220,11 +218,10 @@ def test_background_from_gif(self, tmp_path: Path) -> None: original_value = im.convert("RGB").getpixel((1, 1)) # Save as WEBP - out_webp = str(tmp_path / "temp.webp") im.save(out_webp, save_all=True) # Save as GIF - out_gif = str(tmp_path / "temp.gif") + out_gif = tmp_path / "temp.gif" with Image.open(out_webp) as im: im.save(out_gif) @@ -234,10 +231,10 @@ def test_background_from_gif(self, tmp_path: Path) -> None: assert difference < 5 def test_duration(self, tmp_path: Path) -> None: + out_webp = tmp_path / "temp.webp" + with Image.open("Tests/images/dispose_bgnd.gif") as im: assert im.info["duration"] == 1000 - - out_webp = str(tmp_path / "temp.webp") im.save(out_webp, save_all=True) with Image.open(out_webp) as reloaded: @@ -245,7 +242,7 @@ def test_duration(self, tmp_path: Path) -> None: assert reloaded.info["duration"] == 1000 def test_roundtrip_rgba_palette(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" im = Image.new("RGBA", (1, 1)).convert("P") assert im.mode == "P" assert im.palette is not None diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index f8d6168bafd..3754d784ac3 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -13,10 +13,6 @@ SUPPORTED = False -_VALID_WEBP_MODES = {"RGBX": True, "RGBA": True, "RGB": True} - -_VALID_WEBP_LEGACY_MODES = {"RGB": True, "RGBA": True} - _VP8_MODES_BY_IDENTIFIER = { b"VP8 ": "RGB", b"VP8X": "RGBA", @@ -247,27 +243,16 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Make sure image mode is supported frame = ims - rawmode = ims.mode - if ims.mode not in _VALID_WEBP_MODES: - alpha = ( - "A" in ims.mode - or "a" in ims.mode - or (ims.mode == "P" and "A" in ims.im.getpalettemode()) - ) - rawmode = "RGBA" if alpha else "RGB" - frame = ims.convert(rawmode) - - if rawmode == "RGB": - # For faster conversion, use RGBX - rawmode = "RGBX" + if frame.mode not in ("RGBX", "RGBA", "RGB"): + frame = frame.convert("RGBA" if im.has_transparency_data else "RGB") # Append the frame to the animation encoder enc.add( - frame.tobytes("raw", rawmode), + frame.tobytes(), round(timestamp), frame.size[0], frame.size[1], - rawmode, + frame.mode, lossless, quality, alpha_quality, @@ -310,7 +295,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: method = im.encoderinfo.get("method", 4) exact = 1 if im.encoderinfo.get("exact") else 0 - if im.mode not in _VALID_WEBP_LEGACY_MODES: + if im.mode not in ("RGB", "RGBA"): im = im.convert("RGBA" if im.has_transparency_data else "RGB") data = _webp.WebPEncode( From 8bb3134b1d2fa2a13111a6555e09bbc26b96ff7a Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 1 Sep 2024 19:41:45 +0400 Subject: [PATCH 02/10] call _webp.WebPEncode with ptr --- src/PIL/WebPImagePlugin.py | 7 +--- src/_webp.c | 75 ++++++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 3754d784ac3..3eeba9400f5 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -295,17 +295,14 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: method = im.encoderinfo.get("method", 4) exact = 1 if im.encoderinfo.get("exact") else 0 - if im.mode not in ("RGB", "RGBA"): + if im.mode not in ("RGBX", "RGBA", "RGB"): im = im.convert("RGBA" if im.has_transparency_data else "RGB") data = _webp.WebPEncode( - im.tobytes(), - im.size[0], - im.size[1], + im.im.ptr, lossless, float(quality), float(alpha_quality), - im.mode, icc_profile, method, exact, diff --git a/src/_webp.c b/src/_webp.c index f59ad30367b..bfd9de5c0a5 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -566,32 +566,65 @@ static PyTypeObject WebPAnimDecoder_Type = { 0, /*tp_getset*/ }; +/* -------------------------------------------------------------------- */ +/* Frame import */ +/* -------------------------------------------------------------------- */ + +static int +import_frame_libwebp(WebPPicture *frame, Imaging im) { + UINT32 mask = 0; + + if (strcmp(im->mode, "RGBA") && strcmp(im->mode, "RGB") && strcmp(im->mode, "RGBX")) { + PyErr_SetString(PyExc_ValueError, "unsupported image mode"); + return -1; + } + + if (strcmp(im->mode, "RGBA")) { + mask = MASK_UINT32_CHANNEL_3; + } + + frame->width = im->xsize; + frame->height = im->ysize; + frame->use_argb = 1; // Don't convert RGB pixels to YUV + + if (!WebPPictureAlloc(frame)) { + PyErr_SetString(PyExc_MemoryError, "can't allocate picture frame"); + return -2; + } + + for (int y = 0; y < im->ysize; ++y) { + UINT8 *src = (UINT8 *)im->image32[y]; + UINT32 *dst = frame->argb + frame->argb_stride * y; + for (int x = 0; x < im->xsize; ++x) { + UINT32 pix = MAKE_UINT32(src[x * 4 + 2], src[x * 4 + 1], src[x * 4], src[x * 4 + 3]); + dst[x] = pix | mask; + } + } + + return 0; +} + /* -------------------------------------------------------------------- */ /* Legacy WebP Support */ /* -------------------------------------------------------------------- */ PyObject * WebPEncode_wrapper(PyObject *self, PyObject *args) { - int width; - int height; int lossless; float quality_factor; float alpha_quality_factor; int method; int exact; - uint8_t *rgb; + Imaging im; + PyObject *i0; uint8_t *icc_bytes; uint8_t *exif_bytes; uint8_t *xmp_bytes; uint8_t *output; - char *mode; - Py_ssize_t size; Py_ssize_t icc_size; Py_ssize_t exif_size; Py_ssize_t xmp_size; size_t ret_size; - int rgba_mode; - int channels; int ok; ImagingSectionCookie cookie; WebPConfig config; @@ -600,15 +633,11 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "y#iiiffss#iis#s#", - (char **)&rgb, - &size, - &width, - &height, + "Oiffs#iis#s#", + &i0, &lossless, &quality_factor, &alpha_quality_factor, - &mode, &icc_bytes, &icc_size, &method, @@ -621,15 +650,12 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { return NULL; } - rgba_mode = strcmp(mode, "RGBA") == 0; - if (!rgba_mode && strcmp(mode, "RGB") != 0) { - Py_RETURN_NONE; + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); + return NULL; } - channels = rgba_mode ? 4 : 3; - if (size < width * height * channels) { - Py_RETURN_NONE; - } + im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); // Setup config for this frame if (!WebPConfigInit(&config)) { @@ -652,14 +678,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { PyErr_SetString(PyExc_ValueError, "could not initialise picture"); return NULL; } - pic.width = width; - pic.height = height; - pic.use_argb = 1; // Don't convert RGB pixels to YUV - if (rgba_mode) { - WebPPictureImportRGBA(&pic, rgb, channels * width); - } else { - WebPPictureImportRGB(&pic, rgb, channels * width); + if (import_frame_libwebp(&pic, im)) { + return NULL; } WebPMemoryWriterInit(&writer); From 0962b468b71ea9cdb99417d091c75e86c29f768a Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 3 Sep 2024 13:47:31 +0400 Subject: [PATCH 03/10] ImagingSectionEnter for WebPAnimEncoder --- src/_webp.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index bfd9de5c0a5..ede261df655 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -190,6 +190,7 @@ _anim_encoder_add(PyObject *self, PyObject *args) { float quality_factor; float alpha_quality_factor; int method; + ImagingSectionCookie cookie; WebPConfig config; WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; WebPAnimEncoder *enc = encp->enc; @@ -246,8 +247,11 @@ _anim_encoder_add(PyObject *self, PyObject *args) { WebPPictureImportRGB(frame, rgb, 3 * width); } - // Add the frame to the encoder - if (!WebPAnimEncoderAdd(enc, frame, timestamp, &config)) { + ImagingSectionEnter(&cookie); + int ok = WebPAnimEncoderAdd(enc, frame, timestamp, &config); + ImagingSectionLeave(&cookie); + + if (!ok) { PyErr_SetString(PyExc_RuntimeError, WebPAnimEncoderGetError(enc)); return NULL; } From 4d271c8ec87355962e0345a00d9e4032007a13c5 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 3 Sep 2024 13:58:40 +0400 Subject: [PATCH 04/10] import_frame for anim_encoder_add --- src/PIL/WebPImagePlugin.py | 7 +-- src/_webp.c | 114 ++++++++++++++++++------------------- 2 files changed, 56 insertions(+), 65 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 3eeba9400f5..8251316d3d4 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -248,11 +248,8 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Append the frame to the animation encoder enc.add( - frame.tobytes(), + frame.im.ptr, round(timestamp), - frame.size[0], - frame.size[1], - frame.mode, lossless, quality, alpha_quality, @@ -270,7 +267,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), 0, 0, "", lossless, quality, alpha_quality, 0) + enc.add(None, round(timestamp), lossless, quality, alpha_quality, 0) # Get the final output from the encoder data = enc.assemble(icc_profile, exif, xmp) diff --git a/src/_webp.c b/src/_webp.c index ede261df655..9717b9bc0d1 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -83,6 +83,46 @@ HandleMuxError(WebPMuxError err, char *chunk) { return NULL; } +/* -------------------------------------------------------------------- */ +/* Frame import */ +/* -------------------------------------------------------------------- */ + +static int +import_frame_libwebp(WebPPicture *frame, Imaging im) { + UINT32 mask = 0; + + if (strcmp(im->mode, "RGBA") && strcmp(im->mode, "RGB") && + strcmp(im->mode, "RGBX")) { + PyErr_SetString(PyExc_ValueError, "unsupported image mode"); + return -1; + } + + if (strcmp(im->mode, "RGBA")) { + mask = MASK_UINT32_CHANNEL_3; + } + + frame->width = im->xsize; + frame->height = im->ysize; + frame->use_argb = 1; // Don't convert RGB pixels to YUV + + if (!WebPPictureAlloc(frame)) { + PyErr_SetString(PyExc_MemoryError, "can't allocate picture frame"); + return -2; + } + + for (int y = 0; y < im->ysize; ++y) { + UINT8 *src = (UINT8 *)im->image32[y]; + UINT32 *dst = frame->argb + frame->argb_stride * y; + for (int x = 0; x < im->xsize; ++x) { + UINT32 pix = + MAKE_UINT32(src[x * 4 + 2], src[x * 4 + 1], src[x * 4], src[x * 4 + 3]); + dst[x] = pix | mask; + } + } + + return 0; +} + /* -------------------------------------------------------------------- */ /* WebP Animation Support */ /* -------------------------------------------------------------------- */ @@ -180,12 +220,9 @@ _anim_encoder_dealloc(PyObject *self) { PyObject * _anim_encoder_add(PyObject *self, PyObject *args) { - uint8_t *rgb; - Py_ssize_t size; + PyObject *i0; + Imaging im; int timestamp; - int width; - int height; - char *mode; int lossless; float quality_factor; float alpha_quality_factor; @@ -198,13 +235,9 @@ _anim_encoder_add(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "z#iiisiffi", - (char **)&rgb, - &size, + "Oiiffi", + &i0, ×tamp, - &width, - &height, - &mode, &lossless, &quality_factor, &alpha_quality_factor, @@ -214,11 +247,18 @@ _anim_encoder_add(PyObject *self, PyObject *args) { } // Check for NULL frame, which sets duration of final frame - if (!rgb) { + if (i0 == Py_None) { WebPAnimEncoderAdd(enc, NULL, timestamp, NULL); Py_RETURN_NONE; } + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); + return NULL; + } + + im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + // Setup config for this frame if (!WebPConfigInit(&config)) { PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!"); @@ -235,16 +275,8 @@ _anim_encoder_add(PyObject *self, PyObject *args) { return NULL; } - // 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, "RGBX") == 0) { - WebPPictureImportRGBX(frame, rgb, 4 * width); - } else { - WebPPictureImportRGB(frame, rgb, 3 * width); + if (import_frame_libwebp(frame, im)) { + return NULL; } ImagingSectionEnter(&cookie); @@ -570,44 +602,6 @@ static PyTypeObject WebPAnimDecoder_Type = { 0, /*tp_getset*/ }; -/* -------------------------------------------------------------------- */ -/* Frame import */ -/* -------------------------------------------------------------------- */ - -static int -import_frame_libwebp(WebPPicture *frame, Imaging im) { - UINT32 mask = 0; - - if (strcmp(im->mode, "RGBA") && strcmp(im->mode, "RGB") && strcmp(im->mode, "RGBX")) { - PyErr_SetString(PyExc_ValueError, "unsupported image mode"); - return -1; - } - - if (strcmp(im->mode, "RGBA")) { - mask = MASK_UINT32_CHANNEL_3; - } - - frame->width = im->xsize; - frame->height = im->ysize; - frame->use_argb = 1; // Don't convert RGB pixels to YUV - - if (!WebPPictureAlloc(frame)) { - PyErr_SetString(PyExc_MemoryError, "can't allocate picture frame"); - return -2; - } - - for (int y = 0; y < im->ysize; ++y) { - UINT8 *src = (UINT8 *)im->image32[y]; - UINT32 *dst = frame->argb + frame->argb_stride * y; - for (int x = 0; x < im->xsize; ++x) { - UINT32 pix = MAKE_UINT32(src[x * 4 + 2], src[x * 4 + 1], src[x * 4], src[x * 4 + 3]); - dst[x] = pix | mask; - } - } - - return 0; -} - /* -------------------------------------------------------------------- */ /* Legacy WebP Support */ /* -------------------------------------------------------------------- */ From d1f40a94ffe783aceb7050d85fa5c17ecc4961a7 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 16 Sep 2024 10:52:06 +0200 Subject: [PATCH 05/10] Use Image.getim() instead of ImagingCore.ptr --- src/PIL/WebPImagePlugin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 8251316d3d4..ab545563f7f 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -239,7 +239,6 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: for idx in range(nfr): ims.seek(idx) - ims.load() # Make sure image mode is supported frame = ims @@ -248,7 +247,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Append the frame to the animation encoder enc.add( - frame.im.ptr, + frame.getim(), round(timestamp), lossless, quality, @@ -296,7 +295,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: im = im.convert("RGBA" if im.has_transparency_data else "RGB") data = _webp.WebPEncode( - im.im.ptr, + im.getim(), lossless, float(quality), float(alpha_quality), From 31d36e6b70e1e835bcc6d70f363cda49d3bc9e98 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 16 Sep 2024 11:04:00 +0200 Subject: [PATCH 06/10] Use current frame for transparency detection --- Tests/test_file_webp.py | 2 +- src/PIL/WebPImagePlugin.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 719831db9fb..e7c88727966 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -208,7 +208,7 @@ def test_invalid_background( def test_background_from_gif(self, tmp_path: Path) -> None: out_webp = tmp_path / "temp.webp" - + # Save L mode GIF with background with Image.open("Tests/images/no_palette_with_background.gif") as im: im.save(out_webp, save_all=True) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index ab545563f7f..1a714d7ea34 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -243,7 +243,9 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Make sure image mode is supported frame = ims if frame.mode not in ("RGBX", "RGBA", "RGB"): - frame = frame.convert("RGBA" if im.has_transparency_data else "RGB") + frame = frame.convert( + "RGBA" if frame.has_transparency_data else "RGB" + ) # Append the frame to the animation encoder enc.add( From 1d5b330758c1e9210a8c00c457b03a1f19903939 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 16 Sep 2024 15:37:57 +0200 Subject: [PATCH 07/10] Move common conversion in _convert_frame --- src/PIL/WebPImagePlugin.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 1a714d7ea34..64188f28cb3 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -149,6 +149,13 @@ def tell(self) -> int: return self.__logical_frame +def _convert_frame(im: Image.Image) -> Image.Image: + # Make sure image mode is supported + if im.mode not in ("RGBX", "RGBA", "RGB"): + im = im.convert("RGBA" if im.has_transparency_data else "RGB") + return im + + def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: encoderinfo = im.encoderinfo.copy() append_images = list(encoderinfo.get("append_images", [])) @@ -240,12 +247,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: for idx in range(nfr): ims.seek(idx) - # Make sure image mode is supported - frame = ims - if frame.mode not in ("RGBX", "RGBA", "RGB"): - frame = frame.convert( - "RGBA" if frame.has_transparency_data else "RGB" - ) + frame = _convert_frame(ims) # Append the frame to the animation encoder enc.add( @@ -293,8 +295,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: method = im.encoderinfo.get("method", 4) exact = 1 if im.encoderinfo.get("exact") else 0 - if im.mode not in ("RGBX", "RGBA", "RGB"): - im = im.convert("RGBA" if im.has_transparency_data else "RGB") + im = _convert_frame(im) data = _webp.WebPEncode( im.getim(), From a988750595af555e07af0e937ab6f6697a5bb1e1 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 16 Sep 2024 16:37:39 +0200 Subject: [PATCH 08/10] Try fix bigendian --- src/_webp.c | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index 9717b9bc0d1..92d5c20fec5 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -89,7 +89,7 @@ HandleMuxError(WebPMuxError err, char *chunk) { static int import_frame_libwebp(WebPPicture *frame, Imaging im) { - UINT32 mask = 0; + int drop_alpha = strcmp(im->mode, "RGBA"); if (strcmp(im->mode, "RGBA") && strcmp(im->mode, "RGB") && strcmp(im->mode, "RGBX")) { @@ -97,10 +97,6 @@ import_frame_libwebp(WebPPicture *frame, Imaging im) { return -1; } - if (strcmp(im->mode, "RGBA")) { - mask = MASK_UINT32_CHANNEL_3; - } - frame->width = im->xsize; frame->height = im->ysize; frame->use_argb = 1; // Don't convert RGB pixels to YUV @@ -113,10 +109,18 @@ import_frame_libwebp(WebPPicture *frame, Imaging im) { for (int y = 0; y < im->ysize; ++y) { UINT8 *src = (UINT8 *)im->image32[y]; UINT32 *dst = frame->argb + frame->argb_stride * y; - for (int x = 0; x < im->xsize; ++x) { - UINT32 pix = - MAKE_UINT32(src[x * 4 + 2], src[x * 4 + 1], src[x * 4], src[x * 4 + 3]); - dst[x] = pix | mask; + if (drop_alpha) { + 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)); + } + } 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)); + } } } From 83c7043471df575e446c44a4d384df56cd76538b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Sep 2024 15:54:27 +1000 Subject: [PATCH 09/10] Rename variable, since alpha channel is not dropped --- src/_webp.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index 92d5c20fec5..dfda7048de4 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -89,8 +89,6 @@ HandleMuxError(WebPMuxError err, char *chunk) { static int import_frame_libwebp(WebPPicture *frame, Imaging im) { - int drop_alpha = strcmp(im->mode, "RGBA"); - if (strcmp(im->mode, "RGBA") && strcmp(im->mode, "RGB") && strcmp(im->mode, "RGBX")) { PyErr_SetString(PyExc_ValueError, "unsupported image mode"); @@ -106,10 +104,11 @@ import_frame_libwebp(WebPPicture *frame, Imaging im) { return -2; } + int ignore_fourth_channel = strcmp(im->mode, "RGBA"); for (int y = 0; y < im->ysize; ++y) { UINT8 *src = (UINT8 *)im->image32[y]; UINT32 *dst = frame->argb + frame->argb_stride * y; - if (drop_alpha) { + 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) | From 75cb1c1b87ae4be028f3e15141c14c28dd0d04a0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Sep 2024 16:02:23 +1000 Subject: [PATCH 10/10] Test unsupported image mode --- Tests/test_file_webp.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index e7c88727966..79f6bb4e0e4 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -127,6 +127,11 @@ def test_save_all(self, tmp_path: Path) -> None: reloaded.seek(1) assert_image_similar(im2, reloaded, 1) + 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"", "") + def test_icc_profile(self, tmp_path: Path) -> None: self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) self._roundtrip(