From cdadf931e36cb3816096197ce91abfbe6b11df92 Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Fri, 30 Aug 2024 20:36:22 +0000 Subject: [PATCH 01/61] Improve error messages --- Tests/test_imagefont.py | 27 +++++++++++++++++++++++++++ src/PIL/ImageFont.py | 19 +++++++++++++++---- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 340cc47420b..e2a7b759ad5 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -460,6 +460,17 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: assert mask.size == (108, 13) +def test_load_raises_if_image_not_found(tmp_path) -> None: + font_path = tmp_path / "file.font" + font_path.write_bytes(b"") + with pytest.raises(OSError) as excinfo: + ImageFont.load(font_path) + + pre = tmp_path / "file" + msg = f"cannot find glyph data file {pre}.{{png|gif|pbm}}" + assert msg in str(excinfo.value) + + def test_load_path_not_found() -> None: # Arrange filename = "somefilenamethatdoesntexist.ttf" @@ -471,6 +482,22 @@ def test_load_path_not_found() -> None: ImageFont.truetype(filename) +def test_load_path_exisitng_path(tmp_path) -> None: + # First, the file doens't exist, so we don't suggest `load` + some_path = tmp_path / "file.ttf" + with pytest.raises(OSError) as excinfo: + ImageFont.load_path(str(some_path)) + assert str(some_path) in str(excinfo.value) + assert "did you mean" not in str(excinfo.value) + + # The file exists, so the error message suggests to use `load` instead + some_path.write_bytes(b"") + with pytest.raises(OSError) as excinfo: + ImageFont.load_path(str(some_path)) + assert str(some_path) in str(excinfo.value) + assert " did you mean" in str(excinfo.value) + + def test_load_non_font_bytes() -> None: with open("Tests/images/hopper.jpg", "rb") as f: with pytest.raises(OSError): diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 2ab65bfefe4..072acc31f31 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -98,11 +98,13 @@ class ImageFont: def _load_pilfont(self, filename: str) -> None: with open(filename, "rb") as fp: image: ImageFile.ImageFile | None = None + filename_body = os.path.splitext(filename)[0] + for ext in (".png", ".gif", ".pbm"): if image: image.close() try: - fullname = os.path.splitext(filename)[0] + ext + fullname = filename_body + ext image = Image.open(fullname) except Exception: pass @@ -112,7 +114,9 @@ def _load_pilfont(self, filename: str) -> None: else: if image: image.close() - msg = "cannot find glyph data file" + + pre = filename_body + msg = f"cannot find glyph data file {pre}.{{png|gif|pbm}}" raise OSError(msg) self.file = fullname @@ -224,7 +228,7 @@ def __init__( raise core.ex if size <= 0: - msg = "font size must be greater than 0" + msg = f"font size must be greater than 0, not {size}" raise ValueError(msg) self.path = font @@ -774,6 +778,8 @@ def load(filename: str) -> ImageFont: :param filename: Name of font file. :return: A font object. :exception OSError: If the file could not be read. + + .. seealso:: :py:func:`PIL.ImageFont.truetype` """ f = ImageFont() f._load_pilfont(filename) @@ -850,6 +856,8 @@ def truetype( :return: A font object. :exception OSError: If the file could not be read. :exception ValueError: If the font size is not greater than zero. + + .. seealso:: :py:func:`PIL.ImageFont.load` """ def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont: @@ -927,7 +935,10 @@ def load_path(filename: str | bytes) -> ImageFont: return load(os.path.join(directory, filename)) except OSError: pass - msg = "cannot find font file" + msg = f"cannot find font file '{filename}' in `sys.path`" + if os.path.exists(filename): + msg += f" did you mean `ImageFont.load({filename})` instead?" + raise OSError(msg) From 95194a2050081169f0b7db02040da91b1994da5a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Aug 2024 20:51:26 +1000 Subject: [PATCH 02/61] Use tempfile.NamedTemporaryFile --- Tests/test_imagefont.py | 45 ++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index e2a7b759ad5..80cd0d578a5 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -5,6 +5,7 @@ import re import shutil import sys +import tempfile from io import BytesIO from pathlib import Path from typing import Any, BinaryIO @@ -460,15 +461,15 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: assert mask.size == (108, 13) -def test_load_raises_if_image_not_found(tmp_path) -> None: - font_path = tmp_path / "file.font" - font_path.write_bytes(b"") - with pytest.raises(OSError) as excinfo: - ImageFont.load(font_path) +def test_load_when_image_not_found(tmp_path: Path) -> None: + tmpfile = tmp_path / "file.font" + tmpfile.write_bytes(b"") + tempfile = str(tmpfile) + with pytest.raises(OSError) as e: + ImageFont.load(tempfile) - pre = tmp_path / "file" - msg = f"cannot find glyph data file {pre}.{{png|gif|pbm}}" - assert msg in str(excinfo.value) + root = os.path.splitext(tempfile)[0] + assert str(e.value) == f"cannot find glyph data file {root}.{{png|gif|pbm}}" def test_load_path_not_found() -> None: @@ -476,26 +477,24 @@ def test_load_path_not_found() -> None: filename = "somefilenamethatdoesntexist.ttf" # Act/Assert - with pytest.raises(OSError): + with pytest.raises(OSError) as e: ImageFont.load_path(filename) + + # The file doesn't exist, so don't suggest `load` + assert filename in str(e.value) + assert "did you mean" not in str(e.value) with pytest.raises(OSError): ImageFont.truetype(filename) -def test_load_path_exisitng_path(tmp_path) -> None: - # First, the file doens't exist, so we don't suggest `load` - some_path = tmp_path / "file.ttf" - with pytest.raises(OSError) as excinfo: - ImageFont.load_path(str(some_path)) - assert str(some_path) in str(excinfo.value) - assert "did you mean" not in str(excinfo.value) - - # The file exists, so the error message suggests to use `load` instead - some_path.write_bytes(b"") - with pytest.raises(OSError) as excinfo: - ImageFont.load_path(str(some_path)) - assert str(some_path) in str(excinfo.value) - assert " did you mean" in str(excinfo.value) +def test_load_path_existing_path() -> None: + with tempfile.NamedTemporaryFile() as tmp: + with pytest.raises(OSError) as e: + ImageFont.load_path(tmp.name) + + # The file exists, so the error message suggests to use `load` instead + assert tmp.name in str(e.value) + assert " did you mean" in str(e.value) def test_load_non_font_bytes() -> None: From e0a75b6d695b7be82c090adb1896022e42d344ff Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Aug 2024 19:43:35 +1000 Subject: [PATCH 03/61] Renamed variable for first part of splitext to root --- src/PIL/ImageFont.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 072acc31f31..43e86a7ed25 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -98,13 +98,13 @@ class ImageFont: def _load_pilfont(self, filename: str) -> None: with open(filename, "rb") as fp: image: ImageFile.ImageFile | None = None - filename_body = os.path.splitext(filename)[0] + root = os.path.splitext(filename)[0] for ext in (".png", ".gif", ".pbm"): if image: image.close() try: - fullname = filename_body + ext + fullname = root + ext image = Image.open(fullname) except Exception: pass @@ -115,8 +115,7 @@ def _load_pilfont(self, filename: str) -> None: if image: image.close() - pre = filename_body - msg = f"cannot find glyph data file {pre}.{{png|gif|pbm}}" + msg = f"cannot find glyph data file {root}.{{png|gif|pbm}}" raise OSError(msg) self.file = fullname @@ -937,7 +936,7 @@ def load_path(filename: str | bytes) -> ImageFont: pass msg = f"cannot find font file '{filename}' in `sys.path`" if os.path.exists(filename): - msg += f" did you mean `ImageFont.load({filename})` instead?" + msg += f", did you mean `ImageFont.load({filename})` instead?" raise OSError(msg) From fcca8a3059f94640dee493164b54ec466c13ff74 Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Sat, 31 Aug 2024 19:03:11 +0000 Subject: [PATCH 04/61] Fix accidental indent --- Tests/test_imagefont.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index e2a7b759ad5..80ec7bd6b32 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -487,8 +487,8 @@ def test_load_path_exisitng_path(tmp_path) -> None: some_path = tmp_path / "file.ttf" with pytest.raises(OSError) as excinfo: ImageFont.load_path(str(some_path)) - assert str(some_path) in str(excinfo.value) - assert "did you mean" not in str(excinfo.value) + assert str(some_path) in str(excinfo.value) + assert "did you mean" not in str(excinfo.value) # The file exists, so the error message suggests to use `load` instead some_path.write_bytes(b"") From ef51e7a1c70ae684615724750c32ba3ca6923123 Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Sun, 1 Sep 2024 19:49:44 +0000 Subject: [PATCH 05/61] Fix wrong indentation for assert --- Tests/test_imagefont.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 80cd0d578a5..66e6947c2be 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -480,9 +480,9 @@ def test_load_path_not_found() -> None: with pytest.raises(OSError) as e: ImageFont.load_path(filename) - # The file doesn't exist, so don't suggest `load` - assert filename in str(e.value) - assert "did you mean" not in str(e.value) + # The file doesn't exist, so don't suggest `load` + assert filename in str(e.value) + assert "did you mean" not in str(e.value) with pytest.raises(OSError): ImageFont.truetype(filename) From e14072e9738275ffe2898f32d36cbd2f3c687cf3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 19:08:07 +1000 Subject: [PATCH 06/61] Added further detail --- src/PIL/ImageFont.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 43e86a7ed25..abfa4aa80fb 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -771,14 +771,13 @@ def getlength(self, text: str | bytes, *args: Any, **kwargs: Any) -> float: def load(filename: str) -> ImageFont: """ - Load a font file. This function loads a font object from the given - bitmap font file, and returns the corresponding font object. + Load a font file. This function loads a font object from the given + bitmap font file, and returns the corresponding font object. For loading TrueType + or OpenType fonts instead, see :py:func:`~PIL.ImageFont.truetype`. :param filename: Name of font file. :return: A font object. :exception OSError: If the file could not be read. - - .. seealso:: :py:func:`PIL.ImageFont.truetype` """ f = ImageFont() f._load_pilfont(filename) @@ -794,7 +793,8 @@ def truetype( ) -> FreeTypeFont: """ Load a TrueType or OpenType font from a file or file-like object, - and create a font object. + and create a font object. For loading bitmap fonts instead, + see :py:func:`~PIL.ImageFont.load` and :py:func:`~PIL.ImageFont.load_path`. This function loads a font object from the given file or file-like object, and creates a font object for a font of the given size. @@ -855,8 +855,6 @@ def truetype( :return: A font object. :exception OSError: If the file could not be read. :exception ValueError: If the font size is not greater than zero. - - .. seealso:: :py:func:`PIL.ImageFont.load` """ def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont: From dbe979d032afed04b93f9ce1aaa9c7f8e23542b1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 19:09:01 +1000 Subject: [PATCH 07/61] Sort extensions alphabetically in error message --- Tests/test_imagefont.py | 2 +- src/PIL/ImageFont.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 66e6947c2be..32cd1db8a70 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -469,7 +469,7 @@ def test_load_when_image_not_found(tmp_path: Path) -> None: ImageFont.load(tempfile) root = os.path.splitext(tempfile)[0] - assert str(e.value) == f"cannot find glyph data file {root}.{{png|gif|pbm}}" + assert str(e.value) == f"cannot find glyph data file {root}.{{gif|pbm|png}}" def test_load_path_not_found() -> None: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index abfa4aa80fb..b6d69019da8 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -115,7 +115,7 @@ def _load_pilfont(self, filename: str) -> None: if image: image.close() - msg = f"cannot find glyph data file {root}.{{png|gif|pbm}}" + msg = f"cannot find glyph data file {root}.{{gif|pbm|png}}" raise OSError(msg) self.file = fullname From 547832fd592ee1392533be5bf935c19c690a665f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 19:49:21 +1000 Subject: [PATCH 08/61] Use tempfile.NamedTemporaryFile --- Tests/test_imagefont.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 32cd1db8a70..e3d84756791 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -461,14 +461,15 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: assert mask.size == (108, 13) -def test_load_when_image_not_found(tmp_path: Path) -> None: - tmpfile = tmp_path / "file.font" - tmpfile.write_bytes(b"") - tempfile = str(tmpfile) +def test_load_when_image_not_found() -> None: + with tempfile.NamedTemporaryFile(delete=False) as tmp: + pass with pytest.raises(OSError) as e: - ImageFont.load(tempfile) + ImageFont.load(tmp.name) - root = os.path.splitext(tempfile)[0] + os.unlink(tmp.name) + + root = os.path.splitext(tmp.name)[0] assert str(e.value) == f"cannot find glyph data file {root}.{{gif|pbm|png}}" @@ -492,8 +493,8 @@ def test_load_path_existing_path() -> None: with pytest.raises(OSError) as e: ImageFont.load_path(tmp.name) - # The file exists, so the error message suggests to use `load` instead - assert tmp.name in str(e.value) + # The file exists, so the error message suggests to use `load` instead + assert tmp.name in str(e.value) assert " did you mean" in str(e.value) From 0a8e6dbedb84b50ca2ca8762ae1279a66c385bad Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sat, 31 Aug 2024 20:41:37 +0400 Subject: [PATCH 09/61] 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 10/61] 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 11/61] 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 12/61] 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 d2efd7dd5fcd19b7cb7768fc90ffa51676ff5e7a Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Wed, 11 Sep 2024 10:32:42 +0200 Subject: [PATCH 13/61] Update src/PIL/ImageFont.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageFont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index b6d69019da8..7ee3aaa55ec 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -934,7 +934,7 @@ def load_path(filename: str | bytes) -> ImageFont: pass msg = f"cannot find font file '{filename}' in `sys.path`" if os.path.exists(filename): - msg += f", did you mean `ImageFont.load({filename})` instead?" + msg += f", did you mean `ImageFont.load(\"{filename}\")` instead?" raise OSError(msg) From e4f13020e16925f1dcfe738218e4d03a3c3992bb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 08:33:09 +0000 Subject: [PATCH 14/61] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/ImageFont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 7ee3aaa55ec..55ab6195184 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -934,7 +934,7 @@ def load_path(filename: str | bytes) -> ImageFont: pass msg = f"cannot find font file '{filename}' in `sys.path`" if os.path.exists(filename): - msg += f", did you mean `ImageFont.load(\"{filename}\")` instead?" + msg += f', did you mean `ImageFont.load("{filename}")` instead?' raise OSError(msg) From 77503156b17a8314652dcd69941cbdb222dd7bcf Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Wed, 11 Sep 2024 14:22:45 +0200 Subject: [PATCH 15/61] Update src/PIL/ImageFont.py Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/ImageFont.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 55ab6195184..0fccee32ff5 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -932,9 +932,9 @@ def load_path(filename: str | bytes) -> ImageFont: return load(os.path.join(directory, filename)) except OSError: pass - msg = f"cannot find font file '{filename}' in `sys.path`" + msg = f"cannot find font file '{filename}' in sys.path" if os.path.exists(filename): - msg += f', did you mean `ImageFont.load("{filename}")` instead?' + msg += f', did you mean ImageFont.load("{filename}") instead?' raise OSError(msg) From 32c514d24ccac400b025c981bb27b48d6e78b2b6 Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Wed, 11 Sep 2024 14:54:53 +0200 Subject: [PATCH 16/61] Update src/PIL/ImageFont.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageFont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 0fccee32ff5..89efe5bd8cc 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -932,7 +932,7 @@ def load_path(filename: str | bytes) -> ImageFont: return load(os.path.join(directory, filename)) except OSError: pass - msg = f"cannot find font file '{filename}' in sys.path" + msg = f'cannot find font file "{filename}" in sys.path' if os.path.exists(filename): msg += f', did you mean ImageFont.load("{filename}") instead?' From c01d2d05772dd52e316ccd0b868a1c7b560cc391 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 Sep 2024 21:57:45 +1000 Subject: [PATCH 17/61] Updated libjpeg-turbo to 3.0.4 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index d1e87899fee..4d40f7fabfc 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -22,7 +22,7 @@ else HARFBUZZ_VERSION=8.5.0 fi LIBPNG_VERSION=1.6.44 -JPEGTURBO_VERSION=3.0.3 +JPEGTURBO_VERSION=3.0.4 OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.6.2 TIFF_VERSION=4.6.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b478c1c6b7a..e2022d28337 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,7 +113,7 @@ def cmd_msbuild( "FREETYPE": "2.13.3", "FRIBIDI": "1.0.15", "HARFBUZZ": "9.0.0", - "JPEGTURBO": "3.0.3", + "JPEGTURBO": "3.0.4", "LCMS2": "2.16", "LIBPNG": "1.6.44", "LIBWEBP": "1.4.0", From 8a086edbe965bff4d89fc3b189e021f0b1a2b322 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Sep 2024 08:22:58 +1000 Subject: [PATCH 18/61] Cast handle to int --- Tests/test_imagewin.py | 12 ++++++++++++ src/PIL/ImageWin.py | 19 +++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index a836bb90b6e..e7af160ddd2 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -60,6 +60,18 @@ def test_dib_mode_string(self) -> None: with pytest.raises(ValueError): ImageWin.Dib(mode) + def test_dib_hwnd(self) -> None: + mode = "RGBA" + size = (128, 128) + wnd = 0 + + dib = ImageWin.Dib(mode, size) + hwnd = ImageWin.HWND(wnd) + + dib.expose(hwnd) + dib.draw(hwnd, (0, 0) + size) + assert isinstance(dib.query_palette(hwnd), int) + def test_dib_paste(self) -> None: # Arrange im = hopper() diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index 6fc7cfaf528..98c28f29f1d 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -98,14 +98,15 @@ def expose(self, handle: int | HDC | HWND) -> None: HDC or HWND instance. In PythonWin, you can use ``CDC.GetHandleAttrib()`` to get a suitable handle. """ + handle_int = int(handle) if isinstance(handle, HWND): - dc = self.image.getdc(handle) + dc = self.image.getdc(handle_int) try: self.image.expose(dc) finally: - self.image.releasedc(handle, dc) + self.image.releasedc(handle_int, dc) else: - self.image.expose(handle) + self.image.expose(handle_int) def draw( self, @@ -124,14 +125,15 @@ def draw( """ if src is None: src = (0, 0) + self.size + handle_int = int(handle) if isinstance(handle, HWND): - dc = self.image.getdc(handle) + dc = self.image.getdc(handle_int) try: self.image.draw(dc, dst, src) finally: - self.image.releasedc(handle, dc) + self.image.releasedc(handle_int, dc) else: - self.image.draw(handle, dst, src) + self.image.draw(handle_int, dst, src) def query_palette(self, handle: int | HDC | HWND) -> int: """ @@ -148,14 +150,15 @@ def query_palette(self, handle: int | HDC | HWND) -> int: :return: The number of entries that were changed (if one or more entries, this indicates that the image should be redrawn). """ + handle_int = int(handle) if isinstance(handle, HWND): - handle = self.image.getdc(handle) + handle = self.image.getdc(handle_int) try: result = self.image.query_palette(handle) finally: self.image.releasedc(handle, handle) else: - result = self.image.query_palette(handle) + result = self.image.query_palette(handle_int) return result def paste( From d1f40a94ffe783aceb7050d85fa5c17ecc4961a7 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 16 Sep 2024 10:52:06 +0200 Subject: [PATCH 19/61] 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 20/61] 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 21/61] 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 22/61] 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 75e4d5a10d171b9f101f09a671826f3f302139cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Sep 2024 06:12:03 +0000 Subject: [PATCH 23/61] Update dependency cibuildwheel to v2.21.1 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 210f29d3a0c..7dc3a53fe4d 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.21.0 +cibuildwheel==2.21.1 From f1e86965f6f791bf1af57f4d202a0c342221190b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Sep 2024 20:26:06 +1000 Subject: [PATCH 24/61] Use transposed size after opening for TIFF images --- Tests/test_image_copy.py | 2 ++ Tests/test_image_resize.py | 4 +--- Tests/test_image_thumbnail.py | 8 +++----- Tests/test_numpy.py | 4 +++- src/PIL/Image.py | 20 +++++--------------- src/PIL/ImageFile.py | 2 +- src/PIL/TiffImagePlugin.py | 16 +++++++++++++++- 7 files changed, 30 insertions(+), 26 deletions(-) diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index 027e5338b7a..a0b829cc565 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -49,5 +49,7 @@ def test_copy_zero() -> None: @skip_unless_feature("libtiff") def test_deepcopy() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: + assert im.size == (590, 88) + out = copy.deepcopy(im) assert out.size == (590, 88) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index c9e3045121f..d9ddf500913 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -300,9 +300,7 @@ def resize(mode: str, size: tuple[int, int] | list[int]) -> None: im.resize((10, 10), "unknown") @skip_unless_feature("libtiff") - def test_load_first(self) -> None: - # load() may change the size of the image - # Test that resize() is calling it before getting the size + def test_transposed(self) -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: im = im.resize((64, 64)) assert im.size == (64, 64) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index bdbf09c407e..01bd4b1d76b 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -92,15 +92,13 @@ def test_no_resize() -> None: @skip_unless_feature("libtiff") -def test_load_first() -> None: - # load() may change the size of the image - # Test that thumbnail() is calling it before performing size calculations +def test_transposed() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: + assert im.size == (590, 88) + im.thumbnail((64, 64)) assert im.size == (64, 10) - # Test thumbnail(), without draft(), - # on an image that is large enough once load() has changed the size with Image.open("Tests/images/g4_orientation_5.tif") as im: im.thumbnail((590, 88), reducing_gap=None) assert im.size == (590, 88) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 312e32e0ca7..040472d69e3 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -238,8 +238,10 @@ def test_zero_size() -> None: @skip_unless_feature("libtiff") -def test_load_first() -> None: +def test_transposed() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: + assert im.size == (590, 88) + a = numpy.array(im) assert a.shape == (88, 590) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f88cdf6e2f7..3f94cef3892 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2332,7 +2332,6 @@ def resize( msg = "reducing_gap must be 1.0 or greater" raise ValueError(msg) - self.load() if box is None: box = (0, 0) + self.size @@ -2781,27 +2780,18 @@ def round_aspect(number: float, key: Callable[[int], float]) -> int: ) return x, y + preserved_size = preserve_aspect_ratio() + if preserved_size is None: + return + final_size = preserved_size + box = None - final_size: tuple[int, int] if reducing_gap is not None: - preserved_size = preserve_aspect_ratio() - if preserved_size is None: - return - final_size = preserved_size - res = self.draft( None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap)) ) if res is not None: box = res[1] - if box is None: - self.load() - - # load() may have changed the size of the image - preserved_size = preserve_aspect_ratio() - if preserved_size is None: - return - final_size = preserved_size if self.size != final_size: im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index b8bf0b7fe90..721319fd7bf 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -322,7 +322,7 @@ def load(self) -> Image.core.PixelAccess | None: def load_prepare(self) -> None: # create image memory if necessary - if self._im is None or self.im.mode != self.mode or self.im.size != self.size: + if self._im is None or self.im.mode != self.mode: self.im = Image.core.new(self.mode, self.size) # create palette (optional) if self.mode == "P": diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index cc16cbfb083..28e50474471 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1275,6 +1275,11 @@ def load(self) -> Image.core.PixelAccess | None: return self._load_libtiff() return super().load() + def load_prepare(self) -> None: + if self._im is None and self._will_be_transposed: + self.im = Image.core.new(self.mode, self.size[::-1]) + ImageFile.ImageFile.load_prepare(self) + def load_end(self) -> None: # allow closing if we're on the first frame, there's no next # This is the ImageFile.load path only, libtiff specific below. @@ -1416,7 +1421,16 @@ def _setup(self) -> None: if not isinstance(xsize, int) or not isinstance(ysize, int): msg = "Invalid dimensions" raise ValueError(msg) - self._size = xsize, ysize + self._will_be_transposed = self.tag_v2.get(ExifTags.Base.Orientation) in ( + 5, + 6, + 7, + 8, + ) + if self._will_be_transposed: + self._size = ysize, xsize + else: + self._size = xsize, ysize logger.debug("- size: %s", self.size) From 84e275d90629bfdf508f81a1142eddd5d5ea46e4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Sep 2024 20:27:35 +1000 Subject: [PATCH 25/61] Loading does not change mode --- src/PIL/ImageFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 721319fd7bf..d69d8456850 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -322,7 +322,7 @@ def load(self) -> Image.core.PixelAccess | None: def load_prepare(self) -> None: # create image memory if necessary - if self._im is None or self.im.mode != self.mode: + if self._im is None: self.im = Image.core.new(self.mode, self.size) # create palette (optional) if self.mode == "P": From 9135fd0fb24607510f38b5fa4b1b4d279974363a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Sep 2024 20:56:55 +1000 Subject: [PATCH 26/61] Mention limit in error message --- src/PIL/PngImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index c268d7b1a28..28ade293e96 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -140,7 +140,7 @@ def _safe_zlib_decompress(s: bytes) -> bytes: dobj = zlib.decompressobj() plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) if dobj.unconsumed_tail: - msg = "Decompressed Data Too Large" + msg = "Decompressed data too large for PngImagePlugin.MAX_TEXT_CHUNK" raise ValueError(msg) return plaintext From a859695d9a0bf9d9dc20ef7c053909658a12df6a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 18 Sep 2024 21:17:49 +1000 Subject: [PATCH 27/61] Rearranged code Co-authored-by: Alexander Karpinsky --- src/PIL/TiffImagePlugin.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 28e50474471..437f7d98bd7 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1421,12 +1421,8 @@ def _setup(self) -> None: if not isinstance(xsize, int) or not isinstance(ysize, int): msg = "Invalid dimensions" raise ValueError(msg) - self._will_be_transposed = self.tag_v2.get(ExifTags.Base.Orientation) in ( - 5, - 6, - 7, - 8, - ) + orientation = self.tag_v2.get(ExifTags.Base.Orientation) + self._will_be_transposed = orientation in (5, 6, 7, 8) if self._will_be_transposed: self._size = ysize, xsize else: From a92dca66bdeb26106447079fcf2d36bc397298c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Sep 2024 21:22:59 +1000 Subject: [PATCH 28/61] Use raw size for striped tiles --- src/PIL/TiffImagePlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 437f7d98bd7..c024e57fce4 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1569,7 +1569,7 @@ def _setup(self) -> None: if STRIPOFFSETS in self.tag_v2: offsets = self.tag_v2[STRIPOFFSETS] h = self.tag_v2.get(ROWSPERSTRIP, ysize) - w = self.size[0] + w = xsize else: # tiled image offsets = self.tag_v2[TILEOFFSETS] @@ -1603,9 +1603,9 @@ def _setup(self) -> None: ) ) x = x + w - if x >= self.size[0]: + if x >= xsize: x, y = 0, y + h - if y >= self.size[1]: + if y >= ysize: x = y = 0 layer += 1 else: From 629f5be52eb9043d70b5d5467f10a07ad3a68cdf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Sep 2024 22:56:29 +1000 Subject: [PATCH 29/61] In seek(), create core image at size needed for loading --- src/PIL/TiffImagePlugin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index c024e57fce4..14e6ea2786e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1194,8 +1194,8 @@ def seek(self, frame: int) -> None: # Create a new core image object on second and # subsequent frames in the image. Image may be # different size/mode. - Image._decompression_bomb_check(self.size) - self.im = Image.core.new(self.mode, self.size) + Image._decompression_bomb_check(self._tile_size) + self.im = Image.core.new(self.mode, self._tile_size) def _seek(self, frame: int) -> None: self.fp = self._fp @@ -1276,8 +1276,8 @@ def load(self) -> Image.core.PixelAccess | None: return super().load() def load_prepare(self) -> None: - if self._im is None and self._will_be_transposed: - self.im = Image.core.new(self.mode, self.size[::-1]) + if self._im is None: + self.im = Image.core.new(self.mode, self._tile_size) ImageFile.ImageFile.load_prepare(self) def load_end(self) -> None: @@ -1421,9 +1421,9 @@ def _setup(self) -> None: if not isinstance(xsize, int) or not isinstance(ysize, int): msg = "Invalid dimensions" raise ValueError(msg) + self._tile_size = xsize, ysize orientation = self.tag_v2.get(ExifTags.Base.Orientation) - self._will_be_transposed = orientation in (5, 6, 7, 8) - if self._will_be_transposed: + if orientation in (5, 6, 7, 8): self._size = ysize, xsize else: self._size = xsize, ysize From 9adb476f37f28cd4a27bb66c6242e4d11925b53b Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 18 Sep 2024 23:58:23 +1000 Subject: [PATCH 30/61] Rearranged text --- src/PIL/ImageFont.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 70cd57de7be..b694b817e65 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -808,10 +808,10 @@ def truetype( ) -> FreeTypeFont: """ Load a TrueType or OpenType font from a file or file-like object, - and create a font object. For loading bitmap fonts instead, - see :py:func:`~PIL.ImageFont.load` and :py:func:`~PIL.ImageFont.load_path`. - This function loads a font object from the given file or file-like - object, and creates a font object for a font of the given size. + and create a font object. This function loads a font object from the given + file or file-like object, and creates a font object for a font of the given + size. For loading bitmap fonts instead, see :py:func:`~PIL.ImageFont.load` + and :py:func:`~PIL.ImageFont.load_path`. Pillow uses FreeType to open font files. On Windows, be aware that FreeType will keep the file open as long as the FreeTypeFont object exists. Windows From 362ffaf9b7f063609bcd02ef30b1288e16e99cd1 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 18 Aug 2024 20:39:14 -0500 Subject: [PATCH 31/61] implement tiff exif multistrip support --- Tests/test_file_tiff.py | 3 ++- Tests/test_file_tiff_metadata.py | 22 ++++++++++++++++++++++ src/PIL/TiffImagePlugin.py | 8 +++++--- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 190f83f40fc..44da25295ef 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -108,7 +108,8 @@ def test_bigtiff(self, tmp_path: Path) -> None: assert_image_equal_tofile(im, "Tests/images/hopper.tif") with Image.open("Tests/images/hopper_bigtiff.tif") as im: - # multistrip support not yet implemented + # The data type of this file's StripOffsets tag is LONG8, + # which is not yet supported for offset data when saving multiple frames. del im.tag_v2[273] outfile = str(tmp_path / "temp.tif") diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 1e0310001a5..00fe745cf59 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -181,6 +181,28 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG +def test_save_multiple_stripoffsets() -> None: + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[TiffImagePlugin.STRIPOFFSETS] = (123, 456) + assert ifd.tagtype[TiffImagePlugin.STRIPOFFSETS] == TiffTags.LONG + + # all values are in little-endian + assert ifd.tobytes() == ( + # number of tags == 1 + b"\x01\x00" + # tag id (2 bytes), type (2 bytes), count (4 bytes), value (4 bytes) + # == 273, 4, 2, 18 + # == TiffImagePlugin.STRIPOFFSETS, TiffTags.LONG, 2, 18 + # the value is the index of the tag data + b"\x11\x01\x04\x00\x02\x00\x00\x00\x12\x00\x00\x00" + # end of tags marker + b"\x00\x00\x00\x00" + # tag data == (149, 482) == (123 + 26, 456 + 26) + # 26 is the number of bytes before this data + b"\x95\x00\x00\x00\xe2\x01\x00\x00" + ) + + def test_no_duplicate_50741_tag() -> None: assert TAG_IDS["MakerNoteSafety"] == 50741 assert TAG_IDS["BestQualityScale"] == 50780 diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 14e6ea2786e..d4c46a79778 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -991,9 +991,11 @@ def tobytes(self, offset: int = 0) -> bytes: if stripoffsets is not None: tag, typ, count, value, data = entries[stripoffsets] if data: - msg = "multistrip support not yet implemented" - raise NotImplementedError(msg) - value = self._pack("L", self._unpack("L", value)[0] + offset) + size, handler = self._load_dispatch[typ] + values = [val + offset for val in handler(self, data, self.legacy_api)] + data = self._write_dispatch[typ](self, *values) + else: + value = self._pack("L", self._unpack("L", value)[0] + offset) entries[stripoffsets] = tag, typ, count, value, data # pass 2: write entries to file From 1392eab89b9c7e6bbc1fa5a1d3af2434bcb04e5d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 19 Sep 2024 09:21:23 +1000 Subject: [PATCH 32/61] Updated Ghostscript to 10.4.0 --- .appveyor.yml | 4 ++-- .github/workflows/test-windows.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index f490561cdcd..de5e527422d 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -34,8 +34,8 @@ install: - xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images - curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip - 7z x nasm-win64.zip -oc:\ -- choco install ghostscript --version=10.3.1 -- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH% +- choco install ghostscript --version=10.4.0 +- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.04.0\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | c:\python39\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index f58e8dae3a6..13147d86ba5 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -86,8 +86,8 @@ jobs: choco install nasm --no-progress echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - choco install ghostscript --version=10.3.1 --no-progress - echo "C:\Program Files\gs\gs10.03.1\bin" >> $env:GITHUB_PATH + choco install ghostscript --version=10.4.0 --no-progress + echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images From 0f47ecd4325268156c27fa86799d31829f31fcea Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 19 Sep 2024 15:07:06 +1000 Subject: [PATCH 33/61] Update CHANGES.rst [ci skip] --- CHANGES.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f23ec609fa4..4ab9eaf2bd8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,18 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Use transposed size after opening for TIFF images #8390 + [radarhere, homm] + +- Improve ImageFont error messages #8338 + [yngvem, radarhere, hugovk] + +- Mention MAX_TEXT_CHUNK limit in PNG error message #8391 + [radarhere] + +- Cast Dib handle to int #8385 + [radarhere] + - Accept float stroke widths #8369 [radarhere] From 46f2fa17be9768ec2647988b0446dd848b833a5b Mon Sep 17 00:00:00 2001 From: PavlNekrasov <95914807+PavlNekrasov@users.noreply.github.com> Date: Thu, 19 Sep 2024 22:53:45 +0300 Subject: [PATCH 34/61] fix dereference before checking for NULL in the `ImagingTransformAffine` function The `imIn` pointer is checked for `NULL`, but it seems to be dereferenced before this check. You must first make sure that the pointer is not `NULL` before using it. --- src/libImaging/Geometry.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index 2bfeed7b6c6..264c7d1691f 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -1035,6 +1035,10 @@ ImagingTransformAffine( double xx, yy; double xo, yo; + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + if (filterid || imIn->type == IMAGING_TYPE_SPECIAL) { return ImagingGenericTransform( imOut, imIn, x0, y0, x1, y1, affine_transform, a, filterid, fill @@ -1046,10 +1050,6 @@ ImagingTransformAffine( return ImagingScaleAffine(imOut, imIn, x0, y0, x1, y1, a, fill); } - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { - return (Imaging)ImagingError_ModeError(); - } - if (x0 < 0) { x0 = 0; } From 9424b1a8920201b52086c0b06b5148db33255668 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:59:08 +0000 Subject: [PATCH 35/61] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/libImaging/Geometry.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index 264c7d1691f..84aa442f067 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -1037,7 +1037,7 @@ ImagingTransformAffine( if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { return (Imaging)ImagingError_ModeError(); - } + } if (filterid || imIn->type == IMAGING_TYPE_SPECIAL) { return ImagingGenericTransform( From 693a68b2bb7fdf01bbafb7070320c16ec57ca78d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 20 Sep 2024 18:46:47 +1000 Subject: [PATCH 36/61] Updated link to OSS Fuzz issues --- README.md | 2 +- docs/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b4c6d2987ff..5bbebaccb4a 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ As of 2019, Pillow development is Code coverage - Fuzzing Status diff --git a/docs/index.rst b/docs/index.rst index 3a12953f08b..18f5c3d13e7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -54,7 +54,7 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Fri, 20 Sep 2024 19:20:53 +1000 Subject: [PATCH 37/61] Check image value before use --- src/libImaging/Geometry.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index 84aa442f067..1e2abd7e75c 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -791,15 +791,15 @@ ImagingGenericTransform( char *out; double xx, yy; + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + ImagingTransformFilter filter = getfilter(imIn, filterid); if (!filter) { return (Imaging)ImagingError_ValueError("bad filter number"); } - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { - return (Imaging)ImagingError_ModeError(); - } - ImagingCopyPalette(imOut, imIn); ImagingSectionEnter(&cookie); From 8b6fa92cc885cbd1d7b0beb60aaad9ab47298cbf Mon Sep 17 00:00:00 2001 From: Yay295 Date: Fri, 20 Sep 2024 08:39:11 -0500 Subject: [PATCH 38/61] rewrite some comments Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_tiff_metadata.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 00fe745cf59..36aabf4f82c 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -191,14 +191,15 @@ def test_save_multiple_stripoffsets() -> None: # number of tags == 1 b"\x01\x00" # tag id (2 bytes), type (2 bytes), count (4 bytes), value (4 bytes) - # == 273, 4, 2, 18 - # == TiffImagePlugin.STRIPOFFSETS, TiffTags.LONG, 2, 18 - # the value is the index of the tag data + # TiffImagePlugin.STRIPOFFSETS, TiffTags.LONG, 2, 18 + # where STRIPOFFSETS is 273, LONG is 4 + # and 18 is the offset of the tag data b"\x11\x01\x04\x00\x02\x00\x00\x00\x12\x00\x00\x00" - # end of tags marker + # end of entries b"\x00\x00\x00\x00" - # tag data == (149, 482) == (123 + 26, 456 + 26) - # 26 is the number of bytes before this data + # 26 is the total number of bytes output, + # the offset for any auxiliary strip data that will then be appended + # (123 + 26, 456 + 26) == (149, 482) b"\x95\x00\x00\x00\xe2\x01\x00\x00" ) From 83c7043471df575e446c44a4d384df56cd76538b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Sep 2024 15:54:27 +1000 Subject: [PATCH 39/61] 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 40/61] 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( From 8fa5ba844361536135d3d6cb67e3a3f4a1a651f4 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 19 Sep 2024 11:05:52 -0500 Subject: [PATCH 41/61] add common windows ci test script --- .appveyor.yml | 5 ++--- .ci/test.cmd | 3 +++ .github/workflows/test-windows.yml | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 .ci/test.cmd diff --git a/.appveyor.yml b/.appveyor.yml index f490561cdcd..7d45e11ee5c 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -53,9 +53,8 @@ test_script: - cd c:\pillow - '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma' - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% -- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' -- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' -#- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest? +- path %PYTHON%;%PATH% +- .ci\test.cmd after_test: - curl -Os https://uploader.codecov.io/latest/windows/codecov.exe diff --git a/.ci/test.cmd b/.ci/test.cmd new file mode 100644 index 00000000000..aafc9290c80 --- /dev/null +++ b/.ci/test.cmd @@ -0,0 +1,3 @@ +python.exe -c "from PIL import Image" +IF ERRORLEVEL 1 EXIT /B +python.exe -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index f58e8dae3a6..e36dc05fd09 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -190,8 +190,8 @@ jobs: - name: Test Pillow run: | - path %GITHUB_WORKSPACE%\\winbuild\\build\\bin;%PATH% - python.exe -m pytest -vx -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests + path %GITHUB_WORKSPACE%\winbuild\build\bin;%PATH% + .ci\test.cmd shell: cmd - name: Prepare to upload errors From be3192ecd41581761a78913950ec3fb4be4dd981 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 19 Sep 2024 12:08:34 -0500 Subject: [PATCH 42/61] use .ci/test.sh in test-mingw.yml --- .ci/test.sh | 2 +- .github/workflows/test-mingw.yml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.ci/test.sh b/.ci/test.sh index 8ff7c5f6483..3f0ddc350a9 100755 --- a/.ci/test.sh +++ b/.ci/test.sh @@ -4,4 +4,4 @@ set -e python3 -c "from PIL import Image" -python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests $REVERSE +python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests $REVERSE diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index e5e1ec32e2b..c7a73439ca9 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -80,8 +80,7 @@ jobs: - name: Test Pillow run: | python3 selftest.py --installed - python3 -c "from PIL import Image" - python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests + .ci/test.sh - name: Upload coverage uses: codecov/codecov-action@v4 From 6081640aa5b68a177ed54cfd03e4ae6a748e8be8 Mon Sep 17 00:00:00 2001 From: Nulano Date: Sun, 22 Sep 2024 22:46:35 +0200 Subject: [PATCH 43/61] winbuild: Build freetype.vcxproj to fix passing in custom parameters. --- winbuild/build_prepare.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e2022d28337..d350e2575a5 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -292,8 +292,12 @@ def cmd_msbuild( }, "build": [ cmd_rmdir("objs"), - cmd_msbuild("MSBuild.sln", "Release Static", "Clean"), - cmd_msbuild("MSBuild.sln", "Release Static", "Build"), + cmd_msbuild( + r"builds\windows\vc2010\freetype.vcxproj", "Release Static", "Clean" + ), + cmd_msbuild( + r"builds\windows\vc2010\freetype.vcxproj", "Release Static", "Build" + ), cmd_xcopy("include", "{inc_dir}"), ], "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], From 8adf15a6ee1bab667a0e7b76d19ea0f26b29ce85 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 23 Sep 2024 09:29:16 +1000 Subject: [PATCH 44/61] Updated raqm to 0.10.2 --- depends/install_raqm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index 070ba23a178..5d862403ee0 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -2,7 +2,7 @@ # install raqm -archive=libraqm-0.10.1 +archive=libraqm-0.10.2 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz From 9fe014876ab3dbc2a765ae1175659cc0d7bc7e42 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 22 Sep 2024 22:42:39 +1000 Subject: [PATCH 45/61] Shared libraries may be located within usr/lib64 --- depends/install_imagequant.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 867b31e4d19..060d9840ec9 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -23,7 +23,12 @@ else cargo cinstall --prefix=/usr --destdir=. # Copy into place - sudo cp usr/lib/libimagequant.so* /usr/lib/ + if [ -d "usr/lib64" ]; then + lib="lib64" + else + lib="lib" + fi + sudo cp usr/$lib/libimagequant.so* /usr/lib/ sudo cp usr/include/libimagequant.h /usr/include/ if [ -n "$GITHUB_ACTIONS" ]; then From 3f8b496431c3eb2f0cedad35669e725fd3be45e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 24 Sep 2024 09:54:26 +1000 Subject: [PATCH 46/61] Updated harfbuzz to 10.0.0 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4d40f7fabfc..92e9ad5af9a 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -17,7 +17,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 if [[ "$MB_ML_VER" != 2014 ]]; then - HARFBUZZ_VERSION=9.0.0 + HARFBUZZ_VERSION=10.0.0 else HARFBUZZ_VERSION=8.5.0 fi diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index d350e2575a5..db592d23c4e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -112,7 +112,7 @@ def cmd_msbuild( "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.15", - "HARFBUZZ": "9.0.0", + "HARFBUZZ": "10.0.0", "JPEGTURBO": "3.0.4", "LCMS2": "2.16", "LIBPNG": "1.6.44", From 61c83a882cc0a1b78f2e8f1e3ce38616f95c6f3e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Sep 2024 00:00:22 +1000 Subject: [PATCH 47/61] Updated macOS version to 10.15 for PyPy x86-64 --- .github/workflows/wheels.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 11564c14256..d3c2ac44c80 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -105,13 +105,18 @@ jobs: - name: "macOS 10.10 x86_64" os: macos-13 cibw_arch: x86_64 - build: "pp310* cp3{9,10,11}*" + build: "cp3{9,10,11}*" macosx_deployment_target: "10.10" - name: "macOS 10.13 x86_64" os: macos-13 cibw_arch: x86_64 build: "cp3{12,13}*" macosx_deployment_target: "10.13" + - name: "macOS 10.15 x86_64" + os: macos-13 + cibw_arch: x86_64 + build: "pp310*" + macosx_deployment_target: "10.15" - name: "macOS arm64" os: macos-latest cibw_arch: arm64 From 72144ec5300d92bfffd029ccfb52e4d087a0bd95 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Sep 2024 08:33:18 +1000 Subject: [PATCH 48/61] Updated harfbuzz to 10.0.1 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 92e9ad5af9a..b5fbdc4215e 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -17,7 +17,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 if [[ "$MB_ML_VER" != 2014 ]]; then - HARFBUZZ_VERSION=10.0.0 + HARFBUZZ_VERSION=10.0.1 else HARFBUZZ_VERSION=8.5.0 fi diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index db592d23c4e..84ad6417e50 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -112,7 +112,7 @@ def cmd_msbuild( "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.15", - "HARFBUZZ": "10.0.0", + "HARFBUZZ": "10.0.1", "JPEGTURBO": "3.0.4", "LCMS2": "2.16", "LIBPNG": "1.6.44", From 5d16eb73eb658daceb7b0af9f8d29cca96888a48 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Sep 2024 19:07:32 +1000 Subject: [PATCH 49/61] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4ab9eaf2bd8..ae7370a79b4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Fixed writing multiple StripOffsets to TIFF #8317 + [Yay295, radarhere] + +- Shared imagequant libraries may be located within usr/lib64 #8407 + [radarhere] + +- Fix dereference before checking for NULL in ImagingTransformAffine #8398 + [PavlNekrasov] + - Use transposed size after opening for TIFF images #8390 [radarhere, homm] From d8e3572caad2f3dce65db6951909f161a4021687 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Sep 2024 08:52:29 +1000 Subject: [PATCH 50/61] Updated fribidi to 1.0.16 --- winbuild/build_prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 84ad6417e50..026d9d30695 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -111,7 +111,7 @@ def cmd_msbuild( V = { "BROTLI": "1.1.0", "FREETYPE": "2.13.3", - "FRIBIDI": "1.0.15", + "FRIBIDI": "1.0.16", "HARFBUZZ": "10.0.1", "JPEGTURBO": "3.0.4", "LCMS2": "2.16", From 4ca2b92503ed0250d7e48bf4c187c4fd1dbbdc54 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Sep 2024 14:59:27 +1000 Subject: [PATCH 51/61] Use $IS_ALPINE instead of $MB_ML_LIBC --- .github/workflows/wheels-dependencies.sh | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index b5fbdc4215e..5175a2a9228 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -156,12 +156,10 @@ if [[ -n "$IS_MACOS" ]]; then fi brew install meson pkg-config -elif [[ "$MB_ML_LIBC" == "manylinux" ]]; then - if [[ "$HARFBUZZ_VERSION" != 8.5.0 ]]; then - yum install -y meson - fi -else +elif [[ -n "$IS_ALPINE" ]]; then apk add meson +elif [[ "$HARFBUZZ_VERSION" != 8.5.0 ]]; then + yum install -y meson fi wrap_wheel_builder build From 71da6d8952c174fa0a465fa6caf98989d6eff9c9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Sep 2024 16:37:10 +1000 Subject: [PATCH 52/61] Downgrade harfbuzz on OSS Fuzz --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 5175a2a9228..4289afdf3a5 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -16,7 +16,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 -if [[ "$MB_ML_VER" != 2014 ]]; then +if [[ "$MB_ML_VER" != 2014 ]] && [[ -z "$SANITIZER" ]]; then HARFBUZZ_VERSION=10.0.1 else HARFBUZZ_VERSION=8.5.0 From fb8db83122a2f942958fc93103ad7b043cca3a3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Sep 2024 17:35:06 +1000 Subject: [PATCH 53/61] Updated harfbuzz to 10.0.1 on macOS --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4289afdf3a5..bb0f8a307e3 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -16,7 +16,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 -if [[ "$MB_ML_VER" != 2014 ]] && [[ -z "$SANITIZER" ]]; then +if [[ -n "$IS_MACOS" ]] || ([[ "$MB_ML_VER" != 2014 ]] && [[ -z "$SANITIZER" ]]); then HARFBUZZ_VERSION=10.0.1 else HARFBUZZ_VERSION=8.5.0 From 485a062010d0942f88686dbf7ece6a765d329e53 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Sep 2024 23:13:03 +1000 Subject: [PATCH 54/61] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ae7370a79b4..6ff35ea4f98 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Use Capsule for WebP saving #8386 + [homm, radarhere] + - Fixed writing multiple StripOffsets to TIFF #8317 [Yay295, radarhere] From bda62c1ac52f6a843263d2e195025d204a094f78 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Sep 2024 22:11:03 +1000 Subject: [PATCH 55/61] Revert "Temporarily disable cifuzz" This reverts commit 31469407166026ec6d74d2df07196ef2d4e32ab4. --- .github/workflows/cifuzz.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 033ff98ce74..eb73fc6a7cd 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -24,8 +24,6 @@ concurrency: jobs: Fuzzing: - # Disabled until google/oss-fuzz#11419 upgrades Python to 3.9+ - if: false runs-on: ubuntu-latest steps: - name: Build Fuzzers From e306546bf18b01c9bfa68ea397f2f5d5690113e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Sep 2024 15:10:16 +1000 Subject: [PATCH 56/61] Use ruff check --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 94f7565d826..ec415962740 100644 --- a/Makefile +++ b/Makefile @@ -117,7 +117,7 @@ lint-fix: python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black python3 -m black . python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff - python3 -m ruff --fix . + python3 -m ruff check --fix . .PHONY: mypy mypy: From 2e73ffe7034457461b13da71b05645999dd3c0ef Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Sep 2024 15:29:09 +1000 Subject: [PATCH 57/61] Exclude multibuild from black and ruff --- pyproject.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 228c344e864..0d0a6f17084 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,9 +97,13 @@ config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" +[tool.black] +exclude = "wheels/multibuild" + [tool.ruff] -fix = true +exclude = [ "wheels/multibuild" ] +fix = true lint.select = [ "C4", # flake8-comprehensions "E", # pycodestyle errors From 30fca7a1d62a8b41f3536adb35355f5aa81c6a2f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Sep 2024 18:38:45 +1000 Subject: [PATCH 58/61] Install meson through pip --- .github/workflows/wheels-dependencies.sh | 36 ++++++++---------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index bb0f8a307e3..b3996d5a110 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -16,11 +16,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 -if [[ -n "$IS_MACOS" ]] || ([[ "$MB_ML_VER" != 2014 ]] && [[ -z "$SANITIZER" ]]); then - HARFBUZZ_VERSION=10.0.1 -else - HARFBUZZ_VERSION=8.5.0 -fi +HARFBUZZ_VERSION=10.0.1 LIBPNG_VERSION=1.6.44 JPEGTURBO_VERSION=3.0.4 OPENJPEG_VERSION=2.5.2 @@ -65,21 +61,15 @@ function build_brotli { } function build_harfbuzz { - if [[ "$HARFBUZZ_VERSION" == 8.5.0 ]]; then - export FREETYPE_LIBS=-lfreetype - export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/ - build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no - export FREETYPE_LIBS="" - export FREETYPE_CFLAGS="" - else - local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) - (cd $out_dir \ - && meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled) - (cd $out_dir/build \ - && meson install) - if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp /usr/local/lib64/libharfbuzz* /usr/local/lib - fi + python3 -m pip install meson ninja + + local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) + (cd $out_dir \ + && meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled) + (cd $out_dir/build \ + && meson install) + if [[ "$MB_ML_LIBC" == "manylinux" ]]; then + cp /usr/local/lib64/libharfbuzz* /usr/local/lib fi } @@ -155,11 +145,7 @@ if [[ -n "$IS_MACOS" ]]; then brew remove --ignore-dependencies webp fi - brew install meson pkg-config -elif [[ -n "$IS_ALPINE" ]]; then - apk add meson -elif [[ "$HARFBUZZ_VERSION" != 8.5.0 ]]; then - yum install -y meson + brew install pkg-config fi wrap_wheel_builder build From e976096c2eda0a41c49e0f06e6f84f6b3d0c99b0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Sep 2024 20:39:04 +1000 Subject: [PATCH 59/61] Allow libimagequant shared library path to change --- depends/install_imagequant.sh | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 060d9840ec9..8d62d5ac733 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -23,19 +23,14 @@ else cargo cinstall --prefix=/usr --destdir=. # Copy into place - if [ -d "usr/lib64" ]; then - lib="lib64" - else - lib="lib" - fi - sudo cp usr/$lib/libimagequant.so* /usr/lib/ + sudo find usr -name libimagequant.so* -exec cp {} /usr/lib/ \; sudo cp usr/include/libimagequant.h /usr/include/ if [ -n "$GITHUB_ACTIONS" ]; then # Copy to cache rm -rf ~/cache-$archive_name mkdir ~/cache-$archive_name - cp usr/lib/libimagequant.so* ~/cache-$archive_name/ + find usr -name libimagequant.so* -exec cp {} ~/cache-$archive_name/ \; cp usr/include/libimagequant.h ~/cache-$archive_name/ fi From ed143f5fec816c7164ff2d8a76deef6a8abfaf77 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Sep 2024 06:40:50 +1000 Subject: [PATCH 60/61] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6ff35ea4f98..b4644c541ef 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,15 +5,15 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Improved copying imagequant libraries #8420 + [radarhere] + - Use Capsule for WebP saving #8386 [homm, radarhere] - Fixed writing multiple StripOffsets to TIFF #8317 [Yay295, radarhere] -- Shared imagequant libraries may be located within usr/lib64 #8407 - [radarhere] - - Fix dereference before checking for NULL in ImagingTransformAffine #8398 [PavlNekrasov] From 3a734b5d4b2ee9d602e4d7d3e89530498bac5e68 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 29 Sep 2024 05:19:39 +0000 Subject: [PATCH 61/61] Update scientific-python/upload-nightly-action action to v0.6.0 --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d3c2ac44c80..ee0c331660a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -278,7 +278,7 @@ jobs: path: dist merge-multiple: true - name: Upload wheels to scientific-python-nightly-wheels - uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0 + uses: scientific-python/upload-nightly-action@ccf29c805b5d0c1dc31fa97fcdb962be074cade3 # 0.6.0 with: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}