diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 6a0a940b987..75413aed89c 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -257,7 +257,7 @@ def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None: "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) ) def test_render_multiline_text_align( - font: ImageFont.FreeTypeFont, align: str, ext: str + font: ImageFont.FreeTypeFont, align: ImageDraw.Align, ext: str ) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -272,7 +272,7 @@ def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: # Act/Assert with pytest.raises(ValueError): - draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") + draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") # type: ignore[arg-type] def test_draw_align(font: ImageFont.FreeTypeFont) -> None: @@ -795,7 +795,7 @@ def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None: ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), ) def test_anchor( - layout_engine: ImageFont.Layout, anchor: str, left: int, top: int + layout_engine: ImageFont.Layout, anchor: ImageFont.Anchor, left: int, top: int ) -> None: name, text = "quick", "Quick" path = f"Tests/images/test_anchor_{name}_{anchor}.png" @@ -842,7 +842,7 @@ def test_anchor( ), ) def test_anchor_multiline( - layout_engine: ImageFont.Layout, anchor: str, align: str + layout_engine: ImageFont.Layout, anchor: ImageFont.Anchor, align: ImageDraw.Align ) -> None: target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" text = "a\nlong\ntext sample" @@ -868,22 +868,24 @@ def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None: for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]: with pytest.raises(ValueError): - font.getmask2("hello", anchor=anchor) + font.getmask2("hello", anchor=anchor) # type: ignore[arg-type] with pytest.raises(ValueError): - font.getbbox("hello", anchor=anchor) + font.getbbox("hello", anchor=anchor) # type: ignore[arg-type] with pytest.raises(ValueError): - d.text((0, 0), "hello", anchor=anchor) + d.text((0, 0), "hello", anchor=anchor) # type: ignore[arg-type] with pytest.raises(ValueError): - d.textbbox((0, 0), "hello", anchor=anchor) + d.textbbox((0, 0), "hello", anchor=anchor) # type: ignore[arg-type] with pytest.raises(ValueError): - d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + d.multiline_text((0, 0), "foo\nbar", anchor=anchor) # type: ignore[arg-type] with pytest.raises(ValueError): - d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) - for anchor in ["lt", "lb"]: + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) # type: ignore[arg-type] + + anchors: list[ImageFont.Anchor] = ["lt", "lb"] + for anchor2 in anchors: with pytest.raises(ValueError): - d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + d.multiline_text((0, 0), "foo\nbar", anchor=anchor2) with pytest.raises(ValueError): - d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor2) @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 24c7b871a68..a48cafbbf37 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -216,7 +216,7 @@ def test_getlength( d = ImageDraw.Draw(im) try: - assert d.textlength(text, ttf, direction) == expected + assert d.textlength(text, ttf, direction) == expected # type: ignore[arg-type] except ValueError as ex: if ( direction == "ttb" @@ -232,7 +232,9 @@ def test_getlength( ("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"), ids=("caron-above", "caron-below", "double-breve", "overline"), ) -def test_getlength_combine(mode: str, direction: str, text: str) -> None: +def test_getlength_combine( + mode: str, direction: ImageFont.Direction, text: str +) -> None: if text == "i\u0305i" and direction == "ttb": pytest.skip("fails with this font") @@ -252,7 +254,7 @@ def test_getlength_combine(mode: str, direction: str, text: str) -> None: @pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) -def test_anchor_ttb(anchor: str) -> None: +def test_anchor_ttb(anchor: ImageFont.Anchor) -> None: text = "f" path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120) @@ -309,7 +311,11 @@ def test_anchor_ttb(anchor: str) -> None: "name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests] ) def test_combine( - name: str, text: str, dir: str | None, anchor: str | None, epsilon: float + name: str, + text: str, + anchor: ImageFont.Anchor | None, + dir: ImageFont.Direction | None, + epsilon: float, ) -> None: path = f"Tests/images/test_combine_{name}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) @@ -341,7 +347,7 @@ def test_combine( ("rm", "right"), # pass with getsize ), ) -def test_combine_multiline(anchor: str, align: str) -> None: +def test_combine_multiline(anchor: ImageFont.Anchor, align: ImageDraw.Align) -> None: # test that multiline text uses getlength, not getsize or getbbox path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png" @@ -367,17 +373,17 @@ def test_anchor_invalid_ttb() -> None: for anchor in ["", "l", "a", "lax", "xa", "la", "ls", "ld", "lx"]: with pytest.raises(ValueError): - font.getmask2("hello", anchor=anchor, direction="ttb") + font.getmask2("hello", anchor=anchor, direction="ttb") # type: ignore[arg-type] with pytest.raises(ValueError): - font.getbbox("hello", anchor=anchor, direction="ttb") + font.getbbox("hello", anchor=anchor, direction="ttb") # type: ignore[arg-type] with pytest.raises(ValueError): - d.text((0, 0), "hello", anchor=anchor, direction="ttb") + d.text((0, 0), "hello", anchor=anchor, direction="ttb") # type: ignore[arg-type] with pytest.raises(ValueError): - d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb") + d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb") # type: ignore[arg-type] with pytest.raises(ValueError): - d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb") + d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb") # type: ignore[arg-type] with pytest.raises(ValueError): - d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb") + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb") # type: ignore[arg-type] # ttb multiline text does not support anchors at all with pytest.raises(ValueError): d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb") diff --git a/docs/example/anchors.py b/docs/example/anchors.py index 2ee11103f1a..d4d4f3c8f66 100644 --- a/docs/example/anchors.py +++ b/docs/example/anchors.py @@ -5,7 +5,7 @@ font = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 16) -def test(anchor: str) -> Image.Image: +def test(anchor: ImageFont.Anchor) -> Image.Image: im = Image.new("RGBA", (200, 100), "white") d = ImageDraw.Draw(im) d.line(((100, 0), (100, 100)), "gray") @@ -17,9 +17,12 @@ def test(anchor: str) -> Image.Image: if __name__ == "__main__": im = Image.new("RGBA", (600, 300), "white") d = ImageDraw.Draw(im) - for y, row in enumerate( - (("ma", "mt", "mm"), ("ms", "mb", "md"), ("ls", "ms", "rs")) - ): + anchors: list[list[ImageFont.Anchor]] = [ + ["ma", "mt", "mm"], + ["ms", "mb", "md"], + ["ls", "ms", "rs"], + ] + for y, row in enumerate(anchors): for x, anchor in enumerate(row): im.paste(test(anchor), (x * 200, y * 100)) if x != 0: diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index d9d9cac6e13..98c9a2386be 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -92,6 +92,11 @@ Constants raise a :py:exc:`ValueError` if the number of characters is over this limit. The check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. +.. class:: Anchor + + Type hint literal with the possible anchor values. See :ref:`text-anchors` for + details. + Dictionaries ------------ diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 1012dcbc039..e86ddd7b81e 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -39,7 +39,7 @@ from . import Image, ImageColor from ._deprecate import deprecate -from ._typing import Align, Anchor, Coords, Direction +from ._typing import Coords # experimental access to the outline API Outline: Callable[[], Image.core._Outline] | None @@ -51,6 +51,8 @@ if TYPE_CHECKING: from . import ImageDraw2, ImageFont + Align = Literal["left", "center", "right"] + _Ink = Union[float, tuple[int, ...], str] """ @@ -583,10 +585,10 @@ def text( | ImageFont.TransposedFont | None ) = None, - anchor: Anchor | None = None, + anchor: ImageFont.Anchor | None = None, spacing: float = 4, align: Align = "left", - direction: Direction | None = None, + direction: ImageFont.Direction | None = None, features: list[str] | None = None, language: str | None = None, stroke_width: float = 0, @@ -708,10 +710,10 @@ def multiline_text( | ImageFont.TransposedFont | None ) = None, - anchor: Anchor | None = None, + anchor: ImageFont.Anchor | None = None, spacing: float = 4, align: Align = "left", - direction: Direction | None = None, + direction: ImageFont.Direction | None = None, features: list[str] | None = None, language: str | None = None, stroke_width: float = 0, @@ -798,7 +800,7 @@ def textlength( | ImageFont.TransposedFont | None ) = None, - direction: Direction | None = None, + direction: ImageFont.Direction | None = None, features: list[str] | None = None, language: str | None = None, embedded_color: bool = False, @@ -828,10 +830,10 @@ def textbbox( | ImageFont.TransposedFont | None ) = None, - anchor: Anchor | None = None, + anchor: ImageFont.Anchor | None = None, spacing: float = 4, align: Align = "left", - direction: Direction | None = None, + direction: ImageFont.Direction | None = None, features: list[str] | None = None, language: str | None = None, stroke_width: float = 0, @@ -878,10 +880,10 @@ def multiline_textbbox( | ImageFont.TransposedFont | None ) = None, - anchor: Anchor | None = None, + anchor: ImageFont.Anchor | None = None, spacing: float = 4, align: Align = "left", - direction: Direction | None = None, + direction: ImageFont.Direction | None = None, features: list[str] | None = None, language: str | None = None, stroke_width: float = 0, diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index d8c2655609e..48462e2c14a 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -34,7 +34,7 @@ from enum import IntEnum from io import BytesIO from types import ModuleType -from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast +from typing import IO, TYPE_CHECKING, Any, BinaryIO, Literal, TypedDict, cast from . import Image, features from ._typing import StrOrBytesPath @@ -45,6 +45,35 @@ from ._imaging import ImagingFont from ._imagingft import Font + Anchor = Literal[ + "la", + "lt", + "lm", + "ls", + "lb", + "ld", + "ma", + "mt", + "mm", + "ms", + "mb", + "md", + "ra", + "rt", + "rm", + "rs", + "rb", + "rd", + "sa", + "st", + "sm", + "ss", + "sb", + "sd", + ] + + Direction = Literal["rtl", "ltr", "ttb"] + class Axis(TypedDict): minimum: int | None @@ -313,7 +342,7 @@ def getlength( self, text: str | bytes, mode: str = "", - direction: str | None = None, + direction: Direction | None = None, features: list[str] | None = None, language: str | None = None, ) -> float: @@ -392,11 +421,11 @@ def getbbox( self, text: str | bytes, mode: str = "", - direction: str | None = None, + direction: Direction | None = None, features: list[str] | None = None, language: str | None = None, stroke_width: float = 0, - anchor: str | None = None, + anchor: Anchor | None = None, ) -> tuple[float, float, float, float]: """ Returns bounding box (in pixels) of given text relative to given anchor @@ -458,11 +487,11 @@ def getmask( self, text: str | bytes, mode: str = "", - direction: str | None = None, + direction: Direction | None = None, features: list[str] | None = None, language: str | None = None, stroke_width: float = 0, - anchor: str | None = None, + anchor: Anchor | None = None, ink: int = 0, start: tuple[float, float] | None = None, ) -> Image.core.ImagingCore: @@ -549,11 +578,11 @@ def getmask2( self, text: str | bytes, mode: str = "", - direction: str | None = None, + direction: Direction | None = None, features: list[str] | None = None, language: str | None = None, stroke_width: float = 0, - anchor: str | None = None, + anchor: Anchor | None = None, ink: int = 0, start: tuple[float, float] | None = None, *args: Any, diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 9cc9822f5b4..e32deec2e2b 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -24,11 +24,11 @@ class Font: string: str | bytes, fill: Callable[[int, int], _imaging.ImagingCore], mode: str, - dir: str | None, + dir: ImageFont.Direction | None, features: list[str] | None, lang: str | None, stroke_width: float, - anchor: str | None, + anchor: ImageFont.Anchor | None, foreground_ink_long: int, x_start: float, y_start: float, @@ -38,17 +38,17 @@ class Font: self, string: str | bytes | bytearray, mode: str, - dir: str | None, + dir: ImageFont.Direction | None, features: list[str] | None, lang: str | None, - anchor: str | None, + anchor: ImageFont.Anchor | None, /, ) -> tuple[tuple[int, int], tuple[int, int]]: ... def getlength( self, string: str | bytes, mode: str, - dir: str | None, + dir: ImageFont.Direction | None, features: list[str] | None, lang: str | None, /, diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 6aba6ebe910..d5955ac52d1 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -3,7 +3,7 @@ import os import sys from collections.abc import Sequence -from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeVar, Union +from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union if TYPE_CHECKING: from numbers import _IntegralLike as IntegralLike @@ -49,34 +49,4 @@ def read(self, length: int = ..., /) -> _T_co: ... StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] -Anchor = Literal[ - "la", - "lt", - "lm", - "ls", - "lb", - "ld", - "ma", - "mt", - "mm", - "ms", - "mb", - "md", - "ra", - "rt", - "rm", - "rs", - "rb", - "rd", - "sa", - "st", - "ss", - "sb", - "sd", -] - -Align = Literal["left", "center", "right"] - -Direction = Literal["rtl", "ltr", "ttb"] - __all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"]