Skip to content

Commit c7aee34

Browse files
committed
Added YAFF font format support
Signed-off-by: Endre Szabo <git@end.re>
1 parent 852a832 commit c7aee34

7 files changed

Lines changed: 908 additions & 12 deletions

File tree

Tests/fonts/test_yaff.yaff

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Test YAFF font for Pillow test suite.
2+
# Contains a small set of ASCII glyphs with kerning.
3+
4+
name: Test Font 8px
5+
family: Test
6+
spacing: proportional
7+
ascent: 7
8+
descent: 2
9+
10+
default-char: 0x3f
11+
12+
u+0020:
13+
'A':
14+
.......
15+
.......
16+
.......
17+
.......
18+
.......
19+
.......
20+
.......
21+
.......
22+
.......
23+
24+
left-bearing: 0
25+
right-bearing: 0
26+
shift-up: -2
27+
28+
u+0041:
29+
.......
30+
.......
31+
.@@@@..
32+
@....@.
33+
@@@@@@.
34+
@....@.
35+
@....@.
36+
.......
37+
.......
38+
39+
left-bearing: 0
40+
right-bearing: 0
41+
shift-up: -2
42+
right-kerning:
43+
u+0056 -2
44+
u+0054 -1
45+
46+
u+0056:
47+
.......
48+
.......
49+
@....@.
50+
@....@.
51+
@....@.
52+
.@..@..
53+
..@@...
54+
.......
55+
.......
56+
57+
left-bearing: 0
58+
right-bearing: 0
59+
shift-up: -2
60+
left-kerning:
61+
u+0041 -1
62+
63+
u+0054:
64+
........
65+
........
66+
@@@@@@@@
67+
...@@...
68+
...@@...
69+
...@@...
70+
...@@...
71+
........
72+
........
73+
74+
left-bearing: 0
75+
right-bearing: 0
76+
shift-up: -2
77+
78+
u+002e:
79+
...
80+
...
81+
...
82+
...
83+
...
84+
...
85+
.@.
86+
...
87+
...
88+
89+
left-bearing: 0
90+
right-bearing: 0
91+
shift-up: -2
92+
93+
u+003f:
94+
.......
95+
.......
96+
.@@@@..
97+
@....@.
98+
..@@@..
99+
.......
100+
..@....
101+
.......
102+
.......
103+
104+
left-bearing: 0
105+
right-bearing: 0
106+
shift-up: -2

Tests/test_font_yaff.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
from PIL import Image, ImageDraw, ImageFont
6+
7+
FONT_PATH = str(Path(__file__).parent / "fonts" / "test_yaff.yaff")
8+
9+
10+
class TestYaffFont:
11+
def test_load_yaff(self) -> None:
12+
font = ImageFont.yaff(FONT_PATH)
13+
assert len(font.glyphs) > 0
14+
assert font.ascent == 7
15+
assert font.descent == 2
16+
17+
def test_load_yaff_pathlib(self) -> None:
18+
font = ImageFont.yaff(Path(FONT_PATH))
19+
assert len(font.glyphs) > 0
20+
21+
def test_load_yaff_fileobj(self) -> None:
22+
with open(FONT_PATH, "rb") as f:
23+
font = ImageFont.yaff(f)
24+
assert len(font.glyphs) > 0
25+
26+
def test_getmetrics(self) -> None:
27+
font = ImageFont.yaff(FONT_PATH)
28+
ascent, descent = font.getmetrics()
29+
assert ascent == 7
30+
assert descent == 2
31+
32+
def test_getlength(self) -> None:
33+
font = ImageFont.yaff(FONT_PATH)
34+
length = font.getlength("A")
35+
assert length > 0
36+
37+
def test_getlength_kerning(self) -> None:
38+
font = ImageFont.yaff(FONT_PATH)
39+
# AV has kerning: right-kerning on A for V is -2, left-kerning on V for A is -1
40+
length_av = font.getlength("AV")
41+
length_a = font.getlength("A")
42+
length_v = font.getlength("V")
43+
assert length_av < length_a + length_v
44+
assert length_av == length_a + length_v - 3 # -2 + -1 = -3
45+
46+
def test_getlength_kerning_at(self) -> None:
47+
font = ImageFont.yaff(FONT_PATH)
48+
# AT has kerning: right-kerning on A for T is -1
49+
length_at = font.getlength("AT")
50+
length_a = font.getlength("A")
51+
length_t = font.getlength("T")
52+
assert length_at < length_a + length_t
53+
assert length_at == length_a + length_t - 1
54+
55+
def test_getbbox(self) -> None:
56+
font = ImageFont.yaff(FONT_PATH)
57+
bbox = font.getbbox("A")
58+
assert bbox[0] == 0
59+
assert bbox[1] == 0
60+
assert bbox[2] > 0
61+
assert bbox[3] == font.ascent + font.descent
62+
63+
def test_getmask(self) -> None:
64+
font = ImageFont.yaff(FONT_PATH)
65+
mask = font.getmask("A")
66+
assert mask is not None
67+
assert mask.size[0] > 0
68+
assert mask.size[1] > 0
69+
70+
def test_getmask_mode_l(self) -> None:
71+
font = ImageFont.yaff(FONT_PATH)
72+
mask = font.getmask("A", mode="L")
73+
assert mask is not None
74+
75+
def test_getmask2(self) -> None:
76+
font = ImageFont.yaff(FONT_PATH)
77+
mask, offset = font.getmask2("A")
78+
assert mask is not None
79+
assert offset == (0, 0)
80+
81+
def test_render_text(self) -> None:
82+
font = ImageFont.yaff(FONT_PATH)
83+
im = Image.new("1", (50, 20))
84+
draw = ImageDraw.Draw(im)
85+
draw.text((5, 5), "AVA", fill=1, font=font)
86+
# Check that some pixels were drawn
87+
assert im.getbbox() is not None
88+
89+
def test_render_text_rgb(self) -> None:
90+
font = ImageFont.yaff(FONT_PATH)
91+
im = Image.new("RGB", (50, 20), "white")
92+
draw = ImageDraw.Draw(im)
93+
draw.text((5, 5), "A", fill="black", font=font)
94+
# Check some pixels changed from white
95+
pixels = list(im.get_flattened_data())
96+
assert any(p != (255, 255, 255) for p in pixels)
97+
98+
def test_default_char(self) -> None:
99+
font = ImageFont.yaff(FONT_PATH)
100+
# Character not in font should use default char (0x3f = '?')
101+
length_unknown = font.getlength("\u00ff")
102+
length_question = font.getlength("?")
103+
assert length_unknown == length_question
104+
105+
def test_empty_text(self) -> None:
106+
font = ImageFont.yaff(FONT_PATH)
107+
length = font.getlength("")
108+
assert length == 0
109+
110+
def test_bytes_text(self) -> None:
111+
font = ImageFont.yaff(FONT_PATH)
112+
length = font.getlength(b"A")
113+
assert length > 0
114+
115+
116+
class TestYaffFontParser:
117+
def test_parse_unicode_label(self) -> None:
118+
from PIL.YaffFontFile import _parse_label
119+
120+
assert _parse_label("u+0041") == [0x41]
121+
assert _parse_label("U+0041") == [0x41]
122+
123+
def test_parse_quoted_label(self) -> None:
124+
from PIL.YaffFontFile import _parse_label
125+
126+
assert _parse_label("'A'") == [0x41]
127+
128+
def test_parse_hex_codepoint(self) -> None:
129+
from PIL.YaffFontFile import _parse_label
130+
131+
assert _parse_label("0x41") == [0x41]
132+
133+
def test_parse_decimal_codepoint(self) -> None:
134+
from PIL.YaffFontFile import _parse_label
135+
136+
assert _parse_label("65") == [0x41]
137+
138+
def test_parse_tag_label(self) -> None:
139+
from PIL.YaffFontFile import _parse_label
140+
141+
assert _parse_label('"some_tag"') == []
142+
143+
def test_kerning_values(self) -> None:
144+
font = ImageFont.yaff(FONT_PATH)
145+
glyph_a = font.glyphs[0x41]
146+
assert 0x56 in glyph_a.right_kerning
147+
assert glyph_a.right_kerning[0x56] == -2
148+
assert 0x54 in glyph_a.right_kerning
149+
assert glyph_a.right_kerning[0x54] == -1
150+
151+
glyph_v = font.glyphs[0x56]
152+
assert 0x41 in glyph_v.left_kerning
153+
assert glyph_v.left_kerning[0x41] == -1

docs/releasenotes/12.2.0.rst

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,28 @@ introduced in Pillow 10.3.0.
1313

1414
The data being read is now limited to only the necessary amount.
1515

16-
Fix OOB write with invalid tile extents
17-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
16+
:cve:`2026-42311`: Fix OOB write with invalid tile extents
17+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1818

1919
Pillow 12.1.1 addressed :cve:`2026-25990` by improving checks for tile extents to
2020
prevent an OOB write from specially crafted PSD images in Pillow >= 10.3.0. However,
2121
these checks did not consider integer overflow. This has been corrected.
2222

23-
Prevent PDF parsing trailer infinite loop
24-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23+
:cve:`2026-42310`: Prevent PDF parsing trailer infinite loop
24+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2525

2626
When parsing a PDF, if a trailer refers to itself, or a more complex cyclic loop
2727
exists, then an infinite loop occurs. Pillow now keeps a record of which trailers it
2828
has already processed. PdfParser was added in Pillow 4.2.0.
2929

30-
Integer overflow when processing fonts
31-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
30+
:cve:`2026-42308`: Integer overflow when processing fonts
31+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3232

3333
If a font advances for each glyph by an exceeding large amount, when Pillow keeps track
3434
of the current position, it may lead to an integer overflow. This has been fixed.
3535

36-
Heap buffer overflow with nested list coordinates
37-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
36+
:cve:`2026-42309`: Heap buffer overflow with nested list coordinates
37+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3838

3939
Passing nested lists as coordinates to APIs that accept coordinates such as
4040
``ImagePath.Path``, :py:meth:`~PIL.ImageDraw.ImageDraw.polygon`

src/PIL/ImageDraw.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060

6161
class ImageDraw:
6262
font: (
63-
ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None
63+
ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | ImageFont.YaffImageFont | None
6464
) = None
6565

6666
def __init__(self, im: Image.Image, mode: str | None = None) -> None:
@@ -105,7 +105,7 @@ def __init__(self, im: Image.Image, mode: str | None = None) -> None:
105105

106106
def getfont(
107107
self,
108-
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
108+
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | ImageFont.YaffImageFont:
109109
"""
110110
Get the current default font.
111111
@@ -132,7 +132,7 @@ def getfont(
132132

133133
def _getfont(
134134
self, font_size: float | None
135-
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
135+
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | ImageFont.YaffImageFont:
136136
if font_size is not None:
137137
from . import ImageFont
138138

0 commit comments

Comments
 (0)