From c92f59d758e0a1e308b148f31dd3b7f3f68f94b4 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 7 May 2024 14:30:34 +0200 Subject: [PATCH 01/17] Add various type annotations --- src/PIL/Image.py | 58 +++++++++++++++++++++++++++++------------- src/PIL/ImageDraw.py | 19 +++++++------- src/PIL/ImageFont.py | 31 ++++++++++++---------- src/PIL/_imagingft.pyi | 37 ++++++++++++++++++++++++++- 4 files changed, 104 insertions(+), 41 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 2184ef8ea9e..f81e95695a8 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,7 +41,7 @@ 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 @@ def entropy(self, mask=None, extrema=None): 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 @@ def _get_safe_box(self, size, resample, box): 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 @@ def resize(self, size, resample=None, box=None, reducing_gap=None) -> 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 @@ def reduce(self, factor, box=None): 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 @@ def tell(self) -> int: """ 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 @@ def round_aspect(number, key): # FIXME: the different transform methods need further explanation # instead of bloating the method docs, add a separate chapter. + @overload + def transform( + self, + 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, - method, - data=None, - resample=Resampling.NEAREST, - fill=1, - fillcolor=None, + 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 d3efe64865e..579489fdeed 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -34,10 +34,11 @@ 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 @@ def __init__(self, im: Image.Image, mode: str | None = None) -> None: 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 @@ def draw_corners(pieslice) -> None: 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 @@ def _multiline_spacing(self, font, spacing, stroke_width): def text( self, - xy, + xy: tuple[int, int], text, fill=None, font=None, @@ -591,7 +592,7 @@ def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: def multiline_text( self, - xy, + xy: tuple[int, int], text, fill=None, font=None, @@ -678,15 +679,15 @@ def multiline_text( 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 256c581df0c..536ee5fe607 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -33,12 +33,15 @@ 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 @@ class Layout(IntEnum): 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 @@ def getmask(self, text, mode="", *args, **kwargs): 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 @@ def getbbox(self, text, *args, **kwargs): 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 @@ def __setstate__(self, state): 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 @@ def getmetrics(self): """ 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 @@ def getlength(self, text, mode="", direction=None, features=None, language=None) 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 @@ def getlength(self, text, *args, **kwargs): 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 freetype(font): 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 e27843e5338..2c2ea9a54a7 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: ... From 1aa3886ed76b3f8fc60d604a34dffe573b491c20 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 12:33:59 +0000 Subject: [PATCH 02/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/Image.py | 30 ++++++++++++++++++++++++++---- src/PIL/ImageFont.py | 16 +++++++++++++--- src/PIL/_imagingft.pyi | 36 +++++++++++++++++++++++++++++------- 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f81e95695a8..9f55ea9242c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -481,9 +481,11 @@ def _getscaleoffset(expr): # -------------------------------------------------------------------- # Implementation wrapper + class _GetDataTransform(Protocol): def getdata(self) -> tuple[Transform, Sequence[int]]: ... + class Image: """ This class represents an image object. To create @@ -1689,7 +1691,12 @@ def entropy(self, mask=None, extrema=None): return self.im.entropy(extrema) return self.im.entropy() - 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: + 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 @@ -2124,7 +2131,13 @@ def _get_safe_box(self, size, resample, box): min(self.size[1], math.ceil(box[3] + support_y)), ) - 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: + 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. @@ -2230,7 +2243,11 @@ def resize(self, size: tuple[int, int], resample: Resampling | None = None, box: return self._new(self.im.resize(size, resample, box)) - def reduce(self, factor: int | tuple[int, int], box: tuple[int, int, int, int] | None = None) -> Image: + 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``, @@ -2578,7 +2595,12 @@ def tell(self) -> int: """ return 0 - def thumbnail(self, size: tuple[int, int], resample: Resampling = Resampling.BICUBIC, reducing_gap: float = 2.0) -> None: + 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 diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 536ee5fe607..fb7e1d8b614 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -158,7 +158,9 @@ def getmask(self, text, mode="", *args, **kwargs): Image._decompression_bomb_check(self.font.getsize(text)) return self.font.getmask(text, mode) - def getbbox(self, text: str, *args: object, **kwargs: object) -> tuple[int, int, int, int]: + def getbbox( + self, text: str, *args: object, **kwargs: object + ) -> tuple[int, int, int, int]: """ Returns bounding box (in pixels) of given text. @@ -274,7 +276,9 @@ def getmetrics(self): """ return self.font.ascent, self.font.descent - def getlength(self, text: str, mode="", direction=None, features=None, language=None) -> float: + 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. @@ -744,7 +748,13 @@ def load(filename: str) -> ImageFont: return f -def truetype(font: StrOrBytesPath | BinaryIO | None = None, size: float = 10, index: int = 0, encoding: str = "", layout_engine: Layout | None = None) -> FreeTypeFont: +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. diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 2c2ea9a54a7..987e7fd6f49 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -6,7 +6,6 @@ class _Axis(TypedDict): maximum: int | None name: str | None - class Font: @property def family(self) -> str | None: ... @@ -24,15 +23,38 @@ class Font: 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 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 getfont( + filename: str | bytes | bytearray, + size, + index=..., + encoding=..., + font_bytes=..., + layout_engine=..., +) -> Font: ... def __getattr__(name: str) -> Any: ... From d44e9fccb16c63005fbffce06c16a0afc2b26667 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 7 May 2024 14:53:26 +0200 Subject: [PATCH 03/17] Various fixes --- src/PIL/Image.py | 46 ++++++++++++++++++++++++++++---------------- src/PIL/ImageFont.py | 2 +- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 9f55ea9242c..f6f070feef3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -483,7 +483,7 @@ def _getscaleoffset(expr): class _GetDataTransform(Protocol): - def getdata(self) -> tuple[Transform, Sequence[int]]: ... + def getdata(self) -> tuple[Transform, Sequence[float]]: ... class Image: @@ -2134,7 +2134,7 @@ def _get_safe_box(self, size, resample, box): def resize( self, size: tuple[int, int], - resample: Resampling | None = None, + resample: int | None = None, box: tuple[float, float, float, float] | None = None, reducing_gap: float | None = None, ) -> Image: @@ -2202,13 +2202,13 @@ def resize( msg = "reducing_gap must be 1.0 or greater" raise ValueError(msg) - size = tuple(size) + size = cast(tuple[int, int], tuple(size)) self.load() if box is None: box = (0, 0) + self.size else: - box = tuple(box) + box = cast(tuple[float, float, float, float], tuple(box)) if self.size == size and box == (0, 0) + self.size: return self.copy() @@ -2266,7 +2266,7 @@ def reduce( if box is None: box = (0, 0) + self.size else: - box = tuple(box) + box = cast(tuple[int, int, int, int], tuple(box)) if factor == (1, 1) and box == (0, 0) + self.size: return self.copy() @@ -2283,7 +2283,7 @@ def reduce( def rotate( self, angle: float, - resample: Resampling = Resampling.NEAREST, + resample: int = Resampling.NEAREST, expand: bool = False, center: tuple[int, int] | None = None, translate: tuple[int, int] | None = None, @@ -2598,7 +2598,7 @@ def tell(self) -> int: def thumbnail( self, size: tuple[int, int], - resample: Resampling = Resampling.BICUBIC, + resample: int = Resampling.BICUBIC, reducing_gap: float = 2.0, ) -> None: """ @@ -2661,20 +2661,22 @@ def round_aspect(number, key): box = None if reducing_gap is not None: - size = preserve_aspect_ratio() - if size is None: + preserved_size = preserve_aspect_ratio() + if preserved_size is None: return + size = preserved_size - res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap)) + res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap)) # type: ignore[arg-type] if res is not None: box = res[1] if box is None: self.load() # load() may have changed the size of the image - size = preserve_aspect_ratio() - if size is None: + preserved_size = preserve_aspect_ratio() + if preserved_size is None: return + size = preserved_size if self.size != size: im = self.resize(size, resample, box=box, reducing_gap=reducing_gap) @@ -2693,8 +2695,8 @@ def transform( self, size: tuple[int, int], method: Transform | ImageTransformHandler, - data: Sequence[int], - resample: Resampling = Resampling.NEAREST, + data: Sequence[float], + resample: int = Resampling.NEAREST, fill: int = 1, fillcolor: float | tuple[float, ...] | str | None = None, ) -> Image: ... @@ -2704,7 +2706,17 @@ def transform( size: tuple[int, int], method: _GetDataTransform, data: None = None, - resample: Resampling = Resampling.NEAREST, + resample: int = Resampling.NEAREST, + fill: int = 1, + fillcolor: float | tuple[float, ...] | str | None = None, + ) -> Image: ... + @overload + def transform( + self, + size: tuple[int, int], + method: Transform | ImageTransformHandler | _GetDataTransform, + data: Sequence[float] | None = None, + resample: int = Resampling.NEAREST, fill: int = 1, fillcolor: float | tuple[float, ...] | str | None = None, ) -> Image: ... @@ -2712,8 +2724,8 @@ def transform( self, size: tuple[int, int], method: Transform | ImageTransformHandler | _GetDataTransform, - data: Sequence[int] | None = None, - resample: Resampling = Resampling.NEAREST, + data: Sequence[float] | None = None, + resample: int = Resampling.NEAREST, fill: int = 1, fillcolor: float | tuple[float, ...] | str | None = None, ) -> Image: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index fb7e1d8b614..a1b722765a3 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -40,7 +40,7 @@ from ._util import is_directory, is_path if TYPE_CHECKING: - from _imagingft import Font + from ._imagingft import Font class Layout(IntEnum): From d63caf266d2561b1646ed378761332e0855dd73d Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 7 May 2024 15:59:20 +0200 Subject: [PATCH 04/17] Various fixes --- src/PIL/Image.py | 44 +++++++-------------------------------- src/PIL/ImageDraw.py | 24 ++++++++++----------- src/PIL/ImageFont.py | 13 +++++++++--- src/PIL/ImageTransform.py | 4 ++-- src/PIL/_imaging.pyi | 15 +++++++++++++ 5 files changed, 47 insertions(+), 53 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f6f070feef3..9b0c24ec09a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,7 +41,7 @@ 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, overload +from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, cast # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -483,7 +483,9 @@ def _getscaleoffset(expr): class _GetDataTransform(Protocol): - def getdata(self) -> tuple[Transform, Sequence[float]]: ... + def getdata( + self, + ) -> tuple[Transform, Sequence[Any]]: ... class Image: @@ -2690,41 +2692,11 @@ def round_aspect(number, key): # FIXME: the different transform methods need further explanation # instead of bloating the method docs, add a separate chapter. - @overload - def transform( - self, - size: tuple[int, int], - method: Transform | ImageTransformHandler, - data: Sequence[float], - resample: int = 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: int = Resampling.NEAREST, - fill: int = 1, - fillcolor: float | tuple[float, ...] | str | None = None, - ) -> Image: ... - @overload - def transform( - self, - size: tuple[int, int], - method: Transform | ImageTransformHandler | _GetDataTransform, - data: Sequence[float] | None = None, - resample: int = 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[float] | None = None, + data: Sequence[Any] | None = None, resample: int = Resampling.NEAREST, fill: int = 1, fillcolor: float | tuple[float, ...] | str | None = None, @@ -2803,7 +2775,7 @@ def getdata(self): im.info = self.info.copy() if method == Transform.MESH: # list of quads - for box, quad in data: + for box, quad in cast(Sequence[tuple[float, float]], data): im.__transformer( box, self, Transform.QUAD, quad, resample, fillcolor is None ) @@ -2961,7 +2933,7 @@ def transform( self, size: tuple[int, int], image: Image, - **options: dict[str, str | int | tuple[int, ...] | list[int]], + **options: dict[str, str | int | tuple[int, ...] | list[int]] | int, ) -> Image: pass @@ -3830,7 +3802,7 @@ def _get_ifd_dict(self, offset, group=None): return self._fixup_dict(info) def _get_head(self): - version = b"\x2B" if self.bigtiff else b"\x2A" + version = b"\x2b" if self.bigtiff else b"\x2a" if self.endian == "<": head = b"II" + version + b"\x00" + o32le(8) else: diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 579489fdeed..ec8a9a67d5f 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -118,7 +118,7 @@ def getfont(self) -> FreeTypeFont | ImageFont: self.font = ImageFont.load_default() return self.font - def _getfont(self, font_size: float | None): + def _getfont(self, font_size: float | None) -> FreeTypeFont | ImageFont: if font_size is not None: from . import ImageFont @@ -451,13 +451,13 @@ def draw_corners(pieslice) -> None: right[3] -= r + 1 self.draw.draw_rectangle(right, ink, 1) - def _multiline_check(self, text: str | bytes) -> bool: - split_character = "\n" if isinstance(text, str) else b"\n" + def _multiline_check(self, text: AnyStr) -> bool: + split_character = cast(AnyStr, "\n" if isinstance(text, str) else b"\n") return split_character in text def _multiline_split(self, text: AnyStr) -> list[AnyStr]: - split_character = "\n" if isinstance(text, str) else b"\n" + split_character = cast(AnyStr, "\n" if isinstance(text, str) else b"\n") return text.split(split_character) @@ -470,10 +470,10 @@ def _multiline_spacing(self, font, spacing, stroke_width): def text( self, - xy: tuple[int, int], - text, + xy: tuple[float, float], + text: str, fill=None, - font=None, + font: FreeTypeFont | ImageFont | None = None, anchor=None, spacing=4, align="left", @@ -527,7 +527,7 @@ def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: coord.append(int(xy[i])) start.append(math.modf(xy[i])[0]) try: - mask, offset = font.getmask2( + mask, offset = font.getmask2( # type: ignore[union-attr,misc] text, mode, direction=direction, @@ -543,7 +543,7 @@ def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: coord = [coord[0] + offset[0], coord[1] + offset[1]] except AttributeError: try: - mask = font.getmask( + mask = font.getmask( # type: ignore[misc] text, mode, direction, @@ -592,7 +592,7 @@ def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: def multiline_text( self, - xy: tuple[int, int], + xy: tuple[float, float], text, fill=None, font=None, @@ -625,7 +625,7 @@ def multiline_text( font = self._getfont(font_size) widths = [] - max_width = 0 + max_width: float = 0 lines = self._multiline_split(text) line_spacing = self._multiline_spacing(font, spacing, stroke_width) for line in lines: @@ -779,7 +779,7 @@ def multiline_textbbox( font = self._getfont(font_size) widths = [] - max_width = 0 + max_width: float = 0 lines = self._multiline_split(text) line_spacing = self._multiline_spacing(font, spacing, stroke_width) for line in lines: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a1b722765a3..9eca3bc9877 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -35,11 +35,14 @@ from io import BytesIO from typing import TYPE_CHECKING, BinaryIO +from PIL import ImageFile + from . import Image from ._typing import StrOrBytesPath from ._util import is_directory, is_path if TYPE_CHECKING: + from ._imaging import ImagingFont from ._imagingft import Font @@ -84,11 +87,11 @@ def _string_length_check(text: str | bytes) -> None: class ImageFont: """PIL font wrapper""" - font: Font + font: ImagingFont def _load_pilfont(self, filename: str) -> None: with open(filename, "rb") as fp: - image = None + image: ImageFile.ImageFile | None = None for ext in (".png", ".gif", ".pbm"): if image: image.close() @@ -198,6 +201,8 @@ def getlength(self, text: str, *args: object, **kwargs: object) -> int: class FreeTypeFont: """FreeType font wrapper (requires _imagingft service)""" + font: Font + def __init__( self, font: StrOrBytesPath | BinaryIO | None = None, @@ -261,7 +266,7 @@ def __setstate__(self, state): path, size, index, encoding, layout_engine = state self.__init__(path, size, index, encoding, layout_engine) - def getname(self) -> tuple[str, str]: + def getname(self) -> tuple[str | None, str | None]: """ :return: A tuple of the font family (e.g. Helvetica) and the font style (e.g. Bold) @@ -876,6 +881,7 @@ def load_path(filename: str | bytes) -> ImageFont: """ for directory in sys.path: if is_directory(directory): + assert isinstance(directory, str) if not isinstance(filename, str): filename = filename.decode("utf-8") try: @@ -900,6 +906,7 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: :return: A font object. """ + f: FreeTypeFont | ImageFont if core.__class__.__name__ == "module" or size is not None: f = truetype( BytesIO( diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 6aa82dadd9c..80a6116b7cf 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -14,7 +14,7 @@ # from __future__ import annotations -from typing import Sequence +from typing import Any, Sequence from . import Image @@ -34,7 +34,7 @@ def transform( self, size: tuple[int, int], image: Image.Image, - **options: dict[str, str | int | tuple[int, ...] | list[int]], + **options: Any, ) -> Image.Image: """Perform the transform. Called from :py:meth:`.Image.transform`.""" # can be overridden diff --git a/src/PIL/_imaging.pyi b/src/PIL/_imaging.pyi index e27843e5338..d85eb84fa69 100644 --- a/src/PIL/_imaging.pyi +++ b/src/PIL/_imaging.pyi @@ -1,3 +1,18 @@ from typing import Any +from typing_extensions import Buffer + +class ImagingCore: + def __getattr__(self, name: str) -> Any: ... + +class ImagingFont: + def __getattr__(self, name: str) -> Any: ... + +class ImagingDraw: + def __getattr__(self, name: str) -> Any: ... + +class PixelAccess: + def __getattr__(self, name: str) -> Any: ... + +def font(image, glyphdata: Buffer) -> ImagingFont: ... def __getattr__(name: str) -> Any: ... From ef35d7926439e6fe8c36abc0846f859aaf3a893d Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Wed, 8 May 2024 12:14:37 +0200 Subject: [PATCH 05/17] Python 3.8 compatibility --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 9b0c24ec09a..31e6fdb83a8 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2204,7 +2204,7 @@ def resize( msg = "reducing_gap must be 1.0 or greater" raise ValueError(msg) - size = cast(tuple[int, int], tuple(size)) + size = cast("tuple[int, int]", tuple(size)) self.load() if box is None: From 7ae8d37138c8678e4a84210aa899df422fadaab1 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Wed, 8 May 2024 12:14:59 +0200 Subject: [PATCH 06/17] Make `GetDataTransform` public --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 31e6fdb83a8..ed1621e6244 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -482,7 +482,7 @@ def _getscaleoffset(expr): # Implementation wrapper -class _GetDataTransform(Protocol): +class GetDataTransform(Protocol): def getdata( self, ) -> tuple[Transform, Sequence[Any]]: ... @@ -2695,7 +2695,7 @@ def round_aspect(number, key): def transform( self, size: tuple[int, int], - method: Transform | ImageTransformHandler | _GetDataTransform, + method: Transform | ImageTransformHandler | GetDataTransform, data: Sequence[Any] | None = None, resample: int = Resampling.NEAREST, fill: int = 1, From 296050f3823c4648e6e7eb351e433343eddc9cee Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Wed, 8 May 2024 12:26:45 +0200 Subject: [PATCH 07/17] More Python 3.8 compatibility --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ed1621e6244..8348ea257d8 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2210,7 +2210,7 @@ def resize( if box is None: box = (0, 0) + self.size else: - box = cast(tuple[float, float, float, float], tuple(box)) + box = cast("tuple[float, float, float, float]", tuple(box)) if self.size == size and box == (0, 0) + self.size: return self.copy() @@ -2268,7 +2268,7 @@ def reduce( if box is None: box = (0, 0) + self.size else: - box = cast(tuple[int, int, int, int], tuple(box)) + box = cast("tuple[int, int, int, int]", tuple(box)) if factor == (1, 1) and box == (0, 0) + self.size: return self.copy() From bb8718e58162cdcd6a9b80eca45f7b2c8321bca9 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Wed, 8 May 2024 12:54:44 +0200 Subject: [PATCH 08/17] Hopefully the last Python 3.8 instance :/ --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 8348ea257d8..f39580996a3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2775,7 +2775,7 @@ def getdata(self): im.info = self.info.copy() if method == Transform.MESH: # list of quads - for box, quad in cast(Sequence[tuple[float, float]], data): + for box, quad in cast("Sequence[tuple[float, float]]", data): im.__transformer( box, self, Transform.QUAD, quad, resample, fillcolor is None ) From 431fe0dcc8ff8a28fbe89c1668d7090f247aaed8 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Fri, 10 May 2024 11:46:35 +0200 Subject: [PATCH 09/17] Rename protocol to SupportsGetData --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f39580996a3..154862a6fbc 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -482,7 +482,7 @@ def _getscaleoffset(expr): # Implementation wrapper -class GetDataTransform(Protocol): +class SupportsGetData(Protocol): def getdata( self, ) -> tuple[Transform, Sequence[Any]]: ... @@ -2695,7 +2695,7 @@ def round_aspect(number, key): def transform( self, size: tuple[int, int], - method: Transform | ImageTransformHandler | GetDataTransform, + method: Transform | ImageTransformHandler | SupportsGetData, data: Sequence[Any] | None = None, resample: int = Resampling.NEAREST, fill: int = 1, From 9b44abb6b7f77043ac337fe8171d0ecdbb4b7882 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Fri, 10 May 2024 11:48:36 +0200 Subject: [PATCH 10/17] Add SupportsGetData to documentation --- docs/reference/Image.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 0d9b4d93d77..c0d9095cd3c 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -365,6 +365,12 @@ Classes .. autoclass:: PIL.Image.ImagePointHandler .. autoclass:: PIL.Image.ImageTransformHandler +Protocols +--------- + +.. autoclass:: SupportsGetData + :show-inheritance: + Constants --------- From 13cf2bc70f4bb5de7c0a083303d4a232104d7852 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 May 2024 11:16:52 +1000 Subject: [PATCH 11/17] Moved SupportsArrayInterface under Protocols heading --- docs/reference/Image.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index c0d9095cd3c..d917a3c9271 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -78,8 +78,6 @@ Constructing images ^^^^^^^^^^^^^^^^^^^ .. autofunction:: new -.. autoclass:: SupportsArrayInterface - :show-inheritance: .. autofunction:: fromarray .. autofunction:: frombytes .. autofunction:: frombuffer @@ -368,6 +366,8 @@ Classes Protocols --------- +.. autoclass:: SupportsArrayInterface + :show-inheritance: .. autoclass:: SupportsGetData :show-inheritance: From 6310280428a49ea5495953a824b1dfa85a4d5223 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Sat, 11 May 2024 10:44:52 +0200 Subject: [PATCH 12/17] Move an import behind the TYPE_CHECKING flag Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageFont.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 9eca3bc9877..f2936bae67d 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -35,13 +35,12 @@ from io import BytesIO from typing import TYPE_CHECKING, BinaryIO -from PIL import ImageFile - from . import Image from ._typing import StrOrBytesPath from ._util import is_directory, is_path if TYPE_CHECKING: + from . import ImageFile from ._imaging import ImagingFont from ._imagingft import Font From 6d6dfd176cf00a864a62dff6dd881099cc3bcec8 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Sat, 11 May 2024 10:46:20 +0200 Subject: [PATCH 13/17] Revert unnecessary formatting change --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 154862a6fbc..53f38f0b241 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3802,7 +3802,7 @@ def _get_ifd_dict(self, offset, group=None): return self._fixup_dict(info) def _get_head(self): - version = b"\x2b" if self.bigtiff else b"\x2a" + version = b"\x2B" if self.bigtiff else b"\x2A" if self.endian == "<": head = b"II" + version + b"\x00" + o32le(8) else: From b2316f46cb4fc084fc15cb2848eca8b19cbc4329 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Sat, 18 May 2024 11:22:57 +0200 Subject: [PATCH 14/17] Use just `str` for `_string_length_check` Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageFont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index f2936bae67d..747c0c05077 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -61,7 +61,7 @@ class Layout(IntEnum): core = DeferredError.new(ex) -def _string_length_check(text: str | bytes) -> None: +def _string_length_check(text: str) -> None: if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: msg = "too many characters in string" raise ValueError(msg) From d566c04d5b9b2f7587015b110e588b073a24cf2d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Jun 2024 14:20:01 +1000 Subject: [PATCH 15/17] Updated type hints --- src/PIL/Image.py | 22 +++++++--------- src/PIL/ImageDraw.py | 29 +++++++++++++++------ src/PIL/ImageFont.py | 53 +++++++++++++++++++------------------- src/PIL/JpegImagePlugin.py | 2 +- src/PIL/_imaging.pyi | 4 +-- src/PIL/_imagingft.pyi | 23 ++++++++++++----- 6 files changed, 75 insertions(+), 58 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c02c7d6b6bf..2ea26877d13 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -506,7 +506,7 @@ def _getscaleoffset(expr): class SupportsGetData(Protocol): def getdata( self, - ) -> tuple[Transform, Sequence[Any]]: ... + ) -> tuple[Transform, Sequence[int]]: ... class Image: @@ -1295,7 +1295,7 @@ def _crop(self, im, box): return im.crop((x0, y0, x1, y1)) def draft( - self, mode: str, size: tuple[int, int] + self, mode: str | None, size: tuple[int, int] ) -> tuple[str, tuple[int, int, float, float]] | None: """ Configures the image file loader so it returns a version of the @@ -1719,7 +1719,7 @@ def entropy(self, mask=None, extrema=None): def paste( self, - im: Image | str | int | tuple[int, ...], + im: Image | str | float | tuple[int, ...], box: tuple[int, int, int, int] | tuple[int, int] | None = None, mask: Image | None = None, ) -> None: @@ -1750,7 +1750,7 @@ def paste( See :py:meth:`~PIL.Image.Image.alpha_composite` if you want to combine images with respect to their alpha channels. - :param im: Source image or pixel value (integer or tuple). + :param im: Source image or pixel value (integer, float or tuple). :param box: An optional 4-tuple giving the region to paste into. If a 2-tuple is used instead, it's treated as the upper left corner. If omitted or None, the source is pasted into the @@ -2228,13 +2228,9 @@ def resize( msg = "reducing_gap must be 1.0 or greater" raise ValueError(msg) - size = cast("tuple[int, int]", tuple(size)) - self.load() if box is None: box = (0, 0) + self.size - else: - box = cast("tuple[float, float, float, float]", tuple(box)) if self.size == size and box == (0, 0) + self.size: return self.copy() @@ -2291,8 +2287,6 @@ def reduce( if box is None: box = (0, 0) + self.size - else: - box = cast("tuple[int, int, int, int]", tuple(box)) if factor == (1, 1) and box == (0, 0) + self.size: return self.copy() @@ -2692,7 +2686,9 @@ def round_aspect(number, key): return size = preserved_size - res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap)) # type: ignore[arg-type] + res = self.draft( + None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap)) + ) if res is not None: box = res[1] if box is None: @@ -2799,7 +2795,7 @@ def getdata(self): im.info = self.info.copy() if method == Transform.MESH: # list of quads - for box, quad in cast("Sequence[tuple[float, float]]", data): + for box, quad in data: im.__transformer( box, self, Transform.QUAD, quad, resample, fillcolor is None ) @@ -2957,7 +2953,7 @@ def transform( self, size: tuple[int, int], image: Image, - **options: dict[str, str | int | tuple[int, ...] | list[int]] | int, + **options: str | int | tuple[int, ...] | list[int], ) -> Image: pass diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 1887a393352..0663d9ddf85 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -456,14 +456,12 @@ def draw_corners(pieslice) -> None: self.draw.draw_rectangle(right, ink, 1) def _multiline_check(self, text: AnyStr) -> bool: - split_character = cast(AnyStr, "\n" if isinstance(text, str) else b"\n") + split_character = "\n" if isinstance(text, str) else b"\n" return split_character in text def _multiline_split(self, text: AnyStr) -> list[AnyStr]: - split_character = cast(AnyStr, "\n" if isinstance(text, str) else b"\n") - - return text.split(split_character) + return text.split("\n" if isinstance(text, str) else b"\n") def _multiline_spacing(self, font, spacing, stroke_width): return ( @@ -477,7 +475,12 @@ def text( xy: tuple[float, float], text: str, fill=None, - font: ImageFont.FreeTypeFont | ImageFont.ImageFont | None = None, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, anchor=None, spacing=4, align="left", @@ -597,9 +600,14 @@ def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: def multiline_text( self, xy: tuple[float, float], - text, + text: str, fill=None, - font=None, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, anchor=None, spacing=4, align="left", @@ -684,7 +692,12 @@ def multiline_text( def textlength( self, text: str, - font: ImageFont.FreeTypeFont | ImageFont.ImageFont | None = None, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, direction=None, features=None, language=None, diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 747c0c05077..a9925483e40 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -33,11 +33,11 @@ import warnings from enum import IntEnum from io import BytesIO -from typing import TYPE_CHECKING, BinaryIO +from typing import IO, TYPE_CHECKING, Any, BinaryIO from . import Image from ._typing import StrOrBytesPath -from ._util import is_directory, is_path +from ._util import is_path if TYPE_CHECKING: from . import ImageFile @@ -61,7 +61,7 @@ class Layout(IntEnum): core = DeferredError.new(ex) -def _string_length_check(text: str) -> None: +def _string_length_check(text: str | bytes | bytearray) -> None: if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: msg = "too many characters in string" raise ValueError(msg) @@ -113,7 +113,7 @@ def _load_pilfont(self, filename: str) -> None: self._load_pilfont_data(fp, image) image.close() - def _load_pilfont_data(self, file, image): + def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: # read PILfont header if file.readline() != b"PILfont\n": msg = "Not a PILfont file" @@ -161,7 +161,7 @@ def getmask(self, text, mode="", *args, **kwargs): return self.font.getmask(text, mode) def getbbox( - self, text: str, *args: object, **kwargs: object + self, text: str | bytes | bytearray, *args: Any, **kwargs: Any ) -> tuple[int, int, int, int]: """ Returns bounding box (in pixels) of given text. @@ -180,7 +180,9 @@ def getbbox( width, height = self.font.getsize(text) return 0, 0, width, height - def getlength(self, text: str, *args: object, **kwargs: object) -> int: + def getlength( + self, text: str | bytes | bytearray, *args: Any, **kwargs: Any + ) -> int: """ Returns length (in pixels) of given text. This is the amount by which following text should be offset. @@ -357,13 +359,13 @@ def getlength( def getbbox( self, text: str, - mode="", - direction=None, - features=None, - language=None, - stroke_width=0, - anchor=None, - ) -> tuple[int, int, int, int]: + mode: str = "", + direction: str | None = None, + features: str | None = None, + language: str | None = None, + stroke_width: float = 0, + anchor: str | None = None, + ) -> tuple[float, float, float, float]: """ Returns bounding box (in pixels) of given text relative to given anchor when rendered in font with provided direction, features, and language. @@ -513,7 +515,7 @@ def getmask( def getmask2( self, - text, + text: str, mode="", direction=None, features=None, @@ -641,7 +643,7 @@ def font_variant( layout_engine=layout_engine or self.layout_engine, ) - def get_variation_names(self): + def get_variation_names(self) -> list[bytes]: """ :returns: A list of the named styles in a variation font. :exception OSError: If the font is not a variation font. @@ -683,10 +685,11 @@ def get_variation_axes(self): msg = "FreeType 2.9.1 or greater is required" raise NotImplementedError(msg) from e for axis in axes: - axis["name"] = axis["name"].replace(b"\x00", b"") + if axis["name"]: + axis["name"] = axis["name"].replace(b"\x00", b"") return axes - def set_variation_by_axes(self, axes): + def set_variation_by_axes(self, axes: list[float]) -> None: """ :param axes: A list of values for each axis. :exception OSError: If the font is not a variation font. @@ -731,7 +734,7 @@ def getbbox(self, text, *args, **kwargs): return 0, 0, height, width return 0, 0, width, height - def getlength(self, text, *args, **kwargs): + def getlength(self, text: str, *args, **kwargs) -> float: if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): msg = "text length is undefined for text rotated by 90 or 270 degrees" raise ValueError(msg) @@ -878,15 +881,13 @@ def load_path(filename: str | bytes) -> ImageFont: :return: A font object. :exception OSError: If the file could not be read. """ + if not isinstance(filename, str): + filename = filename.decode("utf-8") for directory in sys.path: - if is_directory(directory): - assert isinstance(directory, str) - if not isinstance(filename, str): - filename = filename.decode("utf-8") - try: - return load(os.path.join(directory, filename)) - except OSError: - pass + try: + return load(os.path.join(directory, filename)) + except OSError: + pass msg = "cannot find font file" raise OSError(msg) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 909911dfe37..e1c61f991c5 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -425,7 +425,7 @@ def load_read(self, read_bytes: int) -> bytes: return s def draft( - self, mode: str, size: tuple[int, int] + self, mode: str | None, size: tuple[int, int] ) -> tuple[str, tuple[int, int, float, float]] | None: if len(self.tile) != 1: return None diff --git a/src/PIL/_imaging.pyi b/src/PIL/_imaging.pyi index d85eb84fa69..1fe95441715 100644 --- a/src/PIL/_imaging.pyi +++ b/src/PIL/_imaging.pyi @@ -1,7 +1,5 @@ from typing import Any -from typing_extensions import Buffer - class ImagingCore: def __getattr__(self, name: str) -> Any: ... @@ -14,5 +12,5 @@ class ImagingDraw: class PixelAccess: def __getattr__(self, name: str) -> Any: ... -def font(image, glyphdata: Buffer) -> ImagingFont: ... +def font(image, glyphdata: bytes) -> ImagingFont: ... def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 987e7fd6f49..b023efe0110 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -1,5 +1,7 @@ from typing import Any, TypedDict +from . import _imaging + class _Axis(TypedDict): minimum: int | None default: int | None @@ -37,21 +39,28 @@ class Font: x_start=..., y_start=..., /, - ) -> tuple[Any, tuple[int, int]]: ... + ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ... def getsize( - self, string: str, mode=..., dir=..., features=..., lang=..., anchor=..., / + self, + string: str | bytes | bytearray, + 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]: ... + ) -> float: ... + def getvarnames(self) -> list[bytes]: ... + def getvaraxes(self) -> list[_Axis] | None: ... def setvarname(self, instance_index: int, /) -> None: ... def setvaraxes(self, axes: list[float], /) -> None: ... def getfont( - filename: str | bytes | bytearray, - size, + filename: str | bytes, + size: float, index=..., encoding=..., font_bytes=..., From d2603b779aec4810349621542b795e79c0144d8e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Jun 2024 15:42:24 +1000 Subject: [PATCH 16/17] im color could be a tuple with a single float --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 2ea26877d13..d9eb73e453c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1719,7 +1719,7 @@ def entropy(self, mask=None, extrema=None): def paste( self, - im: Image | str | float | tuple[int, ...], + im: Image | str | float | tuple[float, ...], box: tuple[int, int, int, int] | tuple[int, int] | None = None, mask: Image | None = None, ) -> None: From 45cdc53bbb609212590b0558061aa2991cc87a5d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Jun 2024 18:01:26 +1000 Subject: [PATCH 17/17] Updated type hints --- Tests/test_image_rotate.py | 4 ++-- docs/handbook/concepts.rst | 6 ++++++ docs/reference/Image.rst | 1 - src/PIL/Image.py | 10 +++++----- src/PIL/ImageDraw.py | 11 ++++++----- src/PIL/ImageFont.py | 2 +- src/PIL/_imagingft.pyi | 2 +- 7 files changed, 21 insertions(+), 15 deletions(-) diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index c10c96da6f9..252a15db742 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -124,8 +124,8 @@ def test_fastpath_translate() -> None: def test_center() -> None: im = hopper() rotate(im, im.mode, 45, center=(0, 0)) - rotate(im, im.mode, 45, translate=(im.size[0] / 2, 0)) - rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] / 2, 0)) + rotate(im, im.mode, 45, translate=(im.size[0] // 2, 0)) + rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] // 2, 0)) def test_rotate_no_fill() -> None: diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 5094dbf3f27..7da1078c14e 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -144,10 +144,12 @@ pixel, the Python Imaging Library provides different resampling *filters*. .. py:currentmodule:: PIL.Image .. data:: Resampling.NEAREST + :noindex: Pick one nearest pixel from the input image. Ignore all other input pixels. .. data:: Resampling.BOX + :noindex: Each pixel of source image contributes to one pixel of the destination image with identical weights. @@ -158,6 +160,7 @@ pixel, the Python Imaging Library provides different resampling *filters*. .. versionadded:: 3.4.0 .. data:: Resampling.BILINEAR + :noindex: For resize calculate the output pixel value using linear interpolation on all pixels that may contribute to the output value. @@ -165,6 +168,7 @@ pixel, the Python Imaging Library provides different resampling *filters*. in the input image is used. .. data:: Resampling.HAMMING + :noindex: Produces a sharper image than :data:`Resampling.BILINEAR`, doesn't have dislocations on local level like with :data:`Resampling.BOX`. @@ -174,6 +178,7 @@ pixel, the Python Imaging Library provides different resampling *filters*. .. versionadded:: 3.4.0 .. data:: Resampling.BICUBIC + :noindex: For resize calculate the output pixel value using cubic interpolation on all pixels that may contribute to the output value. @@ -181,6 +186,7 @@ pixel, the Python Imaging Library provides different resampling *filters*. in the input image is used. .. data:: Resampling.LANCZOS + :noindex: Calculate the output pixel value using a high-quality Lanczos filter (a truncated sinc) on all pixels that may contribute to the output value. diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index d917a3c9271..1c095a11453 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -424,7 +424,6 @@ See :ref:`concept-filters` for details. .. autoclass:: Resampling :members: :undoc-members: - :noindex: Dither modes ^^^^^^^^^^^^ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d9eb73e453c..13d374345a6 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2303,8 +2303,8 @@ def reduce( def rotate( self, angle: float, - resample: int = Resampling.NEAREST, - expand: bool = False, + resample: Resampling = Resampling.NEAREST, + expand: int | bool = False, center: tuple[int, int] | None = None, translate: tuple[int, int] | None = None, fillcolor: float | tuple[float, ...] | str | None = None, @@ -2617,8 +2617,8 @@ def tell(self) -> int: def thumbnail( self, - size: tuple[int, int], - resample: int = Resampling.BICUBIC, + size: tuple[float, float], + resample: Resampling = Resampling.BICUBIC, reducing_gap: float = 2.0, ) -> None: """ @@ -2953,7 +2953,7 @@ def transform( self, size: tuple[int, int], image: Image, - **options: str | int | tuple[int, ...] | list[int], + **options: Any, ) -> Image: pass diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 0663d9ddf85..9796189bb4b 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -95,7 +95,9 @@ def __init__(self, im: Image.Image, mode: str | None = None) -> None: if TYPE_CHECKING: from . import ImageFont - def getfont(self) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + def getfont( + self, + ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: """ Get the current default font. @@ -122,14 +124,13 @@ def getfont(self) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: def _getfont( self, font_size: float | None - ) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: if font_size is not None: from . import ImageFont - font = ImageFont.load_default(font_size) + return ImageFont.load_default(font_size) else: - font = self.getfont() - return font + return self.getfont() def _getink(self, ink, fill=None) -> tuple[int | None, int | None]: if ink is None and fill is None: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a9925483e40..87261f51920 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -361,7 +361,7 @@ def getbbox( text: str, mode: str = "", direction: str | None = None, - features: str | None = None, + features: list[str] | None = None, language: str | None = None, stroke_width: float = 0, anchor: str | None = None, diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index b023efe0110..6e0ddd2f165 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -6,7 +6,7 @@ class _Axis(TypedDict): minimum: int | None default: int | None maximum: int | None - name: str | None + name: bytes | None class Font: @property