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 | """Sets one line of statuses twice: above in a house font alone, below with a
second font behind it, so the characters the house font has no glyph for can be
seen appearing.
The Python twin of the `write_font_fallback` example in Rust. The house font
here is IBM Plex Sans, which draws a tick but neither a star, an umbrella nor a
cross. Set alone, those three come out as the empty box every font carries for a
character it does not have. Set through a chain, each is handed to DejaVu Sans,
which does have it, and the line is drawn in as many fonts as it takes.
The closing note counts the stretches the chain cuts the line into, and names the
two fonts as the files themselves name them.
Under the list, the house font is set alone three more times, each time told something
else to draw for a character it lacks: the box every font carries, which is what it
does unless told otherwise; nothing at all; and a question mark in its place. Each line
is labelled with the policy the font reports.
A last line sets a temperature in kelvins with stand-ins turned on: the house font has no
kelvin sign, and draws the capital K it stands for.
The foot of the page sets the ten figures twice: in the house font, which has a glyph
for each, and in the house font told to take the figures from the second font all the
same, so the second font's figures stand where the house font's would.
The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which set
is drawn. The four symbols are not language: they stand the same in every one.
Usage: python examples/write_font_fallback.py [out.pdf]
HQF_PDF_LANG=fr python examples/write_font_fallback.py
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import _language
import _licence
import _out
import hqf_pdf
# Where the committed fonts sit.
FONT_DIR = _out.ROOT / "crates" / "hqf-pdf" / "tests" / "fonts"
# The four symbols the statuses are marked with, in the order they are drawn.
#
# The tick is in both fonts; the star, the umbrella and the cross are in the second
# alone, which is what the two settings show.
MARKS = ("✓", "★", "☂", "✗")
# What stands between two statuses on the line. A run of spaces would be set as one, so
# the statuses are held apart by a character.
GAP = " · "
@dataclass(frozen=True)
class Words:
"""Every word the page draws, in one language.
What is not language stays out of it: the four symbols are marks, and the two font
names are what the files call themselves.
"""
# The line across the top of the page.
title: str
# Why a document needs a font behind its own, in two sentences.
why: str
# The heading over the setting in the house font alone.
alone: str
# The heading over the setting that falls back.
chained: str
# The four statuses, each drawn after its mark.
statuses: tuple[str, str, str, str]
# The closing note, cut where the count and the two font names go into it.
note_before: str
note_between: str
note_joiner: str
note_after: str
# The three headings over the list of marks.
head_mark: str
head_status: str
head_font: str
# The heading over the three settings of the house font alone.
policies: str
# What each policy is called: the box, nothing, and a character in its place,
# `{character}` standing for the character.
policy_names: tuple[str, str, str]
# What stands before the line set with stand-ins.
stand_ins: str
# The heading over the two settings of the figures.
figures: str
# What stands before the figures the house font draws itself.
own_figures: str
# What stands before the figures the second font draws for it.
figures_behind: str
def line(self) -> str:
"""Return the line of statuses, each after its mark."""
return GAP.join(f"{mark} {status}" for mark, status in zip(MARKS, self.statuses))
def note(self, stretches: int, house: str, behind: str) -> str:
"""Return the closing note, with the count and the two font names in it."""
return (
f"{self.note_before}{stretches}{self.note_between}"
f"{house}{self.note_joiner}{behind}{self.note_after}"
)
# The page in English.
ENGLISH = Words(
title="A font behind the font",
why=(
"A house font rarely covers every character a document has to draw. Without a "
"second font behind it, a character it has no glyph for comes out as the empty "
"box below.\nA chain hands that character to the next font instead, so the "
"line is set in as many fonts as it takes and every mark is drawn."
),
alone="The house font alone: what it has no glyph for, it cannot draw.",
chained="The house font, with a second one behind it.",
statuses=(
"Paid",
"Priority",
"Weather hold",
"Refused",
),
note_before="The line above is cut into ",
note_between=" stretches, drawn by two fonts: ",
note_joiner=", then ",
note_after=". The words are in the first; the marks it lacks are in the second.",
head_mark="Mark",
head_status="Status",
head_font="Drawn by",
policies="The house font alone, told three ways what to draw for what it lacks:",
policy_names=(
"the box every font carries",
"nothing at all",
"“{character}” in its place",
),
stand_ins="told to draw what it lacks as the character it stands for",
figures="The house font's figures, drawn once by the house font and once by the second font:",
own_figures="its own figures",
figures_behind="the second font's figures",
)
# The page in French.
FRENCH = Words(
title="Une police derrière la police",
why=(
"Une police d’entreprise couvre rarement tous les caractères qu’un document "
"doit afficher. Sans seconde police derrière elle, un caractère dont elle n’a "
"pas le glyphe sort en case vide, comme ci-dessous.\nUne chaîne, elle, confie "
"ce caractère à la police suivante : la ligne est composée dans autant de "
"polices qu’il faut, et chaque signe s’affiche."
),
alone=(
"La police d’entreprise seule : faute de glyphe, le caractère sort en case "
"vide."
),
chained="La police d’entreprise, avec une seconde police derrière elle.",
statuses=(
"Payé",
"Prioritaire",
"Attente météo",
"Refusé",
),
note_before="La ligne du dessus est découpée en ",
note_between=" tronçons, composés avec deux polices : ",
note_joiner=", puis ",
note_after=(
". Les mots viennent de la première ; les signes qui lui manquent, de la "
"seconde."
),
head_mark="Signe",
head_status="État",
head_font="Police",
policies=(
"La police d’entreprise seule, avec trois consignes pour ce qui lui manque :"
),
policy_names=(
"la case que contient toute police",
"rien du tout",
"« {character} » à sa place",
),
stand_ins=(
"avec pour consigne de remplacer un caractère qui lui manque par celui dont il "
"tient lieu"
),
figures="La police de l’entreprise compose les dix chiffres avec les siens, puis avec ceux de la seconde police :",
own_figures="ses propres chiffres",
figures_behind="chiffres de la seconde police",
)
# The page in Dutch.
DUTCH = Words(
title="Een lettertype achter het lettertype",
why=(
"Een huislettertype dekt zelden elk teken dat een document moet tekenen. "
"Zonder een tweede lettertype erachter komt een teken waarvoor het geen glyph "
"heeft eruit als het lege vakje hieronder.\nEen keten geeft dat teken door "
"aan het volgende lettertype, dus wordt de regel gezet in zoveel lettertypen "
"als nodig en wordt elk teken getekend."
),
alone="Het huislettertype alleen: wat het niet heeft, kan het niet tekenen.",
chained="Het huislettertype, met een tweede erachter.",
statuses=(
"Betaald",
"Voorrang",
"Weersvertraging",
"Geweigerd",
),
note_before="De regel hierboven is opgeknipt in ",
note_between=" stukken, getekend door twee lettertypen: ",
note_joiner=", dan ",
note_after=". De woorden staan in het eerste; de tekens die het mist, in het tweede.",
head_mark="Teken",
head_status="Status",
head_font="Getekend door",
policies="Het huislettertype alleen, op drie manieren verteld wat het moet tekenen voor wat het mist:",
policy_names=(
"het vakje dat elk lettertype heeft",
"helemaal niets",
"„{character}” op zijn plaats",
),
stand_ins="gevraagd om wat het mist te tekenen als het teken waar het voor staat",
figures="De cijfers van het huislettertype, eerst getekend door het huislettertype zelf, daarna door het tweede lettertype:",
own_figures="zijn eigen cijfers",
figures_behind="die van het tweede lettertype",
)
# The page in Portuguese.
PORTUGUESE = Words(
title="Um tipo de letra por trás do tipo de letra",
why=(
"Um tipo de letra da casa raramente cobre todos os caracteres que um "
"documento tem de desenhar. Sem um segundo por trás, um carácter para o qual "
"não tem glifo sai como a caixa vazia abaixo.\nUma cadeia passa esse carácter "
"ao tipo de letra seguinte, por isso a linha compõe-se com tantos tipos de "
"letra quantos forem precisos e cada marca é desenhada."
),
alone="O tipo de letra da casa sozinho: o que não tem, não o pode desenhar.",
chained="O tipo de letra da casa, com um segundo por trás.",
statuses=(
"Pago",
"Prioridade",
"Retido pelo mau tempo",
"Recusado",
),
note_before="A linha acima é cortada em ",
note_between=" troços, desenhados por dois tipos de letra: ",
note_joiner=", depois ",
note_after=". As palavras estão no primeiro; as marcas que lhe faltam, no segundo.",
head_mark="Marca",
head_status="Estado",
head_font="Desenhado por",
policies="O tipo de letra da casa sozinho, com três formas de lhe dizer o que desenhar no lugar do que lhe falta:",
policy_names=(
"a caixa que todo o tipo de letra leva",
"nada",
"«{character}» no seu lugar",
),
stand_ins="pede-se-lhe que desenhe o que lhe falta como o carácter que representa",
figures="Os dez algarismos, desenhados primeiro pelo tipo de letra da casa e depois pelo segundo tipo de letra:",
own_figures="os seus algarismos",
figures_behind="os do segundo tipo de letra",
)
# The page in Italian.
ITALIAN = Words(
title="Un font dietro il font",
why=(
"Un font aziendale copre di rado ogni carattere che un documento deve "
"disegnare. Senza un secondo font dietro, un carattere per cui non ha glifo "
"esce come il riquadro vuoto qui sotto.\nUna catena passa quel carattere al "
"font successivo, così la riga si compone con quanti font servono e ogni "
"segno è disegnato."
),
alone="Il font aziendale da solo: ciò per cui non ha glifo, non lo può disegnare.",
chained="Il font aziendale, con un secondo dietro.",
statuses=(
"Pagato",
"Priorità",
"Fermo per maltempo",
"Rifiutato",
),
note_before="La riga qui sopra è tagliata in ",
note_between=" tratti, disegnati da due font: ",
note_joiner=", poi ",
note_after=". Le parole sono nel primo; i segni che gli mancano, nel secondo.",
head_mark="Segno",
head_status="Stato",
head_font="Disegnato da",
policies="Il font aziendale da solo, con tre modi di dirgli cosa disegnare per ciò che gli manca:",
policy_names=(
"il riquadro che ogni font porta",
"niente del tutto",
"«{character}» al suo posto",
),
stand_ins="gli si chiede di disegnare ciò che gli manca come il carattere che rappresenta",
figures="Le cifre del font aziendale, disegnate prima dal font stesso, poi dal secondo font:",
own_figures="le cifre del font aziendale",
figures_behind="le cifre del secondo font",
)
# The page in Spanish.
SPANISH = Words(
title="Una fuente detrás de la fuente",
why=(
"Una fuente de la casa rara vez cubre todos los caracteres que un documento "
"tiene que dibujar. Sin una segunda fuente detrás, un carácter para el que no "
"tiene glifo sale como la caja vacía de abajo.\nUna cadena de fuentes pasa "
"ese carácter a la fuente siguiente, así que la línea se compone con tantas "
"fuentes como haga falta y cada marca se dibuja."
),
alone="La fuente de la casa sola: lo que no tiene, no lo puede dibujar.",
chained="La fuente de la casa, con una segunda detrás.",
statuses=(
"Pagado",
"Prioridad",
"Retenido por mal tiempo",
"Rechazado",
),
note_before="La línea de arriba se corta en ",
note_between=" tramos, dibujados por dos fuentes: ",
note_joiner=", luego ",
note_after=". Las palabras están en la primera; las marcas que le faltan, en la segunda.",
head_mark="Marca",
head_status="Estado",
head_font="Dibujado por",
policies="La fuente de la casa sola, con tres formas de decirle qué dibujar en lugar de lo que le falta:",
policy_names=(
"la caja que lleva toda fuente",
"nada en absoluto",
"«{character}» en su lugar",
),
stand_ins="se le pide dibujar lo que le falta como el carácter que representa",
figures="Las cifras, dibujadas una vez por la fuente de la casa y otra por la segunda fuente:",
own_figures="cifras propias",
figures_behind="cifras de la segunda fuente",
)
# The page in German.
GERMAN = Words(
title="Eine Schrift hinter der Schrift",
why=(
"Eine Hausschrift deckt selten jedes Zeichen ab, das ein Dokument zeichnen "
"muss. Ohne eine zweite Schrift dahinter erscheint ein Zeichen, für das sie "
"keinen Glyphen hat, als das leere Kästchen unten.\nEine Kette reicht dieses "
"Zeichen stattdessen an die nächste Schrift weiter, sodass die Zeile in so "
"vielen Schriften wie nötig gesetzt wird und jedes Zeichen gezeichnet wird."
),
alone="Die Hausschrift allein: Wofür sie keinen Glyphen hat, das kann sie nicht zeichnen.",
chained="Die Hausschrift mit einer zweiten dahinter.",
statuses=(
"Bezahlt",
"Vorrang",
"Wetterbedingt angehalten",
"Abgelehnt",
),
note_before="Die Zeile oben ist in ",
note_between=" Abschnitte geteilt, von zwei Schriften gezeichnet: ",
note_joiner=", dann ",
note_after=". Die Wörter stehen in der ersten; die Zeichen, die ihr fehlen, in der zweiten.",
head_mark="Zeichen",
head_status="Status",
head_font="Gezeichnet von",
policies="Die Hausschrift allein, auf drei Arten angewiesen, was sie für Fehlendes zeichnen soll:",
policy_names=(
"das Kästchen, das jede Schrift mitbringt",
"gar nichts",
"„{character}“ an seiner Stelle",
),
stand_ins="angewiesen, Fehlendes als das Zeichen zu zeichnen, für das es steht",
figures="Die Hausschrift, einmal mit ihren eigenen Ziffern, einmal mit denen der zweiten Schrift:",
own_figures="eigene Ziffern",
figures_behind="Ziffern der zweiten Schrift",
)
# Every language the example is written in. A language is added by writing its own set
# of words and naming it here.
WORDS = {_language.ENGLISH: ENGLISH, _language.FRENCH: FRENCH, _language.GERMAN: GERMAN, _language.SPANISH: SPANISH, _language.ITALIAN: ITALIAN, _language.PORTUGUESE: PORTUGUESE, _language.DUTCH: DUTCH}
# The left edge of everything on the page.
X = 60.0
# The width every block is broken to.
ROOM = 475.0
# The baseline the title sits on.
TITLE_TOP = 780.0
# The top of the block that says why the page matters.
WHY_TOP = 750.0
# The top of the heading over the setting in the house font alone.
ALONE_TOP = 650.0
# The top of the heading over the setting that falls back.
CHAINED_TOP = 560.0
# The top of the closing note.
NOTE_TOP = 470.0
# How far a setting sits below the heading over it.
UNDER_HEADING = 34.0
# The size the statuses are set in.
LINE_SIZE = 15.0
# The top of the list that names the font behind each mark.
LIST_TOP = 400.0
# How far apart two rows of that list sit.
ROW_DROP = 18.0
# Where the three columns of that list begin.
COLUMNS = (X, X + 46.0, X + 176.0)
# The top of the heading over the three settings of the house font alone.
POLICIES_TOP = 280.0
# The character the third of those settings draws in place of a missing one.
STAND_IN = "?"
# The line set with stand-ins: a temperature in kelvins, written with the kelvin sign
# the house font has no glyph for.
KELVINS = "273 \u212a"
# The top of the heading over the two settings of the figures.
FIGURES_TOP = 140.0
# The figures set twice at the foot of the page: the house font has a glyph for each.
FIGURES = "0123456789"
def policy_name(policy, words) -> str:
"""What `policy` is called, in `words`."""
if policy == hqf_pdf.MissingGlyph.drop():
return words.policy_names[1]
if policy == hqf_pdf.MissingGlyph.replace(STAND_IN):
return words.policy_names[2].replace("{character}", STAND_IN)
return words.policy_names[0]
def main() -> None:
language = _language.from_environment()
words = _language.words_of(WORDS, language)
line = words.line()
out = _out.output_path(Path(_language.file_name("font_fallback.pdf", language)).stem)
house = hqf_pdf.Font.from_path(FONT_DIR / "IBMPlexSans-Regular.otf")
told = (
house.missing_glyph,
hqf_pdf.MissingGlyph.drop(),
hqf_pdf.MissingGlyph.replace(STAND_IN),
)
behind = hqf_pdf.Font.from_path(FONT_DIR / "DejaVuSans.ttf")
house_name = house.postscript_name
behind_name = behind.postscript_name
document = hqf_pdf.Document()
document.set_license(_licence.licensed())
alone = [document.add_font(house.with_missing_glyph(policy)) for policy in told]
plain = document.add_font(house)
spare = document.add_font(behind)
chain = plain.falling_back_to(spare)
content = hqf_pdf.Content()
title = hqf_pdf.TextFlow(plain, 18.0)
title.draw(content, title.break_lines(words.title, ROOM), X, TITLE_TOP, ROOM)
# The block that says why the page matters is set through the chain: it is ordinary
# prose, and the chain draws ordinary prose exactly as the house font alone would.
why = hqf_pdf.TextFlow(chain, 10.0, leading=14.0)
why.draw(content, why.break_lines(words.why, ROOM), X, WHY_TOP, ROOM)
# The same line twice: once in the house font on its own, once through the chain.
# Only the font differs.
for top, heading, handle in (
(ALONE_TOP, words.alone, plain),
(CHAINED_TOP, words.chained, chain),
):
label = hqf_pdf.TextFlow(plain, 9.0)
label.draw(content, label.break_lines(heading, ROOM), X, top, ROOM)
statuses = hqf_pdf.TextFlow(handle, LINE_SIZE)
statuses.draw(
content,
statuses.break_lines(line, ROOM),
X,
top - UNDER_HEADING,
ROOM,
)
# What the chain made of the line, counted off the chain itself rather than written
# down: the note says what the page above it holds.
note = words.note(len(chain.runs(line)), house_name, behind_name)
closing = hqf_pdf.TextFlow(chain, 10.0, leading=14.0)
closing.draw(content, closing.break_lines(note, ROOM), X, NOTE_TOP, ROOM)
# Which font drew which mark, read off the chain: a mark the house font has is drawn
# by it, and the rest by the font behind it.
head_row = hqf_pdf.TextFlow(plain, 9.0)
for column, head in zip(COLUMNS, (words.head_mark, words.head_status, words.head_font)):
head_row.draw(content, head_row.break_lines(head, ROOM), column, LIST_TOP, ROOM)
for row, (mark, status) in enumerate(zip(MARKS, words.statuses)):
runs = chain.runs(mark)
drawn_by = house_name if runs and runs[0].font == plain.name else behind_name
top = LIST_TOP - ROW_DROP * float(row + 1)
for column, cell in zip(COLUMNS, (mark, status, drawn_by)):
flow = hqf_pdf.TextFlow(chain, 10.0)
flow.draw(content, flow.break_lines(cell, ROOM), column, top, ROOM)
heading = hqf_pdf.TextFlow(plain, 9.0)
heading_lines = heading.break_lines(words.policies, ROOM)
heading.draw(content, heading_lines, X, POLICIES_TOP, ROOM)
top = POLICIES_TOP - 24.0
for handle in alone:
label = hqf_pdf.TextFlow(plain, 9.0)
named = policy_name(handle.missing_glyph, words)
label.draw(content, label.break_lines(named, 150.0), X, top, 150.0)
statuses = hqf_pdf.TextFlow(handle, 11.0)
statuses.draw(
content,
statuses.break_lines(line, ROOM - 160.0),
X + 160.0,
top,
ROOM - 160.0,
)
top -= 24.0
label = hqf_pdf.TextFlow(plain, 9.0)
label.draw(content, label.break_lines(words.stand_ins, 150.0), X, top, 150.0)
kelvins = hqf_pdf.TextFlow(plain.with_stand_ins(), 11.0)
kelvins.draw(content, kelvins.break_lines(KELVINS, ROOM - 160.0), X + 160.0, top, ROOM - 160.0)
# The ten figures set twice: in the house font, and in the house font told to take
# them from the second font though it has a glyph for each.
heading = hqf_pdf.TextFlow(plain, 9.0)
heading.draw(content, heading.break_lines(words.figures, ROOM), X, FIGURES_TOP, ROOM)
taken = plain.with_characters_drawn_by("0", "9", spare)
top = FIGURES_TOP - 24.0
for said, handle in ((words.own_figures, plain), (words.figures_behind, taken)):
label = hqf_pdf.TextFlow(plain, 9.0)
label.draw(content, label.break_lines(said, 150.0), X, top, 150.0)
figures = hqf_pdf.TextFlow(handle, 11.0)
figures.draw(content, figures.break_lines(FIGURES, ROOM - 160.0), X + 160.0, top, ROOM - 160.0)
top -= 24.0
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()
|