write_font_fallback.rs

Die Rust-Datei des Beispiels „Eine Schrift hinter der Schrift“. Eine Zeile mit Statusangaben, zweimal gesetzt: nur in einer Hausschrift, in der die Zeichen ohne Glyphe als leere Kästchen herauskommen, dann mit einer zweiten Schrift dahinter, die sie zeichnet.

Rust 804 Zeilen

Wofür dieses Beispiel da ist

Eine Hausschrift hat selten eine Glyphe für jedes Zeichen, das ein Dokument braucht. Ein Haken, ein Stern, ein Schirm: Die Schrift enthält sie einfach nicht, und wo eine Glyphe fehlt, lässt die Anzeigesoftware ein leeres Kästchen. Das Tückische ist, dass die Lücke stillschweigend durchgeht. Die Seite sieht fertig aus, der Druck geht hinaus, und der Kunde findet die Lücke — in der einen Statuszeile von vierzig, die zufällig das Symbol verwendet.

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
//! 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 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: `cargo run --example write_font_fallback -- tmp/font_fallback.pdf`
//!        `HQF_PDF_LANG=fr cargo run --example write_font_fallback --
//! tmp/police_de_secours.pdf`

use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use hqf_pdf::content::Content;
use hqf_pdf::font::MissingGlyph;
use hqf_pdf::{Document, Font, FontHandle, Page, TextFlow};

#[path = "shared/out.rs"]
mod out;

#[path = "shared/licence.rs"]
mod licence;

#[path = "shared/language.rs"]
mod language;

#[path = "shared/failure.rs"]
mod failure;

use language::Language;

/// Converts a row number to a float. A page holds far too few rows for a
/// `usize` to lose precision as an `f64`.
#[expect(
    clippy::cast_precision_loss,
    reason = "a row number on a page is a very small whole number"
)]
const fn count_f64(n: usize) -> f64 {
    n as f64
}

/// Where the committed fonts sit.
fn font_dir() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("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.
const MARKS: [&str; 4] = ["\u{2713}", "\u{2605}", "\u{2602}", "\u{2717}"];

/// 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.
const GAP: &str = " \u{00B7} ";

/// The words the page is written in, one set per 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.
#[derive(Debug)]
struct Words {
    /// The line across the top of the page.
    title: &'static str,
    /// Why a document needs a font behind its own, in two sentences.
    why: &'static str,
    /// The heading over the setting in the house font alone.
    alone: &'static str,
    /// The heading over the setting that falls back.
    chained: &'static str,
    /// The four statuses, each drawn after its mark.
    statuses: [&'static str; 4],
    /// The closing note, cut where the count and the two font names go into it.
    note_before: &'static str,
    note_between: &'static str,
    note_joiner: &'static str,
    note_after: &'static str,
    /// The three headings over the list of marks.
    head_mark: &'static str,
    head_status: &'static str,
    head_font: &'static str,
    /// The heading over the three settings of the house font alone.
    policies: &'static str,
    /// What each policy is called: the box, nothing, and a character in its
    /// place, `{character}` standing for the character.
    policy_names: [&'static str; 3],
    /// What stands before the line set with stand-ins.
    stand_ins: &'static str,
    /// The heading over the two settings of the figures.
    figures: &'static str,
    /// What stands before the figures the house font draws itself.
    own_figures: &'static str,
    /// What stands before the figures the second font draws for it.
    figures_behind: &'static str,
}

impl Words {
    /// The words the page is written in, in `language`.
    fn of(language: Language) -> &'static Self {
        language::pick(&WORDS, language)
    }

    /// The line of statuses, each after its mark.
    fn line(&self) -> String {
        let mut line = String::new();
        for (mark, status) in MARKS.iter().zip(self.statuses) {
            if !line.is_empty() {
                line.push_str(GAP);
            }
            line.push_str(mark);
            line.push(' ');
            line.push_str(status);
        }
        line
    }

    /// The closing note, with the count of stretches and the two font names in
    /// it.
    fn note(&self, stretches: usize, house: &str, behind: &str) -> String {
        format!(
            "{}{stretches}{}{house}{}{behind}{}",
            self.note_before, self.note_between, self.note_joiner, self.note_after
        )
    }
}

/// The page in English.
const ENGLISH: Words = 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.\n\
          A 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",
        "\u{201C}{character}\u{201D} 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.
const FRENCH: Words = 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.
const DUTCH: Words = 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.
const PORTUGUESE: Words = 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.
const ITALIAN: Words = 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.
const SPANISH: Words = 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.
const GERMAN: Words = 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.
static WORDS: [(Language, &Words); 7] = [
    (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.
const X: f64 = 60.0;

/// The width every block is broken to.
const ROOM: f64 = 475.0;

/// The baseline the title sits on.
const TITLE_TOP: f64 = 780.0;

/// The top of the block that says why the page matters.
const WHY_TOP: f64 = 750.0;

/// The top of the heading over the setting in the house font alone.
const ALONE_TOP: f64 = 650.0;

/// The top of the heading over the setting that falls back.
const CHAINED_TOP: f64 = 560.0;

/// The top of the closing note.
const NOTE_TOP: f64 = 470.0;

/// How far a setting sits below the heading over it.
const UNDER_HEADING: f64 = 34.0;

/// The size the statuses are set in.
const LINE_SIZE: f64 = 15.0;

/// The top of the list that names the font behind each mark.
const LIST_TOP: f64 = 400.0;

/// How far apart two rows of that list sit.
const ROW_DROP: f64 = 18.0;

/// Where the three columns of that list begin.
const COLUMNS: [f64; 3] = [X, X + 46.0, X + 176.0];

/// The top of the heading over the three settings of the house font alone.
const POLICIES_TOP: f64 = 280.0;

/// The character the third of those settings draws in place of a missing one.
const STAND_IN: char = '?';

/// The line set with stand-ins: a temperature in kelvins, written with the
/// kelvin sign the house font has no glyph for.
const KELVINS: &str = "273 \u{212A}";

/// The top of the heading over the two settings of the figures.
const FIGURES_TOP: f64 = 140.0;

/// The figures set twice at the foot of the page: the house font has a glyph
/// for each.
const FIGURES: &str = "0123456789";

/// What `policy` is called, in `words`.
#[allow(
    clippy::match_same_arms,
    reason = "a policy the library adds later reads as the default"
)]
fn policy_name(policy: MissingGlyph, words: &Words) -> String {
    match policy {
        MissingGlyph::Notdef => words.policy_names[0].to_owned(),
        MissingGlyph::Drop => words.policy_names[1].to_owned(),
        MissingGlyph::Replace(shown) => {
            words.policy_names[2].replace("{character}", &shown.to_string())
        }
        _ => words.policy_names[0].to_owned(),
    }
}

/// The house font alone, set once under each policy it was told, the policy it
/// reports beside each line.
fn policies(
    c: &mut Content,
    plain: &FontHandle,
    alone: &[FontHandle],
    line: &str,
    words: &Words,
) -> Result<(), hqf_pdf::Error> {
    let heading = TextFlow::new(plain, 9.0);
    let heading_lines = heading.break_lines(words.policies, ROOM);
    c.begin_text();
    heading.draw(c, &heading_lines, X, POLICIES_TOP, ROOM)?;
    c.end_text();
    let mut top = POLICIES_TOP - 24.0;
    for handle in alone {
        let label = TextFlow::new(plain, 9.0);
        let label_lines = label.break_lines(&policy_name(handle.missing_glyph(), words), 150.0);
        c.begin_text();
        label.draw(c, &label_lines, X, top, 150.0)?;
        c.end_text();
        let statuses = TextFlow::new(handle, 11.0);
        let status_lines = statuses.break_lines(line, ROOM - 160.0);
        c.begin_text();
        statuses.draw(c, &status_lines, X + 160.0, top, ROOM - 160.0)?;
        c.end_text();
        top -= 24.0;
    }

    let label = TextFlow::new(plain, 9.0);
    let label_lines = label.break_lines(words.stand_ins, 150.0);
    c.begin_text();
    label.draw(c, &label_lines, X, top, 150.0)?;
    c.end_text();
    let standing = plain.clone().with_stand_ins();
    let kelvins = TextFlow::new(&standing, 11.0);
    let kelvin_lines = kelvins.break_lines(KELVINS, ROOM - 160.0);
    c.begin_text();
    kelvins.draw(c, &kelvin_lines, X + 160.0, top, ROOM - 160.0)?;
    c.end_text();
    Ok(())
}

/// The ten figures set twice: in the house font, and in the house font told to
/// take them from `behind` though it has a glyph for each, each after the words
/// that say which.
fn figures(
    c: &mut Content,
    plain: &FontHandle,
    behind: &FontHandle,
    words: &Words,
) -> Result<(), hqf_pdf::Error> {
    let heading = TextFlow::new(plain, 9.0);
    let heading_lines = heading.break_lines(words.figures, ROOM);
    c.begin_text();
    heading.draw(c, &heading_lines, X, FIGURES_TOP, ROOM)?;
    c.end_text();
    let taken = plain
        .clone()
        .with_characters_drawn_by('0'..='9', behind.clone());
    let mut top = FIGURES_TOP - 24.0;
    for (said, handle) in [(words.own_figures, plain), (words.figures_behind, &taken)] {
        let label = TextFlow::new(plain, 9.0);
        let label_lines = label.break_lines(said, 150.0);
        c.begin_text();
        label.draw(c, &label_lines, X, top, 150.0)?;
        c.end_text();
        let set = TextFlow::new(handle, 11.0);
        let set_lines = set.break_lines(FIGURES, ROOM - 160.0);
        c.begin_text();
        set.draw(c, &set_lines, X + 160.0, top, ROOM - 160.0)?;
        c.end_text();
        top -= 24.0;
    }
    Ok(())
}

fn main() -> std::process::ExitCode {
    failure::reported(run())
}

fn run() -> Result<(), Box<dyn std::error::Error>> {
    let language = Language::from_environment()?;
    let words = Words::of(language);
    let line = words.line();

    let out = env::args()
        .nth(1)
        .unwrap_or_else(|| language.file_name(&out::default_path("font_fallback")));

    let house = Font::parse(fs::read(font_dir().join("IBMPlexSans-Regular.otf"))?)?;
    let told = [
        house.missing_glyph(),
        MissingGlyph::Drop,
        MissingGlyph::Replace(STAND_IN),
    ];
    let behind = Font::parse(fs::read(font_dir().join("DejaVuSans.ttf"))?)?;
    let house_name = house.postscript_name().to_owned();
    let behind_name = behind.postscript_name().to_owned();

    let mut doc = Document::new();
    doc.set_license(licence::licensed());
    let alone = told.map(|policy| doc.add_font(house.clone().with_missing_glyph(policy)));
    let plain = doc.add_font(house);
    let spare = doc.add_font(behind);
    let chain = plain.clone().falling_back_to(spare.clone());

    let mut c = Content::new();

    let title = TextFlow::new(&plain, 18.0);
    let title_lines = title.break_lines(words.title, ROOM);
    c.begin_text();
    title.draw(&mut c, &title_lines, X, TITLE_TOP, ROOM)?;
    c.end_text();

    // 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.
    let why = TextFlow::new(&chain, 10.0).leading(14.0);
    let why_lines = why.break_lines(words.why, ROOM);
    c.begin_text();
    why.draw(&mut c, &why_lines, X, WHY_TOP, ROOM)?;
    c.end_text();

    // 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),
    ] {
        let label = TextFlow::new(&plain, 9.0);
        let label_lines = label.break_lines(heading, ROOM);
        c.begin_text();
        label.draw(&mut c, &label_lines, X, top, ROOM)?;
        c.end_text();

        let statuses = TextFlow::new(handle, LINE_SIZE);
        let status_lines = statuses.break_lines(&line, ROOM);
        c.begin_text();
        statuses.draw(&mut c, &status_lines, X, top - UNDER_HEADING, ROOM)?;
        c.end_text();
    }

    // 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.
    let note = words.note(chain.runs(&line).len(), &house_name, &behind_name);
    let closing = TextFlow::new(&chain, 10.0).leading(14.0);
    let closing_lines = closing.break_lines(&note, ROOM);
    c.begin_text();
    closing.draw(&mut c, &closing_lines, X, NOTE_TOP, ROOM)?;
    c.end_text();

    // 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.
    let heads = [words.head_mark, words.head_status, words.head_font];
    let head_row = TextFlow::new(&plain, 9.0);
    for (column, head) in COLUMNS.iter().zip(heads) {
        let lines = head_row.break_lines(head, ROOM);
        c.begin_text();
        head_row.draw(&mut c, &lines, *column, LIST_TOP, ROOM)?;
        c.end_text();
    }

    for (row, (mark, status)) in MARKS.iter().zip(words.statuses).enumerate() {
        let drawn_by = if chain
            .runs(mark)
            .first()
            .is_some_and(|run| run.font() == plain.name())
        {
            &house_name
        } else {
            &behind_name
        };
        let top = ROW_DROP.mul_add(-count_f64(row + 1), LIST_TOP);
        for (column, cell) in COLUMNS.iter().zip([mark, status, drawn_by.as_str()]) {
            let flow = TextFlow::new(&chain, 10.0);
            let lines = flow.break_lines(cell, ROOM);
            c.begin_text();
            flow.draw(&mut c, &lines, *column, top, ROOM)?;
            c.end_text();
        }
    }

    policies(&mut c, &plain, &alone, &line, words)?;
    figures(&mut c, &plain, &spare, words)?;

    let mut page = Page::a4();
    page.content = c.into_bytes();
    doc.add_page(page)?;

    let bytes = doc.to_bytes()?;
    if let Some(parent) = Path::new(&out).parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(&out, &bytes)?;
    println!("wrote {out}: {} bytes", bytes.len());
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{
        ALONE_TOP, CHAINED_TOP, LINE_SIZE, MARKS, NOTE_TOP, ROOM, UNDER_HEADING, WORDS, language,
    };
    use hqf_pdf::{Document, Font, TextFlow};

    /// The lines two languages are allowed to write the same way, where two
    /// languages spell a word alike.
    const SPARED: [&str; 3] = [
        // Written the same way in German.
        r#"head_status: "Status""#,
        // Written the same way in Portuguese.
        r#"head_mark: "Marca""#,
        r#"head_status: "Estado""#,
    ];

    /// The two committed fonts, added to a document of their own.
    fn fonts() -> (Document, hqf_pdf::FontHandle, hqf_pdf::FontHandle) {
        let dir = super::font_dir();
        let mut doc = Document::new();
        let house = doc.add_font(
            Font::parse(std::fs::read(dir.join("IBMPlexSans-Regular.otf")).expect("the font"))
                .expect("the font parses"),
        );
        let behind = doc.add_font(
            Font::parse(std::fs::read(dir.join("DejaVuSans.ttf")).expect("the font"))
                .expect("the font parses"),
        );
        (doc, house, behind)
    }

    #[test]
    fn every_language_draws_the_page_in_its_own_words() {
        let untranslated = language::untranslated_lines(&WORDS, &SPARED);

        assert!(
            untranslated.is_empty(),
            "the page says these in more than one language: {untranslated:?}"
        );
    }

    /// The demonstration rests on the house font lacking three of the four
    /// marks and having the fourth: a page whose every mark were drawn by one
    /// font would show nothing at all.
    #[test]
    fn the_house_font_has_one_of_the_marks_and_not_the_other_three() {
        let (_doc, house, behind) = fonts();

        assert!(house.uncovered(MARKS[0]).is_empty(), "the tick is in both");
        for mark in &MARKS[1..] {
            assert_eq!(
                house.uncovered(mark).len(),
                1,
                "the house font has no glyph for {mark}"
            );
            assert!(
                behind.uncovered(mark).is_empty(),
                "the font behind it has one for {mark}"
            );
        }
    }

    /// The line of statuses fits the room it is given in every language, at the
    /// size it is set in.
    #[test]
    fn every_language_sets_its_statuses_on_one_line() {
        let (_doc, house, behind) = fonts();
        let chain = house.falling_back_to(behind);

        for (language, words) in WORDS {
            let line = words.line();
            let width = chain.measure(&line, LINE_SIZE);
            assert!(
                width <= ROOM,
                "{language:?} sets {width} points of statuses in {ROOM}"
            );
            assert_eq!(
                TextFlow::new(&chain, LINE_SIZE)
                    .break_lines(&line, ROOM)
                    .len(),
                1,
                "{language:?} keeps its statuses on one line"
            );
        }
    }

    /// Every language cuts its line into the same number of stretches: the
    /// marks decide where the chain changes font, and the marks are not
    /// language.
    #[test]
    fn every_language_cuts_its_line_into_the_same_stretches() {
        let (_doc, house, behind) = fonts();
        let chain = house.falling_back_to(behind);

        let counts: Vec<usize> = WORDS
            .iter()
            .map(|(_, words)| chain.runs(&words.line()).len())
            .collect();

        // Three marks the house font lacks, each with the words either side of
        // it: seven stretches in all.
        assert!(counts.iter().all(|&count| count == 7), "{counts:?}");
    }

    /// Nothing the page draws runs off the paper: every block is set at a size
    /// and in a room that hold it.
    #[test]
    fn every_language_writes_blocks_no_wider_than_the_room_they_have() {
        let (_doc, house, behind) = fonts();
        let chain = house.clone().falling_back_to(behind);

        for (language, words) in WORDS {
            for (text, size, handle) in [
                (words.title, 18.0, &house),
                (words.alone, 9.0, &house),
                (words.chained, 9.0, &house),
                (words.figures, 9.0, &house),
            ] {
                let width = handle.measure(text, size);
                assert!(
                    width <= ROOM,
                    "{language:?} writes {width} points in {ROOM}: {text}"
                );
            }
            // The two labels of the figures stand on one line each, beside
            // them.
            for label in [words.own_figures, words.figures_behind] {
                let width = house.measure(label, 9.0);
                assert!(
                    width <= 150.0,
                    "{language:?} writes {width} points in 150: {label}"
                );
            }

            let note = words.note(7, "IBMPlexSans-Regular", "DejaVuSans");
            let lines = TextFlow::new(&chain, 10.0)
                .leading(14.0)
                .break_lines(&note, ROOM);
            for line in &lines {
                assert!(
                    line.natural_width() <= ROOM,
                    "{language:?} sets a line of the note wider than {ROOM}"
                );
            }
        }
    }

    /// The setting that falls back stands clear of the note below it, and the
    /// setting in the house font alone stands clear of the heading under it.
    #[test]
    fn nothing_the_page_draws_lands_on_what_comes_after_it() {
        let (_doc, house, behind) = fonts();
        let chain = house.falling_back_to(behind);

        for (language, words) in WORDS {
            let flow = TextFlow::new(&chain, LINE_SIZE);
            let lines = flow.break_lines(&words.line(), ROOM);
            let bottom = CHAINED_TOP - UNDER_HEADING - flow.height(&lines);
            assert!(
                bottom > NOTE_TOP,
                "{language:?} sets its statuses down to {bottom}, over a note at {NOTE_TOP}"
            );
            assert!(
                ALONE_TOP - UNDER_HEADING - flow.height(&lines) > CHAINED_TOP,
                "{language:?} sets its first line of statuses over the heading below it"
            );
        }
    }
}