write_type_specimen.rs

The Rust file of the “Every Free Font, One Page Each” example. The twenty-six free fonts the library carries in place of the fourteen built-in ones, one page each: eight sizes, the same line in the built-in font and in the free one, and every character the font shows.

Rust 773 lines

What This Example Is For

A file that must carry every font it uses — an invoice kept for ten years, a file sent to a printer — cannot lean on the fourteen fonts all reading software holds. The library carries in their place free fonts drawn to the same widths, so a line keeps its length and your layout does not move.

What This Example Shows

  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
//! Writes a type specimen of the free faces the library carries in place of the
//! fourteen standard fonts: one page for each face, then a page of stamps and
//! notes set in the standard fonts.
//!
//! A first page lists the twenty-six faces: the twelve Liberation faces and the
//! twelve TeX Gyre faces, each drawn to the widths of one of the twelve
//! standard text fonts, then HQF Symbol and HQF Dingbats, drawn to the widths
//! of Symbol and `ZapfDingbats`.
//!
//! Each face then has a page of its own: its name, the standard font it stands
//! in for and its licence; a line set at eight sizes, from 8 to 48 points; the
//! same line set twice at 14 points, first in the standard font, which the
//! reading software draws from its own copy and the file does not carry, then
//! in the free face, which the file carries, each with the width it comes out
//! at; and every character the face reaches through the standard font's codes.
//!
//! The last page holds two stamps and two notes whose words are set in standard
//! fonts: the file carries, for each, the free face drawn to that font's
//! widths, so the reading software draws them with the face the file holds.
//!
//! The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks
//! which set the specimen is written in.
//!
//! Usage: `cargo run --example write_type_specimen -- tmp/specimen.pdf`
//!        `HQF_PDF_LANG=fr cargo run --example write_type_specimen`

use std::env;
use std::fs;
use std::path::Path;

use hqf_pdf::annotation::{Annotation, FreeText, Stamp, StampIcon};
use hqf_pdf::content::Content;
use hqf_pdf::font::standard::FreeFamily;
use hqf_pdf::layout::{Slant, Stamp as StampWords};
use hqf_pdf::{Document, Font, FontHandle, Page, Rgb, StandardFont, 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;

/// The words the specimen is written in, one set per language.
#[derive(Debug)]
struct Words {
    /// What the specimen is called, at the head of the first page.
    title: &'static str,
    /// The paragraph under the title.
    prose: &'static str,
    /// The heading over the list of faces.
    contents: &'static str,
    /// Which page a face is on; `{page}` stands for its number.
    page: &'static str,
    /// The line under a face's name; `{standard}` stands for the standard font
    /// it is drawn to the widths of, `{licence}` for its licence.
    drawn_to: &'static str,
    /// The heading over the line set at eight sizes.
    sizes: &'static str,
    /// The heading over the line set twice.
    compared: &'static str,
    /// What the first of the two lines is set in; `{standard}` stands for the
    /// standard font.
    in_standard: &'static str,
    /// What the second is set in; `{face}` stands for the free face.
    in_free: &'static str,
    /// How wide a line comes out; `{width}` stands for the width in points.
    width: &'static str,
    /// The heading over every character; `{count}` stands for how many.
    characters: &'static str,
    /// The last page's heading.
    stamps_title: &'static str,
    /// The paragraph under it.
    stamps_prose: &'static str,
    /// What the first stamp says.
    approved: &'static str,
    /// What the second stamp says.
    confidential: &'static str,
    /// What the two notes say.
    notes: [&'static str; 2],
    /// What a stamp or a note is set in, and what the file carries for it;
    /// `{standard}` and `{face}` stand for them.
    carried: &'static str,
    /// The line set at every size.
    pangram: &'static str,
    /// The mark between the units and the decimals of a figure.
    decimal: char,
}

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

/// The specimen in English.
const ENGLISH: Words = Words {
    title: "The free faces, one by one",
    prose: "A PDF may name fourteen standard fonts without carrying them: the reading software \
            draws them from its own copies. Where a file must carry every font it uses, as an \
            archival file must, the library carries instead a free face drawn to the same widths, \
            so the text keeps its place to within a tenth of a point. Each page that follows shows \
            one of these faces at eight sizes, set against the standard font it stands in for, \
            and every character it reaches.",
    contents: "The twenty-six faces",
    page: "page {page}",
    drawn_to: "Drawn to the widths of {standard} · {licence}",
    sizes: "From 8 to 48 points",
    compared: "The same line at 14 points, in the standard font, then in the free face",
    in_standard: "{standard}, drawn by the reading software from its own copy",
    in_free: "{face}, carried in the file",
    width: "{width} points wide",
    characters: "The {count} characters the face reaches through the standard font's codes",
    stamps_title: "Stamps and notes set in a standard font",
    stamps_prose: "These two stamps and these two notes name a standard font. The file carries, for \
                   each, the free face drawn to that font's widths, so every reading software \
                   draws them with the same letters.",
    approved: "APPROVED",
    confidential: "CONFIDENTIAL",
    notes: [
        "Checked against the order of 3 October.",
        "Total to confirm before payment.",
    ],
    carried: "{standard}, carried as {face}",
    pangram: "Sphinx of black quartz, judge my vow",
    decimal: '.',
};

/// The specimen in French.
const FRENCH: Words = Words {
    title: "Les polices libres, une à une",
    prose: "Un PDF peut nommer quatorze polices standard sans les contenir\u{00A0}: le logiciel \
            de lecture fournit les siennes. Quand un fichier doit contenir toutes ses polices, \
            comme un fichier d’archivage, la bibliothèque y incorpore à la place une police libre \
            aux mêmes largeurs\u{00A0}: le texte garde sa place à un dixième de point près. Chaque \
            page qui suit présente l’une de ces polices en huit tailles, face à la police \
            standard qu’elle remplace, puis tous les caractères qu’elle affiche à sa place.",
    contents: "Les vingt-six polices",
    page: "page {page}",
    drawn_to: "Mêmes largeurs que {standard} · {licence}",
    sizes: "De 8 à 48\u{00A0}points",
    compared: "La même ligne en 14\u{00A0}points, dans la police standard puis dans la police \
               libre",
    in_standard: "{standard}, fournie par le logiciel de lecture",
    in_free: "{face}, incorporée au fichier",
    width: "{width}\u{00A0}points de large",
    characters: "Les {count}\u{00A0}caractères accessibles par les codes de la police standard",
    stamps_title: "Tampons et notes composés dans une police standard",
    stamps_prose: "Ces deux tampons et ces deux notes nomment chacun une police standard. Le \
                   fichier incorpore pour chacun la police libre aux mêmes largeurs\u{00A0}: ils \
                   s’affichent donc à l’identique dans tous les logiciels de lecture.",
    approved: "APPROUVÉ",
    confidential: "CONFIDENTIEL",
    notes: [
        "Vérifié sur la commande du 3\u{00A0}octobre.",
        "Total à confirmer avant paiement.",
    ],
    carried: "{standard}, incorporée sous la forme de {face}",
    pangram: "Portez ce vieux whisky au juge blond qui fume",
    decimal: ',',
};

/// The specimen in German.
const GERMAN: Words = Words {
    title: "Die freien Schriften, eine nach der anderen",
    prose: "Ein PDF kann vierzehn Standardschriften nennen, ohne sie zu enthalten: Die \
            Lesesoftware zeichnet sie mit ihren eigenen Exemplaren. Muss eine Datei alle ihre \
            Schriften enthalten, wie eine Archivdatei, bettet die Bibliothek stattdessen eine \
            freie Schrift mit denselben Breiten ein, und der Text bleibt bis auf einen \
            Zehntelpunkt an seinem Platz. Jede folgende Seite zeigt eine dieser Schriften in acht Größen, \
            neben der Standardschrift, die sie ersetzt, und mit allen Zeichen, die sie erreicht.",
    contents: "Die sechsundzwanzig Schriften",
    page: "Seite {page}",
    drawn_to: "Mit den Breiten von {standard} · {licence}",
    sizes: "Von 8 bis 48 Punkt",
    compared: "Dieselbe Zeile in 14 Punkt, in der Standardschrift, dann in der freien Schrift",
    in_standard: "{standard}, von der Lesesoftware mit ihrem eigenen Exemplar gezeichnet",
    in_free: "{face}, in der Datei eingebettet",
    width: "{width} Punkt breit",
    characters: "Die {count} Zeichen, die die Schrift über die Codes der Standardschrift erreicht",
    stamps_title: "Stempel und Notizen in einer Standardschrift",
    stamps_prose: "Diese zwei Stempel und diese zwei Notizen nennen eine Standardschrift. Für jede \
                   bettet die Datei die freie Schrift mit denselben Breiten ein, und jede \
                   Lesesoftware zeigt sie mit denselben Buchstaben.",
    approved: "GENEHMIGT",
    confidential: "VERTRAULICH",
    notes: [
        "Mit der Bestellung vom 3. Oktober abgeglichen.",
        "Summe vor der Zahlung bestätigen.",
    ],
    carried: "{standard}, eingebettet als {face}",
    pangram: "Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich",
    decimal: ',',
};

/// The specimen in Spanish.
const SPANISH: Words = Words {
    title: "Las fuentes libres, una a una",
    prose: "Un PDF puede nombrar catorce fuentes estándar sin contenerlas: el programa de lectura \
            las dibuja con sus propias copias. Cuando un archivo debe contener todas sus fuentes, \
            como un archivo de conservación, la biblioteca incorpora en su lugar una fuente libre \
            con las mismas anchuras, y el texto conserva su lugar a una décima de punto. Cada \
            página siguiente muestra una de estas fuentes en ocho tamaños, frente a la fuente \
            estándar a la que sustituye, con todos los caracteres que alcanza.",
    contents: "Las veintiséis fuentes",
    page: "página {page}",
    drawn_to: "Con las anchuras de {standard} · {licence}",
    sizes: "De 8 a 48 puntos",
    compared: "La misma línea a 14 puntos, en la fuente estándar y después en la fuente libre",
    in_standard: "{standard}, dibujada por el programa de lectura con su propia copia",
    in_free: "{face}, incorporada al archivo",
    width: "{width} puntos de ancho",
    characters: "Los {count} caracteres que la fuente alcanza con los códigos de la fuente \
                 estándar",
    stamps_title: "Sellos y notas compuestos en una fuente estándar",
    stamps_prose: "Estos dos sellos y estas dos notas nombran una fuente estándar. Para cada uno, \
                   el archivo incorpora la fuente libre con las mismas anchuras, y todos los \
                   programas de lectura los muestran con las mismas letras.",
    approved: "APROBADO",
    confidential: "CONFIDENCIAL",
    notes: [
        "Comprobado con el pedido del 3 de octubre.",
        "Total por confirmar antes del pago.",
    ],
    carried: "{standard}, incorporada como {face}",
    pangram: "Jovencillo emponzoñado de whisky, qué figurota exhibe",
    decimal: ',',
};

/// The specimen in Italian.
const ITALIAN: Words = Words {
    title: "I caratteri liberi, uno per uno",
    prose: "Un PDF può nominare quattordici caratteri standard senza contenerli: il programma di \
            lettura li disegna con le proprie copie. Quando un file deve contenere tutti i suoi \
            caratteri, come un file d’archivio, la libreria vi incorpora al loro posto un \
            carattere libero con le stesse larghezze, e il testo resta al suo posto entro un decimo \
            di punto. Ogni pagina che segue mostra uno di questi caratteri in otto corpi, accanto \
            al carattere standard che sostituisce, con tutti i segni che raggiunge.",
    contents: "I ventisei caratteri",
    page: "pagina {page}",
    drawn_to: "Con le larghezze di {standard} · {licence}",
    sizes: "Da 8 a 48 punti",
    compared: "La stessa riga in corpo 14, nel carattere standard e poi nel carattere libero",
    in_standard: "{standard}, disegnato dal programma di lettura con la propria copia",
    in_free: "{face}, incorporato nel file",
    width: "{width} punti di larghezza",
    characters: "I {count} segni che il carattere raggiunge con i codici del carattere standard",
    stamps_title: "Timbri e note composti in un carattere standard",
    stamps_prose: "Questi due timbri e queste due note nominano un carattere standard. Per \
                   ciascuno, il file incorpora il carattere libero con le stesse larghezze, e \
                   ogni programma di lettura li mostra con le stesse lettere.",
    approved: "APPROVATO",
    confidential: "RISERVATO",
    notes: [
        "Verificato sull’ordine del 3 ottobre.",
        "Totale da confermare prima del pagamento.",
    ],
    carried: "{standard}, incorporato come {face}",
    pangram: "Pranzo d’acqua fa volti sghembi",
    decimal: ',',
};

/// The specimen in Portuguese.
const PORTUGUESE: Words = Words {
    title: "Os tipos livres, um a um",
    prose: "Um PDF pode nomear catorze tipos padrão sem os conter: o programa de leitura \
            desenha-os com as suas próprias cópias. Quando um ficheiro tem de conter todos os \
            seus tipos, como um ficheiro de arquivo, a biblioteca incorpora em vez deles um tipo \
            livre com as mesmas larguras, e o texto mantém o seu lugar a uma décima de ponto. \
            Cada página seguinte mostra um destes tipos em oito tamanhos, ao lado do tipo padrão \
            que substitui, com todos os caracteres que alcança.",
    contents: "Os vinte e seis tipos",
    page: "página {page}",
    drawn_to: "Com as larguras de {standard} · {licence}",
    sizes: "De 8 a 48 pontos",
    compared: "A mesma linha a 14 pontos, no tipo padrão e depois no tipo livre",
    in_standard: "{standard}, desenhado pelo programa de leitura com a sua própria cópia",
    in_free: "{face}, incorporado no ficheiro",
    width: "{width} pontos de largura",
    characters: "Os {count} caracteres que o tipo alcança pelos códigos do tipo padrão",
    stamps_title: "Carimbos e notas compostos num tipo padrão",
    stamps_prose: "Estes dois carimbos e estas duas notas nomeiam um tipo padrão. Para cada um, o \
                   ficheiro incorpora o tipo livre com as mesmas larguras, e todos os programas \
                   de leitura os mostram com as mesmas letras.",
    approved: "APROVADO",
    confidential: "CONFIDENCIAL",
    notes: [
        "Conferido com a encomenda de 3 de outubro.",
        "Total a confirmar antes do pagamento.",
    ],
    carried: "{standard}, incorporado como {face}",
    pangram: "Um pequeno jabuti xereta viu dez cegonhas felizes",
    decimal: ',',
};

/// The specimen in Dutch.
const DUTCH: Words = Words {
    title: "De vrije lettertypen, een voor een",
    prose: "Een pdf kan veertien standaardlettertypen noemen zonder ze te bevatten: de \
            leessoftware tekent ze met haar eigen exemplaren. Moet een bestand al zijn \
            lettertypen bevatten, zoals een archiefbestand, dan sluit de bibliotheek in hun plaats \
            een vrij lettertype met dezelfde breedtes in, en de tekst blijft tot op een \
            tiende punt op zijn plaats. Elke volgende pagina toont een van deze lettertypen in \
            acht groottes, naast het standaardlettertype dat het vervangt, met alle tekens die \
            het bereikt.",
    contents: "De zesentwintig lettertypen",
    page: "pagina {page}",
    drawn_to: "Met de breedtes van {standard} · {licence}",
    sizes: "Van 8 tot 48 punt",
    compared: "Dezelfde regel op 14 punt, in het standaardlettertype en daarna in het vrije \
               lettertype",
    in_standard: "{standard}, door de leessoftware getekend met haar eigen exemplaar",
    in_free: "{face}, ingesloten in het bestand",
    width: "{width} punt breed",
    characters: "De {count} tekens die het lettertype via de codes van het standaardlettertype \
                 bereikt",
    stamps_title: "Stempels en notities in een standaardlettertype",
    stamps_prose: "Deze twee stempels en deze twee notities noemen een standaardlettertype. Voor \
                   elk ervan sluit het bestand het vrije lettertype met dezelfde breedtes in, en \
                   elke leessoftware toont ze met dezelfde letters.",
    approved: "GOEDGEKEURD",
    confidential: "VERTROUWELIJK",
    notes: [
        "Gecontroleerd met de bestelling van 3 oktober.",
        "Totaal te bevestigen vóór betaling.",
    ],
    carried: "{standard}, ingesloten als {face}",
    pangram: "Pa’s wijze lynx bezag vroom het fikse aquaduct",
    decimal: ',',
};

/// Every language the specimen is written in.
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 a page.
const LEFT: f64 = 56.0;

/// How wide a block of text is.
const WIDTH: f64 = 483.0;

/// The first baseline of a page.
const TOP: f64 = 790.0;

/// The sizes the line is set at, in points.
const SIZES: [f64; 8] = [8.0, 10.0, 12.0, 14.0, 18.0, 24.0, 36.0, 48.0];

/// The size from which the short line takes the place of the long one.
const LARGE: f64 = 24.0;

/// The short line, set at the large sizes: letters and figures, which are the
/// same in every language.
const SHOWCASE: &str = "Aa Gg Qq \u{00C9}\u{00E9} 0123";

/// The lines of the two faces that draw signs rather than letters: Symbol's
/// Greek letters and signs, then `ZapfDingbats`'s ornaments, each set at every
/// size.
const SIGNS: [&str; 2] = [
    "\u{03A3} \u{221A} \u{222B} \u{03B1}\u{03B2}\u{03B3} \u{2265} \u{221E}",
    "\u{2713} \u{2708} \u{260E} \u{2605} \u{2702} \u{2764}",
];

/// The characters of `WinAnsiEncoding` between codes 128 and 159, which are not
/// the characters of the same numbers.
const WINANSI_HIGH: &str = "\u{20AC}\u{201A}\u{0192}\u{201E}\u{2026}\u{2020}\u{2021}\u{02C6}\
                            \u{2030}\u{0160}\u{2039}\u{0152}\u{017D}\u{2018}\u{2019}\u{201C}\
                            \u{201D}\u{2022}\u{2013}\u{2014}\u{02DC}\u{2122}\u{0161}\u{203A}\
                            \u{0153}\u{017E}\u{0178}";

/// How wide one cell of the grid of characters is.
const CELL: f64 = 26.0;

/// The size the grid of characters is set at.
const GRID_SIZE: f64 = 15.0;

/// One face of the specimen: the standard font it stands in for, and the family
/// it comes from.
#[derive(Debug, Clone, Copy)]
struct Face {
    /// The standard font it is drawn to the widths of.
    standard: StandardFont,
    /// The family it comes from.
    family: FreeFamily,
}

impl Face {
    /// The faces of the specimen: the twelve Liberation faces, the twelve TeX
    /// Gyre faces, then HQF Symbol and HQF Dingbats, which both families share.
    fn all() -> Vec<Self> {
        let text = |family| {
            StandardFont::ALL
                .into_iter()
                .filter(|standard| !standard.font_specific())
                .map(move |standard| Self { standard, family })
        };
        let signs = StandardFont::ALL
            .into_iter()
            .filter(|standard| standard.font_specific())
            .map(|standard| Self {
                standard,
                family: FreeFamily::Liberation,
            });
        text(FreeFamily::Liberation)
            .chain(text(FreeFamily::TexGyre))
            .chain(signs)
            .collect()
    }

    /// The licence the face is given under.
    const fn licence(self) -> &'static str {
        match (self.standard, self.family) {
            (StandardFont::Symbol, _) => "Apache License 2.0",
            (StandardFont::ZapfDingbats, _) => "Bitstream Vera Fonts Copyright",
            (_, FreeFamily::TexGyre) => "GUST Font License",
            _ => "SIL Open Font License 1.1",
        }
    }

    /// The line the face is set in: the pangram, or the signs it draws.
    const fn line(self, words: &Words) -> &'static str {
        match self.standard {
            StandardFont::Symbol => SIGNS[0],
            StandardFont::ZapfDingbats => SIGNS[1],
            _ => words.pangram,
        }
    }

    /// The characters the face reaches through the standard font's codes: those
    /// of `WinAnsiEncoding` for a text font, every character the face maps for
    /// the other two; spaces left out.
    fn characters(self, free: &Font) -> Vec<char> {
        let mut found: Vec<char> = if self.standard.font_specific() {
            free.coverage().map(|(character, _)| character).collect()
        } else {
            ('\u{21}'..='\u{7E}')
                .chain(WINANSI_HIGH.chars())
                .chain('\u{A1}'..='\u{FF}')
                .collect()
        };
        found.retain(|character| !character.is_whitespace() && *character != '\u{AD}');
        found.sort_unstable();
        found.dedup();
        found
    }
}

/// A figure with two decimals, written with `words`'s decimal mark.
fn figure(value: f64, words: &Words) -> String {
    format!("{value:.2}").replace('.', &words.decimal.to_string())
}

/// Sets `text` in `handle` at `size` with its first baseline at `top`, wrapped
/// to `width` from `left`, and hands back the ordinate the block ends at.
fn block(
    c: &mut Content,
    handle: &FontHandle,
    size: f64,
    (left, top, width): (f64, f64, f64),
    text: &str,
) -> Result<f64, Box<dyn std::error::Error>> {
    let flow = TextFlow::new(handle, size);
    let lines = flow.break_lines(text, width);
    c.begin_text();
    flow.draw(c, &lines, left, top, width)?;
    c.end_text();
    Ok(top - flow.height(&lines))
}

/// Sets `text` in `handle` at `size` on one line, from `left` at baseline `y`.
fn line(
    c: &mut Content,
    handle: &FontHandle,
    size: f64,
    (left, y): (f64, f64),
    text: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    c.begin_text();
    c.set_font(handle, size)?;
    c.text_origin(left, y)?;
    c.show_glyphs(&handle.glyphs(text));
    c.end_text();
    Ok(())
}

/// The first page: the title, the paragraph, and each face with the page it is
/// on.
fn contents(
    label: &FontHandle,
    faces: &[(Face, String)],
    words: &Words,
) -> Result<Page, Box<dyn std::error::Error>> {
    let mut c = Content::new();
    c.set_fill(Rgb::gray(0.0))?;
    let mut top = block(&mut c, label, 22.0, (LEFT, TOP, WIDTH), words.title)? - 10.0;
    top = block(&mut c, label, 10.0, (LEFT, top, WIDTH), words.prose)? - 18.0;
    top = block(&mut c, label, 13.0, (LEFT, top, WIDTH), words.contents)? - 16.0;
    for (at, (face, name)) in faces.iter().enumerate() {
        let on_page = words.page.replace("{page}", &(at + 2).to_string());
        c.set_fill(Rgb::gray(0.0))?;
        line(&mut c, label, 9.5, (LEFT, top), name)?;
        c.set_fill(Rgb::gray(0.4))?;
        line(
            &mut c,
            label,
            9.5,
            (LEFT + 190.0, top),
            face.standard.postscript_name(),
        )?;
        line(&mut c, label, 9.5, (LEFT + 400.0, top), &on_page)?;
        top -= 16.0;
    }
    let mut on_page = Page::a4();
    on_page.content = c.into_bytes();
    Ok(on_page)
}

/// The page of one face: its name, what it stands in for and its licence, its
/// line at every size, the line set twice, and every character it reaches.
#[allow(
    clippy::suboptimal_flops,
    reason = "the Python twin computes the same sums, which a fused multiply-add would round apart"
)]
fn specimen(
    label: &FontHandle,
    (shown, name): &(Face, String),
    (free, handle, standard_handle): (&Font, &FontHandle, &FontHandle),
    words: &Words,
) -> Result<Page, Box<dyn std::error::Error>> {
    let mut c = Content::new();
    c.set_fill(Rgb::gray(0.0))?;
    // A symbol face has no letters to spell its own name with.
    let named_in = if shown.standard.font_specific() {
        label
    } else {
        handle
    };
    let mut top = block(&mut c, named_in, 26.0, (LEFT, TOP, WIDTH), name)? - 4.0;
    c.set_fill(Rgb::gray(0.4))?;
    let drawn_to = words
        .drawn_to
        .replace("{standard}", shown.standard.postscript_name())
        .replace("{licence}", shown.licence());
    top = block(&mut c, label, 9.0, (LEFT, top, WIDTH), &drawn_to)? - 16.0;

    c.set_fill(Rgb::gray(0.0))?;
    top = block(&mut c, label, 11.0, (LEFT, top, WIDTH), words.sizes)? - 6.0;
    for size in SIZES {
        let text = if size < LARGE || shown.standard.font_specific() {
            shown.line(words)
        } else {
            SHOWCASE
        };
        top -= size * 0.9;
        c.set_fill(Rgb::gray(0.5))?;
        line(&mut c, label, 7.0, (LEFT, top), &format!("{size:.0}"))?;
        c.set_fill(Rgb::gray(0.0))?;
        line(&mut c, handle, size, (LEFT + 26.0, top), text)?;
        top -= size * 0.35 + 6.0;
    }

    top = block(
        &mut c,
        label,
        11.0,
        (LEFT, top - 12.0, WIDTH),
        words.compared,
    )? - 15.0;
    let text = shown.line(words);
    for (set_in, said) in [
        (
            standard_handle,
            words
                .in_standard
                .replace("{standard}", shown.standard.postscript_name()),
        ),
        (handle, words.in_free.replace("{face}", name)),
    ] {
        let wide = words
            .width
            .replace("{width}", &figure(set_in.measure(text, 14.0), words));
        c.set_fill(Rgb::gray(0.5))?;
        line(&mut c, label, 7.0, (LEFT, top), &format!("{said} · {wide}"))?;
        c.set_fill(Rgb::gray(0.0))?;
        line(&mut c, set_in, 14.0, (LEFT, top - 17.0), text)?;
        top -= 34.0;
    }

    let characters = shown.characters(free);
    let heading = words
        .characters
        .replace("{count}", &characters.len().to_string());
    top = block(&mut c, label, 11.0, (LEFT, top - 6.0, WIDTH), &heading)? - 10.0 - GRID_SIZE;
    let columns = (WIDTH / CELL).floor();
    for (at, character) in characters.iter().enumerate() {
        #[allow(
            clippy::cast_precision_loss,
            reason = "a page holds a few hundred characters at most"
        )]
        let at = at as f64;
        let column = at % columns;
        let row = (at / columns).floor();
        let text = character.to_string();
        let advance = handle.measure(&text, GRID_SIZE);
        let x = LEFT + column * CELL + (CELL - advance) * 0.5;
        line(&mut c, handle, GRID_SIZE, (x, top - row * 22.0), &text)?;
    }

    let mut page = Page::a4();
    page.content = c.into_bytes();
    Ok(page)
}

/// The last page: two stamps and two notes whose words are set in standard
/// fonts, each with what the file carries for it.
fn stamps(
    doc: &mut Document,
    label: &FontHandle,
    words: &Words,
) -> Result<Page, Box<dyn std::error::Error>> {
    let mut c = Content::new();
    c.set_fill(Rgb::gray(0.0))?;
    let mut top = block(&mut c, label, 18.0, (LEFT, TOP, WIDTH), words.stamps_title)? - 8.0;
    top = block(&mut c, label, 10.0, (LEFT, top, WIDTH), words.stamps_prose)? - 30.0;

    let mut page = Page::a4();
    let set_in = [
        StandardFont::HelveticaBold,
        StandardFont::TimesBold,
        StandardFont::TimesRoman,
        StandardFont::Courier,
    ];
    for (at, named) in set_in.into_iter().enumerate() {
        let font = doc.add_standard_font(named);
        let free = named.free_face(FreeFamily::Liberation)?;
        let carried = words
            .carried
            .replace("{standard}", named.postscript_name())
            .replace("{face}", free.postscript_name());
        let y = top - 90.0;
        let annotation: Annotation = match at {
            0 | 1 => {
                let (icon, said, color) = if at == 0 {
                    (StampIcon::Approved, words.approved, Rgb::new(0.0, 0.5, 0.0))
                } else {
                    (
                        StampIcon::Confidential,
                        words.confidential,
                        Rgb::new(0.75, 0.0, 0.0),
                    )
                };
                Stamp::new(LEFT, y, 220.0, 70.0)
                    .icon(icon)
                    .words(&StampWords::new(&font, said).slant(Slant::Flat).color(color))?
                    .into()
            }
            _ => {
                let flow = TextFlow::new(&font, 12.0);
                Annotation::FreeText(FreeText::new(
                    LEFT,
                    y + 30.0,
                    260.0,
                    40.0,
                    &flow,
                    words.notes[at - 2],
                )?)
            }
        };
        page.annotations.push(annotation);
        c.set_fill(Rgb::gray(0.4))?;
        block(
            &mut c,
            label,
            9.0,
            (LEFT + 290.0, y + 30.0, WIDTH - 290.0),
            &carried,
        )?;
        top -= 120.0;
    }
    page.content = c.into_bytes();
    Ok(page)
}

/// The specimen, written in `words`.
fn build(words: &Words) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    let mut doc = Document::new();
    doc.set_license(licence::licensed());
    let label = doc.add_font(StandardFont::Helvetica.free_face(FreeFamily::Liberation)?);

    let faces: Vec<(Face, String)> = Face::all()
        .into_iter()
        .map(|face| -> Result<_, hqf_pdf::Error> {
            let free = face.standard.free_face(face.family)?;
            Ok((face, free.postscript_name().to_owned()))
        })
        .collect::<Result<_, _>>()?;

    let first = contents(&label, &faces, words)?;
    doc.add_page(first)?;
    for named in &faces {
        let free = named.0.standard.free_face(named.0.family)?;
        let handle = doc.add_font(free.clone());
        let standard = doc.add_standard_font(named.0.standard);
        let page = specimen(&label, named, (&free, &handle, &standard), words)?;
        doc.add_page(page)?;
    }
    let last = stamps(&mut doc, &label, words)?;
    doc.add_page(last)?;
    Ok(doc.to_bytes()?)
}

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

    // A named file is written as named; the default one carries the language,
    // so the languages do not overwrite each other in `tmp/`.
    let out = env::args()
        .nth(1)
        .unwrap_or_else(|| language.file_name(&out::default_path("type_specimen")));
    let bytes = build(words)?;

    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::{WORDS, language};

    /// The lines two languages are allowed to write the same way.
    const SPARED: [&str; 5] = [
        r#"page: "page {page}""#,
        // Written the same way in Spanish and Portuguese.
        r#"page: "página {page}""#,
        // Written the same way in Italian and Dutch.
        r#"page: "pagina {page}""#,
        r#"approved: "APROBADO""#,
        r#"confidential: "CONFIDENCIAL""#,
    ];

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

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