Skip to content

Commit 535bf23

Browse files
authored
Merge pull request #8341 from uploadcare/use-ptr
Use ImagingCore.ptr instead of ImagingCore.id
2 parents 029ec85 + a227f22 commit 535bf23

19 files changed

+262
-191
lines changed

Tests/test_image_getim.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
from __future__ import annotations
22

3+
import pytest
4+
35
from .helper import hopper
46

57

68
def test_sanity() -> None:
79
im = hopper()
8-
type_repr = repr(type(im.getim()))
910

11+
type_repr = repr(type(im.getim()))
1012
assert "PyCapsule" in type_repr
11-
assert isinstance(im.im.id, int)
13+
14+
with pytest.warns(DeprecationWarning):
15+
assert isinstance(im.im.id, int)
16+
17+
with pytest.warns(DeprecationWarning):
18+
ptrs = dict(im.im.unsafe_ptrs)
19+
assert ptrs.keys() == {"image8", "image32", "image"}

Tests/test_imagemorph.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -324,17 +324,17 @@ def test_set_lut() -> None:
324324

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

330330
with pytest.raises(RuntimeError):
331-
_imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id)
331+
_imagingmorph.apply(bytes(lut), imrgb_ptr, iml_ptr)
332332

333333
with pytest.raises(RuntimeError):
334-
_imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id)
334+
_imagingmorph.apply(bytes(lut), iml_ptr, imrgb_ptr)
335335

336336
with pytest.raises(RuntimeError):
337-
_imagingmorph.match(bytes(lut), imrgb.im.id)
337+
_imagingmorph.match(bytes(lut), imrgb_ptr)
338338

339339
# Should not raise
340-
_imagingmorph.match(bytes(lut), iml.im.id)
340+
_imagingmorph.match(bytes(lut), iml_ptr)

docs/deprecations.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,16 @@ Specific WebP Feature Checks
165165
``True`` if the WebP module is installed, until they are removed in Pillow
166166
12.0.0 (2025-10-15).
167167

168+
Get internal pointers to objects
169+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
170+
171+
.. deprecated:: 11.0.0
172+
173+
``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been
174+
deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining
175+
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
176+
``Image.Image.getim()``, which returns a ``Capsule`` object.
177+
168178
Removed features
169179
----------------
170180

docs/releasenotes/11.0.0.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`).
7373

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

76+
Get internal pointers to objects
77+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
78+
79+
.. deprecated:: 11.0.0
80+
81+
``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been
82+
deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining
83+
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
84+
``Image.Image.getim()``, which returns a ``Capsule`` object.
85+
7686
ICNS (width, height, scale) sizes
7787
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7888

src/PIL/Image.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -225,12 +225,7 @@ class Quantize(IntEnum):
225225
from IPython.lib.pretty import PrettyPrinter
226226

227227
from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin
228-
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
229-
230-
if sys.version_info >= (3, 13):
231-
from types import CapsuleType
232-
else:
233-
CapsuleType = object
228+
from ._typing import CapsuleType, NumpyArray, StrOrBytesPath, TypeGuard
234229
ID: list[str] = []
235230
OPEN: dict[
236231
str,

src/PIL/ImageCms.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -349,19 +349,17 @@ def point(self, im: Image.Image) -> Image.Image:
349349
return self.apply(im)
350350

351351
def apply(self, im: Image.Image, imOut: Image.Image | None = None) -> Image.Image:
352-
im.load()
353352
if imOut is None:
354353
imOut = Image.new(self.output_mode, im.size, None)
355-
self.transform.apply(im.im.id, imOut.im.id)
354+
self.transform.apply(im.getim(), imOut.getim())
356355
imOut.info["icc_profile"] = self.output_profile.tobytes()
357356
return imOut
358357

359358
def apply_in_place(self, im: Image.Image) -> Image.Image:
360-
im.load()
361359
if im.mode != self.output_mode:
362360
msg = "mode mismatch"
363361
raise ValueError(msg) # wrong output mode
364-
self.transform.apply(im.im.id, im.im.id)
362+
self.transform.apply(im.getim(), im.getim())
365363
im.info["icc_profile"] = self.output_profile.tobytes()
366364
return im
367365

src/PIL/ImageMath.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,12 @@ def apply(
5959
if im2 is None:
6060
# unary operation
6161
out = Image.new(mode or im_1.mode, im_1.size, None)
62-
im_1.load()
6362
try:
6463
op = getattr(_imagingmath, f"{op}_{im_1.mode}")
6564
except AttributeError as e:
6665
msg = f"bad operand type for '{op}'"
6766
raise TypeError(msg) from e
68-
_imagingmath.unop(op, out.im.id, im_1.im.id)
67+
_imagingmath.unop(op, out.getim(), im_1.getim())
6968
else:
7069
# binary operation
7170
im_2 = self.__fixup(im2)
@@ -86,14 +85,12 @@ def apply(
8685
if im_2.size != size:
8786
im_2 = im_2.crop((0, 0) + size)
8887
out = Image.new(mode or im_1.mode, im_1.size, None)
89-
im_1.load()
90-
im_2.load()
9188
try:
9289
op = getattr(_imagingmath, f"{op}_{im_1.mode}")
9390
except AttributeError as e:
9491
msg = f"bad operand type for '{op}'"
9592
raise TypeError(msg) from e
96-
_imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id)
93+
_imagingmath.binop(op, out.getim(), im_1.getim(), im_2.getim())
9794
return _Operand(out)
9895

9996
# unary operators

src/PIL/ImageMorph.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ def apply(self, image: Image.Image) -> tuple[int, Image.Image]:
213213
msg = "Image mode must be L"
214214
raise ValueError(msg)
215215
outimage = Image.new(image.mode, image.size, None)
216-
count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id)
216+
count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim())
217217
return count, outimage
218218

219219
def match(self, image: Image.Image) -> list[tuple[int, int]]:
@@ -229,7 +229,7 @@ def match(self, image: Image.Image) -> list[tuple[int, int]]:
229229
if image.mode != "L":
230230
msg = "Image mode must be L"
231231
raise ValueError(msg)
232-
return _imagingmorph.match(bytes(self.lut), image.im.id)
232+
return _imagingmorph.match(bytes(self.lut), image.getim())
233233

234234
def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]:
235235
"""Get a list of all turned on pixels in a binary image
@@ -240,7 +240,7 @@ def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]:
240240
if image.mode != "L":
241241
msg = "Image mode must be L"
242242
raise ValueError(msg)
243-
return _imagingmorph.get_on_pixels(image.im.id)
243+
return _imagingmorph.get_on_pixels(image.getim())
244244

245245
def load_lut(self, filename: str) -> None:
246246
"""Load an operator from an mrl file"""

src/PIL/ImageTk.py

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,12 @@
3232

3333
from . import Image, ImageFile
3434

35+
if TYPE_CHECKING:
36+
from ._typing import CapsuleType
37+
3538
# --------------------------------------------------------------------
3639
# Check for Tkinter interface hooks
3740

38-
_pilbitmap_ok = None
39-
40-
41-
def _pilbitmap_check() -> int:
42-
global _pilbitmap_ok
43-
if _pilbitmap_ok is None:
44-
try:
45-
im = Image.new("1", (1, 1))
46-
tkinter.BitmapImage(data=f"PIL:{im.im.id}")
47-
_pilbitmap_ok = 1
48-
except tkinter.TclError:
49-
_pilbitmap_ok = 0
50-
return _pilbitmap_ok
51-
5241

5342
def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None:
5443
source = None
@@ -62,18 +51,18 @@ def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None:
6251

6352

6453
def _pyimagingtkcall(
65-
command: str, photo: PhotoImage | tkinter.PhotoImage, id: int
54+
command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: CapsuleType
6655
) -> None:
6756
tk = photo.tk
6857
try:
69-
tk.call(command, photo, id)
58+
tk.call(command, photo, repr(ptr))
7059
except tkinter.TclError:
7160
# activate Tkinter hook
7261
# may raise an error if it cannot attach to Tkinter
7362
from . import _imagingtk
7463

7564
_imagingtk.tkinit(tk.interpaddr())
76-
tk.call(command, photo, id)
65+
tk.call(command, photo, repr(ptr))
7766

7867

7968
# --------------------------------------------------------------------
@@ -142,7 +131,10 @@ def __init__(
142131
self.paste(image)
143132

144133
def __del__(self) -> None:
145-
name = self.__photo.name
134+
try:
135+
name = self.__photo.name
136+
except AttributeError:
137+
return
146138
self.__photo.name = None
147139
try:
148140
self.__photo.tk.call("image", "delete", name)
@@ -185,15 +177,14 @@ def paste(self, im: Image.Image) -> None:
185177
the bitmap image.
186178
"""
187179
# convert to blittable
188-
im.load()
180+
ptr = im.getim()
189181
image = im.im
190-
if image.isblock() and im.mode == self.__mode:
191-
block = image
192-
else:
193-
block = image.new_block(self.__mode, im.size)
182+
if not image.isblock() or im.mode != self.__mode:
183+
block = Image.core.new_block(self.__mode, im.size)
194184
image.convert2(block, image) # convert directly between buffers
185+
ptr = block.ptr
195186

196-
_pyimagingtkcall("PyImagingPhoto", self.__photo, block.id)
187+
_pyimagingtkcall("PyImagingPhoto", self.__photo, ptr)
197188

198189

199190
# --------------------------------------------------------------------
@@ -225,18 +216,13 @@ def __init__(self, image: Image.Image | None = None, **kw: Any) -> None:
225216
self.__mode = image.mode
226217
self.__size = image.size
227218

228-
if _pilbitmap_check():
229-
# fast way (requires the pilbitmap booster patch)
230-
image.load()
231-
kw["data"] = f"PIL:{image.im.id}"
232-
self.__im = image # must keep a reference
233-
else:
234-
# slow but safe way
235-
kw["data"] = image.tobitmap()
236-
self.__photo = tkinter.BitmapImage(**kw)
219+
self.__photo = tkinter.BitmapImage(data=image.tobitmap(), **kw)
237220

238221
def __del__(self) -> None:
239-
name = self.__photo.name
222+
try:
223+
name = self.__photo.name
224+
except AttributeError:
225+
return
240226
self.__photo.name = None
241227
try:
242228
self.__photo.tk.call("image", "delete", name)
@@ -273,9 +259,8 @@ def __str__(self) -> str:
273259
def getimage(photo: PhotoImage) -> Image.Image:
274260
"""Copies the contents of a PhotoImage to a PIL image memory."""
275261
im = Image.new("RGBA", (photo.width(), photo.height()))
276-
block = im.im
277262

278-
_pyimagingtkcall("PyImagingPhotoGet", photo, block.id)
263+
_pyimagingtkcall("PyImagingPhotoGet", photo, im.getim())
279264

280265
return im
281266

src/PIL/_imagingcms.pyi

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import datetime
22
import sys
33
from typing import Literal, SupportsFloat, TypedDict
44

5+
from ._typing import CapsuleType
6+
57
littlecms_version: str | None
68

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

110112
class CmsTransform:
111-
def apply(self, id_in: int, id_out: int) -> int: ...
113+
def apply(self, id_in: CapsuleType, id_out: CapsuleType) -> int: ...
112114

113115
def profile_open(profile: str, /) -> CmsProfile: ...
114116
def profile_frombytes(profile: bytes, /) -> CmsProfile: ...

0 commit comments

Comments
 (0)