Added type hints for PixelAccess methods and others

This commit is contained in:
Nulano 2024-04-29 23:19:36 +02:00
parent 9b1390792c
commit 5f805c39cc
7 changed files with 97 additions and 92 deletions

View File

@ -679,23 +679,7 @@ Methods
:param hints: An optional list of hints. :param hints: An optional list of hints.
:returns: A (drawing context, drawing resource factory) tuple. :returns: A (drawing context, drawing resource factory) tuple.
.. py:method:: floodfill(image, xy, value, border=None, thresh=0) .. autofunction:: PIL.ImageDraw.floodfill
.. warning:: This method is experimental.
Fills a bounded region with a given color.
:param image: Target image.
:param xy: Seed position (a 2-item coordinate tuple).
:param value: Fill color.
:param border: Optional border value. If given, the region consists of
pixels with a color different from the border color. If not given,
the region consists of pixels having the same color as the seed
pixel.
:param thresh: Optional threshold value which specifies a maximum
tolerable difference of a pixel value from the 'background' in
order for it to be replaced. Useful for filling regions of non-
homogeneous, but similar, colors.
.. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/ .. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/
.. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist .. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist

View File

@ -44,42 +44,7 @@ Access using negative indexes is also possible. ::
----------------------------- -----------------------------
.. class:: PixelAccess .. class:: PixelAccess
:canonical: PIL.Image.PixelAccess
.. method:: __setitem__(self, xy, color): .. automethod:: PIL.Image.PixelAccess.__getitem__
.. automethod:: PIL.Image.PixelAccess.__setitem__
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
:param xy: The pixel coordinate, given as (x, y).
:param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)
.. method:: __getitem__(self, xy):
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).
:returns: a pixel value for single band images, a tuple of
pixel values for multiband images.
.. method:: putpixel(self, xy, color):
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).
:param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)
.. method:: getpixel(self, xy):
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).
:returns: a pixel value for single band images, a tuple of
pixel values for multiband images.

View File

@ -44,3 +44,4 @@ Access using negative indexes is also possible. ::
.. autoclass:: PIL.PyAccess.PyAccess() .. autoclass:: PIL.PyAccess.PyAccess()
:members: :members:
:special-members: __getitem__, __setitem__

View File

@ -41,7 +41,7 @@ import warnings
from collections.abc import Callable, MutableMapping from collections.abc import Callable, MutableMapping
from enum import IntEnum from enum import IntEnum
from types import ModuleType from types import ModuleType
from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, cast from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, SupportsInt, cast
# VERSION was removed in Pillow 6.0.0. # VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0.
@ -482,6 +482,31 @@ def _getscaleoffset(expr):
# Implementation wrapper # Implementation wrapper
class PixelAccess(Protocol):
def __getitem__(self, xy: tuple[int, 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 multi-band images.
:param xy: The pixel coordinate, given as (x, y).
:returns: a pixel value for single band images, a tuple of
pixel values for multiband images.
"""
raise NotImplementedError()
def __setitem__(self, xy: tuple[int, int], color: float | tuple[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.
:param xy: The pixel coordinate, given as (x, y).
:param color: The pixel value according to its mode,
e.g. tuple (r, g, b) for RGB mode.
"""
raise NotImplementedError()
class Image: class Image:
""" """
This class represents an image object. To create This class represents an image object. To create
@ -834,7 +859,7 @@ class Image:
msg = "cannot decode image data" msg = "cannot decode image data"
raise ValueError(msg) raise ValueError(msg)
def load(self): def load(self) -> 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
@ -847,7 +872,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: :ref:`PixelAccess` or :py:class:`PIL.PyAccess` :rtype: :py:class:`.PixelAccess` or :py:class:`.PyAccess`
""" """
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
@ -876,6 +901,7 @@ class Image:
if self.pyaccess: if self.pyaccess:
return self.pyaccess return self.pyaccess
return self.im.pixel_access(self.readonly) return self.im.pixel_access(self.readonly)
return None
def verify(self): def verify(self):
""" """
@ -1485,7 +1511,7 @@ class Image:
self._exif._loaded = False self._exif._loaded = False
self.getexif() self.getexif()
def get_child_images(self): def get_child_images(self) -> list[ImageFile.ImageFile]:
child_images = [] child_images = []
exif = self.getexif() exif = self.getexif()
ifds = [] ifds = []
@ -1509,10 +1535,7 @@ class Image:
fp = self.fp fp = self.fp
thumbnail_offset = ifd.get(513) thumbnail_offset = ifd.get(513)
if thumbnail_offset is not None: if thumbnail_offset is not None:
try: thumbnail_offset += getattr(self, "_exif_offset", 0)
thumbnail_offset += self._exif_offset
except AttributeError:
pass
self.fp.seek(thumbnail_offset) self.fp.seek(thumbnail_offset)
data = self.fp.read(ifd.get(514)) data = self.fp.read(ifd.get(514))
fp = io.BytesIO(data) fp = io.BytesIO(data)
@ -1578,7 +1601,7 @@ class Image:
or "transparency" in self.info or "transparency" in self.info
) )
def apply_transparency(self): def apply_transparency(self) -> None:
""" """
If a P mode image has a "transparency" key in the info dictionary, If a P mode image has a "transparency" key in the info dictionary,
remove the key and instead apply the transparency to the palette. remove the key and instead apply the transparency to the palette.
@ -1590,6 +1613,7 @@ class Image:
from . import ImagePalette from . import ImagePalette
palette = self.getpalette("RGBA") palette = self.getpalette("RGBA")
assert palette is not None
transparency = self.info["transparency"] transparency = self.info["transparency"]
if isinstance(transparency, bytes): if isinstance(transparency, bytes):
for i, alpha in enumerate(transparency): for i, alpha in enumerate(transparency):
@ -1601,7 +1625,9 @@ class Image:
del self.info["transparency"] del self.info["transparency"]
def getpixel(self, xy): def getpixel(
self, xy: tuple[SupportsInt, SupportsInt]
) -> float | tuple[int, ...] | None:
""" """
Returns the pixel value at a given position. Returns the pixel value at a given position.
@ -1865,7 +1891,7 @@ class Image:
lut = [round(i) for i in lut] lut = [round(i) for i in lut]
return self._new(self.im.point(lut, mode)) return self._new(self.im.point(lut, mode))
def putalpha(self, alpha): def putalpha(self, alpha: Image | int) -> None:
""" """
Adds or replaces the alpha layer in this image. If the image Adds or replaces the alpha layer in this image. If the image
does not have an alpha layer, it's converted to "LA" or "RGBA". does not have an alpha layer, it's converted to "LA" or "RGBA".
@ -1912,6 +1938,7 @@ class Image:
alpha = alpha.convert("L") alpha = alpha.convert("L")
else: else:
# constant alpha # constant alpha
alpha = cast(int, alpha) # see python/typing#1013
try: try:
self.im.fillband(band, alpha) self.im.fillband(band, alpha)
except (AttributeError, ValueError): except (AttributeError, ValueError):
@ -1975,7 +2002,7 @@ class Image:
self.palette.mode = "RGB" self.palette.mode = "RGB"
self.load() # install new palette self.load() # install new palette
def putpixel(self, xy, value): def putpixel(self, xy: tuple[int, int], value: float | tuple[int, ...]) -> None:
""" """
Modifies the pixel at the given position. The color is given as Modifies the pixel at the given position. The color is given as
a single numerical value for single-band images, and a tuple for a single numerical value for single-band images, and a tuple for
@ -2015,7 +2042,7 @@ class Image:
value = value[:3] value = value[:3]
value = self.palette.getcolor(value, self) value = self.palette.getcolor(value, self)
if self.mode == "PA": if self.mode == "PA":
value = (value, alpha) value = (value, alpha) # type: ignore[assignment]
return self.im.putpixel(xy, value) return self.im.putpixel(xy, value)
def remap_palette(self, dest_map, source_palette=None): def remap_palette(self, dest_map, source_palette=None):

View File

@ -898,9 +898,17 @@ def getdraw(im=None, hints=None):
return im, handler return im, handler
def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None: def floodfill(
image: Image.Image,
xy: tuple[int, int],
value: float | tuple[int, ...],
border: float | tuple[int, ...] | None = None,
thresh: float = 0,
) -> None:
""" """
(experimental) Fills a bounded region with a given color. .. warning:: This method is experimental.
Fills a bounded region with a given color.
:param image: Target image. :param image: Target image.
:param xy: Seed position (a 2-item coordinate tuple). See :param xy: Seed position (a 2-item coordinate tuple). See
@ -918,6 +926,7 @@ def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None:
# based on an implementation by Eric S. Raymond # based on an implementation by Eric S. Raymond
# amended by yo1995 @20180806 # amended by yo1995 @20180806
pixel = image.load() pixel = image.load()
assert pixel is not None
x, y = xy x, y = xy
try: try:
background = pixel[x, y] background = pixel[x, y]

View File

@ -22,11 +22,17 @@ import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
from typing import Union, cast
from . import Image from . import Image
def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None): def grab(
bbox: tuple[int, int, int, int] | None = None,
include_layered_windows: bool = False,
all_screens: bool = False,
xdisplay: str | None = None,
) -> Image.Image:
if xdisplay is None: if xdisplay is None:
if sys.platform == "darwin": if sys.platform == "darwin":
fh, filepath = tempfile.mkstemp(".png") fh, filepath = tempfile.mkstemp(".png")
@ -36,7 +42,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
left, top, right, bottom = bbox left, top, right, bottom = bbox
args += ["-R", f"{left},{top},{right-left},{bottom-top}"] args += ["-R", f"{left},{top},{right-left},{bottom-top}"]
subprocess.call(args + ["-x", filepath]) subprocess.call(args + ["-x", filepath])
im = Image.open(filepath) im: Image.Image = Image.open(filepath)
im.load() im.load()
os.unlink(filepath) os.unlink(filepath)
if bbox: if bbox:
@ -63,6 +69,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
left, top, right, bottom = bbox left, top, right, bottom = bbox
im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
return im return im
xdisplay = cast(Union[str, None], xdisplay) # type: ignore[redundant-cast, unused-ignore]
try: try:
if not Image.core.HAVE_XCB: if not Image.core.HAVE_XCB:
msg = "Pillow was built without XCB support" msg = "Pillow was built without XCB support"
@ -77,7 +84,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
fh, filepath = tempfile.mkstemp(".png") fh, filepath = tempfile.mkstemp(".png")
os.close(fh) os.close(fh)
subprocess.call(["gnome-screenshot", "-f", filepath]) subprocess.call(["gnome-screenshot", "-f", filepath])
im = Image.open(filepath) im: Image.Image = Image.open(filepath)
im.load() im.load()
os.unlink(filepath) os.unlink(filepath)
if bbox: if bbox:
@ -94,7 +101,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
return im return im
def grabclipboard(): def grabclipboard() -> Image.Image | list[str] | None:
if sys.platform == "darwin": if sys.platform == "darwin":
fh, filepath = tempfile.mkstemp(".png") fh, filepath = tempfile.mkstemp(".png")
os.close(fh) os.close(fh)

View File

@ -22,6 +22,7 @@ from __future__ import annotations
import logging import logging
import sys import sys
from typing import TYPE_CHECKING
from ._deprecate import deprecate from ._deprecate import deprecate
@ -50,7 +51,7 @@ logger = logging.getLogger(__name__)
class PyAccess: class PyAccess:
def __init__(self, img, readonly=False): def __init__(self, img: Image.Image, readonly: bool = False) -> None:
deprecate("PyAccess", 11) deprecate("PyAccess", 11)
vals = dict(img.im.unsafe_ptrs) vals = dict(img.im.unsafe_ptrs)
self.readonly = readonly self.readonly = readonly
@ -70,14 +71,15 @@ class PyAccess:
# logger.debug("%s", vals) # logger.debug("%s", vals)
self._post_init() self._post_init()
def _post_init(self): def _post_init(self) -> None:
pass pass
def __setitem__(self, xy, color): def __setitem__(self, xy: tuple[int, int], color: float | tuple[int, ...]) -> None:
""" """
Modifies the pixel at x,y. The color is given as a single Modifies the pixel at x,y. The color is given as a single
numerical value for single band images, and a tuple for numerical value for single band images, and a tuple for
multi-band images 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 :param xy: The pixel coordinate, given as (x, y). See
:ref:`coordinate-system`. :ref:`coordinate-system`.
@ -104,11 +106,11 @@ class PyAccess:
color = color[:3] color = color[:3]
color = self._palette.getcolor(color, self._img) color = self._palette.getcolor(color, self._img)
if self._im.mode == "PA": if self._im.mode == "PA":
color = (color, alpha) color = (color, alpha) # type: ignore[assignment]
return self.set_pixel(x, y, color) return self.set_pixel(x, y, color)
def __getitem__(self, xy): def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]:
""" """
Returns the pixel at x,y. The pixel is returned as a single Returns the pixel at x,y. The pixel is returned as a single
value for single band images or a tuple for multiple band value for single band images or a tuple for multiple band
@ -130,13 +132,19 @@ class PyAccess:
putpixel = __setitem__ putpixel = __setitem__
getpixel = __getitem__ getpixel = __getitem__
def check_xy(self, xy): def check_xy(self, xy: tuple[int, int]) -> tuple[int, int]:
(x, y) = xy (x, y) = xy
if not (0 <= x < self.xsize and 0 <= y < self.ysize): if not (0 <= x < self.xsize and 0 <= y < self.ysize):
msg = "pixel location out of range" msg = "pixel location out of range"
raise ValueError(msg) raise ValueError(msg)
return xy 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, ...]) -> None:
raise NotImplementedError()
class _PyAccess32_2(PyAccess): class _PyAccess32_2(PyAccess):
"""PA, LA, stored in first and last bytes of a 32 bit word""" """PA, LA, stored in first and last bytes of a 32 bit word"""
@ -144,7 +152,7 @@ class _PyAccess32_2(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> tuple[int, int]:
pixel = self.pixels[y][x] pixel = self.pixels[y][x]
return pixel.r, pixel.a return pixel.r, pixel.a
@ -161,7 +169,7 @@ class _PyAccess32_3(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> tuple[int, int, int]:
pixel = self.pixels[y][x] pixel = self.pixels[y][x]
return pixel.r, pixel.g, pixel.b return pixel.r, pixel.g, pixel.b
@ -180,7 +188,7 @@ class _PyAccess32_4(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> tuple[int, int, int, int]:
pixel = self.pixels[y][x] pixel = self.pixels[y][x]
return pixel.r, pixel.g, pixel.b, pixel.a return pixel.r, pixel.g, pixel.b, pixel.a
@ -199,7 +207,7 @@ class _PyAccess8(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = self.image8 self.pixels = self.image8
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> int:
return self.pixels[y][x] return self.pixels[y][x]
def set_pixel(self, x, y, color): def set_pixel(self, x, y, color):
@ -217,7 +225,7 @@ class _PyAccessI16_N(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("unsigned short **", self.image) self.pixels = ffi.cast("unsigned short **", self.image)
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> int:
return self.pixels[y][x] return self.pixels[y][x]
def set_pixel(self, x, y, color): def set_pixel(self, x, y, color):
@ -235,7 +243,7 @@ class _PyAccessI16_L(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_I16 **", self.image) self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> int:
pixel = self.pixels[y][x] pixel = self.pixels[y][x]
return pixel.l + pixel.r * 256 return pixel.l + pixel.r * 256
@ -256,7 +264,7 @@ class _PyAccessI16_B(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_I16 **", self.image) self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> int:
pixel = self.pixels[y][x] pixel = self.pixels[y][x]
return pixel.l * 256 + pixel.r return pixel.l * 256 + pixel.r
@ -277,7 +285,7 @@ class _PyAccessI32_N(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = self.image32 self.pixels = self.image32
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> int:
return self.pixels[y][x] return self.pixels[y][x]
def set_pixel(self, x, y, color): def set_pixel(self, x, y, color):
@ -296,7 +304,7 @@ class _PyAccessI32_Swap(PyAccess):
chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], chars[1], chars[0] chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], chars[1], chars[0]
return ffi.cast("int *", chars)[0] return ffi.cast("int *", chars)[0]
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> int:
return self.reverse(self.pixels[y][x]) return self.reverse(self.pixels[y][x])
def set_pixel(self, x, y, color): def set_pixel(self, x, y, color):
@ -309,7 +317,7 @@ class _PyAccessF(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("float **", self.image32) self.pixels = ffi.cast("float **", self.image32)
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> float:
return self.pixels[y][x] return self.pixels[y][x]
def set_pixel(self, x, y, color): def set_pixel(self, x, y, color):
@ -357,9 +365,13 @@ else:
mode_map["I;32B"] = _PyAccessI32_N mode_map["I;32B"] = _PyAccessI32_N
def new(img, readonly=False): def new(img: Image.Image, readonly: bool = False) -> PyAccess | None:
access_type = mode_map.get(img.mode, None) access_type = mode_map.get(img.mode, None)
if not access_type: if not access_type:
logger.debug("PyAccess Not Implemented: %s", img.mode) logger.debug("PyAccess Not Implemented: %s", img.mode)
return None return None
return access_type(img, readonly) return access_type(img, readonly)
if TYPE_CHECKING:
from . import Image