Skip to content

Commit

Permalink
Merge pull request #8032 from nulano/type_hints
Browse files Browse the repository at this point in the history
Added type hints for PixelAccess related methods and others
  • Loading branch information
radarhere committed Jun 25, 2024
2 parents a941f09 + ab18395 commit d2b5e11
Show file tree
Hide file tree
Showing 9 changed files with 78 additions and 71 deletions.
3 changes: 3 additions & 0 deletions Tests/test_imagegrab.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def test_grabclipboard_file(self) -> None:
p.communicate()

im = ImageGrab.grabclipboard()
assert isinstance(im, list)
assert len(im) == 1
assert os.path.samefile(im[0], "Tests/images/hopper.gif")

Expand All @@ -105,6 +106,7 @@ def test_grabclipboard_png(self) -> None:
p.communicate()

im = ImageGrab.grabclipboard()
assert isinstance(im, Image.Image)
assert_image_equal_tofile(im, "Tests/images/hopper.png")

@pytest.mark.skipif(
Expand All @@ -120,6 +122,7 @@ def test_grabclipboard_wl_clipboard(self, ext: str) -> None:
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(
Expand Down
18 changes: 1 addition & 17 deletions docs/reference/ImageDraw.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
37 changes: 9 additions & 28 deletions docs/reference/PixelAccess.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions docs/reference/PyAccess.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ Access using negative indexes is also possible. ::

.. autoclass:: PIL.PyAccess.PyAccess()
:members:
:special-members: __getitem__, __setitem__
47 changes: 33 additions & 14 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,16 @@
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.
Expand Down Expand Up @@ -218,7 +227,7 @@ class Quantize(IntEnum):
# Registries

if TYPE_CHECKING:
from . import ImageFile
from . import ImageFile, PyAccess
ID: list[str] = []
OPEN: dict[
str,
Expand Down Expand Up @@ -871,7 +880,7 @@ def frombytes(
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
Expand All @@ -884,7 +893,7 @@ def load(self):
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
Expand Down Expand Up @@ -913,6 +922,7 @@ def load(self):
if self.pyaccess:
return self.pyaccess
return self.im.pixel_access(self.readonly)
return None

def verify(self) -> None:
"""
Expand Down Expand Up @@ -1102,7 +1112,10 @@ def convert_transparency(
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.
Expand Down Expand Up @@ -1152,7 +1165,10 @@ def convert_transparency(
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":
Expand Down Expand Up @@ -1657,7 +1673,9 @@ def apply_transparency(self) -> None:

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.
Expand Down Expand Up @@ -1941,15 +1959,14 @@ def point(self, data):
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()
Expand Down Expand Up @@ -1988,6 +2005,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):
Expand Down Expand Up @@ -2056,7 +2074,9 @@ def putpalette(self, data, rawmode="RGB") -> None:
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
Expand Down Expand Up @@ -2094,9 +2114,8 @@ def putpixel(self, xy, value):
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):
Expand Down
5 changes: 4 additions & 1 deletion src/PIL/ImageDraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down
16 changes: 12 additions & 4 deletions src/PIL/ImageGrab.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@
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")
Expand Down Expand Up @@ -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")
):
Expand All @@ -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)
Expand Down
17 changes: 11 additions & 6 deletions src/PIL/PyAccess.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ def __init__(self, img: Image.Image, readonly: bool = False) -> None:
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
Expand Down Expand Up @@ -107,13 +111,12 @@ def __setitem__(self, xy, color):
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
Expand Down Expand Up @@ -145,7 +148,9 @@ def check_xy(self, xy: tuple[int, int]) -> tuple[int, int]:
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()


Expand Down
5 changes: 4 additions & 1 deletion src/PIL/_imaging.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
Expand Down

0 comments on commit d2b5e11

Please sign in to comment.