Pillow

Форк
0
/
test_imagefont.py 
1149 строк · 35.5 Кб
1
from __future__ import annotations
2

3
import copy
4
import os
5
import re
6
import shutil
7
import sys
8
from io import BytesIO
9
from pathlib import Path
10
from typing import Any, BinaryIO
11

12
import pytest
13
from packaging.version import parse as parse_version
14

15
from PIL import Image, ImageDraw, ImageFont, features
16
from PIL._typing import StrOrBytesPath
17

18
from .helper import (
19
    assert_image_equal,
20
    assert_image_equal_tofile,
21
    assert_image_similar_tofile,
22
    is_win32,
23
    skip_unless_feature,
24
    skip_unless_feature_version,
25
)
26

27
FONT_PATH = "Tests/fonts/FreeMono.ttf"
28
FONT_SIZE = 20
29

30
TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward"
31

32

33
pytestmark = skip_unless_feature("freetype2")
34

35

36
def test_sanity() -> None:
37
    version = features.version_module("freetype2")
38
    assert version is not None
39
    assert re.search(r"\d+\.\d+\.\d+$", version)
40

41

42
@pytest.fixture(
43
    scope="module",
44
    params=[
45
        pytest.param(ImageFont.Layout.BASIC),
46
        pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")),
47
    ],
48
)
49
def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout:
50
    return request.param
51

52

53
@pytest.fixture(scope="module")
54
def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont:
55
    return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine)
56

57

58
def test_font_properties(font: ImageFont.FreeTypeFont) -> None:
59
    assert font.path == FONT_PATH
60
    assert font.size == FONT_SIZE
61

62
    font_copy = font.font_variant()
63
    assert font_copy.path == FONT_PATH
64
    assert font_copy.size == FONT_SIZE
65

66
    font_copy = font.font_variant(size=FONT_SIZE + 1)
67
    assert font_copy.size == FONT_SIZE + 1
68

69
    second_font_path = "Tests/fonts/DejaVuSans/DejaVuSans.ttf"
70
    font_copy = font.font_variant(font=second_font_path)
71
    assert font_copy.path == second_font_path
72

73

74
def _render(
75
    font: StrOrBytesPath | BinaryIO, layout_engine: ImageFont.Layout
76
) -> Image.Image:
77
    txt = "Hello World!"
78
    ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine)
79
    ttf.getbbox(txt)
80

81
    img = Image.new("RGB", (256, 64), "white")
82
    d = ImageDraw.Draw(img)
83
    d.text((10, 10), txt, font=ttf, fill="black")
84

85
    return img
86

87

88
@pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH)))
89
def test_font_with_name(layout_engine: ImageFont.Layout, font: str | Path) -> None:
90
    _render(font, layout_engine)
91

92

93
def test_font_with_filelike(layout_engine: ImageFont.Layout) -> None:
94
    def _font_as_bytes() -> BytesIO:
95
        with open(FONT_PATH, "rb") as f:
96
            font_bytes = BytesIO(f.read())
97
        return font_bytes
98

99
    ttf = ImageFont.truetype(_font_as_bytes(), FONT_SIZE, layout_engine=layout_engine)
100
    ttf_copy = ttf.font_variant()
101
    assert ttf_copy.font_bytes == ttf.font_bytes
102

103
    _render(_font_as_bytes(), layout_engine)
104
    # Usage note:  making two fonts from the same buffer fails.
105
    # shared_bytes = _font_as_bytes()
106
    # _render(shared_bytes)
107
    # with pytest.raises(Exception):
108
    #   _render(shared_bytes)
109

110

111
def test_font_with_open_file(layout_engine: ImageFont.Layout) -> None:
112
    with open(FONT_PATH, "rb") as f:
113
        _render(f, layout_engine)
114

115

116
def test_render_equal(layout_engine: ImageFont.Layout) -> None:
117
    img_path = _render(FONT_PATH, layout_engine)
118
    with open(FONT_PATH, "rb") as f:
119
        font_filelike = BytesIO(f.read())
120
    img_filelike = _render(font_filelike, layout_engine)
121

122
    assert_image_equal(img_path, img_filelike)
123

124

125
def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None:
126
    tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf"))
127
    try:
128
        shutil.copy(FONT_PATH, tempfile)
129
    except UnicodeEncodeError:
130
        pytest.skip("Non-ASCII path could not be created")
131

132
    ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine)
133

134

135
def test_transparent_background(font: ImageFont.FreeTypeFont) -> None:
136
    im = Image.new(mode="RGBA", size=(300, 100))
137
    draw = ImageDraw.Draw(im)
138

139
    txt = "Hello World!"
140
    draw.text((10, 10), txt, font=font)
141

142
    target = "Tests/images/transparent_background_text.png"
143
    assert_image_similar_tofile(im, target, 4.09)
144

145
    target = "Tests/images/transparent_background_text_L.png"
146
    assert_image_similar_tofile(im.convert("L"), target, 0.01)
147

148

149
def test_I16(font: ImageFont.FreeTypeFont) -> None:
150
    im = Image.new(mode="I;16", size=(300, 100))
151
    draw = ImageDraw.Draw(im)
152

153
    txt = "Hello World!"
154
    draw.text((10, 10), txt, fill=0xFFFE, font=font)
155

156
    assert im.getpixel((12, 14)) == 0xFFFE
157

158
    target = "Tests/images/transparent_background_text_L.png"
159
    assert_image_similar_tofile(im.convert("L"), target, 0.01)
160

161

162
def test_textbbox_equal(font: ImageFont.FreeTypeFont) -> None:
163
    im = Image.new(mode="RGB", size=(300, 100))
164
    draw = ImageDraw.Draw(im)
165

166
    txt = "Hello World!"
167
    bbox = draw.textbbox((10, 10), txt, font)
168
    draw.text((10, 10), txt, font=font)
169
    draw.rectangle(bbox)
170

171
    assert_image_similar_tofile(im, "Tests/images/rectangle_surrounding_text.png", 2.5)
172

173

174
@pytest.mark.parametrize(
175
    "text, mode, fontname, size, length_basic, length_raqm",
176
    (
177
        # basic test
178
        ("text", "L", "FreeMono.ttf", 15, 36, 36),
179
        ("text", "1", "FreeMono.ttf", 15, 36, 36),
180
        # issue 4177
181
        ("rrr", "L", "DejaVuSans/DejaVuSans.ttf", 18, 21, 22.21875),
182
        ("rrr", "1", "DejaVuSans/DejaVuSans.ttf", 18, 24, 22.21875),
183
        # test 'l' not including extra margin
184
        # using exact value 2047 / 64 for raqm, checked with debugger
185
        ("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375),
186
        ("ill", "1", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375),
187
    ),
188
)
189
def test_getlength(
190
    text: str,
191
    mode: str,
192
    fontname: str,
193
    size: int,
194
    layout_engine: ImageFont.Layout,
195
    length_basic: int,
196
    length_raqm: float,
197
) -> None:
198
    f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine)
199

200
    im = Image.new(mode, (1, 1), 0)
201
    d = ImageDraw.Draw(im)
202

203
    if layout_engine == ImageFont.Layout.BASIC:
204
        length = d.textlength(text, f)
205
        assert length == length_basic
206
    else:
207
        # disable kerning, kerning metrics changed
208
        length = d.textlength(text, f, features=["-kern"])
209
        assert length == length_raqm
210

211

212
def test_float_size(layout_engine: ImageFont.Layout) -> None:
213
    lengths = []
214
    for size in (48, 48.5, 49):
215
        f = ImageFont.truetype(
216
            "Tests/fonts/NotoSans-Regular.ttf", size, layout_engine=layout_engine
217
        )
218
        lengths.append(f.getlength("text"))
219
    assert lengths[0] != lengths[1] != lengths[2]
220

221

222
def test_render_multiline(font: ImageFont.FreeTypeFont) -> None:
223
    im = Image.new(mode="RGB", size=(300, 100))
224
    draw = ImageDraw.Draw(im)
225
    line_spacing = font.getbbox("A")[3] + 4
226
    lines = TEST_TEXT.split("\n")
227
    y: float = 0
228
    for line in lines:
229
        draw.text((0, y), line, font=font)
230
        y += line_spacing
231

232
    # some versions of freetype have different horizontal spacing.
233
    # setting a tight epsilon, I'm showing the original test failure
234
    # at epsilon = ~38.
235
    assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2)
236

237

238
def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None:
239
    # Test that text() correctly connects to multiline_text()
240
    # and that align defaults to left
241
    im = Image.new(mode="RGB", size=(300, 100))
242
    draw = ImageDraw.Draw(im)
243
    draw.text((0, 0), TEST_TEXT, font=font)
244

245
    assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 0.01)
246

247
    # Test that text() can pass on additional arguments
248
    # to multiline_text()
249
    draw.text(
250
        (0, 0), TEST_TEXT, fill=None, font=font, anchor=None, spacing=4, align="left"
251
    )
252
    draw.text((0, 0), TEST_TEXT, None, font, None, 4, "left")
253

254

255
@pytest.mark.parametrize(
256
    "align, ext", (("left", ""), ("center", "_center"), ("right", "_right"))
257
)
258
def test_render_multiline_text_align(
259
    font: ImageFont.FreeTypeFont, align: str, ext: str
260
) -> None:
261
    im = Image.new(mode="RGB", size=(300, 100))
262
    draw = ImageDraw.Draw(im)
263
    draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align)
264

265
    assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01)
266

267

268
def test_unknown_align(font: ImageFont.FreeTypeFont) -> None:
269
    im = Image.new(mode="RGB", size=(300, 100))
270
    draw = ImageDraw.Draw(im)
271

272
    # Act/Assert
273
    with pytest.raises(ValueError):
274
        draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown")
275

276

277
def test_draw_align(font: ImageFont.FreeTypeFont) -> None:
278
    im = Image.new("RGB", (300, 100), "white")
279
    draw = ImageDraw.Draw(im)
280
    line = "some text"
281
    draw.text((100, 40), line, (0, 0, 0), font=font, align="left")
282

283

284
def test_multiline_bbox(font: ImageFont.FreeTypeFont) -> None:
285
    im = Image.new(mode="RGB", size=(300, 100))
286
    draw = ImageDraw.Draw(im)
287

288
    # Test that textbbox() correctly connects to multiline_textbbox()
289
    assert draw.textbbox((0, 0), TEST_TEXT, font=font) == draw.multiline_textbbox(
290
        (0, 0), TEST_TEXT, font=font
291
    )
292

293
    # Test that multiline_textbbox corresponds to ImageFont.textbbox()
294
    # for single line text
295
    assert font.getbbox("A") == draw.multiline_textbbox((0, 0), "A", font=font)
296

297
    # Test that textbbox() can pass on additional arguments
298
    # to multiline_textbbox()
299
    draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4)
300

301

302
def test_multiline_width(font: ImageFont.FreeTypeFont) -> None:
303
    im = Image.new(mode="RGB", size=(300, 100))
304
    draw = ImageDraw.Draw(im)
305

306
    assert (
307
        draw.textbbox((0, 0), "longest line", font=font)[2]
308
        == draw.multiline_textbbox((0, 0), "longest line\nline", font=font)[2]
309
    )
310

311

312
def test_multiline_spacing(font: ImageFont.FreeTypeFont) -> None:
313
    im = Image.new(mode="RGB", size=(300, 100))
314
    draw = ImageDraw.Draw(im)
315
    draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10)
316

317
    assert_image_similar_tofile(im, "Tests/images/multiline_text_spacing.png", 2.5)
318

319

320
@pytest.mark.parametrize(
321
    "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
322
)
323
def test_rotated_transposed_font(
324
    font: ImageFont.FreeTypeFont, orientation: Image.Transpose
325
) -> None:
326
    img_gray = Image.new("L", (100, 100))
327
    draw = ImageDraw.Draw(img_gray)
328
    word = "testing"
329

330
    transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
331

332
    # Original font
333
    draw.font = font
334
    bbox_a = draw.textbbox((10, 10), word)
335

336
    # Rotated font
337
    draw.font = transposed_font
338
    bbox_b = draw.textbbox((20, 20), word)
339

340
    # Check (w, h) of box a is (h, w) of box b
341
    assert (
342
        bbox_a[2] - bbox_a[0],
343
        bbox_a[3] - bbox_a[1],
344
    ) == (
345
        bbox_b[3] - bbox_b[1],
346
        bbox_b[2] - bbox_b[0],
347
    )
348

349
    # Check top left co-ordinates are correct
350
    assert bbox_b[:2] == (20, 20)
351

352
    # text length is undefined for vertical text
353
    with pytest.raises(ValueError):
354
        draw.textlength(word)
355

356

357
@pytest.mark.parametrize(
358
    "orientation",
359
    (
360
        None,
361
        Image.Transpose.ROTATE_180,
362
        Image.Transpose.FLIP_LEFT_RIGHT,
363
        Image.Transpose.FLIP_TOP_BOTTOM,
364
    ),
365
)
366
def test_unrotated_transposed_font(
367
    font: ImageFont.FreeTypeFont, orientation: Image.Transpose
368
) -> None:
369
    img_gray = Image.new("L", (100, 100))
370
    draw = ImageDraw.Draw(img_gray)
371
    word = "testing"
372

373
    transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
374

375
    # Original font
376
    draw.font = font
377
    bbox_a = draw.textbbox((10, 10), word)
378
    length_a = draw.textlength(word)
379

380
    # Rotated font
381
    draw.font = transposed_font
382
    bbox_b = draw.textbbox((20, 20), word)
383
    length_b = draw.textlength(word)
384

385
    # Check boxes a and b are same size
386
    assert (
387
        bbox_a[2] - bbox_a[0],
388
        bbox_a[3] - bbox_a[1],
389
    ) == (
390
        bbox_b[2] - bbox_b[0],
391
        bbox_b[3] - bbox_b[1],
392
    )
393

394
    # Check top left co-ordinates are correct
395
    assert bbox_b[:2] == (20, 20)
396

397
    assert length_a == length_b
398

399

400
@pytest.mark.parametrize(
401
    "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
402
)
403
def test_rotated_transposed_font_get_mask(
404
    font: ImageFont.FreeTypeFont, orientation: Image.Transpose
405
) -> None:
406
    # Arrange
407
    text = "mask this"
408
    transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
409

410
    # Act
411
    mask = transposed_font.getmask(text)
412

413
    # Assert
414
    assert mask.size == (13, 108)
415

416

417
@pytest.mark.parametrize(
418
    "orientation",
419
    (
420
        None,
421
        Image.Transpose.ROTATE_180,
422
        Image.Transpose.FLIP_LEFT_RIGHT,
423
        Image.Transpose.FLIP_TOP_BOTTOM,
424
    ),
425
)
426
def test_unrotated_transposed_font_get_mask(
427
    font: ImageFont.FreeTypeFont, orientation: Image.Transpose
428
) -> None:
429
    # Arrange
430
    text = "mask this"
431
    transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
432

433
    # Act
434
    mask = transposed_font.getmask(text)
435

436
    # Assert
437
    assert mask.size == (108, 13)
438

439

440
def test_free_type_font_get_name(font: ImageFont.FreeTypeFont) -> None:
441
    assert ("FreeMono", "Regular") == font.getname()
442

443

444
def test_free_type_font_get_metrics(font: ImageFont.FreeTypeFont) -> None:
445
    ascent, descent = font.getmetrics()
446

447
    assert isinstance(ascent, int)
448
    assert isinstance(descent, int)
449
    assert (ascent, descent) == (16, 4)
450

451

452
def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None:
453
    # Arrange
454
    text = "mask this"
455

456
    # Act
457
    mask = font.getmask(text)
458

459
    # Assert
460
    assert mask.size == (108, 13)
461

462

463
def test_load_path_not_found() -> None:
464
    # Arrange
465
    filename = "somefilenamethatdoesntexist.ttf"
466

467
    # Act/Assert
468
    with pytest.raises(OSError):
469
        ImageFont.load_path(filename)
470
    with pytest.raises(OSError):
471
        ImageFont.truetype(filename)
472

473

474
def test_load_non_font_bytes() -> None:
475
    with open("Tests/images/hopper.jpg", "rb") as f:
476
        with pytest.raises(OSError):
477
            ImageFont.truetype(f)
478

479

480
def test_default_font() -> None:
481
    # Arrange
482
    txt = "This is a default font using FreeType support."
483
    im = Image.new(mode="RGB", size=(300, 100))
484
    draw = ImageDraw.Draw(im)
485

486
    # Act
487
    default_font = ImageFont.load_default()
488
    draw.text((10, 10), txt, font=default_font)
489

490
    larger_default_font = ImageFont.load_default(size=14)
491
    draw.text((10, 60), txt, font=larger_default_font)
492

493
    # Assert
494
    assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
495

496

497
@pytest.mark.parametrize("mode", ("", "1", "RGBA"))
498
def test_getbbox(font: ImageFont.FreeTypeFont, mode: str) -> None:
499
    assert (0, 4, 12, 16) == font.getbbox("A", mode)
500

501

502
def test_getbbox_empty(font: ImageFont.FreeTypeFont) -> None:
503
    # issue #2614, should not crash.
504
    assert (0, 0, 0, 0) == font.getbbox("")
505

506

507
def test_render_empty(font: ImageFont.FreeTypeFont) -> None:
508
    # issue 2666
509
    im = Image.new(mode="RGB", size=(300, 100))
510
    target = im.copy()
511
    draw = ImageDraw.Draw(im)
512
    # should not crash here.
513
    draw.text((10, 10), "", font=font)
514
    assert_image_equal(im, target)
515

516

517
def test_unicode_extended(layout_engine: ImageFont.Layout) -> None:
518
    # issue #3777
519
    text = "A\u278A\U0001F12B"
520
    target = "Tests/images/unicode_extended.png"
521

522
    ttf = ImageFont.truetype(
523
        "Tests/fonts/NotoSansSymbols-Regular.ttf",
524
        FONT_SIZE,
525
        layout_engine=layout_engine,
526
    )
527
    img = Image.new("RGB", (100, 60))
528
    d = ImageDraw.Draw(img)
529
    d.text((10, 10), text, font=ttf)
530

531
    # fails with 14.7
532
    assert_image_similar_tofile(img, target, 6.2)
533

534

535
@pytest.mark.parametrize(
536
    "platform, font_directory",
537
    (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")),
538
)
539
@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
540
def test_find_font(
541
    monkeypatch: pytest.MonkeyPatch, platform: str, font_directory: str
542
) -> None:
543
    def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None:
544
        # Make a copy of FreeTypeFont so we can patch the original
545
        free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)
546
        with monkeypatch.context() as m:
547
            m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False)
548

549
            def loadable_font(
550
                filepath: str, size: int, index: int, encoding: str, *args: Any
551
            ) -> ImageFont.FreeTypeFont:
552
                _freeTypeFont = getattr(ImageFont, "_FreeTypeFont")
553
                if filepath == path_to_fake:
554
                    return _freeTypeFont(FONT_PATH, size, index, encoding, *args)
555
                return _freeTypeFont(filepath, size, index, encoding, *args)
556

557
            m.setattr(ImageFont, "FreeTypeFont", loadable_font)
558
            font = ImageFont.truetype(fontname)
559
            # Make sure it's loaded
560
            name = font.getname()
561
            assert ("FreeMono", "Regular") == name
562

563
    # A lot of mocking here - this is more for hitting code and
564
    # catching syntax like errors
565
    monkeypatch.setattr(sys, "platform", platform)
566
    if platform == "linux":
567
        monkeypatch.setenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
568
        monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
569

570
    def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]:
571
        if path == font_directory:
572
            return [
573
                (
574
                    path,
575
                    [],
576
                    ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"],
577
                )
578
            ]
579
        return [(path, [], ["some_random_font.ttf"])]
580

581
    monkeypatch.setattr(os, "walk", fake_walker)
582

583
    # Test that the font loads both with and without the extension
584
    _test_fake_loading_font(font_directory + "/Arial.ttf", "Arial.ttf")
585
    _test_fake_loading_font(font_directory + "/Arial.ttf", "Arial")
586

587
    # Test that non-ttf fonts can be found without the extension
588
    _test_fake_loading_font(font_directory + "/Single.otf", "Single")
589

590
    # Test that ttf fonts are preferred if the extension is not specified
591
    _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate")
592

593

594
def test_imagefont_getters(font: ImageFont.FreeTypeFont) -> None:
595
    assert font.getmetrics() == (16, 4)
596
    assert font.font.ascent == 16
597
    assert font.font.descent == 4
598
    assert font.font.height == 20
599
    assert font.font.x_ppem == 20
600
    assert font.font.y_ppem == 20
601
    assert font.font.glyphs == 4177
602
    assert font.getbbox("A") == (0, 4, 12, 16)
603
    assert font.getbbox("AB") == (0, 4, 24, 16)
604
    assert font.getbbox("M") == (0, 4, 12, 16)
605
    assert font.getbbox("y") == (0, 7, 12, 20)
606
    assert font.getbbox("a") == (0, 7, 12, 16)
607
    assert font.getlength("A") == 12
608
    assert font.getlength("AB") == 24
609
    assert font.getlength("M") == 12
610
    assert font.getlength("y") == 12
611
    assert font.getlength("a") == 12
612

613

614
@pytest.mark.parametrize("stroke_width", (0, 2))
615
def test_getsize_stroke(font: ImageFont.FreeTypeFont, stroke_width: int) -> None:
616
    assert font.getbbox("A", stroke_width=stroke_width) == (
617
        0 - stroke_width,
618
        4 - stroke_width,
619
        12 + stroke_width,
620
        16 + stroke_width,
621
    )
622

623

624
def test_complex_font_settings() -> None:
625
    t = ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.BASIC)
626
    with pytest.raises(KeyError):
627
        t.getmask("абвг", direction="rtl")
628
    with pytest.raises(KeyError):
629
        t.getmask("абвг", features=["-kern"])
630
    with pytest.raises(KeyError):
631
        t.getmask("абвг", language="sr")
632

633

634
def test_variation_get(font: ImageFont.FreeTypeFont) -> None:
635
    version = features.version_module("freetype2")
636
    assert version is not None
637
    freetype = parse_version(version)
638
    if freetype < parse_version("2.9.1"):
639
        with pytest.raises(NotImplementedError):
640
            font.get_variation_names()
641
        with pytest.raises(NotImplementedError):
642
            font.get_variation_axes()
643
        return
644

645
    with pytest.raises(OSError):
646
        font.get_variation_names()
647
    with pytest.raises(OSError):
648
        font.get_variation_axes()
649

650
    font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf")
651
    assert font.get_variation_names(), [
652
        b"ExtraLight",
653
        b"Light",
654
        b"Regular",
655
        b"Semibold",
656
        b"Bold",
657
        b"Black",
658
        b"Black Medium Contrast",
659
        b"Black High Contrast",
660
        b"Default",
661
    ]
662
    assert font.get_variation_axes() == [
663
        {"name": b"Weight", "minimum": 200, "maximum": 900, "default": 389},
664
        {"name": b"Contrast", "minimum": 0, "maximum": 100, "default": 0},
665
    ]
666

667
    font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf")
668
    assert font.get_variation_names() == [
669
        b"20",
670
        b"40",
671
        b"60",
672
        b"80",
673
        b"100",
674
        b"120",
675
        b"140",
676
        b"160",
677
        b"180",
678
        b"200",
679
        b"220",
680
        b"240",
681
        b"260",
682
        b"280",
683
        b"300",
684
        b"Regular",
685
    ]
686
    assert font.get_variation_axes() == [
687
        {"name": b"Size", "minimum": 0, "maximum": 300, "default": 0}
688
    ]
689

690

691
def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None:
692
    im = Image.new("RGB", (100, 75), "white")
693
    d = ImageDraw.Draw(im)
694
    d.text((10, 10), "Text", font=font, fill="black")
695

696
    try:
697
        assert_image_similar_tofile(im, path, epsilon)
698
    except AssertionError:
699
        if "_adobe" in path:
700
            path = path.replace("_adobe", "_adobe_older_harfbuzz")
701
            assert_image_similar_tofile(im, path, epsilon)
702
        else:
703
            raise
704

705

706
def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None:
707
    version = features.version_module("freetype2")
708
    assert version is not None
709
    freetype = parse_version(version)
710
    if freetype < parse_version("2.9.1"):
711
        with pytest.raises(NotImplementedError):
712
            font.set_variation_by_name("Bold")
713
        return
714

715
    with pytest.raises(OSError):
716
        font.set_variation_by_name("Bold")
717

718
    font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
719
    _check_text(font, "Tests/images/variation_adobe.png", 11)
720
    for name in ("Bold", b"Bold"):
721
        font.set_variation_by_name(name)
722
        assert font.getname()[1] == "Bold"
723
    _check_text(font, "Tests/images/variation_adobe_name.png", 16)
724

725
    font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
726
    _check_text(font, "Tests/images/variation_tiny.png", 40)
727
    for name in ("200", b"200"):
728
        font.set_variation_by_name(name)
729
        assert font.getname()[1] == "200"
730
    _check_text(font, "Tests/images/variation_tiny_name.png", 40)
731

732

733
def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None:
734
    version = features.version_module("freetype2")
735
    assert version is not None
736
    freetype = parse_version(version)
737
    if freetype < parse_version("2.9.1"):
738
        with pytest.raises(NotImplementedError):
739
            font.set_variation_by_axes([100])
740
        return
741

742
    with pytest.raises(OSError):
743
        font.set_variation_by_axes([500, 50])
744

745
    font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
746
    font.set_variation_by_axes([500, 50])
747
    _check_text(font, "Tests/images/variation_adobe_axes.png", 11.05)
748

749
    font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
750
    font.set_variation_by_axes([100])
751
    _check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)
752

753

754
@pytest.mark.parametrize(
755
    "anchor, left, top",
756
    (
757
        # test horizontal anchors
758
        ("ls", 0, -36),
759
        ("ms", -64, -36),
760
        ("rs", -128, -36),
761
        # test vertical anchors
762
        ("ma", -64, 16),
763
        ("mt", -64, 0),
764
        ("mm", -64, -17),
765
        ("mb", -64, -44),
766
        ("md", -64, -51),
767
    ),
768
    ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"),
769
)
770
def test_anchor(
771
    layout_engine: ImageFont.Layout, anchor: str, left: int, top: int
772
) -> None:
773
    name, text = "quick", "Quick"
774
    path = f"Tests/images/test_anchor_{name}_{anchor}.png"
775

776
    if layout_engine == ImageFont.Layout.RAQM:
777
        width, height = (129, 44)
778
    else:
779
        width, height = (128, 44)
780

781
    bbox_expected = (left, top, left + width, top + height)
782

783
    f = ImageFont.truetype(
784
        "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=layout_engine
785
    )
786

787
    im = Image.new("RGB", (200, 200), "white")
788
    d = ImageDraw.Draw(im)
789
    d.line(((0, 100), (200, 100)), "gray")
790
    d.line(((100, 0), (100, 200)), "gray")
791
    d.text((100, 100), text, fill="black", anchor=anchor, font=f)
792

793
    assert d.textbbox((0, 0), text, f, anchor=anchor) == bbox_expected
794

795
    assert_image_similar_tofile(im, path, 7)
796

797

798
@pytest.mark.parametrize(
799
    "anchor, align",
800
    (
801
        # test horizontal anchors
802
        ("lm", "left"),
803
        ("lm", "center"),
804
        ("lm", "right"),
805
        ("mm", "left"),
806
        ("mm", "center"),
807
        ("mm", "right"),
808
        ("rm", "left"),
809
        ("rm", "center"),
810
        ("rm", "right"),
811
        # test vertical anchors
812
        ("ma", "center"),
813
        # ("mm", "center"),  # duplicate
814
        ("md", "center"),
815
    ),
816
)
817
def test_anchor_multiline(
818
    layout_engine: ImageFont.Layout, anchor: str, align: str
819
) -> None:
820
    target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png"
821
    text = "a\nlong\ntext sample"
822

823
    f = ImageFont.truetype(
824
        "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=layout_engine
825
    )
826

827
    # test render
828
    im = Image.new("RGB", (600, 400), "white")
829
    d = ImageDraw.Draw(im)
830
    d.line(((0, 200), (600, 200)), "gray")
831
    d.line(((300, 0), (300, 400)), "gray")
832
    d.multiline_text((300, 200), text, fill="black", anchor=anchor, font=f, align=align)
833

834
    assert_image_similar_tofile(im, target, 4)
835

836

837
def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None:
838
    im = Image.new("RGB", (100, 100), "white")
839
    d = ImageDraw.Draw(im)
840
    d.font = font
841

842
    for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]:
843
        with pytest.raises(ValueError):
844
            font.getmask2("hello", anchor=anchor)
845
        with pytest.raises(ValueError):
846
            font.getbbox("hello", anchor=anchor)
847
        with pytest.raises(ValueError):
848
            d.text((0, 0), "hello", anchor=anchor)
849
        with pytest.raises(ValueError):
850
            d.textbbox((0, 0), "hello", anchor=anchor)
851
        with pytest.raises(ValueError):
852
            d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
853
        with pytest.raises(ValueError):
854
            d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor)
855
    for anchor in ["lt", "lb"]:
856
        with pytest.raises(ValueError):
857
            d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
858
        with pytest.raises(ValueError):
859
            d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor)
860

861

862
@pytest.mark.parametrize("bpp", (1, 2, 4, 8))
863
def test_bitmap_font(layout_engine: ImageFont.Layout, bpp: int) -> None:
864
    text = "Bitmap Font"
865
    layout_name = ["basic", "raqm"][layout_engine]
866
    target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png"
867
    font = ImageFont.truetype(
868
        f"Tests/fonts/DejaVuSans/DejaVuSans-24-{bpp}-stripped.ttf",
869
        24,
870
        layout_engine=layout_engine,
871
    )
872

873
    im = Image.new("RGB", (160, 35), "white")
874
    draw = ImageDraw.Draw(im)
875
    draw.text((2, 2), text, "black", font)
876

877
    assert_image_equal_tofile(im, target)
878

879

880
def test_bitmap_font_stroke(layout_engine: ImageFont.Layout) -> None:
881
    text = "Bitmap Font"
882
    layout_name = ["basic", "raqm"][layout_engine]
883
    target = f"Tests/images/bitmap_font_stroke_{layout_name}.png"
884
    font = ImageFont.truetype(
885
        "Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf",
886
        24,
887
        layout_engine=layout_engine,
888
    )
889

890
    im = Image.new("RGB", (160, 35), "white")
891
    draw = ImageDraw.Draw(im)
892
    draw.text((2, 2), text, "black", font, stroke_width=2, stroke_fill="red")
893

894
    assert_image_similar_tofile(im, target, 0.03)
895

896

897
@pytest.mark.parametrize("embedded_color", (False, True))
898
def test_bitmap_blend(layout_engine: ImageFont.Layout, embedded_color: bool) -> None:
899
    font = ImageFont.truetype(
900
        "Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine
901
    )
902

903
    im = Image.new("RGBA", (128, 96), "white")
904
    d = ImageDraw.Draw(im)
905
    d.text((16, 16), "AA", font=font, fill="#8E2F52", embedded_color=embedded_color)
906

907
    assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png")
908

909

910
def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None:
911
    txt = "Hello World!"
912
    ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
913
    ttf.getbbox(txt)
914

915
    im = Image.new("RGB", (300, 64), "white")
916
    d = ImageDraw.Draw(im)
917
    d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True)
918

919
    assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1)
920

921

922
@pytest.mark.parametrize("fontmode", ("1", "L", "RGBA"))
923
def test_float_coord(layout_engine: ImageFont.Layout, fontmode: str) -> None:
924
    txt = "Hello World!"
925
    ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
926

927
    im = Image.new("RGB", (300, 64), "white")
928
    d = ImageDraw.Draw(im)
929
    if fontmode == "1":
930
        d.fontmode = "1"
931

932
    embedded_color = fontmode == "RGBA"
933
    d.text((9.5, 9.5), txt, font=ttf, fill="#fa6", embedded_color=embedded_color)
934
    try:
935
        assert_image_similar_tofile(im, "Tests/images/text_float_coord.png", 3.9)
936
    except AssertionError:
937
        if fontmode == "1" and layout_engine == ImageFont.Layout.BASIC:
938
            assert_image_similar_tofile(
939
                im, "Tests/images/text_float_coord_1_alt.png", 1
940
            )
941
        else:
942
            raise
943

944

945
def test_cbdt(layout_engine: ImageFont.Layout) -> None:
946
    try:
947
        font = ImageFont.truetype(
948
            "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
949
        )
950

951
        im = Image.new("RGB", (128, 96), "white")
952
        d = ImageDraw.Draw(im)
953

954
        d.text((16, 16), "AB", font=font, embedded_color=True)
955

956
        assert_image_equal_tofile(im, "Tests/images/cbdt.png")
957
    except OSError as e:  # pragma: no cover
958
        assert str(e) in ("unimplemented feature", "unknown file format")
959
        pytest.skip("freetype compiled without libpng or CBDT support")
960

961

962
def test_cbdt_mask(layout_engine: ImageFont.Layout) -> None:
963
    try:
964
        font = ImageFont.truetype(
965
            "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
966
        )
967

968
        im = Image.new("RGB", (128, 96), "white")
969
        d = ImageDraw.Draw(im)
970

971
        d.text((16, 16), "AB", "green", font=font)
972

973
        assert_image_equal_tofile(im, "Tests/images/cbdt_mask.png")
974
    except OSError as e:  # pragma: no cover
975
        assert str(e) in ("unimplemented feature", "unknown file format")
976
        pytest.skip("freetype compiled without libpng or CBDT support")
977

978

979
def test_sbix(layout_engine: ImageFont.Layout) -> None:
980
    try:
981
        font = ImageFont.truetype(
982
            "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
983
        )
984

985
        im = Image.new("RGB", (400, 400), "white")
986
        d = ImageDraw.Draw(im)
987

988
        d.text((50, 50), "\uE901", font=font, embedded_color=True)
989

990
        assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1)
991
    except OSError as e:  # pragma: no cover
992
        assert str(e) in ("unimplemented feature", "unknown file format")
993
        pytest.skip("freetype compiled without libpng or SBIX support")
994

995

996
def test_sbix_mask(layout_engine: ImageFont.Layout) -> None:
997
    try:
998
        font = ImageFont.truetype(
999
            "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
1000
        )
1001

1002
        im = Image.new("RGB", (400, 400), "white")
1003
        d = ImageDraw.Draw(im)
1004

1005
        d.text((50, 50), "\uE901", (100, 0, 0), font=font)
1006

1007
        assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1)
1008
    except OSError as e:  # pragma: no cover
1009
        assert str(e) in ("unimplemented feature", "unknown file format")
1010
        pytest.skip("freetype compiled without libpng or SBIX support")
1011

1012

1013
@skip_unless_feature_version("freetype2", "2.10.0")
1014
def test_colr(layout_engine: ImageFont.Layout) -> None:
1015
    font = ImageFont.truetype(
1016
        "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
1017
        size=64,
1018
        layout_engine=layout_engine,
1019
    )
1020

1021
    im = Image.new("RGB", (300, 75), "white")
1022
    d = ImageDraw.Draw(im)
1023

1024
    d.text((15, 5), "Bungee", font=font, embedded_color=True)
1025

1026
    assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 21)
1027

1028

1029
@skip_unless_feature_version("freetype2", "2.10.0")
1030
def test_colr_mask(layout_engine: ImageFont.Layout) -> None:
1031
    font = ImageFont.truetype(
1032
        "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
1033
        size=64,
1034
        layout_engine=layout_engine,
1035
    )
1036

1037
    im = Image.new("RGB", (300, 75), "white")
1038
    d = ImageDraw.Draw(im)
1039

1040
    d.text((15, 5), "Bungee", "black", font=font)
1041

1042
    assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
1043

1044

1045
def test_woff2(layout_engine: ImageFont.Layout) -> None:
1046
    try:
1047
        font = ImageFont.truetype(
1048
            "Tests/fonts/OpenSans.woff2",
1049
            size=64,
1050
            layout_engine=layout_engine,
1051
        )
1052
    except OSError as e:
1053
        assert str(e) in ("unimplemented feature", "unknown file format")
1054
        pytest.skip("FreeType compiled without brotli or WOFF2 support")
1055

1056
    im = Image.new("RGB", (350, 100), "white")
1057
    d = ImageDraw.Draw(im)
1058

1059
    d.text((15, 5), "OpenSans", "black", font=font)
1060

1061
    assert_image_similar_tofile(im, "Tests/images/test_woff2.png", 5)
1062

1063

1064
def test_render_mono_size() -> None:
1065
    # issue 4177
1066

1067
    im = Image.new("P", (100, 30), "white")
1068
    draw = ImageDraw.Draw(im)
1069
    ttf = ImageFont.truetype(
1070
        "Tests/fonts/DejaVuSans/DejaVuSans.ttf",
1071
        18,
1072
        layout_engine=ImageFont.Layout.BASIC,
1073
    )
1074

1075
    draw.text((10, 10), "r" * 10, "black", ttf)
1076
    assert_image_equal_tofile(im, "Tests/images/text_mono.gif")
1077

1078

1079
def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None:
1080
    with pytest.raises(ValueError):
1081
        font.getlength("A" * 1_000_001)
1082
    with pytest.raises(ValueError):
1083
        font.getbbox("A" * 1_000_001)
1084
    with pytest.raises(ValueError):
1085
        font.getmask2("A" * 1_000_001)
1086

1087
    transposed_font = ImageFont.TransposedFont(font)
1088
    with pytest.raises(ValueError):
1089
        transposed_font.getlength("A" * 1_000_001)
1090

1091
    imagefont = ImageFont.ImageFont()
1092
    with pytest.raises(ValueError):
1093
        imagefont.getlength("A" * 1_000_001)
1094
    with pytest.raises(ValueError):
1095
        imagefont.getbbox("A" * 1_000_001)
1096
    with pytest.raises(ValueError):
1097
        imagefont.getmask("A" * 1_000_001)
1098

1099

1100
def test_bytes(font: ImageFont.FreeTypeFont) -> None:
1101
    assert font.getlength(b"test") == font.getlength("test")
1102

1103
    assert font.getbbox(b"test") == font.getbbox("test")
1104

1105
    assert_image_equal(
1106
        Image.Image()._new(font.getmask(b"test")),
1107
        Image.Image()._new(font.getmask("test")),
1108
    )
1109

1110
    assert_image_equal(
1111
        Image.Image()._new(font.getmask2(b"test")[0]),
1112
        Image.Image()._new(font.getmask2("test")[0]),
1113
    )
1114
    assert font.getmask2(b"test")[1] == font.getmask2("test")[1]
1115

1116

1117
@pytest.mark.parametrize(
1118
    "test_file",
1119
    [
1120
        "Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf",
1121
        "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf",
1122
    ],
1123
)
1124
def test_oom(test_file: str) -> None:
1125
    with open(test_file, "rb") as f:
1126
        font = ImageFont.truetype(BytesIO(f.read()))
1127
        with pytest.raises(Image.DecompressionBombError):
1128
            font.getmask("Test Text")
1129

1130

1131
def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None:
1132
    monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False)
1133
    with pytest.warns(UserWarning) as record:
1134
        font = ImageFont.truetype(
1135
            FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.RAQM
1136
        )
1137
    assert font.layout_engine == ImageFont.Layout.BASIC
1138
    assert str(record[-1].message) == (
1139
        "Raqm layout was requested, but Raqm is not available. "
1140
        "Falling back to basic layout."
1141
    )
1142

1143

1144
@pytest.mark.parametrize("size", [-1, 0])
1145
def test_invalid_truetype_sizes_raise_valueerror(
1146
    layout_engine: ImageFont.Layout, size: int
1147
) -> None:
1148
    with pytest.raises(ValueError):
1149
        ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine)
1150

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.