From f6f3dab6724d72a60ebf0244843a82418f7d7740 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Dec 2023 17:42:17 +1100 Subject: [PATCH 001/688] Import UnidentifiedImageError directly --- Tests/oss-fuzz/test_fuzzers.py | 4 ++-- Tests/test_file_eps.py | 4 ++-- Tests/test_file_psd.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 68834045a..186a0efd3 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -6,7 +6,7 @@ import fuzzers import packaging import pytest -from PIL import Image, features +from PIL import Image, UnidentifiedImageError, features from Tests.helper import skip_unless_feature if sys.platform.startswith("win32"): @@ -42,7 +42,7 @@ def test_fuzz_images(path): except ( Image.DecompressionBombError, Image.DecompressionBombWarning, - Image.UnidentifiedImageError, + UnidentifiedImageError, ): # Known Image.* exceptions assert True diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index c479c384a..360ae11b5 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -3,7 +3,7 @@ import io import pytest -from PIL import EpsImagePlugin, Image, features +from PIL import EpsImagePlugin, Image, UnidentifiedImageError, features from .helper import ( assert_image_similar, @@ -417,7 +417,7 @@ def test_emptyline(): ) def test_timeout(test_file): with open(test_file, "rb") as f: - with pytest.raises(Image.UnidentifiedImageError): + with pytest.raises(UnidentifiedImageError): with Image.open(f): pass diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 8b06ce2b1..d98f23356 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -3,7 +3,7 @@ import warnings import pytest -from PIL import Image, PsdImagePlugin +from PIL import Image, PsdImagePlugin, UnidentifiedImageError from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy @@ -146,11 +146,11 @@ def test_combined_larger_than_size(): [ ( "Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd", - Image.UnidentifiedImageError, + UnidentifiedImageError, ), ( "Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd", - Image.UnidentifiedImageError, + UnidentifiedImageError, ), ("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError), ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), From 8070fe10f1ccc346539f87c88fb65ea55d001b9a Mon Sep 17 00:00:00 2001 From: Nulano Date: Sat, 23 Dec 2023 14:41:50 +0100 Subject: [PATCH 002/688] pass build config before setuptools command; add build_editable to custom build backend --- .github/workflows/wheels.yml | 2 ++ _custom_build/backend.py | 55 +++++++++++++----------------------- pyproject.toml | 2 +- setup.py | 34 +++++++++++++++++++--- 4 files changed, 53 insertions(+), 40 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 5adff7ec1..0c8a941de 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -5,6 +5,7 @@ on: paths: - ".ci/requirements-cibw.txt" - ".github/workflows/wheel*" + - "setup.py" - "wheels/*" - "winbuild/build_prepare.py" - "winbuild/fribidi.cmake" @@ -14,6 +15,7 @@ on: paths: - ".ci/requirements-cibw.txt" - ".github/workflows/wheel*" + - "setup.py" - "wheels/*" - "winbuild/build_prepare.py" - "winbuild/fribidi.cmake" diff --git a/_custom_build/backend.py b/_custom_build/backend.py index d1537b809..0b183a587 100644 --- a/_custom_build/backend.py +++ b/_custom_build/backend.py @@ -11,41 +11,16 @@ backend_class = build_wheel.__self__.__class__ class _CustomBuildMetaBackend(backend_class): def run_setup(self, setup_script="setup.py"): if self.config_settings: + params = [] + for k, v in self.config_settings.items(): + if isinstance(v, list): + msg = "Conflicting options: " + ", ".join( + f"'--config-setting {k}={v_}'" for v_ in v + ) + raise ValueError(msg) + params.append(f"--pillow-configuration={k}={v}") - def config_has(key, value): - settings = self.config_settings.get(key) - if settings: - if not isinstance(settings, list): - settings = [settings] - return value in settings - - flags = [] - for dependency in ( - "zlib", - "jpeg", - "tiff", - "freetype", - "raqm", - "lcms", - "webp", - "webpmux", - "jpeg2000", - "imagequant", - "xcb", - ): - if config_has(dependency, "enable"): - flags.append("--enable-" + dependency) - elif config_has(dependency, "disable"): - flags.append("--disable-" + dependency) - for dependency in ("raqm", "fribidi"): - if config_has(dependency, "vendor"): - flags.append("--vendor-" + dependency) - if self.config_settings.get("platform-guessing") == "disable": - flags.append("--disable-platform-guessing") - if self.config_settings.get("debug") == "true": - flags.append("--debug") - if flags: - sys.argv = sys.argv[:1] + ["build_ext"] + flags + sys.argv[1:] + sys.argv = sys.argv[:1] + params + sys.argv[1:] return super().run_setup(setup_script) def build_wheel( @@ -54,5 +29,15 @@ class _CustomBuildMetaBackend(backend_class): self.config_settings = config_settings return super().build_wheel(wheel_directory, config_settings, metadata_directory) + def build_editable( + self, wheel_directory, config_settings=None, metadata_directory=None + ): + self.config_settings = config_settings + return super().build_editable( + wheel_directory, config_settings, metadata_directory + ) -build_wheel = _CustomBuildMetaBackend().build_wheel + +_backend = _CustomBuildMetaBackend() +build_wheel = _backend.build_wheel +build_editable = _backend.build_editable diff --git a/pyproject.toml b/pyproject.toml index da2537b21..d63e401af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ version = {attr = "PIL.__version__"} [tool.cibuildwheel] before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 -config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" +config-settings = "raqm=vendor fribidi=vendor imagequant=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" diff --git a/setup.py b/setup.py index 1bf0bcff5..c74165fb7 100755 --- a/setup.py +++ b/setup.py @@ -28,6 +28,9 @@ def get_version(): return locals()["__version__"] +configuration = {} + + PILLOW_VERSION = get_version() FREETYPE_ROOT = None HARFBUZZ_ROOT = None @@ -334,15 +337,24 @@ class pil_build_ext(build_ext): + [("add-imaging-libs=", None, "Add libs to _imaging build")] ) + @staticmethod + def check_configuration(option, value): + return True if configuration.get(option) == value else None + def initialize_options(self): - self.disable_platform_guessing = None + self.disable_platform_guessing = self.check_configuration( + "platform-guessing", "disable" + ) self.add_imaging_libs = "" build_ext.initialize_options(self) for x in self.feature: - setattr(self, f"disable_{x}", None) - setattr(self, f"enable_{x}", None) + setattr(self, f"disable_{x}", self.check_configuration(x, "disable")) + setattr(self, f"enable_{x}", self.check_configuration(x, "enable")) for x in ("raqm", "fribidi"): - setattr(self, f"vendor_{x}", None) + setattr(self, f"vendor_{x}", self.check_configuration(x, "vendor")) + if self.check_configuration("debug", "true"): + self.debug = True + self.parallel = configuration.get("parallel") def finalize_options(self): build_ext.finalize_options(self) @@ -390,6 +402,9 @@ class pil_build_ext(build_ext): raise ValueError(msg) _dbg("Using vendored version of %s", x) self.feature.vendor.add(x) + if x == "raqm": + _dbg("--vendor-raqm implies --enable-raqm") + self.feature.required.add(x) def _update_extension(self, name, libraries, define_macros=None, sources=None): for extension in self.extensions: @@ -985,6 +1000,17 @@ ext_modules = [ Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), ] + +# parse configuration from _custom_build/backend.py +while len(sys.argv[1]) >= 2 and sys.argv[1].startswith("--pillow-configuration="): + _, key, value = sys.argv[1].split("=", 2) + old = configuration.get(key) + if old is not None: + msg = f"Conflicting options: '-C {key}={old}' and '-C {key}={value}'" + raise ValueError(msg) + configuration[key] = value + del sys.argv[1] + try: setup( cmdclass={"build_ext": pil_build_ext}, From b4e690049d81dc2569d6f02c5b0c1b9eb29a07b2 Mon Sep 17 00:00:00 2001 From: Nulano Date: Sun, 31 Dec 2023 02:22:05 +0100 Subject: [PATCH 003/688] document config setting "-C parallel=n" for number of CPUs to use for compilation --- docs/installation.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index fbcfbb907..03011619f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -390,9 +390,10 @@ After navigating to the Pillow directory, run:: Build Options """"""""""""" -* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use - multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` - sets the number of CPUs to use, or can disable parallel building by +* Config setting: ``-C parallel=n``. Can also be given + with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use + multiprocessing to build the extension. Setting ``-C parallel=n`` + sets the number of CPUs to use to ``n``, or can disable parallel building by using a setting of 1. By default, it uses 4 CPUs, or if 4 are not available, as many as are present. @@ -417,14 +418,13 @@ Build Options used to compile the standard Pillow wheels. Compiling libraqm requires a C99-compliant compiler. -* Build flag: ``-C platform-guessing=disable``. Skips all of the +* Config setting: ``-C platform-guessing=disable``. Skips all of the platform dependent guessing of include and library directories for automated build systems that configure the proper paths in the environment variables (e.g. Buildroot). -* Build flag: ``-C debug=true``. Adds a debugging flag to the include and - library search process to dump all paths searched for and found to - stdout. +* Config setting: ``-C debug=true``. Adds a debugging flag to the include and + library search process to dump all paths searched for and found to stdout. Sample usage:: From f27b838a451d0b20befea03da7b2d58dd1da7821 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 2 Jan 2024 15:47:47 +0100 Subject: [PATCH 004/688] support multiple --config-settings --- _custom_build/backend.py | 12 +++++------- pyproject.toml | 2 +- setup.py | 13 +++---------- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/_custom_build/backend.py b/_custom_build/backend.py index 0b183a587..2c670ff0a 100644 --- a/_custom_build/backend.py +++ b/_custom_build/backend.py @@ -12,13 +12,11 @@ class _CustomBuildMetaBackend(backend_class): def run_setup(self, setup_script="setup.py"): if self.config_settings: params = [] - for k, v in self.config_settings.items(): - if isinstance(v, list): - msg = "Conflicting options: " + ", ".join( - f"'--config-setting {k}={v_}'" for v_ in v - ) - raise ValueError(msg) - params.append(f"--pillow-configuration={k}={v}") + for key, values in self.config_settings.items(): + if not isinstance(values, list): + values = [values] + for value in values: + params.append(f"--pillow-configuration={key}={value}") sys.argv = sys.argv[:1] + params + sys.argv[1:] return super().run_setup(setup_script) diff --git a/pyproject.toml b/pyproject.toml index d63e401af..da2537b21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ version = {attr = "PIL.__version__"} [tool.cibuildwheel] before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 -config-settings = "raqm=vendor fribidi=vendor imagequant=disable" +config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" diff --git a/setup.py b/setup.py index c74165fb7..686e8889f 100755 --- a/setup.py +++ b/setup.py @@ -339,7 +339,7 @@ class pil_build_ext(build_ext): @staticmethod def check_configuration(option, value): - return True if configuration.get(option) == value else None + return True if value in configuration.get(option, []) else None def initialize_options(self): self.disable_platform_guessing = self.check_configuration( @@ -354,7 +354,7 @@ class pil_build_ext(build_ext): setattr(self, f"vendor_{x}", self.check_configuration(x, "vendor")) if self.check_configuration("debug", "true"): self.debug = True - self.parallel = configuration.get("parallel") + self.parallel = configuration.get("parallel", [None])[-1] def finalize_options(self): build_ext.finalize_options(self) @@ -402,9 +402,6 @@ class pil_build_ext(build_ext): raise ValueError(msg) _dbg("Using vendored version of %s", x) self.feature.vendor.add(x) - if x == "raqm": - _dbg("--vendor-raqm implies --enable-raqm") - self.feature.required.add(x) def _update_extension(self, name, libraries, define_macros=None, sources=None): for extension in self.extensions: @@ -1004,11 +1001,7 @@ ext_modules = [ # parse configuration from _custom_build/backend.py while len(sys.argv[1]) >= 2 and sys.argv[1].startswith("--pillow-configuration="): _, key, value = sys.argv[1].split("=", 2) - old = configuration.get(key) - if old is not None: - msg = f"Conflicting options: '-C {key}={old}' and '-C {key}={value}'" - raise ValueError(msg) - configuration[key] = value + configuration.setdefault(key, []).append(value) del sys.argv[1] try: From 01e5f06da055490f70b1294f39a2718297ee8067 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 2 Jan 2024 16:12:37 +0100 Subject: [PATCH 005/688] document editable mode installation in winbuild/build.rst --- winbuild/build.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/winbuild/build.rst b/winbuild/build.rst index a8e4ebaa6..26d0da0a3 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -87,11 +87,18 @@ are set by running ``winbuild\build\build_env.cmd`` and install Pillow with pip: winbuild\build\build_env.cmd python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . +You can also install Pillow in `editable mode`_:: + + winbuild\build\build_env.cmd + python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor -e . + To build a wheel instead, run:: winbuild\build\build_env.cmd python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . +.. _editable mode: https://setuptools.pypa.io/en/latest/userguide/development_mode.html + Testing Pillow -------------- From df99d48a0cb98cad2f6ee5ae65b0be5df8d84ef3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 09:26:56 +1100 Subject: [PATCH 006/688] Simplified code --- src/PIL/PsdImagePlugin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 5cff56413..9f7bf7829 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -185,14 +185,13 @@ def _layerinfo(fp, ct_bytes): # image info mode = [] ct_types = i16(read(2)) - types = list(range(ct_types)) - if len(types) > 4: - fp.seek(len(types) * 6 + 12, io.SEEK_CUR) + if ct_types > 4: + fp.seek(ct_types * 6 + 12, io.SEEK_CUR) size = i32(read(4)) fp.seek(size, io.SEEK_CUR) continue - for _ in types: + for _ in range(ct_types): type = i16(read(2)) if type == 65535: From c2907dc04967109391a77eea00f7d583a0a0395f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 23:53:45 +1100 Subject: [PATCH 007/688] Layer co-ordinates may be negative --- Tests/images/negative_top_left_layer.psd | Bin 0 -> 8220 bytes Tests/test_file_psd.py | 5 +++++ src/PIL/PsdImagePlugin.py | 9 +++++---- src/PIL/_binary.py | 10 ++++++++++ 4 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 Tests/images/negative_top_left_layer.psd diff --git a/Tests/images/negative_top_left_layer.psd b/Tests/images/negative_top_left_layer.psd new file mode 100644 index 0000000000000000000000000000000000000000..be9d9d6d03e357d4e3c31aecf0d3f90f9fc49974 GIT binary patch literal 8220 zcmeHLOKclO82+84w2B(Fg$kj{J{Py>O@s5)Tnk;DCfU!vP66^uVR05)uyyaj1IWz(WGT1y$ysS-%1)G!l)3h{_mtCT}}nG*az3o1HEcM}Oi^6FbX3af3SXscWnN8yQRS zkv;f{D6LjB3(OcF9sQ9%?~YCK@mQzlGb7Uxh;VkX$C1md)eI*XI|AUip+A3OS~z{? zDPjBfh(8Y7;a->|Mo>RmD$TJO8<*d|jC}e+WLO*9+)_JX(!fA>1hhC})cp|UXHZTz z!a9>*MR~8WNRX-6`zTNPxIvV^X8KAb|G?xbm(f9#{qp<_`nDHkq1q=``eb9#chE*d zn**lfBL2wfNH&@L9Fv{e67G!`7PXz!eS87?1K1vb6HtI6lt6$AEP)L*2tmZ&MNI(n zI6i%g`TO|%63(jNEE^1*F$*3zK=^!MF+dwEJ#GUlsd80p=QO~!?^>;29|YL`62Q%i zt=1oJwOTjd1{nPo;8V9Z-@A+v{|)sQd%6Q}0_=Jo;DakYUF8*k4=(`R_YLM7;p?!R z9I-5bU)}|nI0JCcHGuJ_Bi?X~jl(-%0hE!exvK!Ly$8HWg)-I`3h#FGOp7j4~@bc%4L8hTQ2-6pAH5O(=3Ana26Ds_C+#E3%+u zbv3EWGFrzj_jbgPxNj_zV8WU@+^iGh91VpEt=eur{_{tl#0A^T$Dd3U!Izr0>dN~Zz;^j<2WvK_UF`##V))XB=~LtY<8-MGLd} zINPsodWLQ4TuiEFI+IbdVkVukM9oU2#9W1@#foYeNjXI`ik0qzD!4&NT!S(w1W(wA zqgh&p=2TfsB~4A#vW6y-6g5RtlXGb$XXbKQtq`YT)&9jXQTZ1fa8)@**71c9Fmofwe|0a>Pzdx#Yc;WwsRrT7AAXKY7r zZQA_1aF)L?NBC*oxY&s>YDH<`}MGrhHY~b&bqgCV)xxx(PuRq~n z6vT%)30?EXB4rUm#za{W75T4>No|iM_TK2YJSF{&lqqh|_oBghkq%#A_&$`nFT^%! zxPv=c+QGP!O9;YnkxlVCq-#jmrWn|i@i4oFbZv@(O&Jff>p!DwymzIdF8&N^;Drg` zuFtStX|eJ-Yk2u$cWM>J*ncUpOYAm=pX_#b3GiANyTop3F8-Em|J8DYS+KHgj;v5W zy2aI&TpLxpnHuKs4!;5vI%7r+C4O-`rTwfiCIzSP* zx;hXQBQmxHbt}H9hJM^W1H*XSHUn!n)>?zmw&GXT+I8e=0NR#tbrrc9fVL%EwblUH L`ro%zxbyW->#0jd literal 0 HcmV?d00001 diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 8b06ce2b1..e3c1f447a 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -112,6 +112,11 @@ def test_rgba(): assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png") +def test_negative_top_left_layer() -> None: + with Image.open("Tests/images/negative_top_left_layer.psd") as im: + assert im.layers[0][2] == (-50, -50, 50, 50) + + def test_layer_skip(): with Image.open("Tests/images/five_channels.psd") as im: assert im.n_frames == 1 diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 9f7bf7829..d29bcf997 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -24,6 +24,7 @@ from ._binary import i8 from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import si16be as si16 +from ._binary import si32be as si32 MODES = { # (photoshop mode, bits) -> (pil mode, required channels) @@ -177,10 +178,10 @@ def _layerinfo(fp, ct_bytes): for _ in range(abs(ct)): # bounding box - y0 = i32(read(4)) - x0 = i32(read(4)) - y1 = i32(read(4)) - x1 = i32(read(4)) + y0 = si32(read(4)) + x0 = si32(read(4)) + y1 = si32(read(4)) + x1 = si32(read(4)) # image info mode = [] diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index 0a07e8d0e..4594ccce3 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -77,6 +77,16 @@ def si32le(c: bytes, o: int = 0) -> int: return unpack_from(" int: + """ + Converts a 4-bytes (32 bits) string to a signed integer, big endian. + + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string + """ + return unpack_from(">i", c, o)[0] + + def i16be(c: bytes, o: int = 0) -> int: return unpack_from(">H", c, o)[0] From 2fbd7dda839fb1cb4d9ba0a1e9abecf34e4786ec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Jan 2024 21:10:02 +1100 Subject: [PATCH 008/688] Use consistent arguments for load_seek --- src/PIL/EpsImagePlugin.py | 2 +- src/PIL/IcoImagePlugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index d2e60aa07..690fb3586 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -408,7 +408,7 @@ class EpsImageFile(ImageFile.ImageFile): self.tile = [] return Image.Image.load(self) - def load_seek(self, *args, **kwargs): + def load_seek(self, pos): # we can't incrementally load, so force ImageFile.parser to # use our custom load method by defining this method. pass diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 1b22f8645..2b21d957f 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -339,7 +339,7 @@ class IcoImageFile(ImageFile.ImageFile): self.size = im.size - def load_seek(self): + def load_seek(self, pos): # Flag the ImageFile.Parser so that it # just does all the decode at the end. pass From 543b5a674160c86d8b128e0af6c706a2e96c16d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Jan 2024 21:11:16 +1100 Subject: [PATCH 009/688] Use consistent arguments for load_read --- src/PIL/ImageFile.py | 2 +- src/PIL/XpmImagePlugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 0923979af..17b1b3203 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -328,7 +328,7 @@ class ImageFile(Image.Image): # pass # may be defined for blocked formats (e.g. PNG) - # def load_read(self, bytes): + # def load_read(self, read_bytes): # pass def _seek_check(self, frame): diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index bf73c9bef..3125f8d52 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -103,7 +103,7 @@ class XpmImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] - def load_read(self, bytes): + def load_read(self, read_bytes): # # load all image data in one chunk From 44e77a22b572a3ffe158a8b4b63c218ae549b8ac Mon Sep 17 00:00:00 2001 From: FangFuxin <38530078+lajiyuan@users.noreply.github.com> Date: Fri, 12 Jan 2024 15:46:49 +0800 Subject: [PATCH 010/688] Fix png image plugin load_end func handle truncated file. --- Tests/images/end_trunc_file.png | Bin 0 -> 30339 bytes Tests/test_file_png.py | 9 +++++++++ src/PIL/PngImagePlugin.py | 8 +++++++- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 Tests/images/end_trunc_file.png diff --git a/Tests/images/end_trunc_file.png b/Tests/images/end_trunc_file.png new file mode 100644 index 0000000000000000000000000000000000000000..5e88c5e4fa2a6ac010219a2469b83efa21102f0e GIT binary patch literal 30339 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 mOz9jWq7NYmRv5v_Ot0CrAEL_t(ca^tlC literal 0 HcmV?d00001 diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ff3862110..9b6a4c5e6 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -777,6 +777,15 @@ class TestFilePng: mystdout = mystdout.buffer with Image.open(mystdout) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) + + def test_end_truncated_file(self): + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + with Image.open("Tests/images/end_trunc_file.png") as im: + assert_image_equal_tofile(im, "Tests/images/end_trunc_file.png") + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False + @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 823f12492..1248fb785 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -981,7 +981,13 @@ class PngImageFile(ImageFile.ImageFile): except EOFError: if cid == b"fdAT": length -= 4 - ImageFile._safe_read(self.fp, length) + try: + ImageFile._safe_read(self.fp, length) + except OSError as e: + if ImageFile.LOAD_TRUNCATED_IMAGES: + break + else: + raise e except AttributeError: logger.debug("%r %s %s (unknown)", cid, pos, length) s = ImageFile._safe_read(self.fp, length) From b2711c3e8b019d0d069d58069912abd4ecb99e61 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Jan 2024 07:36:57 +0000 Subject: [PATCH 011/688] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_png.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 9b6a4c5e6..3c285b077 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -777,7 +777,7 @@ class TestFilePng: mystdout = mystdout.buffer with Image.open(mystdout) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) - + def test_end_truncated_file(self): ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -787,7 +787,6 @@ class TestFilePng: ImageFile.LOAD_TRUNCATED_IMAGES = False - @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") @skip_unless_feature("zlib") class TestTruncatedPngPLeaks(PillowLeakTestCase): From fe7b6d9e80ae8ee65d2b4e615971a00690b876fe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jan 2024 18:39:33 +1100 Subject: [PATCH 012/688] Corrected expected image path --- Tests/test_file_png.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 3c285b077..aa2aac906 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -782,7 +782,7 @@ class TestFilePng: ImageFile.LOAD_TRUNCATED_IMAGES = True try: with Image.open("Tests/images/end_trunc_file.png") as im: - assert_image_equal_tofile(im, "Tests/images/end_trunc_file.png") + assert_image_equal_tofile(im, "Tests/images/hopper.png") finally: ImageFile.LOAD_TRUNCATED_IMAGES = False From 62e6d62518f21333fbde364d7b7a57e25d39061b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jan 2024 18:49:25 +1100 Subject: [PATCH 013/688] Test error is raised without LOAD_TRUNCATED_IMAGES --- .../{end_trunc_file.png => truncated_end_chunk.png} | Bin Tests/test_file_png.py | 8 ++++++-- 2 files changed, 6 insertions(+), 2 deletions(-) rename Tests/images/{end_trunc_file.png => truncated_end_chunk.png} (100%) diff --git a/Tests/images/end_trunc_file.png b/Tests/images/truncated_end_chunk.png similarity index 100% rename from Tests/images/end_trunc_file.png rename to Tests/images/truncated_end_chunk.png diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index aa2aac906..0884ddcc3 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -778,10 +778,14 @@ class TestFilePng: with Image.open(mystdout) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) - def test_end_truncated_file(self): + def test_truncated_end_chunk(self): + with Image.open("Tests/images/truncated_end_chunk.png") as im: + with pytest.raises(OSError): + im.load() + ImageFile.LOAD_TRUNCATED_IMAGES = True try: - with Image.open("Tests/images/end_trunc_file.png") as im: + with Image.open("Tests/images/truncated_end_chunk.png") as im: assert_image_equal_tofile(im, "Tests/images/hopper.png") finally: ImageFile.LOAD_TRUNCATED_IMAGES = False From 74af933a9f0441e1285c398675dc3eca1de5406c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Sat, 20 Jan 2024 10:08:14 +0100 Subject: [PATCH 014/688] Link to stable setuptools documentation Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- winbuild/build.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build.rst b/winbuild/build.rst index 26d0da0a3..f40982cd5 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -97,7 +97,7 @@ To build a wheel instead, run:: winbuild\build\build_env.cmd python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . -.. _editable mode: https://setuptools.pypa.io/en/latest/userguide/development_mode.html +.. _editable mode: https://setuptools.pypa.io/en/stable/userguide/development_mode.html Testing Pillow -------------- From 420150f0e251c9519a5aea1c24546a44d489828c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Sat, 20 Jan 2024 14:56:20 +0100 Subject: [PATCH 015/688] Update winbuild/build.rst --- winbuild/build.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build.rst b/winbuild/build.rst index f40982cd5..c980d9c75 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -92,7 +92,7 @@ You can also install Pillow in `editable mode`_:: winbuild\build\build_env.cmd python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor -e . -To build a wheel instead, run:: +To build a binary wheel instead, run:: winbuild\build\build_env.cmd python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . From 474411b52a3619f01bb626ce8e56100094886d1a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jan 2024 08:52:14 +1100 Subject: [PATCH 016/688] Updated zlib to 1.3.1 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 3ec314873..a30d0468c 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -29,7 +29,7 @@ else GIFLIB_VERSION=5.2.1 fi if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then - ZLIB_VERSION=1.3 + ZLIB_VERSION=1.3.1 else ZLIB_VERSION=1.2.8 fi diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index df33ea493..3117065e8 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -143,9 +143,9 @@ DEPS = { "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": "https://zlib.net/zlib13.zip", - "filename": "zlib13.zip", - "dir": "zlib-1.3", + "url": "https://zlib.net/zlib131.zip", + "filename": "zlib131.zip", + "dir": "zlib-1.3.1", "license": "README", "license_pattern": "Copyright notice:\n\n(.+)$", "build": [ From 8caae8739f16cfddade0848ba686b1a40a0c10b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jan 2024 18:51:41 +1100 Subject: [PATCH 017/688] Restored testing of non-TrueType default font --- Tests/test_font_leaks.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 4e29a856b..d29e9bcfc 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,8 +1,10 @@ from __future__ import annotations -from PIL import Image, ImageDraw, ImageFont +from PIL import Image, ImageDraw, ImageFont, _util -from .helper import PillowLeakTestCase, skip_unless_feature +from .helper import PillowLeakTestCase, features, skip_unless_feature + +original_core = ImageFont.core class TestTTypeFontLeak(PillowLeakTestCase): @@ -31,5 +33,9 @@ class TestDefaultFontLeak(TestTTypeFontLeak): mem_limit = 1024 # k def test_leak(self): + if features.check_module("freetype2"): + ImageFont.core = _util.DeferredError(ImportError) default_font = ImageFont.load_default() + ImageFont.core = original_core + self._test_font(default_font) From e2d1b2663d2b392089bfe59c7a17d2afbf74ce7f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 24 Jan 2024 08:12:06 +1100 Subject: [PATCH 018/688] Restored original state using finally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- Tests/test_font_leaks.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index d29e9bcfc..5eea0c34d 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -35,7 +35,9 @@ class TestDefaultFontLeak(TestTTypeFontLeak): def test_leak(self): if features.check_module("freetype2"): ImageFont.core = _util.DeferredError(ImportError) - default_font = ImageFont.load_default() - ImageFont.core = original_core + try: + default_font = ImageFont.load_default() + finally: + ImageFont.core = original_core self._test_font(default_font) From cf9e6ff2563be6d0458856e2aa35e54973709752 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Jan 2024 08:10:07 +1100 Subject: [PATCH 019/688] Updated libjpeg-turbo to 3.0.2 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 3ec314873..9013e8ae2 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -18,7 +18,7 @@ ARCHIVE_SDIR=pillow-depends-main FREETYPE_VERSION=2.13.2 HARFBUZZ_VERSION=8.3.0 LIBPNG_VERSION=1.6.40 -JPEGTURBO_VERSION=3.0.1 +JPEGTURBO_VERSION=3.0.2 OPENJPEG_VERSION=2.5.0 XZ_VERSION=5.4.5 TIFF_VERSION=4.6.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index df33ea493..92cbcdf7a 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,9 +113,9 @@ ARCHITECTURES = { DEPS = { "libjpeg": { "url": SF_PROJECTS - + "/libjpeg-turbo/files/3.0.1/libjpeg-turbo-3.0.1.tar.gz/download", - "filename": "libjpeg-turbo-3.0.1.tar.gz", - "dir": "libjpeg-turbo-3.0.1", + + "/libjpeg-turbo/files/3.0.2/libjpeg-turbo-3.0.2.tar.gz/download", + "filename": "libjpeg-turbo-3.0.2.tar.gz", + "dir": "libjpeg-turbo-3.0.2", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" From 5721577e4e6f5d64a5c50ab8732eea9e5a437120 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Jan 2024 20:20:53 +1100 Subject: [PATCH 020/688] Stop reading EPS at EOF marker --- Tests/test_file_eps.py | 8 ++++++++ src/PIL/EpsImagePlugin.py | 8 ++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 8def9a435..5ba3a0c14 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -436,3 +436,11 @@ def test_eof_before_bounding_box(): with pytest.raises(OSError): with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"): pass + + +def test_invalid_data_after_eof() -> None: + with open("Tests/images/illuCS6_preview.eps", "rb") as f: + img_bytes = io.BytesIO(f.read() + b"\r\n%" + (b" " * 255)) + + with Image.open(img_bytes) as img: + assert img.mode == "RGB" diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index d2e60aa07..94b163bc4 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -356,14 +356,10 @@ class EpsImageFile(ImageFile.ImageFile): self._size = columns, rows return + elif bytes_mv[:5] == b"%%EOF": + break elif trailer_reached and reading_trailer_comments: # Load EPS trailer - - # if this line starts with "%%EOF", - # then we've reached the end of the file - if bytes_mv[:5] == b"%%EOF": - break - s = str(bytes_mv[:bytes_read], "latin-1") _read_comment(s) elif bytes_mv[:9] == b"%%Trailer": From b8769d1cf5782f3db934b861ad764dc9b1466fb4 Mon Sep 17 00:00:00 2001 From: FangFuxin <38530078+lajiyuan@users.noreply.github.com> Date: Wed, 31 Jan 2024 21:02:50 +0800 Subject: [PATCH 021/688] Update Tests/test_file_png.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_png.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 0884ddcc3..ec8794b30 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -778,7 +778,7 @@ class TestFilePng: with Image.open(mystdout) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) - def test_truncated_end_chunk(self): + def test_truncated_end_chunk(self) -> None: with Image.open("Tests/images/truncated_end_chunk.png") as im: with pytest.raises(OSError): im.load() From dfb48ff297aa2b227a98f20ff0ae5a0009644ad3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Feb 2024 19:16:15 +1100 Subject: [PATCH 022/688] Match mask size to pasted image size --- Tests/test_file_gif.py | 15 +++++++++++++++ src/PIL/GifImagePlugin.py | 4 +--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 3f550fd11..263c897ef 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1105,6 +1105,21 @@ def test_append_images(tmp_path: Path) -> None: assert reread.n_frames == 10 +def test_append_different_size_image(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im = Image.new("RGB", (100, 100)) + bigger_im = Image.new("RGB", (200, 200), "#f00") + + im.save(out, save_all=True, append_images=[bigger_im]) + + with Image.open(out) as reread: + assert reread.size == (100, 100) + + reread.seek(1) + assert reread.size == (100, 100) + + def test_transparent_optimize(tmp_path: Path) -> None: # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses # transparency. diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 57d87078b..935b95ca8 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -649,9 +649,7 @@ def _write_multiple_frames(im, fp, palette): if "transparency" in encoderinfo: # When the delta is zero, fill the image with transparency diff_frame = im_frame.copy() - fill = Image.new( - "P", diff_frame.size, encoderinfo["transparency"] - ) + fill = Image.new("P", delta.size, encoderinfo["transparency"]) if delta.mode == "RGBA": r, g, b, a = delta.split() mask = ImageMath.eval( From 469db5114cf317ea128bd8c4b508eed537b7ce9e Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Tue, 6 Feb 2024 15:41:08 -0500 Subject: [PATCH 023/688] Release GIL while calling WebPAnimDecoderGetNext --- src/_webp.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_webp.c b/src/_webp.c index a1b4dbc1a..4e7d41f11 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -450,12 +450,16 @@ _anim_decoder_get_next(PyObject *self) { int timestamp; PyObject *bytes; PyObject *ret; + ImagingSectionCookie cookie; WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; + ImagingSectionEnter(&cookie); if (!WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp)) { + ImagingSectionLeave(&cookie); PyErr_SetString(PyExc_OSError, "failed to read next frame"); return NULL; } + ImagingSectionLeave(&cookie); bytes = PyBytes_FromStringAndSize( (char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height); From c93b23239d4cbe8b8c6d4d6c04db35763a25db62 Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Wed, 7 Feb 2024 20:20:27 -0500 Subject: [PATCH 024/688] Update src/_webp.c Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/_webp.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index 4e7d41f11..927d8dc3f 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -448,18 +448,19 @@ PyObject * _anim_decoder_get_next(PyObject *self) { uint8_t *buf; int timestamp; + int ok; PyObject *bytes; PyObject *ret; ImagingSectionCookie cookie; WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; ImagingSectionEnter(&cookie); - if (!WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp)) { - ImagingSectionLeave(&cookie); + ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp) + ImagingSectionLeave(&cookie); + if (!ok) { PyErr_SetString(PyExc_OSError, "failed to read next frame"); return NULL; } - ImagingSectionLeave(&cookie); bytes = PyBytes_FromStringAndSize( (char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height); From cb39b1c89e71f67ce4dacd41cebf723ff86306dd Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 8 Feb 2024 12:29:06 +1100 Subject: [PATCH 025/688] Corrected syntax --- src/_webp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_webp.c b/src/_webp.c index 927d8dc3f..47592547c 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -455,7 +455,7 @@ _anim_decoder_get_next(PyObject *self) { WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; ImagingSectionEnter(&cookie); - ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp) + ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp); ImagingSectionLeave(&cookie); if (!ok) { PyErr_SetString(PyExc_OSError, "failed to read next frame"); From e6a521130e975f89529442597735a6a48cc3685f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 Feb 2024 19:47:09 +1100 Subject: [PATCH 026/688] If previous disposal was 2, do not fill identical pixels --- Tests/test_file_gif.py | 3 +++ src/PIL/GifImagePlugin.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index db9d3586c..0399c6b67 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -647,6 +647,9 @@ def test_dispose2_palette(tmp_path: Path) -> None: # Center remains red every frame assert rgb_img.getpixel((50, 50)) == circle + # Check that frame transparency wasn't added unnecessarily + assert img._frame_transparency is None + def test_dispose2_diff(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index dc842d7a3..73a5487d9 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -638,7 +638,11 @@ def _write_multiple_frames(im, fp, palette): background_im = Image.new("P", im_frame.size, background) background_im.putpalette(im_frames[0]["im"].palette) delta, bbox = _getbbox(background_im, im_frame) - if encoderinfo.get("optimize") and im_frame.mode != "1": + if ( + encoderinfo.get("optimize") + and im_frames[-1]["encoderinfo"].get("disposal") != 2 + and im_frame.mode != "1" + ): if "transparency" not in encoderinfo: try: encoderinfo["transparency"] = ( From 373c62e5cbcd27a4e497e61b44cda89911f38807 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 19:47:50 +1100 Subject: [PATCH 027/688] Use subprocess with CREATE_NO_WINDOW flag in WindowsViewer --- src/PIL/ImageShow.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index c03122c11..4a801e5b0 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -138,6 +138,17 @@ class WindowsViewer(Viewer): f'&& del /f "{file}"' ) + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + subprocess.Popen( + self.get_command(path, **options), + shell=True, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW"), + ) # nosec + return 1 + if sys.platform == "win32": register(WindowsViewer) From 430f50606e2fc5620c6605cd43eb360be6bbd655 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 21:33:32 +1100 Subject: [PATCH 028/688] Current delta is determined by previous disposal --- Tests/test_file_gif.py | 19 +++++++++++++++++++ src/PIL/GifImagePlugin.py | 10 +++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 0399c6b67..6527d90de 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -737,6 +737,25 @@ def test_dispose2_background_frame(tmp_path: Path) -> None: assert im.n_frames == 3 +def test_dispose2_previous_frame(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im = Image.new("P", (100, 100)) + im.info["transparency"] = 0 + d = ImageDraw.Draw(im) + d.rectangle([(0, 0), (100, 50)], 1) + im.putpalette((0, 0, 0, 255, 0, 0)) + + im2 = Image.new("P", (100, 100)) + im2.putpalette((0, 0, 0)) + + im.save(out, save_all=True, append_images=[im2], disposal=[0, 2]) + + with Image.open(out) as im: + im.seek(1) + assert im.getpixel((0, 0)) == (0, 0, 0, 255) + + def test_transparency_in_second_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/different_transparency.gif") as im: diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 73a5487d9..9368dd7e7 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -629,7 +629,7 @@ def _write_multiple_frames(im, fp, palette): "duration" ] continue - if encoderinfo.get("disposal") == 2: + if im_frames[-1]["encoderinfo"].get("disposal") == 2: if background_im is None: color = im.encoderinfo.get( "transparency", im.info.get("transparency", (0, 0, 0)) @@ -637,12 +637,8 @@ def _write_multiple_frames(im, fp, palette): background = _get_background(im_frame, color) background_im = Image.new("P", im_frame.size, background) background_im.putpalette(im_frames[0]["im"].palette) - delta, bbox = _getbbox(background_im, im_frame) - if ( - encoderinfo.get("optimize") - and im_frames[-1]["encoderinfo"].get("disposal") != 2 - and im_frame.mode != "1" - ): + bbox = _getbbox(background_im, im_frame)[1] + elif encoderinfo.get("optimize") and im_frame.mode != "1": if "transparency" not in encoderinfo: try: encoderinfo["transparency"] = ( From 47eaf0937f8e4f10fce8473007aba449c0c280f7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Feb 2024 22:26:23 +1100 Subject: [PATCH 029/688] Use IO[bytes] in type hints --- src/PIL/ImageFile.py | 4 ++-- src/PIL/MspImagePlugin.py | 3 ++- src/PIL/PcxImagePlugin.py | 3 ++- src/PIL/PpmImagePlugin.py | 4 ++-- src/PIL/SgiImagePlugin.py | 4 ++-- src/PIL/TgaImagePlugin.py | 4 ++-- src/PIL/XbmImagePlugin.py | 4 ++-- 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 487f53efe..e929b665e 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -32,7 +32,7 @@ import io import itertools import struct import sys -from typing import Any, NamedTuple +from typing import IO, Any, NamedTuple from . import Image from ._deprecate import deprecate @@ -616,7 +616,7 @@ class PyCodecState: class PyCodec: - fd: io.BytesIO | None + fd: IO[bytes] | None def __init__(self, mode, *args): self.im = None diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index bb7e466a7..65cc70624 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -26,6 +26,7 @@ from __future__ import annotations import io import struct +from typing import IO from . import Image, ImageFile from ._binary import i16le as i16 @@ -163,7 +164,7 @@ Image.register_decoder("MSP", MspDecoder) # write MSP files (uncompressed only) -def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as MSP" raise OSError(msg) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 3e0968a83..026bfd9a0 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -28,6 +28,7 @@ from __future__ import annotations import io import logging +from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 @@ -143,7 +144,7 @@ SAVE = { } -def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 3e45ba95c..6ac7a9bbc 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -16,7 +16,7 @@ from __future__ import annotations import math -from io import BytesIO +from typing import IO from . import Image, ImageFile from ._binary import i16be as i16 @@ -324,7 +324,7 @@ class PpmDecoder(ImageFile.PyDecoder): # -------------------------------------------------------------------- -def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode == "1": rawmode, head = "1;I", b"P4" elif im.mode == "L": diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index ccf661ff1..7bd84ebd4 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -24,7 +24,7 @@ from __future__ import annotations import os import struct -from io import BytesIO +from typing import IO from . import Image, ImageFile from ._binary import i16be as i16 @@ -125,7 +125,7 @@ class SgiImageFile(ImageFile.ImageFile): ] -def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode not in {"RGB", "RGBA", "L"}: msg = "Unsupported SGI image mode" raise ValueError(msg) diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 584932d2c..828701342 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -18,7 +18,7 @@ from __future__ import annotations import warnings -from io import BytesIO +from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 @@ -175,7 +175,7 @@ SAVE = { } -def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 0291e2858..eee727436 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -21,7 +21,7 @@ from __future__ import annotations import re -from io import BytesIO +from typing import IO from . import Image, ImageFile @@ -70,7 +70,7 @@ class XbmImageFile(ImageFile.ImageFile): self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] -def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as XBM" raise OSError(msg) From 63987b7abaf6907a8985b867be154debfef0ec1b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Feb 2024 06:55:13 +1100 Subject: [PATCH 030/688] Set mode to L if palette is missing --- Tests/test_file_tga.py | 5 +++++ src/PIL/TgaImagePlugin.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index bd8e522c7..87a59ff3d 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -133,6 +133,11 @@ def test_small_palette(tmp_path: Path) -> None: assert reloaded.getpalette() == colors +def test_missing_palette() -> None: + with Image.open("Tests/images/dilation4.lut") as im: + assert im.mode == "L" + + def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper("PA") out = str(tmp_path / "temp.tga") diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 584932d2c..5d4827525 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -85,7 +85,7 @@ class TgaImageFile(ImageFile.ImageFile): elif depth == 16: self._mode = "LA" elif imagetype in (1, 9): - self._mode = "P" + self._mode = "P" if colormaptype else "L" elif imagetype in (2, 10): self._mode = "RGB" if depth == 32: From 818500b329555969cdb852c81d667cc70faaed94 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Feb 2024 07:10:44 +1100 Subject: [PATCH 031/688] Raise an error if map depth is unknown --- Tests/images/p_8.tga | Bin 0 -> 18 bytes Tests/test_file_tga.py | 7 ++++++- src/PIL/TgaImagePlugin.py | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 Tests/images/p_8.tga diff --git a/Tests/images/p_8.tga b/Tests/images/p_8.tga new file mode 100644 index 0000000000000000000000000000000000000000..73759a2822419fd38134be80acf7918c0a437e43 GIT binary patch literal 18 Wcmb1RWMp7qVB`Q23=KfY!2kdU!vQh? literal 0 HcmV?d00001 diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 87a59ff3d..75c592da2 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -7,7 +7,7 @@ from pathlib import Path import pytest -from PIL import Image +from PIL import Image, UnidentifiedImageError from .helper import assert_image_equal, assert_image_equal_tofile, hopper @@ -65,6 +65,11 @@ def test_sanity(mode, tmp_path: Path) -> None: roundtrip(original_im) +def test_palette_depth_8(tmp_path: Path) -> None: + with pytest.raises(UnidentifiedImageError): + Image.open("Tests/images/p_8.tga") + + def test_palette_depth_16(tmp_path: Path) -> None: with Image.open("Tests/images/p_16.tga") as im: assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png") diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 5d4827525..b6748c25e 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -128,6 +128,9 @@ class TgaImageFile(ImageFile.ImageFile): self.palette = ImagePalette.raw( "BGRA", b"\0" * 4 * start + self.fp.read(4 * size) ) + else: + msg = "unknown TGA map depth" + raise SyntaxError(msg) # setup tile descriptor try: From 21e5d5d082dfe47fda2779e10d446e6593c04dd8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Feb 2024 09:17:22 +1100 Subject: [PATCH 032/688] Use palette when loading --- Tests/test_file_ico.py | 11 +++++++++++ src/PIL/IcoImagePlugin.py | 2 ++ 2 files changed, 13 insertions(+) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 65f090931..e75561f69 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -38,6 +38,17 @@ def test_black_and_white() -> None: assert im.size == (16, 16) +def test_palette(tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.ico") + + im = Image.new("P", (16, 16)) + im.save(temp_file) + + with Image.open(temp_file) as reloaded: + assert reloaded.mode == "P" + assert reloaded.palette is not None + + def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 1b22f8645..b558fdf34 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -329,6 +329,8 @@ class IcoImageFile(ImageFile.ImageFile): self.im = im.im self.pyaccess = None self._mode = im.mode + if im.palette: + self.palette = im.palette if im.size != self.size: warnings.warn("Image was not the expected size") From 3199c0ea40c041d41fe2499c86893a7e795f0929 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Feb 2024 20:20:42 +1100 Subject: [PATCH 033/688] Decoder and encoders subclass PyDecoder and PyEncoder --- Tests/test_file_jpeg.py | 8 +----- Tests/test_image.py | 16 ++++------- Tests/test_imagefile.py | 64 ++++++++++++++++++++--------------------- src/PIL/Image.py | 14 ++++----- 4 files changed, 45 insertions(+), 57 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 654242148..4858d92e6 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -986,13 +986,7 @@ class TestFileJpeg: def decode(self, buffer: bytes) -> tuple[int, int]: return 0, 0 - decoder = InfiniteMockPyDecoder(None) - - def closure(mode: str, *args) -> InfiniteMockPyDecoder: - decoder.__init__(mode, *args) - return decoder - - Image.register_decoder("INFINITE", closure) + Image.register_decoder("INFINITE", InfiniteMockPyDecoder) with Image.open(TEST_FILE) as im: im.tile = [ diff --git a/Tests/test_image.py b/Tests/test_image.py index 4c04e0da4..aae51eaa4 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -16,6 +16,7 @@ from PIL import ( ExifTags, Image, ImageDraw, + ImageFile, ImagePalette, UnidentifiedImageError, features, @@ -1038,25 +1039,20 @@ class TestImage: assert im.fp is None -class MockEncoder: - args: tuple[str, ...] - - -def mock_encode(*args: str) -> MockEncoder: - encoder = MockEncoder() - encoder.args = args - return encoder +class MockEncoder(ImageFile.PyEncoder): + pass class TestRegistry: def test_encode_registry(self) -> None: - Image.register_encoder("MOCK", mock_encode) + Image.register_encoder("MOCK", MockEncoder) assert "MOCK" in Image.ENCODERS enc = Image._getencoder("RGB", "MOCK", ("args",), extra=("extra",)) assert isinstance(enc, MockEncoder) - assert enc.args == ("RGB", "args", "extra") + assert enc.mode == "RGB" + assert enc.args == ("args", "extra") def test_encode_registry_fail(self) -> None: with pytest.raises(OSError): diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 491409781..cf251c9ce 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from typing import Any import pytest @@ -201,12 +202,22 @@ class TestImageFile: class MockPyDecoder(ImageFile.PyDecoder): + def __init__(self, mode: str, *args: Any) -> None: + MockPyDecoder.last = self + + super().__init__(mode, *args) + def decode(self, buffer): # eof return -1, 0 class MockPyEncoder(ImageFile.PyEncoder): + def __init__(self, mode: str, *args: Any) -> None: + MockPyEncoder.last = self + + super().__init__(mode, *args) + def encode(self, buffer): return 1, 1, b"" @@ -228,19 +239,8 @@ class MockImageFile(ImageFile.ImageFile): class CodecsTest: @classmethod def setup_class(cls) -> None: - cls.decoder = MockPyDecoder(None) - cls.encoder = MockPyEncoder(None) - - def decoder_closure(mode, *args): - cls.decoder.__init__(mode, *args) - return cls.decoder - - def encoder_closure(mode, *args): - cls.encoder.__init__(mode, *args) - return cls.encoder - - Image.register_decoder("MOCK", decoder_closure) - Image.register_encoder("MOCK", encoder_closure) + Image.register_decoder("MOCK", MockPyDecoder) + Image.register_encoder("MOCK", MockPyEncoder) class TestPyDecoder(CodecsTest): @@ -251,13 +251,13 @@ class TestPyDecoder(CodecsTest): im.load() - assert self.decoder.state.xoff == xoff - assert self.decoder.state.yoff == yoff - assert self.decoder.state.xsize == xsize - assert self.decoder.state.ysize == ysize + assert MockPyDecoder.last.state.xoff == xoff + assert MockPyDecoder.last.state.yoff == yoff + assert MockPyDecoder.last.state.xsize == xsize + assert MockPyDecoder.last.state.ysize == ysize with pytest.raises(ValueError): - self.decoder.set_as_raw(b"\x00") + MockPyDecoder.last.set_as_raw(b"\x00") def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -267,10 +267,10 @@ class TestPyDecoder(CodecsTest): im.load() - assert self.decoder.state.xoff == 0 - assert self.decoder.state.yoff == 0 - assert self.decoder.state.xsize == 200 - assert self.decoder.state.ysize == 200 + assert MockPyDecoder.last.state.xoff == 0 + assert MockPyDecoder.last.state.yoff == 0 + assert MockPyDecoder.last.state.xsize == 200 + assert MockPyDecoder.last.state.ysize == 200 def test_negsize(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -315,10 +315,10 @@ class TestPyEncoder(CodecsTest): im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] ) - assert self.encoder.state.xoff == xoff - assert self.encoder.state.yoff == yoff - assert self.encoder.state.xsize == xsize - assert self.encoder.state.ysize == ysize + assert MockPyEncoder.last.state.xoff == xoff + assert MockPyEncoder.last.state.yoff == yoff + assert MockPyEncoder.last.state.xsize == xsize + assert MockPyEncoder.last.state.ysize == ysize def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -329,10 +329,10 @@ class TestPyEncoder(CodecsTest): fp = BytesIO() ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) - assert self.encoder.state.xoff == 0 - assert self.encoder.state.yoff == 0 - assert self.encoder.state.xsize == 200 - assert self.encoder.state.ysize == 200 + assert MockPyEncoder.last.state.xoff == 0 + assert MockPyEncoder.last.state.yoff == 0 + assert MockPyEncoder.last.state.xsize == 200 + assert MockPyEncoder.last.state.ysize == 200 def test_negsize(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -340,12 +340,12 @@ class TestPyEncoder(CodecsTest): im = MockImageFile(buf) fp = BytesIO() - self.encoder.cleanup_called = False + MockPyEncoder.last = None with pytest.raises(ValueError): ImageFile._save( im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] ) - assert self.encoder.cleanup_called + assert MockPyEncoder.last.cleanup_called with pytest.raises(ValueError): ImageFile._save( diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a770488b7..eba30537f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -229,8 +229,8 @@ MIME: dict[str, str] = {} SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} EXTENSION: dict[str, str] = {} -DECODERS: dict[str, object] = {} -ENCODERS: dict[str, object] = {} +DECODERS: dict[str, type[ImageFile.PyDecoder]] = {} +ENCODERS: dict[str, type[ImageFile.PyEncoder]] = {} # -------------------------------------------------------------------- # Modes @@ -3524,28 +3524,26 @@ def registered_extensions(): return EXTENSION -def register_decoder(name: str, decoder) -> None: +def register_decoder(name: str, decoder: type[ImageFile.PyDecoder]) -> None: """ Registers an image decoder. This function should not be used in application code. :param name: The name of the decoder - :param decoder: A callable(mode, args) that returns an - ImageFile.PyDecoder object + :param decoder: An ImageFile.PyDecoder object .. versionadded:: 4.1.0 """ DECODERS[name] = decoder -def register_encoder(name, encoder): +def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None: """ Registers an image encoder. This function should not be used in application code. :param name: The name of the encoder - :param encoder: A callable(mode, args) that returns an - ImageFile.PyEncoder object + :param encoder: An ImageFile.PyEncoder object .. versionadded:: 4.1.0 """ From 26e0f6df56c1289d52b156642c6ee1197d2bf69b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 10:18:54 +1100 Subject: [PATCH 034/688] Pin Python 3.13 on Windows to a3 --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 75fccf795..79a2e60b2 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -32,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-alpha.3"] timeout-minutes: 30 From 5c858d75e4a58e895bed56c2ff6c0bae245c88cf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 10:45:52 +1100 Subject: [PATCH 035/688] Added type hints --- src/PIL/Image.py | 33 ++++---- src/PIL/ImageColor.py | 2 +- src/PIL/ImageOps.py | 171 +++++++++++++++++++++++++++------------- src/PIL/ImagePalette.py | 7 +- 4 files changed, 139 insertions(+), 74 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a770488b7..ba81a22c7 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1430,7 +1430,7 @@ class Image: root = ElementTree.fromstring(xmp_tags) return {get_name(root.tag): get_value(root)} - def getexif(self): + def getexif(self) -> Exif: """ Gets EXIF data from the image. @@ -1438,7 +1438,6 @@ class Image: """ if self._exif is None: self._exif = Exif() - self._exif._loaded = False elif self._exif._loaded: return self._exif self._exif._loaded = True @@ -1525,7 +1524,7 @@ class Image: self.load() return self.im.ptr - def getpalette(self, rawmode="RGB"): + def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None: """ Returns the image palette as a list. @@ -1615,7 +1614,7 @@ class Image: x, y = self.im.getprojection() return list(x), list(y) - def histogram(self, mask=None, extrema=None): + def histogram(self, mask=None, extrema=None) -> list[int]: """ Returns a histogram for the image. The histogram is returned as a list of pixel counts, one for each pixel value in the source @@ -1804,7 +1803,7 @@ class Image: result = alpha_composite(background, overlay) self.paste(result, box) - def point(self, lut, mode=None): + def point(self, lut, mode: str | None = None) -> Image: """ Maps this image through a lookup table or function. @@ -1928,7 +1927,7 @@ class Image: self.im.putdata(data, scale, offset) - def putpalette(self, data, rawmode="RGB"): + def putpalette(self, data, rawmode="RGB") -> None: """ Attaches a palette to this image. The image must be a "P", "PA", "L" or "LA" image. @@ -2108,7 +2107,7 @@ class Image: min(self.size[1], math.ceil(box[3] + support_y)), ) - def resize(self, size, resample=None, box=None, reducing_gap=None): + def resize(self, size, resample=None, box=None, reducing_gap=None) -> Image: """ Returns a resized copy of this image. @@ -2200,10 +2199,11 @@ class Image: if factor_x > 1 or factor_y > 1: reduce_box = self._get_safe_box(size, resample, box) factor = (factor_x, factor_y) - if callable(self.reduce): - self = self.reduce(factor, box=reduce_box) - else: - self = Image.reduce(self, factor, box=reduce_box) + self = ( + self.reduce(factor, box=reduce_box) + if callable(self.reduce) + else Image.reduce(self, factor, box=reduce_box) + ) box = ( (box[0] - reduce_box[0]) / factor_x, (box[1] - reduce_box[1]) / factor_y, @@ -2818,7 +2818,7 @@ class Image: self.im.transform2(box, image.im, method, data, resample, fill) - def transpose(self, method): + def transpose(self, method: Transpose) -> Image: """ Transpose image (flip or rotate in 90 degree steps) @@ -2870,7 +2870,9 @@ class ImagePointHandler: (for use with :py:meth:`~PIL.Image.Image.point`) """ - pass + @abc.abstractmethod + def point(self, im: Image) -> Image: + pass class ImageTransformHandler: @@ -3690,6 +3692,7 @@ class Exif(_ExifBase): endian = None bigtiff = False + _loaded = False def __init__(self): self._data = {} @@ -3805,7 +3808,7 @@ class Exif(_ExifBase): return merged_dict - def tobytes(self, offset=8): + def tobytes(self, offset: int = 8) -> bytes: from . import TiffImagePlugin head = self._get_head() @@ -3960,7 +3963,7 @@ class Exif(_ExifBase): del self._info[tag] self._data[tag] = value - def __delitem__(self, tag): + def __delitem__(self, tag: int) -> None: if self._info is not None and tag in self._info: del self._info[tag] else: diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index ad59b0667..5fb80b753 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -124,7 +124,7 @@ def getrgb(color): @lru_cache -def getcolor(color, mode): +def getcolor(color, mode: str) -> tuple[int, ...]: """ Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index a9e626b2b..6218c723f 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -21,6 +21,7 @@ from __future__ import annotations import functools import operator import re +from typing import Protocol, Sequence, cast from . import ExifTags, Image, ImagePalette @@ -28,7 +29,7 @@ from . import ExifTags, Image, ImagePalette # helpers -def _border(border): +def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]: if isinstance(border, tuple): if len(border) == 2: left, top = right, bottom = border @@ -39,7 +40,7 @@ def _border(border): return left, top, right, bottom -def _color(color, mode): +def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]: if isinstance(color, str): from . import ImageColor @@ -47,7 +48,7 @@ def _color(color, mode): return color -def _lut(image, lut): +def _lut(image: Image.Image, lut: list[int]) -> Image.Image: if image.mode == "P": # FIXME: apply to lookup table, not image data msg = "mode P support coming soon" @@ -65,7 +66,13 @@ def _lut(image, lut): # actions -def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): +def autocontrast( + image: Image.Image, + cutoff: float | tuple[float, float] = 0, + ignore: int | Sequence[int] | None = None, + mask: Image.Image | None = None, + preserve_tone: bool = False, +) -> Image.Image: """ Maximize (normalize) image contrast. This function calculates a histogram of the input image (or mask region), removes ``cutoff`` percent of the @@ -97,10 +104,9 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): h = histogram[layer : layer + 256] if ignore is not None: # get rid of outliers - try: + if isinstance(ignore, int): h[ignore] = 0 - except TypeError: - # assume sequence + else: for ix in ignore: h[ix] = 0 if cutoff: @@ -112,7 +118,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): for ix in range(256): n = n + h[ix] # remove cutoff% pixels from the low end - cut = n * cutoff[0] // 100 + cut = int(n * cutoff[0] // 100) for lo in range(256): if cut > h[lo]: cut = cut - h[lo] @@ -123,7 +129,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): if cut <= 0: break # remove cutoff% samples from the high end - cut = n * cutoff[1] // 100 + cut = int(n * cutoff[1] // 100) for hi in range(255, -1, -1): if cut > h[hi]: cut = cut - h[hi] @@ -156,7 +162,15 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): return _lut(image, lut) -def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoint=127): +def colorize( + image: Image.Image, + black: str | tuple[int, ...], + white: str | tuple[int, ...], + mid: str | int | tuple[int, ...] | None = None, + blackpoint: int = 0, + whitepoint: int = 255, + midpoint: int = 127, +) -> Image.Image: """ Colorize grayscale image. This function calculates a color wedge which maps all black pixels in @@ -188,10 +202,9 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi assert 0 <= blackpoint <= midpoint <= whitepoint <= 255 # Define colors from arguments - black = _color(black, "RGB") - white = _color(white, "RGB") - if mid is not None: - mid = _color(mid, "RGB") + rgb_black = cast(Sequence[int], _color(black, "RGB")) + rgb_white = cast(Sequence[int], _color(white, "RGB")) + rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None # Empty lists for the mapping red = [] @@ -200,18 +213,24 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi # Create the low-end values for i in range(0, blackpoint): - red.append(black[0]) - green.append(black[1]) - blue.append(black[2]) + red.append(rgb_black[0]) + green.append(rgb_black[1]) + blue.append(rgb_black[2]) # Create the mapping (2-color) - if mid is None: + if rgb_mid is None: range_map = range(0, whitepoint - blackpoint) for i in range_map: - red.append(black[0] + i * (white[0] - black[0]) // len(range_map)) - green.append(black[1] + i * (white[1] - black[1]) // len(range_map)) - blue.append(black[2] + i * (white[2] - black[2]) // len(range_map)) + red.append( + rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map) + ) + green.append( + rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map) + ) + blue.append( + rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map) + ) # Create the mapping (3-color) else: @@ -219,26 +238,36 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi range_map2 = range(0, whitepoint - midpoint) for i in range_map1: - red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1)) - green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1)) - blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1)) + red.append( + rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1) + ) + green.append( + rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1) + ) + blue.append( + rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1) + ) for i in range_map2: - red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2)) - green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2)) - blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2)) + red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2)) + green.append( + rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2) + ) + blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2)) # Create the high-end values for i in range(0, 256 - whitepoint): - red.append(white[0]) - green.append(white[1]) - blue.append(white[2]) + red.append(rgb_white[0]) + green.append(rgb_white[1]) + blue.append(rgb_white[2]) # Return converted image image = image.convert("RGB") return _lut(image, red + green + blue) -def contain(image, size, method=Image.Resampling.BICUBIC): +def contain( + image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a resized version of the image, set to the maximum width and height within the requested size, while maintaining the original aspect ratio. @@ -267,7 +296,9 @@ def contain(image, size, method=Image.Resampling.BICUBIC): return image.resize(size, resample=method) -def cover(image, size, method=Image.Resampling.BICUBIC): +def cover( + image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a resized version of the image, so that the requested size is covered, while maintaining the original aspect ratio. @@ -296,7 +327,13 @@ def cover(image, size, method=Image.Resampling.BICUBIC): return image.resize(size, resample=method) -def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)): +def pad( + image: Image.Image, + size: tuple[int, int], + method: int = Image.Resampling.BICUBIC, + color: str | int | tuple[int, ...] | None = None, + centering: tuple[float, float] = (0.5, 0.5), +) -> Image.Image: """ Returns a resized and padded version of the image, expanded to fill the requested aspect ratio and size. @@ -334,7 +371,7 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 return out -def crop(image, border=0): +def crop(image: Image.Image, border: int = 0) -> Image.Image: """ Remove border from image. The same amount of pixels are removed from all four sides. This function works on all image modes. @@ -349,7 +386,9 @@ def crop(image, border=0): return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) -def scale(image, factor, resample=Image.Resampling.BICUBIC): +def scale( + image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a rescaled image by a specific factor given in parameter. A factor greater than 1 expands the image, between 0 and 1 contracts the @@ -372,7 +411,19 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC): return image.resize(size, resample) -def deform(image, deformer, resample=Image.Resampling.BILINEAR): +class _SupportsGetMesh(Protocol): + def getmesh( + self, image: Image.Image + ) -> list[ + tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]] + ]: ... + + +def deform( + image: Image.Image, + deformer: _SupportsGetMesh, + resample: int = Image.Resampling.BILINEAR, +) -> Image.Image: """ Deform the image. @@ -388,7 +439,7 @@ def deform(image, deformer, resample=Image.Resampling.BILINEAR): ) -def equalize(image, mask=None): +def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image: """ Equalize the image histogram. This function applies a non-linear mapping to the input image, in order to create a uniform @@ -419,7 +470,11 @@ def equalize(image, mask=None): return _lut(image, lut) -def expand(image, border=0, fill=0): +def expand( + image: Image.Image, + border: int | tuple[int, ...] = 0, + fill: str | int | tuple[int, ...] = 0, +) -> Image.Image: """ Add border to the image @@ -445,7 +500,13 @@ def expand(image, border=0, fill=0): return out -def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, 0.5)): +def fit( + image: Image.Image, + size: tuple[int, int], + method: int = Image.Resampling.BICUBIC, + bleed: float = 0.0, + centering: tuple[float, float] = (0.5, 0.5), +) -> Image.Image: """ Returns a resized and cropped version of the image, cropped to the requested aspect ratio and size. @@ -479,13 +540,12 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, # kevin@cazabon.com # https://www.cazabon.com - # ensure centering is mutable - centering = list(centering) + centering_x, centering_y = centering - if not 0.0 <= centering[0] <= 1.0: - centering[0] = 0.5 - if not 0.0 <= centering[1] <= 1.0: - centering[1] = 0.5 + if not 0.0 <= centering_x <= 1.0: + centering_x = 0.5 + if not 0.0 <= centering_y <= 1.0: + centering_y = 0.5 if not 0.0 <= bleed < 0.5: bleed = 0.0 @@ -522,8 +582,8 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, crop_height = live_size[0] / output_ratio # make the crop - crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering[0] - crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering[1] + crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x + crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height) @@ -531,7 +591,7 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, return image.resize(size, method, box=crop) -def flip(image): +def flip(image: Image.Image) -> Image.Image: """ Flip the image vertically (top to bottom). @@ -541,7 +601,7 @@ def flip(image): return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) -def grayscale(image): +def grayscale(image: Image.Image) -> Image.Image: """ Convert the image to grayscale. @@ -551,7 +611,7 @@ def grayscale(image): return image.convert("L") -def invert(image): +def invert(image: Image.Image) -> Image.Image: """ Invert (negate) the image. @@ -562,7 +622,7 @@ def invert(image): return image.point(lut) if image.mode == "1" else _lut(image, lut) -def mirror(image): +def mirror(image: Image.Image) -> Image.Image: """ Flip image horizontally (left to right). @@ -572,7 +632,7 @@ def mirror(image): return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) -def posterize(image, bits): +def posterize(image: Image.Image, bits: int) -> Image.Image: """ Reduce the number of bits for each color channel. @@ -585,7 +645,7 @@ def posterize(image, bits): return _lut(image, lut) -def solarize(image, threshold=128): +def solarize(image: Image.Image, threshold: int = 128) -> Image.Image: """ Invert all pixel values above a threshold. @@ -602,7 +662,7 @@ def solarize(image, threshold=128): return _lut(image, lut) -def exif_transpose(image, *, in_place=False): +def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None: """ If an image has an EXIF Orientation tag, other than 1, transpose the image accordingly, and remove the orientation data. @@ -616,7 +676,7 @@ def exif_transpose(image, *, in_place=False): """ image.load() image_exif = image.getexif() - orientation = image_exif.get(ExifTags.Base.Orientation) + orientation = image_exif.get(ExifTags.Base.Orientation, 1) method = { 2: Image.Transpose.FLIP_LEFT_RIGHT, 3: Image.Transpose.ROTATE_180, @@ -653,3 +713,4 @@ def exif_transpose(image, *, in_place=False): return transposed_image elif not in_place: return image.copy() + return None diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 2b6cecc61..770d10025 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -18,6 +18,7 @@ from __future__ import annotations import array +from typing import Sequence from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile @@ -34,11 +35,11 @@ class ImagePalette: Defaults to an empty palette. """ - def __init__(self, mode="RGB", palette=None): + def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None: self.mode = mode self.rawmode = None # if set, palette contains raw data self.palette = palette or bytearray() - self.dirty = None + self.dirty: int | None = None @property def palette(self): @@ -127,7 +128,7 @@ class ImagePalette: raise ValueError(msg) from e return index - def getcolor(self, color, image=None): + def getcolor(self, color, image=None) -> int: """Given an rgb tuple, allocate palette entry. .. warning:: This method is experimental. From d3b974b78607d36fbfa392b80c81adbd08d277ac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 11:19:32 +1100 Subject: [PATCH 036/688] Use font in ImageDraw examples --- docs/deprecations.rst | 8 ++++---- docs/releasenotes/9.2.0.rst | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 205fcb9ab..a58ce9bcb 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -232,10 +232,10 @@ Previous code:: im = Image.new("RGB", (100, 100)) draw = ImageDraw.Draw(im) - width, height = draw.textsize("Hello world") + width, height = draw.textsize("Hello world", font) width, height = font.getsize_multiline("Hello\nworld") - width, height = draw.multiline_textsize("Hello\nworld") + width, height = draw.multiline_textsize("Hello\nworld", font) Use instead:: @@ -247,9 +247,9 @@ Use instead:: im = Image.new("RGB", (100, 100)) draw = ImageDraw.Draw(im) - width = draw.textlength("Hello world") + width = draw.textlength("Hello world", font) - left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld") + left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld", font) width, height = right - left, bottom - top FreeTypeFont.getmask2 fill parameter diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index b875edf8e..3b8d2535f 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -69,10 +69,10 @@ Previous code:: im = Image.new("RGB", (100, 100)) draw = ImageDraw.Draw(im) - width, height = draw.textsize("Hello world") + width, height = draw.textsize("Hello world", font) width, height = font.getsize_multiline("Hello\nworld") - width, height = draw.multiline_textsize("Hello\nworld") + width, height = draw.multiline_textsize("Hello\nworld", font) Use instead:: @@ -84,9 +84,9 @@ Use instead:: im = Image.new("RGB", (100, 100)) draw = ImageDraw.Draw(im) - width = draw.textlength("Hello world") + width = draw.textlength("Hello world", font) - left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld") + left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld", font) width, height = right - left, bottom - top API Additions From 617b9cbc00759f042d2c38ea63a14adca2bc902b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 11:50:48 +1100 Subject: [PATCH 037/688] Describe difference between size and bbox --- docs/deprecations.rst | 4 ++++ docs/releasenotes/9.2.0.rst | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a58ce9bcb..c90ad481a 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -252,6 +252,10 @@ Use instead:: left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld", font) width, height = right - left, bottom - top +Previously, the ``size`` methods returned a ``height`` that included the vertical +offset of the text, while the new ``bbox`` methods explicitly distinguish this as a +``top`` offset. + FreeTypeFont.getmask2 fill parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 3b8d2535f..495926ca7 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -89,6 +89,10 @@ Use instead:: left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld", font) width, height = right - left, bottom - top +Previously, the ``size`` methods returned a ``height`` that included the vertical +offset of the text, while the new ``bbox`` methods explicitly distinguish this as a +``top`` offset. + API Additions ============= From 1a108281b9b6d894574ec63534a043385549b3be Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 14:03:56 +1100 Subject: [PATCH 038/688] Removed unused code --- Tests/test_format_hsv.py | 4 ---- Tests/test_image_paste.py | 1 - 2 files changed, 5 deletions(-) diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 73aaae6e7..da909c06c 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -12,10 +12,6 @@ def int_to_float(i): return i / 255 -def str_to_float(i): - return ord(i) / 255 - - def tuple_to_ints(tp): x, y, z = tp return int(x * 255.0), int(y * 255.0), int(z * 255.0) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index ce7345572..2966f724f 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -8,7 +8,6 @@ from .helper import CachedProperty, assert_image_equal class TestImagingPaste: - masks = {} size = 128 def assert_9points_image( From 5ff7d926fd24acc2d6d575959635d59123b308a6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 15:00:38 +1100 Subject: [PATCH 039/688] Added type hints --- Tests/test_features.py | 7 +- Tests/test_file_blp.py | 2 +- Tests/test_file_bmp.py | 4 +- Tests/test_file_im.py | 2 +- Tests/test_file_pcx.py | 6 +- Tests/test_file_pdf.py | 15 ++-- Tests/test_file_tiff.py | 19 ++--- Tests/test_format_hsv.py | 19 +++-- Tests/test_imagechops.py | 6 +- Tests/test_imagedraw2.py | 11 +-- Tests/test_imageenhance.py | 8 ++- Tests/test_imagefile.py | 2 +- Tests/test_imagefont.py | 140 ++++++++++++++++++++++--------------- Tests/test_imageops.py | 22 +++--- Tests/test_imageops_usm.py | 14 ++-- Tests/test_imagepath.py | 13 ++-- 16 files changed, 170 insertions(+), 120 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index de74e9c18..8d2d198ff 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -2,6 +2,7 @@ from __future__ import annotations import io import re +from typing import Callable import pytest @@ -29,7 +30,7 @@ def test_version() -> None: # Check the correctness of the convenience function # and the format of version numbers - def test(name, function) -> None: + def test(name: str, function: Callable[[str], bool]) -> None: version = features.version(name) if not features.check(name): assert version is None @@ -73,12 +74,12 @@ def test_libimagequant_version() -> None: @pytest.mark.parametrize("feature", features.modules) -def test_check_modules(feature) -> None: +def test_check_modules(feature: str) -> None: assert features.check_module(feature) in [True, False] @pytest.mark.parametrize("feature", features.codecs) -def test_check_codecs(feature) -> None: +def test_check_codecs(feature: str) -> None: assert features.check_codec(feature) in [True, False] diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 3904d3bc5..1e2f20c40 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -71,7 +71,7 @@ def test_save(tmp_path: Path) -> None: "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", ], ) -def test_crashes(test_file) -> None: +def test_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index c36466e02..1eaff0c7d 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -16,7 +16,7 @@ from .helper import ( def test_sanity(tmp_path: Path) -> None: - def roundtrip(im) -> None: + def roundtrip(im: Image.Image) -> None: outfile = str(tmp_path / "temp.bmp") im.save(outfile, "BMP") @@ -194,7 +194,7 @@ def test_rle4() -> None: ("Tests/images/bmp/g/pal8rle.bmp", 1064), ), ) -def test_rle8_eof(file_name, length) -> None: +def test_rle8_eof(file_name: str, length: int) -> None: with open(file_name, "rb") as fp: data = fp.read(length) with Image.open(io.BytesIO(data)) as im: diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index f932069b9..036965bf5 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -82,7 +82,7 @@ def test_eoferror() -> None: @pytest.mark.parametrize("mode", ("RGB", "P", "PA")) -def test_roundtrip(mode, tmp_path: Path) -> None: +def test_roundtrip(mode: str, tmp_path: Path) -> None: out = str(tmp_path / "temp.im") im = hopper(mode) im.save(out) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index a2486be40..ab9f9663e 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -9,7 +9,7 @@ from PIL import Image, ImageFile, PcxImagePlugin from .helper import assert_image_equal, hopper -def _roundtrip(tmp_path: Path, im) -> None: +def _roundtrip(tmp_path: Path, im: Image.Image) -> None: f = str(tmp_path / "temp.pcx") im.save(f) with Image.open(f) as im2: @@ -44,7 +44,7 @@ def test_invalid_file() -> None: @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) -def test_odd(tmp_path: Path, mode) -> None: +def test_odd(tmp_path: Path, mode: str) -> None: # See issue #523, odd sized images should have a stride that's even. # Not that ImageMagick or GIMP write PCX that way. # We were not handling properly. @@ -89,7 +89,7 @@ def test_large_count(tmp_path: Path) -> None: _roundtrip(tmp_path, im) -def _test_buffer_overflow(tmp_path: Path, im, size: int = 1024) -> None: +def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None: _last = ImageFile.MAXBLOCK ImageFile.MAXBLOCK = size try: diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 65a93c138..d39a86565 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -6,6 +6,7 @@ import os.path import tempfile import time from pathlib import Path +from typing import Any, Generator import pytest @@ -14,7 +15,7 @@ from PIL import Image, PdfParser, features from .helper import hopper, mark_if_feature_version, skip_unless_feature -def helper_save_as_pdf(tmp_path: Path, mode, **kwargs): +def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str: # Arrange im = hopper(mode) outfile = str(tmp_path / ("temp_" + mode + ".pdf")) @@ -41,13 +42,13 @@ def helper_save_as_pdf(tmp_path: Path, mode, **kwargs): @pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK")) -def test_save(tmp_path: Path, mode) -> None: +def test_save(tmp_path: Path, mode: str) -> None: helper_save_as_pdf(tmp_path, mode) @skip_unless_feature("jpg_2000") @pytest.mark.parametrize("mode", ("LA", "RGBA")) -def test_save_alpha(tmp_path: Path, mode) -> None: +def test_save_alpha(tmp_path: Path, mode: str) -> None: helper_save_as_pdf(tmp_path, mode) @@ -112,7 +113,7 @@ def test_resolution(tmp_path: Path) -> None: {"dpi": (75, 150), "resolution": 200}, ), ) -def test_dpi(params, tmp_path: Path) -> None: +def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None: im = hopper() outfile = str(tmp_path / "temp.pdf") @@ -156,7 +157,7 @@ def test_save_all(tmp_path: Path) -> None: assert os.path.getsize(outfile) > 0 # Test appending using a generator - def im_generator(ims): + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims im.save(outfile, save_all=True, append_images=im_generator(ims)) @@ -226,7 +227,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None: im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) -def check_pdf_pages_consistency(pdf) -> None: +def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None: pages_info = pdf.read_indirect(pdf.pages_ref) assert b"Parent" not in pages_info assert b"Kids" in pages_info @@ -339,7 +340,7 @@ def test_pdf_append_to_bytesio() -> None: @pytest.mark.timeout(1) @pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") @pytest.mark.parametrize("newline", (b"\r", b"\n")) -def test_redos(newline) -> None: +def test_redos(newline: bytes) -> None: malicious = b" trailer<<>>" + newline * 3456 # This particular exception isn't relevant here. diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index a16b76e19..0110948ae 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -4,6 +4,8 @@ import os import warnings from io import BytesIO from pathlib import Path +from types import ModuleType +from typing import Generator import pytest @@ -20,6 +22,7 @@ from .helper import ( is_win32, ) +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: @@ -156,7 +159,7 @@ class TestFileTiff: "resolution_unit, dpi", [(None, 72.8), (2, 72.8), (3, 184.912)], ) - def test_load_float_dpi(self, resolution_unit, dpi) -> None: + def test_load_float_dpi(self, resolution_unit: int | None, dpi: float) -> None: with Image.open( "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" ) as im: @@ -284,7 +287,7 @@ class TestFileTiff: ("Tests/images/multipage.tiff", 3), ), ) - def test_n_frames(self, path, n_frames) -> None: + def test_n_frames(self, path: str, n_frames: int) -> None: with Image.open(path) as im: assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) @@ -402,7 +405,7 @@ class TestFileTiff: assert len_before == len_after + 1 @pytest.mark.parametrize("legacy_api", (False, True)) - def test_load_byte(self, legacy_api) -> None: + def test_load_byte(self, legacy_api: bool) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc" ret = ifd.load_byte(data, legacy_api) @@ -431,7 +434,7 @@ class TestFileTiff: assert 0x8825 in im.tag_v2 def test_exif(self, tmp_path: Path) -> None: - def check_exif(exif) -> None: + def check_exif(exif: Image.Exif) -> None: assert sorted(exif.keys()) == [ 256, 257, @@ -511,7 +514,7 @@ class TestFileTiff: assert im.getexif()[273] == (1408, 1907) @pytest.mark.parametrize("mode", ("1", "L")) - def test_photometric(self, mode, tmp_path: Path) -> None: + def test_photometric(self, mode: str, tmp_path: Path) -> None: filename = str(tmp_path / "temp.tif") im = hopper(mode) im.save(filename, tiffinfo={262: 0}) @@ -660,7 +663,7 @@ class TestFileTiff: assert_image_equal_tofile(reloaded, infile) @pytest.mark.parametrize("mode", ("P", "PA")) - def test_palette(self, mode, tmp_path: Path) -> None: + def test_palette(self, mode: str, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") im = hopper(mode) @@ -689,7 +692,7 @@ class TestFileTiff: assert reread.n_frames == 3 # Test appending using a generator - def im_generator(ims): + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims mp = BytesIO() @@ -860,7 +863,7 @@ class TestFileTiff: ], ) @pytest.mark.timeout(2) - def test_oom(self, test_file) -> None: + def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning): with Image.open(test_file): diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 73aaae6e7..fe055bf4b 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -2,21 +2,22 @@ from __future__ import annotations import colorsys import itertools +from typing import Callable from PIL import Image from .helper import assert_image_similar, hopper -def int_to_float(i): +def int_to_float(i: int) -> float: return i / 255 -def str_to_float(i): +def str_to_float(i: str) -> float: return ord(i) / 255 -def tuple_to_ints(tp): +def tuple_to_ints(tp: tuple[float, float, float]) -> tuple[int, int, int]: x, y, z = tp return int(x * 255.0), int(y * 255.0), int(z * 255.0) @@ -25,7 +26,7 @@ def test_sanity() -> None: Image.new("HSV", (100, 100)) -def wedge(): +def wedge() -> Image.Image: w = Image._wedge() w90 = w.rotate(90) @@ -49,7 +50,11 @@ def wedge(): return img -def to_xxx_colorsys(im, func, mode): +def to_xxx_colorsys( + im: Image.Image, + func: Callable[[float, float, float], tuple[float, float, float]], + mode: str, +) -> Image.Image: # convert the hard way using the library colorsys routines. (r, g, b) = im.split() @@ -70,11 +75,11 @@ def to_xxx_colorsys(im, func, mode): return hsv -def to_hsv_colorsys(im): +def to_hsv_colorsys(im: Image.Image) -> Image.Image: return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") -def to_rgb_colorsys(im): +def to_rgb_colorsys(im: Image.Image) -> Image.Image: return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 94f57e066..7e2290c15 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Callable + from PIL import Image, ImageChops from .helper import assert_image_equal, hopper @@ -387,7 +389,9 @@ def test_overlay() -> None: def test_logical() -> None: - def table(op, a, b): + def table( + op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int + ) -> tuple[int, int, int, int]: out = [] for x in (a, b): imx = Image.new("1", (1, 1), x) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 07a25b84b..3171eb9ae 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -5,6 +5,7 @@ import os.path import pytest from PIL import Image, ImageDraw, ImageDraw2, features +from PIL._typing import Coords from .helper import ( assert_image_equal, @@ -56,7 +57,7 @@ def test_sanity() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(bbox) -> None: +def test_ellipse(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -84,7 +85,7 @@ def test_ellipse_edge() -> None: @pytest.mark.parametrize("points", POINTS) -def test_line(points) -> None: +def test_line(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -98,7 +99,7 @@ def test_line(points) -> None: @pytest.mark.parametrize("points", POINTS) -def test_line_pen_as_brush(points) -> None: +def test_line_pen_as_brush(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -114,7 +115,7 @@ def test_line_pen_as_brush(points) -> None: @pytest.mark.parametrize("points", POINTS) -def test_polygon(points) -> None: +def test_polygon(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -129,7 +130,7 @@ def test_polygon(points) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox) -> None: +def test_rectangle(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index 9ce9cda82..6ebc61e1b 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -22,7 +22,7 @@ def test_crash() -> None: ImageEnhance.Sharpness(im).enhance(0.5) -def _half_transparent_image(): +def _half_transparent_image() -> Image.Image: # returns an image, half transparent, half solid im = hopper("RGB") @@ -34,7 +34,9 @@ def _half_transparent_image(): return im -def _check_alpha(im, original, op, amount) -> None: +def _check_alpha( + im: Image.Image, original: Image.Image, op: str, amount: float +) -> None: assert im.getbands() == original.getbands() assert_image_equal( im.getchannel("A"), @@ -44,7 +46,7 @@ def _check_alpha(im, original, op, amount) -> None: @pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness")) -def test_alpha(op) -> None: +def test_alpha(op: str) -> None: # Issue https://github.com/python-pillow/Pillow/issues/899 # Is alpha preserved through image enhancement? diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 491409781..44521a8b3 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -31,7 +31,7 @@ SAFEBLOCK = ImageFile.SAFEBLOCK class TestImageFile: def test_parser(self) -> None: - def roundtrip(format): + def roundtrip(format: str) -> tuple[Image.Image, Image.Image]: im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): im = im.convert("1") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 909026dc8..c79b36ca4 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -7,11 +7,13 @@ import shutil import sys from io import BytesIO from pathlib import Path +from typing import BinaryIO import pytest from packaging.version import parse as parse_version from PIL import Image, ImageDraw, ImageFont, features +from PIL._typing import StrOrBytesPath from .helper import ( assert_image_equal, @@ -47,11 +49,11 @@ def layout_engine(request): @pytest.fixture(scope="module") -def font(layout_engine): +def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont: return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine) -def test_font_properties(font) -> None: +def test_font_properties(font: ImageFont.FreeTypeFont) -> None: assert font.path == FONT_PATH assert font.size == FONT_SIZE @@ -67,7 +69,9 @@ def test_font_properties(font) -> None: assert font_copy.path == second_font_path -def _render(font, layout_engine): +def _render( + font: StrOrBytesPath | BinaryIO, layout_engine: ImageFont.Layout +) -> Image.Image: txt = "Hello World!" ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine) ttf.getbbox(txt) @@ -80,12 +84,12 @@ def _render(font, layout_engine): @pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH))) -def test_font_with_name(layout_engine, font) -> None: +def test_font_with_name(layout_engine: ImageFont.Layout, font: str | Path) -> None: _render(font, layout_engine) -def test_font_with_filelike(layout_engine) -> None: - def _font_as_bytes(): +def test_font_with_filelike(layout_engine: ImageFont.Layout) -> None: + def _font_as_bytes() -> BytesIO: with open(FONT_PATH, "rb") as f: font_bytes = BytesIO(f.read()) return font_bytes @@ -102,12 +106,12 @@ def test_font_with_filelike(layout_engine) -> None: # _render(shared_bytes) -def test_font_with_open_file(layout_engine) -> None: +def test_font_with_open_file(layout_engine: ImageFont.Layout) -> None: with open(FONT_PATH, "rb") as f: _render(f, layout_engine) -def test_render_equal(layout_engine) -> None: +def test_render_equal(layout_engine: ImageFont.Layout) -> None: img_path = _render(FONT_PATH, layout_engine) with open(FONT_PATH, "rb") as f: font_filelike = BytesIO(f.read()) @@ -116,7 +120,7 @@ def test_render_equal(layout_engine) -> None: assert_image_equal(img_path, img_filelike) -def test_non_ascii_path(tmp_path: Path, layout_engine) -> None: +def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None: tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) try: shutil.copy(FONT_PATH, tempfile) @@ -126,7 +130,7 @@ def test_non_ascii_path(tmp_path: Path, layout_engine) -> None: ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine) -def test_transparent_background(font) -> None: +def test_transparent_background(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGBA", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -140,7 +144,7 @@ def test_transparent_background(font) -> None: assert_image_similar_tofile(im.convert("L"), target, 0.01) -def test_I16(font) -> None: +def test_I16(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="I;16", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -153,7 +157,7 @@ def test_I16(font) -> None: assert_image_similar_tofile(im.convert("L"), target, 0.01) -def test_textbbox_equal(font) -> None: +def test_textbbox_equal(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -181,7 +185,13 @@ def test_textbbox_equal(font) -> None: ), ) def test_getlength( - text, mode, fontname, size, layout_engine, length_basic, length_raqm + text: str, + mode: str, + fontname: str, + size: int, + layout_engine: ImageFont.Layout, + length_basic: int, + length_raqm: float, ) -> None: f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine) @@ -207,7 +217,7 @@ def test_float_size() -> None: assert lengths[0] != lengths[1] != lengths[2] -def test_render_multiline(font) -> None: +def test_render_multiline(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) line_spacing = font.getbbox("A")[3] + 4 @@ -223,7 +233,7 @@ def test_render_multiline(font) -> None: assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) -def test_render_multiline_text(font) -> None: +def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None: # Test that text() correctly connects to multiline_text() # and that align defaults to left im = Image.new(mode="RGB", size=(300, 100)) @@ -243,7 +253,9 @@ def test_render_multiline_text(font) -> None: @pytest.mark.parametrize( "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) ) -def test_render_multiline_text_align(font, align, ext) -> None: +def test_render_multiline_text_align( + font: ImageFont.FreeTypeFont, align: str, ext: str +) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align) @@ -251,7 +263,7 @@ def test_render_multiline_text_align(font, align, ext) -> None: assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) -def test_unknown_align(font) -> None: +def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -260,14 +272,14 @@ def test_unknown_align(font) -> None: draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") -def test_draw_align(font) -> None: +def test_draw_align(font: ImageFont.FreeTypeFont) -> None: im = Image.new("RGB", (300, 100), "white") draw = ImageDraw.Draw(im) line = "some text" draw.text((100, 40), line, (0, 0, 0), font=font, align="left") -def test_multiline_bbox(font) -> None: +def test_multiline_bbox(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -285,7 +297,7 @@ def test_multiline_bbox(font) -> None: draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4) -def test_multiline_width(font) -> None: +def test_multiline_width(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -295,7 +307,7 @@ def test_multiline_width(font) -> None: ) -def test_multiline_spacing(font) -> None: +def test_multiline_spacing(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10) @@ -306,7 +318,9 @@ def test_multiline_spacing(font) -> None: @pytest.mark.parametrize( "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) ) -def test_rotated_transposed_font(font, orientation) -> None: +def test_rotated_transposed_font( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: img_gray = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_gray) word = "testing" @@ -347,7 +361,9 @@ def test_rotated_transposed_font(font, orientation) -> None: Image.Transpose.FLIP_TOP_BOTTOM, ), ) -def test_unrotated_transposed_font(font, orientation) -> None: +def test_unrotated_transposed_font( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: img_gray = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_gray) word = "testing" @@ -382,7 +398,9 @@ def test_unrotated_transposed_font(font, orientation) -> None: @pytest.mark.parametrize( "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) ) -def test_rotated_transposed_font_get_mask(font, orientation) -> None: +def test_rotated_transposed_font_get_mask( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: # Arrange text = "mask this" transposed_font = ImageFont.TransposedFont(font, orientation=orientation) @@ -403,7 +421,9 @@ def test_rotated_transposed_font_get_mask(font, orientation) -> None: Image.Transpose.FLIP_TOP_BOTTOM, ), ) -def test_unrotated_transposed_font_get_mask(font, orientation) -> None: +def test_unrotated_transposed_font_get_mask( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: # Arrange text = "mask this" transposed_font = ImageFont.TransposedFont(font, orientation=orientation) @@ -415,11 +435,11 @@ def test_unrotated_transposed_font_get_mask(font, orientation) -> None: assert mask.size == (108, 13) -def test_free_type_font_get_name(font) -> None: +def test_free_type_font_get_name(font: ImageFont.FreeTypeFont) -> None: assert ("FreeMono", "Regular") == font.getname() -def test_free_type_font_get_metrics(font) -> None: +def test_free_type_font_get_metrics(font: ImageFont.FreeTypeFont) -> None: ascent, descent = font.getmetrics() assert isinstance(ascent, int) @@ -427,7 +447,7 @@ def test_free_type_font_get_metrics(font) -> None: assert (ascent, descent) == (16, 4) -def test_free_type_font_get_mask(font) -> None: +def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: # Arrange text = "mask this" @@ -473,16 +493,16 @@ def test_default_font() -> None: @pytest.mark.parametrize("mode", (None, "1", "RGBA")) -def test_getbbox(font, mode) -> None: +def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None: assert (0, 4, 12, 16) == font.getbbox("A", mode) -def test_getbbox_empty(font) -> None: +def test_getbbox_empty(font: ImageFont.FreeTypeFont) -> None: # issue #2614, should not crash. assert (0, 0, 0, 0) == font.getbbox("") -def test_render_empty(font) -> None: +def test_render_empty(font: ImageFont.FreeTypeFont) -> None: # issue 2666 im = Image.new(mode="RGB", size=(300, 100)) target = im.copy() @@ -492,7 +512,7 @@ def test_render_empty(font) -> None: assert_image_equal(im, target) -def test_unicode_extended(layout_engine) -> None: +def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: # issue #3777 text = "A\u278A\U0001F12B" target = "Tests/images/unicode_extended.png" @@ -516,7 +536,7 @@ def test_unicode_extended(layout_engine) -> None: ) @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") def test_find_font(monkeypatch, platform, font_directory) -> None: - def _test_fake_loading_font(path_to_fake, fontname) -> None: + def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None: # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) with monkeypatch.context() as m: @@ -567,7 +587,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None: _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate") -def test_imagefont_getters(font) -> None: +def test_imagefont_getters(font: ImageFont.FreeTypeFont) -> None: assert font.getmetrics() == (16, 4) assert font.font.ascent == 16 assert font.font.descent == 4 @@ -588,7 +608,7 @@ def test_imagefont_getters(font) -> None: @pytest.mark.parametrize("stroke_width", (0, 2)) -def test_getsize_stroke(font, stroke_width) -> None: +def test_getsize_stroke(font: ImageFont.FreeTypeFont, stroke_width: int) -> None: assert font.getbbox("A", stroke_width=stroke_width) == ( 0 - stroke_width, 4 - stroke_width, @@ -607,7 +627,7 @@ def test_complex_font_settings() -> None: t.getmask("абвг", language="sr") -def test_variation_get(font) -> None: +def test_variation_get(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -662,7 +682,7 @@ def test_variation_get(font) -> None: ] -def _check_text(font, path, epsilon): +def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None: im = Image.new("RGB", (100, 75), "white") d = ImageDraw.Draw(im) d.text((10, 10), "Text", font=font, fill="black") @@ -677,7 +697,7 @@ def _check_text(font, path, epsilon): raise -def test_variation_set_by_name(font) -> None: +def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -702,7 +722,7 @@ def test_variation_set_by_name(font) -> None: _check_text(font, "Tests/images/variation_tiny_name.png", 40) -def test_variation_set_by_axes(font) -> None: +def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -737,7 +757,9 @@ def test_variation_set_by_axes(font) -> None: ), ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), ) -def test_anchor(layout_engine, anchor, left, top) -> None: +def test_anchor( + layout_engine: ImageFont.Layout, anchor: str, left: int, top: int +) -> None: name, text = "quick", "Quick" path = f"Tests/images/test_anchor_{name}_{anchor}.png" @@ -782,7 +804,9 @@ def test_anchor(layout_engine, anchor, left, top) -> None: ("md", "center"), ), ) -def test_anchor_multiline(layout_engine, anchor, align) -> None: +def test_anchor_multiline( + layout_engine: ImageFont.Layout, anchor: str, align: str +) -> None: target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" text = "a\nlong\ntext sample" @@ -800,7 +824,7 @@ def test_anchor_multiline(layout_engine, anchor, align) -> None: assert_image_similar_tofile(im, target, 4) -def test_anchor_invalid(font) -> None: +def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None: im = Image.new("RGB", (100, 100), "white") d = ImageDraw.Draw(im) d.font = font @@ -826,7 +850,7 @@ def test_anchor_invalid(font) -> None: @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) -def test_bitmap_font(layout_engine, bpp) -> None: +def test_bitmap_font(layout_engine: ImageFont.Layout, bpp: int) -> None: text = "Bitmap Font" layout_name = ["basic", "raqm"][layout_engine] target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" @@ -843,7 +867,7 @@ def test_bitmap_font(layout_engine, bpp) -> None: assert_image_equal_tofile(im, target) -def test_bitmap_font_stroke(layout_engine) -> None: +def test_bitmap_font_stroke(layout_engine: ImageFont.Layout) -> None: text = "Bitmap Font" layout_name = ["basic", "raqm"][layout_engine] target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" @@ -861,7 +885,7 @@ def test_bitmap_font_stroke(layout_engine) -> None: @pytest.mark.parametrize("embedded_color", (False, True)) -def test_bitmap_blend(layout_engine, embedded_color) -> None: +def test_bitmap_blend(layout_engine: ImageFont.Layout, embedded_color: bool) -> None: font = ImageFont.truetype( "Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine ) @@ -873,7 +897,7 @@ def test_bitmap_blend(layout_engine, embedded_color) -> None: assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png") -def test_standard_embedded_color(layout_engine) -> None: +def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None: txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) ttf.getbbox(txt) @@ -886,7 +910,7 @@ def test_standard_embedded_color(layout_engine) -> None: @pytest.mark.parametrize("fontmode", ("1", "L", "RGBA")) -def test_float_coord(layout_engine, fontmode): +def test_float_coord(layout_engine: ImageFont.Layout, fontmode: str) -> None: txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) @@ -908,7 +932,7 @@ def test_float_coord(layout_engine, fontmode): raise -def test_cbdt(layout_engine) -> None: +def test_cbdt(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine @@ -925,7 +949,7 @@ def test_cbdt(layout_engine) -> None: pytest.skip("freetype compiled without libpng or CBDT support") -def test_cbdt_mask(layout_engine) -> None: +def test_cbdt_mask(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine @@ -942,7 +966,7 @@ def test_cbdt_mask(layout_engine) -> None: pytest.skip("freetype compiled without libpng or CBDT support") -def test_sbix(layout_engine) -> None: +def test_sbix(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine @@ -959,7 +983,7 @@ def test_sbix(layout_engine) -> None: pytest.skip("freetype compiled without libpng or SBIX support") -def test_sbix_mask(layout_engine) -> None: +def test_sbix_mask(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine @@ -977,7 +1001,7 @@ def test_sbix_mask(layout_engine) -> None: @skip_unless_feature_version("freetype2", "2.10.0") -def test_colr(layout_engine) -> None: +def test_colr(layout_engine: ImageFont.Layout) -> None: font = ImageFont.truetype( "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", size=64, @@ -993,7 +1017,7 @@ def test_colr(layout_engine) -> None: @skip_unless_feature_version("freetype2", "2.10.0") -def test_colr_mask(layout_engine) -> None: +def test_colr_mask(layout_engine: ImageFont.Layout) -> None: font = ImageFont.truetype( "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", size=64, @@ -1008,7 +1032,7 @@ def test_colr_mask(layout_engine) -> None: assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) -def test_woff2(layout_engine) -> None: +def test_woff2(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/OpenSans.woff2", @@ -1042,7 +1066,7 @@ def test_render_mono_size() -> None: assert_image_equal_tofile(im, "Tests/images/text_mono.gif") -def test_too_many_characters(font) -> None: +def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None: with pytest.raises(ValueError): font.getlength("A" * 1_000_001) with pytest.raises(ValueError): @@ -1070,7 +1094,7 @@ def test_too_many_characters(font) -> None: "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf", ], ) -def test_oom(test_file) -> None: +def test_oom(test_file: str) -> None: with open(test_file, "rb") as f: font = ImageFont.truetype(BytesIO(f.read())) with pytest.raises(Image.DecompressionBombError): @@ -1091,6 +1115,8 @@ def test_raqm_missing_warning(monkeypatch) -> None: @pytest.mark.parametrize("size", [-1, 0]) -def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size) -> None: +def test_invalid_truetype_sizes_raise_valueerror( + layout_engine: ImageFont.Layout, size: int +) -> None: with pytest.raises(ValueError): ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 50bf404ae..b320e79c1 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -14,7 +14,7 @@ from .helper import ( class Deformer: - def getmesh(self, im): + def getmesh(self, im: Image.Image) -> list[tuple[tuple[int, ...], tuple[int, ...]]]: x, y = im.size return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] @@ -108,7 +108,7 @@ def test_fit_same_ratio() -> None: @pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512))) -def test_contain(new_size) -> None: +def test_contain(new_size: tuple[int, int]) -> None: im = hopper() new_im = ImageOps.contain(im, new_size) assert new_im.size == (256, 256) @@ -132,7 +132,7 @@ def test_contain_round() -> None: ("hopper.png", (256, 256)), # square ), ) -def test_cover(image_name, expected_size) -> None: +def test_cover(image_name: str, expected_size: tuple[int, int]) -> None: with Image.open("Tests/images/" + image_name) as im: new_im = ImageOps.cover(im, (256, 256)) assert new_im.size == expected_size @@ -168,7 +168,7 @@ def test_pad_round() -> None: @pytest.mark.parametrize("mode", ("P", "PA")) -def test_palette(mode) -> None: +def test_palette(mode: str) -> None: im = hopper(mode) # Expand @@ -210,7 +210,7 @@ def test_scale() -> None: @pytest.mark.parametrize("border", (10, (1, 2, 3, 4))) -def test_expand_palette(border) -> None: +def test_expand_palette(border: int | tuple[int, int, int, int]) -> None: with Image.open("Tests/images/p_16.tga") as im: im_expanded = ImageOps.expand(im, border, (255, 0, 0)) @@ -366,7 +366,7 @@ def test_exif_transpose() -> None: for ext in exts: with Image.open("Tests/images/hopper" + ext) as base_im: - def check(orientation_im) -> None: + def check(orientation_im: Image.Image) -> None: for im in [ orientation_im, orientation_im.copy(), @@ -445,7 +445,7 @@ def test_autocontrast_cutoff() -> None: # Test the cutoff argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: - def autocontrast(cutoff): + def autocontrast(cutoff: int | tuple[int, int]): return ImageOps.autocontrast(img, cutoff).histogram() assert autocontrast(10) == autocontrast((10, 10)) @@ -486,20 +486,20 @@ def test_autocontrast_mask_real_input() -> None: assert result_nomask != result assert_tuple_approx_equal( ImageStat.Stat(result, mask=rect_mask).median, - [195, 202, 184], + (195, 202, 184), threshold=2, msg="autocontrast with mask pixel incorrect", ) assert_tuple_approx_equal( ImageStat.Stat(result_nomask).median, - [119, 106, 79], + (119, 106, 79), threshold=2, msg="autocontrast without mask pixel incorrect", ) def test_autocontrast_preserve_tone() -> None: - def autocontrast(mode, preserve_tone): + def autocontrast(mode: str, preserve_tone: bool) -> Image.Image: im = hopper(mode) return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram() @@ -533,7 +533,7 @@ def test_autocontrast_preserve_gradient() -> None: @pytest.mark.parametrize( "color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0)) ) -def test_autocontrast_preserve_one_color(color) -> None: +def test_autocontrast_preserve_one_color(color: tuple[int, int, int]) -> None: img = Image.new("RGB", (10, 10), color) # single color images shouldn't change diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 03302e20f..519d79105 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,12 +1,14 @@ from __future__ import annotations +from typing import Generator + import pytest from PIL import Image, ImageFilter @pytest.fixture -def test_images(): +def test_images() -> Generator[dict[str, Image.Image], None, None]: ims = { "im": Image.open("Tests/images/hopper.ppm"), "snakes": Image.open("Tests/images/color_snakes.png"), @@ -18,7 +20,7 @@ def test_images(): im.close() -def test_filter_api(test_images) -> None: +def test_filter_api(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] test_filter = ImageFilter.GaussianBlur(2.0) @@ -32,7 +34,7 @@ def test_filter_api(test_images) -> None: assert i.size == (128, 128) -def test_usm_formats(test_images) -> None: +def test_usm_formats(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] usm = ImageFilter.UnsharpMask @@ -50,7 +52,7 @@ def test_usm_formats(test_images) -> None: im.convert("YCbCr").filter(usm) -def test_blur_formats(test_images) -> None: +def test_blur_formats(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] blur = ImageFilter.GaussianBlur @@ -68,7 +70,7 @@ def test_blur_formats(test_images) -> None: im.convert("YCbCr").filter(blur) -def test_usm_accuracy(test_images) -> None: +def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None: snakes = test_images["snakes"] src = snakes.convert("RGB") @@ -77,7 +79,7 @@ def test_usm_accuracy(test_images) -> None: assert i.tobytes() == src.tobytes() -def test_blur_accuracy(test_images) -> None: +def test_blur_accuracy(test_images: dict[str, Image.Image]) -> None: snakes = test_images["snakes"] i = snakes.filter(ImageFilter.GaussianBlur(0.4)) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 8ba745f21..bd600b177 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -3,6 +3,7 @@ from __future__ import annotations import array import math import struct +from typing import Sequence import pytest @@ -75,7 +76,9 @@ def test_path_constructors(coords) -> None: [[0.0, 1.0]], ), ) -def test_invalid_path_constructors(coords) -> None: +def test_invalid_path_constructors( + coords: tuple[str, str] | Sequence[Sequence[int]] +) -> None: # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) @@ -93,7 +96,7 @@ def test_invalid_path_constructors(coords) -> None: [0, 1, 2], ), ) -def test_path_odd_number_of_coordinates(coords) -> None: +def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None: # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) @@ -111,7 +114,9 @@ def test_path_odd_number_of_coordinates(coords) -> None: (1, (0.0, 0.0, 0.0, 0.0)), ], ) -def test_getbbox(coords, expected) -> None: +def test_getbbox( + coords: int | list[int], expected: tuple[float, float, float, float] +) -> None: # Arrange p = ImagePath.Path(coords) @@ -135,7 +140,7 @@ def test_getbbox_no_args() -> None: (list(range(6)), [(0.0, 3.0), (4.0, 9.0), (8.0, 15.0)]), ], ) -def test_map(coords, expected) -> None: +def test_map(coords: int | list[int], expected: list[tuple[float, float]]) -> None: # Arrange p = ImagePath.Path(coords) From 96fc60d5d2aa0ad13be0951efb1fe990a64f190a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 20:21:25 +1100 Subject: [PATCH 040/688] Removed mypy excludes --- pyproject.toml | 4 ---- tox.ini | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 48c59f2a1..e687f4bcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,7 +140,3 @@ follow_imports = "silent" warn_redundant_casts = true warn_unreachable = true warn_unused_ignores = true -exclude = [ - '^src/PIL/FpxImagePlugin.py$', - '^src/PIL/MicImagePlugin.py$', -] diff --git a/tox.ini b/tox.ini index 8c818df7a..3ef011c9e 100644 --- a/tox.ini +++ b/tox.ini @@ -41,6 +41,7 @@ deps = packaging types-cffi types-defusedxml + types-olefile extras = typing commands = From b6fdf2e9e7a65bf23cac224b2ab96c6b1d2c8449 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Feb 2024 07:54:58 +1100 Subject: [PATCH 041/688] Updated package name for Tidelift --- .github/FUNDING.yml | 2 +- README.md | 2 +- docs/conf.py | 2 +- docs/index.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e0e6804bf..8fc6bd0ad 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -tidelift: "pypi/Pillow" +tidelift: "pypi/pillow" diff --git a/README.md b/README.md index 6ca870166..9776c40e2 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ As of 2019, Pillow development is src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"> Tidelift + src="https://tidelift.com/badges/package/pypi/pillow?style=flat"> Newest PyPI version diff --git a/docs/conf.py b/docs/conf.py index 9ae7ae605..97289c91d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -326,7 +326,7 @@ linkcheck_allowed_redirects = { r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/", - r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", + r"https://tidelift.com/badges/package/pypi/pillow?.*": r"https://img.shields.io/badge/.*", r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", } diff --git a/docs/index.rst b/docs/index.rst index 558369919..bf2feea9a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -49,7 +49,7 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Mon, 19 Feb 2024 17:08:21 +1100 Subject: [PATCH 042/688] Added image to illustrate size vs bbox --- docs/deprecations.rst | 8 ++++++-- docs/example/size_vs_bbox.png | Bin 0 -> 12934 bytes docs/releasenotes/9.2.0.rst | 8 ++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 docs/example/size_vs_bbox.png diff --git a/docs/deprecations.rst b/docs/deprecations.rst index c90ad481a..74021a218 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -253,8 +253,12 @@ Use instead:: width, height = right - left, bottom - top Previously, the ``size`` methods returned a ``height`` that included the vertical -offset of the text, while the new ``bbox`` methods explicitly distinguish this as a -``top`` offset. +offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` +offset. + +.. image:: ./example/size_vs_bbox.png + :alt: Demonstration of size height vs bbox top and bottom + :align: center FreeTypeFont.getmask2 fill parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/example/size_vs_bbox.png b/docs/example/size_vs_bbox.png new file mode 100644 index 0000000000000000000000000000000000000000..11a05d2a8146a612741c7e4ec4d3cf0ce05a35a0 GIT binary patch literal 12934 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 literal 0 HcmV?d00001 diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 495926ca7..db514068f 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -90,8 +90,12 @@ Use instead:: width, height = right - left, bottom - top Previously, the ``size`` methods returned a ``height`` that included the vertical -offset of the text, while the new ``bbox`` methods explicitly distinguish this as a -``top`` offset. +offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` +offset. + +.. image:: ./example/size_vs_bbox.png + :alt: Demonstration of size height vs bbox top and bottom + :align: center API Additions ============= From 0692ad8cdd4fe979478d910b705dcc12f8e82f61 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Feb 2024 23:29:58 +1100 Subject: [PATCH 043/688] Updated giflib on macOS to 5.2.2 --- .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 26bf2f6d6..1ec2811f6 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -24,7 +24,7 @@ XZ_VERSION=5.4.5 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 if [[ -n "$IS_MACOS" ]]; then - GIFLIB_VERSION=5.1.4 + GIFLIB_VERSION=5.2.2 else GIFLIB_VERSION=5.2.1 fi From fe1edb1e0f8608e3924f67b53776f653c418ddfe Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 19 Feb 2024 16:47:07 +0200 Subject: [PATCH 044/688] Install mypy from requirements file So Renovate can update it on a schedule --- .ci/requirements-mypy.txt | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .ci/requirements-mypy.txt diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt new file mode 100644 index 000000000..ed3269460 --- /dev/null +++ b/.ci/requirements-mypy.txt @@ -0,0 +1 @@ +mypy==1.7.1 diff --git a/tox.ini b/tox.ini index 8c818df7a..85800ff8d 100644 --- a/tox.ini +++ b/tox.ini @@ -33,10 +33,10 @@ commands = [testenv:mypy] skip_install = true deps = + -r .ci/requirements-mypy.txt IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython - mypy==1.7.1 numpy packaging types-cffi From 7490aee8362c589a475035bf86c05b29c894593c Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 19 Feb 2024 20:18:18 +0100 Subject: [PATCH 045/688] Show how to use anchors to align text in imagefont deprecations --- docs/deprecations.rst | 24 ++++++++++++++++++++++++ docs/releasenotes/9.2.0.rst | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 74021a218..9efb1316d 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -260,6 +260,30 @@ offset. :alt: Demonstration of size height vs bbox top and bottom :align: center +If you are using these methods for aligning text, consider using :ref:`text-anchors` instead +which avoid issues that can occur with non-English text or unusual fonts. +For example, instead of the following code:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width, height = draw.textsize("Hello world", font) + x, y = (100 - width) / 2, (100 - height) / 2 + draw.text((x, y), "Hello world", font=font) + +Use instead:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + draw.text((100 / 2, 100 / 2), "Hello world", font=font, anchor="mm") + FreeTypeFont.getmask2 fill parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index db514068f..e8bf33b60 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -97,6 +97,30 @@ offset. :alt: Demonstration of size height vs bbox top and bottom :align: center +If you are using these methods for aligning text, consider using :ref:`text-anchors` instead +which avoid issues that can occur with non-English text or unusual fonts. +For example, instead of the following code:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width, height = draw.textsize("Hello world", font) + x, y = (100 - width) / 2, (100 - height) / 2 + draw.text((x, y), "Hello world", font=font) + +Use instead:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + draw.text((100 / 2, 100 / 2), "Hello world", font=font, anchor="mm") + API Additions ============= From 531b1e1b9a6b3f83519d9b6687523474f4a18d83 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 19 Feb 2024 22:50:06 +0200 Subject: [PATCH 046/688] Remove outdated installation warnings --- docs/installation.rst | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 116bdcf2f..980bbd99d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -9,15 +9,6 @@ Installation }); -Warnings --------- - -.. warning:: Pillow and PIL cannot co-exist in the same environment. Before installing Pillow, please uninstall PIL. - -.. warning:: Pillow >= 1.0 no longer supports ``import Image``. Please use ``from PIL import Image`` instead. - -.. warning:: Pillow >= 2.1.0 no longer supports ``import _imaging``. Please use ``from PIL.Image import core as _imaging`` instead. - Python Support -------------- From e39765d755cc2d37e79d07f58ebc77a8e44812c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 20 Feb 2024 15:41:20 +1100 Subject: [PATCH 047/688] Added type hints --- Tests/test_deprecate.py | 6 +++--- Tests/test_file_ico.py | 2 +- Tests/test_file_iptc.py | 2 +- Tests/test_file_msp.py | 2 +- Tests/test_file_png.py | 2 ++ Tests/test_file_psd.py | 2 +- Tests/test_file_tga.py | 4 ++-- Tests/test_file_tiff_metadata.py | 6 ++++-- Tests/test_imagecms.py | 28 ++++++++++++++++++++++------ Tests/test_imagefont.py | 22 ++++++++++++---------- Tests/test_imagegrab.py | 6 ++++-- Tests/test_imagepath.py | 8 +++++--- Tests/test_imageqt.py | 2 +- Tests/test_imageshow.py | 10 ++++++---- Tests/test_imagewin_pointers.py | 2 +- Tests/test_numpy.py | 6 +++--- Tests/test_qt_image_qapplication.py | 4 ++-- Tests/test_qt_image_toqimage.py | 2 +- Tests/test_util.py | 2 +- 19 files changed, 73 insertions(+), 45 deletions(-) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 6ffc8f6f5..584d8f91d 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -20,7 +20,7 @@ from PIL import _deprecate ), ], ) -def test_version(version, expected) -> None: +def test_version(version: int | None, expected: str) -> None: with pytest.warns(DeprecationWarning, match=expected): _deprecate.deprecate("Old thing", version, "new thing") @@ -46,7 +46,7 @@ def test_unknown_version() -> None: ), ], ) -def test_old_version(deprecated, plural, expected) -> None: +def test_old_version(deprecated: str, plural: bool, expected: str) -> None: expected = r"" with pytest.raises(RuntimeError, match=expected): _deprecate.deprecate(deprecated, 1, plural=plural) @@ -76,7 +76,7 @@ def test_replacement_and_action() -> None: "Upgrade to new thing.", ], ) -def test_action(action) -> None: +def test_action(action: str) -> None: expected = ( r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Upgrade to new thing\." diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 65f090931..f69a290fa 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -135,7 +135,7 @@ def test_different_bit_depths(tmp_path: Path) -> None: @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) -def test_save_to_bytes_bmp(mode) -> None: +def test_save_to_bytes_bmp(mode: str) -> None: output = io.BytesIO() im = hopper(mode) im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 9c0969437..88c30d468 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -98,7 +98,7 @@ def test_i() -> None: assert ret == 97 -def test_dump(monkeypatch) -> None: +def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: # Arrange c = b"abc" # Temporarily redirect stdout diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index f9f81d114..b0964aabe 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -52,7 +52,7 @@ def test_open_windows_v1() -> None: assert isinstance(im, MspImagePlugin.MspImageFile) -def _assert_file_image_equal(source_path, target_path) -> None: +def _assert_file_image_equal(source_path: str, target_path: str) -> None: with Image.open(source_path) as im: assert_image_equal_tofile(im, target_path) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index d4a634316..c51f56ce7 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -6,6 +6,7 @@ import warnings import zlib from io import BytesIO from pathlib import Path +from types import ModuleType from typing import Any import pytest @@ -23,6 +24,7 @@ from .helper import ( skip_unless_feature, ) +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 7eca8d9b1..e60638b22 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -157,7 +157,7 @@ def test_combined_larger_than_size() -> None: ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), ], ) -def test_crashes(test_file, raises) -> None: +def test_crashes(test_file: str, raises) -> None: with open(test_file, "rb") as f: with pytest.raises(raises): with Image.open(f): diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index bd8e522c7..3c6da50c5 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -22,8 +22,8 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} @pytest.mark.parametrize("mode", _MODES) -def test_sanity(mode, tmp_path: Path) -> None: - def roundtrip(original_im) -> None: +def test_sanity(mode: str, tmp_path: Path) -> None: + def roundtrip(original_im: Image.Image) -> None: out = str(tmp_path / "temp.tga") original_im.save(out, rle=rle) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index bb6225d07..d7a18c725 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -189,7 +189,9 @@ def test_iptc(tmp_path: Path) -> None: @pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1"))) -def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None: +def test_writing_other_types_to_ascii( + value: bytes | int, expected: str, tmp_path: Path +) -> None: info = TiffImagePlugin.ImageFileDirectory_v2() tag = TiffTags.TAGS_V2[271] @@ -206,7 +208,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None: @pytest.mark.parametrize("value", (1, IFDRational(1))) -def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None: +def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 83fc38ed3..21a0dd75b 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -237,7 +237,7 @@ def test_invalid_color_temperature() -> None: @pytest.mark.parametrize("flag", ("my string", -1)) -def test_invalid_flag(flag) -> None: +def test_invalid_flag(flag: str | int) -> None: with hopper() as im: with pytest.raises( ImageCms.PyCMSError, match="flags must be an integer between 0 and " @@ -335,12 +335,26 @@ def test_extended_information() -> None: o = ImageCms.getOpenProfile(SRGB) p = o.profile - def assert_truncated_tuple_equal(tup1, tup2, digits: int = 10) -> None: + def assert_truncated_tuple_equal( + tup1: tuple[tuple[float, float, float], ...] | tuple[float], + tup2: ( + tuple[tuple[tuple[float, float, float], ...], ...] + | tuple[tuple[float, float, float], ...] + | tuple[float] + ), + digits: int = 10, + ) -> None: # Helper function to reduce precision of tuples of floats # recursively and then check equality. power = 10**digits - def truncate_tuple(tuple_or_float): + def truncate_tuple( + tuple_or_float: ( + tuple[tuple[tuple[float, float, float], ...], ...] + | tuple[tuple[float, float, float], ...] + | tuple[float, ...] + ) + ) -> tuple[tuple[float, ...], ...]: return tuple( ( truncate_tuple(val) @@ -504,8 +518,10 @@ def test_profile_typesafety() -> None: ImageCms.ImageCmsProfile(1).tobytes() -def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel) -> None: - def create_test_image(): +def assert_aux_channel_preserved( + mode: str, transform_in_place: bool, preserved_channel: str +) -> None: + def create_test_image() -> Image.Image: # set up test image with something interesting in the tested aux channel. # fmt: off nine_grid_deltas = [ @@ -633,7 +649,7 @@ def test_auxiliary_channels_isolated() -> None: @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) -def test_rgb_lab(mode) -> None: +def test_rgb_lab(mode: str) -> None: im = Image.new(mode, (1, 1)) converted_im = im.convert("LAB") assert converted_im.getpixel((0, 0)) == (0, 128, 128) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index c79b36ca4..05b5d4716 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -7,7 +7,7 @@ import shutil import sys from io import BytesIO from pathlib import Path -from typing import BinaryIO +from typing import Any, BinaryIO import pytest from packaging.version import parse as parse_version @@ -44,7 +44,7 @@ def test_sanity() -> None: pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), ], ) -def layout_engine(request): +def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout: return request.param @@ -535,21 +535,23 @@ def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), ) @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") -def test_find_font(monkeypatch, platform, font_directory) -> None: +def test_find_font( + monkeypatch: pytest.MonkeyPatch, platform: str, font_directory: str +) -> None: def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None: # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) with monkeypatch.context() as m: m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False) - def loadable_font(filepath, size, index, encoding, *args, **kwargs): + def loadable_font( + filepath: str, size: int, index: int, encoding: str, *args: Any + ): if filepath == path_to_fake: return ImageFont._FreeTypeFont( - FONT_PATH, size, index, encoding, *args, **kwargs + FONT_PATH, size, index, encoding, *args ) - return ImageFont._FreeTypeFont( - filepath, size, index, encoding, *args, **kwargs - ) + return ImageFont._FreeTypeFont(filepath, size, index, encoding, *args) m.setattr(ImageFont, "FreeTypeFont", loadable_font) font = ImageFont.truetype(fontname) @@ -563,7 +565,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None: if platform == "linux": monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") - def fake_walker(path): + def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]: if path == font_directory: return [ ( @@ -1101,7 +1103,7 @@ def test_oom(test_file: str) -> None: font.getmask("Test Text") -def test_raqm_missing_warning(monkeypatch) -> None: +def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) with pytest.warns(UserWarning) as record: font = ImageFont.truetype( diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 235a2f993..e23adeb70 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -84,6 +84,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200 @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_grabclipboard_file(self) -> None: p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + assert p.stdin is not None p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') p.communicate() @@ -94,6 +95,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200 @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_grabclipboard_png(self) -> None: p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + assert p.stdin is not None p.stdin.write( rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") $ms = new-object System.IO.MemoryStream(, $bytes) @@ -113,7 +115,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes) reason="Linux with wl-clipboard only", ) @pytest.mark.parametrize("ext", ("gif", "png", "ico")) - def test_grabclipboard_wl_clipboard(self, ext) -> None: + def test_grabclipboard_wl_clipboard(self, ext: str) -> None: image_path = "Tests/images/hopper." + ext with open(image_path, "rb") as fp: subprocess.call(["wl-copy"], stdin=fp) @@ -128,6 +130,6 @@ $ms = new-object System.IO.MemoryStream(, $bytes) reason="Linux with wl-clipboard only", ) @pytest.mark.parametrize("arg", ("text", "--clear")) - def test_grabclipboard_wl_clipboard_errors(self, arg): + def test_grabclipboard_wl_clipboard_errors(self, arg: str) -> None: subprocess.call(["wl-copy", arg]) assert ImageGrab.grabclipboard() is None diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index bd600b177..9487560af 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -58,7 +58,9 @@ def test_path() -> None: ImagePath.Path((0, 1)), ), ) -def test_path_constructors(coords) -> None: +def test_path_constructors( + coords: Sequence[float] | array.array[float] | ImagePath.Path, +) -> None: # Arrange / Act p = ImagePath.Path(coords) @@ -206,9 +208,9 @@ class Evil: def __init__(self) -> None: self.corrupt = Image.core.path(0x4000000000000000) - def __getitem__(self, i): + def __getitem__(self, i: int) -> bytes: x = self.corrupt[i] return struct.pack("dd", x[0], x[1]) - def __setitem__(self, i, x) -> None: + def __setitem__(self, i: int, x: bytes) -> None: self.corrupt[i] = struct.unpack("dd", x) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 909f97167..88ad1f9ee 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -28,7 +28,7 @@ def test_rgb() -> None: assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) - def checkrgb(r, g, b) -> None: + def checkrgb(r: int, g: int, b: int) -> None: val = ImageQt.rgb(r, g, b) val = val % 2**24 # drop the alpha assert val >> 16 == r diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index f7269d45b..8d741d94a 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + import pytest from PIL import Image, ImageShow @@ -24,9 +26,9 @@ def test_register() -> None: "order", [-1, 0], ) -def test_viewer_show(order) -> None: +def test_viewer_show(order: int) -> None: class TestViewer(ImageShow.Viewer): - def show_image(self, image, **options) -> bool: + def show_image(self, image: Image.Image, **options: Any) -> bool: self.methodCalled = True return True @@ -48,7 +50,7 @@ def test_viewer_show(order) -> None: reason="Only run on CIs; hangs on Windows CIs", ) @pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) -def test_show(mode) -> None: +def test_show(mode: str) -> None: im = hopper(mode) assert ImageShow.show(im) @@ -73,7 +75,7 @@ def test_viewer() -> None: @pytest.mark.parametrize("viewer", ImageShow._viewers) -def test_viewers(viewer) -> None: +def test_viewers(viewer: ImageShow.Viewer) -> None: try: viewer.get_command("test.jpg") except NotImplementedError: diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index c7f633e62..f59ee7284 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -70,7 +70,7 @@ if is_win32(): ] CreateDIBSection.restype = ctypes.wintypes.HBITMAP - def serialize_dib(bi, pixels): + def serialize_dib(bi, pixels) -> bytearray: bf = BITMAPFILEHEADER() bf.bfType = 0x4D42 bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 6ba95c2d7..9f4e6534e 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -14,7 +14,7 @@ TEST_IMAGE_SIZE = (10, 10) def test_numpy_to_image() -> None: - def to_image(dtype, bands: int = 1, boolean: int = 0): + def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image: if bands == 1: if boolean: data = [0, 255] * 50 @@ -99,7 +99,7 @@ def test_1d_array() -> None: assert_image(Image.fromarray(a), "L", (1, 5)) -def _test_img_equals_nparray(img, np) -> None: +def _test_img_equals_nparray(img: Image.Image, np) -> None: assert len(np.shape) >= 2 np_size = np.shape[1], np.shape[0] assert img.size == np_size @@ -157,7 +157,7 @@ def test_save_tiff_uint16() -> None: ("HSV", numpy.uint8), ), ) -def test_to_array(mode, dtype) -> None: +def test_to_array(mode: str, dtype) -> None: img = hopper(mode) # Resize to non-square diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 7d6c0a8cb..3cd323553 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest -from PIL import ImageQt +from PIL import Image, ImageQt from .helper import assert_image_equal_tofile, assert_image_similar, hopper @@ -37,7 +37,7 @@ if ImageQt.qt_is_installed: lbl.setPixmap(pixmap1.copy()) -def roundtrip(expected) -> None: +def roundtrip(expected: Image.Image) -> None: result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb assert_image_similar(result, expected.convert("RGB"), 1) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index a222a7d71..6110be707 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -17,7 +17,7 @@ if ImageQt.qt_is_installed: @pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) -def test_sanity(mode, tmp_path: Path) -> None: +def test_sanity(mode: str, tmp_path: Path) -> None: src = hopper(mode) data = ImageQt.toqimage(src) diff --git a/Tests/test_util.py b/Tests/test_util.py index 73e4acd55..197ef79ee 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -10,7 +10,7 @@ from PIL import _util @pytest.mark.parametrize( "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")] ) -def test_is_path(test_path) -> None: +def test_is_path(test_path: str | Path | PurePath) -> None: # Act it_is = _util.is_path(test_path) From a655d7606e2f12f0e7700ef754ed92a6da45f658 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 20 Feb 2024 21:27:30 +1100 Subject: [PATCH 048/688] Simplified type hints --- Tests/test_imagecms.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 21a0dd75b..a7bb31db5 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -6,6 +6,7 @@ import re import shutil from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -336,25 +337,13 @@ def test_extended_information() -> None: p = o.profile def assert_truncated_tuple_equal( - tup1: tuple[tuple[float, float, float], ...] | tuple[float], - tup2: ( - tuple[tuple[tuple[float, float, float], ...], ...] - | tuple[tuple[float, float, float], ...] - | tuple[float] - ), - digits: int = 10, + tup1: tuple[Any, ...], tup2: tuple[Any, ...], digits: int = 10 ) -> None: # Helper function to reduce precision of tuples of floats # recursively and then check equality. power = 10**digits - def truncate_tuple( - tuple_or_float: ( - tuple[tuple[tuple[float, float, float], ...], ...] - | tuple[tuple[float, float, float], ...] - | tuple[float, ...] - ) - ) -> tuple[tuple[float, ...], ...]: + def truncate_tuple(tuple_or_float: tuple[Any, ...]) -> tuple[Any, ...]: return tuple( ( truncate_tuple(val) From 64579510c018440edb1b0c9c67f36d478f969f38 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 20 Feb 2024 21:37:06 +1100 Subject: [PATCH 049/688] Updated alt text --- docs/deprecations.rst | 2 +- docs/releasenotes/9.2.0.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 74021a218..9fd64fdaa 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -257,7 +257,7 @@ offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` offset. .. image:: ./example/size_vs_bbox.png - :alt: Demonstration of size height vs bbox top and bottom + :alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself. :align: center FreeTypeFont.getmask2 fill parameter diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index db514068f..b596a6ab2 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -94,7 +94,7 @@ offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` offset. .. image:: ./example/size_vs_bbox.png - :alt: Demonstration of size height vs bbox top and bottom + :alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself. :align: center API Additions From 56a02b76eb403356436e6480c5ada23930825e36 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 20 Feb 2024 21:37:40 +1100 Subject: [PATCH 050/688] Corrected image path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- docs/releasenotes/9.2.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index b596a6ab2..677438bc6 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -93,7 +93,7 @@ Previously, the ``size`` methods returned a ``height`` that included the vertica offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` offset. -.. image:: ./example/size_vs_bbox.png +.. image:: ../example/size_vs_bbox.png :alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself. :align: center From 5b20811cabd4594fd4fafc596e3f94afe12bb361 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 20 Feb 2024 20:36:36 +0100 Subject: [PATCH 051/688] Add `--bugreport` argument to __main__.py to omit supported formats --- src/PIL/__main__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/__main__.py b/src/PIL/__main__.py index 943789923..32de33624 100644 --- a/src/PIL/__main__.py +++ b/src/PIL/__main__.py @@ -1,5 +1,7 @@ from __future__ import annotations +import sys + from .features import pilinfo -pilinfo() +pilinfo(supported_formats="--bugreport" not in sys.argv) From 10712be53d575a37d9dc2522c9f8f0f62871f3b6 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 20 Feb 2024 21:21:10 +0100 Subject: [PATCH 052/688] Build docs for Python changes --- .github/workflows/docs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4319cc8ff..92e860cb5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,10 +7,12 @@ on: paths: - ".github/workflows/docs.yml" - "docs/**" + - "src/PIL/**" pull_request: paths: - ".github/workflows/docs.yml" - "docs/**" + - "src/PIL/**" workflow_dispatch: permissions: From ab9dfd8181868922abee46dffb11ffcb9a772958 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 20 Feb 2024 20:37:33 +0100 Subject: [PATCH 053/688] Add sys.{executable,base_prefix,prefix} to features.pilinfo --- Tests/test_features.py | 12 +++++++++--- Tests/test_main.py | 12 +++++++++--- src/PIL/features.py | 9 +++++++-- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index 8d2d198ff..3fffa032f 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -129,9 +129,15 @@ def test_pilinfo() -> None: while lines[0].startswith(" "): lines = lines[1:] assert lines[0] == "-" * 68 - assert lines[1].startswith("Python modules loaded from ") - assert lines[2].startswith("Binary modules loaded from ") - assert lines[3] == "-" * 68 + assert lines[1].startswith("Python executable is") + lines = lines[2:] + if lines[0].startswith("Environment Python files loaded from"): + lines = lines[1:] + assert lines[0].startswith("System Python files loaded from") + assert lines[1] == "-" * 68 + assert lines[2].startswith("Python Pillow modules loaded from ") + assert lines[3].startswith("Binary Pillow modules loaded from ") + assert lines[4] == "-" * 68 jpeg = ( "\n" + "-" * 68 diff --git a/Tests/test_main.py b/Tests/test_main.py index 46259f1dc..e13e0c5e3 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -15,9 +15,15 @@ def test_main() -> None: while lines[0].startswith(" "): lines = lines[1:] assert lines[0] == "-" * 68 - assert lines[1].startswith("Python modules loaded from ") - assert lines[2].startswith("Binary modules loaded from ") - assert lines[3] == "-" * 68 + assert lines[1].startswith("Python executable is") + lines = lines[2:] + if lines[0].startswith("Environment Python files loaded from"): + lines = lines[1:] + assert lines[0].startswith("System Python files loaded from") + assert lines[1] == "-" * 68 + assert lines[2].startswith("Python Pillow modules loaded from ") + assert lines[3].startswith("Binary Pillow modules loaded from ") + assert lines[4] == "-" * 68 jpeg = ( os.linesep + "-" * 68 diff --git a/src/PIL/features.py b/src/PIL/features.py index b14d6df13..7c5112ef6 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -249,12 +249,17 @@ def pilinfo(out=None, supported_formats=True): for py_version in py_version[1:]: print(f" {py_version.strip()}", file=out) print("-" * 68, file=out) + print(f"Python executable is {sys.executable or 'unknown'}", file=out) + if sys.prefix != sys.base_prefix: + print(f"Environment Python files loaded from {sys.prefix}", file=out) + print(f"System Python files loaded from {sys.base_prefix}", file=out) + print("-" * 68, file=out) print( - f"Python modules loaded from {os.path.dirname(Image.__file__)}", + f"Python Pillow modules loaded from {os.path.dirname(Image.__file__)}", file=out, ) print( - f"Binary modules loaded from {os.path.dirname(Image.core.__file__)}", + f"Binary Pillow modules loaded from {os.path.dirname(Image.core.__file__)}", file=out, ) print("-" * 68, file=out) From 89c44be404081c77e68457c1c50a78e96a6d42b9 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 20 Feb 2024 20:39:40 +0100 Subject: [PATCH 054/688] Mention `python -m PIL --bugreport` in the issue template --- .github/ISSUE_TEMPLATE/ISSUE_REPORT.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md b/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md index 115f6135d..cfd576f35 100644 --- a/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md +++ b/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md @@ -48,6 +48,10 @@ Thank you. * Python: * Pillow: +```text +please paste the output of running `python3 -m PIL --bugreport` here +``` +