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()
|