From d431c97ba3b19d40d4090763ca25499536287141 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Apr 2024 19:28:52 +1000 Subject: [PATCH 01/50] Deprecate BGR;15, BGR;16 and BGR;24 --- Tests/helper.py | 7 ++++++- Tests/test_image.py | 30 +++++++++++++++++++++++++----- Tests/test_image_access.py | 6 +++++- Tests/test_image_putdata.py | 3 ++- Tests/test_lib_pack.py | 13 ++++++++----- docs/deprecations.rst | 7 +++++++ docs/handbook/concepts.rst | 3 --- src/PIL/Image.py | 7 +++++++ src/PIL/ImageMode.py | 4 ++++ 9 files changed, 64 insertions(+), 16 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index c1399e89b..213d99427 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -273,7 +273,12 @@ def _cached_hopper(mode: str) -> Image.Image: im = hopper("L") else: im = hopper() - return im.convert(mode) + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + im = im.convert(mode) + else: + im = im.convert(mode) + return im def djpeg_available() -> bool: diff --git a/Tests/test_image.py b/Tests/test_image.py index 941ec40d9..ed80be503 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -66,7 +66,11 @@ image_mode_names = [name for name, _ in image_modes] class TestImage: @pytest.mark.parametrize("mode", image_mode_names) def test_image_modes_success(self, mode: str) -> None: - Image.new(mode, (1, 1)) + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + Image.new(mode, (1, 1)) + else: + Image.new(mode, (1, 1)) @pytest.mark.parametrize("mode", ("", "bad", "very very long")) def test_image_modes_fail(self, mode: str) -> None: @@ -1050,7 +1054,11 @@ class TestImageBytes: im = hopper(mode) source_bytes = im.tobytes() - reloaded = Image.frombytes(mode, im.size, source_bytes) + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + reloaded = Image.frombytes(mode, im.size, source_bytes) + else: + reloaded = Image.frombytes(mode, im.size, source_bytes) assert reloaded.tobytes() == source_bytes @pytest.mark.parametrize("mode", image_mode_names) @@ -1058,17 +1066,29 @@ class TestImageBytes: im = hopper(mode) source_bytes = im.tobytes() - reloaded = Image.new(mode, im.size) + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + reloaded = Image.new(mode, im.size) + else: + reloaded = Image.new(mode, im.size) reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes @pytest.mark.parametrize(("mode", "pixelsize"), image_modes) def test_getdata_putdata(self, mode: str, pixelsize: int) -> None: - im = Image.new(mode, (2, 2)) + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + im = Image.new(mode, (2, 2)) + else: + im = Image.new(mode, (2, 2)) source_bytes = bytes(range(im.width * im.height * pixelsize)) im.frombytes(source_bytes) - reloaded = Image.new(mode, im.size) + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + reloaded = Image.new(mode, im.size) + else: + reloaded = Image.new(mode, im.size) reloaded.putdata(im.getdata()) assert_image_equal(im, reloaded) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 8c42da57a..8bb90710a 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -229,7 +229,11 @@ class TestImageGetPixel(AccessTest): ), ) def test_basic(self, mode: str) -> None: - self.check(mode) + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + self.check(mode) + else: + self.check(mode) def test_list(self) -> None: im = hopper() diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 73145faac..dad26ef14 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -81,7 +81,8 @@ def test_mode_F() -> None: @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) def test_mode_BGR(mode: str) -> None: data = [(16, 32, 49), (32, 32, 98)] - im = Image.new(mode, (1, 2)) + with pytest.warns(DeprecationWarning): + im = Image.new(mode, (1, 2)) im.putdata(data) assert list(im.getdata()) == data diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 6a0e704b8..f80c5b78c 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -359,11 +359,14 @@ class TestLibUnpack: ) def test_BGR(self) -> None: - self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8)) - self.assert_unpack( - "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) - ) - self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + with pytest.warns(DeprecationWarning): + self.assert_unpack( + "BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8) + ) + self.assert_unpack( + "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) + ) + self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) def test_RGBA(self) -> None: self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index c3d1ba4f0..da4e9e597 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -100,6 +100,13 @@ ImageMath eval() ``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or :py:meth:`~PIL.ImageMath.unsafe_eval` instead. +BGR;15, BGR 16 and BGR;24 +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 + +The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated. + Removed features ---------------- diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index e0975a121..5094dbf3f 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -59,9 +59,6 @@ Pillow also provides limited support for a few additional modes, including: * ``I;16L`` (16-bit little endian unsigned integer pixels) * ``I;16B`` (16-bit big endian unsigned integer pixels) * ``I;16N`` (16-bit native endian unsigned integer pixels) - * ``BGR;15`` (15-bit reversed true colour) - * ``BGR;16`` (16-bit reversed true colour) - * ``BGR;24`` (24-bit reversed true colour) Premultiplied alpha is where the values for each other channel have been multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)`` diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3ae901060..26be42779 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -55,6 +55,7 @@ from . import ( _plugins, ) from ._binary import i32le, o32be, o32le +from ._deprecate import deprecate from ._typing import StrOrBytesPath, TypeGuard from ._util import DeferredError, is_path @@ -939,6 +940,9 @@ class Image: :returns: An :py:class:`~PIL.Image.Image` object. """ + if mode in ("BGR;15", "BGR;16", "BGR;24"): + deprecate(mode, 12) + self.load() has_transparency = "transparency" in self.info @@ -2956,6 +2960,9 @@ def new( :returns: An :py:class:`~PIL.Image.Image` object. """ + if mode in ("BGR;15", "BGR;16", "BGR;24"): + deprecate(mode, 12) + _check_size(size) if color is None: diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 5e05c5f43..7bd2afcf2 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -18,6 +18,8 @@ import sys from functools import lru_cache from typing import NamedTuple +from ._deprecate import deprecate + class ModeDescriptor(NamedTuple): """Wrapper for mode strings.""" @@ -63,6 +65,8 @@ def getmode(mode: str) -> ModeDescriptor: "PA": ("RGB", "L", ("P", "A"), "|u1"), } if mode in modes: + if mode in ("BGR;15", "BGR;16", "BGR;24"): + deprecate(mode, 12) base_mode, base_type, bands, type_str = modes[mode] return ModeDescriptor(mode, bands, base_mode, base_type, type_str) From 139245a3db00cf4cc6de4ed726eba4561dcd5cec Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 27 Mar 2024 10:29:19 -0500 Subject: [PATCH 02/50] use namedtuple for image mode info --- Tests/test_image.py | 67 ++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 941ec40d9..779785c53 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -8,7 +8,7 @@ import sys import tempfile import warnings from pathlib import Path -from typing import IO +from typing import IO, NamedTuple import pytest @@ -33,34 +33,39 @@ from .helper import ( skip_unless_feature, ) -# name, pixel size + +class ImageModeInfo(NamedTuple): + name: str + pixel_size: int + + image_modes = ( - ("1", 1), - ("L", 1), - ("LA", 4), - ("La", 4), - ("P", 1), - ("PA", 4), - ("F", 4), - ("I", 4), - ("I;16", 2), - ("I;16L", 2), - ("I;16B", 2), - ("I;16N", 2), - ("RGB", 4), - ("RGBA", 4), - ("RGBa", 4), - ("RGBX", 4), - ("BGR;15", 2), - ("BGR;16", 2), - ("BGR;24", 3), - ("CMYK", 4), - ("YCbCr", 4), - ("HSV", 4), - ("LAB", 4), + ImageModeInfo("1", 1), + ImageModeInfo("L", 1), + ImageModeInfo("LA", 4), + ImageModeInfo("La", 4), + ImageModeInfo("P", 1), + ImageModeInfo("PA", 4), + ImageModeInfo("F", 4), + ImageModeInfo("I", 4), + ImageModeInfo("I;16", 2), + ImageModeInfo("I;16L", 2), + ImageModeInfo("I;16B", 2), + ImageModeInfo("I;16N", 2), + ImageModeInfo("RGB", 4), + ImageModeInfo("RGBA", 4), + ImageModeInfo("RGBa", 4), + ImageModeInfo("RGBX", 4), + ImageModeInfo("BGR;15", 2), + ImageModeInfo("BGR;16", 2), + ImageModeInfo("BGR;24", 3), + ImageModeInfo("CMYK", 4), + ImageModeInfo("YCbCr", 4), + ImageModeInfo("HSV", 4), + ImageModeInfo("LAB", 4), ) -image_mode_names = [name for name, _ in image_modes] +image_mode_names = [mode.name for mode in image_modes] class TestImage: @@ -1062,13 +1067,13 @@ class TestImageBytes: reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize(("mode", "pixelsize"), image_modes) - def test_getdata_putdata(self, mode: str, pixelsize: int) -> None: - im = Image.new(mode, (2, 2)) - source_bytes = bytes(range(im.width * im.height * pixelsize)) + @pytest.mark.parametrize("mode", image_modes) + def test_getdata_putdata(self, mode: ImageModeInfo) -> None: + im = Image.new(mode.name, (2, 2)) + source_bytes = bytes(range(im.width * im.height * mode.pixel_size)) im.frombytes(source_bytes) - reloaded = Image.new(mode, im.size) + reloaded = Image.new(mode.name, im.size) reloaded.putdata(im.getdata()) assert_image_equal(im, reloaded) From 5a4b771fb00389a43dbf32a4c13d73c9b39e3f5d Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 2 Jan 2023 16:55:31 -0600 Subject: [PATCH 03/50] move image mode info variables to helper.py --- Tests/helper.py | 36 +++++++++++++++++++++++++++++++++++- Tests/test_image.py | 39 ++++----------------------------------- 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index c1399e89b..32ea99fdd 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -13,7 +13,7 @@ import sysconfig import tempfile from functools import lru_cache from io import BytesIO -from typing import Any, Callable, Sequence +from typing import Any, Callable, NamedTuple, Sequence import pytest from packaging.version import parse as parse_version @@ -29,6 +29,40 @@ elif "GITHUB_ACTIONS" in os.environ: uploader = "github_actions" +class ImageModeInfo(NamedTuple): + name: str + pixel_size: int + + +image_modes = ( + ImageModeInfo("1", 1), + ImageModeInfo("L", 1), + ImageModeInfo("LA", 4), + ImageModeInfo("La", 4), + ImageModeInfo("P", 1), + ImageModeInfo("PA", 4), + ImageModeInfo("F", 4), + ImageModeInfo("I", 4), + ImageModeInfo("I;16", 2), + ImageModeInfo("I;16L", 2), + ImageModeInfo("I;16B", 2), + ImageModeInfo("I;16N", 2), + ImageModeInfo("RGB", 4), + ImageModeInfo("RGBA", 4), + ImageModeInfo("RGBa", 4), + ImageModeInfo("RGBX", 4), + ImageModeInfo("BGR;15", 2), + ImageModeInfo("BGR;16", 2), + ImageModeInfo("BGR;24", 3), + ImageModeInfo("CMYK", 4), + ImageModeInfo("YCbCr", 4), + ImageModeInfo("HSV", 4), + ImageModeInfo("LAB", 4), +) + +image_mode_names = [mode.name for mode in image_modes] + + def upload(a: Image.Image, b: Image.Image) -> str | None: if uploader == "show": # local img.show for errors. diff --git a/Tests/test_image.py b/Tests/test_image.py index 779785c53..5654307f1 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -8,7 +8,7 @@ import sys import tempfile import warnings from pathlib import Path -from typing import IO, NamedTuple +from typing import IO import pytest @@ -23,51 +23,20 @@ from PIL import ( ) from .helper import ( + ImageModeInfo, assert_image_equal, assert_image_equal_tofile, assert_image_similar_tofile, assert_not_all_same, hopper, + image_mode_names, + image_modes, is_win32, mark_if_feature_version, skip_unless_feature, ) -class ImageModeInfo(NamedTuple): - name: str - pixel_size: int - - -image_modes = ( - ImageModeInfo("1", 1), - ImageModeInfo("L", 1), - ImageModeInfo("LA", 4), - ImageModeInfo("La", 4), - ImageModeInfo("P", 1), - ImageModeInfo("PA", 4), - ImageModeInfo("F", 4), - ImageModeInfo("I", 4), - ImageModeInfo("I;16", 2), - ImageModeInfo("I;16L", 2), - ImageModeInfo("I;16B", 2), - ImageModeInfo("I;16N", 2), - ImageModeInfo("RGB", 4), - ImageModeInfo("RGBA", 4), - ImageModeInfo("RGBa", 4), - ImageModeInfo("RGBX", 4), - ImageModeInfo("BGR;15", 2), - ImageModeInfo("BGR;16", 2), - ImageModeInfo("BGR;24", 3), - ImageModeInfo("CMYK", 4), - ImageModeInfo("YCbCr", 4), - ImageModeInfo("HSV", 4), - ImageModeInfo("LAB", 4), -) - -image_mode_names = [mode.name for mode in image_modes] - - class TestImage: @pytest.mark.parametrize("mode", image_mode_names) def test_image_modes_success(self, mode: str) -> None: From 0fed6a5fbcbe40aff7540693a438d18d825c839e Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 31 Mar 2024 23:29:01 -0500 Subject: [PATCH 04/50] use common image mode list for TestImageGetPixel tests --- Tests/test_image_access.py | 38 ++++++++------------------------------ 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 8c42da57a..96afc87a4 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -10,7 +10,7 @@ import pytest from PIL import Image -from .helper import assert_image_equal, hopper, is_win32 +from .helper import assert_image_equal, hopper, image_mode_names, is_win32 # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 @@ -138,8 +138,8 @@ class TestImageGetPixel(AccessTest): if bands == 1: return 1 if mode in ("BGR;15", "BGR;16"): - # These modes have less than 8 bits per band - # So (1, 2, 3) cannot be roundtripped + # These modes have less than 8 bits per band, + # so (1, 2, 3) cannot be roundtripped. return (16, 32, 49) return tuple(range(1, bands + 1)) @@ -168,16 +168,15 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # Check 0 + # check 0x0 image with None initial color im = Image.new(mode, (0, 0), None) assert im.load() is not None - error = ValueError if self._need_cffi_access else IndexError with pytest.raises(error): im.putpixel((0, 0), expected_color) with pytest.raises(error): im.getpixel((0, 0)) - # Check 0 negative index + # check negative index with pytest.raises(error): im.putpixel((-1, -1), expected_color) with pytest.raises(error): @@ -198,36 +197,15 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # Check 0 + # check 0x0 image with initial color im = Image.new(mode, (0, 0), expected_color) with pytest.raises(error): im.getpixel((0, 0)) - # Check 0 negative index + # check negative index with pytest.raises(error): im.getpixel((-1, -1)) - @pytest.mark.parametrize( - "mode", - ( - "1", - "L", - "LA", - "I", - "I;16", - "I;16B", - "F", - "P", - "PA", - "BGR;15", - "BGR;16", - "BGR;24", - "RGB", - "RGBA", - "RGBX", - "CMYK", - "YCbCr", - ), - ) + @pytest.mark.parametrize("mode", image_mode_names) def test_basic(self, mode: str) -> None: self.check(mode) From da7198c98740e9607dac0bf97ae073cf21510634 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 30 Mar 2024 14:32:45 -0500 Subject: [PATCH 05/50] fix ImagingAccess for I;16N on big-endian --- src/libImaging/Access.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 091c84e18..04618df09 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -81,12 +81,6 @@ get_pixel_16B(Imaging im, int x, int y, void *color) { #endif } -static void -get_pixel_16(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image[y][x + x]; - memcpy(color, in, sizeof(UINT16)); -} - static void get_pixel_BGR15(Imaging im, int x, int y, void *color) { UINT8 *in = (UINT8 *)&im->image8[y][x * 2]; @@ -207,7 +201,11 @@ ImagingAccessInit() { ADD("I;16", get_pixel_16L, put_pixel_16L); ADD("I;16L", get_pixel_16L, put_pixel_16L); ADD("I;16B", get_pixel_16B, put_pixel_16B); - ADD("I;16N", get_pixel_16, put_pixel_16L); +#ifdef WORDS_BIGENDIAN + ADD("I;16N", get_pixel_16B, put_pixel_16B); +#else + ADD("I;16N", get_pixel_16L, put_pixel_16L); +#endif ADD("I;32L", get_pixel_32L, put_pixel_32L); ADD("I;32B", get_pixel_32B, put_pixel_32B); ADD("F", get_pixel_32, put_pixel_32); From 5dabc6cf14c78d397d2b31d81d6a7fe9e10dbd3b Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 30 Mar 2024 15:10:04 -0500 Subject: [PATCH 06/50] fix I;16N lib pack test --- Tests/test_lib_pack.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 6a0e704b8..f34ff7d02 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -216,7 +216,10 @@ class TestLibPack: ) def test_I16(self) -> None: - self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) + if sys.byteorder == "little": + self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) + else: + self.assert_pack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506) def test_F_float(self) -> None: self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) From fe79ae5653e42e5b5c42ff5428eeef36e4bbe7a6 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 6 Apr 2024 10:48:38 -0500 Subject: [PATCH 07/50] get pixel size by counting bytes in 1x1 image --- Tests/helper.py | 57 ++++++++++++++++++++------------------------- Tests/test_image.py | 19 +++++++++------ 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 32ea99fdd..f0bb8af00 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -13,7 +13,7 @@ import sysconfig import tempfile from functools import lru_cache from io import BytesIO -from typing import Any, Callable, NamedTuple, Sequence +from typing import Any, Callable, Sequence import pytest from packaging.version import parse as parse_version @@ -29,39 +29,32 @@ elif "GITHUB_ACTIONS" in os.environ: uploader = "github_actions" -class ImageModeInfo(NamedTuple): - name: str - pixel_size: int - - -image_modes = ( - ImageModeInfo("1", 1), - ImageModeInfo("L", 1), - ImageModeInfo("LA", 4), - ImageModeInfo("La", 4), - ImageModeInfo("P", 1), - ImageModeInfo("PA", 4), - ImageModeInfo("F", 4), - ImageModeInfo("I", 4), - ImageModeInfo("I;16", 2), - ImageModeInfo("I;16L", 2), - ImageModeInfo("I;16B", 2), - ImageModeInfo("I;16N", 2), - ImageModeInfo("RGB", 4), - ImageModeInfo("RGBA", 4), - ImageModeInfo("RGBa", 4), - ImageModeInfo("RGBX", 4), - ImageModeInfo("BGR;15", 2), - ImageModeInfo("BGR;16", 2), - ImageModeInfo("BGR;24", 3), - ImageModeInfo("CMYK", 4), - ImageModeInfo("YCbCr", 4), - ImageModeInfo("HSV", 4), - ImageModeInfo("LAB", 4), +image_mode_names = ( + "1", + "L", + "LA", + "La", + "P", + "PA", + "F", + "I", + "I;16", + "I;16L", + "I;16B", + "I;16N", + "RGB", + "RGBA", + "RGBa", + "RGBX", + "BGR;15", + "BGR;16", + "BGR;24", + "CMYK", + "YCbCr", + "HSV", + "LAB", ) -image_mode_names = [mode.name for mode in image_modes] - def upload(a: Image.Image, b: Image.Image) -> str | None: if uploader == "show": diff --git a/Tests/test_image.py b/Tests/test_image.py index 5654307f1..8b20de0a9 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -23,14 +23,12 @@ from PIL import ( ) from .helper import ( - ImageModeInfo, assert_image_equal, assert_image_equal_tofile, assert_image_similar_tofile, assert_not_all_same, hopper, image_mode_names, - image_modes, is_win32, mark_if_feature_version, skip_unless_feature, @@ -1036,13 +1034,20 @@ class TestImageBytes: reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", image_modes) - def test_getdata_putdata(self, mode: ImageModeInfo) -> None: - im = Image.new(mode.name, (2, 2)) - source_bytes = bytes(range(im.width * im.height * mode.pixel_size)) + @pytest.mark.parametrize("mode", image_mode_names) + def test_getdata_putdata(self, mode: str) -> None: + # create an image with 1 pixel to get its pixel size + im = Image.new(mode, (1, 1)) + pixel_size = len(im.tobytes()) + + # create a new image with incrementing byte values + im = Image.new(mode, (2, 2)) + source_bytes = bytes(range(im.width * im.height * pixel_size)) im.frombytes(source_bytes) - reloaded = Image.new(mode.name, im.size) + # copy the data from the previous image to a new image + # and check that they are the same + reloaded = Image.new(mode, im.size) reloaded.putdata(im.getdata()) assert_image_equal(im, reloaded) From 5573ec74901945c58e700b2becb34ac800607bbe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Apr 2024 22:54:47 +1000 Subject: [PATCH 08/50] use hopper() for test_getdata_putdata() --- Tests/test_image.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 8b20de0a9..090b80f87 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1036,17 +1036,7 @@ class TestImageBytes: @pytest.mark.parametrize("mode", image_mode_names) def test_getdata_putdata(self, mode: str) -> None: - # create an image with 1 pixel to get its pixel size - im = Image.new(mode, (1, 1)) - pixel_size = len(im.tobytes()) - - # create a new image with incrementing byte values - im = Image.new(mode, (2, 2)) - source_bytes = bytes(range(im.width * im.height * pixel_size)) - im.frombytes(source_bytes) - - # copy the data from the previous image to a new image - # and check that they are the same + im = hopper(mode) reloaded = Image.new(mode, im.size) reloaded.putdata(im.getdata()) assert_image_equal(im, reloaded) From 5c960d6abc25aa94044a0831743e061383d3f486 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Apr 2024 23:01:51 +1000 Subject: [PATCH 09/50] rename "image_mode_names" to "modes" --- Tests/helper.py | 2 +- Tests/test_image.py | 10 +++++----- Tests/test_image_access.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index f0bb8af00..5d206f644 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -29,7 +29,7 @@ elif "GITHUB_ACTIONS" in os.environ: uploader = "github_actions" -image_mode_names = ( +modes = ( "1", "L", "LA", diff --git a/Tests/test_image.py b/Tests/test_image.py index 090b80f87..9985f2346 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -28,15 +28,15 @@ from .helper import ( assert_image_similar_tofile, assert_not_all_same, hopper, - image_mode_names, is_win32, mark_if_feature_version, + modes, skip_unless_feature, ) class TestImage: - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_image_modes_success(self, mode: str) -> None: Image.new(mode, (1, 1)) @@ -1017,7 +1017,7 @@ class TestImage: class TestImageBytes: - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_roundtrip_bytes_constructor(self, mode: str) -> None: im = hopper(mode) source_bytes = im.tobytes() @@ -1025,7 +1025,7 @@ class TestImageBytes: reloaded = Image.frombytes(mode, im.size, source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_roundtrip_bytes_method(self, mode: str) -> None: im = hopper(mode) source_bytes = im.tobytes() @@ -1034,7 +1034,7 @@ class TestImageBytes: reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_getdata_putdata(self, mode: str) -> None: im = hopper(mode) reloaded = Image.new(mode, im.size) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 96afc87a4..50afb2a23 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -10,7 +10,7 @@ import pytest from PIL import Image -from .helper import assert_image_equal, hopper, image_mode_names, is_win32 +from .helper import assert_image_equal, hopper, is_win32, modes # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 @@ -205,7 +205,7 @@ class TestImageGetPixel(AccessTest): with pytest.raises(error): im.getpixel((-1, -1)) - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_basic(self, mode: str) -> None: self.check(mode) From 98510570e6a54398c7975f1128b3de91fd8aad1f Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 20 Apr 2024 09:19:20 -0500 Subject: [PATCH 10/50] ignore BGR;15/16 test failure on big-endian --- Tests/test_image.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/test_image.py b/Tests/test_image.py index 9985f2346..4a056df40 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -28,6 +28,7 @@ from .helper import ( assert_image_similar_tofile, assert_not_all_same, hopper, + is_big_endian, is_win32, mark_if_feature_version, modes, @@ -1036,6 +1037,8 @@ class TestImageBytes: @pytest.mark.parametrize("mode", modes) def test_getdata_putdata(self, mode: str) -> None: + if is_big_endian and mode in ("BGR;15", "BGR;16"): + pytest.xfail(f"Known failure of {mode} on big-endian") im = hopper(mode) reloaded = Image.new(mode, im.size) reloaded.putdata(im.getdata()) From bb2411dd01197d0a393dbcfdccfdd487a8c6d7be Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Apr 2024 08:11:45 +1000 Subject: [PATCH 11/50] Support reading P mode TIFF images with padding --- src/PIL/TiffImagePlugin.py | 1 + src/libImaging/Unpack.c | 1 + 2 files changed, 2 insertions(+) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 10ac9ea3a..106f09c2d 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -244,6 +244,7 @@ OPEN_INFO = { (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"), (II, 3, (1,), 1, (8,), ()): ("P", "P"), (MM, 3, (1,), 1, (8,), ()): ("P", "P"), + (II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"), (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index a84dc0a6f..e351aa2f1 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1582,6 +1582,7 @@ static struct { {"P", "P", 8, copy1}, {"P", "P;R", 8, unpackLR}, {"P", "L", 8, copy1}, + {"P", "PX", 16, unpackL16B}, /* palette w. alpha */ {"PA", "PA", 16, unpackLA}, From d5c1ff4b43b0b17d2379939cf941b06e3b0c3464 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Apr 2024 22:22:25 +1000 Subject: [PATCH 12/50] Removed type hint ignores --- src/PIL/ImageCms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 4af1b79e2..5f5c5df54 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -838,8 +838,8 @@ def getProfileName(profile: _CmsProfileCompatible) -> str: if not (model or manufacturer): return (profile.profile.profile_description or "") + "\n" - if not manufacturer or len(model) > 30: # type: ignore[arg-type] - return model + "\n" # type: ignore[operator] + if not manufacturer or (model and len(model) > 30): + return f"{model}\n" return f"{model} - {manufacturer}\n" except (AttributeError, OSError, TypeError, ValueError) as v: From 745eb23a87b40707af9474ad732fc6d13b94628f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Apr 2024 22:22:40 +1000 Subject: [PATCH 13/50] Use LAB hopper file if conversion is not supported --- Tests/helper.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/helper.py b/Tests/helper.py index c1399e89b..680825d4b 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -273,7 +273,14 @@ def _cached_hopper(mode: str) -> Image.Image: im = hopper("L") else: im = hopper() - return im.convert(mode) + try: + im = im.convert(mode) + except ImportError: + if mode == "LAB": + im = Image.open("Tests/images/hopper.Lab.tif") + else: + raise + return im def djpeg_available() -> bool: From 023d017da00f8adb8de86719509145c405369ac8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Apr 2024 18:26:20 +1000 Subject: [PATCH 14/50] Deprecate libtiff < 4 --- docs/deprecations.rst | 8 ++++++++ src/PIL/TiffImagePlugin.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index c3d1ba4f0..91bc150b3 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -100,6 +100,14 @@ ImageMath eval() ``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or :py:meth:`~PIL.ImageMath.unsafe_eval` instead. +Support for libtiff earlier than 4 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 + +Support for libtiff earlier than 4 has been deprecated. Upgrade to a newer version of +libtiff instead. + Removed features ---------------- diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 10ac9ea3a..13bcf5d86 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -56,6 +56,7 @@ from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 +from ._deprecate import deprecate from .TiffTags import TYPES logger = logging.getLogger(__name__) @@ -276,6 +277,9 @@ PREFIXES = [ b"II\x2B\x00", # BigTIFF with little-endian byte order ] +if not getattr(Image.core, "libtiff_support_custom_tags", True): + deprecate("Support for libtiff earlier than 4", 12) + def _accept(prefix: bytes) -> bool: return prefix[:4] in PREFIXES From c7bb152ed94002a83d648d2a1b9703c2bf27e8e6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Apr 2024 18:30:00 +1000 Subject: [PATCH 15/50] support_custom_tags attribute is not present if libtiff is not supported --- Tests/test_file_libtiff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 71f1b6f1d..e1867cffb 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -741,7 +741,7 @@ class TestFileLibTiff(LibTiffTestCase): pytest.param( True, marks=pytest.mark.skipif( - not Image.core.libtiff_support_custom_tags, + not getattr(Image.core, "libtiff_support_custom_tags", False), reason="Custom tags not supported by older libtiff", ), ), From 0df8796e1910619741a36bb1a2d334bdb941e440 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Apr 2024 18:45:41 +1000 Subject: [PATCH 16/50] Parametrized test --- Tests/test_file_libtiff.py | 93 ++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index e1867cffb..11883ad24 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -242,7 +242,24 @@ class TestFileLibTiff(LibTiffTestCase): im.save(out, tiffinfo=new_ifd) - def test_custom_metadata(self, tmp_path: Path) -> None: + @pytest.mark.parametrize( + "libtiff", + ( + pytest.param( + True, + marks=pytest.mark.skipif( + not getattr(Image.core, "libtiff_support_custom_tags", False), + reason="Custom tags not supported by older libtiff", + ), + ), + False, + ), + ) + def test_custom_metadata( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool + ) -> None: + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff) + class Tc(NamedTuple): value: Any type: int @@ -281,53 +298,43 @@ class TestFileLibTiff(LibTiffTestCase): ) } - libtiffs = [False] - if Image.core.libtiff_support_custom_tags: - libtiffs.append(True) + def check_tags( + tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] + ) -> None: + im = hopper() - for libtiff in libtiffs: - TiffImagePlugin.WRITE_LIBTIFF = libtiff + out = str(tmp_path / "temp.tif") + im.save(out, tiffinfo=tiffinfo) - def check_tags( - tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] - ) -> None: - im = hopper() + with Image.open(out) as reloaded: + for tag, value in tiffinfo.items(): + reloaded_value = reloaded.tag_v2[tag] + if ( + isinstance(reloaded_value, TiffImagePlugin.IFDRational) + and libtiff + ): + # libtiff does not support real RATIONALS + assert round(abs(float(reloaded_value) - float(value)), 7) == 0 + continue - out = str(tmp_path / "temp.tif") - im.save(out, tiffinfo=tiffinfo) + assert reloaded_value == value - with Image.open(out) as reloaded: - for tag, value in tiffinfo.items(): - reloaded_value = reloaded.tag_v2[tag] - if ( - isinstance(reloaded_value, TiffImagePlugin.IFDRational) - and libtiff - ): - # libtiff does not support real RATIONALS - assert ( - round(abs(float(reloaded_value) - float(value)), 7) == 0 - ) - continue + # Test with types + ifd = TiffImagePlugin.ImageFileDirectory_v2() + for tag, tagdata in custom.items(): + ifd[tag] = tagdata.value + ifd.tagtype[tag] = tagdata.type + check_tags(ifd) - assert reloaded_value == value - - # Test with types - ifd = TiffImagePlugin.ImageFileDirectory_v2() - for tag, tagdata in custom.items(): - ifd[tag] = tagdata.value - ifd.tagtype[tag] = tagdata.type - check_tags(ifd) - - # Test without types. This only works for some types, int for example are - # always encoded as LONG and not SIGNED_LONG. - check_tags( - { - tag: tagdata.value - for tag, tagdata in custom.items() - if tagdata.supported_by_default - } - ) - TiffImagePlugin.WRITE_LIBTIFF = False + # Test without types. This only works for some types, int for example are + # always encoded as LONG and not SIGNED_LONG. + check_tags( + { + tag: tagdata.value + for tag, tagdata in custom.items() + if tagdata.supported_by_default + } + ) def test_osubfiletype(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") From e144e418791eb205765b74da793487a038675984 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 22 Apr 2024 19:14:23 +1000 Subject: [PATCH 17/50] Updated wording Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/deprecations.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 91bc150b3..7882ec5ee 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -100,13 +100,13 @@ ImageMath eval() ``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or :py:meth:`~PIL.ImageMath.unsafe_eval` instead. -Support for libtiff earlier than 4 +Support for LibTIFF earlier than 4 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 10.4.0 -Support for libtiff earlier than 4 has been deprecated. Upgrade to a newer version of -libtiff instead. +Support for LibTIFF earlier than version 4 has been deprecated. +Upgrade to a newer version of LibTIFF instead. Removed features ---------------- From 2e1d2b2029eefe7255c1c11f81a53f862a7861e1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Apr 2024 19:15:38 +1000 Subject: [PATCH 18/50] Updated deprecation message --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 13bcf5d86..82ac47647 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -278,7 +278,7 @@ PREFIXES = [ ] if not getattr(Image.core, "libtiff_support_custom_tags", True): - deprecate("Support for libtiff earlier than 4", 12) + deprecate("Support for LibTIFF earlier than version 4", 12) def _accept(prefix: bytes) -> bool: From 5a0a288dd048097d2ffa62fb2d5a24cf5727bbb9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Apr 2024 19:16:55 +1000 Subject: [PATCH 19/50] Added release notes --- docs/releasenotes/10.4.0.rst | 54 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 55 insertions(+) create mode 100644 docs/releasenotes/10.4.0.rst diff --git a/docs/releasenotes/10.4.0.rst b/docs/releasenotes/10.4.0.rst new file mode 100644 index 000000000..0c2926732 --- /dev/null +++ b/docs/releasenotes/10.4.0.rst @@ -0,0 +1,54 @@ +10.4.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +Support for LibTIFF earlier than 4 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support for LibTIFF earlier than version 4 has been deprecated. +Upgrade to a newer version of LibTIFF instead. + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 089d44b90..6ee5fb6c8 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 10.4.0 10.3.0 10.2.0 10.1.0 From d4a4b59ee39dcf2e8dddafebfecccaaf709ec8bb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:43:48 +0300 Subject: [PATCH 20/50] Sphinx extension to add dates to release notes Co-authored-by: Jason R. Coombs --- docs/conf.py | 1 + docs/dater.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 docs/dater.py diff --git a/docs/conf.py b/docs/conf.py index 392cf317e..f12b30e65 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,6 +28,7 @@ needs_sphinx = "7.3" # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "dater", "sphinx.ext.autodoc", "sphinx.ext.extlinks", "sphinx.ext.intersphinx", diff --git a/docs/dater.py b/docs/dater.py new file mode 100644 index 000000000..d9e583547 --- /dev/null +++ b/docs/dater.py @@ -0,0 +1,52 @@ +""" +Sphinx extension to add timestamps to release notes based on Git versions. + +Based on https://github.com/jaraco/rst.linker, with thanks to Jason R. Coombs. +""" + +from __future__ import annotations + +import datetime as dt +import os +import re +import subprocess +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from sphinx.application import Sphinx + +DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+") +VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n") + + +def get_date_for(git_version: str) -> dt.datetime | None: + cmd = ["git", "log", "-1", "--format=%ai", git_version] + try: + with open(os.devnull, "w", encoding="utf-8") as devnull: + out = subprocess.check_output( + cmd, stderr=devnull, text=True, encoding="utf-8" + ) + ts = out.strip() + return dt.datetime.fromisoformat(ts) + except subprocess.CalledProcessError: + return None + + +def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None: + if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])): + old_title = m.group(1) + + if tag_datetime := get_date_for(old_title): + new_title = f"{old_title} ({tag_datetime:%Y-%m-%d})" + else: + new_title = f"{old_title} (unreleased)" + + new_underline = "-" * len(new_title) + + result = source[0].replace(m.group(0), f"{new_title}\n{new_underline}\n", 1) + source[0] = result + + +def setup(app: Sphinx) -> dict[str, bool]: + app.connect("source-read", add_date) + return {"parallel_read_safe": True} From 35003343386f3d2d40f179e0c18f96e0b58ce102 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:58:44 +0300 Subject: [PATCH 21/50] Fetch tags on Read the Docs --- .readthedocs.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 0c8f935d5..b83ba05b1 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -6,6 +6,10 @@ build: os: ubuntu-22.04 tools: python: "3" + jobs: + post_checkout: + - git remote add upstream https://github.com/python-pillow/Pillow.git # For forks + - git fetch upstream --tags python: install: From 7f6ad116d1213948b318423cfd58990ec1e85c07 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Apr 2024 08:02:42 +1000 Subject: [PATCH 22/50] Update CHANGES.rst [ci skip] --- CHANGES.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 196f8ed20..85dc0b43c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,18 @@ Changelog (Pillow) 10.4.0 (unreleased) ------------------- +- Support reading P mode TIFF images with padding #7996 + [radarhere] + +- Deprecate support for libtiff < 4 #7998 + [radarhere, hugovk] + +- Corrected ImageShow UnixViewer command #7987 + [radarhere] + +- Use functools.cached_property in ImageStat #7952 + [nulano, hugovk, radarhere] + - Add support for reading BITMAPV2INFOHEADER and BITMAPV3INFOHEADER #7956 [Cirras, radarhere] From 4a4eb0f3eeb76f59c9c890bf74c91a910e98a3eb Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 23 Apr 2024 01:08:42 -0500 Subject: [PATCH 23/50] remove semicolon after function definition --- src/_imaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index 520e50793..9b521f552 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3737,7 +3737,7 @@ _getattr_unsafe_ptrs(ImagingObject *self, void *closure) { self->image->image32, "image", self->image->image); -}; +} static struct PyGetSetDef getsetters[] = { {"mode", (getter)_getattr_mode}, From b9307f08d14e499e75edf574ff15c9b5c1f62a7d Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 23 Apr 2024 12:02:25 -0500 Subject: [PATCH 24/50] remove unused variable --- src/display.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/display.c b/src/display.c index ef2ff3754..6b66ddafb 100644 --- a/src/display.c +++ b/src/display.c @@ -427,7 +427,6 @@ error: PyObject * PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) { - int clip; HANDLE handle = NULL; int size; void *data; From eee53ba6647b75bbecc56e98a6a99d7c2360d1ca Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 23 Apr 2024 13:06:22 -0500 Subject: [PATCH 25/50] extract band count check --- src/libImaging/Matrix.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c index 182eb62a7..ec7f4d93e 100644 --- a/src/libImaging/Matrix.c +++ b/src/libImaging/Matrix.c @@ -24,11 +24,11 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { ImagingSectionCookie cookie; /* Assume there's enough data in the buffer */ - if (!im) { + if (!im || im->bands != 3) { return (Imaging)ImagingError_ModeError(); } - if (strcmp(mode, "L") == 0 && im->bands == 3) { + if (strcmp(mode, "L") == 0) { imOut = ImagingNewDirty("L", im->xsize, im->ysize); if (!imOut) { return NULL; @@ -47,7 +47,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { } ImagingSectionLeave(&cookie); - } else if (strlen(mode) == 3 && im->bands == 3) { + } else if (strlen(mode) == 3) { imOut = ImagingNewDirty(mode, im->xsize, im->ysize); if (!imOut) { return NULL; From 46b85e6ab4eb1a5f1781acca2fb085826b328c3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Apr 2024 11:02:56 +1000 Subject: [PATCH 26/50] Simplified code --- src/libImaging/Convert.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 7e60a960c..64840d08c 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -254,9 +254,8 @@ static void rgb2i16l(UINT8 *out_, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++, in += 4) { - UINT8 v = CLIP16(L24(in) >> 16); - *out_++ = v; - *out_++ = v >> 8; + *out_++ = L24(in) >> 16; + *out_++ = 0; } } @@ -264,9 +263,8 @@ static void rgb2i16b(UINT8 *out_, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++, in += 4) { - UINT8 v = CLIP16(L24(in) >> 16); - *out_++ = v >> 8; - *out_++ = v; + *out_++ = 0; + *out_++ = L24(in) >> 16; } } From 03627d92a739d31269de0af61714b45db04069de Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:23:44 +0300 Subject: [PATCH 27/50] GitHub Actions: Python 3.8 and 3.9 are on macos-13 but not macos-14 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 643273e58..4573fde90 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,9 +57,9 @@ jobs: - python-version: "3.10" PYTHONOPTIMIZE: 2 # M1 only available for 3.10+ - - os: "macos-latest" + - os: "macos-13" python-version: "3.9" - - os: "macos-latest" + - os: "macos-13" python-version: "3.8" exclude: - os: "macos-14" From 76c17a10f0563f2343c7dada04af217093d3ef67 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:24:23 +0300 Subject: [PATCH 28/50] GitHub Actions: macos-13 is Intel but macos-latest will be M1 --- .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 36bb54050..b2fbd3140 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -97,7 +97,7 @@ jobs: matrix: include: - name: "macOS x86_64" - os: macos-latest + os: macos-13 cibw_arch: x86_64 macosx_deployment_target: "10.10" - name: "macOS arm64" From ccf1efb3efb371818455289b8c04aa21f18d4c4d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Apr 2024 23:06:06 +1000 Subject: [PATCH 29/50] Use subprocess.DEVNULL --- docs/dater.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/dater.py b/docs/dater.py index d9e583547..04855956b 100644 --- a/docs/dater.py +++ b/docs/dater.py @@ -7,7 +7,6 @@ Based on https://github.com/jaraco/rst.linker, with thanks to Jason R. Coombs. from __future__ import annotations import datetime as dt -import os import re import subprocess from typing import TYPE_CHECKING @@ -22,11 +21,10 @@ VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n") def get_date_for(git_version: str) -> dt.datetime | None: cmd = ["git", "log", "-1", "--format=%ai", git_version] try: - with open(os.devnull, "w", encoding="utf-8") as devnull: - out = subprocess.check_output( - cmd, stderr=devnull, text=True, encoding="utf-8" - ) - ts = out.strip() + out = subprocess.check_output( + cmd, stderr=subprocess.DEVNULL, text=True, encoding="utf-8" + ) + ts = out.strip() return dt.datetime.fromisoformat(ts) except subprocess.CalledProcessError: return None From 4af831e70c41441dbe0cdebf9e2cbd46ae71eb92 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Apr 2024 23:45:25 +1000 Subject: [PATCH 30/50] Accept '.zlib-ng' suffix to zlib version --- Tests/test_features.py | 2 ++ Tests/test_file_png.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index 3a528a7c8..2d402ca91 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -37,6 +37,8 @@ def test_version() -> None: else: assert function(name) == version if name != "PIL": + if name == "zlib" and version is not None: + version = version.replace(".zlib-ng", "") assert version is None or re.search(r"\d+(\.\d+)*$", version) for module in features.modules: diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 30fb14c44..19462dcb5 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -85,7 +85,9 @@ class TestFilePng: def test_sanity(self, tmp_path: Path) -> None: # internal version number - assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib")) + assert re.search( + r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", features.version_codec("zlib") + ) test_file = str(tmp_path / "temp.png") From 03bcf03567ae934feaa737ee9128c19136cb7803 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Apr 2024 08:41:15 +1000 Subject: [PATCH 31/50] Removed Fedora 38 and added Fedora 40 --- .github/workflows/test-docker.yml | 2 +- docs/installation/platform-support.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 70426d7b5..8f4a4d090 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -47,8 +47,8 @@ jobs: debian-11-bullseye-amd64, debian-12-bookworm-x86, debian-12-bookworm-amd64, - fedora-38-amd64, fedora-39-amd64, + fedora-40-amd64, gentoo, ubuntu-20.04-focal-amd64, ubuntu-22.04-jammy-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index af205a4e8..c08a53a43 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -31,10 +31,10 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 12 Bookworm | 3.11 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ -| Fedora 38 | 3.11 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Fedora 39 | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 40 | 3.12 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 12 Monterey | 3.8, 3.9 | x86-64 | From 02db41119018f313df060c254a22f44a95057e15 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Apr 2024 09:14:48 +1000 Subject: [PATCH 32/50] Added release notes --- docs/releasenotes/10.4.0.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/releasenotes/10.4.0.rst b/docs/releasenotes/10.4.0.rst index 0c2926732..3150bf4e0 100644 --- a/docs/releasenotes/10.4.0.rst +++ b/docs/releasenotes/10.4.0.rst @@ -23,6 +23,11 @@ TODO Deprecations ============ +BGR;15, BGR 16 and BGR;24 +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated. + Support for LibTIFF earlier than 4 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 35ffbdc9cde567ae629ae01ddce3116a35f3483d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 01:38:43 +0000 Subject: [PATCH 33/50] Update dependency mypy to v1.10.0 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 6b0535fc1..a0dcb92d2 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1 +1 @@ -mypy==1.9.0 +mypy==1.10.0 From 5faebadd56ab3ec66139d123fcee54d3ff89ad30 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Apr 2024 11:06:04 +1000 Subject: [PATCH 34/50] BGR;16 does not fail on big-endian --- Tests/test_image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 4a056df40..8e7e40c05 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1037,8 +1037,8 @@ class TestImageBytes: @pytest.mark.parametrize("mode", modes) def test_getdata_putdata(self, mode: str) -> None: - if is_big_endian and mode in ("BGR;15", "BGR;16"): - pytest.xfail(f"Known failure of {mode} on big-endian") + if is_big_endian and mode == "BGR;15": + pytest.xfail("Known failure of BGR;15 on big-endian") im = hopper(mode) reloaded = Image.new(mode, im.size) reloaded.putdata(im.getdata()) From bc35bf0c9e2b3c0a5702a21daa02d5982932f8d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Apr 2024 13:10:45 +1000 Subject: [PATCH 35/50] Use split instead of datetime --- docs/dater.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/dater.py b/docs/dater.py index 04855956b..f9fb0c1da 100644 --- a/docs/dater.py +++ b/docs/dater.py @@ -6,7 +6,6 @@ Based on https://github.com/jaraco/rst.linker, with thanks to Jason R. Coombs. from __future__ import annotations -import datetime as dt import re import subprocess from typing import TYPE_CHECKING @@ -18,24 +17,23 @@ DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+") VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n") -def get_date_for(git_version: str) -> dt.datetime | None: +def get_date_for(git_version: str) -> str | None: cmd = ["git", "log", "-1", "--format=%ai", git_version] try: out = subprocess.check_output( cmd, stderr=subprocess.DEVNULL, text=True, encoding="utf-8" ) - ts = out.strip() - return dt.datetime.fromisoformat(ts) except subprocess.CalledProcessError: return None + return out.split()[0] def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None: if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])): old_title = m.group(1) - if tag_datetime := get_date_for(old_title): - new_title = f"{old_title} ({tag_datetime:%Y-%m-%d})" + if tag_date := get_date_for(old_title): + new_title = f"{old_title} ({tag_date})" else: new_title = f"{old_title} (unreleased)" From bbd5a87e6046bd0c0eb9ad105102a0d019277eb5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Apr 2024 16:16:33 +1000 Subject: [PATCH 36/50] Combined conditions --- src/_imagingcms.c | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/_imagingcms.c b/src/_imagingcms.c index f18d55a57..63d78f84d 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -213,34 +213,37 @@ cms_transform_dealloc(CmsTransformObject *self) { static cmsUInt32Number findLCMStype(char *PILmode) { - if (strcmp(PILmode, "RGB") == 0) { + if ( + strcmp(PILmode, "RGB") == 0 || + strcmp(PILmode, "RGBA") == 0 || + strcmp(PILmode, "RGBX") == 0 + ) { return TYPE_RGBA_8; - } else if (strcmp(PILmode, "RGBA") == 0) { - return TYPE_RGBA_8; - } else if (strcmp(PILmode, "RGBX") == 0) { - return TYPE_RGBA_8; - } else if (strcmp(PILmode, "RGBA;16B") == 0) { + } + if (strcmp(PILmode, "RGBA;16B") == 0) { return TYPE_RGBA_16; - } else if (strcmp(PILmode, "CMYK") == 0) { + } + if (strcmp(PILmode, "CMYK") == 0) { return TYPE_CMYK_8; - } else if (strcmp(PILmode, "L") == 0) { - return TYPE_GRAY_8; - } else if (strcmp(PILmode, "L;16") == 0) { + } + if (strcmp(PILmode, "L;16") == 0) { return TYPE_GRAY_16; - } else if (strcmp(PILmode, "L;16B") == 0) { + } + if (strcmp(PILmode, "L;16B") == 0) { return TYPE_GRAY_16_SE; - } else if (strcmp(PILmode, "YCCA") == 0) { + } + if ( + strcmp(PILmode, "YCCA") == 0 || + strcmp(PILmode, "YCC") == 0 + ) { return TYPE_YCbCr_8; - } else if (strcmp(PILmode, "YCC") == 0) { - return TYPE_YCbCr_8; - } else if (strcmp(PILmode, "LAB") == 0) { + } + if (strcmp(PILmode, "LAB") == 0) { // LabX equivalent like ALab, but not reversed -- no #define in lcms2 return (COLORSPACE_SH(PT_LabV2) | CHANNELS_SH(3) | BYTES_SH(1) | EXTRA_SH(1)); } - else { - /* take a wild guess... */ - return TYPE_GRAY_8; - } + /* presume "L" by default */ + return TYPE_GRAY_8; } #define Cms_Min(a, b) ((a) < (b) ? (a) : (b)) From 0099de0ed904c2021850fbb5a19908c36f908890 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 25 Apr 2024 16:00:14 +0300 Subject: [PATCH 37/50] Add deprecation helper for Image.new with BGR; modes --- Tests/test_image.py | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index ed80be503..e8339424d 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -63,14 +63,19 @@ image_modes = ( image_mode_names = [name for name, _ in image_modes] +# Deprecation helper +def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + return Image.new(mode, size) + else: + return Image.new(mode, size) + + class TestImage: @pytest.mark.parametrize("mode", image_mode_names) def test_image_modes_success(self, mode: str) -> None: - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): - Image.new(mode, (1, 1)) - else: - Image.new(mode, (1, 1)) + helper_image_new(mode, (1, 1)) @pytest.mark.parametrize("mode", ("", "bad", "very very long")) def test_image_modes_fail(self, mode: str) -> None: @@ -1066,29 +1071,17 @@ class TestImageBytes: im = hopper(mode) source_bytes = im.tobytes() - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): - reloaded = Image.new(mode, im.size) - else: - reloaded = Image.new(mode, im.size) + reloaded = helper_image_new(mode, im.size) reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes @pytest.mark.parametrize(("mode", "pixelsize"), image_modes) def test_getdata_putdata(self, mode: str, pixelsize: int) -> None: - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): - im = Image.new(mode, (2, 2)) - else: - im = Image.new(mode, (2, 2)) + im = helper_image_new(mode, (2, 2)) source_bytes = bytes(range(im.width * im.height * pixelsize)) im.frombytes(source_bytes) - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): - reloaded = Image.new(mode, im.size) - else: - reloaded = Image.new(mode, im.size) + reloaded = helper_image_new(mode, im.size) reloaded.putdata(im.getdata()) assert_image_equal(im, reloaded) From a4080a72494042183cff6fbadeba3026fb6fa711 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 25 Apr 2024 08:51:33 -0500 Subject: [PATCH 38/50] clean up comments in test_image_access.py --- Tests/test_image_access.py | 40 +++++++++++++------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 50afb2a23..2cd2e0c34 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -33,7 +33,7 @@ except ImportError: class AccessTest: - # initial value + # Initial value _init_cffi_access = Image.USE_CFFI_ACCESS _need_cffi_access = False @@ -151,7 +151,7 @@ class TestImageGetPixel(AccessTest): self.color(mode) if expected_color_int is None else expected_color_int ) - # check putpixel + # Check putpixel im = Image.new(mode, (1, 1), None) im.putpixel((0, 0), expected_color) actual_color = im.getpixel((0, 0)) @@ -160,7 +160,7 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # check putpixel negative index + # Check putpixel negative index im.putpixel((-1, -1), expected_color) actual_color = im.getpixel((-1, -1)) assert actual_color == expected_color, ( @@ -168,7 +168,7 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # check 0x0 image with None initial color + # Check 0x0 image with None initial color im = Image.new(mode, (0, 0), None) assert im.load() is not None error = ValueError if self._need_cffi_access else IndexError @@ -176,13 +176,13 @@ class TestImageGetPixel(AccessTest): im.putpixel((0, 0), expected_color) with pytest.raises(error): im.getpixel((0, 0)) - # check negative index + # Check negative index with pytest.raises(error): im.putpixel((-1, -1), expected_color) with pytest.raises(error): im.getpixel((-1, -1)) - # check initial color + # Check initial color im = Image.new(mode, (1, 1), expected_color) actual_color = im.getpixel((0, 0)) assert actual_color == expected_color, ( @@ -190,18 +190,18 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # check initial color negative index + # Check initial color negative index actual_color = im.getpixel((-1, -1)) assert actual_color == expected_color, ( f"initial color failed with negative index for mode {mode}, " f"expected {expected_color} got {actual_color}" ) - # check 0x0 image with initial color + # Check 0x0 image with initial color im = Image.new(mode, (0, 0), expected_color) with pytest.raises(error): im.getpixel((0, 0)) - # check negative index + # Check negative index with pytest.raises(error): im.getpixel((-1, -1)) @@ -216,7 +216,7 @@ class TestImageGetPixel(AccessTest): @pytest.mark.parametrize("mode", ("I;16", "I;16B")) @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)) def test_signedness(self, mode: str, expected_color: int) -> None: - # see https://github.com/python-pillow/Pillow/issues/452 + # See https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* self.check(mode, expected_color) @@ -276,13 +276,6 @@ class TestCffi(AccessTest): im = Image.new(mode, (10, 10), 40000) self._test_get_access(im) - # These don't actually appear to be modes that I can actually make, - # as unpack sets them directly into the I mode. - # im = Image.new('I;32L', (10, 10), -2**10) - # self._test_get_access(im) - # im = Image.new('I;32B', (10, 10), 2**10) - # self._test_get_access(im) - def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None: """Are we writing the correct bits into the image? @@ -314,23 +307,18 @@ class TestCffi(AccessTest): self._test_set_access(hopper("LA"), (128, 128)) self._test_set_access(hopper("1"), 255) self._test_set_access(hopper("P"), 128) - # self._test_set_access(i, (128, 128)) #PA -- undone how to make + self._test_set_access(hopper("PA"), (128, 128)) self._test_set_access(hopper("F"), 1024.0) for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): im = Image.new(mode, (10, 10), 40000) self._test_set_access(im, 45000) - # im = Image.new('I;32L', (10, 10), -(2**10)) - # self._test_set_access(im, -(2**13)+1) - # im = Image.new('I;32B', (10, 10), 2**10) - # self._test_set_access(im, 2**13-1) - @pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_not_implemented(self) -> None: assert PyAccess.new(hopper("BGR;15")) is None - # ref https://github.com/python-pillow/Pillow/pull/2009 + # Ref https://github.com/python-pillow/Pillow/pull/2009 def test_reference_counting(self) -> None: size = 10 @@ -339,7 +327,7 @@ class TestCffi(AccessTest): with pytest.warns(DeprecationWarning): px = Image.new("L", (size, 1), 0).load() for i in range(size): - # pixels can contain garbage if image is released + # Pixels can contain garbage if image is released assert px[i, 0] == 0 @pytest.mark.parametrize("mode", ("P", "PA")) @@ -456,7 +444,7 @@ int main(int argc, char* argv[]) env = os.environ.copy() env["PATH"] = sys.prefix + ";" + env["PATH"] - # do not display the Windows Error Reporting dialog + # Do not display the Windows Error Reporting dialog getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) process = subprocess.Popen(["embed_pil.exe"], env=env) From c3ded3abdaa64d4ba75445b43d09d4fcf3129d73 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 Apr 2024 09:13:00 +1000 Subject: [PATCH 39/50] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 85dc0b43c..c5df1f8f7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 10.4.0 (unreleased) ------------------- +- Deprecate BGR;15, BGR;16 and BGR;24 modes #7978 + [radarhere, hugovk] + +- Fix ImagingAccess for I;16N on big-endian #7921 + [Yay295, radarhere] + - Support reading P mode TIFF images with padding #7996 [radarhere] From 8cc48b24fe83dc81e6a5e6a83f287d333d74a44f Mon Sep 17 00:00:00 2001 From: Cees Timmerman Date: Fri, 26 Apr 2024 17:17:44 +0200 Subject: [PATCH 40/50] Update ExifTags.py Fixed typo. No other instances in this repo. --- src/PIL/ExifTags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 60a4d9774..39b4aa552 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -346,7 +346,7 @@ class Interop(IntEnum): InteropVersion = 2 RelatedImageFileFormat = 4096 RelatedImageWidth = 4097 - RleatedImageHeight = 4098 + RelatedImageHeight = 4098 class IFD(IntEnum): From 86fb383739597acafc5e452e38eb3b87370d5eb1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Apr 2024 14:08:36 +1000 Subject: [PATCH 41/50] Corrected big-endian check --- Tests/test_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index b7f814080..e1490d6a0 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1050,7 +1050,7 @@ class TestImageBytes: @pytest.mark.parametrize("mode", modes) def test_getdata_putdata(self, mode: str) -> None: - if is_big_endian and mode == "BGR;15": + if is_big_endian() and mode == "BGR;15": pytest.xfail("Known failure of BGR;15 on big-endian") im = hopper(mode) reloaded = helper_image_new(mode, im.size) From d01e43e796e3d35e6c562145cef13f3e1ffc646d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 Apr 2024 09:11:33 +1000 Subject: [PATCH 42/50] Removed direct invocation of setup.py --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 477d92609..1f9b4a370 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,6 @@ .PHONY: clean clean: - python3 setup.py clean rm src/PIL/*.so || true rm -r build || true find . -name __pycache__ | xargs rm -r || true From 36869833c723a6ff54de74ec5c5d630a770d6d1d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 28 Apr 2024 23:06:39 +1000 Subject: [PATCH 43/50] Added Ubuntu 24.04 --- .github/workflows/test-docker.yml | 13 +++++++------ .github/workflows/test-valgrind.yml | 4 ++-- docs/installation/platform-support.rst | 4 +++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 8f4a4d090..c53f23a9f 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -36,8 +36,8 @@ jobs: docker: [ # Run slower jobs first to give them a headstart and reduce waiting time ubuntu-22.04-jammy-arm64v8, - ubuntu-22.04-jammy-ppc64le, - ubuntu-22.04-jammy-s390x, + ubuntu-24.04-noble-ppc64le, + ubuntu-24.04-noble-s390x, # Then run the remainder alpine, amazon-2-amd64, @@ -52,14 +52,15 @@ jobs: gentoo, ubuntu-20.04-focal-amd64, ubuntu-22.04-jammy-amd64, + ubuntu-24.04-noble-amd64, ] dockerTag: [main] include: - docker: "ubuntu-22.04-jammy-arm64v8" qemu-arch: "aarch64" - - docker: "ubuntu-22.04-jammy-ppc64le" + - docker: "ubuntu-24.04-noble-ppc64le" qemu-arch: "ppc64le" - - docker: "ubuntu-22.04-jammy-s390x" + - docker: "ubuntu-24.04-noble-s390x" qemu-arch: "s390x" name: ${{ matrix.docker }} @@ -81,8 +82,8 @@ jobs: - name: Docker build run: | - # The Pillow user in the docker container is UID 1000 - sudo chown -R 1000 $GITHUB_WORKSPACE + # The Pillow user in the docker container is UID 1001 + sudo chown -R 1001 $GITHUB_WORKSPACE docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} sudo chown -R runner $GITHUB_WORKSPACE diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 59bb958ec..63aec586b 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -50,7 +50,7 @@ jobs: - name: Build and Run Valgrind run: | - # The Pillow user in the docker container is UID 1000 - sudo chown -R 1000 $GITHUB_WORKSPACE + # The Pillow user in the docker container is UID 1001 + sudo chown -R 1001 $GITHUB_WORKSPACE docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} sudo chown -R runner $GITHUB_WORKSPACE diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index c08a53a43..888966c51 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -47,7 +47,9 @@ These platforms are built and tested for every change. | Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, 3.13, PyPy3 | | | +----------------------------+---------------------+ -| | 3.10 | arm64v8, ppc64le, | +| | 3.10 | arm64v8 | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, | | | | s390x | +----------------------------------+----------------------------+---------------------+ | Windows Server 2016 | 3.8 | x86-64 | From 65d73ea970c31c33e23a55273dbdea24376efe04 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 Apr 2024 18:54:16 +1000 Subject: [PATCH 44/50] Python 3.8 and 3.9 are tested on macOS 13 --- docs/installation/platform-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index c08a53a43..02c409356 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -37,7 +37,7 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 12 Monterey | 3.8, 3.9 | x86-64 | +| macOS 13 Ventura | 3.8, 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | | | PyPy3 | | From 2250fbeb9a8a1b61c9ec8e7df0902a0c35bc495e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 Apr 2024 20:19:05 +1000 Subject: [PATCH 45/50] Added type hints --- src/PIL/Image.py | 12 +++++++----- src/PIL/ImageFile.py | 2 +- src/PIL/JpegImagePlugin.py | 8 +++++--- src/PIL/PngImagePlugin.py | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a17edfa39..33b3da9a6 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,7 +41,7 @@ import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum from types import ModuleType -from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, cast +from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, cast # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -877,7 +877,7 @@ class Image: return self.pyaccess return self.im.pixel_access(self.readonly) - def verify(self): + def verify(self) -> None: """ Verifies the contents of a file. For data read from a file, this method attempts to determine if the file is broken, without @@ -1267,7 +1267,9 @@ class Image: return im.crop((x0, y0, x1, y1)) - def draft(self, mode, size): + def draft( + self, mode: str, size: tuple[int, int] + ) -> tuple[str, tuple[int, int, float, float]] | None: """ Configures the image file loader so it returns a version of the image that as closely as possible matches the given mode and @@ -1290,7 +1292,7 @@ class Image: """ pass - def _expand(self, xmargin, ymargin=None): + def _expand(self, xmargin: int, ymargin: int | None = None) -> Image: if ymargin is None: ymargin = xmargin self.load() @@ -3450,7 +3452,7 @@ def eval(image, *args): return image.point(args[0]) -def merge(mode, bands): +def merge(mode: str, bands: Sequence[Image]) -> Image: """ Merge a set of single band images into a new multiband image. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 0283fa2fd..27885e654 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -163,7 +163,7 @@ class ImageFile(Image.Image): self.tile = [] super().__setstate__(state) - def verify(self): + def verify(self) -> None: """Check file integrity""" # raise exception if something's wrong. must be called diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index e3c0083e9..715a358a3 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -424,13 +424,15 @@ class JpegImageFile(ImageFile.ImageFile): return s - def draft(self, mode, size): + def draft( + self, mode: str, size: tuple[int, int] + ) -> tuple[str, tuple[int, int, float, float]] | None: if len(self.tile) != 1: - return + return None # Protect from second call if self.decoderconfig: - return + return None d, e, o, a = self.tile[0] scale = 1 diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 8b81e54ea..012e0b61b 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -783,7 +783,7 @@ class PngImageFile(ImageFile.ImageFile): self.seek(frame) return self._text - def verify(self): + def verify(self) -> None: """Verify PNG file""" if self.fp is None: From e8cddfbc6a2a6167e395fc8cfad9ea29a38f108b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 3 May 2024 08:45:39 +1000 Subject: [PATCH 46/50] Updated codecov/codecov-action to v4 --- .github/workflows/test-cygwin.yml | 3 ++- .github/workflows/test-docker.yml | 3 ++- .github/workflows/test-mingw.yml | 3 ++- .github/workflows/test-windows.yml | 3 ++- .github/workflows/test.yml | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 9674a4665..7972730ca 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -132,11 +132,12 @@ jobs: bash.exe .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v3.1.5 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: GHA_Cygwin name: Cygwin Python 3.${{ matrix.python-minor-version }} + token: ${{ secrets.CODECOV_ORG_TOKEN }} success: permissions: diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index c53f23a9f..6afed74db 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -100,11 +100,12 @@ jobs: MATRIX_DOCKER: ${{ matrix.docker }} - name: Upload coverage - uses: codecov/codecov-action@v3.1.5 + uses: codecov/codecov-action@v4 with: flags: GHA_Docker name: ${{ matrix.docker }} gcov: true + token: ${{ secrets.CODECOV_ORG_TOKEN }} success: permissions: diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a07a27c46..a773ca453 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -85,8 +85,9 @@ jobs: python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests - name: Upload coverage - uses: codecov/codecov-action@v3.1.5 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: GHA_Windows name: "MSYS2 MinGW" + token: ${{ secrets.CODECOV_ORG_TOKEN }} diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 40994c60a..9edc15173 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -213,11 +213,12 @@ jobs: shell: pwsh - name: Upload coverage - uses: codecov/codecov-action@v3.1.5 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: GHA_Windows name: ${{ runner.os }} Python ${{ matrix.python-version }} + token: ${{ secrets.CODECOV_ORG_TOKEN }} success: permissions: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4573fde90..aa5646caf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -150,11 +150,12 @@ jobs: .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v3.1.5 + uses: codecov/codecov-action@v4 with: flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} gcov: true + token: ${{ secrets.CODECOV_ORG_TOKEN }} success: permissions: From ac1eb57c03e182752e1207cd477300650fce4dc0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 3 May 2024 09:43:50 +1000 Subject: [PATCH 47/50] Install git --- .github/workflows/test-cygwin.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7972730ca..1269ef8cb 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -55,6 +55,7 @@ jobs: packages: > gcc-g++ ghostscript + git ImageMagick jpeg libfreetype-devel From 74063feadca98b847ee8e239531d2cfb73de12e5 Mon Sep 17 00:00:00 2001 From: mrKazzila Date: Sat, 4 May 2024 19:21:49 +0300 Subject: [PATCH 48/50] chore: add f-string formatting --- src/PIL/DdsImagePlugin.py | 8 ++++---- src/PIL/ImImagePlugin.py | 2 +- src/PIL/Image.py | 14 +++++++------- src/PIL/ImageMath.py | 4 ++-- src/PIL/ImageMode.py | 8 ++++---- src/PIL/ImageMorph.py | 2 +- src/PIL/ImageWin.py | 2 +- src/PIL/PalmImagePlugin.py | 2 +- src/PIL/PdfParser.py | 4 ++-- src/PIL/PngImagePlugin.py | 2 +- src/PIL/SpiderImagePlugin.py | 10 +++++----- src/PIL/TiffImagePlugin.py | 12 ++++++------ src/PIL/features.py | 4 ++-- 13 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 3032e4aec..2496088af 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -271,16 +271,16 @@ class D3DFMT(IntEnum): module = sys.modules[__name__] for item in DDSD: assert item.name is not None - setattr(module, "DDSD_" + item.name, item.value) + setattr(module, f"DDSD_{item.name}", item.value) for item1 in DDSCAPS: assert item1.name is not None - setattr(module, "DDSCAPS_" + item1.name, item1.value) + setattr(module, f"DDSCAPS_{item1.name}", item1.value) for item2 in DDSCAPS2: assert item2.name is not None - setattr(module, "DDSCAPS2_" + item2.name, item2.value) + setattr(module, f"DDSCAPS2_{item2.name}", item2.value) for item3 in DDPF: assert item3.name is not None - setattr(module, "DDPF_" + item3.name, item3.value) + setattr(module, f"DDPF_{item3.name}", item3.value) DDS_FOURCC = DDPF.FOURCC DDS_RGB = DDPF.RGB diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 4613e40b6..77b396387 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -196,7 +196,7 @@ class ImImageFile(ImageFile.ImageFile): n += 1 else: - msg = "Syntax error in IM header: " + s.decode("ascii", "replace") + msg = f"Syntax error in IM header: {s.decode('ascii', 'replace')}" raise SyntaxError(msg) if not n: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 33b3da9a6..2184ef8ea 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -405,7 +405,7 @@ def _getdecoder(mode, decoder_name, args, extra=()): try: # get decoder - decoder = getattr(core, decoder_name + "_decoder") + decoder = getattr(core, f"{decoder_name}_decoder") except AttributeError as e: msg = f"decoder {decoder_name} not available" raise OSError(msg) from e @@ -428,7 +428,7 @@ def _getencoder(mode, encoder_name, args, extra=()): try: # get encoder - encoder = getattr(core, encoder_name + "_encoder") + encoder = getattr(core, f"{encoder_name}_encoder") except AttributeError as e: msg = f"encoder {encoder_name} not available" raise OSError(msg) from e @@ -603,7 +603,7 @@ class Image: ) -> str: suffix = "" if format: - suffix = "." + format + suffix = f".{format}" if not file: f, filename = tempfile.mkstemp(suffix) @@ -2180,7 +2180,7 @@ class Image: (Resampling.HAMMING, "Image.Resampling.HAMMING"), ) ] - msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] + msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" raise ValueError(msg) if reducing_gap is not None and reducing_gap < 1.0: @@ -2825,7 +2825,7 @@ class Image: (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), ) ] - msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] + msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" raise ValueError(msg) image.load() @@ -3223,8 +3223,8 @@ _fromarray_typemap = { ((1, 1, 3), "|u1"): ("RGB", "RGB"), ((1, 1, 4), "|u1"): ("RGBA", "RGBA"), # shortcuts: - ((1, 1), _ENDIAN + "i4"): ("I", "I"), - ((1, 1), _ENDIAN + "f4"): ("F", "F"), + ((1, 1), f"{_ENDIAN}i4"): ("I", "I"), + ((1, 1), f"{_ENDIAN}f4"): ("F", "F"), } diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 77472a24c..6664434ea 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -61,7 +61,7 @@ class _Operand: out = Image.new(mode or im_1.mode, im_1.size, None) im_1.load() try: - op = getattr(_imagingmath, op + "_" + im_1.mode) + op = getattr(_imagingmath, f"{op}_{im_1.mode}") except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e @@ -89,7 +89,7 @@ class _Operand: im_1.load() im_2.load() try: - op = getattr(_imagingmath, op + "_" + im_1.mode) + op = getattr(_imagingmath, f"{op}_{im_1.mode}") except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 7bd2afcf2..92a08d2cb 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -44,8 +44,8 @@ def getmode(mode: str) -> ModeDescriptor: # Bits need to be extended to bytes "1": ("L", "L", ("1",), "|b1"), "L": ("L", "L", ("L",), "|u1"), - "I": ("L", "I", ("I",), endian + "i4"), - "F": ("L", "F", ("F",), endian + "f4"), + "I": ("L", "I", ("I",), f"{endian}i4"), + "F": ("L", "F", ("F",), f"{endian}f4"), "P": ("P", "L", ("P",), "|u1"), "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"), "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"), @@ -78,8 +78,8 @@ def getmode(mode: str) -> ModeDescriptor: "I;16LS": "u2", "I;16BS": ">i2", - "I;16N": endian + "u2", - "I;16NS": endian + "i2", + "I;16N": f"{endian}u2", + "I;16NS": f"{endian}i2", "I;32": "u4", "I;32L": " Date: Sat, 4 May 2024 19:26:22 +0300 Subject: [PATCH 49/50] chore: update __repr__ for PdfName --- src/PIL/PdfParser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index d6f2ebd44..1485cafe0 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -225,7 +225,7 @@ class PdfName: return hash(self.name) def __repr__(self): - return f"PdfName({repr(self.name)})" + return f"{self.__class__.__name__}({repr(self.name)})" @classmethod def from_pdf_stream(cls, data): From 71b8d99b3699bab3e7217af7453199853671409f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 4 May 2024 19:27:42 +0000 Subject: [PATCH 50/50] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/PdfParser.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 1485cafe0..c1ed78797 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -144,9 +144,7 @@ class XrefTable: elif key in self.deleted_entries: generation = self.deleted_entries[key] else: - msg = ( - f"object ID {key} cannot be deleted because it doesn't exist" - ) + msg = f"object ID {key} cannot be deleted because it doesn't exist" raise IndexError(msg) def __contains__(self, key):