diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 2184ef8ea..f81e95695 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,7 +41,7 @@ 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, cast +from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, cast, overload # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -481,6 +481,8 @@ def _getscaleoffset(expr): # -------------------------------------------------------------------- # Implementation wrapper +class _GetDataTransform(Protocol): + def getdata(self) -> tuple[Transform, Sequence[int]]: ... class Image: """ @@ -1687,7 +1689,7 @@ class Image: return self.im.entropy(extrema) return self.im.entropy() - def paste(self, im, box=None, mask=None) -> None: + def paste(self, im: Image | str | int | tuple[int, ...], box: tuple[int, int, int, int] | tuple[int, int] | None = None, mask: Image | None = None) -> None: """ Pastes another image into this image. The box argument is either a 2-tuple giving the upper left corner, a 4-tuple defining the @@ -2122,7 +2124,7 @@ class Image: min(self.size[1], math.ceil(box[3] + support_y)), ) - def resize(self, size, resample=None, box=None, reducing_gap=None) -> Image: + def resize(self, size: tuple[int, int], resample: Resampling | None = None, box: tuple[float, float, float, float] | None = None, reducing_gap: float | None = None) -> Image: """ Returns a resized copy of this image. @@ -2228,7 +2230,7 @@ class Image: return self._new(self.im.resize(size, resample, box)) - def reduce(self, factor, box=None): + def reduce(self, factor: int | tuple[int, int], box: tuple[int, int, int, int] | None = None) -> Image: """ Returns a copy of the image reduced ``factor`` times. If the size of the image is not dividable by ``factor``, @@ -2263,13 +2265,13 @@ class Image: def rotate( self, - angle, - resample=Resampling.NEAREST, - expand=0, - center=None, - translate=None, - fillcolor=None, - ): + angle: float, + resample: Resampling = Resampling.NEAREST, + expand: bool = False, + center: tuple[int, int] | None = None, + translate: tuple[int, int] | None = None, + fillcolor: float | tuple[float, ...] | str | None = None, + ) -> Image: """ Returns a rotated copy of this image. This method returns a copy of this image, rotated the given number of degrees counter @@ -2576,7 +2578,7 @@ class Image: """ return 0 - def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0): + def thumbnail(self, size: tuple[int, int], resample: Resampling = Resampling.BICUBIC, reducing_gap: float = 2.0) -> None: """ Make this image into a thumbnail. This method modifies the image to contain a thumbnail version of itself, no larger than @@ -2664,14 +2666,34 @@ class Image: # FIXME: the different transform methods need further explanation # instead of bloating the method docs, add a separate chapter. + @overload def transform( self, - size, - method, - data=None, - resample=Resampling.NEAREST, - fill=1, - fillcolor=None, + size: tuple[int, int], + method: Transform | ImageTransformHandler, + data: Sequence[int], + resample: Resampling = Resampling.NEAREST, + fill: int = 1, + fillcolor: float | tuple[float, ...] | str | None = None, + ) -> Image: ... + @overload + def transform( + self, + size: tuple[int, int], + method: _GetDataTransform, + data: None = None, + resample: Resampling = Resampling.NEAREST, + fill: int = 1, + fillcolor: float | tuple[float, ...] | str | None = None, + ) -> Image: ... + def transform( + self, + size: tuple[int, int], + method: Transform | ImageTransformHandler | _GetDataTransform, + data: Sequence[int] | None = None, + resample: Resampling = Resampling.NEAREST, + fill: int = 1, + fillcolor: float | tuple[float, ...] | str | None = None, ) -> Image: """ Transforms this image. This method creates a new image with the diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index d3efe6486..579489fde 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -34,10 +34,11 @@ from __future__ import annotations import math import numbers import struct -from typing import Sequence, cast +from typing import AnyStr, Sequence, cast from . import Image, ImageColor from ._typing import Coords +from .ImageFont import FreeTypeFont, ImageFont """ A simple 2D drawing interface for PIL images. @@ -92,7 +93,7 @@ class ImageDraw: self.fontmode = "L" # aliasing is okay for other modes self.fill = False - def getfont(self): + def getfont(self) -> FreeTypeFont | ImageFont: """ Get the current default font. @@ -450,12 +451,12 @@ class ImageDraw: right[3] -= r + 1 self.draw.draw_rectangle(right, ink, 1) - def _multiline_check(self, text) -> bool: + def _multiline_check(self, text: str | bytes) -> bool: split_character = "\n" if isinstance(text, str) else b"\n" return split_character in text - def _multiline_split(self, text) -> list[str | bytes]: + def _multiline_split(self, text: AnyStr) -> list[AnyStr]: split_character = "\n" if isinstance(text, str) else b"\n" return text.split(split_character) @@ -469,7 +470,7 @@ class ImageDraw: def text( self, - xy, + xy: tuple[int, int], text, fill=None, font=None, @@ -591,7 +592,7 @@ class ImageDraw: def multiline_text( self, - xy, + xy: tuple[int, int], text, fill=None, font=None, @@ -678,15 +679,15 @@ class ImageDraw: def textlength( self, - text, - font=None, + text: str, + font: FreeTypeFont | ImageFont | None = None, direction=None, features=None, language=None, embedded_color=False, *, font_size=None, - ): + ) -> float: """Get the length of a given string, in pixels with 1/64 precision.""" if self._multiline_check(text): msg = "can't measure length of multiline text" diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 256c581df..536ee5fe6 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -33,12 +33,15 @@ import sys import warnings from enum import IntEnum from io import BytesIO -from typing import BinaryIO +from typing import TYPE_CHECKING, BinaryIO from . import Image from ._typing import StrOrBytesPath from ._util import is_directory, is_path +if TYPE_CHECKING: + from _imagingft import Font + class Layout(IntEnum): BASIC = 0 @@ -56,7 +59,7 @@ except ImportError as ex: core = DeferredError.new(ex) -def _string_length_check(text): +def _string_length_check(text: str | bytes) -> None: if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: msg = "too many characters in string" raise ValueError(msg) @@ -81,7 +84,9 @@ def _string_length_check(text): class ImageFont: """PIL font wrapper""" - def _load_pilfont(self, filename): + font: Font + + def _load_pilfont(self, filename: str) -> None: with open(filename, "rb") as fp: image = None for ext in (".png", ".gif", ".pbm"): @@ -153,7 +158,7 @@ class ImageFont: Image._decompression_bomb_check(self.font.getsize(text)) return self.font.getmask(text, mode) - def getbbox(self, text, *args, **kwargs): + def getbbox(self, text: str, *args: object, **kwargs: object) -> tuple[int, int, int, int]: """ Returns bounding box (in pixels) of given text. @@ -171,7 +176,7 @@ class ImageFont: width, height = self.font.getsize(text) return 0, 0, width, height - def getlength(self, text, *args, **kwargs): + def getlength(self, text: str, *args: object, **kwargs: object) -> int: """ Returns length (in pixels) of given text. This is the amount by which following text should be offset. @@ -254,7 +259,7 @@ class FreeTypeFont: path, size, index, encoding, layout_engine = state self.__init__(path, size, index, encoding, layout_engine) - def getname(self): + def getname(self) -> tuple[str, str]: """ :return: A tuple of the font family (e.g. Helvetica) and the font style (e.g. Bold) @@ -269,7 +274,7 @@ class FreeTypeFont: """ return self.font.ascent, self.font.descent - def getlength(self, text, mode="", direction=None, features=None, language=None): + def getlength(self, text: str, mode="", direction=None, features=None, language=None) -> float: """ Returns length (in pixels with 1/64 precision) of given text when rendered in font with provided direction, features, and language. @@ -343,14 +348,14 @@ class FreeTypeFont: def getbbox( self, - text, + text: str, mode="", direction=None, features=None, language=None, stroke_width=0, anchor=None, - ): + ) -> tuple[int, int, int, int]: """ Returns bounding box (in pixels) of given text relative to given anchor when rendered in font with provided direction, features, and language. @@ -725,7 +730,7 @@ class TransposedFont: return self.font.getlength(text, *args, **kwargs) -def load(filename): +def load(filename: str) -> ImageFont: """ Load a font file. This function loads a font object from the given bitmap font file, and returns the corresponding font object. @@ -739,7 +744,7 @@ def load(filename): return f -def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): +def truetype(font: StrOrBytesPath | BinaryIO | None = None, size: float = 10, index: int = 0, encoding: str = "", layout_engine: Layout | None = None) -> FreeTypeFont: """ Load a TrueType or OpenType font from a file or file-like object, and create a font object. @@ -800,7 +805,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): :exception ValueError: If the font size is not greater than zero. """ - def freetype(font): + def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont: return FreeTypeFont(font, size, index, encoding, layout_engine) try: @@ -850,7 +855,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): raise -def load_path(filename): +def load_path(filename: str | bytes) -> ImageFont: """ Load font file. Same as :py:func:`~PIL.ImageFont.load`, but searches for a bitmap font along the Python path. diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index e27843e53..2c2ea9a54 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -1,3 +1,38 @@ -from typing import Any +from typing import Any, TypedDict + +class _Axis(TypedDict): + minimum: int | None + default: int | None + maximum: int | None + name: str | None + + +class Font: + @property + def family(self) -> str | None: ... + @property + def style(self) -> str | None: ... + @property + def ascent(self) -> int: ... + @property + def descent(self) -> int: ... + @property + def height(self) -> int: ... + @property + def x_ppem(self) -> int: ... + @property + def y_ppem(self) -> int: ... + @property + def glyphs(self) -> int: ... + + def render(self, string: str, fill, mode = ..., dir = ..., features = ..., lang = ..., stroke_width = ..., anchor = ..., foreground_ink_long = ..., x_start = ..., y_start = ..., /) -> tuple[Any, tuple[int, int]]: ... + def getsize(self, string: str, mode = ..., dir = ..., features = ..., lang = ..., anchor = ..., /) -> tuple[tuple[int, int], tuple[int, int]]: ... + def getlength(self, string: str, mode = ..., dir = ..., features = ..., lang = ..., /) -> int: ... + def getvarnames(self) -> list[str]: ... + def getvaraxes(self) -> list[_Axis]: ... + def setvarname(self, instance_index: int, /) -> None: ... + def setvaraxes(self, axes: list[float], /) -> None: ... + +def getfont(filename: str | bytes | bytearray, size, index = ..., encoding = ..., font_bytes = ..., layout_engine = ...) -> Font: ... def __getattr__(name: str) -> Any: ...