From 651d49e3f87cc70b2e759ea28b557c40d1fe146c Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 29 Apr 2024 23:19:36 +0200 Subject: [PATCH] Added type hints for PixelAccess methods and others --- docs/reference/ImageDraw.rst | 18 +------------ docs/reference/PixelAccess.rst | 25 ++---------------- src/PIL/Image.py | 38 ++++++++++++++++++--------- src/PIL/ImageDraw.py | 13 +++++++-- src/PIL/ImageGrab.py | 11 ++++++-- src/PIL/PyAccess.py | 48 +++++++++++++++++++++------------- 6 files changed, 78 insertions(+), 75 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 4ccfacae75e..e7339ecbe03 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -679,23 +679,7 @@ Methods :param hints: An optional list of hints. :returns: A (drawing context, drawing resource factory) tuple. -.. py:method:: floodfill(image, xy, value, border=None, thresh=0) - - .. 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. +.. autofunction:: PIL.ImageDraw.floodfill .. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/ .. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index 04d6f5dcd58..e3ea5cbc46a 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -49,7 +49,7 @@ Access using negative indexes is also possible. :: 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 + 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) @@ -57,28 +57,7 @@ Access using negative indexes is also possible. :: .. 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 + 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 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a17edfa391c..89a159e3e0a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,7 +41,7 @@ from collections.abc import Callable, MutableMapping from enum import IntEnum 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. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -59,6 +59,9 @@ from ._typing import StrOrBytesPath, TypeGuard from ._util import DeferredError, is_path +if TYPE_CHECKING: + from . import PyAccess + ElementTree: ModuleType | None try: from defusedxml import ElementTree @@ -482,6 +485,14 @@ def _getscaleoffset(expr): # Implementation wrapper +class PixelAccess: + def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]: + raise NotImplementedError() + + def __setitem__(self, xy: tuple[int, int], value: float | tuple[int, ...]) -> None: + raise NotImplementedError() + + class Image: """ This class represents an image object. To create @@ -834,7 +845,7 @@ def frombytes(self, data: bytes, decoder_name: str = "raw", *args) -> None: msg = "cannot decode image data" raise ValueError(msg) - def load(self): + def load(self) -> PixelAccess | PyAccess.PyAccess | None: """ Allocates storage for the image and loads the pixel data. In normal cases, you don't need to call this method, since the @@ -876,6 +887,7 @@ def load(self): if self.pyaccess: return self.pyaccess return self.im.pixel_access(self.readonly) + return None def verify(self): """ @@ -1485,7 +1497,7 @@ def _reload_exif(self) -> None: self._exif._loaded = False self.getexif() - def get_child_images(self): + def get_child_images(self) -> list[ImageFile.ImageFile]: child_images = [] exif = self.getexif() ifds = [] @@ -1509,10 +1521,7 @@ def get_child_images(self): fp = self.fp thumbnail_offset = ifd.get(513) if thumbnail_offset is not None: - try: - thumbnail_offset += self._exif_offset - except AttributeError: - pass + thumbnail_offset += getattr(self, "_exif_offset", 0) self.fp.seek(thumbnail_offset) data = self.fp.read(ifd.get(514)) fp = io.BytesIO(data) @@ -1578,7 +1587,7 @@ def has_transparency_data(self) -> bool: 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, remove the key and instead apply the transparency to the palette. @@ -1589,7 +1598,7 @@ def apply_transparency(self): from . import ImagePalette - palette = self.getpalette("RGBA") + palette = cast(list[int], self.getpalette("RGBA")) transparency = self.info["transparency"] if isinstance(transparency, bytes): for i, alpha in enumerate(transparency): @@ -1601,7 +1610,9 @@ def apply_transparency(self): 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. @@ -1865,7 +1876,7 @@ def point(self, data): lut = [round(i) for i in lut] 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 does not have an alpha layer, it's converted to "LA" or "RGBA". @@ -1912,6 +1923,7 @@ def putalpha(self, alpha): alpha = alpha.convert("L") else: # constant alpha + alpha = cast(int, alpha) # see python/typing#1013 try: self.im.fillband(band, alpha) except (AttributeError, ValueError): @@ -1975,7 +1987,7 @@ def putpalette(self, data, rawmode="RGB") -> None: self.palette.mode = "RGB" 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 a single numerical value for single-band images, and a tuple for @@ -2015,7 +2027,7 @@ def putpixel(self, xy, value): value = value[:3] value = self.palette.getcolor(value, self) if self.mode == "PA": - value = (value, alpha) + value = (value, alpha) # type: ignore[assignment] return self.im.putpixel(xy, value) def remap_palette(self, dest_map, source_palette=None): diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index d3efe64865e..6c848d4a2f5 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -898,9 +898,17 @@ def getdraw(im=None, hints=None): 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 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 # amended by yo1995 @20180806 pixel = image.load() + assert pixel x, y = xy try: background = pixel[x, y] diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 3f3be706d96..5b7d590b7d0 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -22,11 +22,17 @@ import subprocess import sys import tempfile +from typing import Union, cast 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 sys.platform == "darwin": fh, filepath = tempfile.mkstemp(".png") @@ -63,6 +69,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N left, top, right, bottom = bbox im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) return im + xdisplay = cast(Union[str, None], xdisplay) try: if not Image.core.HAVE_XCB: msg = "Pillow was built without XCB support" @@ -94,7 +101,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N return im -def grabclipboard(): +def grabclipboard() -> Image.Image | list[str] | None: if sys.platform == "darwin": fh, filepath = tempfile.mkstemp(".png") os.close(fh) diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 2c831913d69..af95956662d 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -22,6 +22,7 @@ import logging import sys +from typing import TYPE_CHECKING from ._deprecate import deprecate @@ -50,7 +51,7 @@ class PyAccess: - def __init__(self, img, readonly=False): + def __init__(self, img: Image.Image, readonly: bool = False) -> None: deprecate("PyAccess", 11) vals = dict(img.im.unsafe_ptrs) self.readonly = readonly @@ -70,14 +71,15 @@ def __init__(self, img, readonly=False): # logger.debug("%s", vals) self._post_init() - def _post_init(self): + def _post_init(self) -> None: 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 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 :ref:`coordinate-system`. @@ -104,11 +106,11 @@ def __setitem__(self, xy, color): color = color[:3] color = self._palette.getcolor(color, self._img) if self._im.mode == "PA": - color = (color, alpha) + color = (color, alpha) # type: ignore[assignment] 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 value for single band images or a tuple for multiple band @@ -130,13 +132,19 @@ def __getitem__(self, xy): putpixel = __setitem__ getpixel = __getitem__ - def check_xy(self, xy): + 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, ...]) -> None: + raise NotImplementedError() + class _PyAccess32_2(PyAccess): """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): 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] return pixel.r, pixel.a @@ -161,7 +169,7 @@ class _PyAccess32_3(PyAccess): def _post_init(self, *args, **kwargs): 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] return pixel.r, pixel.g, pixel.b @@ -180,7 +188,7 @@ class _PyAccess32_4(PyAccess): def _post_init(self, *args, **kwargs): 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] return pixel.r, pixel.g, pixel.b, pixel.a @@ -199,7 +207,7 @@ class _PyAccess8(PyAccess): def _post_init(self, *args, **kwargs): self.pixels = self.image8 - def get_pixel(self, x, y): + def get_pixel(self, x: int, y: int) -> int: return self.pixels[y][x] def set_pixel(self, x, y, color): @@ -217,7 +225,7 @@ class _PyAccessI16_N(PyAccess): def _post_init(self, *args, **kwargs): 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] def set_pixel(self, x, y, color): @@ -235,7 +243,7 @@ class _PyAccessI16_L(PyAccess): def _post_init(self, *args, **kwargs): 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] return pixel.l + pixel.r * 256 @@ -256,7 +264,7 @@ class _PyAccessI16_B(PyAccess): def _post_init(self, *args, **kwargs): 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] return pixel.l * 256 + pixel.r @@ -277,7 +285,7 @@ class _PyAccessI32_N(PyAccess): def _post_init(self, *args, **kwargs): self.pixels = self.image32 - def get_pixel(self, x, y): + def get_pixel(self, x: int, y: int) -> int: return self.pixels[y][x] def set_pixel(self, x, y, color): @@ -296,7 +304,7 @@ def reverse(self, i): 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, y): + def get_pixel(self, x: int, y: int) -> int: return self.reverse(self.pixels[y][x]) def set_pixel(self, x, y, color): @@ -309,7 +317,7 @@ class _PyAccessF(PyAccess): def _post_init(self, *args, **kwargs): 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] def set_pixel(self, x, y, color): @@ -357,9 +365,13 @@ def set_pixel(self, x, y, color): 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) if not access_type: logger.debug("PyAccess Not Implemented: %s", img.mode) return None return access_type(img, readonly) + + +if TYPE_CHECKING: + from . import Image