Skip to content

Commit

Permalink
Merge pull request #8341 from uploadcare/use-ptr
Browse files Browse the repository at this point in the history
Use ImagingCore.ptr instead of ImagingCore.id
  • Loading branch information
hugovk authored Oct 7, 2024
2 parents 029ec85 + a227f22 commit 535bf23
Show file tree
Hide file tree
Showing 19 changed files with 262 additions and 191 deletions.
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 ptrs.keys() == {"image8", "image32", "image"}
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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. 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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. 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)
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
57 changes: 21 additions & 36 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
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)
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,18 +216,13 @@ 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)
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:
name = self.__photo.name
try:
name = self.__photo.name
except AttributeError:
return
self.__photo.name = None
try:
self.__photo.tk.call("image", "delete", name)
Expand Down Expand Up @@ -273,9 +259,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
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 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;
}

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 '%s' Capsule", IMAGING_MAGIC);
return NULL;
}

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

static int
Expand Down
Loading

0 comments on commit 535bf23

Please sign in to comment.