Added type hints

This commit is contained in:
Andrew Murray 2024-07-25 22:55:49 +10:00
parent 6dd4b3c826
commit 726cdf5eed
10 changed files with 223 additions and 158 deletions

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import os import os
from pathlib import Path from pathlib import Path
from typing import AnyStr
import pytest import pytest
@ -92,7 +93,7 @@ def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None:
def _test_high_characters( def _test_high_characters(
request: pytest.FixtureRequest, tmp_path: Path, message: str | bytes request: pytest.FixtureRequest, tmp_path: Path, message: AnyStr
) -> None: ) -> None:
tempname = save_font(request, tmp_path) tempname = save_font(request, tmp_path)
font = ImageFont.load(tempname) font = ImageFont.load(tempname)

View File

@ -717,14 +717,14 @@ def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None:
font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
_check_text(font, "Tests/images/variation_adobe.png", 11) _check_text(font, "Tests/images/variation_adobe.png", 11)
for name in ["Bold", b"Bold"]: for name in ("Bold", b"Bold"):
font.set_variation_by_name(name) font.set_variation_by_name(name)
assert font.getname()[1] == "Bold" assert font.getname()[1] == "Bold"
_check_text(font, "Tests/images/variation_adobe_name.png", 16) _check_text(font, "Tests/images/variation_adobe_name.png", 16)
font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
_check_text(font, "Tests/images/variation_tiny.png", 40) _check_text(font, "Tests/images/variation_tiny.png", 40)
for name in ["200", b"200"]: for name in ("200", b"200"):
font.set_variation_by_name(name) font.set_variation_by_name(name)
assert font.getname()[1] == "200" assert font.getname()[1] == "200"
_check_text(font, "Tests/images/variation_tiny_name.png", 40) _check_text(font, "Tests/images/variation_tiny_name.png", 40)

View File

@ -91,3 +91,11 @@ Constants
Set to 1,000,000, to protect against potential DOS attacks. Pillow will Set to 1,000,000, to protect against potential DOS attacks. Pillow will
raise a :py:exc:`ValueError` if the number of characters is over this limit. The 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``. check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``.
Dictionaries
------------
.. autoclass:: Axis
:members:
:undoc-members:
:show-inheritance:

View File

@ -78,3 +78,7 @@ on some Python versions.
An internal interface module previously known as :mod:`~PIL._imaging`, An internal interface module previously known as :mod:`~PIL._imaging`,
implemented in :file:`_imaging.c`. implemented in :file:`_imaging.c`.
.. py:class:: ImagingCore
A representation of the image data.

View File

@ -159,6 +159,4 @@ exclude = [
'^Tests/oss-fuzz/fuzz_font.py$', '^Tests/oss-fuzz/fuzz_font.py$',
'^Tests/oss-fuzz/fuzz_pillow.py$', '^Tests/oss-fuzz/fuzz_pillow.py$',
'^Tests/test_qt_image_qapplication.py$', '^Tests/test_qt_image_qapplication.py$',
'^Tests/test_font_pcf_charsets.py$',
'^Tests/test_font_pcf.py$',
] ]

View File

@ -218,6 +218,8 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# Registries # Registries
if TYPE_CHECKING: if TYPE_CHECKING:
from xml.etree.ElementTree import Element
from . import ImageFile, ImagePalette from . import ImageFile, ImagePalette
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
ID: list[str] = [] ID: list[str] = []
@ -241,9 +243,9 @@ ENCODERS: dict[str, type[ImageFile.PyEncoder]] = {}
_ENDIAN = "<" if sys.byteorder == "little" else ">" _ENDIAN = "<" if sys.byteorder == "little" else ">"
def _conv_type_shape(im): def _conv_type_shape(im: Image) -> tuple[tuple[int, ...], str]:
m = ImageMode.getmode(im.mode) m = ImageMode.getmode(im.mode)
shape = (im.height, im.width) shape: tuple[int, ...] = (im.height, im.width)
extra = len(m.bands) extra = len(m.bands)
if extra != 1: if extra != 1:
shape += (extra,) shape += (extra,)
@ -470,10 +472,10 @@ class _E:
self.scale = scale self.scale = scale
self.offset = offset self.offset = offset
def __neg__(self): def __neg__(self) -> _E:
return _E(-self.scale, -self.offset) return _E(-self.scale, -self.offset)
def __add__(self, other): def __add__(self, other) -> _E:
if isinstance(other, _E): if isinstance(other, _E):
return _E(self.scale + other.scale, self.offset + other.offset) return _E(self.scale + other.scale, self.offset + other.offset)
return _E(self.scale, self.offset + other) return _E(self.scale, self.offset + other)
@ -486,14 +488,14 @@ class _E:
def __rsub__(self, other): def __rsub__(self, other):
return other + -self return other + -self
def __mul__(self, other): def __mul__(self, other) -> _E:
if isinstance(other, _E): if isinstance(other, _E):
return NotImplemented return NotImplemented
return _E(self.scale * other, self.offset * other) return _E(self.scale * other, self.offset * other)
__rmul__ = __mul__ __rmul__ = __mul__
def __truediv__(self, other): def __truediv__(self, other) -> _E:
if isinstance(other, _E): if isinstance(other, _E):
return NotImplemented return NotImplemented
return _E(self.scale / other, self.offset / other) return _E(self.scale / other, self.offset / other)
@ -718,9 +720,9 @@ class Image:
return self._repr_image("JPEG") return self._repr_image("JPEG")
@property @property
def __array_interface__(self): def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]:
# numpy array interface support # numpy array interface support
new = {"version": 3} new: dict[str, str | bytes | int | tuple[int, ...]] = {"version": 3}
try: try:
if self.mode == "1": if self.mode == "1":
# Binary images need to be extended from bits to bytes # Binary images need to be extended from bits to bytes
@ -1418,7 +1420,7 @@ class Image:
return out return out
return self.im.getcolors(maxcolors) return self.im.getcolors(maxcolors)
def getdata(self, band: int | None = None): def getdata(self, band: int | None = None) -> core.ImagingCore:
""" """
Returns the contents of this image as a sequence object Returns the contents of this image as a sequence object
containing pixel values. The sequence object is flattened, so containing pixel values. The sequence object is flattened, so
@ -1467,8 +1469,8 @@ class Image:
def get_name(tag: str) -> str: def get_name(tag: str) -> str:
return re.sub("^{[^}]+}", "", tag) return re.sub("^{[^}]+}", "", tag)
def get_value(element): def get_value(element: Element) -> str | dict[str, Any] | None:
value = {get_name(k): v for k, v in element.attrib.items()} value: dict[str, Any] = {get_name(k): v for k, v in element.attrib.items()}
children = list(element) children = list(element)
if children: if children:
for child in children: for child in children:
@ -1712,7 +1714,7 @@ class Image:
return self.im.histogram(extrema) return self.im.histogram(extrema)
return self.im.histogram() return self.im.histogram()
def entropy(self, mask=None, extrema=None): def entropy(self, mask: Image | None = None, extrema=None):
""" """
Calculates and returns the entropy for the image. Calculates and returns the entropy for the image.
@ -1996,7 +1998,7 @@ class Image:
def putdata( def putdata(
self, self,
data: Sequence[float] | Sequence[Sequence[int]] | NumpyArray, data: Sequence[float] | Sequence[Sequence[int]] | core.ImagingCore | NumpyArray,
scale: float = 1.0, scale: float = 1.0,
offset: float = 0.0, offset: float = 0.0,
) -> None: ) -> None:
@ -2184,7 +2186,12 @@ class Image:
return m_im return m_im
def _get_safe_box(self, size, resample, box): def _get_safe_box(
self,
size: tuple[int, int],
resample: Resampling,
box: tuple[float, float, float, float],
) -> tuple[int, int, int, int]:
"""Expands the box so it includes adjacent pixels """Expands the box so it includes adjacent pixels
that may be used by resampling with the given resampling filter. that may be used by resampling with the given resampling filter.
""" """
@ -2294,7 +2301,7 @@ class Image:
factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1 factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1
factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1 factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1
if factor_x > 1 or factor_y > 1: if factor_x > 1 or factor_y > 1:
reduce_box = self._get_safe_box(size, resample, box) reduce_box = self._get_safe_box(size, cast(Resampling, resample), box)
factor = (factor_x, factor_y) factor = (factor_x, factor_y)
self = ( self = (
self.reduce(factor, box=reduce_box) self.reduce(factor, box=reduce_box)
@ -2430,7 +2437,7 @@ class Image:
0.0, 0.0,
] ]
def transform(x, y, matrix): def transform(x: float, y: float, matrix: list[float]) -> tuple[float, float]:
(a, b, c, d, e, f) = matrix (a, b, c, d, e, f) = matrix
return a * x + b * y + c, d * x + e * y + f return a * x + b * y + c, d * x + e * y + f
@ -2445,9 +2452,9 @@ class Image:
xx = [] xx = []
yy = [] yy = []
for x, y in ((0, 0), (w, 0), (w, h), (0, h)): for x, y in ((0, 0), (w, 0), (w, h), (0, h)):
x, y = transform(x, y, matrix) transformed_x, transformed_y = transform(x, y, matrix)
xx.append(x) xx.append(transformed_x)
yy.append(y) yy.append(transformed_y)
nw = math.ceil(max(xx)) - math.floor(min(xx)) nw = math.ceil(max(xx)) - math.floor(min(xx))
nh = math.ceil(max(yy)) - math.floor(min(yy)) nh = math.ceil(max(yy)) - math.floor(min(yy))
@ -2705,7 +2712,7 @@ class Image:
provided_size = tuple(map(math.floor, size)) provided_size = tuple(map(math.floor, size))
def preserve_aspect_ratio() -> tuple[int, int] | None: def preserve_aspect_ratio() -> tuple[int, int] | None:
def round_aspect(number, key): def round_aspect(number: float, key: Callable[[int], float]) -> int:
return max(min(math.floor(number), math.ceil(number), key=key), 1) return max(min(math.floor(number), math.ceil(number), key=key), 1)
x, y = provided_size x, y = provided_size
@ -2849,7 +2856,13 @@ class Image:
return im return im
def __transformer( def __transformer(
self, box, image, method, data, resample=Resampling.NEAREST, fill=1 self,
box: tuple[int, int, int, int],
image: Image,
method,
data,
resample: int = Resampling.NEAREST,
fill: bool = True,
): ):
w = box[2] - box[0] w = box[2] - box[0]
h = box[3] - box[1] h = box[3] - box[1]
@ -2899,11 +2912,12 @@ class Image:
Resampling.BICUBIC, Resampling.BICUBIC,
): ):
if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS):
msg = { unusable: dict[int, str] = {
Resampling.BOX: "Image.Resampling.BOX", Resampling.BOX: "Image.Resampling.BOX",
Resampling.HAMMING: "Image.Resampling.HAMMING", Resampling.HAMMING: "Image.Resampling.HAMMING",
Resampling.LANCZOS: "Image.Resampling.LANCZOS", Resampling.LANCZOS: "Image.Resampling.LANCZOS",
}[resample] + f" ({resample}) cannot be used." }
msg = unusable[resample] + f" ({resample}) cannot be used."
else: else:
msg = f"Unknown resampling filter ({resample})." msg = f"Unknown resampling filter ({resample})."
@ -3843,7 +3857,7 @@ class Exif(_ExifBase):
print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99
""" """
endian = None endian: str | None = None
bigtiff = False bigtiff = False
_loaded = False _loaded = False
@ -3892,7 +3906,7 @@ class Exif(_ExifBase):
head += b"\x00\x00\x00\x00" head += b"\x00\x00\x00\x00"
return head return head
def load(self, data): def load(self, data: bytes) -> None:
# Extract EXIF information. This is highly experimental, # Extract EXIF information. This is highly experimental,
# and is likely to be replaced with something better in a future # and is likely to be replaced with something better in a future
# version. # version.
@ -3911,7 +3925,7 @@ class Exif(_ExifBase):
self._info = None self._info = None
return return
self.fp = io.BytesIO(data) self.fp: IO[bytes] = io.BytesIO(data)
self.head = self.fp.read(8) self.head = self.fp.read(8)
# process dictionary # process dictionary
from . import TiffImagePlugin from . import TiffImagePlugin
@ -3921,7 +3935,7 @@ class Exif(_ExifBase):
self.fp.seek(self._info.next) self.fp.seek(self._info.next)
self._info.load(self.fp) self._info.load(self.fp)
def load_from_fp(self, fp, offset=None): def load_from_fp(self, fp: IO[bytes], offset: int | None = None) -> None:
self._loaded_exif = None self._loaded_exif = None
self._data.clear() self._data.clear()
self._hidden_data.clear() self._hidden_data.clear()

View File

@ -36,7 +36,7 @@ import numbers
import struct import struct
from collections.abc import Sequence from collections.abc import Sequence
from types import ModuleType from types import ModuleType
from typing import TYPE_CHECKING, AnyStr, Callable, Union, cast from typing import TYPE_CHECKING, Any, AnyStr, Callable, Union, cast
from . import Image, ImageColor from . import Image, ImageColor
from ._deprecate import deprecate from ._deprecate import deprecate
@ -561,7 +561,12 @@ class ImageDraw:
def _multiline_split(self, text: AnyStr) -> list[AnyStr]: def _multiline_split(self, text: AnyStr) -> list[AnyStr]:
return text.split("\n" if isinstance(text, str) else b"\n") return text.split("\n" if isinstance(text, str) else b"\n")
def _multiline_spacing(self, font, spacing, stroke_width): def _multiline_spacing(
self,
font: ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont,
spacing: float,
stroke_width: float,
) -> float:
return ( return (
self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3]
+ stroke_width + stroke_width
@ -571,25 +576,25 @@ class ImageDraw:
def text( def text(
self, self,
xy: tuple[float, float], xy: tuple[float, float],
text: str, text: AnyStr,
fill=None, fill: _Ink | None = None,
font: ( font: (
ImageFont.ImageFont ImageFont.ImageFont
| ImageFont.FreeTypeFont | ImageFont.FreeTypeFont
| ImageFont.TransposedFont | ImageFont.TransposedFont
| None | None
) = None, ) = None,
anchor=None, anchor: str | None = None,
spacing=4, spacing: float = 4,
align="left", align: str = "left",
direction=None, direction: str | None = None,
features=None, features: list[str] | None = None,
language=None, language: str | None = None,
stroke_width=0, stroke_width: float = 0,
stroke_fill=None, stroke_fill: _Ink | None = None,
embedded_color=False, embedded_color: bool = False,
*args, *args: Any,
**kwargs, **kwargs: Any,
) -> None: ) -> None:
"""Draw text.""" """Draw text."""
if embedded_color and self.mode not in ("RGB", "RGBA"): if embedded_color and self.mode not in ("RGB", "RGBA"):
@ -623,15 +628,14 @@ class ImageDraw:
return fill_ink return fill_ink
return ink return ink
def draw_text(ink, stroke_width=0) -> None: def draw_text(ink: int, stroke_width: float = 0) -> None:
mode = self.fontmode mode = self.fontmode
if stroke_width == 0 and embedded_color: if stroke_width == 0 and embedded_color:
mode = "RGBA" mode = "RGBA"
coord = [] coord = []
start = []
for i in range(2): for i in range(2):
coord.append(int(xy[i])) coord.append(int(xy[i]))
start.append(math.modf(xy[i])[0]) start = (math.modf(xy[0])[0], math.modf(xy[1])[0])
try: try:
mask, offset = font.getmask2( # type: ignore[union-attr,misc] mask, offset = font.getmask2( # type: ignore[union-attr,misc]
text, text,
@ -697,25 +701,25 @@ class ImageDraw:
def multiline_text( def multiline_text(
self, self,
xy: tuple[float, float], xy: tuple[float, float],
text: str, text: AnyStr,
fill=None, fill: _Ink | None = None,
font: ( font: (
ImageFont.ImageFont ImageFont.ImageFont
| ImageFont.FreeTypeFont | ImageFont.FreeTypeFont
| ImageFont.TransposedFont | ImageFont.TransposedFont
| None | None
) = None, ) = None,
anchor=None, anchor: str | None = None,
spacing=4, spacing: float = 4,
align="left", align: str = "left",
direction=None, direction: str | None = None,
features=None, features: list[str] | None = None,
language=None, language: str | None = None,
stroke_width=0, stroke_width: float = 0,
stroke_fill=None, stroke_fill: _Ink | None = None,
embedded_color=False, embedded_color: bool = False,
*, *,
font_size=None, font_size: float | None = None,
) -> None: ) -> None:
if direction == "ttb": if direction == "ttb":
msg = "ttb direction is unsupported for multiline text" msg = "ttb direction is unsupported for multiline text"
@ -788,19 +792,19 @@ class ImageDraw:
def textlength( def textlength(
self, self,
text: str, text: AnyStr,
font: ( font: (
ImageFont.ImageFont ImageFont.ImageFont
| ImageFont.FreeTypeFont | ImageFont.FreeTypeFont
| ImageFont.TransposedFont | ImageFont.TransposedFont
| None | None
) = None, ) = None,
direction=None, direction: str | None = None,
features=None, features: list[str] | None = None,
language=None, language: str | None = None,
embedded_color=False, embedded_color: bool = False,
*, *,
font_size=None, font_size: float | None = None,
) -> float: ) -> float:
"""Get the length of a given string, in pixels with 1/64 precision.""" """Get the length of a given string, in pixels with 1/64 precision."""
if self._multiline_check(text): if self._multiline_check(text):
@ -817,20 +821,25 @@ class ImageDraw:
def textbbox( def textbbox(
self, self,
xy, xy: tuple[float, float],
text, text: AnyStr,
font=None, font: (
anchor=None, ImageFont.ImageFont
spacing=4, | ImageFont.FreeTypeFont
align="left", | ImageFont.TransposedFont
direction=None, | None
features=None, ) = None,
language=None, anchor: str | None = None,
stroke_width=0, spacing: float = 4,
embedded_color=False, align: str = "left",
direction: str | None = None,
features: list[str] | None = None,
language: str | None = None,
stroke_width: float = 0,
embedded_color: bool = False,
*, *,
font_size=None, font_size: float | None = None,
) -> tuple[int, int, int, int]: ) -> tuple[float, float, float, float]:
"""Get the bounding box of a given string, in pixels.""" """Get the bounding box of a given string, in pixels."""
if embedded_color and self.mode not in ("RGB", "RGBA"): if embedded_color and self.mode not in ("RGB", "RGBA"):
msg = "Embedded color supported only in RGB and RGBA modes" msg = "Embedded color supported only in RGB and RGBA modes"
@ -862,20 +871,25 @@ class ImageDraw:
def multiline_textbbox( def multiline_textbbox(
self, self,
xy, xy: tuple[float, float],
text, text: AnyStr,
font=None, font: (
anchor=None, ImageFont.ImageFont
spacing=4, | ImageFont.FreeTypeFont
align="left", | ImageFont.TransposedFont
direction=None, | None
features=None, ) = None,
language=None, anchor: str | None = None,
stroke_width=0, spacing: float = 4,
embedded_color=False, align: str = "left",
direction: str | None = None,
features: list[str] | None = None,
language: str | None = None,
stroke_width: float = 0,
embedded_color: bool = False,
*, *,
font_size=None, font_size: float | None = None,
) -> tuple[int, int, int, int]: ) -> tuple[float, float, float, float]:
if direction == "ttb": if direction == "ttb":
msg = "ttb direction is unsupported for multiline text" msg = "ttb direction is unsupported for multiline text"
raise ValueError(msg) raise ValueError(msg)
@ -914,7 +928,7 @@ class ImageDraw:
elif anchor[1] == "d": elif anchor[1] == "d":
top -= (len(lines) - 1) * line_spacing top -= (len(lines) - 1) * line_spacing
bbox: tuple[int, int, int, int] | None = None bbox: tuple[float, float, float, float] | None = None
for idx, line in enumerate(lines): for idx, line in enumerate(lines):
left = xy[0] left = xy[0]

View File

@ -34,7 +34,7 @@ import warnings
from enum import IntEnum from enum import IntEnum
from io import BytesIO from io import BytesIO
from types import ModuleType from types import ModuleType
from typing import IO, TYPE_CHECKING, Any, BinaryIO from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict
from . import Image from . import Image
from ._typing import StrOrBytesPath from ._typing import StrOrBytesPath
@ -46,6 +46,13 @@ if TYPE_CHECKING:
from ._imagingft import Font from ._imagingft import Font
class Axis(TypedDict):
minimum: int | None
default: int | None
maximum: int | None
name: bytes | None
class Layout(IntEnum): class Layout(IntEnum):
BASIC = 0 BASIC = 0
RAQM = 1 RAQM = 1
@ -138,7 +145,9 @@ class ImageFont:
self.font = Image.core.font(image.im, data) self.font = Image.core.font(image.im, data)
def getmask(self, text, mode="", *args, **kwargs): def getmask(
self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any
) -> Image.core.ImagingCore:
""" """
Create a bitmap for the text. Create a bitmap for the text.
@ -236,7 +245,7 @@ class FreeTypeFont:
self.layout_engine = layout_engine self.layout_engine = layout_engine
def load_from_bytes(f): def load_from_bytes(f) -> None:
self.font_bytes = f.read() self.font_bytes = f.read()
self.font = core.getfont( self.font = core.getfont(
"", size, index, encoding, self.font_bytes, layout_engine "", size, index, encoding, self.font_bytes, layout_engine
@ -283,7 +292,12 @@ class FreeTypeFont:
return self.font.ascent, self.font.descent return self.font.ascent, self.font.descent
def getlength( def getlength(
self, text: str | bytes, mode="", direction=None, features=None, language=None self,
text: str | bytes,
mode: str = "",
direction: str | None = None,
features: list[str] | None = None,
language: str | None = None,
) -> float: ) -> float:
""" """
Returns length (in pixels with 1/64 precision) of given text when rendered Returns length (in pixels with 1/64 precision) of given text when rendered
@ -424,16 +438,16 @@ class FreeTypeFont:
def getmask( def getmask(
self, self,
text, text: str | bytes,
mode="", mode: str = "",
direction=None, direction: str | None = None,
features=None, features: list[str] | None = None,
language=None, language: str | None = None,
stroke_width=0, stroke_width: float = 0,
anchor=None, anchor: str | None = None,
ink=0, ink: int = 0,
start=None, start: tuple[float, float] | None = None,
): ) -> Image.core.ImagingCore:
""" """
Create a bitmap for the text. Create a bitmap for the text.
@ -516,17 +530,17 @@ class FreeTypeFont:
def getmask2( def getmask2(
self, self,
text: str | bytes, text: str | bytes,
mode="", mode: str = "",
direction=None, direction: str | None = None,
features=None, features: list[str] | None = None,
language=None, language: str | None = None,
stroke_width=0, stroke_width: float = 0,
anchor=None, anchor: str | None = None,
ink=0, ink: int = 0,
start=None, start: tuple[float, float] | None = None,
*args, *args: Any,
**kwargs, **kwargs: Any,
): ) -> tuple[Image.core.ImagingCore, tuple[int, int]]:
""" """
Create a bitmap for the text. Create a bitmap for the text.
@ -599,7 +613,7 @@ class FreeTypeFont:
if start is None: if start is None:
start = (0, 0) start = (0, 0)
def fill(width, height): def fill(width: int, height: int) -> Image.core.ImagingCore:
size = (width, height) size = (width, height)
Image._decompression_bomb_check(size) Image._decompression_bomb_check(size)
return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) return Image.core.fill("RGBA" if mode == "RGBA" else "L", size)
@ -619,8 +633,13 @@ class FreeTypeFont:
) )
def font_variant( def font_variant(
self, font=None, size=None, index=None, encoding=None, layout_engine=None self,
): font: StrOrBytesPath | 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, Create a copy of this FreeTypeFont object,
using any specified arguments to override the settings. using any specified arguments to override the settings.
@ -655,7 +674,7 @@ class FreeTypeFont:
raise NotImplementedError(msg) from e raise NotImplementedError(msg) from e
return [name.replace(b"\x00", b"") for name in names] 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) -> None:
""" """
:param name: The name of the style. :param name: The name of the style.
:exception OSError: If the font is not a variation font. :exception OSError: If the font is not a variation font.
@ -674,7 +693,7 @@ class FreeTypeFont:
self.font.setvarname(index) self.font.setvarname(index)
def get_variation_axes(self): def get_variation_axes(self) -> list[Axis]:
""" """
:returns: A list of the axes in a variation font. :returns: A list of the axes in a variation font.
:exception OSError: If the font is not a variation font. :exception OSError: If the font is not a variation font.
@ -704,7 +723,9 @@ class FreeTypeFont:
class TransposedFont: class TransposedFont:
"""Wrapper for writing rotated or mirrored text""" """Wrapper for writing rotated or mirrored text"""
def __init__(self, font, orientation=None): def __init__(
self, font: ImageFont | FreeTypeFont, orientation: Image.Transpose | None = None
):
""" """
Wrapper that creates a transposed font from any existing font Wrapper that creates a transposed font from any existing font
object. object.
@ -718,13 +739,17 @@ class TransposedFont:
self.font = font self.font = font
self.orientation = orientation # any 'transpose' argument, or None self.orientation = orientation # any 'transpose' argument, or None
def getmask(self, text, mode="", *args, **kwargs): def getmask(
self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any
) -> Image.core.ImagingCore:
im = self.font.getmask(text, mode, *args, **kwargs) im = self.font.getmask(text, mode, *args, **kwargs)
if self.orientation is not None: if self.orientation is not None:
return im.transpose(self.orientation) return im.transpose(self.orientation)
return im return im
def getbbox(self, text, *args, **kwargs): def getbbox(
self, text: str | bytes, *args: Any, **kwargs: Any
) -> tuple[int, int, float, float]:
# TransposedFont doesn't support getmask2, move top-left point to (0, 0) # TransposedFont doesn't support getmask2, move top-left point to (0, 0)
# this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont # this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont
left, top, right, bottom = self.font.getbbox(text, *args, **kwargs) left, top, right, bottom = self.font.getbbox(text, *args, **kwargs)
@ -734,7 +759,7 @@ class TransposedFont:
return 0, 0, height, width return 0, 0, height, width
return 0, 0, width, height return 0, 0, width, height
def getlength(self, text: str | bytes, *args, **kwargs) -> float: def getlength(self, text: str | bytes, *args: Any, **kwargs: Any) -> float:
if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): 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" msg = "text length is undefined for text rotated by 90 or 270 degrees"
raise ValueError(msg) raise ValueError(msg)

View File

@ -1,6 +1,7 @@
from typing import Any from typing import Any
class ImagingCore: class ImagingCore:
def __getitem__(self, index: int) -> float: ...
def __getattr__(self, name: str) -> Any: ... def __getattr__(self, name: str) -> Any: ...
class ImagingFont: class ImagingFont:

View File

@ -1,12 +1,6 @@
from typing import Any, TypedDict from typing import Any, Callable
from . import _imaging from . import ImageFont, _imaging
class _Axis(TypedDict):
minimum: int | None
default: int | None
maximum: int | None
name: bytes | None
class Font: class Font:
@property @property
@ -28,42 +22,48 @@ class Font:
def render( def render(
self, self,
string: str | bytes, string: str | bytes,
fill, fill: Callable[[int, int], _imaging.ImagingCore],
mode=..., mode: str,
dir=..., dir: str | None,
features=..., features: list[str] | None,
lang=..., lang: str | None,
stroke_width=..., stroke_width: float,
anchor=..., anchor: str | None,
foreground_ink_long=..., foreground_ink_long: int,
x_start=..., x_start: float,
y_start=..., y_start: float,
/, /,
) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ... ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ...
def getsize( def getsize(
self, self,
string: str | bytes | bytearray, string: str | bytes | bytearray,
mode=..., mode: str,
dir=..., dir: str | None,
features=..., features: list[str] | None,
lang=..., lang: str | None,
anchor=..., anchor: str | None,
/, /,
) -> tuple[tuple[int, int], tuple[int, int]]: ... ) -> tuple[tuple[int, int], tuple[int, int]]: ...
def getlength( def getlength(
self, string: str | bytes, mode=..., dir=..., features=..., lang=..., / self,
string: str | bytes,
mode: str,
dir: str | None,
features: list[str] | None,
lang: str | None,
/,
) -> float: ... ) -> float: ...
def getvarnames(self) -> list[bytes]: ... def getvarnames(self) -> list[bytes]: ...
def getvaraxes(self) -> list[_Axis] | None: ... def getvaraxes(self) -> list[ImageFont.Axis]: ...
def setvarname(self, instance_index: int, /) -> None: ... def setvarname(self, instance_index: int, /) -> None: ...
def setvaraxes(self, axes: list[float], /) -> None: ... def setvaraxes(self, axes: list[float], /) -> None: ...
def getfont( def getfont(
filename: str | bytes, filename: str | bytes,
size: float, size: float,
index=..., index: int,
encoding=..., encoding: str,
font_bytes=..., font_bytes: bytes,
layout_engine=..., layout_engine: int,
) -> Font: ... ) -> Font: ...
def __getattr__(name: str) -> Any: ... def __getattr__(name: str) -> Any: ...