From 26c4798707ad65e63fea0560657cb0c2125ba6c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 18 Sep 2023 20:34:05 +1000 Subject: [PATCH 01/26] Revert "Merge pull request #7311 from k128/main" This reverts commit 39d866b17d99206c11d931e12c108825df68f47b, reversing changes made to f39f74fb82348ce87dfc9e4766ee473132ce84d3. --- Tests/test_file_webp.py | 1 + src/PIL/WebPImagePlugin.py | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 3832441c0..a7b6c735a 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -233,4 +233,5 @@ class TestFileWebp: im.save(out_webp, save_all=True) with Image.open(out_webp) as reloaded: + reloaded.load() assert reloaded.info["duration"] == 1000 diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index a6e1a2a00..028e5d2bd 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -74,9 +74,6 @@ class WebPImageFile(ImageFile.ImageFile): self.info["background"] = (bg_r, bg_g, bg_b, bg_a) self.n_frames = frame_count self.is_animated = self.n_frames > 1 - ret = self._decoder.get_next() - if ret is not None: - self.info["duration"] = ret[1] self._mode = "RGB" if mode == "RGBX" else mode self.rawmode = mode self.tile = [] @@ -93,7 +90,7 @@ class WebPImageFile(ImageFile.ImageFile): self.info["xmp"] = xmp # Initialize seek state - self._reset() + self._reset(reset=False) def _getexif(self): if "exif" not in self.info: @@ -116,8 +113,9 @@ class WebPImageFile(ImageFile.ImageFile): # Set logical frame to requested position self.__logical_frame = frame - def _reset(self): - self._decoder.reset() + def _reset(self, reset=True): + if reset: + self._decoder.reset() self.__physical_frame = 0 self.__loaded = -1 self.__timestamp = 0 From c50b11b3caaf32c9be699f24956628feab3ce9b2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 18 Sep 2023 16:56:29 +0300 Subject: [PATCH 02/26] Attempt download from pillow-depends mirror first --- winbuild/build_prepare.py | 44 ++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index d9122e680..c15d0e583 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -421,25 +421,41 @@ def find_msvs(): } -def extract_dep(url, filename): - import tarfile +def download_dep(url: str, file: str) -> None: import urllib.request + + ex = None + for i in range(3): + try: + print(f"Fetching {url} (attempt {i + 1})...") + content = urllib.request.urlopen(url).read() + with open(file, "wb") as f: + f.write(content) + break + except urllib.error.URLError as e: + ex = e + + if ex: + raise RuntimeError(ex) + + +def extract_dep(url: str, filename: str) -> None: + import tarfile import zipfile file = os.path.join(args.depends_dir, filename) if not os.path.exists(file): - ex = None - for i in range(3): - try: - print("Fetching %s (attempt %d)..." % (url, i + 1)) - content = urllib.request.urlopen(url).read() - with open(file, "wb") as f: - f.write(content) - break - except urllib.error.URLError as e: - ex = e - else: - raise RuntimeError(ex) + # First try our mirror + mirror_url = ( + f"https://raw.githubusercontent.com/" + f"python-pillow/pillow-depends/main/{filename}" + ) + try: + download_dep(mirror_url, file) + except RuntimeError as exc: + # Otherwise try upstream + print(exc) + download_dep(url, file) print("Extracting " + filename) sources_dir_abs = os.path.abspath(sources_dir) From 66bf71bafaccb24b47bc1100216fa5cbaf716b99 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 18 Sep 2023 17:03:18 +0300 Subject: [PATCH 03/26] Add type hints --- winbuild/build_prepare.py | 43 +++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index c15d0e583..d6040d408 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import os import platform @@ -7,36 +9,40 @@ import struct import subprocess -def cmd_cd(path): +def cmd_cd(path: str) -> str: return f"cd /D {path}" -def cmd_set(name, value): +def cmd_set(name: str, value: str) -> str: return f"set {name}={value}" -def cmd_append(name, value): +def cmd_append(name: str, value: str) -> str: op = "path " if name == "PATH" else f"set {name}=" return op + f"%{name}%;{value}" -def cmd_copy(src, tgt): +def cmd_copy(src: str, tgt: str) -> str: return f'copy /Y /B "{src}" "{tgt}"' -def cmd_xcopy(src, tgt): +def cmd_xcopy(src: str, tgt: str) -> str: return f'xcopy /Y /E "{src}" "{tgt}"' -def cmd_mkdir(path): +def cmd_mkdir(path: str) -> str: return f'mkdir "{path}"' -def cmd_rmdir(path): +def cmd_rmdir(path: str) -> str: return f'rmdir /S /Q "{path}"' -def cmd_nmake(makefile=None, target="", params=None): +def cmd_nmake( + makefile: str | None = None, + target: str = "", + params: str | list[str] | tuple[str, ...] = None, +) -> str: if params is None: params = "" elif isinstance(params, (list, tuple)): @@ -55,7 +61,7 @@ def cmd_nmake(makefile=None, target="", params=None): ) -def cmds_cmake(target, *params): +def cmds_cmake(target: str | tuple[str, ...] | list[str], *params) -> list[str]: if not isinstance(target, str): target = " ".join(target) @@ -80,8 +86,11 @@ def cmds_cmake(target, *params): def cmd_msbuild( - file, configuration="Release", target="Build", platform="{msbuild_arch}" -): + file: str, + configuration: str = "Release", + target: str = "Build", + platform: str = "{msbuild_arch}", +) -> str: return " ".join( [ "{msbuild}", @@ -365,7 +374,7 @@ deps = { # based on distutils._msvccompiler from CPython 3.7.4 -def find_msvs(): +def find_msvs() -> dict[str, str] | None: root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles") if not root: print("Program Files not found") @@ -482,7 +491,7 @@ def extract_dep(url: str, filename: str) -> None: raise RuntimeError(msg) -def write_script(name, lines): +def write_script(name: str, lines: list[str]) -> None: name = os.path.join(args.build_dir, name) lines = [line.format(**prefs) for line in lines] print("Writing " + name) @@ -493,7 +502,7 @@ def write_script(name, lines): print(" " + line) -def get_footer(dep): +def get_footer(dep: dict) -> list[str]: lines = [] for out in dep.get("headers", []): lines.append(cmd_copy(out, "{inc_dir}")) @@ -504,7 +513,7 @@ def get_footer(dep): return lines -def build_env(): +def build_env() -> None: lines = [ "if defined DISTUTILS_USE_SDK goto end", cmd_set("INCLUDE", "{inc_dir}"), @@ -520,7 +529,7 @@ def build_env(): write_script("build_env.cmd", lines) -def build_dep(name): +def build_dep(name: str) -> str: dep = deps[name] dir = dep["dir"] file = f"build_dep_{name}.cmd" @@ -570,7 +579,7 @@ def build_dep(name): return file -def build_dep_all(): +def build_dep_all() -> None: lines = [r'call "{build_dir}\build_env.cmd"'] for dep_name in deps: print() From cf79e2c3d3f2c3ef743776707cd79ec36657d946 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 18 Sep 2023 17:06:30 +0300 Subject: [PATCH 04/26] Capitalise constants --- winbuild/build_prepare.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index d6040d408..34bfaa964 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -105,14 +105,14 @@ def cmd_msbuild( SF_PROJECTS = "https://sourceforge.net/projects" -architectures = { +ARCHITECTURES = { "x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"}, "x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } # dependencies, listed in order of compilation -deps = { +DEPS = { "libjpeg": { "url": SF_PROJECTS + "/libjpeg-turbo/files/3.0.0/libjpeg-turbo-3.0.0.tar.gz/download", @@ -530,7 +530,7 @@ def build_env() -> None: def build_dep(name: str) -> str: - dep = deps[name] + dep = DEPS[name] dir = dep["dir"] file = f"build_dep_{name}.cmd" @@ -581,7 +581,7 @@ def build_dep(name: str) -> str: def build_dep_all() -> None: lines = [r'call "{build_dir}\build_env.cmd"'] - for dep_name in deps: + for dep_name in DEPS: print() if dep_name in disabled: print(f"Skipping disabled dependency {dep_name}") @@ -627,7 +627,7 @@ if __name__ == "__main__": ) parser.add_argument( "--architecture", - choices=architectures, + choices=ARCHITECTURES, default=os.environ.get( "ARCHITECTURE", ( @@ -659,7 +659,7 @@ if __name__ == "__main__": ) args = parser.parse_args() - arch_prefs = architectures[args.architecture] + arch_prefs = ARCHITECTURES[args.architecture] print("Target architecture:", args.architecture) msvs = find_msvs() @@ -718,7 +718,7 @@ if __name__ == "__main__": # TODO find NASM automatically } - for k, v in deps.items(): + for k, v in DEPS.items(): prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"]) print() From 82c3999bc969225d616b0cea370d8d6e27ab7896 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 18 Sep 2023 17:10:29 +0300 Subject: [PATCH 05/26] Don't download entire pillow-depends.zip (851 MB) --- .appveyor.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 60132a9a3..cc4d56d0b 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -21,13 +21,11 @@ environment: install: - '%PYTHON%\%EXECUTABLE% --version' - '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip' -- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip - curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip -- 7z x pillow-depends.zip -oc:\ - 7z x pillow-test-images.zip -oc:\ -- mv c:\pillow-depends-main c:\pillow-depends - xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images -- 7z x ..\pillow-depends\nasm-2.16.01-win64.zip -oc:\ +- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip +- 7z x nasm-win64.zip -oc:\ - choco install ghostscript --version=10.0.0.20230317 - path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH% - cd c:\pillow\winbuild\ From 15b1d6085c87d19f6f76c2c94b00d8940be22dd0 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 18 Sep 2023 18:17:47 +0300 Subject: [PATCH 06/26] Use curl with --ssl-no-revoke To fix: curl: (35) schannel: next InitializeSecurityContext failed: Unknown error (0x80092013) - The revocation function was unable to check revocation because the revocation server was offline. --- .appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index cc4d56d0b..4546e2a5b 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -21,10 +21,10 @@ environment: install: - '%PYTHON%\%EXECUTABLE% --version' - '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip' -- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip +- curl -fsSL --ssl-no-revoke -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - 7z x pillow-test-images.zip -oc:\ - xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images -- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip +- curl -fsSL --ssl-no-revoke -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip - 7z x nasm-win64.zip -oc:\ - choco install ghostscript --version=10.0.0.20230317 - path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH% From f4d9c44e3104184a389178b1f80be5bbddad2aac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 20 Sep 2023 19:24:43 +1000 Subject: [PATCH 07/26] Restrict "params" to list or None --- winbuild/build_prepare.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 34bfaa964..a56353e46 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -41,14 +41,9 @@ def cmd_rmdir(path: str) -> str: def cmd_nmake( makefile: str | None = None, target: str = "", - params: str | list[str] | tuple[str, ...] = None, + params: list[str] | None = None, ) -> str: - if params is None: - params = "" - elif isinstance(params, (list, tuple)): - params = " ".join(params) - else: - params = str(params) + params = "" if params is None else " ".join(params) return " ".join( [ From 1ba79d2bee268b1ad21881007fb703a230397153 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 20 Sep 2023 14:07:04 +0300 Subject: [PATCH 08/26] Re-instate for/else to avoid a raise after an error and a subsequent success --- winbuild/build_prepare.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 34bfaa964..20bb846bc 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -443,8 +443,7 @@ def download_dep(url: str, file: str) -> None: break except urllib.error.URLError as e: ex = e - - if ex: + else: raise RuntimeError(ex) From 9c754ebab14b3fe4f2609957d2ae7d11fbad23fe Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 21 Sep 2023 12:20:17 +0300 Subject: [PATCH 09/26] Re-remove --ssl-no-revoke --- .appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 4546e2a5b..cc4d56d0b 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -21,10 +21,10 @@ environment: install: - '%PYTHON%\%EXECUTABLE% --version' - '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip' -- curl -fsSL --ssl-no-revoke -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip +- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - 7z x pillow-test-images.zip -oc:\ - xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images -- curl -fsSL --ssl-no-revoke -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip +- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip - 7z x nasm-win64.zip -oc:\ - choco install ghostscript --version=10.0.0.20230317 - path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH% From 6bbed1add0fa7579fdc09d70d43ad3c1fcc41eee Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 25 Sep 2023 20:10:44 +1000 Subject: [PATCH 10/26] Added has_transparency_data() --- Tests/test_image.py | 25 +++++++++++++++++++++++++ docs/reference/Image.rst | 1 + src/PIL/Image.py | 19 ++++++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 7df1916ef..487035a3e 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -906,6 +906,31 @@ class TestImage: im = Image.new("RGB", size) assert im.tobytes() == b"" + def test_has_transparency_data(self): + for mode in ("1", "L", "P", "RGB"): + im = Image.new(mode, (1, 1)) + assert not im.has_transparency_data() + + for mode in ("LA", "La", "PA", "RGBA", "RGBa"): + im = Image.new(mode, (1, 1)) + assert im.has_transparency_data() + + # P mode with "transparency" info + with Image.open("Tests/images/first_frame_transparency.gif") as im: + assert "transparency" in im.info + assert im.has_transparency_data() + + # RGB mode with "transparency" info + with Image.open("Tests/images/rgb_trns.png") as im: + assert "transparency" in im.info + assert im.has_transparency_data() + + # P mode with RGBA palette + im = Image.new("RGBA", (1, 1)).convert("P") + assert im.mode == "P" + assert im.palette.mode == "RGBA" + assert im.has_transparency_data() + def test_apply_transparency(self): im = Image.new("P", (1, 1)) im.putpalette((0, 0, 0, 1, 1, 1)) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 66e6b2a0c..ae8f923cb 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -195,6 +195,7 @@ This helps to get the bounding box coordinates of the input image:: .. automethod:: PIL.Image.Image.getpalette .. automethod:: PIL.Image.Image.getpixel .. automethod:: PIL.Image.Image.getprojection +.. automethod:: PIL.Image.Image.has_transparency_data .. automethod:: PIL.Image.Image.histogram .. automethod:: PIL.Image.Image.paste .. automethod:: PIL.Image.Image.point diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 2a6b4646b..96ce96d55 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -915,7 +915,7 @@ class Image: self.load() - has_transparency = self.info.get("transparency") is not None + has_transparency = "transparency" in self.info if not mode and self.mode == "P": # determine default mode if self.palette: @@ -1531,6 +1531,23 @@ class Image: rawmode = mode return list(self.im.getpalette(mode, rawmode)) + def has_transparency_data(self): + """ + Determine if an image has transparency data, whether in the form of an + alpha channel, a palette with an alpha channel, or a "transparency" key + in the info dictionary. + + Note the image might still appear solid, if all of the values shown + within are opaque. + + :returns: A boolean. + """ + return ( + self.mode in ("LA", "La", "PA", "RGBA", "RGBa") + or (self.mode == "P" and self.palette.mode == "RGBA") + or "transparency" in self.info + ) + def apply_transparency(self): """ If a P mode image has a "transparency" key in the info dictionary, From ad12caecda408aae101a9e59f0efd8b1be4e6744 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 25 Sep 2023 20:28:25 +1000 Subject: [PATCH 11/26] Convert RGBA palette to RGBA image when saving WebP --- Tests/test_file_webp.py | 10 ++++++++++ src/PIL/WebPImagePlugin.py | 7 +------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 3832441c0..c8e4e6aec 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -234,3 +234,13 @@ class TestFileWebp: with Image.open(out_webp) as reloaded: assert reloaded.info["duration"] == 1000 + + def test_roundtrip_rgba_palette(self, tmp_path): + temp_file = str(tmp_path / "temp.webp") + im = Image.new("RGBA", (1, 1)).convert("P") + assert im.mode == "P" + assert im.palette.mode == "RGBA" + im.save(temp_file) + + with Image.open(temp_file) as im: + assert im.getpixel((0, 0)) == (0, 0, 0, 0) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index a6e1a2a00..86169b780 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -332,12 +332,7 @@ def _save(im, fp, filename): exact = 1 if im.encoderinfo.get("exact") else 0 if im.mode not in _VALID_WEBP_LEGACY_MODES: - alpha = ( - "A" in im.mode - or "a" in im.mode - or (im.mode == "P" and "transparency" in im.info) - ) - im = im.convert("RGBA" if alpha else "RGB") + im = im.convert("RGBA" if im.has_transparency_data() else "RGB") data = _webp.WebPEncode( im.tobytes(), From 0d1e83098d768b8212bd832f06210b9ad5c3f667 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 9 Sep 2023 13:07:12 +0300 Subject: [PATCH 12/26] Add pyupgrade to pre-commit --- .pre-commit-config.yaml | 6 ++++++ src/PIL/BdfFontFile.py | 4 ++-- src/PIL/EpsImagePlugin.py | 4 ++-- src/PIL/ImageGrab.py | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 460661166..5bd004001 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,10 @@ repos: + - repo: https://github.com/asottile/pyupgrade + rev: v3.13.0 + hooks: + - id: pyupgrade + args: [--py38-plus] + - repo: https://github.com/psf/black rev: 23.7.0 hooks: diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index 075d46290..161954831 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -68,11 +68,11 @@ def bdf_char(f): # followed by the width in x (BBw), height in y (BBh), # and x and y displacement (BBxoff0, BByoff0) # of the lower left corner from the origin of the character. - width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()] + width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split()) # The word DWIDTH # followed by the width in x and y of the character in device pixels. - dwx, dwy = [int(p) for p in props["DWIDTH"].split()] + dwx, dwy = (int(p) for p in props["DWIDTH"].split()) bbox = ( (dwx, dwy), diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index b96ce9603..404c8f6a7 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -339,9 +339,9 @@ class EpsImageFile(ImageFile.ImageFile): # data start identifier (the image data follows after a single line # consisting only of this quoted value) image_data_values = byte_arr[11:bytes_read].split(None, 7) - columns, rows, bit_depth, mode_id = [ + columns, rows, bit_depth, mode_id = ( int(value) for value in image_data_values[:4] - ] + ) if bit_depth == 1: self._mode = "1" diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 43019f74a..bcfffc3dc 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -166,7 +166,7 @@ def grabclipboard(): msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" raise NotImplementedError(msg) - p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p = subprocess.run(args, capture_output=True) err = p.stderr if err: msg = f"{args[0]} error: {err.strip().decode()}" From 9d104b241ee671cd0ce704c9a3343656f7137a1a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 9 Sep 2023 13:16:25 +0300 Subject: [PATCH 13/26] Use black-pre-commit-mirror for faster mypyc-compiled wheels --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5bd004001..f952534a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,8 +5,8 @@ repos: - id: pyupgrade args: [--py38-plus] - - repo: https://github.com/psf/black - rev: 23.7.0 + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 23.9.1 hooks: - id: black args: [--target-version=py38] From 507b7d519a013ccbbda2b4d94c63acd1e056976c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 9 Sep 2023 14:03:39 +0300 Subject: [PATCH 14/26] Fix LOG011 avoid pre-formatting log messages --- Tests/helper.py | 4 ++-- src/PIL/TiffImagePlugin.py | 45 ++++++++++++++++++++------------------ 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 69246bfcf..677a0981b 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -91,7 +91,7 @@ def assert_image_equal(a, b, msg=None): if HAS_UPLOADER: try: url = test_image_results.upload(a, b) - logger.error(f"Url for test images: {url}") + logger.error("URL for test images: %s", url) except Exception: pass @@ -126,7 +126,7 @@ def assert_image_similar(a, b, epsilon, msg=None): if HAS_UPLOADER: try: url = test_image_results.upload(a, b) - logger.error(f"Url for test images: {url}") + logger.error("URL for test images: %s", url) except Exception: pass raise e diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 5d3bc4f83..96de03a3e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -823,7 +823,7 @@ class ImageFileDirectory_v2(MutableMapping): try: unit_size, handler = self._load_dispatch[typ] except KeyError: - logger.debug(msg + f" - unsupported type {typ}") + logger.debug("%s - unsupported type %s", msg, typ) continue # ignore unsupported type size = count * unit_size if size > (8 if self._bigtiff else 4): @@ -880,7 +880,7 @@ class ImageFileDirectory_v2(MutableMapping): if tag == STRIPOFFSETS: stripoffsets = len(entries) typ = self.tagtype.get(tag) - logger.debug(f"Tag {tag}, Type: {typ}, Value: {repr(value)}") + logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) is_ifd = typ == TiffTags.LONG and isinstance(value, dict) if is_ifd: if self._endian == "<": @@ -929,7 +929,7 @@ class ImageFileDirectory_v2(MutableMapping): # pass 2: write entries to file for tag, typ, count, value, data in entries: - logger.debug(f"{tag} {typ} {count} {repr(value)} {repr(data)}") + logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data)) result += self._pack("HHL4s", tag, typ, count, value) # -- overwrite here for multi-page -- @@ -1098,8 +1098,8 @@ class TiffImageFile(ImageFile.ImageFile): self._n_frames = None logger.debug("*** TiffImageFile._open ***") - logger.debug(f"- __first: {self.__first}") - logger.debug(f"- ifh: {repr(ifh)}") # Use repr to avoid str(bytes) + logger.debug("- __first: %s", self.__first) + logger.debug("- ifh: %s", repr(ifh)) # Use repr to avoid str(bytes) # and load the first frame self._seek(0) @@ -1137,12 +1137,15 @@ class TiffImageFile(ImageFile.ImageFile): msg = "no more images in TIFF file" raise EOFError(msg) logger.debug( - f"Seeking to frame {frame}, on frame {self.__frame}, " - f"__next {self.__next}, location: {self.fp.tell()}" + "Seeking to frame %s, on frame %s, __next %s, location: %s", + frame, + self.__frame, + self.__next, + self.fp.tell(), ) self.fp.seek(self.__next) self._frame_pos.append(self.__next) - logger.debug("Loading tags, location: %s" % self.fp.tell()) + logger.debug("Loading tags, location: %s", self.fp.tell()) self.tag_v2.load(self.fp) if self.tag_v2.next in self._frame_pos: # This IFD has already been processed @@ -1330,18 +1333,18 @@ class TiffImageFile(ImageFile.ImageFile): fillorder = self.tag_v2.get(FILLORDER, 1) logger.debug("*** Summary ***") - logger.debug(f"- compression: {self._compression}") - logger.debug(f"- photometric_interpretation: {photo}") - logger.debug(f"- planar_configuration: {self._planar_configuration}") - logger.debug(f"- fill_order: {fillorder}") - logger.debug(f"- YCbCr subsampling: {self.tag.get(YCBCRSUBSAMPLING)}") + logger.debug("- compression: %s", self._compression) + logger.debug("- photometric_interpretation: %s", photo) + logger.debug("- planar_configuration: %s", self._planar_configuration) + logger.debug("- fill_order: %s", fillorder) + logger.debug("- YCbCr subsampling: %s", self.tag.get(YCBCRSUBSAMPLING)) # size xsize = int(self.tag_v2.get(IMAGEWIDTH)) ysize = int(self.tag_v2.get(IMAGELENGTH)) self._size = xsize, ysize - logger.debug(f"- size: {self.size}") + logger.debug("- size: %s", self.size) sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,)) if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1: @@ -1397,7 +1400,7 @@ class TiffImageFile(ImageFile.ImageFile): bps_tuple, extra_tuple, ) - logger.debug(f"format key: {key}") + logger.debug("format key: %s", key) try: self._mode, rawmode = OPEN_INFO[key] except KeyError as e: @@ -1405,8 +1408,8 @@ class TiffImageFile(ImageFile.ImageFile): msg = "unknown pixel mode" raise SyntaxError(msg) from e - logger.debug(f"- raw mode: {rawmode}") - logger.debug(f"- pil mode: {self.mode}") + logger.debug("- raw mode: %s", rawmode) + logger.debug("- pil mode: %s", self.mode) self.info["compression"] = self._compression @@ -1447,7 +1450,7 @@ class TiffImageFile(ImageFile.ImageFile): if fillorder == 2: # Replace fillorder with fillorder=1 key = key[:3] + (1,) + key[4:] - logger.debug(f"format key: {key}") + logger.debug("format key: %s", key) # this should always work, since all the # fillorder==2 modes have a corresponding # fillorder=1 mode @@ -1610,7 +1613,7 @@ def _save(im, fp, filename): info = exif else: info = {} - logger.debug("Tiffinfo Keys: %s" % list(info)) + logger.debug("Tiffinfo Keys: %s", list(info)) if isinstance(info, ImageFileDirectory_v1): info = info.to_v2() for key in info: @@ -1743,7 +1746,7 @@ def _save(im, fp, filename): ifd[JPEGQUALITY] = quality logger.debug("Saving using libtiff encoder") - logger.debug("Items: %s" % sorted(ifd.items())) + logger.debug("Items: %s", sorted(ifd.items())) _fp = 0 if hasattr(fp, "fileno"): try: @@ -1811,7 +1814,7 @@ def _save(im, fp, filename): if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1: atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0] - logger.debug("Converted items: %s" % sorted(atts.items())) + logger.debug("Converted items: %s", sorted(atts.items())) # libtiff always expects the bytes in native order. # we're storing image byte order. So, if the rawmode From 36d0bf044b53080b09fcca13998960e4ab2f6035 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 9 Sep 2023 14:04:38 +0300 Subject: [PATCH 15/26] Fix LOG005 use exception() within an exception handler --- Tests/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/helper.py b/Tests/helper.py index 677a0981b..de5468d84 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -126,7 +126,7 @@ def assert_image_similar(a, b, epsilon, msg=None): if HAS_UPLOADER: try: url = test_image_results.upload(a, b) - logger.error("URL for test images: %s", url) + logger.exception("URL for test images: %s", url) except Exception: pass raise e From 3891e9e228a40c140a23215c53e010a3c3b92b58 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 9 Sep 2023 14:04:47 +0300 Subject: [PATCH 16/26] Add flake8-logging to pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f952534a2..4cb991bbe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: hooks: - id: flake8 additional_dependencies: - [flake8-2020, flake8-errmsg, flake8-implicit-str-concat] + [flake8-2020, flake8-errmsg, flake8-implicit-str-concat, flake8-logging] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 From f76b63d016b20d6829e80fb82c4bdfd481a7438a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 9 Sep 2023 14:08:40 +0300 Subject: [PATCH 17/26] Add end-of-file-fixer to pre-commit --- .pre-commit-config.yaml | 2 ++ Tests/fonts/DejaVuSans/LICENSE.txt | 2 +- Tests/icc/LICENSE.txt | 1 - depends/install_raqm.sh | 1 - depends/install_raqm_cmake.sh | 1 - depends/termux.sh | 1 - docs/newer-versions.csv | 2 +- docs/older-versions.csv | 2 +- docs/releasenotes/3.0.0.rst | 1 - docs/releasenotes/3.3.2.rst | 4 ---- docs/releasenotes/4.1.1.rst | 2 -- docs/releasenotes/4.2.1.rst | 1 - docs/releasenotes/8.0.0.rst | 3 --- src/libImaging/Sgi.h | 2 +- 14 files changed, 6 insertions(+), 19 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4cb991bbe..49b60497f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,6 +54,8 @@ repos: - id: check-json - id: check-toml - id: check-yaml + - id: end-of-file-fixer + exclude: ^Tests/images/ - repo: https://github.com/sphinx-contrib/sphinx-lint rev: v0.6.8 diff --git a/Tests/fonts/DejaVuSans/LICENSE.txt b/Tests/fonts/DejaVuSans/LICENSE.txt index 30516578f..be6a4d84c 100644 --- a/Tests/fonts/DejaVuSans/LICENSE.txt +++ b/Tests/fonts/DejaVuSans/LICENSE.txt @@ -37,4 +37,4 @@ The Font Software may be sold as part of a larger software package but no copy o THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. -Except as contained in this notice, the name of Tavmjong Bah shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from Tavmjong Bah. For further information, contact: tavmjong @ free . fr. \ No newline at end of file +Except as contained in this notice, the name of Tavmjong Bah shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from Tavmjong Bah. For further information, contact: tavmjong @ free . fr. diff --git a/Tests/icc/LICENSE.txt b/Tests/icc/LICENSE.txt index 7d289c331..7119461ed 100644 --- a/Tests/icc/LICENSE.txt +++ b/Tests/icc/LICENSE.txt @@ -22,4 +22,3 @@ and that the name of ICC shall not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. ICC makes no representations about the suitability of this software for any purpose. - diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index 24c1f9c30..070ba23a1 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -11,4 +11,3 @@ pushd $archive meson build --prefix=/usr && sudo ninja -C build install popd - diff --git a/depends/install_raqm_cmake.sh b/depends/install_raqm_cmake.sh index 7d2c399df..37d9d1160 100755 --- a/depends/install_raqm_cmake.sh +++ b/depends/install_raqm_cmake.sh @@ -15,4 +15,3 @@ make && sudo make install cd .. popd - diff --git a/depends/termux.sh b/depends/termux.sh index 1acc09c44..d437029fd 100755 --- a/depends/termux.sh +++ b/depends/termux.sh @@ -2,4 +2,3 @@ pkg install -y python ndk-sysroot clang make \ libjpeg-turbo - diff --git a/docs/newer-versions.csv b/docs/newer-versions.csv index 1457d59de..e21caf520 100644 --- a/docs/newer-versions.csv +++ b/docs/newer-versions.csv @@ -5,4 +5,4 @@ Pillow 9.3 - 9.5,,Yes,Yes,Yes,Yes,Yes,, Pillow 9.0 - 9.2,,,Yes,Yes,Yes,Yes,, Pillow 8.3.2 - 8.4,,,Yes,Yes,Yes,Yes,Yes, Pillow 8.0 - 8.3.1,,,,Yes,Yes,Yes,Yes, -Pillow 7.0 - 7.2,,,,,Yes,Yes,Yes,Yes \ No newline at end of file +Pillow 7.0 - 7.2,,,,,Yes,Yes,Yes,Yes diff --git a/docs/older-versions.csv b/docs/older-versions.csv index 6058f0524..aa696bc18 100644 --- a/docs/older-versions.csv +++ b/docs/older-versions.csv @@ -5,4 +5,4 @@ Pillow 5.2 - 5.4,,Yes,Yes,Yes,Yes,,,Yes,,, Pillow 5.0 - 5.1,,,Yes,Yes,Yes,,,Yes,,, Pillow 4,,,Yes,Yes,Yes,Yes,,Yes,,, Pillow 2 - 3,,,,Yes,Yes,Yes,Yes,Yes,Yes,, -Pillow < 2,,,,,,,,Yes,Yes,Yes,Yes \ No newline at end of file +Pillow < 2,,,,,,,,Yes,Yes,Yes,Yes diff --git a/docs/releasenotes/3.0.0.rst b/docs/releasenotes/3.0.0.rst index 67569d337..e8eada73c 100644 --- a/docs/releasenotes/3.0.0.rst +++ b/docs/releasenotes/3.0.0.rst @@ -49,4 +49,3 @@ The external dependencies on libjpeg and zlib are now required by default. If the headers or libraries are not found, then installation will abort with an error. This behaviour can be disabled with the ``--disable-libjpeg`` and ``--disable-zlib`` flags. - diff --git a/docs/releasenotes/3.3.2.rst b/docs/releasenotes/3.3.2.rst index 68a09a3c8..8845b976a 100644 --- a/docs/releasenotes/3.3.2.rst +++ b/docs/releasenotes/3.3.2.rst @@ -34,7 +34,3 @@ image size can lead to a smaller allocation than expected, leading to arbitrary writes. This issue was found by Cris Neckar at Divergent Security. - - - - diff --git a/docs/releasenotes/4.1.1.rst b/docs/releasenotes/4.1.1.rst index 7aa3c1fbf..1b5757015 100644 --- a/docs/releasenotes/4.1.1.rst +++ b/docs/releasenotes/4.1.1.rst @@ -20,5 +20,3 @@ CPython 3.6.1 to not work on installations of C-Python 3.6.0. This fix undefines PySlice_GetIndicesEx if it exists to restore compatibility with both 3.6.0 and 3.6.1. See https://bugs.python.org/issue29943 for more details. - - diff --git a/docs/releasenotes/4.2.1.rst b/docs/releasenotes/4.2.1.rst index c9e953da4..0730936fe 100644 --- a/docs/releasenotes/4.2.1.rst +++ b/docs/releasenotes/4.2.1.rst @@ -8,4 +8,3 @@ Fixed Windows PyPy Build A change in the 4.2.0 cycle broke the Windows PyPy build. This has been fixed, and PyPy is now part of the Windows CI matrix. - diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst index fe2658047..00c691a74 100644 --- a/docs/releasenotes/8.0.0.rst +++ b/docs/releasenotes/8.0.0.rst @@ -175,6 +175,3 @@ Dark theme for docs ^^^^^^^^^^^^^^^^^^^ The https://pillow.readthedocs.io documentation will use a dark theme if the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query. - - - diff --git a/src/libImaging/Sgi.h b/src/libImaging/Sgi.h index 39dd68825..797e5cbf9 100644 --- a/src/libImaging/Sgi.h +++ b/src/libImaging/Sgi.h @@ -36,4 +36,4 @@ typedef struct { /* image data size from file descriptor */ long bufsize; -} SGISTATE; \ No newline at end of file +} SGISTATE; From 1f188f5bb40c504849b06995e4ee5779cac4cd33 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 9 Sep 2023 14:12:47 +0300 Subject: [PATCH 18/26] Add trailing-whitespace to pre-commit --- .github/workflows/release-drafter.yml | 2 +- .github/workflows/stale.yml | 2 +- .pre-commit-config.yaml | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 9e2fdc096..8fc7bd379 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -10,7 +10,7 @@ on: permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 24b8f85d1..31f63e1c6 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -8,7 +8,7 @@ on: permissions: issues: write -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 49b60497f..f0957c73a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,6 +56,8 @@ repos: - id: check-yaml - id: end-of-file-fixer exclude: ^Tests/images/ + - id: trailing-whitespace + exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/sphinx-contrib/sphinx-lint rev: v0.6.8 From 132357ac46ea4d3850bf4bb7e0fb14ad7807f1c6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 9 Sep 2023 14:32:42 +0300 Subject: [PATCH 19/26] Add check-executables-have-shebangs to pre-commit and remove executable flags --- .pre-commit-config.yaml | 1 + Tests/check_j2k_leaks.py | 0 Tests/images/negative_size.ppm | Bin _custom_build/backend.py | 0 4 files changed, 1 insertion(+) mode change 100755 => 100644 Tests/check_j2k_leaks.py mode change 100755 => 100644 Tests/images/negative_size.ppm mode change 100755 => 100644 _custom_build/backend.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f0957c73a..7fe5aacbe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,6 +50,7 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: + - id: check-executables-have-shebangs - id: check-merge-conflict - id: check-json - id: check-toml diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py old mode 100755 new mode 100644 diff --git a/Tests/images/negative_size.ppm b/Tests/images/negative_size.ppm old mode 100755 new mode 100644 diff --git a/_custom_build/backend.py b/_custom_build/backend.py old mode 100755 new mode 100644 From faa66eaa6c1728b1fef23e516e7142f0c914f41d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 26 Sep 2023 20:10:12 +1000 Subject: [PATCH 20/26] Added type hint Co-authored-by: Hugo van Kemenade --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 96ce96d55..d0d5c1884 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1531,7 +1531,7 @@ class Image: rawmode = mode return list(self.im.getpalette(mode, rawmode)) - def has_transparency_data(self): + def has_transparency_data(self) -> bool: """ Determine if an image has transparency data, whether in the form of an alpha channel, a palette with an alpha channel, or a "transparency" key From 1c30809245347efda6bf98ba71ace90240efb302 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 26 Sep 2023 19:54:34 +1000 Subject: [PATCH 21/26] Allow for LA or PA in the future --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d0d5c1884..842e5db56 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1544,7 +1544,7 @@ class Image: """ return ( self.mode in ("LA", "La", "PA", "RGBA", "RGBa") - or (self.mode == "P" and self.palette.mode == "RGBA") + or (self.mode == "P" and self.palette.mode.endswith("A")) or "transparency" in self.info ) From e27d7a6f8484d3fbbff121a7463960caccb1ff97 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 26 Sep 2023 20:14:22 +1000 Subject: [PATCH 22/26] Changed has_transparency_data() to property --- Tests/test_image.py | 10 +++++----- docs/reference/Image.rst | 2 +- src/PIL/Image.py | 1 + src/PIL/WebPImagePlugin.py | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 487035a3e..b9c57770c 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -909,27 +909,27 @@ class TestImage: def test_has_transparency_data(self): for mode in ("1", "L", "P", "RGB"): im = Image.new(mode, (1, 1)) - assert not im.has_transparency_data() + assert not im.has_transparency_data for mode in ("LA", "La", "PA", "RGBA", "RGBa"): im = Image.new(mode, (1, 1)) - assert im.has_transparency_data() + assert im.has_transparency_data # P mode with "transparency" info with Image.open("Tests/images/first_frame_transparency.gif") as im: assert "transparency" in im.info - assert im.has_transparency_data() + assert im.has_transparency_data # RGB mode with "transparency" info with Image.open("Tests/images/rgb_trns.png") as im: assert "transparency" in im.info - assert im.has_transparency_data() + assert im.has_transparency_data # P mode with RGBA palette im = Image.new("RGBA", (1, 1)).convert("P") assert im.mode == "P" assert im.palette.mode == "RGBA" - assert im.has_transparency_data() + assert im.has_transparency_data def test_apply_transparency(self): im = Image.new("P", (1, 1)) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index ae8f923cb..e356469b6 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -195,7 +195,7 @@ This helps to get the bounding box coordinates of the input image:: .. automethod:: PIL.Image.Image.getpalette .. automethod:: PIL.Image.Image.getpixel .. automethod:: PIL.Image.Image.getprojection -.. automethod:: PIL.Image.Image.has_transparency_data +.. autoproperty:: PIL.Image.Image.has_transparency_data .. automethod:: PIL.Image.Image.histogram .. automethod:: PIL.Image.Image.paste .. automethod:: PIL.Image.Image.point diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 842e5db56..244d2e435 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1531,6 +1531,7 @@ class Image: rawmode = mode return list(self.im.getpalette(mode, rawmode)) + @property def has_transparency_data(self) -> bool: """ Determine if an image has transparency data, whether in the form of an diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 86169b780..fa34769e7 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -332,7 +332,7 @@ def _save(im, fp, filename): exact = 1 if im.encoderinfo.get("exact") else 0 if im.mode not in _VALID_WEBP_LEGACY_MODES: - im = im.convert("RGBA" if im.has_transparency_data() else "RGB") + im = im.convert("RGBA" if im.has_transparency_data else "RGB") data = _webp.WebPEncode( im.tobytes(), From a088d54509e42e4eeed37d618b42d775c0d16ef5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Sep 2023 07:05:07 +1000 Subject: [PATCH 23/26] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index d1dce8e0d..428e8d9f4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -29,9 +29,6 @@ Changelog (Pillow) - Added session type check for Linux in ImageGrab.grabclipboard() #7332 [TheNooB2706, radarhere, hugovk] -- Read WebP duration after opening #7311 - [k128, radarhere] - - Allow "loop=None" when saving GIF images #7329 [radarhere] From 4142fc43abda6f793930df4b3dea1c2ed8b85ef2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 30 Sep 2023 10:14:10 +1000 Subject: [PATCH 24/26] Added release notes --- docs/reference/Image.rst | 3 ++- docs/releasenotes/10.1.0.rst | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index e356469b6..4281b182c 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -195,7 +195,6 @@ This helps to get the bounding box coordinates of the input image:: .. automethod:: PIL.Image.Image.getpalette .. automethod:: PIL.Image.Image.getpixel .. automethod:: PIL.Image.Image.getprojection -.. autoproperty:: PIL.Image.Image.has_transparency_data .. automethod:: PIL.Image.Image.histogram .. automethod:: PIL.Image.Image.paste .. automethod:: PIL.Image.Image.point @@ -352,6 +351,8 @@ Instances of the :py:class:`Image` class have the following attributes: .. seealso:: :attr:`~Image.is_animated`, :func:`~Image.seek` and :func:`~Image.tell` +.. autoattribute:: PIL.Image.Image.has_transparency_data + Classes ------- diff --git a/docs/releasenotes/10.1.0.rst b/docs/releasenotes/10.1.0.rst index da5153cce..ad076c1f3 100644 --- a/docs/releasenotes/10.1.0.rst +++ b/docs/releasenotes/10.1.0.rst @@ -32,10 +32,16 @@ TODO API Additions ============= -TODO -^^^^ +has_transparency_data +^^^^^^^^^^^^^^^^^^^^^ -TODO +Images now have :py:attr:`~PIL.Image.Image.has_transparency_data` to indicate +whether the image has transparency data, whether in the form of an alpha +channel, a palette with an alpha channel, or a "transparency" key in the +:py:attr:`~PIL.Image.Image.info` dictionary. + +Even if this attribute is true, the image might still appear solid, if all of +the values shown within are opaque. Security ======== From 2433ffadf228a93992e35b628bd9d18c5c299155 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 30 Sep 2023 12:54:05 +1000 Subject: [PATCH 25/26] Updated macOS tested Pillow versions --- docs/installation.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/installation.rst b/docs/installation.rst index ca78b2998..81f05c0d9 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -498,6 +498,8 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+===========================+==================+==============+ +| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | ++----------------------------------+---------------------------+------------------+--------------+ | macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | | +---------------------------+------------------+ | | | 3.7 | 9.5.0 | | From 9ed9b115328e262de184537f29818ba66ea2e6b0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 30 Sep 2023 17:41:43 +1000 Subject: [PATCH 26/26] Corrected macOS version name --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 81f05c0d9..8af936b08 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -504,7 +504,7 @@ These platforms have been reported to work at the versions mentioned. | +---------------------------+------------------+ | | | 3.7 | 9.5.0 | | +----------------------------------+---------------------------+------------------+--------------+ -| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | +| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ | macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | | +---------------------------+------------------+--------------+