From 43cc1e36595422047b98659e7346ee1c8b08bd6f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Jun 2024 15:02:50 +1000 Subject: [PATCH] Removed PyAccess and Image.USE_CFFI_ACCESS --- .ci/install.sh | 2 - .coveragerc | 1 - .github/workflows/macos-install.sh | 3 - .github/workflows/test-cygwin.yml | 1 - .github/workflows/test-mingw.yml | 1 - Tests/bench_cffi_access.py | 54 ---- Tests/test_image_access.py | 168 +------------ codecov.yml | 1 - docs/deprecations.rst | 24 +- docs/reference/PyAccess.rst | 47 ---- docs/reference/index.rst | 1 - docs/releasenotes/11.0.0.rst | 9 + src/PIL/GifImagePlugin.py | 1 - src/PIL/IcoImagePlugin.py | 1 - src/PIL/Image.py | 31 +-- src/PIL/ImageOps.py | 1 - src/PIL/PngImagePlugin.py | 4 - src/PIL/PyAccess.py | 381 ----------------------------- tox.ini | 2 - 19 files changed, 33 insertions(+), 700 deletions(-) delete mode 100644 Tests/bench_cffi_access.py delete mode 100644 docs/reference/PyAccess.rst delete mode 100644 src/PIL/PyAccess.py diff --git a/.ci/install.sh b/.ci/install.sh index 30b64349d..c6e166171 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -28,8 +28,6 @@ fi python3 -m pip install --upgrade pip 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 defusedxml python3 -m pip install olefile diff --git a/.coveragerc b/.coveragerc index 018cc1cbf..a94a25678 100644 --- a/.coveragerc +++ b/.coveragerc @@ -19,6 +19,5 @@ exclude_also = [run] omit = Tests/32bit_segfault_check.py - Tests/bench_cffi_access.py Tests/check_*.py Tests/createfontdatachunk.py diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index f8f191d38..ae346e8b6 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -18,9 +18,6 @@ else fi 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 defusedxml python3 -m pip install olefile diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 1269ef8cb..97422d569 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -72,7 +72,6 @@ jobs: make netpbm perl - python3${{ matrix.python-minor-version }}-cffi python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-numpy diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a773ca453..e5e1ec32e 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -64,7 +64,6 @@ jobs: mingw-w64-x86_64-libtiff \ mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-openjpeg2 \ - mingw-w64-x86_64-python3-cffi \ mingw-w64-x86_64-python3-numpy \ mingw-w64-x86_64-python3-olefile \ mingw-w64-x86_64-python3-setuptools \ diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py deleted file mode 100644 index c7d105836..000000000 --- a/Tests/bench_cffi_access.py +++ /dev/null @@ -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) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 8abb1f69f..52ffef5e8 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -12,19 +12,6 @@ from PIL import Image 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 try: import numpy @@ -32,21 +19,7 @@ except ImportError: numpy = None -class AccessTest: - # 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): +class TestImagePutPixel: def test_sanity(self) -> None: im1 = hopper() 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) -class TestImageGetPixel(AccessTest): +class TestImageGetPixel: @staticmethod def color(mode: str) -> int | tuple[int, ...]: bands = Image.getmodebands(mode) @@ -144,9 +117,6 @@ class TestImageGetPixel(AccessTest): return tuple(range(1, bands + 1)) 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 = ( 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 im = Image.new(mode, (0, 0), None) assert im.load() is not None - error = ValueError if self._need_cffi_access else IndexError - with pytest.raises(error): + with pytest.raises(IndexError): im.putpixel((0, 0), expected_color) - with pytest.raises(error): + with pytest.raises(IndexError): im.getpixel((0, 0)) # Check negative index - with pytest.raises(error): + with pytest.raises(IndexError): im.putpixel((-1, -1), expected_color) - with pytest.raises(error): + with pytest.raises(IndexError): im.getpixel((-1, -1)) # Check initial color @@ -199,10 +168,10 @@ class TestImageGetPixel(AccessTest): # Check 0x0 image with initial color im = Image.new(mode, (0, 0), expected_color) - with pytest.raises(error): + with pytest.raises(IndexError): im.getpixel((0, 0)) # Check negative index - with pytest.raises(error): + with pytest.raises(IndexError): im.getpixel((-1, -1)) @pytest.mark.parametrize("mode", Image.MODES) @@ -235,126 +204,7 @@ class TestImageGetPixel(AccessTest): assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) -@pytest.mark.filterwarnings("ignore::DeprecationWarning") -@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): +class TestImagePutPixelError: IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"] IMAGE_MODES2 = ["L", "I", "I;16"] INVALID_TYPES = ["foo", 1.0, None] diff --git a/codecov.yml b/codecov.yml index 1ea7974eb..8646576bb 100644 --- a/codecov.yml +++ b/codecov.yml @@ -17,6 +17,5 @@ coverage: # Matches 'omit:' in .coveragerc ignore: - "Tests/32bit_segfault_check.py" - - "Tests/bench_cffi_access.py" - "Tests/check_*.py" - "Tests/createfontdatachunk.py" diff --git a/docs/deprecations.rst b/docs/deprecations.rst index b7c15ff63..792fd1c70 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,18 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, 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 ~~~~~~~~~~~~~~~~~~~~~~~ @@ -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, 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 ~~~~~~~~~~ diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst deleted file mode 100644 index 04b2a47ee..000000000 --- a/docs/reference/PyAccess.rst +++ /dev/null @@ -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__ diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 82c75e373..effcd3c46 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -32,7 +32,6 @@ Reference JpegPresets PSDraw PixelAccess - PyAccess features ../PIL plugins diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index e04ae9fd6..bde791b1e 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -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, 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 ============ diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 284128c77..8018e25dc 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -331,7 +331,6 @@ class GifImageFile(ImageFile.ImageFile): LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY or palette ): - self.pyaccess = None if "transparency" in self.info: self.im.putpalettealpha(self.info["transparency"], 0) self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 227fcf35c..086c87b1a 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -329,7 +329,6 @@ class IcoImageFile(ImageFile.ImageFile): # if tile is PNG, it won't really be loaded yet im.load() self.im = im.im - self.pyaccess = None self._mode = im.mode if im.palette: self.palette = im.palette diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d41c06523..dbdb3b132 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -125,14 +125,6 @@ except ImportError as v: raise -USE_CFFI_ACCESS = False -cffi: ModuleType | None -try: - import cffi -except ImportError: - cffi = None - - def isImageType(t: Any) -> TypeGuard[Image]: """ Checks if an object is an image object. @@ -229,7 +221,7 @@ if hasattr(core, "DEFAULT_STRATEGY"): # Registries if TYPE_CHECKING: - from . import ImageFile, PyAccess + from . import ImageFile ID: list[str] = [] OPEN: dict[ str, @@ -549,7 +541,6 @@ class Image: self.palette = None self.info = {} self.readonly = 0 - self.pyaccess = None self._exif = None @property @@ -631,7 +622,6 @@ class Image: def _copy(self) -> None: self.load() self.im = self.im.copy() - self.pyaccess = None self.readonly = 0 def _ensure_mutable(self) -> None: @@ -882,7 +872,7 @@ class Image: msg = "cannot decode image data" 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 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. :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: # realize palette @@ -915,14 +905,6 @@ class Image: ) 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 None @@ -1685,8 +1667,6 @@ class Image: """ self.load() - if self.pyaccess: - return self.pyaccess.getpixel(xy) return self.im.getpixel(tuple(xy)) def getprojection(self) -> tuple[list[int], list[int]]: @@ -1983,7 +1963,6 @@ class Image: msg = "alpha channel could not be added" raise ValueError(msg) from e # sanity check self.im = im - self.pyaccess = None self._mode = self.im.mode except KeyError as e: msg = "illegal image mode" @@ -2101,9 +2080,6 @@ class Image: self._copy() self.load() - if self.pyaccess: - return self.pyaccess.putpixel(xy, value) - if ( self.mode in ("P", "PA") and isinstance(value, (list, tuple)) @@ -2768,7 +2744,6 @@ class Image: self._mode = self.im.mode self.readonly = 0 - self.pyaccess = None # FIXME: the different transform methods need further explanation # instead of bloating the method docs, add a separate chapter. diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index a84c08345..55335872d 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -698,7 +698,6 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image transposed_image = image.transpose(method) if in_place: image.im = transposed_image.im - image.pyaccess = None image._size = transposed_image._size exif_image = image if in_place else transposed_image diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index d2834929b..34ea77c5e 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -851,8 +851,6 @@ class PngImageFile(ImageFile.ImageFile): self.png.rewind() self.__prepare_idat = self.__rewind_idat self.im = None - if self.pyaccess: - self.pyaccess = None self.info = self.png.im_info self.tile = self.png.im_tile self.fp = self._fp @@ -1039,8 +1037,6 @@ class PngImageFile(ImageFile.ImageFile): mask = updated.convert("RGBA") self._prev_im.paste(updated, self.dispose_extent, mask) self.im = self._prev_im - if self.pyaccess: - self.pyaccess = None def _getexif(self) -> dict[str, Any] | None: if "exif" not in self.info: diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py deleted file mode 100644 index 3be1ccace..000000000 --- a/src/PIL/PyAccess.py +++ /dev/null @@ -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) diff --git a/tox.ini b/tox.ini index 85a2020d6..d225a3106 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,6 @@ env_list = [testenv] deps = - cffi numpy extras = tests @@ -39,7 +38,6 @@ deps = ipython numpy packaging - types-cffi types-defusedxml types-olefile extras =