Removed PyAccess and Image.USE_CFFI_ACCESS

This commit is contained in:
Andrew Murray 2024-06-29 15:02:50 +10:00
parent eb5bf18192
commit 43cc1e3659
19 changed files with 33 additions and 700 deletions

View File

@ -28,8 +28,6 @@ fi
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel python3 -m pip install --upgrade wheel
# TODO Update condition when cffi supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install olefile python3 -m pip install olefile

View File

@ -19,6 +19,5 @@ exclude_also =
[run] [run]
omit = omit =
Tests/32bit_segfault_check.py Tests/32bit_segfault_check.py
Tests/bench_cffi_access.py
Tests/check_*.py Tests/check_*.py
Tests/createfontdatachunk.py Tests/createfontdatachunk.py

View File

@ -18,9 +18,6 @@ else
fi fi
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
# TODO Update condition when cffi supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install olefile python3 -m pip install olefile

View File

@ -72,7 +72,6 @@ jobs:
make make
netpbm netpbm
perl perl
python3${{ matrix.python-minor-version }}-cffi
python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-numpy

View File

@ -64,7 +64,6 @@ jobs:
mingw-w64-x86_64-libtiff \ mingw-w64-x86_64-libtiff \
mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \ mingw-w64-x86_64-openjpeg2 \
mingw-w64-x86_64-python3-cffi \
mingw-w64-x86_64-python3-numpy \ mingw-w64-x86_64-python3-numpy \
mingw-w64-x86_64-python3-olefile \ mingw-w64-x86_64-python3-olefile \
mingw-w64-x86_64-python3-setuptools \ mingw-w64-x86_64-python3-setuptools \

View File

@ -1,54 +0,0 @@
from __future__ import annotations
import time
from PIL import PyAccess
from .helper import hopper
# Not running this test by default. No DOS against CI.
def iterate_get(size, access) -> None:
(w, h) = size
for x in range(w):
for y in range(h):
access[(x, y)]
def iterate_set(size, access) -> None:
(w, h) = size
for x in range(w):
for y in range(h):
access[(x, y)] = (x % 256, y % 256, 0)
def timer(func, label, *args) -> None:
iterations = 5000
starttime = time.time()
for x in range(iterations):
func(*args)
if time.time() - starttime > 10:
break
endtime = time.time()
print(
f"{label}: completed {x + 1} iterations in {endtime - starttime:.4f}s, "
f"{(endtime - starttime) / (x + 1.0):.6f}s per iteration"
)
def test_direct() -> None:
im = hopper()
im.load()
# im = Image.new("RGB", (2000, 2000), (1, 3, 2))
caccess = im.im.pixel_access(False)
access = PyAccess.new(im, False)
assert access is not None
assert caccess[(0, 0)] == access[(0, 0)]
print(f"Size: {im.width}x{im.height}")
timer(iterate_get, "PyAccess - get", im.size, access)
timer(iterate_set, "PyAccess - set", im.size, access)
timer(iterate_get, "C-api - get", im.size, caccess)
timer(iterate_set, "C-api - set", im.size, caccess)

View File

@ -12,19 +12,6 @@ from PIL import Image
from .helper import assert_image_equal, hopper, is_win32 from .helper import assert_image_equal, hopper, is_win32
# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2
# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670
cffi: ModuleType | None
if os.environ.get("PYTHONOPTIMIZE") == "2":
cffi = None
else:
try:
import cffi
from PIL import PyAccess
except ImportError:
cffi = None
numpy: ModuleType | None numpy: ModuleType | None
try: try:
import numpy import numpy
@ -32,21 +19,7 @@ except ImportError:
numpy = None numpy = None
class AccessTest: class TestImagePutPixel:
# Initial value
_init_cffi_access = Image.USE_CFFI_ACCESS
_need_cffi_access = False
@classmethod
def setup_class(cls) -> None:
Image.USE_CFFI_ACCESS = cls._need_cffi_access
@classmethod
def teardown_class(cls) -> None:
Image.USE_CFFI_ACCESS = cls._init_cffi_access
class TestImagePutPixel(AccessTest):
def test_sanity(self) -> None: def test_sanity(self) -> None:
im1 = hopper() im1 = hopper()
im2 = Image.new(im1.mode, im1.size, 0) im2 = Image.new(im1.mode, im1.size, 0)
@ -131,7 +104,7 @@ class TestImagePutPixel(AccessTest):
assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
class TestImageGetPixel(AccessTest): class TestImageGetPixel:
@staticmethod @staticmethod
def color(mode: str) -> int | tuple[int, ...]: def color(mode: str) -> int | tuple[int, ...]:
bands = Image.getmodebands(mode) bands = Image.getmodebands(mode)
@ -144,9 +117,6 @@ class TestImageGetPixel(AccessTest):
return tuple(range(1, bands + 1)) return tuple(range(1, bands + 1))
def check(self, mode: str, expected_color_int: int | None = None) -> None: def check(self, mode: str, expected_color_int: int | None = None) -> None:
if self._need_cffi_access and mode.startswith("BGR;"):
pytest.skip("Support not added to deprecated module for BGR;* modes")
expected_color = ( expected_color = (
self.color(mode) if expected_color_int is None else expected_color_int self.color(mode) if expected_color_int is None else expected_color_int
) )
@ -171,15 +141,14 @@ class TestImageGetPixel(AccessTest):
# Check 0x0 image with None initial color # Check 0x0 image with None initial color
im = Image.new(mode, (0, 0), None) im = Image.new(mode, (0, 0), None)
assert im.load() is not None assert im.load() is not None
error = ValueError if self._need_cffi_access else IndexError with pytest.raises(IndexError):
with pytest.raises(error):
im.putpixel((0, 0), expected_color) im.putpixel((0, 0), expected_color)
with pytest.raises(error): with pytest.raises(IndexError):
im.getpixel((0, 0)) im.getpixel((0, 0))
# Check negative index # Check negative index
with pytest.raises(error): with pytest.raises(IndexError):
im.putpixel((-1, -1), expected_color) im.putpixel((-1, -1), expected_color)
with pytest.raises(error): with pytest.raises(IndexError):
im.getpixel((-1, -1)) im.getpixel((-1, -1))
# Check initial color # Check initial color
@ -199,10 +168,10 @@ class TestImageGetPixel(AccessTest):
# Check 0x0 image with initial color # Check 0x0 image with initial color
im = Image.new(mode, (0, 0), expected_color) im = Image.new(mode, (0, 0), expected_color)
with pytest.raises(error): with pytest.raises(IndexError):
im.getpixel((0, 0)) im.getpixel((0, 0))
# Check negative index # Check negative index
with pytest.raises(error): with pytest.raises(IndexError):
im.getpixel((-1, -1)) im.getpixel((-1, -1))
@pytest.mark.parametrize("mode", Image.MODES) @pytest.mark.parametrize("mode", Image.MODES)
@ -235,126 +204,7 @@ class TestImageGetPixel(AccessTest):
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
@pytest.mark.filterwarnings("ignore::DeprecationWarning") class TestImagePutPixelError:
@pytest.mark.skipif(cffi is None, reason="No CFFI")
class TestCffiPutPixel(TestImagePutPixel):
_need_cffi_access = True
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
@pytest.mark.skipif(cffi is None, reason="No CFFI")
class TestCffiGetPixel(TestImageGetPixel):
_need_cffi_access = True
@pytest.mark.skipif(cffi is None, reason="No CFFI")
class TestCffi(AccessTest):
_need_cffi_access = True
def _test_get_access(self, im: Image.Image) -> None:
"""Do we get the same thing as the old pixel access
Using private interfaces, forcing a capi access and
a pyaccess for the same image"""
caccess = im.im.pixel_access(False)
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, False)
assert access is not None
w, h = im.size
for x in range(0, w, 10):
for y in range(0, h, 10):
assert access[(x, y)] == caccess[(x, y)]
# Access an out-of-range pixel
with pytest.raises(ValueError):
access[(access.xsize + 1, access.ysize + 1)]
def test_get_vs_c(self) -> None:
with pytest.warns(DeprecationWarning):
rgb = hopper("RGB")
rgb.load()
self._test_get_access(rgb)
for mode in ("RGBA", "L", "LA", "1", "P", "F"):
self._test_get_access(hopper(mode))
for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
im = Image.new(mode, (10, 10), 40000)
self._test_get_access(im)
def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None:
"""Are we writing the correct bits into the image?
Using private interfaces, forcing a capi access and
a pyaccess for the same image"""
caccess = im.im.pixel_access(False)
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, False)
assert access is not None
w, h = im.size
for x in range(0, w, 10):
for y in range(0, h, 10):
access[(x, y)] = color
assert color == caccess[(x, y)]
# Attempt to set the value on a read-only image
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, True)
assert access is not None
with pytest.raises(ValueError):
access[(0, 0)] = color
def test_set_vs_c(self) -> None:
rgb = hopper("RGB")
with pytest.warns(DeprecationWarning):
rgb.load()
self._test_set_access(rgb, (255, 128, 0))
self._test_set_access(hopper("RGBA"), (255, 192, 128, 0))
self._test_set_access(hopper("L"), 128)
self._test_set_access(hopper("LA"), (128, 128))
self._test_set_access(hopper("1"), 255)
self._test_set_access(hopper("P"), 128)
self._test_set_access(hopper("PA"), (128, 128))
self._test_set_access(hopper("F"), 1024.0)
for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
im = Image.new(mode, (10, 10), 40000)
self._test_set_access(im, 45000)
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_not_implemented(self) -> None:
assert PyAccess.new(hopper("BGR;15")) is None
# Ref https://github.com/python-pillow/Pillow/pull/2009
def test_reference_counting(self) -> None:
size = 10
for _ in range(10):
# Do not save references to the image, only to the access object
with pytest.warns(DeprecationWarning):
px = Image.new("L", (size, 1), 0).load()
for i in range(size):
# Pixels can contain garbage if image is released
assert px[i, 0] == 0
@pytest.mark.parametrize("mode", ("P", "PA"))
def test_p_putpixel_rgb_rgba(self, mode: str) -> None:
for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)):
im = Image.new(mode, (1, 1))
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, False)
assert access is not None
access.putpixel((0, 0), color)
if len(color) == 3:
color += (255,)
assert im.convert("RGBA").getpixel((0, 0)) == color
class TestImagePutPixelError(AccessTest):
IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"] IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"]
IMAGE_MODES2 = ["L", "I", "I;16"] IMAGE_MODES2 = ["L", "I", "I;16"]
INVALID_TYPES = ["foo", 1.0, None] INVALID_TYPES = ["foo", 1.0, None]

View File

@ -17,6 +17,5 @@ coverage:
# Matches 'omit:' in .coveragerc # Matches 'omit:' in .coveragerc
ignore: ignore:
- "Tests/32bit_segfault_check.py" - "Tests/32bit_segfault_check.py"
- "Tests/bench_cffi_access.py"
- "Tests/check_*.py" - "Tests/check_*.py"
- "Tests/createfontdatachunk.py" - "Tests/createfontdatachunk.py"

View File

@ -12,18 +12,6 @@ Deprecated features
Below are features which are considered deprecated. Where appropriate, Below are features which are considered deprecated. Where appropriate,
a :py:exc:`DeprecationWarning` is issued. a :py:exc:`DeprecationWarning` is issued.
PyAccess and Image.USE_CFFI_ACCESS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 10.0.0
Since Pillow's C API is now faster than PyAccess on PyPy,
:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow
11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead.
``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is
similarly deprecated.
ImageFile.raise_oserror ImageFile.raise_oserror
~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~
@ -138,6 +126,18 @@ This class was only made as a helper to be used internally,
so there is no replacement. If you need this functionality though, so there is no replacement. If you need this functionality though,
it is a very short class that can easily be recreated in your own code. it is a very short class that can easily be recreated in your own code.
PyAccess and Image.USE_CFFI_ACCESS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 10.0.0
.. versionremoved:: 11.0.0
Since Pillow's C API is now faster than PyAccess on PyPy, ``PyAccess`` has been
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.
Tk/Tcl 8.4 Tk/Tcl 8.4
~~~~~~~~~~ ~~~~~~~~~~

View File

@ -1,47 +0,0 @@
.. py:module:: PIL.PyAccess
.. py:currentmodule:: PIL.PyAccess
:py:mod:`~PIL.PyAccess` Module
==============================
The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version.
.. note:: Accessing individual pixels is fairly slow. If you are
looping over all of the pixels in an image, there is likely
a faster way using other parts of the Pillow API.
:mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps`
have methods for many standard operations. If you wish to perform
a custom mapping, check out :py:meth:`~PIL.Image.Image.point`.
Example
-------
The following script loads an image, accesses one pixel from it, then changes it. ::
from PIL import Image
with Image.open("hopper.jpg") as im:
px = im.load()
print(px[4, 4])
px[4, 4] = (0, 0, 0)
print(px[4, 4])
Results in the following::
(23, 24, 68)
(0, 0, 0)
Access using negative indexes is also possible. ::
px[-1, -1] = (0, 0, 0)
print(px[-1, -1])
:py:class:`PyAccess` Class
--------------------------
.. autoclass:: PIL.PyAccess.PyAccess()
:members:
:special-members: __getitem__, __setitem__

View File

@ -32,7 +32,6 @@ Reference
JpegPresets JpegPresets
PSDraw PSDraw
PixelAccess PixelAccess
PyAccess
features features
../PIL ../PIL
plugins plugins

View File

@ -25,6 +25,15 @@ This class was only made as a helper to be used internally,
so there is no replacement. If you need this functionality though, so there is no replacement. If you need this functionality though,
it is a very short class that can easily be recreated in your own code. it is a very short class that can easily be recreated in your own code.
PyAccess and Image.USE_CFFI_ACCESS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Since Pillow's C API is now faster than PyAccess on PyPy, ``PyAccess`` has been
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.
Deprecations Deprecations
============ ============

View File

@ -331,7 +331,6 @@ class GifImageFile(ImageFile.ImageFile):
LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
or palette or palette
): ):
self.pyaccess = None
if "transparency" in self.info: if "transparency" in self.info:
self.im.putpalettealpha(self.info["transparency"], 0) self.im.putpalettealpha(self.info["transparency"], 0)
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)

View File

@ -329,7 +329,6 @@ class IcoImageFile(ImageFile.ImageFile):
# if tile is PNG, it won't really be loaded yet # if tile is PNG, it won't really be loaded yet
im.load() im.load()
self.im = im.im self.im = im.im
self.pyaccess = None
self._mode = im.mode self._mode = im.mode
if im.palette: if im.palette:
self.palette = im.palette self.palette = im.palette

View File

@ -125,14 +125,6 @@ except ImportError as v:
raise raise
USE_CFFI_ACCESS = False
cffi: ModuleType | None
try:
import cffi
except ImportError:
cffi = None
def isImageType(t: Any) -> TypeGuard[Image]: def isImageType(t: Any) -> TypeGuard[Image]:
""" """
Checks if an object is an image object. Checks if an object is an image object.
@ -229,7 +221,7 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# Registries # Registries
if TYPE_CHECKING: if TYPE_CHECKING:
from . import ImageFile, PyAccess from . import ImageFile
ID: list[str] = [] ID: list[str] = []
OPEN: dict[ OPEN: dict[
str, str,
@ -549,7 +541,6 @@ class Image:
self.palette = None self.palette = None
self.info = {} self.info = {}
self.readonly = 0 self.readonly = 0
self.pyaccess = None
self._exif = None self._exif = None
@property @property
@ -631,7 +622,6 @@ class Image:
def _copy(self) -> None: def _copy(self) -> None:
self.load() self.load()
self.im = self.im.copy() self.im = self.im.copy()
self.pyaccess = None
self.readonly = 0 self.readonly = 0
def _ensure_mutable(self) -> None: def _ensure_mutable(self) -> None:
@ -882,7 +872,7 @@ class Image:
msg = "cannot decode image data" msg = "cannot decode image data"
raise ValueError(msg) raise ValueError(msg)
def load(self) -> core.PixelAccess | PyAccess.PyAccess | None: def load(self) -> core.PixelAccess | None:
""" """
Allocates storage for the image and loads the pixel data. In Allocates storage for the image and loads the pixel data. In
normal cases, you don't need to call this method, since the normal cases, you don't need to call this method, since the
@ -895,7 +885,7 @@ class Image:
operations. See :ref:`file-handling` for more information. operations. See :ref:`file-handling` for more information.
:returns: An image access object. :returns: An image access object.
:rtype: :py:class:`.PixelAccess` or :py:class:`.PyAccess` :rtype: :py:class:`.PixelAccess`
""" """
if self.im is not None and self.palette and self.palette.dirty: if self.im is not None and self.palette and self.palette.dirty:
# realize palette # realize palette
@ -915,14 +905,6 @@ class Image:
) )
if self.im is not None: if self.im is not None:
if cffi and USE_CFFI_ACCESS:
if self.pyaccess:
return self.pyaccess
from . import PyAccess
self.pyaccess = PyAccess.new(self, self.readonly)
if self.pyaccess:
return self.pyaccess
return self.im.pixel_access(self.readonly) return self.im.pixel_access(self.readonly)
return None return None
@ -1685,8 +1667,6 @@ class Image:
""" """
self.load() self.load()
if self.pyaccess:
return self.pyaccess.getpixel(xy)
return self.im.getpixel(tuple(xy)) return self.im.getpixel(tuple(xy))
def getprojection(self) -> tuple[list[int], list[int]]: def getprojection(self) -> tuple[list[int], list[int]]:
@ -1983,7 +1963,6 @@ class Image:
msg = "alpha channel could not be added" msg = "alpha channel could not be added"
raise ValueError(msg) from e # sanity check raise ValueError(msg) from e # sanity check
self.im = im self.im = im
self.pyaccess = None
self._mode = self.im.mode self._mode = self.im.mode
except KeyError as e: except KeyError as e:
msg = "illegal image mode" msg = "illegal image mode"
@ -2101,9 +2080,6 @@ class Image:
self._copy() self._copy()
self.load() self.load()
if self.pyaccess:
return self.pyaccess.putpixel(xy, value)
if ( if (
self.mode in ("P", "PA") self.mode in ("P", "PA")
and isinstance(value, (list, tuple)) and isinstance(value, (list, tuple))
@ -2768,7 +2744,6 @@ class Image:
self._mode = self.im.mode self._mode = self.im.mode
self.readonly = 0 self.readonly = 0
self.pyaccess = None
# FIXME: the different transform methods need further explanation # FIXME: the different transform methods need further explanation
# instead of bloating the method docs, add a separate chapter. # instead of bloating the method docs, add a separate chapter.

View File

@ -698,7 +698,6 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image
transposed_image = image.transpose(method) transposed_image = image.transpose(method)
if in_place: if in_place:
image.im = transposed_image.im image.im = transposed_image.im
image.pyaccess = None
image._size = transposed_image._size image._size = transposed_image._size
exif_image = image if in_place else transposed_image exif_image = image if in_place else transposed_image

View File

@ -851,8 +851,6 @@ class PngImageFile(ImageFile.ImageFile):
self.png.rewind() self.png.rewind()
self.__prepare_idat = self.__rewind_idat self.__prepare_idat = self.__rewind_idat
self.im = None self.im = None
if self.pyaccess:
self.pyaccess = None
self.info = self.png.im_info self.info = self.png.im_info
self.tile = self.png.im_tile self.tile = self.png.im_tile
self.fp = self._fp self.fp = self._fp
@ -1039,8 +1037,6 @@ class PngImageFile(ImageFile.ImageFile):
mask = updated.convert("RGBA") mask = updated.convert("RGBA")
self._prev_im.paste(updated, self.dispose_extent, mask) self._prev_im.paste(updated, self.dispose_extent, mask)
self.im = self._prev_im self.im = self._prev_im
if self.pyaccess:
self.pyaccess = None
def _getexif(self) -> dict[str, Any] | None: def _getexif(self) -> dict[str, Any] | None:
if "exif" not in self.info: if "exif" not in self.info:

View File

@ -1,381 +0,0 @@
#
# The Python Imaging Library
# Pillow fork
#
# Python implementation of the PixelAccess Object
#
# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved.
# Copyright (c) 1995-2009 by Fredrik Lundh.
# Copyright (c) 2013 Eric Soroos
#
# See the README file for information on usage and redistribution
#
# Notes:
#
# * Implements the pixel access object following Access.c
# * Taking only the tuple form, which is used from python.
# * Fill.c uses the integer form, but it's still going to use the old
# Access.c implementation.
#
from __future__ import annotations
import logging
import sys
from typing import TYPE_CHECKING
from ._deprecate import deprecate
FFI: type
try:
from cffi import FFI
defs = """
struct Pixel_RGBA {
unsigned char r,g,b,a;
};
struct Pixel_I16 {
unsigned char l,r;
};
"""
ffi = FFI()
ffi.cdef(defs)
except ImportError as ex:
# Allow error import for doc purposes, but error out when accessing
# anything in core.
from ._util import DeferredError
FFI = ffi = DeferredError.new(ex)
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from . import Image
class PyAccess:
def __init__(self, img: Image.Image, readonly: bool = False) -> None:
deprecate("PyAccess", 11)
vals = dict(img.im.unsafe_ptrs)
self.readonly = readonly
self.image8 = ffi.cast("unsigned char **", vals["image8"])
self.image32 = ffi.cast("int **", vals["image32"])
self.image = ffi.cast("unsigned char **", vals["image"])
self.xsize, self.ysize = img.im.size
self._img = img
# Keep pointer to im object to prevent dereferencing.
self._im = img.im
if self._im.mode in ("P", "PA"):
self._palette = img.palette
# Debugging is polluting test traces, only useful here
# when hacking on PyAccess
# logger.debug("%s", vals)
self._post_init()
def _post_init(self) -> None:
pass
def __setitem__(
self,
xy: tuple[int, int] | list[int],
color: float | tuple[int, ...] | list[int],
) -> None:
"""
Modifies the pixel at x,y. The color is given as a single
numerical value for single band images, and a tuple for
multi-band images. In addition to this, RGB and RGBA tuples
are accepted for P and PA images.
:param xy: The pixel coordinate, given as (x, y). See
:ref:`coordinate-system`.
:param color: The pixel value.
"""
if self.readonly:
msg = "Attempt to putpixel a read only image"
raise ValueError(msg)
(x, y) = xy
if x < 0:
x = self.xsize + x
if y < 0:
y = self.ysize + y
(x, y) = self.check_xy((x, y))
if (
self._im.mode in ("P", "PA")
and isinstance(color, (list, tuple))
and len(color) in [3, 4]
):
# RGB or RGBA value for a P or PA image
if self._im.mode == "PA":
alpha = color[3] if len(color) == 4 else 255
color = color[:3]
palette_index = self._palette.getcolor(color, self._img)
color = (palette_index, alpha) if self._im.mode == "PA" else palette_index
return self.set_pixel(x, y, color)
def __getitem__(self, xy: tuple[int, int] | list[int]) -> float | tuple[int, ...]:
"""
Returns the pixel at x,y. The pixel is returned as a single
value for single band images or a tuple for multiple band
images
:param xy: The pixel coordinate, given as (x, y). See
:ref:`coordinate-system`.
:returns: a pixel value for single band images, a tuple of
pixel values for multiband images.
"""
(x, y) = xy
if x < 0:
x = self.xsize + x
if y < 0:
y = self.ysize + y
(x, y) = self.check_xy((x, y))
return self.get_pixel(x, y)
putpixel = __setitem__
getpixel = __getitem__
def check_xy(self, xy: tuple[int, int]) -> tuple[int, int]:
(x, y) = xy
if not (0 <= x < self.xsize and 0 <= y < self.ysize):
msg = "pixel location out of range"
raise ValueError(msg)
return xy
def get_pixel(self, x: int, y: int) -> float | tuple[int, ...]:
raise NotImplementedError()
def set_pixel(
self, x: int, y: int, color: float | tuple[int, ...] | list[int]
) -> None:
raise NotImplementedError()
class _PyAccess32_2(PyAccess):
"""PA, LA, stored in first and last bytes of a 32 bit word"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
def get_pixel(self, x: int, y: int) -> tuple[int, int]:
pixel = self.pixels[y][x]
return pixel.r, pixel.a
def set_pixel(self, x, y, color):
pixel = self.pixels[y][x]
# tuple
pixel.r = min(color[0], 255)
pixel.a = min(color[1], 255)
class _PyAccess32_3(PyAccess):
"""RGB and friends, stored in the first three bytes of a 32 bit word"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
def get_pixel(self, x: int, y: int) -> tuple[int, int, int]:
pixel = self.pixels[y][x]
return pixel.r, pixel.g, pixel.b
def set_pixel(self, x, y, color):
pixel = self.pixels[y][x]
# tuple
pixel.r = min(color[0], 255)
pixel.g = min(color[1], 255)
pixel.b = min(color[2], 255)
pixel.a = 255
class _PyAccess32_4(PyAccess):
"""RGBA etc, all 4 bytes of a 32 bit word"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
def get_pixel(self, x: int, y: int) -> tuple[int, int, int, int]:
pixel = self.pixels[y][x]
return pixel.r, pixel.g, pixel.b, pixel.a
def set_pixel(self, x, y, color):
pixel = self.pixels[y][x]
# tuple
pixel.r = min(color[0], 255)
pixel.g = min(color[1], 255)
pixel.b = min(color[2], 255)
pixel.a = min(color[3], 255)
class _PyAccess8(PyAccess):
"""1, L, P, 8 bit images stored as uint8"""
def _post_init(self, *args, **kwargs):
self.pixels = self.image8
def get_pixel(self, x: int, y: int) -> int:
return self.pixels[y][x]
def set_pixel(self, x, y, color):
try:
# integer
self.pixels[y][x] = min(color, 255)
except TypeError:
# tuple
self.pixels[y][x] = min(color[0], 255)
class _PyAccessI16_N(PyAccess):
"""I;16 access, native bitendian without conversion"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("unsigned short **", self.image)
def get_pixel(self, x: int, y: int) -> int:
return self.pixels[y][x]
def set_pixel(self, x, y, color):
try:
# integer
self.pixels[y][x] = min(color, 65535)
except TypeError:
# tuple
self.pixels[y][x] = min(color[0], 65535)
class _PyAccessI16_L(PyAccess):
"""I;16L access, with conversion"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
def get_pixel(self, x: int, y: int) -> int:
pixel = self.pixels[y][x]
return pixel.l + pixel.r * 256
def set_pixel(self, x, y, color):
pixel = self.pixels[y][x]
try:
color = min(color, 65535)
except TypeError:
color = min(color[0], 65535)
pixel.l = color & 0xFF
pixel.r = color >> 8
class _PyAccessI16_B(PyAccess):
"""I;16B access, with conversion"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
def get_pixel(self, x: int, y: int) -> int:
pixel = self.pixels[y][x]
return pixel.l * 256 + pixel.r
def set_pixel(self, x, y, color):
pixel = self.pixels[y][x]
try:
color = min(color, 65535)
except Exception:
color = min(color[0], 65535)
pixel.l = color >> 8
pixel.r = color & 0xFF
class _PyAccessI32_N(PyAccess):
"""Signed Int32 access, native endian"""
def _post_init(self, *args, **kwargs):
self.pixels = self.image32
def get_pixel(self, x: int, y: int) -> int:
return self.pixels[y][x]
def set_pixel(self, x, y, color):
self.pixels[y][x] = color
class _PyAccessI32_Swap(PyAccess):
"""I;32L/B access, with byteswapping conversion"""
def _post_init(self, *args, **kwargs):
self.pixels = self.image32
def reverse(self, i):
orig = ffi.new("int *", i)
chars = ffi.cast("unsigned char *", orig)
chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], chars[1], chars[0]
return ffi.cast("int *", chars)[0]
def get_pixel(self, x: int, y: int) -> int:
return self.reverse(self.pixels[y][x])
def set_pixel(self, x, y, color):
self.pixels[y][x] = self.reverse(color)
class _PyAccessF(PyAccess):
"""32 bit float access"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("float **", self.image32)
def get_pixel(self, x: int, y: int) -> float:
return self.pixels[y][x]
def set_pixel(self, x, y, color):
try:
# not a tuple
self.pixels[y][x] = color
except TypeError:
# tuple
self.pixels[y][x] = color[0]
mode_map = {
"1": _PyAccess8,
"L": _PyAccess8,
"P": _PyAccess8,
"I;16N": _PyAccessI16_N,
"LA": _PyAccess32_2,
"La": _PyAccess32_2,
"PA": _PyAccess32_2,
"RGB": _PyAccess32_3,
"LAB": _PyAccess32_3,
"HSV": _PyAccess32_3,
"YCbCr": _PyAccess32_3,
"RGBA": _PyAccess32_4,
"RGBa": _PyAccess32_4,
"RGBX": _PyAccess32_4,
"CMYK": _PyAccess32_4,
"F": _PyAccessF,
"I": _PyAccessI32_N,
}
if sys.byteorder == "little":
mode_map["I;16"] = _PyAccessI16_N
mode_map["I;16L"] = _PyAccessI16_N
mode_map["I;16B"] = _PyAccessI16_B
mode_map["I;32L"] = _PyAccessI32_N
mode_map["I;32B"] = _PyAccessI32_Swap
else:
mode_map["I;16"] = _PyAccessI16_L
mode_map["I;16L"] = _PyAccessI16_L
mode_map["I;16B"] = _PyAccessI16_N
mode_map["I;32L"] = _PyAccessI32_Swap
mode_map["I;32B"] = _PyAccessI32_N
def new(img: Image.Image, readonly: bool = False) -> PyAccess | None:
access_type = mode_map.get(img.mode, None)
if not access_type:
logger.debug("PyAccess Not Implemented: %s", img.mode)
return None
return access_type(img, readonly)

View File

@ -7,7 +7,6 @@ env_list =
[testenv] [testenv]
deps = deps =
cffi
numpy numpy
extras = extras =
tests tests
@ -39,7 +38,6 @@ deps =
ipython ipython
numpy numpy
packaging packaging
types-cffi
types-defusedxml types-defusedxml
types-olefile types-olefile
extras = extras =