write_text_ornaments.py

Die Python-Datei des Beispiels „Durchgestrichene Preise, ein Schatten hinter dem Titel“. Ein Regalschild zum halben Preis: Jede Zeile zeigt den alten Preis durchgestrichen neben dem neuen, die Summe der drei trägt eine Linie darüber, und der Titel ist ein zweites Mal hinter sich selbst gesetzt, versetzt und blasser, sodass er sich über allem lesen lässt.

Python 1043 Zeilen

Wofür dieses Beispiel da ist

Ein Laden, der die Preise in seinem Regal herabsetzt, tut es mit einem Stift: ein Strich durch den alten Preis, der neue daneben geschrieben und eine Linie über der Summe am Fuß, damit niemand sie als weiteren Preis liest. Auf einer Seite, die ein Programm schreibt, müssen diese Striche gesetzt werden — und sie von Hand zu setzen heißt, für jeden eine Höhe zu wählen. Wählt man schlecht, landet der Strich auf einer Maschine mitten durch die Zahlen und auf der nächsten darunter, weil sich die Schrift geändert hat und die geschriebene Zahl nicht.

Was dieses Beispiel zeigt

   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
"""Writes a shelf card that marks its lines the three ways a text can be marked.

The Python twin of the `write_text_ornaments` example in Rust. A rule under the
words, a rule through the letters, a rule over the top: the engine takes all
three from the font rather than from a coordinate, so they follow whatever size
the text is set in. The heading and the mark across the foot each carry a shadow
— the same words set once more, offset and paler, under the words themselves.
The heading's letters are pushed apart and the standfirst's narrowed, both counted
where the lines are broken.

The back of the card sets the rest of what spaces a text: the words of one line
pushed apart, one paragraph set close and then set wide, a figure lifted above the
line to call a note, and the mark's words drawn hollow. Word spacing moves the pen
only at the byte a one-byte encoding gives the space, so the face is added a second
time, reached through such an encoding, for the one line that shows it. Its last
part rules a plain paragraph: one line underlined, then hollow words set on a band
laid down before them.

A third page paints one word each of the four ways a text can be painted without
clipping what follows: filled, outlined, both, and not at all.

Every word is held in `Words`, once per language, and `HQF_PDF_LANG` picks which
set is set. The prices are not words: they stand in one table, shared by both
languages.

Usage: python examples/write_text_ornaments.py [out.pdf] [font.ttf]
       HQF_PDF_LANG=fr python examples/write_text_ornaments.py
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# Where the card's text begins, in points from the left edge of the page.
LEFT = 56.0
# How wide it runs.
WIDTH = 483.0
# The size the heading is set in.
TITLE_SIZE = 32.0
# What every letter of the heading takes on top of the width the face gives it, in
# points.
TITLE_TRACKING = 1.5
# The size the standfirst and the closing note are set in.
NOTE_SIZE = 10.5
# How wide the letters of the standfirst are drawn, as a percentage of the width the
# face gives them.
STANDFIRST_SCALING = 90.0
# The size a line of the shelf is set in.
ROW_SIZE = 13.0
# The rule over the total: its top this far above the baseline, in points, and this
# thick.
TOTAL_RULE = (ROW_SIZE, 1.2)
# The top of the heading's box.
TITLE_TOP = 800.0
# The top of the standfirst.
STANDFIRST_TOP = 748.0
# Where the line under the standfirst is drawn.
HEAD_RULE = 730.0
# The top of the first line of the shelf.
FIRST_ROW = 720.0
# How far apart the lines of the shelf sit.
ROW_STEP = 26.0
# The top of the line that adds the three together.
TOTAL_TOP = 626.0
# The top of the first line of the legend.
LEGEND_TOP = 570.0
# How far apart the lines of the legend sit.
LEGEND_STEP = 24.0
# How far into the box the legend's marked words are set.
LEGEND_INDENT = 24.0
# The top of the closing note.
NOTE_TOP = 470.0
# The box the mark is laid across: its lower-left corner and its size.
MARK_BOX = (LEFT, 100.0, WIDTH, 230.0)

# The size the heading on the back of the card is set in.
BACK_TITLE_SIZE = 22.0
# The top of the heading on the back of the card.
BACK_TITLE_TOP = 800.0
# The top of the words the back of the card opens each of its five parts with.
LABEL_TOPS = (740.0, 648.0, 490.0, 398.0, 288.0)
# How far under its words each part sets what it shows.
LABEL_GAP = 22.0
# What every blank of the spaced line takes on top of a space, in points.
WORD_SPACING = 6.0
# How far apart the two lines of the first part sit.
SAMPLE_STEP = 22.0
# The two leadings the paragraph is set at, close and then wide, in points.
LEADINGS = (12.6, 21.0)
# The gutter between the two settings of the paragraph.
GUTTER = 24.0
# The size the figure calling the note is set in.
CALL_SIZE = 8.0
# How far above the baseline the figure calling the note stands, in points.
CALL_RISE = 5.0
# The figure that calls the note, the same in every language.
CALL = "1"
# How far under the line that calls it the note stands.
NOTE_STEP = 20.0
# The size the hollow words are set in.
HOLLOW_SIZE = 44.0
# How far under its words the baseline of the hollow words stands.
HOLLOW_DROP = 58.0
# The width of the pen that traces the hollow letters.
HOLLOW_PEN = 0.9
# The size the words set on a band are set in.
BAND_SIZE = 22.0
# The band under those words: its top this far above the baseline, in points, and this
# thick.
BAND = (17.0, 20.0)
# How far under the underlined line the words set on a band stand.
BAND_STEP = 24.0
# The top of the note at the foot of the back of the card.
BACK_NOTE_TOP = 196.0


@dataclass(frozen=True)
class Words:
    """Every word the card draws, in one language."""

    # What stands across the head of the card.
    title: str
    # The line under it, which says what the card is showing.
    standfirst: str
    # What each line of the shelf is called, in the order they are set.
    goods: tuple[str, str, str]
    # What stands against the sum of the three.
    total_label: str
    # What opens the legend.
    legend: str
    # The three marks, each named by words set the way the name says.
    under: str
    through: str
    over: str
    # The note at the foot, which says where each mark comes from.
    note: str
    # What the mark laid across the card reads.
    mark: str
    # What stands across the head of the back of the card.
    back_title: str
    # What opens each of the five parts of the back: words, lines, the lifted figure,
    # the hollow letters and the rules.
    labels: tuple[str, str, str, str, str]
    # The line set once as the face spaces it and once with its words pushed apart.
    spaced: str
    # The paragraph set close and then set wide.
    paragraph: str
    # The line the lifted figure calls a note from.
    called: str
    # The note the lifted figure calls.
    called_note: str
    # The line set underlined.
    underlined: str
    # The words set hollow on a band.
    banded: str
    # The note at the foot of the back, which says how each part is made.
    back_note: str
    # What stands across the head of the third page.
    modes_title: str
    # The word the third page paints each way.
    painted: str
    # What each way of painting is called, in the order `MODES` lists them.
    modes: tuple[str, ...]

    def marks(self) -> tuple[str, str, str]:
        """The three marks, in the order the legend sets them."""
        return (self.under, self.through, self.over)


# The card in English.
ENGLISH = Words(
    title="The half-price shelf",
    standfirst="What a shop marks by hand, set by the engine instead.",
    goods=(
        "Atlas of the northern coast",
        "Field guide to the estuary birds",
        "The lighthouse keepers, a history",
    ),
    total_label="The three together",
    legend="The three marks, each set the way it names:",
    under="a rule under the words",
    through="a rule through the letters",
    over="a rule over the top",
    note=(
        "One rule on this card was placed by hand: the heavier one over the "
        "total, standing clear of the figures. The one under the words and the "
        "one through the letters sit where the face itself says they go, and the "
        "one over the legend's last line rides the ascender the face states; "
        "those three follow the size the text is set in. The heading carries a "
        "copy of itself behind it, offset and paler, which is what keeps large "
        "lettering readable over a photograph. The mark laid across the foot "
        "carries one too, turned with it, so the copy falls the same way on the "
        "sheet as it does under the heading. The heading's letters are pushed a "
        "point and a half apart, and the standfirst is narrowed to nine tenths of "
        "the width the face gives it; each line is measured that way as well, so "
        "neither runs past the card."
    ),
    mark="HALF PRICE",
    back_title="On the back: spaced, raised, hollow",
    labels=(
        "Words pushed apart: the same line, then six points more at every space.",
        "Lines pushed apart: one paragraph, set close and then set wide.",
        "A figure raised above the line, the way a note is called:",
        "Letters drawn hollow: only their outline is traced.",
        "Rules along the text: a line underlined, then a band under hollow letters.",
    ),
    spaced="Three books for the price of one and a half.",
    paragraph=(
        "Every book on this shelf came back unread or was opened once in the "
        "shop. The pages are clean, the spines unbroken, and the price is half "
        "the one printed on the cover."
    ),
    called="Half price on every book of the shelf",
    called_note="While the shelf lasts.",
    underlined="Returns are accepted within thirty days.",
    banded="LAST COPIES",
    back_note=(
        "Nothing on this side is placed letter by letter. The room between words "
        "is a setting of the text state, and it moves the pen only at the byte a "
        "one-byte encoding gives the space, so the face is added a second time, "
        "reached through such an encoding. The leading is the distance from one "
        "baseline to the next. The lifted figure stands on a baseline raised five "
        "points, and is set smaller. The hollow letters are painted by their "
        "outline alone, in the colour and the width given to the pen. The underline "
        "and the band belong to the paragraph itself: the band goes down before the "
        "letters, which are painted over it."
    ),
    modes_title="Four ways of painting a letter",
    painted="Ink",
    modes=(
        "filled",
        "outlined",
        "filled and outlined",
        "invisible, yet there to be searched",
    ),
)

# The card in French.
FRENCH = Words(
    title="L’étagère à moitié prix",
    standfirst="Ce qu’un commerçant marque d’ordinaire à la main, le moteur le compose ici.",
    goods=(
        "Atlas de la côte nord",
        "Guide des oiseaux de l’estuaire",
        "Les gardiens de phare, une histoire",
    ),
    total_label="Les trois ensemble",
    legend="Les trois traits, chacun tracé sur les mots qui le nomment :",
    under="un trait sous les mots",
    through="un trait qui barre les lettres",
    over="un trait au-dessus de la ligne",
    note=(
        "Un seul trait de cette fiche a été placé à la main : celui, plus épais, qui "
        "coiffe le total sans toucher les chiffres. Celui qui souligne les mots et celui "
        "qui barre les lettres se placent là où la police elle-même l’indique ; celui qui"
        " surmonte la dernière ligne de la légende repose sur la hauteur d’ascendante que"
        " déclare la police. Ces trois-là suivent la taille du texte. Le titre est "
        "doublé, derrière lui, d’une copie décalée et plus pâle, ce qui garde un grand "
        "lettrage lisible sur une photographie. Le tampon posé en travers du bas de la "
        "fiche en a une aussi, qui tourne avec lui : la copie tombe ainsi sur la feuille "
        "du même côté que sous le titre. Les lettres du titre sont espacées d’un point et"
        " demi de plus, et le sous-titre est ramené aux neuf dixièmes de la largeur que "
        "lui donne la police. Chaque ligne est mesurée avec ces réglages, si bien "
        "qu’aucune ne dépasse de la fiche."
    ),
    mark="MOITIÉ PRIX",
    back_title="Au dos : espacé, en exposant, en creux",
    labels=(
        "Des mots espacés : la même ligne, puis six points de plus à chaque espace.",
        "Des lignes espacées : un même paragraphe, d’abord serré, puis aéré.",
        "Un chiffre en exposant, comme un appel de note :",
        "Des lettres creuses : seul leur contour est tracé.",
        "Des traits : une ligne soulignée, puis une bande sous des lettres creuses.",
    ),
    spaced="Trois livres pour le prix d’un et demi.",
    paragraph=(
        "Chaque livre de cette étagère a été rapporté sans avoir été lu, ou n’a été "
        "ouvert qu’une fois en librairie. Les pages sont propres, les dos intacts, et le "
        "prix est la moitié de celui qui est imprimé sur la couverture."
    ),
    called="Moitié prix sur tous les livres de l’étagère",
    called_note="Dans la limite des livres disponibles.",
    underlined="Les retours sont acceptés sous trente jours.",
    banded="DERNIERS EXEMPLAIRES",
    back_note=(
        "Rien sur cette face n’est placé lettre à lettre. L’espace entre les mots est "
        "un réglage de l’état du texte ; il n’agit que sur l’octet qu’un encodage à un "
        "octet attribue à l’espace. La police est donc ajoutée une seconde fois, sous "
        "un encodage de ce type. L’interligne est la distance d’une ligne de base à la "
        "suivante. Le chiffre en exposant repose sur une ligne de base remontée de cinq "
        "points, dans un corps plus petit. Les lettres creuses ne sont peintes que par "
        "leur contour, avec la couleur et l’épaisseur de trait qui leur sont données. "
        "Le soulignement et la bande appartiennent au paragraphe lui-même : la bande "
        "est posée avant les lettres, qui sont peintes par-dessus."
    ),
    modes_title="Quatre façons de peindre une lettre",
    painted="Encre",
    modes=(
        "rempli",
        "contouré",
        "rempli et contouré",
        "invisible, mais présent pour la recherche",
    ),
)

# The card in Dutch.
DUTCH = Words(
    title="De plank met halve prijzen",
    standfirst="Wat een winkel met de hand markeert, hier door de motor gezet.",
    goods=(
        "Atlas van de noordkust",
        "Veldgids voor de vogels van het estuarium",
        "De vuurtorenwachters, een geschiedenis",
    ),
    total_label="De drie samen",
    legend="De drie markeringen, elk gezet zoals haar naam zegt:",
    under="een lijn onder de woorden",
    through="een lijn door de letters",
    over="een lijn over de bovenkant",
    note=(
        "Eén lijn op deze kaart is met de hand geplaatst: de zwaardere boven het "
        "totaal, vrij van de cijfers. Die onder de woorden en die door de letters "
        "staan waar de letter zelf zegt dat ze horen, en die boven de laatste regel "
        "van het onderschrift rust op de stok die de letter opgeeft; die drie volgen "
        "de grootte waarin de tekst gezet is. De kop heeft een kopie van zichzelf "
        "erachter, verschoven en bleker, en die houdt grote letters leesbaar boven "
        "een foto. De markering schuin over de voet heeft er ook een, mee gedraaid, "
        "dus valt de kopie op het blad net zo als onder de kop. De letters van de kop "
        "staan anderhalve punt uit elkaar, en de intro is versmald tot negen tiende "
        "van de breedte die de letter haar geeft; elke regel is ook zo gemeten, dus "
        "loopt geen van beide over de kaart."
    ),
    mark="HALVE PRIJS",
    back_title="Achterkant: gespatieerd, verhoogd, hol",
    labels=(
        "Woorden uit elkaar: dezelfde regel, dan zes punten extra bij elke spatie.",
        "Regels uit elkaar: één alinea, krap gezet en dan ruim.",
        "Een cijfer boven de regel, zoals een voetnoot wordt aangeroepen:",
        "Letters hol getekend: alleen hun omtrek wordt getrokken.",
        "Lijnen langs de tekst: een onderstreepte regel, dan een band onder holle letters.",
    ),
    spaced="Drie boeken voor de prijs van anderhalf.",
    paragraph=(
        "Elk boek op deze plank kwam ongelezen terug of is één keer in de winkel "
        "geopend. De pagina’s zijn schoon, de ruggen ongebroken, en de prijs is de "
        "helft van die op het omslag."
    ),
    called="Halve prijs op elk boek van de plank",
    called_note="Zolang de voorraad strekt.",
    underlined="Retouren worden binnen dertig dagen aanvaard.",
    banded="LAATSTE EXEMPLAREN",
    back_note=(
        "Niets aan deze kant is letter voor letter geplaatst. De ruimte tussen "
        "woorden is een instelling van de tekststatus, en verplaatst de pen alleen "
        "bij de byte die een codering van één byte aan de spatie geeft, dus wordt de "
        "letter een tweede keer toegevoegd, via zo’n codering. De regelafstand is de "
        "afstand van de ene basislijn tot de volgende. Het verhoogde cijfer staat op "
        "een basislijn vijf punten hoger, en is kleiner gezet. De holle letters "
        "worden alleen door hun omtrek geschilderd, in de kleur en de dikte die de "
        "pen meekreeg. De onderstreping en de band horen bij de alinea zelf: de band "
        "komt vóór de letters, die eroverheen geschilderd worden."
    ),
    modes_title="Vier manieren om een letter te schilderen",
    painted="Inkt",
    modes=(
        "gevuld",
        "omlijnd",
        "gevuld en omlijnd",
        "onzichtbaar, maar vindbaar bij zoeken",
    ),
)

# The card in Portuguese.
PORTUGUESE = Words(
    title="Prateleira a meio preço",
    standfirst="O que uma loja marca à mão, composto pelo motor.",
    goods=(
        "Atlas da costa norte",
        "Guia de campo das aves do estuário",
        "Os faroleiros, uma história",
    ),
    total_label="Os três juntos",
    legend="As três marcas, cada uma composta como o seu nome diz:",
    under="um filete sob as palavras",
    through="um filete através das letras",
    over="um filete por cima",
    note=(
        "Um filete deste cartão foi colocado à mão: o mais grosso por cima do total, "
        "afastado dos números. O que fica sob as palavras e o que atravessa as letras "
        "estão onde o próprio tipo diz que vão, e o que fica sobre a última linha da "
        "legenda apoia-se no ascendente que o tipo indica; esses três seguem o corpo "
        "do texto. O título leva por trás uma cópia de si mesmo, deslocada e mais "
        "pálida, que é o que mantém legíveis as letras grandes sobre uma fotografia. "
        "A marca posta ao longo do pé também leva uma, rodada com ela, por isso a "
        "cópia cai na folha da mesma forma que sob o título. As letras do título são "
        "afastadas ponto e meio, e o subtítulo é estreitado para nove décimos da "
        "largura que o tipo lhe dá; cada linha é medida assim também, por isso "
        "nenhuma sai do cartão."
    ),
    mark="METADE DO PREÇO",
    back_title="No verso: espaçado, elevado, oco",
    labels=(
        "Palavras afastadas: a mesma linha, e depois seis pontos a mais em cada espaço.",
        "Linhas afastadas: um parágrafo, composto apertado e depois largo.",
        "Um número elevado acima da linha, como se chama uma nota:",
        "Letras desenhadas ocas: só o seu contorno é traçado.",
        "Filetes ao longo do texto: uma linha sublinhada, e depois uma faixa sob letras ocas.",
    ),
    spaced="Três livros pelo preço de um e meio.",
    paragraph=(
        "Cada livro desta prateleira voltou por ler ou foi aberto uma vez na loja. As "
        "páginas estão limpas, as lombadas intactas, e o preço é metade do impresso "
        "na capa."
    ),
    called="Metade do preço em todos os livros da prateleira",
    called_note="Até esgotar.",
    underlined="Aceitam-se devoluções no prazo de trinta dias.",
    banded="ÚLTIMOS EXEMPLARES",
    back_note=(
        "Nada deste lado é colocado letra a letra. O espaço entre palavras é uma "
        "definição do estado de texto, e só move a caneta no byte que uma codificação "
        "de um byte dá ao espaço, por isso o tipo é acrescentado uma segunda vez, "
        "através dessa codificação. O entrelinhamento é a distância de uma linha de "
        "base à seguinte. O número elevado assenta numa linha de base cinco pontos "
        "mais alta, e em corpo menor. As letras ocas são pintadas só pelo seu "
        "contorno, na cor e na espessura dadas à caneta. O sublinhado e a faixa "
        "pertencem ao próprio parágrafo: a faixa é posta antes das letras, que são "
        "pintadas por cima."
    ),
    modes_title="Quatro formas de pintar uma letra",
    painted="Tinta",
    modes=(
        "preenchida",
        "contornada",
        "preenchida e contornada",
        "invisível, mas presente para a pesquisa",
    ),
)

# The card in Italian.
ITALIAN = Words(
    title="Lo scaffale a metà prezzo",
    standfirst="Ciò che un negozio segna a mano, composto invece dal motore.",
    goods=(
        "Atlante della costa settentrionale",
        "Guida da campo agli uccelli dell’estuario",
        "I guardiani del faro, una storia",
    ),
    total_label="I tre insieme",
    legend="I tre segni, ciascuno composto come dice il suo nome:",
    under="un filetto sotto le parole",
    through="un filetto attraverso le lettere",
    over="un filetto sopra",
    note=(
        "Un filetto di questo cartoncino è stato collocato a mano: quello più spesso "
        "sopra il totale, staccato dalle cifre. Quello sotto le parole e quello "
        "attraverso le lettere stanno dove dice il carattere stesso, e quello sopra "
        "l’ultima riga della legenda poggia sull’ascendente indicato dal carattere; "
        "quei tre seguono il corpo del testo. Il titolo porta dietro una copia di sé, "
        "spostata e più chiara, ed è ciò che tiene leggibili le lettere grandi su una "
        "fotografia. Il segno che attraversa il piede ne porta una anche lui, ruotata "
        "con esso, così la copia cade sul foglio come sotto il titolo. Le lettere del "
        "titolo sono allargate di un punto e mezzo, e l’occhiello è ristretto a nove "
        "decimi della larghezza che gli dà il carattere; anche ogni riga è misurata "
        "così, perciò nessuna esce dal cartoncino."
    ),
    mark="METÀ PREZZO",
    back_title="Sul retro: spaziato, sollevato, vuoto",
    labels=(
        "Parole allargate: la stessa riga, poi sei punti in più a ogni spazio.",
        "Righe allargate: un paragrafo, composto stretto e poi largo.",
        "Una cifra sollevata sopra la riga, come si richiama una nota:",
        "Lettere disegnate vuote: se ne traccia solo il contorno.",
        "Filetti lungo il testo: una riga sottolineata, poi una fascia sotto lettere vuote.",
    ),
    spaced="Tre libri al prezzo di uno e mezzo.",
    paragraph=(
        "Ogni libro di questo scaffale è tornato non letto o è stato aperto una volta "
        "in negozio. Le pagine sono pulite, i dorsi integri, e il prezzo è la metà di "
        "quello stampato in copertina."
    ),
    called="Metà prezzo su ogni libro dello scaffale",
    called_note="Fino a esaurimento.",
    underlined="I resi si accettano entro trenta giorni.",
    banded="ULTIME COPIE",
    back_note=(
        "Niente su questo lato è collocato lettera per lettera. Lo spazio tra le "
        "parole è un’impostazione dello stato del testo, e sposta la penna solo al "
        "byte che una codifica a un byte dà allo spazio, così il carattere è aggiunto "
        "una seconda volta, attraverso una tale codifica. L’interlinea è la distanza "
        "da una linea di base alla successiva. La cifra sollevata sta su una linea di "
        "base alzata di cinque punti, ed è in corpo minore. Le lettere vuote sono "
        "dipinte solo dal loro contorno, nel colore e nello spessore dati alla penna. "
        "La sottolineatura e la fascia appartengono al paragrafo stesso: la fascia si "
        "stende prima delle lettere, che vi sono dipinte sopra."
    ),
    modes_title="Quattro modi di dipingere una lettera",
    painted="Inchiostro",
    modes=(
        "piena",
        "contornata",
        "piena e contornata",
        "invisibile, eppure lì da cercare",
    ),
)

# The card in Spanish.
SPANISH = Words(
    title="Estante a medio precio",
    standfirst="Lo que una tienda marca a mano, compuesto por el motor.",
    goods=(
        "Atlas de la costa norte",
        "Guía de campo de las aves del estuario",
        "Los fareros, una historia",
    ),
    total_label="Los tres juntos",
    legend="Las tres marcas, cada una compuesta como dice su nombre:",
    under="una raya bajo las palabras",
    through="una raya a través de las letras",
    over="una raya por encima",
    note=(
        "Una raya de esta tarjeta se colocó a mano: la más gruesa sobre el total, "
        "separada de las cifras. La de bajo las palabras y la que cruza las letras "
        "están donde la propia fuente dice que van, y la que va sobre la última línea "
        "de la leyenda se apoya en el ascendente que indica la fuente; esas tres "
        "siguen el cuerpo del texto. El título lleva detrás una copia de sí mismo, "
        "desplazada y más pálida, que es lo que mantiene legibles las letras grandes "
        "sobre una fotografía. La marca que cruza el pie lleva otra, girada con ella, "
        "así que la copia cae igual en la hoja que bajo el título. Las letras del "
        "título se separan punto y medio, y la entradilla se estrecha a nueve décimas "
        "del ancho que le da la fuente; cada línea también se mide así, así que "
        "ninguna se sale de la tarjeta."
    ),
    mark="MITAD DE PRECIO",
    back_title="Al dorso: espaciado, elevado, hueco",
    labels=(
        "Palabras separadas: la misma línea, y luego seis puntos más en cada espacio.",
        "Líneas separadas: un párrafo, compuesto apretado y luego holgado.",
        "Una cifra elevada sobre la línea, como se llama a una nota:",
        "Letras dibujadas huecas: solo se traza su contorno.",
        "Rayas a lo largo del texto: una línea subrayada, y luego una banda bajo letras huecas.",
    ),
    spaced="Tres libros por el precio de uno y medio.",
    paragraph=(
        "Cada libro de esta estantería volvió sin leer o se abrió una vez en la "
        "tienda. Las páginas están limpias, los lomos intactos, y el precio es la "
        "mitad del impreso en la cubierta."
    ),
    called="Mitad de precio en todos los libros de la estantería",
    called_note="Hasta agotar existencias.",
    underlined="Se aceptan devoluciones en treinta días.",
    banded="ÚLTIMOS EJEMPLARES",
    back_note=(
        "Nada en esta cara se coloca letra a letra. El espacio entre palabras es un "
        "ajuste del estado de texto, y solo mueve la pluma en el byte que una "
        "codificación de un byte da al espacio, así que la fuente se añade una "
        "segunda vez, a través de esa codificación. El interlineado es la distancia "
        "de una línea base a la siguiente. La cifra elevada está sobre una línea base "
        "cinco puntos más alta, y en cuerpo menor. Las letras huecas se pintan solo "
        "por su contorno, con el color y el ancho dados a la pluma. El subrayado y la "
        "banda pertenecen al propio párrafo: la banda se pone antes que las letras, "
        "que se pintan encima."
    ),
    modes_title="Cuatro formas de pintar una letra",
    painted="Tinta",
    modes=(
        "rellena",
        "contorneada",
        "rellena y contorneada",
        "invisible, pero presente para las búsquedas",
    ),
)

# The card in German.
GERMAN = Words(
    title="Das Halbpreisregal",
    standfirst="Was ein Laden von Hand auszeichnet, setzt hier die Engine.",
    goods=(
        "Atlas der Nordküste",
        "Bestimmungsbuch der Vögel des Ästuars",
        "Die Leuchtturmwärter, eine Geschichte",
    ),
    total_label="Alle drei zusammen",
    legend="Die drei Auszeichnungen, jede so gesetzt, wie sie heißt:",
    under="eine Linie unter den Wörtern",
    through="eine Linie durch die Buchstaben",
    over="eine Linie über den Buchstaben",
    note=(
        "Eine Linie auf dieser Karte wurde von Hand gesetzt: die kräftigere über der "
        "Summe, frei von den Ziffern. Die unter den Wörtern und die durch die "
        "Buchstaben stehen dort, wo der Schnitt selbst sie vorsieht, und die über der "
        "letzten Zeile der Legende liegt auf der Oberlänge, die der Schnitt angibt; "
        "diese drei folgen der Größe, in der der Text gesetzt ist. Die Überschrift "
        "trägt eine Kopie ihrer selbst dahinter, versetzt und blasser, was große "
        "Schrift über einem Foto lesbar hält. Die quer über den Fuß gelegte "
        "Auszeichnung trägt ebenfalls eine, mit ihr gedreht, sodass die Kopie auf dem "
        "Blatt genauso fällt wie unter der Überschrift. Die Buchstaben der "
        "Überschrift stehen anderthalb Punkt auseinander, und der Vorspann ist auf "
        "neun Zehntel der Breite verengt, die der Schnitt ihm gibt; jede Zeile wird "
        "ebenso gemessen, also läuft keine über die Karte hinaus."
    ),
    mark="HALBER PREIS",
    back_title="Rückseite: gesperrt, gehoben, hohl",
    labels=(
        "Wörter gesperrt: dieselbe Zeile, dann sechs Punkt mehr je Leerzeichen.",
        "Zeilen gesperrt: ein Absatz, eng und dann weit gesetzt.",
        "Eine hochgestellte Ziffer, wie ein Fußnotenzeichen:",
        "Hohle Buchstaben: Nur ihre Kontur wird gezogen.",
        "Linien am Text: unterstrichen, dann ein Band unter hohlen Buchstaben.",
    ),
    spaced="Drei Bücher zum Preis von anderthalb.",
    paragraph=(
        "Jedes Buch in diesem Regal kam ungelesen zurück oder wurde einmal im Laden "
        "aufgeschlagen. Die Seiten sind sauber, die Rücken ungebrochen, und der Preis "
        "ist die Hälfte des auf dem Umschlag gedruckten."
    ),
    called="Halber Preis für jedes Buch des Regals",
    called_note="Solange das Regal reicht.",
    underlined="Rückgabe binnen dreißig Tagen möglich.",
    banded="LETZTE EXEMPLARE",
    back_note=(
        "Nichts auf dieser Seite wird Buchstabe für Buchstabe platziert. Der Raum "
        "zwischen Wörtern ist eine Einstellung des Textzustands, und er bewegt den "
        "Stift nur bei dem Byte, das eine Ein-Byte-Kodierung dem Leerzeichen gibt, "
        "also wird der Schnitt ein zweites Mal hinzugefügt, über eine solche "
        "Kodierung erreicht. Der Zeilenabstand ist der Abstand von einer Grundlinie "
        "zur nächsten. Die gehobene Ziffer steht auf einer um fünf Punkt angehobenen "
        "Grundlinie und ist kleiner gesetzt. Die hohlen Buchstaben werden allein über "
        "ihre Kontur gemalt, in Farbe und Breite des Stifts. Unterstreichung und Band "
        "gehören zum Absatz selbst: Das Band kommt vor den Buchstaben, die "
        "darübergemalt werden."
    ),
    modes_title="Vier Arten, einen Buchstaben zu malen",
    painted="Tinte",
    modes=(
        "gefüllt",
        "umrandet",
        "gefüllt und umrandet",
        "unsichtbar, aber durchsuchbar",
    ),
)

# Every language the card is written in.
WORDS = {_language.ENGLISH: ENGLISH, _language.FRENCH: FRENCH, _language.GERMAN: GERMAN, _language.SPANISH: SPANISH, _language.ITALIAN: ITALIAN, _language.PORTUGUESE: PORTUGUESE, _language.DUTCH: DUTCH}

# What each line of the shelf was marked at and what it is marked at now.
#
# Figures are not words: both languages set the same ones, and the engine writes a
# decimal point in every language it is asked for.
PRICES = (
    ("24.00 EUR", "12.00 EUR"),
    ("18.00 EUR", "9.00 EUR"),
    ("30.00 EUR", "15.00 EUR"),
)

# What the three come to together.
TOTAL = "36.00 EUR"


def shadow_ink() -> hqf_pdf.Rgb:
    """The colour every shadow on the card is drawn in."""
    return hqf_pdf.Rgb.gray(0.78)


def mark_ink() -> hqf_pdf.Rgb:
    """The colour of the mark, which the hollow letters take up on the back."""
    return hqf_pdf.Rgb(0.72, 0.16, 0.16)


def heading(
    content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words
) -> None:
    """Draws the heading, which sets itself once more behind itself."""
    style = hqf_pdf.Style(font, TITLE_SIZE, character_spacing=TITLE_TRACKING)
    text = hqf_pdf.RichText(
        shadow=hqf_pdf.Shadow(1.5, -1.5, shadow_ink())
    ).span(words.title, style)
    lines = text.break_lines(WIDTH)
    text.draw(content, lines, LEFT, TITLE_TOP, WIDTH)


def standfirst(
    content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words
) -> None:
    """Draws the standfirst and the line under it."""
    flow = hqf_pdf.TextFlow(font, NOTE_SIZE, horizontal_scaling=STANDFIRST_SCALING)
    lines = flow.break_lines(words.standfirst, WIDTH)
    flow.draw(content, lines, LEFT, STANDFIRST_TOP, WIDTH)

    content.set_line_width(1.0)
    content.move_to(LEFT, HEAD_RULE)
    content.line_to(LEFT + WIDTH, HEAD_RULE)
    content.stroke()


def shelf_line(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    name: str,
    prices: tuple[str, str],
    top: float,
) -> None:
    """Draws one line of the shelf: what it is called, then what it was marked at
    struck through and what it is marked at now."""
    plain = hqf_pdf.Style(font, ROW_SIZE)
    struck = hqf_pdf.Style(font, ROW_SIZE, strikethrough=True)

    left = hqf_pdf.RichText().span(name, plain)
    left.draw(content, left.break_lines(WIDTH), LEFT, top, WIDTH)

    right = (
        hqf_pdf.RichText(align=hqf_pdf.Align.Right)
        .span(prices[0], struck)
        .span("   ", plain)
        .span(prices[1], plain)
    )
    right.draw(content, right.break_lines(WIDTH), LEFT, top, WIDTH)


def total(content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words) -> None:
    """Draws the line that adds the three together, the sum ruled over the top the
    way a column of figures is ruled before it is totalled."""
    plain = hqf_pdf.Style(font, ROW_SIZE)
    ruled = hqf_pdf.Style(font, ROW_SIZE, overline_at=TOTAL_RULE)

    left = hqf_pdf.RichText().span(words.total_label, plain)
    left.draw(content, left.break_lines(WIDTH), LEFT, TOTAL_TOP, WIDTH)

    right = hqf_pdf.RichText(align=hqf_pdf.Align.Right).span(TOTAL, ruled)
    right.draw(content, right.break_lines(WIDTH), LEFT, TOTAL_TOP, WIDTH)


def legend(content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words) -> None:
    """Draws the legend: a line that says what follows, then the three marks, each
    named by words set the way the name says."""
    plain = hqf_pdf.Style(font, ROW_SIZE)
    opening = hqf_pdf.RichText().span(words.legend, plain)
    opening.draw(content, opening.break_lines(WIDTH), LEFT, LEGEND_TOP, WIDTH)

    marked = (
        hqf_pdf.Style(font, ROW_SIZE, underline=True),
        hqf_pdf.Style(font, ROW_SIZE, strikethrough=True),
        hqf_pdf.Style(font, ROW_SIZE, overline=True),
    )

    # The tops step down one line at a time rather than being multiplied out, so the
    # Rust twin adds the same numbers in the same order.
    top = LEGEND_TOP
    for name, style in zip(words.marks(), marked):
        top -= LEGEND_STEP
        line = hqf_pdf.RichText().span(name, style)
        room = WIDTH - LEGEND_INDENT
        line.draw(content, line.break_lines(room), LEFT + LEGEND_INDENT, top, room)


def note(content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words) -> None:
    """Draws the closing note, which says where each mark comes from."""
    flow = hqf_pdf.TextFlow(font, NOTE_SIZE, align=hqf_pdf.Align.Justify)
    lines = flow.break_lines(words.note, WIDTH)
    flow.draw(content, lines, LEFT, NOTE_TOP, WIDTH)


def mark(content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words) -> None:
    """Draws the mark laid across the foot of the card, which carries a shadow of
    its own."""
    x, y, width, height = MARK_BOX
    stamp = hqf_pdf.Stamp(
        font,
        words.mark,
        slant=hqf_pdf.Slant.Up,
        color=mark_ink(),
        shadow=hqf_pdf.Shadow(2.5, -2.5, shadow_ink()),
    )
    stamp.draw(content, x, y, width, height)


def block(
    content: hqf_pdf.Content,
    flow: hqf_pdf.TextFlow,
    text: str,
    at: tuple[float, float],
    width: float,
) -> float:
    """Sets `text` in `flow` from `top`, and gives back where it ends."""
    left, top = at
    lines = flow.break_lines(text, width)
    flow.draw(content, lines, left, top, width)
    return top - flow.height(lines)


def spaced_words(
    content: hqf_pdf.Content, coded: hqf_pdf.FontHandle, words: Words
) -> None:
    """Draws the first part of the back: one line as the face spaces it, then the
    same line with its words pushed apart."""
    top = LABEL_TOPS[0] - LABEL_GAP
    plain = hqf_pdf.TextFlow(coded, ROW_SIZE)
    block(content, plain, words.spaced, (LEFT, top), WIDTH)
    spaced = hqf_pdf.TextFlow(coded, ROW_SIZE, word_spacing=WORD_SPACING)
    block(content, spaced, words.spaced, (LEFT, top - SAMPLE_STEP), WIDTH)


def spaced_lines(
    content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words
) -> None:
    """Draws the second part of the back: the paragraph at each of the two leadings,
    side by side."""
    top = LABEL_TOPS[1] - LABEL_GAP
    column = (WIDTH - GUTTER) / 2.0
    left = LEFT
    for leading in LEADINGS:
        flow = hqf_pdf.TextFlow(font, NOTE_SIZE, leading=leading)
        block(content, flow, words.paragraph, (left, top), column)
        left += column + GUTTER


def lifted_figure(
    content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words
) -> None:
    """Draws the third part of the back: a line ending on a figure lifted above it,
    then the note the figure calls, opening on the same lifted figure."""
    baseline = LABEL_TOPS[2] - LABEL_GAP - ROW_SIZE

    content.begin_text()
    content.text_position(LEFT, baseline)
    content.set_font(font, ROW_SIZE)
    content.show_runs(font.runs(words.called), ROW_SIZE)
    lifted_call(content, font)
    content.end_text()

    content.begin_text()
    content.text_position(LEFT, baseline - NOTE_STEP)
    lifted_call(content, font)
    content.set_font(font, NOTE_SIZE)
    content.show_runs(font.runs(f" {words.called_note}"), NOTE_SIZE)
    content.end_text()


def lifted_call(content: hqf_pdf.Content, font: hqf_pdf.FontHandle) -> None:
    """Shows the figure that calls the note, smaller and lifted, where the pen stands,
    and sets the baseline back where it was."""
    content.set_font(font, CALL_SIZE)
    content.set_text_rise(CALL_RISE)
    content.show_runs(font.runs(CALL), CALL_SIZE)
    content.set_text_rise(0.0)


def hollow(content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words) -> None:
    """Draws the fourth part of the back: the mark's words traced by their outline
    alone."""
    content.save_state()
    content.set_stroke(mark_ink())
    content.set_line_width(HOLLOW_PEN)
    content.begin_text()
    content.text_position(LEFT, LABEL_TOPS[3] - HOLLOW_DROP)
    content.set_font(font, HOLLOW_SIZE)
    content.set_text_rendering(hqf_pdf.TextRendering.stroke())
    content.show_runs(font.runs(words.mark), HOLLOW_SIZE)
    content.end_text()
    content.restore_state()


def band_ink() -> hqf_pdf.Rgb:
    """The colour of the band the hollow words are set on."""
    return hqf_pdf.Rgb(1.0, 0.84, 0.3)


def ruled_lines(
    content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words
) -> None:
    """Draws the fifth part of the back: a plain line with its rule filled after its
    letters, then hollow words whose band is filled before them."""
    top = LABEL_TOPS[4] - LABEL_GAP
    underlined = hqf_pdf.TextFlow(font, ROW_SIZE, underline=True)
    block(content, underlined, words.underlined, (LEFT, top), WIDTH)

    banded = hqf_pdf.TextFlow(
        font,
        BAND_SIZE,
        color=band_ink(),
        outline=HOLLOW_PEN,
        outline_color=mark_ink(),
        strikethrough_at=BAND,
        rule_stacking=hqf_pdf.Stacking.Under,
    )
    block(content, banded, words.banded, (LEFT, top - BAND_STEP), WIDTH)


def back(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    coded: hqf_pdf.FontHandle,
    words: Words,
) -> None:
    """Draws the back of the card: its heading, the five parts each under the words
    that open it, and the note at the foot."""
    heading = hqf_pdf.TextFlow(font, BACK_TITLE_SIZE)
    block(content, heading, words.back_title, (LEFT, BACK_TITLE_TOP), WIDTH)

    opening = hqf_pdf.TextFlow(font, NOTE_SIZE, color=hqf_pdf.Rgb.gray(0.35))
    for label, top in zip(words.labels, LABEL_TOPS):
        block(content, opening, label, (LEFT, top), WIDTH)

    spaced_words(content, coded, words)
    spaced_lines(content, font, words)
    lifted_figure(content, font, words)
    hollow(content, font, words)
    ruled_lines(content, font, words)

    note = hqf_pdf.TextFlow(font, NOTE_SIZE, align=hqf_pdf.Align.Justify)
    block(content, note, words.back_note, (LEFT, BACK_NOTE_TOP), WIDTH)


# The four ways a text is painted without clipping what follows, in the order the third
# page sets them.
MODES = (
    hqf_pdf.TextRendering.fill(),
    hqf_pdf.TextRendering.stroke(),
    hqf_pdf.TextRendering.fill_and_stroke(),
    hqf_pdf.TextRendering.invisible(),
)

# Where the label of the third page's first row sits, and the drop from one row to the
# next.
MODE_TOP = 740.0
MODE_STEP = 150.0

# The size the third page's word is painted at.
MODE_SIZE = 54.0

# How far under its label a painted word's baseline sits.
MODE_DROP = 70.0



def line_at(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    text: str,
    size: float,
    at: tuple[float, float],
) -> None:
    """Sets `text` on one line from `x`, its baseline at `y`."""
    content.begin_text()
    content.text_position(at[0], at[1])
    content.set_font(font, size)
    content.show_runs(font.runs(text), size)
    content.end_text()


def painted(content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words) -> None:
    """Draws the third page: the word painted each of the four ways, two to a row, each
    under what the way is called."""
    line_at(content, font, words.modes_title, BACK_TITLE_SIZE, (LEFT, BACK_TITLE_TOP))
    half = WIDTH / 2.0
    top = MODE_TOP
    for index, (mode, label) in enumerate(zip(MODES, words.modes)):
        x = LEFT if index % 2 == 0 else LEFT + half
        content.save_state()
        content.set_fill(hqf_pdf.Rgb.gray(0.35))
        line_at(content, font, label, NOTE_SIZE, (x, top))
        content.restore_state()

        baseline = top - MODE_DROP
        content.save_state()
        content.set_fill(mark_ink())
        content.set_stroke(hqf_pdf.Rgb.gray(0.1))
        content.set_line_width(HOLLOW_PEN)
        content.begin_text()
        content.text_position(x, baseline)
        content.set_font(font, MODE_SIZE)
        content.set_text_rendering(mode)
        content.show_runs(font.runs(words.painted), MODE_SIZE)
        content.end_text()
        content.restore_state()
        if index % 2 == 1:
            top -= MODE_STEP


def main() -> None:
    language = _language.from_environment()
    words = _language.words_of(WORDS, language)

    # A named file is written as named; the default one carries the language, so the two
    # languages do not overwrite each other in `tmp/`.
    out = _out.output_path(
        Path(_language.file_name("text_ornaments.pdf", language)).stem
    )

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    document.set_info("Title", words.title)
    face = hqf_pdf.Font.from_path(_out.font_path())
    encoding = hqf_pdf.SimpleEncoding.fitting(face, words.spaced)
    coded_face = face.with_encoding(hqf_pdf.Encoding.simple(encoding))
    font = document.add_font(face)

    content = hqf_pdf.Content()
    heading(content, font, words)
    standfirst(content, font, words)

    # The tops step down one line at a time rather than being multiplied out, so the
    # Rust twin adds the same numbers in the same order.
    top = FIRST_ROW
    for name, prices in zip(words.goods, PRICES):
        shelf_line(content, font, name, prices, top)
        top -= ROW_STEP

    total(content, font, words)
    legend(content, font, words)
    note(content, font, words)
    mark(content, font, words)

    page = hqf_pdf.Page.a4()
    page.set_content(content)
    document.add_page(page)

    coded = document.add_font(coded_face)
    content = hqf_pdf.Content()
    back(content, font, coded, words)
    page = hqf_pdf.Page.a4()
    page.set_content(content)
    document.add_page(page)

    content = hqf_pdf.Content()
    painted(content, font, words)
    page = hqf_pdf.Page.a4()
    page.set_content(content)
    document.add_page(page)

    written = document.write(out)
    print(f"wrote {out}: {written} bytes")


if __name__ == "__main__":
    main()