Skip to content

Commit

Permalink
(WIP) add type hints for ImageFont and _imagingft
Browse files Browse the repository at this point in the history
  • Loading branch information
nulano committed Dec 27, 2023
1 parent 7b7d603 commit 59c28f7
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 46 deletions.
99 changes: 57 additions & 42 deletions src/PIL/ImageFont.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -45,7 +45,7 @@ class Layout(IntEnum):
RAQM = 1


MAX_STRING_LENGTH = 1_000_000
MAX_STRING_LENGTH: int | None = 1_000_000


try:
Expand All @@ -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)
Expand Down Expand Up @@ -189,6 +189,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,
Expand Down Expand Up @@ -253,22 +255,29 @@ 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
baseline to the lowest outline point, a negative value)
"""
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.
Expand Down Expand Up @@ -342,14 +351,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.
Expand Down Expand Up @@ -408,16 +417,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.
Expand Down Expand Up @@ -499,18 +508,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.
Expand Down Expand Up @@ -585,7 +594,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)
Expand Down Expand Up @@ -614,8 +623,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.
Expand All @@ -638,7 +652,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.
Expand All @@ -650,7 +664,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.
Expand All @@ -669,7 +683,7 @@ 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.
Expand All @@ -680,10 +694,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.
Expand Down
82 changes: 80 additions & 2 deletions src/PIL/_imagingft.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
4 changes: 2 additions & 2 deletions src/_imagingft.c
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = {
Expand Down Expand Up @@ -821,7 +821,7 @@ font_render(FontObject *self, PyObject *args) {

if (!PyArg_ParseTuple(
args,
"OO|zzOzizLffO:render",
"OO|zzOzizLff:render",
&string,
&fill,
&mode,
Expand Down

0 comments on commit 59c28f7

Please sign in to comment.