Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use ImagingCore.ptr instead of ImagingCore.id #8341

Merged
merged 39 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2303c06
Use PyCapsule in _imagingmath
homm Sep 1, 2024
a9798e7
Use PyCapsule in _imagingcms
homm Sep 1, 2024
2fcab26
Use PyCapsule in _imagingmorph
homm Sep 1, 2024
f246be7
Use s# in PyArg_ParseTuple
homm Sep 1, 2024
7435a06
Deprecate ImageCore.id and ImageCore.unsafe_ptrs
homm Sep 1, 2024
c69ad03
Remove legacy 1-bit api, fix AttributeError
homm Sep 1, 2024
920c4ac
Use PyCapsule in _imagingtk
homm Sep 2, 2024
147f75e
Use PyCapsule in _imagingft
homm Sep 2, 2024
bf11639
rename PyCapsule -> Capsule
homm Sep 3, 2024
8833548
Move new_block to module
homm Sep 4, 2024
8dcf229
Merge branch 'main' into use-ptr
radarhere Sep 7, 2024
6f9128b
Updated type hint
radarhere Sep 7, 2024
fe002a7
Use PyCapsule in _imagingmath
homm Sep 1, 2024
56bc6a1
Use PyCapsule in _imagingcms
homm Sep 1, 2024
f916b5d
Use PyCapsule in _imagingmorph
homm Sep 1, 2024
7f48567
Use s# in PyArg_ParseTuple
homm Sep 1, 2024
5428e35
Deprecate ImageCore.id and ImageCore.unsafe_ptrs
homm Sep 1, 2024
cb3a4e6
Remove legacy 1-bit api, fix AttributeError
homm Sep 1, 2024
ee65b30
Use PyCapsule in _imagingtk
homm Sep 2, 2024
882ac78
Use PyCapsule in _imagingft
homm Sep 2, 2024
934ae12
rename PyCapsule -> Capsule
homm Sep 3, 2024
d29fa73
Move new_block to module
homm Sep 4, 2024
4318834
Merge branch 'use-ptr' into use-ptr
homm Sep 8, 2024
bd14915
Merge pull request #144 from radarhere/use-ptr
homm Sep 8, 2024
a2988da
ImageCore → ImagingCore
homm Sep 11, 2024
6921f83
Update docs/deprecations.rst
homm Sep 13, 2024
1f3fe6f
Use getim()
radarhere Sep 16, 2024
3b09f43
Merge pull request #145 from radarhere/use-ptr
homm Sep 16, 2024
d8ef314
Remove extra load() calls
homm Sep 16, 2024
bc97369
Increase reference to the image while capsule is alive
homm Sep 16, 2024
af521a1
Merge branch 'main' into use-ptr
homm Sep 18, 2024
aa22b24
Load before trying to catch exceptions
radarhere Sep 21, 2024
5d430ea
Added release notes
radarhere Sep 21, 2024
9f409e8
Use getim()
radarhere Sep 21, 2024
87414b3
Merge pull request #147 from radarhere/use-ptr
homm Sep 22, 2024
11bcd5a
Fix hasattr for ImageTk.PhotoImage.__del__
homm Sep 22, 2024
b9d1768
Catch AttributeError for BitmapImage.__photo
homm Sep 26, 2024
8e332eb
Apply suggestions from code review
homm Oct 7, 2024
a227f22
Apply suggestions from code review [ci skip]
homm Oct 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions Tests/test_image_getim.py
Original file line number Diff line number Diff line change
@@ -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"])
homm marked this conversation as resolved.
Show resolved Hide resolved
12 changes: 6 additions & 6 deletions Tests/test_imagemorph.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,17 +324,17 @@ def test_set_lut() -> None:

def test_wrong_mode() -> None:
lut = ImageMorph.LutBuilder(op_name="corner").build_lut()
imrgb = Image.new("RGB", (10, 10))
iml = Image.new("L", (10, 10))
imrgb_ptr = Image.new("RGB", (10, 10)).getim()
iml_ptr = Image.new("L", (10, 10)).getim()

with pytest.raises(RuntimeError):
_imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id)
_imagingmorph.apply(bytes(lut), imrgb_ptr, iml_ptr)

with pytest.raises(RuntimeError):
_imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id)
_imagingmorph.apply(bytes(lut), iml_ptr, imrgb_ptr)

with pytest.raises(RuntimeError):
_imagingmorph.match(bytes(lut), imrgb.im.id)
_imagingmorph.match(bytes(lut), imrgb_ptr)

# Should not raise
_imagingmorph.match(bytes(lut), iml.im.id)
_imagingmorph.match(bytes(lut), iml_ptr)
10 changes: 10 additions & 0 deletions docs/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,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
homm marked this conversation as resolved.
Show resolved Hide resolved
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. deprecated:: 11.0.0

``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been
deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
``Image.Image.getim()``, which returns a ``Capsule`` object.

Removed features
----------------

Expand Down
10 changes: 10 additions & 0 deletions docs/releasenotes/11.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`).

.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/

Get Internal Pointers to Objects
homm marked this conversation as resolved.
Show resolved Hide resolved
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. deprecated:: 11.0.0

``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been
deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
``Image.Image.getim()``, which returns a ``Capsule`` object.

ICNS (width, height, scale) sizes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
7 changes: 1 addition & 6 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,7 @@ class Quantize(IntEnum):
from IPython.lib.pretty import PrettyPrinter

from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard

if sys.version_info >= (3, 13):
from types import CapsuleType
else:
CapsuleType = object
from ._typing import CapsuleType, NumpyArray, StrOrBytesPath, TypeGuard
ID: list[str] = []
OPEN: dict[
str,
Expand Down
6 changes: 2 additions & 4 deletions src/PIL/ImageCms.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,19 +349,17 @@ def point(self, im: Image.Image) -> Image.Image:
return self.apply(im)

def apply(self, im: Image.Image, imOut: Image.Image | None = None) -> Image.Image:
im.load()
if imOut is None:
imOut = Image.new(self.output_mode, im.size, None)
homm marked this conversation as resolved.
Show resolved Hide resolved
self.transform.apply(im.im.id, imOut.im.id)
self.transform.apply(im.getim(), imOut.getim())
imOut.info["icc_profile"] = self.output_profile.tobytes()
return imOut

def apply_in_place(self, im: Image.Image) -> Image.Image:
im.load()
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.getim(), im.getim())
im.info["icc_profile"] = self.output_profile.tobytes()
return im

Expand Down
7 changes: 2 additions & 5 deletions src/PIL/ImageMath.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,12 @@ def apply(
if im2 is None:
# unary operation
out = Image.new(mode or im_1.mode, im_1.size, None)
im_1.load()
try:
op = getattr(_imagingmath, f"{op}_{im_1.mode}")
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.getim(), im_1.getim())
else:
# binary operation
im_2 = self.__fixup(im2)
Expand All @@ -86,14 +85,12 @@ def apply(
if im_2.size != size:
im_2 = im_2.crop((0, 0) + size)
out = Image.new(mode or im_1.mode, im_1.size, None)
im_1.load()
im_2.load()
try:
op = getattr(_imagingmath, f"{op}_{im_1.mode}")
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.getim(), im_1.getim(), im_2.getim())
return _Operand(out)

# unary operators
Expand Down
6 changes: 3 additions & 3 deletions src/PIL/ImageMorph.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ def apply(self, image: Image.Image) -> tuple[int, Image.Image]:
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.getim(), outimage.getim())
return count, outimage

def match(self, image: Image.Image) -> list[tuple[int, int]]:
Expand All @@ -229,7 +229,7 @@ def match(self, image: Image.Image) -> list[tuple[int, int]]:
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.getim())

def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]:
"""Get a list of all turned on pixels in a binary image
Expand All @@ -240,7 +240,7 @@ def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]:
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.getim())

def load_lut(self, filename: str) -> None:
"""Load an operator from an mrl file"""
Expand Down
54 changes: 19 additions & 35 deletions src/PIL/ImageTk.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,12 @@

from . import Image, ImageFile

if TYPE_CHECKING:
from ._typing import CapsuleType

# --------------------------------------------------------------------
# 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
Expand All @@ -62,18 +51,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: CapsuleType
) -> 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))


# --------------------------------------------------------------------
Expand Down Expand Up @@ -142,7 +131,10 @@ def __init__(
self.paste(image)

def __del__(self) -> None:
name = self.__photo.name
try:
name = self.__photo.name
except AttributeError:
return
homm marked this conversation as resolved.
Show resolved Hide resolved
self.__photo.name = None
try:
self.__photo.tk.call("image", "delete", name)
Expand Down Expand Up @@ -185,15 +177,14 @@ def paste(self, im: Image.Image) -> None:
the bitmap image.
"""
# convert to blittable
im.load()
ptr = im.getim()
image = im.im
if image.isblock() and im.mode == self.__mode:
block = image
else:
block = image.new_block(self.__mode, im.size)
if not image.isblock() or im.mode != self.__mode:
block = Image.core.new_block(self.__mode, im.size)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine this change was just because you didn't like image.new_block(self.__mode, im.size)?

Rather than moving new_block in C, wouldn't it have been better for the C method to access the size on the image object instead of being passed it? Then this call could become image.new_block(self.__mode).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d like to clarify that the reason for this change is not a matter of personal preference. The new_block method doesn't pertain to an existing image but instead creates a new block based on the passed parameters. As such, it makes more sense for it to reside in the Image.core module rather than being a method of the image object itself.

There is no intent to modify or enhance the behavior; I was simply correcting the method's improper placement.

image.convert2(block, image) # convert directly between buffers
ptr = block.ptr

_pyimagingtkcall("PyImagingPhoto", self.__photo, block.id)
_pyimagingtkcall("PyImagingPhoto", self.__photo, ptr)


# --------------------------------------------------------------------
Expand Down Expand Up @@ -225,17 +216,11 @@ def __init__(self, image: Image.Image | None = None, **kw: Any) -> None:
self.__mode = image.mode
self.__size = image.size

if _pilbitmap_check():
# fast way (requires the pilbitmap booster patch)
Copy link
Member

@radarhere radarhere Sep 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, the last mention of this patch was removed in Pillow 4.1.0 by #2360

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
homm marked this conversation as resolved.
Show resolved Hide resolved
self.__photo.name = None
try:
Expand Down Expand Up @@ -273,9 +258,8 @@ def __str__(self) -> str:
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.getim())

return im

Expand Down
4 changes: 3 additions & 1 deletion src/PIL/_imagingcms.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import datetime
import sys
from typing import Literal, SupportsFloat, TypedDict

from ._typing import CapsuleType

littlecms_version: str | None

_Tuple3f = tuple[float, float, float]
Expand Down Expand Up @@ -108,7 +110,7 @@ class CmsProfile:
def is_intent_supported(self, intent: int, direction: int, /) -> int: ...

class CmsTransform:
def apply(self, id_in: int, id_out: int) -> int: ...
def apply(self, id_in: CapsuleType, id_out: CapsuleType) -> int: ...

def profile_open(profile: str, /) -> CmsProfile: ...
def profile_frombytes(profile: bytes, /) -> CmsProfile: ...
Expand Down
5 changes: 5 additions & 0 deletions src/PIL/_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
except (ImportError, AttributeError):
pass

if sys.version_info >= (3, 13):
from types import CapsuleType

Check warning on line 19 in src/PIL/_typing.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/_typing.py#L19

Added line #L19 was not covered by tests
else:
CapsuleType = object

if sys.version_info >= (3, 12):
from collections.abc import Buffer
else:
Expand Down
33 changes: 24 additions & 9 deletions src/Tk/tkImaging.c
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,34 @@

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;

Check warning on line 68 in src/Tk/tkImaging.c

View check run for this annotation

Codecov / codecov/patch

src/Tk/tkImaging.c#L68

Added line #L68 was not covered by tests
}

if (strncmp(name, expected, strlen(expected))) {
return NULL;

Check warning on line 72 in src/Tk/tkImaging.c

View check run for this annotation

Codecov / codecov/patch

src/Tk/tkImaging.c#L72

Added line #L72 was not covered by tests
}

capsule = (PyObject *)strtoull(name + strlen(expected), NULL, 16);

if (direct_pointer) {
return (Imaging)capsule;

Check warning on line 78 in src/Tk/tkImaging.c

View check run for this annotation

Codecov / codecov/patch

src/Tk/tkImaging.c#L78

Added line #L78 was not covered by tests
}

if (!PyCapsule_IsValid(capsule, IMAGING_MAGIC)) {
PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC);

Check warning on line 82 in src/Tk/tkImaging.c

View check run for this annotation

Codecov / codecov/patch

src/Tk/tkImaging.c#L82

Added line #L82 was not covered by tests
return NULL;
}

return (Imaging)id;
return (Imaging)PyCapsule_GetPointer(capsule, IMAGING_MAGIC);
}

static int
Expand Down
Loading
Loading