diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index e23adeb70..5dfa51697 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -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( diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 1404869ca..3e9aa73f8 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -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 diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index 04d6f5dcd..1ac3d034b 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -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. diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index ed58ca3a5..04b2a47ee 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -44,3 +44,4 @@ Access using negative indexes is also possible. :: .. autoclass:: PIL.PyAccess.PyAccess() :members: + :special-members: __getitem__, __setitem__ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f9152469e..c2eac31c7 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -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): diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index a471a7ffd..244d3d5f0 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -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] diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 3f3be706d..e27ca7e50 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -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) diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index fe12cb641..3be1ccace 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -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() diff --git a/src/PIL/_imaging.pyi b/src/PIL/_imaging.pyi index 2bfe421e6..8cccd3ac7 100644 --- a/src/PIL/_imaging.pyi +++ b/src/PIL/_imaging.pyi @@ -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: ...