From 0c335d239bb6f73a45795a8d814670f9ddc8aa45 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Apr 2024 12:38:28 -0500 Subject: [PATCH 01/20] use (void) for empty function parameters --- src/libImaging/JpegDecode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/JpegDecode.c b/src/libImaging/JpegDecode.c index 30c64f235..2970f56d1 100644 --- a/src/libImaging/JpegDecode.c +++ b/src/libImaging/JpegDecode.c @@ -48,7 +48,7 @@ char *libjpeg_turbo_version = NULL; #endif int -ImagingJpegUseJCSExtensions() { +ImagingJpegUseJCSExtensions(void) { int use_jcs_extensions = 0; #ifdef JCS_EXTENSIONS #if defined(LIBJPEG_TURBO_VERSION_NUMBER) From 23c461e431e84fbfb81fca10c5a81c047884559b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Sep 2024 11:56:06 +1000 Subject: [PATCH 02/20] Handle duplicate exif header Co-authored-by: Jamil Zakirov --- Tests/test_image.py | 8 ++++++++ src/PIL/Image.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index b0d719667..97e97acaa 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -775,6 +775,14 @@ class TestImage: exif.load(b"Exif\x00\x00") assert not dict(exif) + def test_duplicate_exif_header(self) -> None: + with Image.open("Tests/images/exif.png") as im: + im.load() + im.info["exif"] = b"Exif\x00\x00" + im.info["exif"] + + exif = im.getexif() + assert exif[274] == 1 + def test_empty_get_ifd(self) -> None: exif = Image.Exif() ifd = exif.get_ifd(0x8769) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1c0cf2936..2fb677ead 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3968,7 +3968,7 @@ class Exif(_ExifBase): self._data.clear() self._hidden_data.clear() self._ifds.clear() - if data and data.startswith(b"Exif\x00\x00"): + while data and data.startswith(b"Exif\x00\x00"): data = data[6:] if not data: self._info = None From 06c331af5d85825b7c2595ca3861730d52b22e00 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Sep 2024 15:16:59 +1000 Subject: [PATCH 03/20] Added type hints to frombuffer --- Tests/test_file_jpeg.py | 4 +++- Tests/test_imagefile.py | 2 +- docs/example/DdsImagePlugin.py | 4 ++-- src/PIL/BlpImagePlugin.py | 2 +- src/PIL/BmpImagePlugin.py | 2 +- src/PIL/DdsImagePlugin.py | 2 +- src/PIL/FitsImagePlugin.py | 2 +- src/PIL/Image.py | 13 ++++++++++--- src/PIL/ImageFile.py | 2 +- src/PIL/MspImagePlugin.py | 2 +- src/PIL/PpmImagePlugin.py | 4 ++-- src/PIL/QoiImagePlugin.py | 2 +- src/PIL/SgiImagePlugin.py | 2 +- 13 files changed, 26 insertions(+), 17 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index affc18625..cde951395 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1011,7 +1011,9 @@ class TestFileJpeg: # Even though this decoder never says that it is finished # the image should still end when there is no new data class InfiniteMockPyDecoder(ImageFile.PyDecoder): - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode( + self, buffer: bytes | Image.SupportsArrayInterface + ) -> tuple[int, int]: return 0, 0 Image.register_decoder("INFINITE", InfiniteMockPyDecoder) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 0985dce9c..1ee684926 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -210,7 +210,7 @@ class MockPyDecoder(ImageFile.PyDecoder): super().__init__(mode, *args) - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: # eof return -1, 0 diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index d8f536076..10828bff0 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -257,7 +257,7 @@ class DdsImageFile(ImageFile.ImageFile): class DXT1Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None try: self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize)) @@ -270,7 +270,7 @@ class DXT1Decoder(ImageFile.PyDecoder): class DXT5Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None try: self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize)) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 8e29898df..e5605635e 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -279,7 +279,7 @@ class BlpImageFile(ImageFile.ImageFile): class _BLPBaseDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: try: self._read_blp_header() self._load() diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 16c8cf6ff..bf8f29577 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -321,7 +321,7 @@ class BmpImageFile(ImageFile.ImageFile): class BmpRleDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None rle4 = self.args[1] data = bytearray() diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 631532470..1b6408237 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -481,7 +481,7 @@ class DdsImageFile(ImageFile.ImageFile): class DdsRgbDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None bitcount, masks = self.args diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index abacc5e67..6bbd2641a 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -126,7 +126,7 @@ class FitsImageFile(ImageFile.ImageFile): class FitsGzipDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None value = gzip.decompress(self.fd.read()) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1c0cf2936..90d028b9b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -856,7 +856,10 @@ class Image: ) def frombytes( - self, data: bytes | bytearray, decoder_name: str = "raw", *args: Any + self, + data: bytes | bytearray | SupportsArrayInterface, + decoder_name: str = "raw", + *args: Any, ) -> None: """ Loads this image with pixel data from a bytes object. @@ -3145,7 +3148,7 @@ def new( def frombytes( mode: str, size: tuple[int, int], - data: bytes | bytearray, + data: bytes | bytearray | SupportsArrayInterface, decoder_name: str = "raw", *args: Any, ) -> Image: @@ -3189,7 +3192,11 @@ def frombytes( def frombuffer( - mode: str, size: tuple[int, int], data, decoder_name: str = "raw", *args: Any + mode: str, + size: tuple[int, int], + data: bytes | SupportsArrayInterface, + decoder_name: str = "raw", + *args: Any, ) -> Image: """ Creates an image memory referencing pixel data in a byte buffer. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 615961ce5..b8bf0b7fe 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -733,7 +733,7 @@ class PyDecoder(PyCodec): def pulls_fd(self) -> bool: return self._pulls_fd - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: """ Override to perform the decoding process. diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 1363ddd1f..f3460a787 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -112,7 +112,7 @@ class MspDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None img = io.BytesIO() diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 3bda2bf28..4e779df17 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -284,7 +284,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): break return data - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: self._comment_spans = False if self.mode == "1": data = self._decode_bitonal() @@ -300,7 +300,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): class PpmDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None data = bytearray() diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index eaf031402..010d3f941 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -47,7 +47,7 @@ class QoiDecoder(ImageFile.PyDecoder): hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 self._previously_seen_pixels[hash_value] = value - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None self._previously_seen_pixels = {} diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index 2c205a926..44254b7a4 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -214,7 +214,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: class SGI16Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None assert self.im is not None From 94315f87c32d67c097ae8fb6eb8052bba9384af4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Sep 2024 19:13:35 +1000 Subject: [PATCH 04/20] Removed unused IFD_LEGACY_API --- docs/deprecations.rst | 7 +++++++ docs/releasenotes/11.0.0.rst | 5 +++++ src/PIL/TiffImagePlugin.py | 3 +-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a9498d5ed..cafda3803 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -142,6 +142,13 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +TiffImagePlugin IFD_LEGACY_API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionremoved:: 11.0.0 + +``TiffImagePlugin.IFD_LEGACY_API`` was removed, as it was an unused setting. + PSFile ~~~~~~ diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index ac9237acf..f0d864f6a 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -40,6 +40,11 @@ removed. Pillow's C API will now be used on PyPy instead. ``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, was similarly removed. +TiffImagePlugin IFD_LEGACY_API +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An unused setting, ``TiffImagePlugin.IFD_LEGACY_API``, has been removed. + Deprecations ============ diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 88f75ce8e..87d9677e0 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -69,7 +69,6 @@ logger = logging.getLogger(__name__) # Set these to true to force use of libtiff for reading or writing. READ_LIBTIFF = False WRITE_LIBTIFF = False -IFD_LEGACY_API = True STRIP_SIZE = 65536 II = b"II" # little-endian (Intel style) @@ -1125,7 +1124,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): return val -# undone -- switch this pointer when IFD_LEGACY_API == False +# undone -- switch this pointer ImageFileDirectory = ImageFileDirectory_v1 From 93a0bf02760e6b9e1764eb5f15c69a2c12d96e00 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Sep 2024 21:33:30 +1000 Subject: [PATCH 05/20] Added type hints to TiffImagePlugin _save --- docs/conf.py | 2 +- src/PIL/TiffImagePlugin.py | 26 +++++++++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 41f9c0e38..a0f5867d7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -121,7 +121,7 @@ nitpicky = True # generating warnings in “nitpicky mode”. Note that type should include the domain name # if present. Example entries would be ('py:func', 'int') or # ('envvar', 'LD_LIBRARY_PATH'). -# nitpick_ignore = [] +nitpick_ignore = [("py:class", "_io.BytesIO")] # -- Options for HTML output ---------------------------------------------- diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 88f75ce8e..53f6ae621 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -46,6 +46,7 @@ import logging import math import os import struct +import sys import warnings from collections.abc import Iterator, MutableMapping from fractions import Fraction @@ -1647,7 +1648,7 @@ SAVE_INFO = { } -def _save(im: Image.Image, fp, filename: str | bytes) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError as e: @@ -1958,7 +1959,7 @@ def _save(im: Image.Image, fp, filename: str | bytes) -> None: setattr(im, "_debug_multipage", ifd) -class AppendingTiffWriter: +class AppendingTiffWriter(io.BytesIO): fieldSizes = [ 0, # None 1, # byte @@ -2068,6 +2069,12 @@ class AppendingTiffWriter: return self.f.tell() - self.offsetOfNewPage def seek(self, offset: int, whence: int = io.SEEK_SET) -> int: + """ + :param offset: Distance to seek. + :param whence: Whether the distance is relative to the start, + end or current position. + :returns: The resulting position, relative to the start. + """ if whence == os.SEEK_SET: offset += self.offsetOfNewPage @@ -2101,8 +2108,16 @@ class AppendingTiffWriter: num_tags = self.readShort() self.f.seek(num_tags * 12, os.SEEK_CUR) - def write(self, data: bytes) -> int | None: - return self.f.write(data) + if sys.version_info >= (3, 12): + from collections.abc import Buffer + + def write(self, data: Buffer, /) -> int: + return self.f.write(data) + + else: + + def write(self, data: Any, /) -> int: + return self.f.write(data) def readShort(self) -> int: (value,) = struct.unpack(self.shortFmt, self.f.read(2)) @@ -2143,7 +2158,8 @@ class AppendingTiffWriter: def close(self) -> None: self.finalize() - self.f.close() + if self.close_fp: + self.f.close() def fixIFD(self) -> None: num_tags = self.readShort() From a6ca45aea661bd037ad0c46771fd047575ab138a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 14:55:45 +1000 Subject: [PATCH 06/20] Use (void) to indicate no parameters --- src/libImaging/Access.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index bf7db281e..1c1937105 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -185,7 +185,7 @@ put_pixel_32(Imaging im, int x, int y, const void *color) { } void -ImagingAccessInit() { +ImagingAccessInit(void) { #define ADD(mode_, get_pixel_, put_pixel_) \ { \ ImagingAccess access = add_item(mode_); \ From 1105256f2b5e6817a23b6215a32bcf1d26687d60 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 18:25:44 +1000 Subject: [PATCH 07/20] Added type hints to setup.py --- setup.py | 257 +++++++++++++++++++++++++++++++------------------------ tox.ini | 2 +- 2 files changed, 146 insertions(+), 113 deletions(-) diff --git a/setup.py b/setup.py index 863d102cf..60707083f 100644 --- a/setup.py +++ b/setup.py @@ -15,18 +15,20 @@ import struct import subprocess import sys import warnings +from collections.abc import Iterator +from typing import Any from setuptools import Extension, setup from setuptools.command.build_ext import build_ext -def get_version(): +def get_version() -> str: version_file = "src/PIL/_version.py" with open(version_file, encoding="utf-8") as f: return f.read().split('"')[1] -configuration = {} +configuration: dict[str, list[str]] = {} PILLOW_VERSION = get_version() @@ -143,7 +145,7 @@ class RequiredDependencyException(Exception): PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version -def _dbg(s, tp=None): +def _dbg(s: str, tp: Any = None) -> None: if DEBUG: if tp: print(s % tp) @@ -151,10 +153,13 @@ def _dbg(s, tp=None): print(s) -def _find_library_dirs_ldconfig(): +def _find_library_dirs_ldconfig() -> list[str]: # Based on ctypes.util from Python 2 ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" + args: list[str] + env: dict[str, str] + expr: str if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): if struct.calcsize("l") == 4: machine = os.uname()[4] + "-32" @@ -184,13 +189,11 @@ def _find_library_dirs_ldconfig(): try: p = subprocess.Popen( - args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env + args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env, text=True ) except OSError: # E.g. command not found return [] - [data, _] = p.communicate() - if isinstance(data, bytes): - data = data.decode("latin1") + data = p.communicate()[0] dirs = [] for dll in re.findall(expr, data): @@ -200,7 +203,9 @@ def _find_library_dirs_ldconfig(): return dirs -def _add_directory(path, subdir, where=None): +def _add_directory( + path: list[str], subdir: str | None, where: int | None = None +) -> None: if subdir is None: return subdir = os.path.realpath(subdir) @@ -216,7 +221,7 @@ def _add_directory(path, subdir, where=None): path.insert(where, subdir) -def _find_include_file(self, include): +def _find_include_file(self: pil_build_ext, include: str) -> int: for directory in self.compiler.include_dirs: _dbg("Checking for include file %s in %s", (include, directory)) if os.path.isfile(os.path.join(directory, include)): @@ -225,7 +230,7 @@ def _find_include_file(self, include): return 0 -def _find_library_file(self, library): +def _find_library_file(self: pil_build_ext, library: str) -> str | None: ret = self.compiler.find_library_file(self.compiler.library_dirs, library) if ret: _dbg("Found library %s at %s", (library, ret)) @@ -234,7 +239,7 @@ def _find_library_file(self, library): return ret -def _find_include_dir(self, dirname, include): +def _find_include_dir(self: pil_build_ext, dirname: str, include: str) -> bool | str: for directory in self.compiler.include_dirs: _dbg("Checking for include file %s in %s", (include, directory)) if os.path.isfile(os.path.join(directory, include)): @@ -245,6 +250,7 @@ def _find_include_dir(self, dirname, include): if os.path.isfile(os.path.join(subdir, include)): _dbg("Found %s in %s", (include, subdir)) return subdir + return False def _cmd_exists(cmd: str) -> bool: @@ -256,7 +262,7 @@ def _cmd_exists(cmd: str) -> bool: ) -def _pkg_config(name): +def _pkg_config(name: str) -> tuple[list[str], list[str]] | None: command = os.environ.get("PKG_CONFIG", "pkg-config") for keep_system in (True, False): try: @@ -283,10 +289,11 @@ def _pkg_config(name): return libs, cflags except Exception: pass + return None class pil_build_ext(build_ext): - class feature: + class ext_feature: features = [ "zlib", "jpeg", @@ -301,25 +308,32 @@ class pil_build_ext(build_ext): ] required = {"jpeg", "zlib"} - vendor = set() + vendor: set[str] = set() - def __init__(self): + def __init__(self) -> None: + self._settings: dict[str, str | bool | None] = {} for f in self.features: - setattr(self, f, None) + self.set(f, None) - def require(self, feat): + def require(self, feat: str) -> bool: return feat in self.required - def want(self, feat): - return getattr(self, feat) is None + def get(self, feat: str) -> str | bool | None: + return self._settings[feat] - def want_vendor(self, feat): + def set(self, feat: str, value: str | bool | None) -> None: + self._settings[feat] = value + + def want(self, feat: str) -> bool: + return self._settings[feat] is None + + def want_vendor(self, feat: str) -> bool: return feat in self.vendor - def __iter__(self): + def __iter__(self) -> Iterator[str]: yield from self.features - feature = feature() + feature = ext_feature() user_options = ( build_ext.user_options @@ -337,10 +351,10 @@ class pil_build_ext(build_ext): ) @staticmethod - def check_configuration(option, value): + def check_configuration(option: str, value: str) -> bool | None: return True if value in configuration.get(option, []) else None - def initialize_options(self): + def initialize_options(self) -> None: self.disable_platform_guessing = self.check_configuration( "platform-guessing", "disable" ) @@ -355,7 +369,7 @@ class pil_build_ext(build_ext): self.debug = True self.parallel = configuration.get("parallel", [None])[-1] - def finalize_options(self): + def finalize_options(self) -> None: build_ext.finalize_options(self) if self.debug: global DEBUG @@ -363,12 +377,16 @@ class pil_build_ext(build_ext): if not self.parallel: # If --parallel (or -j) wasn't specified, we want to reproduce the same # behavior as before, that is, auto-detect the number of jobs. - try: - self.parallel = int( - os.environ.get("MAX_CONCURRENCY", min(4, os.cpu_count())) - ) - except TypeError: - self.parallel = None + self.parallel = None + + cpu_count = os.cpu_count() + if cpu_count is not None: + try: + self.parallel = int( + os.environ.get("MAX_CONCURRENCY", min(4, cpu_count)) + ) + except TypeError: + pass for x in self.feature: if getattr(self, f"disable_{x}"): setattr(self.feature, x, False) @@ -402,7 +420,13 @@ class pil_build_ext(build_ext): _dbg("Using vendored version of %s", x) self.feature.vendor.add(x) - def _update_extension(self, name, libraries, define_macros=None, sources=None): + def _update_extension( + self, + name: str, + libraries: list[str] | list[str | bool | None], + define_macros: list[tuple[str, str | None]] | None = None, + sources: list[str] | None = None, + ) -> None: for extension in self.extensions: if extension.name == name: extension.libraries += libraries @@ -415,13 +439,13 @@ class pil_build_ext(build_ext): extension.extra_link_args = ["--stdlib=libc++"] break - def _remove_extension(self, name): + def _remove_extension(self, name: str) -> None: for extension in self.extensions: if extension.name == name: self.extensions.remove(extension) break - def get_macos_sdk_path(self): + def get_macos_sdk_path(self) -> str | None: try: sdk_path = ( subprocess.check_output(["xcrun", "--show-sdk-path"]) @@ -442,9 +466,9 @@ class pil_build_ext(build_ext): sdk_path = commandlinetools_sdk_path return sdk_path - def build_extensions(self): - library_dirs = [] - include_dirs = [] + def build_extensions(self) -> None: + library_dirs: list[str] = [] + include_dirs: list[str] = [] pkg_config = None if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): @@ -468,19 +492,22 @@ class pil_build_ext(build_ext): root = globals()[root_name] if root is None and root_name in os.environ: - prefix = os.environ[root_name] - root = (os.path.join(prefix, "lib"), os.path.join(prefix, "include")) + root_prefix = os.environ[root_name] + root = ( + os.path.join(root_prefix, "lib"), + os.path.join(root_prefix, "include"), + ) if root is None and pkg_config: - if isinstance(lib_name, tuple): + if isinstance(lib_name, str): + _dbg(f"Looking for `{lib_name}` using pkg-config.") + root = pkg_config(lib_name) + else: for lib_name2 in lib_name: _dbg(f"Looking for `{lib_name2}` using pkg-config.") root = pkg_config(lib_name2) if root: break - else: - _dbg(f"Looking for `{lib_name}` using pkg-config.") - root = pkg_config(lib_name) if isinstance(root, tuple): lib_root, include_root = root @@ -660,22 +687,22 @@ class pil_build_ext(build_ext): _dbg("Looking for zlib") if _find_include_file(self, "zlib.h"): if _find_library_file(self, "z"): - feature.zlib = "z" + feature.set("zlib", "z") elif sys.platform == "win32" and _find_library_file(self, "zlib"): - feature.zlib = "zlib" # alternative name + feature.set("zlib", "zlib") # alternative name if feature.want("jpeg"): _dbg("Looking for jpeg") if _find_include_file(self, "jpeglib.h"): if _find_library_file(self, "jpeg"): - feature.jpeg = "jpeg" + feature.set("jpeg", "jpeg") elif sys.platform == "win32" and _find_library_file(self, "libjpeg"): - feature.jpeg = "libjpeg" # alternative name + feature.set("jpeg", "libjpeg") # alternative name - feature.openjpeg_version = None + feature.set("openjpeg_version", None) if feature.want("jpeg2000"): _dbg("Looking for jpeg2000") - best_version = None + best_version: tuple[int, ...] | None = None best_path = None # Find the best version @@ -705,26 +732,26 @@ class pil_build_ext(build_ext): # rather than having to cope with the versioned # include path _add_directory(self.compiler.include_dirs, best_path, 0) - feature.jpeg2000 = "openjp2" - feature.openjpeg_version = ".".join(str(x) for x in best_version) + feature.set("jpeg2000", "openjp2") + feature.set("openjpeg_version", ".".join(str(x) for x in best_version)) if feature.want("imagequant"): _dbg("Looking for imagequant") if _find_include_file(self, "libimagequant.h"): if _find_library_file(self, "imagequant"): - feature.imagequant = "imagequant" + feature.set("imagequant", "imagequant") elif _find_library_file(self, "libimagequant"): - feature.imagequant = "libimagequant" + feature.set("imagequant", "libimagequant") if feature.want("tiff"): _dbg("Looking for tiff") if _find_include_file(self, "tiff.h"): if _find_library_file(self, "tiff"): - feature.tiff = "tiff" + feature.set("tiff", "tiff") if sys.platform in ["win32", "darwin"] and _find_library_file( self, "libtiff" ): - feature.tiff = "libtiff" + feature.set("tiff", "libtiff") if feature.want("freetype"): _dbg("Looking for freetype") @@ -745,31 +772,31 @@ class pil_build_ext(build_ext): freetype_version = 21 break if freetype_version: - feature.freetype = "freetype" + feature.set("freetype", "freetype") if subdir: _add_directory(self.compiler.include_dirs, subdir, 0) - if feature.freetype and feature.want("raqm"): + if feature.get("freetype") and feature.want("raqm"): if not feature.want_vendor("raqm"): # want system Raqm _dbg("Looking for Raqm") if _find_include_file(self, "raqm.h"): if _find_library_file(self, "raqm"): - feature.raqm = "raqm" + feature.set("raqm", "raqm") elif _find_library_file(self, "libraqm"): - feature.raqm = "libraqm" + feature.set("raqm", "libraqm") else: # want to build Raqm from src/thirdparty _dbg("Looking for HarfBuzz") - feature.harfbuzz = None + feature.set("harfbuzz", None) hb_dir = _find_include_dir(self, "harfbuzz", "hb.h") if hb_dir: if isinstance(hb_dir, str): _add_directory(self.compiler.include_dirs, hb_dir, 0) if _find_library_file(self, "harfbuzz"): - feature.harfbuzz = "harfbuzz" - if feature.harfbuzz: + feature.set("harfbuzz", "harfbuzz") + if feature.get("harfbuzz"): if not feature.want_vendor("fribidi"): # want system FriBiDi _dbg("Looking for FriBiDi") - feature.fribidi = None + feature.set("fribidi", None) fribidi_dir = _find_include_dir(self, "fribidi", "fribidi.h") if fribidi_dir: if isinstance(fribidi_dir, str): @@ -777,19 +804,19 @@ class pil_build_ext(build_ext): self.compiler.include_dirs, fribidi_dir, 0 ) if _find_library_file(self, "fribidi"): - feature.fribidi = "fribidi" - feature.raqm = True + feature.set("fribidi", "fribidi") + feature.set("raqm", True) else: # want to build FriBiDi shim from src/thirdparty - feature.raqm = True + feature.set("raqm", True) if feature.want("lcms"): _dbg("Looking for lcms") if _find_include_file(self, "lcms2.h"): if _find_library_file(self, "lcms2"): - feature.lcms = "lcms2" + feature.set("lcms", "lcms2") elif _find_library_file(self, "lcms2_static"): # alternate Windows name. - feature.lcms = "lcms2_static" + feature.set("lcms", "lcms2_static") if feature.want("webp"): _dbg("Looking for webp") @@ -803,17 +830,17 @@ class pil_build_ext(build_ext): _find_library_file(self, prefix + library) for library in ("webp", "webpmux", "webpdemux") ): - feature.webp = prefix + "webp" + feature.set("webp", prefix + "webp") break if feature.want("xcb"): _dbg("Looking for xcb") if _find_include_file(self, "xcb/xcb.h"): if _find_library_file(self, "xcb"): - feature.xcb = "xcb" + feature.set("xcb", "xcb") for f in feature: - if not getattr(feature, f) and feature.require(f): + if not feature.get(f) and feature.require(f): if f in ("jpeg", "zlib"): raise RequiredDependencyException(f) raise DependencyException(f) @@ -821,10 +848,11 @@ class pil_build_ext(build_ext): # # core library - libs = self.add_imaging_libs.split() - defs = [] - if feature.tiff: - libs.append(feature.tiff) + libs: list[str | bool | None] = [] + libs.extend(self.add_imaging_libs.split()) + defs: list[tuple[str, str | None]] = [] + if feature.get("tiff"): + libs.append(feature.get("tiff")) defs.append(("HAVE_LIBTIFF", None)) if sys.platform == "win32": # This define needs to be defined if-and-only-if it was defined @@ -832,22 +860,22 @@ class pil_build_ext(build_ext): # so we have to guess; by default it is defined in all Windows builds. # See #4237, #5243, #5359 for more information. defs.append(("USE_WIN32_FILEIO", None)) - if feature.jpeg: - libs.append(feature.jpeg) + if feature.get("jpeg"): + libs.append(feature.get("jpeg")) defs.append(("HAVE_LIBJPEG", None)) - if feature.jpeg2000: - libs.append(feature.jpeg2000) + if feature.get("jpeg2000"): + libs.append(feature.get("jpeg2000")) defs.append(("HAVE_OPENJPEG", None)) if sys.platform == "win32" and not PLATFORM_MINGW: defs.append(("OPJ_STATIC", None)) - if feature.zlib: - libs.append(feature.zlib) + if feature.get("zlib"): + libs.append(feature.get("zlib")) defs.append(("HAVE_LIBZ", None)) - if feature.imagequant: - libs.append(feature.imagequant) + if feature.get("imagequant"): + libs.append(feature.get("imagequant")) defs.append(("HAVE_LIBIMAGEQUANT", None)) - if feature.xcb: - libs.append(feature.xcb) + if feature.get("xcb"): + libs.append(feature.get("xcb")) defs.append(("HAVE_XCB", None)) if sys.platform == "win32": libs.extend(["kernel32", "user32", "gdi32"]) @@ -861,22 +889,22 @@ class pil_build_ext(build_ext): # # additional libraries - if feature.freetype: + if feature.get("freetype"): srcs = [] libs = ["freetype"] defs = [] - if feature.raqm: + if feature.get("raqm"): if not feature.want_vendor("raqm"): # using system Raqm defs.append(("HAVE_RAQM", None)) defs.append(("HAVE_RAQM_SYSTEM", None)) - libs.append(feature.raqm) + libs.append(feature.get("raqm")) else: # building Raqm from src/thirdparty defs.append(("HAVE_RAQM", None)) srcs.append("src/thirdparty/raqm/raqm.c") - libs.append(feature.harfbuzz) + libs.append(feature.get("harfbuzz")) if not feature.want_vendor("fribidi"): # using system FriBiDi defs.append(("HAVE_FRIBIDI_SYSTEM", None)) - libs.append(feature.fribidi) + libs.append(feature.get("fribidi")) else: # building FriBiDi shim from src/thirdparty srcs.append("src/thirdparty/fribidi-shim/fribidi.c") self._update_extension("PIL._imagingft", libs, defs, srcs) @@ -884,16 +912,17 @@ class pil_build_ext(build_ext): else: self._remove_extension("PIL._imagingft") - if feature.lcms: - extra = [] + if feature.get("lcms"): + libs = [feature.get("lcms")] if sys.platform == "win32": - extra.extend(["user32", "gdi32"]) - self._update_extension("PIL._imagingcms", [feature.lcms] + extra) + libs.extend(["user32", "gdi32"]) + self._update_extension("PIL._imagingcms", libs) else: self._remove_extension("PIL._imagingcms") - if feature.webp: - libs = [feature.webp, feature.webp + "mux", feature.webp + "demux"] + webp = feature.get("webp") + if isinstance(webp, str): + libs = [webp, webp + "mux", webp + "demux"] self._update_extension("PIL._webp", libs) else: self._remove_extension("PIL._webp") @@ -908,14 +937,14 @@ class pil_build_ext(build_ext): self.summary_report(feature) - def summary_report(self, feature): + def summary_report(self, feature: ext_feature) -> None: print("-" * 68) print("PIL SETUP SUMMARY") print("-" * 68) print(f"version Pillow {PILLOW_VERSION}") - v = sys.version.split("[") - print(f"platform {sys.platform} {v[0].strip()}") - for v in v[1:]: + version = sys.version.split("[") + print(f"platform {sys.platform} {version[0].strip()}") + for v in version[1:]: print(f" [{v.strip()}") print("-" * 68) @@ -926,16 +955,20 @@ class pil_build_ext(build_ext): raqm_extra_info += ", FriBiDi shim" options = [ - (feature.jpeg, "JPEG"), - (feature.jpeg2000, "OPENJPEG (JPEG2000)", feature.openjpeg_version), - (feature.zlib, "ZLIB (PNG/ZIP)"), - (feature.imagequant, "LIBIMAGEQUANT"), - (feature.tiff, "LIBTIFF"), - (feature.freetype, "FREETYPE2"), - (feature.raqm, "RAQM (Text shaping)", raqm_extra_info), - (feature.lcms, "LITTLECMS2"), - (feature.webp, "WEBP"), - (feature.xcb, "XCB (X protocol)"), + (feature.get("jpeg"), "JPEG"), + ( + feature.get("jpeg2000"), + "OPENJPEG (JPEG2000)", + feature.get("openjpeg_version"), + ), + (feature.get("zlib"), "ZLIB (PNG/ZIP)"), + (feature.get("imagequant"), "LIBIMAGEQUANT"), + (feature.get("tiff"), "LIBTIFF"), + (feature.get("freetype"), "FREETYPE2"), + (feature.get("raqm"), "RAQM (Text shaping)", raqm_extra_info), + (feature.get("lcms"), "LITTLECMS2"), + (feature.get("webp"), "WEBP"), + (feature.get("xcb"), "XCB (X protocol)"), ] all = 1 @@ -964,7 +997,7 @@ class pil_build_ext(build_ext): print("") -def debug_build(): +def debug_build() -> bool: return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD diff --git a/tox.ini b/tox.ini index 70b8bf145..e79d88500 100644 --- a/tox.ini +++ b/tox.ini @@ -36,4 +36,4 @@ deps = extras = typing commands = - mypy docs src winbuild Tests {posargs} + mypy conftest.py selftest.py setup.py docs src winbuild Tests {posargs} From a3d23165b9414b17aee42239bead5034d6fa97c5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 19:40:29 +1000 Subject: [PATCH 08/20] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b43466833..c5beebeba 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Handle duplicate EXIF header #8350 + [zakajd, radarhere] + - Return early from BoxBlur if either width or height is zero #8347 [radarhere] From 8b3ef3784bc655e346028a72844d5c6e04083f47 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 8 Sep 2024 08:30:30 +1000 Subject: [PATCH 09/20] Moved Buffer into _typing --- docs/reference/internal_modules.rst | 4 ++++ src/PIL/GifImagePlugin.py | 17 ++++------------- src/PIL/TiffImagePlugin.py | 15 +++------------ src/PIL/_typing.py | 7 ++++++- 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index 31d60cd83..93fd82cf9 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -33,6 +33,10 @@ Internal Modules Provides a convenient way to import type hints that are not available on some Python versions. +.. py:class:: Buffer + + Typing alias. + .. py:class:: IntegralLike Typing alias. diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index a27f7aae9..f206fbb9c 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -29,7 +29,6 @@ import itertools import math import os import subprocess -import sys from enum import IntEnum from functools import cached_property from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union @@ -49,6 +48,7 @@ from ._binary import o16le as o16 if TYPE_CHECKING: from . import _imaging + from ._typing import Buffer class LoadingStrategy(IntEnum): @@ -1157,18 +1157,9 @@ def getdata( class Collector(BytesIO): data = [] - if sys.version_info >= (3, 12): - from collections.abc import Buffer - - def write(self, data: Buffer) -> int: - self.data.append(data) - return len(data) - - else: - - def write(self, data: Any) -> int: - self.data.append(data) - return len(data) + def write(self, data: Buffer) -> int: + self.data.append(data) + return len(data) im.load() # make sure raster data is available diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 53f6ae621..a6b03e171 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -46,7 +46,6 @@ import logging import math import os import struct -import sys import warnings from collections.abc import Iterator, MutableMapping from fractions import Fraction @@ -63,7 +62,7 @@ from ._util import is_path from .TiffTags import TYPES if TYPE_CHECKING: - from ._typing import IntegralLike + from ._typing import Buffer, IntegralLike logger = logging.getLogger(__name__) @@ -2108,16 +2107,8 @@ class AppendingTiffWriter(io.BytesIO): num_tags = self.readShort() self.f.seek(num_tags * 12, os.SEEK_CUR) - if sys.version_info >= (3, 12): - from collections.abc import Buffer - - def write(self, data: Buffer, /) -> int: - return self.f.write(data) - - else: - - def write(self, data: Any, /) -> int: - return self.f.write(data) + def write(self, data: Buffer, /) -> int: + return self.f.write(data) def readShort(self) -> int: (value,) = struct.unpack(self.shortFmt, self.f.read(2)) diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 093778464..da49f7c5d 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -15,6 +15,11 @@ if TYPE_CHECKING: except (ImportError, AttributeError): pass +if sys.version_info >= (3, 12): + from collections.abc import Buffer +else: + Buffer = Any + if sys.version_info >= (3, 10): from typing import TypeGuard else: @@ -40,4 +45,4 @@ class SupportsRead(Protocol[_T_co]): StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] -__all__ = ["IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"] +__all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"] From 89c790f2b03aa44980eaabb6e9b5d312e5002fba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 8 Sep 2024 21:38:21 +1000 Subject: [PATCH 10/20] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c5beebeba..be94594f8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Removed unused TiffImagePlugin IFD_LEGACY_API #8355 + [radarhere] + - Handle duplicate EXIF header #8350 [zakajd, radarhere] From fe002a72606ff33279ce798f77154f5bf8dcef17 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 01:24:51 +0400 Subject: [PATCH 11/20] Use PyCapsule in _imagingmath --- src/PIL/ImageMath.py | 4 +- src/_imagingmath.c | 145 +++++++++++++++++++++++++++---------------- 2 files changed, 94 insertions(+), 55 deletions(-) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 191cc2a5f..b32742657 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -65,7 +65,7 @@ class _Operand: except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.unop(op, out.im.id, im_1.im.id) + _imagingmath.unop(op, out.im.ptr, im_1.im.ptr) else: # binary operation im_2 = self.__fixup(im2) @@ -93,7 +93,7 @@ class _Operand: except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id) + _imagingmath.binop(op, out.im.ptr, im_1.im.ptr, im_2.im.ptr) return _Operand(out) # unary operators diff --git a/src/_imagingmath.c b/src/_imagingmath.c index 550a10903..eafcf1444 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -23,6 +23,9 @@ #define MAX_INT32 2147483647.0 #define MIN_INT32 -2147483648.0 +#define MATH_FUNC_UNOP_MAGIC "Pillow Math unary func" +#define MATH_FUNC_BINOP_MAGIC "Pillow Math binary func" + #define UNOP(name, op, type) \ void name(Imaging out, Imaging im1) { \ int x, y; \ @@ -168,15 +171,28 @@ _unop(PyObject *self, PyObject *args) { Imaging im1; void (*unop)(Imaging, Imaging); - Py_ssize_t op, i0, i1; - if (!PyArg_ParseTuple(args, "nnn", &op, &i0, &i1)) { + PyObject *op, *i0, *i1; + if (!PyArg_ParseTuple(args, "OOO", &op, &i0, &i1)) { return NULL; } - out = (Imaging)i0; - im1 = (Imaging)i1; + if (!PyCapsule_IsValid(op, MATH_FUNC_UNOP_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", MATH_FUNC_UNOP_MAGIC + ); + return NULL; + } + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } - unop = (void *)op; + unop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_UNOP_MAGIC); + out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); unop(out, im1); @@ -191,16 +207,30 @@ _binop(PyObject *self, PyObject *args) { Imaging im2; void (*binop)(Imaging, Imaging, Imaging); - Py_ssize_t op, i0, i1, i2; - if (!PyArg_ParseTuple(args, "nnnn", &op, &i0, &i1, &i2)) { + PyObject *op, *i0, *i1, *i2; + if (!PyArg_ParseTuple(args, "OOOO", &op, &i0, &i1, &i2)) { return NULL; } - out = (Imaging)i0; - im1 = (Imaging)i1; - im2 = (Imaging)i2; + if (!PyCapsule_IsValid(op, MATH_FUNC_BINOP_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", MATH_FUNC_BINOP_MAGIC + ); + return NULL; + } + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !PyCapsule_IsValid(i1, IMAGING_MAGIC) || + !PyCapsule_IsValid(i2, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } - binop = (void *)op; + binop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_BINOP_MAGIC); + out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + im2 = (Imaging)PyCapsule_GetPointer(i2, IMAGING_MAGIC); binop(out, im1, im2); @@ -213,8 +243,17 @@ static PyMethodDef _functions[] = { }; static void -install(PyObject *d, char *name, void *value) { - PyObject *v = PyLong_FromSsize_t((Py_ssize_t)value); +install_unary(PyObject *d, char *name, void *func) { + PyObject *v = PyCapsule_New(func, MATH_FUNC_UNOP_MAGIC, NULL); + if (!v || PyDict_SetItemString(d, name, v)) { + PyErr_Clear(); + } + Py_XDECREF(v); +} + +static void +install_binary(PyObject *d, char *name, void *func) { + PyObject *v = PyCapsule_New(func, MATH_FUNC_BINOP_MAGIC, NULL); if (!v || PyDict_SetItemString(d, name, v)) { PyErr_Clear(); } @@ -225,50 +264,50 @@ static int setup_module(PyObject *m) { PyObject *d = PyModule_GetDict(m); - install(d, "abs_I", abs_I); - install(d, "neg_I", neg_I); - install(d, "add_I", add_I); - install(d, "sub_I", sub_I); - install(d, "diff_I", diff_I); - install(d, "mul_I", mul_I); - install(d, "div_I", div_I); - install(d, "mod_I", mod_I); - install(d, "min_I", min_I); - install(d, "max_I", max_I); - install(d, "pow_I", pow_I); + install_unary(d, "abs_I", abs_I); + install_unary(d, "neg_I", neg_I); + install_binary(d, "add_I", add_I); + install_binary(d, "sub_I", sub_I); + install_binary(d, "diff_I", diff_I); + install_binary(d, "mul_I", mul_I); + install_binary(d, "div_I", div_I); + install_binary(d, "mod_I", mod_I); + install_binary(d, "min_I", min_I); + install_binary(d, "max_I", max_I); + install_binary(d, "pow_I", pow_I); - install(d, "invert_I", invert_I); - install(d, "and_I", and_I); - install(d, "or_I", or_I); - install(d, "xor_I", xor_I); - install(d, "lshift_I", lshift_I); - install(d, "rshift_I", rshift_I); + install_unary(d, "invert_I", invert_I); + install_binary(d, "and_I", and_I); + install_binary(d, "or_I", or_I); + install_binary(d, "xor_I", xor_I); + install_binary(d, "lshift_I", lshift_I); + install_binary(d, "rshift_I", rshift_I); - install(d, "eq_I", eq_I); - install(d, "ne_I", ne_I); - install(d, "lt_I", lt_I); - install(d, "le_I", le_I); - install(d, "gt_I", gt_I); - install(d, "ge_I", ge_I); + install_binary(d, "eq_I", eq_I); + install_binary(d, "ne_I", ne_I); + install_binary(d, "lt_I", lt_I); + install_binary(d, "le_I", le_I); + install_binary(d, "gt_I", gt_I); + install_binary(d, "ge_I", ge_I); - install(d, "abs_F", abs_F); - install(d, "neg_F", neg_F); - install(d, "add_F", add_F); - install(d, "sub_F", sub_F); - install(d, "diff_F", diff_F); - install(d, "mul_F", mul_F); - install(d, "div_F", div_F); - install(d, "mod_F", mod_F); - install(d, "min_F", min_F); - install(d, "max_F", max_F); - install(d, "pow_F", pow_F); + install_unary(d, "abs_F", abs_F); + install_unary(d, "neg_F", neg_F); + install_binary(d, "add_F", add_F); + install_binary(d, "sub_F", sub_F); + install_binary(d, "diff_F", diff_F); + install_binary(d, "mul_F", mul_F); + install_binary(d, "div_F", div_F); + install_binary(d, "mod_F", mod_F); + install_binary(d, "min_F", min_F); + install_binary(d, "max_F", max_F); + install_binary(d, "pow_F", pow_F); - install(d, "eq_F", eq_F); - install(d, "ne_F", ne_F); - install(d, "lt_F", lt_F); - install(d, "le_F", le_F); - install(d, "gt_F", gt_F); - install(d, "ge_F", ge_F); + install_binary(d, "eq_F", eq_F); + install_binary(d, "ne_F", ne_F); + install_binary(d, "lt_F", lt_F); + install_binary(d, "le_F", le_F); + install_binary(d, "gt_F", gt_F); + install_binary(d, "ge_F", ge_F); return 0; } From 56bc6a1a71da0d51666689c9d89a21615edfc757 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 01:32:28 +0400 Subject: [PATCH 12/20] Use PyCapsule in _imagingcms --- src/PIL/ImageCms.py | 4 ++-- src/_imagingcms.c | 21 ++++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index ec10230f1..5e256b0ab 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -352,7 +352,7 @@ class ImageCmsTransform(Image.ImagePointHandler): im.load() if imOut is None: imOut = Image.new(self.output_mode, im.size, None) - self.transform.apply(im.im.id, imOut.im.id) + self.transform.apply(im.im.ptr, imOut.im.ptr) imOut.info["icc_profile"] = self.output_profile.tobytes() return imOut @@ -361,7 +361,7 @@ class ImageCmsTransform(Image.ImagePointHandler): if im.mode != self.output_mode: msg = "mode mismatch" raise ValueError(msg) # wrong output mode - self.transform.apply(im.im.id, im.im.id) + self.transform.apply(im.im.ptr, im.im.ptr) im.info["icc_profile"] = self.output_profile.tobytes() return im diff --git a/src/_imagingcms.c b/src/_imagingcms.c index bafe787a7..444b6a602 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -531,23 +531,26 @@ buildProofTransform(PyObject *self, PyObject *args) { static PyObject * cms_transform_apply(CmsTransformObject *self, PyObject *args) { - Py_ssize_t idIn; - Py_ssize_t idOut; + PyObject *i0, *i1; Imaging im; Imaging imOut; - int result; - - if (!PyArg_ParseTuple(args, "nn:apply", &idIn, &idOut)) { + if (!PyArg_ParseTuple(args, "OO:apply", &i0, &i1)) { return NULL; } - im = (Imaging)idIn; - imOut = (Imaging)idOut; + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } - result = pyCMSdoTransform(im, imOut, self->transform); + im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + imOut = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); - return Py_BuildValue("i", result); + return Py_BuildValue("i", pyCMSdoTransform(im, imOut, self->transform)); } /* -------------------------------------------------------------------- */ From f916b5dc87664b89603f413c179f1ccc6aeab95a Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 01:43:38 +0400 Subject: [PATCH 13/20] Use PyCapsule in _imagingmorph --- Tests/test_imagemorph.py | 8 +++---- src/PIL/ImageMorph.py | 6 ++--- src/_imagingmorph.c | 48 ++++++++++++++++++++++++++++++---------- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 80d8c3815..04860fd61 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -328,13 +328,13 @@ def test_wrong_mode() -> None: iml = Image.new("L", (10, 10)) with pytest.raises(RuntimeError): - _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) + _imagingmorph.apply(bytes(lut), imrgb.im.ptr, iml.im.ptr) with pytest.raises(RuntimeError): - _imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) + _imagingmorph.apply(bytes(lut), iml.im.ptr, imrgb.im.ptr) with pytest.raises(RuntimeError): - _imagingmorph.match(bytes(lut), imrgb.im.id) + _imagingmorph.match(bytes(lut), imrgb.im.ptr) # Should not raise - _imagingmorph.match(bytes(lut), iml.im.id) + _imagingmorph.match(bytes(lut), iml.im.ptr) diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 6a43983d3..bc7ed66d3 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -213,7 +213,7 @@ class MorphOp: msg = "Image mode must be L" raise ValueError(msg) outimage = Image.new(image.mode, image.size, None) - count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) + count = _imagingmorph.apply(bytes(self.lut), image.im.ptr, outimage.im.ptr) return count, outimage def match(self, image: Image.Image) -> list[tuple[int, int]]: @@ -229,7 +229,7 @@ class MorphOp: if image.mode != "L": msg = "Image mode must be L" raise ValueError(msg) - return _imagingmorph.match(bytes(self.lut), image.im.id) + return _imagingmorph.match(bytes(self.lut), image.im.ptr) def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: """Get a list of all turned on pixels in a binary image @@ -240,7 +240,7 @@ class MorphOp: if image.mode != "L": msg = "Image mode must be L" raise ValueError(msg) - return _imagingmorph.get_on_pixels(image.im.id) + return _imagingmorph.get_on_pixels(image.im.ptr) def load_lut(self, filename: str) -> None: """Load an operator from an mrl file""" diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 614dfbe7f..c7933cc7d 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -30,15 +30,15 @@ static PyObject * apply(PyObject *self, PyObject *args) { const char *lut; - PyObject *py_lut; - Py_ssize_t lut_len, i0, i1; + PyObject *py_lut, *i0, *i1; + Py_ssize_t lut_len; Imaging imgin, imgout; int width, height; int row_idx, col_idx; UINT8 **inrows, **outrows; int num_changed_pixels = 0; - if (!PyArg_ParseTuple(args, "Onn", &py_lut, &i0, &i1)) { + if (!PyArg_ParseTuple(args, "OOO", &py_lut, &i0, &i1)) { PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; } @@ -57,8 +57,16 @@ apply(PyObject *self, PyObject *args) { lut = PyBytes_AsString(py_lut); - imgin = (Imaging)i0; - imgout = (Imaging)i1; + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } + + imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + imgout = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); width = imgin->xsize; height = imgin->ysize; @@ -129,8 +137,8 @@ apply(PyObject *self, PyObject *args) { static PyObject * match(PyObject *self, PyObject *args) { const char *lut; - PyObject *py_lut; - Py_ssize_t lut_len, i0; + PyObject *py_lut, *i0; + Py_ssize_t lut_len; Imaging imgin; int width, height; int row_idx, col_idx; @@ -140,7 +148,7 @@ match(PyObject *self, PyObject *args) { return NULL; } - if (!PyArg_ParseTuple(args, "On", &py_lut, &i0)) { + if (!PyArg_ParseTuple(args, "OO", &py_lut, &i0)) { Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; @@ -161,7 +169,15 @@ match(PyObject *self, PyObject *args) { } lut = PyBytes_AsString(py_lut); - imgin = (Imaging)i0; + + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } + + imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { Py_DECREF(ret); @@ -215,7 +231,7 @@ match(PyObject *self, PyObject *args) { */ static PyObject * get_on_pixels(PyObject *self, PyObject *args) { - Py_ssize_t i0; + PyObject *i0; Imaging img; UINT8 **rows; int row_idx, col_idx; @@ -225,12 +241,20 @@ get_on_pixels(PyObject *self, PyObject *args) { return NULL; } - if (!PyArg_ParseTuple(args, "n", &i0)) { + if (!PyArg_ParseTuple(args, "O", &i0)) { Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; } - img = (Imaging)i0; + + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } + + img = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); rows = img->image8; width = img->xsize; height = img->ysize; From 7f48567002a74a9f102dc3b183f872846e2925b3 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 02:00:07 +0400 Subject: [PATCH 14/20] Use s# in PyArg_ParseTuple --- src/_imagingmorph.c | 55 ++++++++++--------------------------- src/libImaging/ImPlatform.h | 1 + 2 files changed, 16 insertions(+), 40 deletions(-) diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index c7933cc7d..715cccf51 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -11,7 +11,6 @@ * See the README file for information on usage and redistribution. */ -#include "Python.h" #include "libImaging/Imaging.h" #define LUT_SIZE (1 << 9) @@ -30,7 +29,7 @@ static PyObject * apply(PyObject *self, PyObject *args) { const char *lut; - PyObject *py_lut, *i0, *i1; + PyObject *i0, *i1; Py_ssize_t lut_len; Imaging imgin, imgout; int width, height; @@ -38,25 +37,15 @@ apply(PyObject *self, PyObject *args) { UINT8 **inrows, **outrows; int num_changed_pixels = 0; - if (!PyArg_ParseTuple(args, "OOO", &py_lut, &i0, &i1)) { - PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); + if (!PyArg_ParseTuple(args, "s#OO", &lut, &lut_len, &i0, &i1)) { return NULL; } - if (!PyBytes_Check(py_lut)) { - PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); - return NULL; - } - - lut_len = PyBytes_Size(py_lut); - if (lut_len < LUT_SIZE) { PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); return NULL; } - lut = PyBytes_AsString(py_lut); - if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { PyErr_Format( @@ -137,39 +126,22 @@ apply(PyObject *self, PyObject *args) { static PyObject * match(PyObject *self, PyObject *args) { const char *lut; - PyObject *py_lut, *i0; + PyObject *i0; Py_ssize_t lut_len; Imaging imgin; int width, height; int row_idx, col_idx; UINT8 **inrows; - PyObject *ret = PyList_New(0); - if (ret == NULL) { + + if (!PyArg_ParseTuple(args, "s#O", &lut, &lut_len, &i0)) { return NULL; } - if (!PyArg_ParseTuple(args, "OO", &py_lut, &i0)) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); - return NULL; - } - - if (!PyBytes_Check(py_lut)) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); - return NULL; - } - - lut_len = PyBytes_Size(py_lut); - if (lut_len < LUT_SIZE) { - Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); return NULL; } - lut = PyBytes_AsString(py_lut); - if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { PyErr_Format( PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC @@ -180,11 +152,15 @@ match(PyObject *self, PyObject *args) { imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { - Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; } + PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } + inrows = imgin->image8; width = imgin->xsize; height = imgin->ysize; @@ -236,14 +212,8 @@ get_on_pixels(PyObject *self, PyObject *args) { UINT8 **rows; int row_idx, col_idx; int width, height; - PyObject *ret = PyList_New(0); - if (ret == NULL) { - return NULL; - } if (!PyArg_ParseTuple(args, "O", &i0)) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; } @@ -259,6 +229,11 @@ get_on_pixels(PyObject *self, PyObject *args) { width = img->xsize; height = img->ysize; + PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } + for (row_idx = 0; row_idx < height; row_idx++) { UINT8 *row = rows[row_idx]; for (col_idx = 0; col_idx < width; col_idx++) { diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index f6e7fb6b9..c9b7e43b4 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -7,6 +7,7 @@ * Copyright (c) Fredrik Lundh 1995-2003. */ +#define PY_SSIZE_T_CLEAN #include "Python.h" /* Check that we have an ANSI compliant compiler */ From 5428e3568e09d69241b03827eecb08d7b5dc9ea9 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 02:19:44 +0400 Subject: [PATCH 15/20] Deprecate ImageCore.id and ImageCore.unsafe_ptrs --- Tests/test_image_getim.py | 12 ++++++++++-- docs/deprecations.rst | 10 ++++++++++ src/_imaging.c | 14 ++++++++++++++ src/libImaging/Imaging.h | 2 +- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index 9afa02b0a..75d97cd44 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,11 +1,19 @@ from __future__ import annotations +import pytest + from .helper import hopper def test_sanity() -> None: im = hopper() - type_repr = repr(type(im.getim())) + type_repr = repr(type(im.getim())) assert "PyCapsule" in type_repr - assert isinstance(im.im.id, int) + + with pytest.warns(DeprecationWarning): + assert isinstance(im.im.id, int) + + with pytest.warns(DeprecationWarning): + ptrs = dict(im.im.unsafe_ptrs) + assert all(k in ptrs for k in ["image8", "image32", "image"]) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index cafda3803..abb902c88 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -136,6 +136,16 @@ Specific WebP Feature Checks ``True`` if the WebP module is installed, until they are removed in Pillow 12.0.0 (2025-10-15). +Get Internal Pointers to Objects +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +``Image.core.ImageCore.id`` and ``Image.core.ImageCore.unsafe_ptrs`` +have been deprecated and will be removed in Pillow 12 (2025-10-15). +They were used for obtaining raw pointers to ``ImageCore`` internals. To interact with +C code, you can use ``Image.core.ImageCore.ptr``, which returns a ``PyCapsule`` object. + Removed features ---------------- diff --git a/src/_imaging.c b/src/_imaging.c index 07d9a64cc..a455c873b 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3707,6 +3707,13 @@ _getattr_bands(ImagingObject *self, void *closure) { static PyObject * _getattr_id(ImagingObject *self, void *closure) { + if (PyErr_WarnEx( + PyExc_DeprecationWarning, + "id property is deprecated and will be removed in Pillow 12.0", + 1 + ) < 0) { + return NULL; + } return PyLong_FromSsize_t((Py_ssize_t)self->image); } @@ -3717,6 +3724,13 @@ _getattr_ptr(ImagingObject *self, void *closure) { static PyObject * _getattr_unsafe_ptrs(ImagingObject *self, void *closure) { + if (PyErr_WarnEx( + PyExc_DeprecationWarning, + "unsafe_ptrs property is deprecated and will be removed in Pillow 12.0", + 1 + ) < 0) { + return NULL; + } return Py_BuildValue( "(sn)(sn)(sn)", "image8", diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 321dce988..870f5ed97 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -61,7 +61,7 @@ typedef struct ImagingOutlineInstance *ImagingOutline; typedef struct ImagingPaletteInstance *ImagingPalette; /* handle magics (used with PyCObject). */ -#define IMAGING_MAGIC "PIL Imaging" +#define IMAGING_MAGIC "Pillow Imaging" /* pixel types */ #define IMAGING_TYPE_UINT8 0 From cb3a4e6a00fd41e42afa78b4289e5e1056fc0e81 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 03:45:18 +0400 Subject: [PATCH 16/20] Remove legacy 1-bit api, fix AttributeError PytestUnraisableExceptionWarning: Exception ignored in: AttributeError: 'PhotoImage' object has no attribute '_PhotoImage__photo' --- src/PIL/ImageTk.py | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index a788c9d1f..d546989ee 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -35,20 +35,6 @@ from . import Image, ImageFile # -------------------------------------------------------------------- # Check for Tkinter interface hooks -_pilbitmap_ok = None - - -def _pilbitmap_check() -> int: - global _pilbitmap_ok - if _pilbitmap_ok is None: - try: - im = Image.new("1", (1, 1)) - tkinter.BitmapImage(data=f"PIL:{im.im.id}") - _pilbitmap_ok = 1 - except tkinter.TclError: - _pilbitmap_ok = 0 - return _pilbitmap_ok - def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None: source = None @@ -142,6 +128,8 @@ class PhotoImage: self.paste(image) def __del__(self) -> None: + if not hasattr(self, "__photo"): + return name = self.__photo.name self.__photo.name = None try: @@ -225,17 +213,11 @@ class BitmapImage: self.__mode = image.mode self.__size = image.size - if _pilbitmap_check(): - # fast way (requires the pilbitmap booster patch) - image.load() - kw["data"] = f"PIL:{image.im.id}" - self.__im = image # must keep a reference - else: - # slow but safe way - kw["data"] = image.tobitmap() - self.__photo = tkinter.BitmapImage(**kw) + self.__photo = tkinter.BitmapImage(data=image.tobitmap(), **kw) def __del__(self) -> None: + if not hasattr(self, "__photo"): + return name = self.__photo.name self.__photo.name = None try: From ee65b305b1fd041b679382003b4ce727bb03392d Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 04:29:34 +0400 Subject: [PATCH 17/20] Use PyCapsule in _imagingtk --- src/PIL/ImageTk.py | 11 +++++------ src/Tk/tkImaging.c | 35 ++++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index d546989ee..279a21716 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -48,18 +48,18 @@ def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None: def _pyimagingtkcall( - command: str, photo: PhotoImage | tkinter.PhotoImage, id: int + command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: object ) -> None: tk = photo.tk try: - tk.call(command, photo, id) + tk.call(command, photo, repr(ptr)) except tkinter.TclError: # activate Tkinter hook # may raise an error if it cannot attach to Tkinter from . import _imagingtk _imagingtk.tkinit(tk.interpaddr()) - tk.call(command, photo, id) + tk.call(command, photo, repr(ptr)) # -------------------------------------------------------------------- @@ -181,7 +181,7 @@ class PhotoImage: block = image.new_block(self.__mode, im.size) image.convert2(block, image) # convert directly between buffers - _pyimagingtkcall("PyImagingPhoto", self.__photo, block.id) + _pyimagingtkcall("PyImagingPhoto", self.__photo, block.ptr) # -------------------------------------------------------------------- @@ -255,9 +255,8 @@ class BitmapImage: def getimage(photo: PhotoImage) -> Image.Image: """Copies the contents of a PhotoImage to a PIL image memory.""" im = Image.new("RGBA", (photo.width(), photo.height())) - block = im.im - _pyimagingtkcall("PyImagingPhotoGet", photo, block.id) + _pyimagingtkcall("PyImagingPhotoGet", photo, im.im.ptr) return im diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 727ee6bed..b33a47d30 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -56,19 +56,36 @@ static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK; static Imaging ImagingFind(const char *name) { - Py_ssize_t id; + PyObject *capsule; + int direct_pointer = 0; + const char *expected = "capsule object \"" IMAGING_MAGIC "\" at 0x"; - /* FIXME: use CObject instead? */ -#if defined(_WIN64) - id = _atoi64(name); -#else - id = atol(name); -#endif - if (!id) { + if (name[0] == '<') { + name++; + } else { + // Special case for PyPy, where the string representation of a Capsule + // refers directly to the pointer itself, not to the PyCapsule object. + direct_pointer = 1; + } + + if (strncmp(name, expected, strlen(expected))) { return NULL; } - return (Imaging)id; + capsule = (PyObject *)strtoull(name + strlen(expected), NULL, 16); + + if (direct_pointer) { + return (Imaging)capsule; + } + + if (!PyCapsule_IsValid(capsule, IMAGING_MAGIC)) { + PyErr_Format( + PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC + ); + return NULL; + } + + return (Imaging)PyCapsule_GetPointer(capsule, IMAGING_MAGIC); } static int From 882ac783eb67a13c1ff8afba9369d1603328718e Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 2 Sep 2024 13:04:57 +0400 Subject: [PATCH 18/20] Use PyCapsule in _imagingft --- src/_imagingft.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 5c446e641..c539ec267 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -830,7 +830,6 @@ font_render(FontObject *self, PyObject *args) { unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ PyObject *image; Imaging im; - Py_ssize_t id; int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ int color = 0; /* is FT_LOAD_COLOR enabled? */ int stroke_width = 0; @@ -929,10 +928,9 @@ font_render(FontObject *self, PyObject *args) { PyMem_Del(glyph_info); return NULL; } - PyObject *imageId = PyObject_GetAttrString(image, "id"); - id = PyLong_AsSsize_t(imageId); - Py_XDECREF(imageId); - im = (Imaging)id; + PyObject *imagePtr = PyObject_GetAttrString(image, "ptr"); + im = (Imaging)PyCapsule_GetPointer(imagePtr, IMAGING_MAGIC); + Py_XDECREF(imagePtr); x_offset -= stroke_width; y_offset -= stroke_width; From 934ae12ed5e98b7d2ec14d1946fad3bce6a42924 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 3 Sep 2024 11:54:24 +0400 Subject: [PATCH 19/20] rename PyCapsule -> Capsule --- docs/deprecations.rst | 2 +- src/Tk/tkImaging.c | 4 +--- src/_imagingcms.c | 4 +--- src/_imagingmath.c | 16 ++++------------ src/_imagingmorph.c | 12 +++--------- 5 files changed, 10 insertions(+), 28 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index abb902c88..dd271a97f 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -144,7 +144,7 @@ Get Internal Pointers to Objects ``Image.core.ImageCore.id`` and ``Image.core.ImageCore.unsafe_ptrs`` have been deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining raw pointers to ``ImageCore`` internals. To interact with -C code, you can use ``Image.core.ImageCore.ptr``, which returns a ``PyCapsule`` object. +C code, you can use ``Image.core.ImageCore.ptr``, which returns a ``Capsule`` object. Removed features ---------------- diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index b33a47d30..a36c3e0bd 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -79,9 +79,7 @@ ImagingFind(const char *name) { } if (!PyCapsule_IsValid(capsule, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 444b6a602..1823bcf03 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -541,9 +541,7 @@ cms_transform_apply(CmsTransformObject *self, PyObject *args) { if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } diff --git a/src/_imagingmath.c b/src/_imagingmath.c index eafcf1444..dbe636707 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -177,16 +177,12 @@ _unop(PyObject *self, PyObject *args) { } if (!PyCapsule_IsValid(op, MATH_FUNC_UNOP_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", MATH_FUNC_UNOP_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", MATH_FUNC_UNOP_MAGIC); return NULL; } if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } @@ -213,17 +209,13 @@ _binop(PyObject *self, PyObject *args) { } if (!PyCapsule_IsValid(op, MATH_FUNC_BINOP_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", MATH_FUNC_BINOP_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", MATH_FUNC_BINOP_MAGIC); return NULL; } if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || !PyCapsule_IsValid(i1, IMAGING_MAGIC) || !PyCapsule_IsValid(i2, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 715cccf51..637295be7 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -48,9 +48,7 @@ apply(PyObject *self, PyObject *args) { if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } @@ -143,9 +141,7 @@ match(PyObject *self, PyObject *args) { } if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } @@ -218,9 +214,7 @@ get_on_pixels(PyObject *self, PyObject *args) { } if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { - PyErr_Format( - PyExc_TypeError, "Expected PyCapsule with '%s' name.", IMAGING_MAGIC - ); + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } From d29fa73ea608449f86082dc312eb132a10901fd0 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Thu, 5 Sep 2024 01:04:35 +0400 Subject: [PATCH 20/20] Move new_block to module --- src/PIL/ImageTk.py | 2 +- src/_imaging.c | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 279a21716..5bad9b546 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -178,7 +178,7 @@ class PhotoImage: if image.isblock() and im.mode == self.__mode: block = image else: - block = image.new_block(self.__mode, im.size) + block = Image.core.new_block(self.__mode, im.size) image.convert2(block, image) # convert directly between buffers _pyimagingtkcall("PyImagingPhoto", self.__photo, block.ptr) diff --git a/src/_imaging.c b/src/_imaging.c index a455c873b..e227d8efc 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3674,15 +3674,12 @@ static struct PyMethodDef methods[] = { /* Unsharpmask extension */ {"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS}, {"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS}, - {"box_blur", (PyCFunction)_box_blur, METH_VARARGS}, /* Special effects */ {"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS}, /* Misc. */ - {"new_block", (PyCFunction)_new_block, METH_VARARGS}, - {"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS}, {NULL, NULL} /* sentinel */ @@ -4212,6 +4209,7 @@ static PyMethodDef functions[] = { {"blend", (PyCFunction)_blend, METH_VARARGS}, {"fill", (PyCFunction)_fill, METH_VARARGS}, {"new", (PyCFunction)_new, METH_VARARGS}, + {"new_block", (PyCFunction)_new_block, METH_VARARGS}, {"merge", (PyCFunction)_merge, METH_VARARGS}, /* Functions */