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

View File

@ -691,23 +691,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

View File

@ -44,42 +44,23 @@ Access using negative indexes is also possible. ::
-----------------------------
.. 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
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
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.
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
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.
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:: 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.
:param color: The pixel value according to its mode,
e.g. tuple (r, g, b) for RGB mode.

View File

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

View File

@ -41,7 +41,16 @@ import warnings
from collections.abc import Callable, MutableMapping
from enum import IntEnum
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.
# PILLOW_VERSION was removed in Pillow 9.0.0.
@ -218,7 +227,7 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# Registries
if TYPE_CHECKING:
from . import ImageFile
from . import ImageFile, PyAccess
ID: list[str] = []
OPEN: dict[
str,
@ -871,7 +880,7 @@ class Image:
msg = "cannot decode image data"
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
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.
: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:
# realize palette
@ -913,6 +922,7 @@ class Image:
if self.pyaccess:
return self.pyaccess
return self.im.pixel_access(self.readonly)
return None
def verify(self) -> None:
"""
@ -1102,7 +1112,10 @@ class Image:
del new_im.info["transparency"]
if trns is not None:
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:
# if we can't make a transparent color, don't leave the old
# transparency hanging around to mess us up.
@ -1152,7 +1165,10 @@ class Image:
if trns is not None:
if new_im.mode == "P":
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:
del new_im.info["transparency"]
if str(e) != "cannot allocate more than 256 colors":
@ -1657,7 +1673,9 @@ class Image:
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.
@ -1941,15 +1959,14 @@ class Image:
flatLut = [round(i) for i in flatLut]
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
does not have an alpha layer, it's converted to "LA" or "RGBA".
The new layer must be either "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
other color value.
image having the same size as this image, or an integer.
"""
self._ensure_mutable()
@ -1988,6 +2005,7 @@ class Image:
alpha = alpha.convert("L")
else:
# constant alpha
alpha = cast(int, alpha) # see python/typing#1013
try:
self.im.fillband(band, alpha)
except (AttributeError, ValueError):
@ -2056,7 +2074,9 @@ class Image:
self.palette.mode = "RGBA" if "A" in rawmode else "RGB"
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
a single numerical value for single-band images, and a tuple for
@ -2094,9 +2114,8 @@ class Image:
if self.mode == "PA":
alpha = value[3] if len(value) == 4 else 255
value = value[:3]
value = self.palette.getcolor(value, self)
if self.mode == "PA":
value = (value, alpha)
palette_index = self.palette.getcolor(value, self)
value = (palette_index, alpha) if self.mode == "PA" else palette_index
return self.im.putpixel(xy, value)
def remap_palette(self, dest_map, source_palette=None):

View File

@ -1007,7 +1007,9 @@ def floodfill(
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
@ -1025,6 +1027,7 @@ def floodfill(
# based on an implementation by Eric S. Raymond
# amended by yo1995 @20180806
pixel = image.load()
assert pixel is not None
x, y = xy
try:
background = pixel[x, y]

View File

@ -26,7 +26,13 @@ import tempfile
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 sys.platform == "darwin":
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
im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
return im
# Cast to Optional[str] needed for Windows and macOS.
display_name: str | None = xdisplay
try:
if not Image.core.HAVE_XCB:
msg = "Pillow was built without XCB support"
raise OSError(msg)
size, data = Image.core.grabscreen_x11(xdisplay)
size, data = Image.core.grabscreen_x11(display_name)
except OSError:
if (
xdisplay is None
display_name is None
and sys.platform not in ("darwin", "win32")
and shutil.which("gnome-screenshot")
):
@ -94,7 +102,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)

View File

@ -77,7 +77,11 @@ class PyAccess:
def _post_init(self) -> None:
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
numerical value for single band images, and a tuple for
@ -107,13 +111,12 @@ class PyAccess:
if self._im.mode == "PA":
alpha = color[3] if len(color) == 4 else 255
color = color[:3]
color = self._palette.getcolor(color, self._img)
if self._im.mode == "PA":
color = (color, alpha)
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]) -> 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
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, ...]:
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()

View File

@ -10,7 +10,10 @@ class ImagingDraw:
def __getattr__(self, name: str) -> Any: ...
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:
def __getattr__(self, name: str) -> Any: ...