From 4d27431ed3542ac50de6021fe8470c0adf1a98ef Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 27 Dec 2023 02:27:46 +0100 Subject: [PATCH] (WIP) add type hints for ImageFont and _imagingft --- src/PIL/Image.py | 2 +- src/PIL/ImageFont.py | 152 +++++++++++++++++++++++++---------------- src/PIL/_imagingft.pyi | 82 +++++++++++++++++++++- src/_imagingft.c | 4 +- 4 files changed, 176 insertions(+), 64 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 5a2e8541928..032ad1481b2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3191,7 +3191,7 @@ def _decompression_bomb_check(size): ) -def open(fp, mode="r", formats=None): +def open(fp, mode="r", formats=None) -> Image: """ Opens and identifies the given image file. diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 41d8fbc17de..1f8137a27f5 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -34,7 +34,7 @@ from enum import IntEnum from io import BytesIO from pathlib import Path -from typing import BinaryIO +from typing import Any, BinaryIO, Sequence from . import Image from ._util import is_directory, is_path @@ -45,7 +45,7 @@ class Layout(IntEnum): RAQM = 1 -MAX_STRING_LENGTH = 1_000_000 +MAX_STRING_LENGTH: int | None = 1_000_000 try: @@ -56,7 +56,7 @@ class Layout(IntEnum): core = DeferredError.new(ex) -def _string_length_check(text): +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) @@ -81,9 +81,9 @@ def _string_length_check(text): class ImageFont: """PIL font wrapper""" - def _load_pilfont(self, filename): + def _load_pilfont(self, filename: str | Path): with open(filename, "rb") as fp: - image = None + image: Image.Image | None = None for ext in (".png", ".gif", ".pbm"): if image: image.close() @@ -106,7 +106,7 @@ def _load_pilfont(self, filename): self._load_pilfont_data(fp, image) image.close() - def _load_pilfont_data(self, file, image): + def _load_pilfont_data(self, file: BinaryIO, image: Image.Image): # read PILfont header if file.readline() != b"PILfont\n": msg = "Not a PILfont file" @@ -131,7 +131,9 @@ def _load_pilfont_data(self, file, image): self.font = Image.core.font(image.im, data) - def getmask(self, text, mode="", *args, **kwargs): + def getmask( + self, text: str, mode: str | None = "", *args, **kwargs + ) -> Any: # TODO returns internal image type """ Create a bitmap for the text. @@ -151,7 +153,7 @@ def getmask(self, text, mode="", *args, **kwargs): """ return self.font.getmask(text, mode) - def getbbox(self, text, *args, **kwargs): + def getbbox(self, text: str, *args, **kwargs) -> tuple[int, int, int, int]: """ Returns bounding box (in pixels) of given text. @@ -169,7 +171,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, **kwargs) -> float: """ Returns length (in pixels) of given text. This is the amount by which following text should be offset. @@ -189,6 +191,8 @@ def getlength(self, text, *args, **kwargs): class FreeTypeFont: """FreeType font wrapper (requires _imagingft service)""" + font: core.Font # TODO remove this + def __init__( self, font: bytes | str | Path | BinaryIO | None = None, @@ -253,14 +257,14 @@ 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) """ return self.font.family, self.font.style - def getmetrics(self): + def getmetrics(self) -> tuple[int, int]: """ :return: A tuple of the font ascent (the distance from the baseline to the highest outline point) and descent (the distance from the @@ -268,7 +272,14 @@ 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: str | None = "", + direction: str | None = None, + features: Sequence[str] | None = None, + language: str | None = None, + ) -> float: """ Returns length (in pixels with 1/64 precision) of given text when rendered in font with provided direction, features, and language. @@ -342,14 +353,14 @@ def getlength(self, text, mode="", direction=None, features=None, language=None) def getbbox( self, - text, - mode="", - direction=None, - features=None, - language=None, - stroke_width=0, - anchor=None, - ): + text: str, + mode: str | None = "", + direction: str | None = None, + features: Sequence[str] | None = None, + language: str | None = None, + stroke_width: int = 0, + anchor: str | None = 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. @@ -408,16 +419,16 @@ def getbbox( def getmask( self, - text, - mode="", - direction=None, - features=None, - language=None, - stroke_width=0, - anchor=None, - ink=0, - start=None, - ): + text: str, + mode: str | None = "", + direction: str | None = None, + features: Sequence[str] | None = None, + language: str | None = None, + stroke_width: int = 0, + anchor: str | None = None, + ink: int = 0, + start: tuple[float, float] | None = None, + ) -> Any: # TODO returns internal image type """ Create a bitmap for the text. @@ -499,18 +510,18 @@ def getmask( def getmask2( self, - text, - mode="", - direction=None, - features=None, - language=None, - stroke_width=0, - anchor=None, - ink=0, - start=None, + text: str, + mode: str | None = "", + direction: str | None = None, + features: Sequence[str] | None = None, + language: str | None = None, + stroke_width: int = 0, + anchor: str | None = None, + ink: int = 0, + start: tuple[float, float] | None = None, *args, **kwargs, - ): + ) -> tuple[Any, tuple[int, int]]: # TODO returns internal image type """ Create a bitmap for the text. @@ -585,7 +596,7 @@ def getmask2( im = None size = None - def fill(width, height): + def fill(width: int, height: int) -> Any: # TODO returns internal image type nonlocal im, size size = (width, height) @@ -614,8 +625,13 @@ def fill(width, height): return im, offset def font_variant( - self, font=None, size=None, index=None, encoding=None, layout_engine=None - ): + self, + font: bytes | str | Path | BinaryIO | None = None, + size: float | None = None, + index: int | None = None, + encoding: str | None = None, + layout_engine: Layout | None = None, + ) -> FreeTypeFont: """ Create a copy of this FreeTypeFont object, using any specified arguments to override the settings. @@ -638,7 +654,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. @@ -650,7 +666,7 @@ def get_variation_names(self): raise NotImplementedError(msg) from e return [name.replace(b"\x00", b"") for name in names] - def set_variation_by_name(self, name): + def set_variation_by_name(self, name: str | bytes): """ :param name: The name of the style. :exception OSError: If the font is not a variation font. @@ -669,7 +685,11 @@ def set_variation_by_name(self, name): self.font.setvarname(index) - def get_variation_axes(self): + def get_variation_axes( + self, + ) -> list[ + core.FontVariationAxis + ]: # TODO return type not available unless type checking """ :returns: A list of the axes in a variation font. :exception OSError: If the font is not a variation font. @@ -680,10 +700,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]): """ :param axes: A list of values for each axis. :exception OSError: If the font is not a variation font. @@ -698,7 +719,11 @@ def set_variation_by_axes(self, axes): class TransposedFont: """Wrapper for writing rotated or mirrored text""" - def __init__(self, font, orientation=None): + def __init__( + self, + font: ImageFont | FreeTypeFont | TransposedFont, + orientation: Image.Transpose | None = None, + ) -> None: """ Wrapper that creates a transposed font from any existing font object. @@ -712,13 +737,15 @@ def __init__(self, font, orientation=None): self.font = font self.orientation = orientation # any 'transpose' argument, or None - def getmask(self, text, mode="", *args, **kwargs): + def getmask( + self, text: str, mode: str | None = "", *args, **kwargs + ) -> Any: # TODO returns internal image type im = self.font.getmask(text, mode, *args, **kwargs) if self.orientation is not None: return im.transpose(self.orientation) return im - def getbbox(self, text, *args, **kwargs): + def getbbox(self, text: str, *args, **kwargs) -> tuple[int, int, int, int]: # TransposedFont doesn't support getmask2, move top-left point to (0, 0) # this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont left, top, right, bottom = self.font.getbbox(text, *args, **kwargs) @@ -728,14 +755,14 @@ 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) return self.font.getlength(text, *args, **kwargs) -def load(filename): +def load(filename: str | Path) -> ImageFont: """ Load a font file. This function loads a font object from the given bitmap font file, and returns the corresponding font object. @@ -749,7 +776,13 @@ def load(filename): return f -def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): +def truetype( + font: bytes | str | Path | 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. @@ -860,7 +893,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. @@ -874,14 +907,15 @@ def load_path(filename): if not isinstance(filename, str): filename = filename.decode("utf-8") try: - return load(os.path.join(directory, filename)) + # TODO sys.path might not be all str + return load(os.path.join(directory, filename)) # type: ignore[arg-type] except OSError: pass msg = "cannot find font file" raise OSError(msg) -def load_default(size=None): +def load_default(size: int | None = None) -> ImageFont | FreeTypeFont: """If FreeType support is available, load a version of Aileron Regular, https://dotcolon.net/font/aileron, with a more limited character set. @@ -896,7 +930,7 @@ def load_default(size=None): :return: A font object. """ if core.__class__.__name__ == "module" or size is not None: - f = truetype( + return truetype( BytesIO( base64.b64decode( b""" @@ -1259,4 +1293,4 @@ def load_default(size=None): ) ), ) - return f + return f diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index b0235555dc5..9543317f633 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -1,5 +1,83 @@ from __future__ import annotations -from typing import Any +from typing import Any, Callable, Sequence, TypedDict -def __getattr__(name: str) -> Any: ... +HAVE_RAQM: bool +HAVE_FRIBIDI: bool +HAVE_HARFBUZZ: bool + +freetype2_version: str | None +raqm_version: str | None +fribidi_version: str | None +harfbuzz_version: str | None + +class FontVariationAxis(TypedDict): + minimum: int | None + default: int | None + maximum: int | None + name: bytes | None + +class Font: + def render( + self, + string: str, + fill: Callable[[int, int], Any], # TODO returns internal image type + mode: str | None = None, + dir: str | None = None, + features: Sequence[str] | None = None, + lang: str | None = None, + stroke_width: int = 0, + anchor: str | None = None, + foreground_ink_long: int = 0, + x_start: float = 0, + y_start: float = 0, + ) -> tuple[int, int]: ... + def getsize( + self, + string: str, + mode: str | None = None, + dir: str | None = None, + features: Sequence[str] | None = None, + lang: str | None = None, + anchor: str | None = None, + ) -> tuple[tuple[int, int], tuple[int, int]]: ... + def getlength( + self, + string: str, + mode: str | None = None, + dir: str | None = None, + features: Sequence[str] | None = None, + lang: str | None = None, + ) -> int: ... + + if ...: # freetype2_version >= 2.9.1 + def getvarnames(self) -> list[bytes]: ... + def getvaraxes(self) -> list[FontVariationAxis]: ... + def setvarname(self, index: int) -> None: ... + def setvaraxes(self, axes: list[float]) -> None: ... + + @property + def family(self) -> str: ... + @property + def style(self) -> str: ... + @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 getfont( + filename: str | bytes | bytearray, + size: float, + index: int = 0, + encoding: str = "", + font_bytes: bytes | bytearray = b"", + layout_engine: int = 0, +) -> Font: ... diff --git a/src/_imagingft.c b/src/_imagingft.c index 68c66ac2c60..f953a890da3 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -121,7 +121,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { FT_Long width; Py_ssize_t index = 0; Py_ssize_t layout_engine = 0; - unsigned char *encoding; + unsigned char *encoding = NULL; unsigned char *font_bytes; Py_ssize_t font_bytes_size = 0; static char *kwlist[] = { @@ -821,7 +821,7 @@ font_render(FontObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "OO|zzOzizLffO:render", + "OO|zzOzizLff:render", &string, &fill, &mode,