Merge pull request #8032 from nulano/type_hints

Added type hints for PixelAccess related methods and others
This commit is contained in:
Andrew Murray 2024-06-25 19:15:31 +10:00 committed by GitHub
commit d2b5e11d2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 78 additions and 71 deletions

View File

@ -89,6 +89,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
p.communicate() p.communicate()
im = ImageGrab.grabclipboard() im = ImageGrab.grabclipboard()
assert isinstance(im, list)
assert len(im) == 1 assert len(im) == 1
assert os.path.samefile(im[0], "Tests/images/hopper.gif") assert os.path.samefile(im[0], "Tests/images/hopper.gif")
@ -105,6 +106,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
p.communicate() p.communicate()
im = ImageGrab.grabclipboard() im = ImageGrab.grabclipboard()
assert isinstance(im, Image.Image)
assert_image_equal_tofile(im, "Tests/images/hopper.png") assert_image_equal_tofile(im, "Tests/images/hopper.png")
@pytest.mark.skipif( @pytest.mark.skipif(
@ -120,6 +122,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
with open(image_path, "rb") as fp: with open(image_path, "rb") as fp:
subprocess.call(["wl-copy"], stdin=fp) subprocess.call(["wl-copy"], stdin=fp)
im = ImageGrab.grabclipboard() im = ImageGrab.grabclipboard()
assert isinstance(im, Image.Image)
assert_image_equal_tofile(im, image_path) assert_image_equal_tofile(im, image_path)
@pytest.mark.skipif( @pytest.mark.skipif(

View File

@ -691,23 +691,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,23 @@ Access using negative indexes is also possible. ::
----------------------------- -----------------------------
.. class:: PixelAccess .. class:: PixelAccess
:canonical: PIL.Image.core.PixelAccess
.. method:: __setitem__(self, xy, color): .. method:: __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]
Modifies the pixel at x,y. The color is given as a single Returns the pixel at x,y. The pixel is returned as a single
numerical value for single band images, and a tuple for value for single band images or 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)
.. 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). :param xy: The pixel coordinate, given as (x, y).
:returns: a pixel value for single band images, a tuple of :returns: a pixel value for single band images, a tuple of
pixel values for multiband images. pixel values for multiband images.
.. method:: putpixel(self, xy, color): .. method:: __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. In addition to this, RGB and RGBA tuples multi-band images.
are accepted for P and PA images.
:param xy: The pixel coordinate, given as (x, y). :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) :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,16 @@ 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, Sequence, Tuple, cast from typing import (
IO,
TYPE_CHECKING,
Any,
Literal,
Protocol,
Sequence,
Tuple,
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.
@ -218,7 +227,7 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# Registries # Registries
if TYPE_CHECKING: if TYPE_CHECKING:
from . import ImageFile from . import ImageFile, PyAccess
ID: list[str] = [] ID: list[str] = []
OPEN: dict[ OPEN: dict[
str, str,
@ -871,7 +880,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) -> core.PixelAccess | PyAccess.PyAccess | 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
@ -884,7 +893,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
@ -913,6 +922,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) -> None: def verify(self) -> None:
""" """
@ -1102,7 +1112,10 @@ class Image:
del new_im.info["transparency"] del new_im.info["transparency"]
if trns is not None: if trns is not None:
try: try:
new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) new_im.info["transparency"] = new_im.palette.getcolor(
cast(Tuple[int, int, int], trns), # trns was converted to RGB
new_im,
)
except Exception: except Exception:
# if we can't make a transparent color, don't leave the old # if we can't make a transparent color, don't leave the old
# transparency hanging around to mess us up. # transparency hanging around to mess us up.
@ -1152,7 +1165,10 @@ class Image:
if trns is not None: if trns is not None:
if new_im.mode == "P": if new_im.mode == "P":
try: try:
new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) new_im.info["transparency"] = new_im.palette.getcolor(
cast(Tuple[int, int, int], trns), # trns was converted to RGB
new_im,
)
except ValueError as e: except ValueError as e:
del new_im.info["transparency"] del new_im.info["transparency"]
if str(e) != "cannot allocate more than 256 colors": if str(e) != "cannot allocate more than 256 colors":
@ -1657,7 +1673,9 @@ class Image:
del self.info["transparency"] del self.info["transparency"]
def getpixel(self, xy): def getpixel(
self, xy: tuple[int, int] | list[int]
) -> float | tuple[int, ...] | None:
""" """
Returns the pixel value at a given position. Returns the pixel value at a given position.
@ -1941,15 +1959,14 @@ class Image:
flatLut = [round(i) for i in flatLut] flatLut = [round(i) for i in flatLut]
return self._new(self.im.point(flatLut, mode)) return self._new(self.im.point(flatLut, 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".
The new layer must be either "L" or "1". The new layer must be either "L" or "1".
:param alpha: The new alpha layer. This can either be an "L" or "1" :param alpha: The new alpha layer. This can either be an "L" or "1"
image having the same size as this image, or an integer or image having the same size as this image, or an integer.
other color value.
""" """
self._ensure_mutable() self._ensure_mutable()
@ -1988,6 +2005,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):
@ -2056,7 +2074,9 @@ class Image:
self.palette.mode = "RGBA" if "A" in rawmode else "RGB" self.palette.mode = "RGBA" if "A" in rawmode else "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, ...] | list[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
@ -2094,9 +2114,8 @@ class Image:
if self.mode == "PA": if self.mode == "PA":
alpha = value[3] if len(value) == 4 else 255 alpha = value[3] if len(value) == 4 else 255
value = value[:3] value = value[:3]
value = self.palette.getcolor(value, self) palette_index = self.palette.getcolor(value, self)
if self.mode == "PA": value = (palette_index, alpha) if self.mode == "PA" else palette_index
value = (value, alpha)
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

@ -1007,7 +1007,9 @@ def floodfill(
thresh: float = 0, thresh: float = 0,
) -> None: ) -> 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
@ -1025,6 +1027,7 @@ def floodfill(
# 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

@ -26,7 +26,13 @@ import tempfile
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:
im: 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")
@ -63,14 +69,16 @@ 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
# Cast to Optional[str] needed for Windows and macOS.
display_name: str | None = xdisplay
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"
raise OSError(msg) raise OSError(msg)
size, data = Image.core.grabscreen_x11(xdisplay) size, data = Image.core.grabscreen_x11(display_name)
except OSError: except OSError:
if ( if (
xdisplay is None display_name is None
and sys.platform not in ("darwin", "win32") and sys.platform not in ("darwin", "win32")
and shutil.which("gnome-screenshot") and shutil.which("gnome-screenshot")
): ):
@ -94,7 +102,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

@ -77,7 +77,11 @@ class PyAccess:
def _post_init(self) -> None: def _post_init(self) -> None:
pass pass
def __setitem__(self, xy, color): 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 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
@ -107,13 +111,12 @@ class PyAccess:
if self._im.mode == "PA": if self._im.mode == "PA":
alpha = color[3] if len(color) == 4 else 255 alpha = color[3] if len(color) == 4 else 255
color = color[:3] color = color[:3]
color = self._palette.getcolor(color, self._img) palette_index = self._palette.getcolor(color, self._img)
if self._im.mode == "PA": color = (palette_index, alpha) if self._im.mode == "PA" else palette_index
color = (color, alpha)
return self.set_pixel(x, y, color) return self.set_pixel(x, y, color)
def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]: 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 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
@ -145,7 +148,9 @@ class PyAccess:
def get_pixel(self, x: int, y: int) -> float | tuple[int, ...]: def get_pixel(self, x: int, y: int) -> float | tuple[int, ...]:
raise NotImplementedError() raise NotImplementedError()
def set_pixel(self, x: int, y: int, color: float | tuple[int, ...]) -> None: def set_pixel(
self, x: int, y: int, color: float | tuple[int, ...] | list[int]
) -> None:
raise NotImplementedError() raise NotImplementedError()

View File

@ -10,7 +10,10 @@ class ImagingDraw:
def __getattr__(self, name: str) -> Any: ... def __getattr__(self, name: str) -> Any: ...
class PixelAccess: class PixelAccess:
def __getattr__(self, name: str) -> Any: ... def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]: ...
def __setitem__(
self, xy: tuple[int, int], color: float | tuple[int, ...]
) -> None: ...
class ImagingDecoder: class ImagingDecoder:
def __getattr__(self, name: str) -> Any: ... def __getattr__(self, name: str) -> Any: ...