1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827 | //! Sets a page in a font read from a Type 1 program, and states what the
//! program says about itself.
//!
//! Adobe's first font format is not a table anybody can look a glyph up in: it
//! is a PostScript program whose glyphs are ciphered, and every measurement
//! below is read out of it rather than written in by hand. The program is
//! carried into the file cut down to the glyphs the page draws, and reading
//! software draws each of them from the very bytes that were handed in.
//!
//! The face is `HqfStroke`, HQF Development's own: a stroke face built by
//! `scripts/build_type1_sample.py`, committed beside the fonts the tests run
//! against. It draws capitals, figures and the marked letters French needs, and
//! nothing else — which is what the page says of it, and what the page is set
//! in.
//!
//! A second page reads the program glyph by glyph: its matrix, its slant,
//! whether its glyphs share one width, where its codes come from, and for each
//! character of one line the code that reaches it, the glyph's name and its
//! advance. The same line is then set a second time with a metrics file written
//! over the program that widens every glyph by a quarter, and the page says how
//! long each line runs.
//!
//! The face is measured on the text metrics file committed beside it,
//! `HqfStroke.afm`, and a third page sets one line twice: without kerning, then
//! kerned on the pairs that file states.
//!
//! Both sets of words are held in `Words`, once per language, and
//! `HQF_PDF_LANG` picks which set is drawn.
//!
//! Usage: `cargo run --example write_type_one_font -- tmp/type1.pdf [face.pfb
//! [face.afm]]`
//! `HQF_PDF_LANG=fr cargo run --example write_type_one_font`
//!
//! A face named without a metrics file is measured on its program alone.
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
use hqf_pdf::font::type1::BuiltinEncoding;
use hqf_pdf::{Document, Font, FontHandle, Page, Rgb, TextFlow, Type1Font};
#[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 Type 1 program the page is set in when none is given on the command
/// line: the one committed for the tests, so the example runs on any machine.
fn default_face() -> PathBuf {
fonts().join("HqfStroke.pfb")
}
/// The text metrics file of the default face, committed beside it.
fn default_metrics() -> PathBuf {
fonts().join("HqfStroke.afm")
}
/// The font the prose is set in: the page says more than a stroke face of
/// capitals can hold.
fn prose_face() -> PathBuf {
fonts().join("DejaVuSans.ttf")
}
/// Where the fonts committed for the tests sit.
fn fonts() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fonts")
}
/// The words the page is written in, one set per language.
#[derive(Debug)]
struct Words {
/// The page's title.
title: &'static str,
/// What the page is about.
lead: &'static str,
/// The label over the three lines set in the face itself.
shown: &'static str,
/// The three lines, in the capitals the face draws.
samples: [&'static str; 3],
/// The label over what the program states about itself.
stated: &'static str,
/// The sentence those figures are put into.
facts: &'static str,
/// The label over the weight.
weight: &'static str,
/// The sentence the two weights are put into.
weighed: &'static str,
/// The label over what of the program travels with the page.
travels: &'static str,
/// How the program is cut down to what the page draws.
carried: &'static str,
/// What such a font cannot do.
caveat: &'static str,
/// The second page's heading.
glyph_by_glyph: &'static str,
/// What the program states of its geometry; `{matrix}`, `{slant}`,
/// `{pitch}` and `{codes}` stand for them.
geometry: &'static str,
/// Whether the glyphs share one width: yes, then no.
pitch: [&'static str; 2],
/// Where the codes come from: the published table, then the program's own,
/// `{count}` standing for how many codes it fills.
codes: [&'static str; 2],
/// One character of the line; `{character}`, `{code}`, `{name}` and
/// `{advance}` stand for them.
reached_by: &'static str,
/// What the line set twice shows; `{program}` and `{metrics}` stand for how
/// long it runs each way.
widened: &'static str,
/// The character that separates the whole part of a figure from its tenths.
decimal: char,
/// The label over the line set twice, on the third page.
kerned: &'static str,
/// The line set twice: every pair the metrics file kerns.
pairs: &'static str,
/// What the two settings of that line show.
kerning: &'static str,
}
impl Words {
/// The words the page is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
}
/// The page in English.
const ENGLISH: Words = Words {
title: "A page set in a Type 1 font",
lead: "The three lines below are drawn from a font program in Adobe's \
first format. It is not a font file with tables to look a glyph up \
in: it is a PostScript program, its glyphs ciphered one by one, and \
the library reads it to find out which code reaches each of them. \
Each glyph is set as wide as the text metrics file that comes with \
the program says, or, without one, as the program itself says. The \
program is then carried into this file cut down to the glyphs this \
page draws, and reading software draws them from those very bytes.",
shown: "Drawn from the program",
samples: [
"HQF DEVELOPMENT",
"ABCDEFGHIJKLM NOPQRSTUVWXYZ 0123456789",
"A STROKE FACE, READ AND CUT TO SIZE.",
],
stated: "What the program states about itself",
facts: "It calls itself %name% and holds %glyphs% glyphs, of which %reached% \
are reached by a code. Every glyph stands in one box, whose corners \
are %low% and %high%; a vertical stem is %stem% thousandths of an em \
thick, and an underline sits %underline% below the line. Not one of \
those figures is written into this page: each is read out of the \
program.",
weight: "What it weighs",
weighed: "The program itself is %program% bytes on disk, and a page \
setting the three lines above in it weighs %document% bytes, the \
program cut down to the glyphs those lines draw. Both figures \
are measured by the program that created this page, not written \
in by hand.",
travels: "What travels with it",
carried: "Like every other kind of font this library carries, a Type 1 \
program travels light: only the glyphs a page actually draws go \
into the file, so a page that says one word carries the letters \
of that word and not a whole alphabet. The format makes that a \
piece of work of its own: each glyph is ciphered on its own \
inside the program, so the library deciphers the program, \
rewrites its list of glyphs to hold only .notdef, the glyphs \
drawn and the glyphs an accented letter is built from, and \
ciphers it again. Each glyph kept is copied byte for byte, still \
under its own cipher, and the pieces of outline several glyphs \
share are kept whole. The font's description then names every \
glyph the file holds. On a face of a few dozen glyphs that saves \
little; on a large one it is most of the program: a face of \
sixty thousand bytes set in three words carries the glyphs of \
those three words and not the rest.",
caveat: "A Type 1 font is a simple font: its codes are one byte, so at most \
256 of its glyphs are reachable at a time and it never sets text \
down the page. Which code reaches which glyph is the program's own \
business — this file repeats the program's own names for them.",
glyph_by_glyph: "The program, glyph by glyph",
geometry: "Its matrix is [{matrix}]; it slants {slant}°; {pitch}; {codes}.",
pitch: [
"every glyph has one width",
"each glyph has a width of its own",
],
codes: [
"its codes follow the table the format publishes",
"it spells out its own codes, {count} of them",
],
reached_by: "{character} is reached by code {code}, the glyph {name}, which advances {advance}",
widened: "The line above is set once from the widths the program states, and once \
with a metrics file written over it that widens every glyph by a \
quarter: at 15 points it runs {program} points, then {metrics}.",
decimal: '.',
kerned: "Kerned from its metrics file",
pairs: PAIRS,
kerning: "The same pairs twice: the first line is set without kerning, the \
second kerned on the pairs of the text metrics file the library \
reads beside the program. Each pair whose facing sides slant or \
stand open is brought together, by as much as a quarter of an em, \
and the move is written into the line itself, so every reader draws \
the letters where they were set.",
};
/// Every pair the metrics file of the default face kerns, one after the other.
const PAIRS: &str = "AT AV AW AY FA LT LV LW LY PA TA VA WA YA";
/// The page in French.
const FRENCH: Words = Words {
title: "Une page composée dans une police Type 1",
lead: "Les trois lignes ci-dessous sont tracées par un programme de police écrit dans \
le premier format d’Adobe. Ce n’est pas un fichier de police avec des tables \
où chercher un glyphe : c’est un programme PostScript, dont chaque glyphe est \
chiffré à part, et la bibliothèque le lit pour savoir quel code mène à chacun. \
Chaque glyphe prend la largeur que donne le fichier de métriques texte fourni \
avec le programme, ou, à défaut, celle que donne le programme lui-même. Le \
programme est ensuite incorporé à ce fichier, réduit aux seuls glyphes de cette \
page, et le logiciel de lecture les trace à partir de ces octets mêmes.",
shown: "Tracé par le programme",
samples: [
"HQF DEVELOPMENT",
"ABCDEFGHIJKLM NOPQRSTUVWXYZ 0123456789",
"UNE POLICE DE TRAITS, LUE PUIS ÉLAGUÉE.",
],
stated: "Ce que le programme dit de lui-même",
facts: "Il s’appelle %name% et contient %glyphs% glyphes, dont %reached% sont \
accessibles par un code. Tous les glyphes tiennent dans une même boîte, dont \
les coins sont %low% et %high% ; un fût vertical a une épaisseur de %stem% \
millièmes de cadratin, et le soulignement se place à %underline% sous la \
ligne. Aucun de ces chiffres n’est écrit à la main dans cette page : chacun \
est lu dans le programme.",
weight: "Ce que cela pèse",
weighed: "Le programme lui-même pèse %program% octets sur le disque, et une page où les \
trois lignes ci-dessus sont composées dans cette police pèse %document% \
octets, y compris le programme réduit à leurs seuls glyphes. Les deux chiffres \
sont mesurés par le logiciel qui a créé cette page, pas écrits à la main.",
travels: "Ce qui l’accompagne",
carried: "Comme toutes les autres sortes de polices que cette bibliothèque incorpore, un \
programme Type 1 voyage léger : seuls les glyphes réellement tracés sur une \
page entrent dans le fichier, si bien qu’une page qui contient un mot emporte \
les lettres de ce mot, et non un alphabet entier. Le format en fait un travail \
à part entière : chaque glyphe y est chiffré séparément, donc la bibliothèque \
déchiffre le programme, réécrit sa liste de glyphes pour n’y garder que \
.notdef, les glyphes tracés et ceux qui servent à construire une lettre \
accentuée, puis le rechiffre. Chaque glyphe gardé est recopié octet pour \
octet, toujours sous son propre chiffrement, et les morceaux de contour que \
plusieurs glyphes partagent sont conservés entiers. La description de la \
police nomme ensuite chaque glyphe que contient le fichier. Sur une police de \
quelques dizaines de glyphes, le gain est faible ; sur une grande, c’est \
l’essentiel du programme : une police de soixante mille octets employée pour \
trois mots n’emporte que les glyphes de ces trois mots.",
caveat: "Une police Type 1 est une police simple : ses codes tiennent sur un octet, \
donc 256 de ses glyphes au plus sont accessibles à la fois, et elle ne compose \
jamais à la verticale. Quel code mène à quel glyphe relève du seul programme — \
ce fichier reprend les noms que le programme leur donne.",
glyph_by_glyph: "Le programme, glyphe par glyphe",
geometry: "Sa matrice est [{matrix}] ; il penche de {slant}° ; {pitch} ; {codes}.",
pitch: [
"tous ses glyphes ont une même chasse",
"chaque glyphe a sa propre chasse",
],
codes: [
"ses codes suivent la table que publie le format",
"il détaille ses propres codes, au nombre de {count}",
],
reached_by: "{character} est atteint par le code {code}, le glyphe {name}, qui avance de {advance}",
widened: "La ligne ci-dessus est composée une fois avec les chasses que donne le \
programme, et une fois avec un fichier de métriques posé par-dessus qui élargit \
chaque glyphe d’un quart : à 15 points elle mesure {program} points, \
puis {metrics}.",
decimal: ',',
kerned: "Crénage lu dans son fichier de métriques",
pairs: PAIRS,
kerning: "Les mêmes paires, deux fois : la première ligne est composée sans crénage, \
la seconde avec le crénage des paires du fichier de métriques texte que la bibliothèque \
lit à côté du programme. Chaque paire dont les côtés en regard sont obliques ou ouverts \
est rapprochée, jusqu’à un quart de cadratin, et ce rapprochement est écrit dans la \
ligne elle-même : tout logiciel de lecture trace les lettres là où elles ont été \
placées.",
};
/// The page in Dutch.
const DUTCH: Words = Words {
title: "Een pagina gezet in een Type 1-lettertype",
lead: "De drie regels hieronder zijn getekend vanuit een lettertypeprogramma in het eerste formaat van Adobe. Het is geen lettertypebestand met tabellen om een glyph in op te zoeken: het is een PostScript-programma, met zijn glyphs een voor een versleuteld, en de bibliotheek leest het om te weten welke code elke glyph bereikt. Elke glyph wordt zo breed gezet als het metriekbestand bij het programma zegt, of, zonder zo’n bestand, als het programma zelf zegt. Het programma wordt dan in dit bestand ingesloten, ingekort tot de glyphs die deze pagina tekent, en leessoftware tekent ze vanuit precies die bytes.",
shown: "Getekend vanuit het programma",
samples: [
"HQF DEVELOPMENT",
"ABCDEFGHIJKLM NOPQRSTUVWXYZ 0123456789",
"EEN LIJNLETTER, GELEZEN EN OP MAAT GESNEDEN.",
],
stated: "Wat het programma over zichzelf zegt",
facts: "Het heet %name% en bevat %glyphs% glyphs, waarvan er %reached% door een code bereikt worden. Elke glyph staat in één kader, met als hoeken %low% en %high%; een verticale stam is %stem% duizendste em dik, en een onderstreping ligt %underline% onder de regel. Geen van die getallen staat op deze pagina geschreven: elk is uit het programma gelezen.",
weight: "Wat het weegt",
weighed: "Het programma zelf is %program% bytes op schijf, en een pagina die er de drie regels hierboven in zet, weegt %document% bytes, met het programma ingekort tot de glyphs die die regels tekenen. Beide getallen zijn gemeten door het programma dat deze pagina maakte, niet met de hand geschreven.",
travels: "Wat ermee meereist",
carried: "Zoals elke andere soort lettertype die deze bibliotheek insluit, reist een Type 1-programma licht: alleen de glyphs die een pagina echt tekent, gaan het bestand in, dus een pagina die één woord zegt, bevat de letters van dat woord en geen heel alfabet. Het formaat maakt daar een eigen karwei van: elke glyph is binnen het programma apart versleuteld, dus ontsleutelt de bibliotheek het programma, herschrijft de lijst van glyphs zodat die alleen .notdef bevat, de getekende glyphs en de glyphs waaruit een letter met accent wordt opgebouwd, en versleutelt het opnieuw. Elke behouden glyph wordt byte voor byte gekopieerd, nog onder zijn eigen versleuteling, en de stukken omtrek die meerdere glyphs delen blijven heel. De beschrijving van het lettertype noemt dan elke glyph die het bestand bevat. Bij een letter van een paar dozijn glyphs scheelt dat weinig; bij een grote is het het grootste deel van het programma: een letter van zestigduizend bytes, gezet in drie woorden, bevat de glyphs van die drie woorden en niet de rest.",
caveat: "Een Type 1-lettertype is een eenvoudig lettertype: de codes zijn één byte, dus hoogstens 256 van de glyphs zijn tegelijk bereikbaar en het zet nooit tekst langs de pagina omlaag. Welke code welke glyph bereikt, is de zaak van het programma: dit bestand herhaalt de namen die het programma ze zelf geeft.",
glyph_by_glyph: "Het programma, glyph voor glyph",
geometry: "De matrix is [{matrix}]; het helt {slant}°; {pitch}; {codes}.",
pitch: [
"elke glyph heeft één breedte",
"elke glyph heeft een eigen breedte",
],
codes: [
"de codes volgen de tabel die het formaat publiceert",
"het schrijft zijn eigen codes uit, {count} stuks",
],
reached_by: "{character} wordt bereikt met code {code}, de glyph {name}, die {advance} opschuift",
widened: "De regel hierboven is één keer gezet met de breedtes die het programma noemt, en één keer met een metriekbestand erover dat elke glyph een kwart breder maakt: op 15 punten loopt hij {program} punten, dan {metrics}.",
decimal: '.',
kerned: "Gekerned uit het metriekbestand",
pairs: PAIRS,
kerning: "Dezelfde paren twee keer: de eerste regel is gezet zonder kerning, de tweede met kerning op de paren van het metriekbestand dat de bibliotheek naast het programma leest. Elk paar waarvan de naar elkaar gekeerde zijden schuin lopen of open staan, wordt naar elkaar toe geschoven, tot een kwart em, en de verplaatsing wordt in de regel zelf geschreven, dus tekent elke lezer de letters waar ze gezet zijn.",
};
/// The page in Portuguese.
const PORTUGUESE: Words = Words {
title: "Uma página composta num tipo de letra Type 1",
lead: "As três linhas abaixo são desenhadas a partir de um programa de tipo de letra no primeiro formato da Adobe. Não é um ficheiro de tipo de letra com tabelas onde procurar um glifo: é um programa PostScript, com os glifos cifrados um a um, e a biblioteca lê-o para saber que código chega a cada um. Cada glifo é composto com a largura que diz o ficheiro de métricas que acompanha o programa ou, sem ele, a que diz o próprio programa. O programa é depois levado para este ficheiro reduzido aos glifos que esta página desenha, e o programa de leitura desenha-os a partir desses mesmos bytes.",
shown: "Desenhado a partir do programa",
samples: [
"HQF DEVELOPMENT",
"ABCDEFGHIJKLM NOPQRSTUVWXYZ 0123456789",
"UM TIPO DE TRAÇOS, LIDO E CORTADO À MEDIDA.",
],
stated: "O que o programa diz de si mesmo",
facts: "Chama-se %name% e tem %glyphs% glifos, dos quais %reached% são alcançados por um código. Cada glifo cabe numa caixa, cujos cantos são %low% e %high%; uma haste vertical tem %stem% milésimos de eme de espessura, e um sublinhado fica %underline% abaixo da linha. Nenhum destes números está escrito nesta página: cada um é lido do programa.",
weight: "O que pesa",
weighed: "O próprio programa ocupa %program% bytes em disco, e uma página que compõe com ele as três linhas acima pesa %document% bytes, com o programa reduzido aos glifos que essas linhas desenham. Os dois números são medidos pelo programa que criou esta página, não escritos à mão.",
travels: "O que viaja com ele",
carried: "Como qualquer outro tipo de letra que esta biblioteca leva, um programa Type 1 viaja leve: só entram no ficheiro os glifos que uma página desenha de facto, por isso uma página que diz uma palavra leva as letras dessa palavra e não um alfabeto inteiro. O formato faz disso um trabalho próprio: cada glifo é cifrado à parte dentro do programa, por isso a biblioteca decifra o programa, reescreve a sua lista de glifos para conter só .notdef, os glifos desenhados e os glifos com que se constrói uma letra acentuada, e volta a cifrá-lo. Cada glifo mantido é copiado byte a byte, ainda sob a sua própria cifra, e os pedaços de contorno que vários glifos partilham são mantidos inteiros. A descrição do tipo de letra nomeia então cada glifo que o ficheiro contém. Num tipo de umas dezenas de glifos isso poupa pouco; num grande é quase todo o programa: um tipo de sessenta mil bytes composto em três palavras leva os glifos dessas três palavras e não o resto.",
caveat: "Um tipo de letra Type 1 é um tipo simples: os seus códigos são de um byte, por isso no máximo 256 dos seus glifos são alcançáveis de cada vez e nunca compõe texto para baixo. Que código chega a que glifo é assunto do programa: este ficheiro repete os nomes que o próprio programa lhes dá.",
glyph_by_glyph: "O programa, glifo a glifo",
geometry: "A sua matriz é [{matrix}]; inclina {slant}°; {pitch}; {codes}.",
pitch: [
"todos os glifos têm a mesma largura",
"cada glifo tem uma largura própria",
],
codes: [
"os seus códigos seguem a tabela que o formato publica",
"detalha os seus próprios códigos, {count} ao todo",
],
reached_by: "{character} é alcançado pelo código {code}, o glifo {name}, que avança {advance}",
widened: "A linha acima é composta uma vez com as larguras que o programa indica, e outra com um ficheiro de métricas escrito por cima que alarga cada glifo em um quarto: a 15 pontos mede {program} pontos, depois {metrics}.",
decimal: '.',
kerned: "Com crenagem do seu ficheiro de métricas",
pairs: PAIRS,
kerning: "Os mesmos pares duas vezes: a primeira linha é composta sem crenagem, a segunda com os pares do ficheiro de métricas que a biblioteca lê ao lado do programa. Cada par cujos lados virados um para o outro se inclinam ou ficam abertos é aproximado, até um quarto de eme, e o movimento é escrito na própria linha, por isso cada leitor desenha as letras onde foram compostas.",
};
/// The page in Italian.
const ITALIAN: Words = Words {
title: "Una pagina composta in un font Type 1",
lead: "Le tre righe qui sotto sono disegnate da un programma di font nel primo formato di Adobe. Non è un file di font con tabelle in cui cercare un glifo: è un programma PostScript, con i glifi cifrati uno per uno, e la libreria lo legge per sapere quale codice raggiunge ciascuno. Ogni glifo è composto largo quanto dice il file di metriche che accompagna il programma o, senza, quanto dice il programma stesso. Il programma è poi portato in questo file ridotto ai glifi che questa pagina disegna, e il programma di lettura li disegna da quegli stessi byte.",
shown: "Disegnato dal programma",
samples: [
"HQF DEVELOPMENT",
"ABCDEFGHIJKLM NOPQRSTUVWXYZ 0123456789",
"UN CARATTERE A TRATTI, LETTO E RIDOTTO.",
],
stated: "Cosa dice di sé il programma",
facts: "Si chiama %name% e contiene %glyphs% glifi, di cui %reached% raggiunti da un codice. Ogni glifo sta in un riquadro, i cui angoli sono %low% e %high%; un’asta verticale è spessa %stem% millesimi di em, e una sottolineatura sta %underline% sotto la riga. Nessuna di queste cifre è scritta in questa pagina: ciascuna è letta dal programma.",
weight: "Quanto pesa",
weighed: "Il programma stesso occupa %program% byte su disco, e una pagina che vi compone le tre righe qui sopra pesa %document% byte, con il programma ridotto ai glifi che quelle righe disegnano. Entrambe le cifre sono misurate dal programma che ha creato questa pagina, non scritte a mano.",
travels: "Cosa viaggia con esso",
carried: "Come ogni altro tipo di font che questa libreria porta, un programma Type 1 viaggia leggero: nel file entrano solo i glifi che una pagina disegna davvero, così una pagina che dice una parola porta le lettere di quella parola e non un alfabeto intero. Il formato ne fa un lavoro a sé: ogni glifo è cifrato separatamente dentro il programma, così la libreria decifra il programma, riscrive il suo elenco di glifi perché contenga solo .notdef, i glifi disegnati e quelli da cui si costruisce una lettera accentata, e lo cifra di nuovo. Ogni glifo conservato è copiato byte per byte, ancora sotto il proprio cifrario, e i pezzi di contorno che più glifi condividono sono conservati interi. La descrizione del font nomina allora ogni glifo che il file contiene. Su un carattere di qualche decina di glifi si risparmia poco; su uno grande è quasi tutto il programma: un carattere di sessantamila byte composto in tre parole porta i glifi di quelle tre parole e non il resto.",
caveat: "Un font Type 1 è un font semplice: i suoi codici sono di un byte, così al massimo 256 dei suoi glifi sono raggiungibili insieme e non compone mai testo verso il basso. Quale codice raggiunge quale glifo è affare del programma: questo file ripete i nomi che il programma stesso dà loro.",
glyph_by_glyph: "Il programma, glifo per glifo",
geometry: "La sua matrice è [{matrix}]; inclina di {slant}°; {pitch}; {codes}.",
pitch: [
"ogni glifo ha la stessa larghezza",
"ogni glifo ha una larghezza propria",
],
codes: [
"i suoi codici seguono la tabella che il formato pubblica",
"elenca i propri codici, {count} in tutto",
],
reached_by: "{character} è raggiunto dal codice {code}, il glifo {name}, che avanza di {advance}",
widened: "La riga qui sopra è composta una volta con le larghezze che indica il programma, e una volta con un file di metriche scritto sopra che allarga ogni glifo di un quarto: a 15 punti misura {program} punti, poi {metrics}.",
decimal: '.',
kerned: "Con la crenatura del suo file di metriche",
pairs: PAIRS,
kerning: "Le stesse coppie due volte: la prima riga è composta senza crenatura, la seconda con le coppie del file di metriche che la libreria legge accanto al programma. Ogni coppia i cui lati affacciati si inclinano o restano aperti è avvicinata, fino a un quarto di em, e lo spostamento è scritto nella riga stessa, così ogni programma di lettura disegna le lettere dove sono state composte.",
};
/// The page in Spanish.
const SPANISH: Words = Words {
title: "Una página compuesta en una fuente Type 1",
lead: "Las tres líneas de abajo se dibujan con un programa de fuente en el primer formato de Adobe. No es un archivo de fuente con tablas en que buscar un glifo: es un programa PostScript, con sus glifos cifrados uno a uno, y la biblioteca lo lee para saber qué código lleva a cada uno. Cada glifo se compone con el ancho que dice el archivo de métricas que acompaña al programa o, sin él, el que dice el propio programa. Luego el programa se lleva a este archivo recortado a los glifos que dibuja esta página, y el programa de lectura los dibuja con esos mismos bytes.",
shown: "Dibujado con el programa",
samples: [
"HQF DEVELOPMENT",
"ABCDEFGHIJKLM NOPQRSTUVWXYZ 0123456789",
"UNA FUENTE DE TRAZOS, LEÍDA Y RECORTADA.",
],
stated: "Lo que el programa dice de sí mismo",
facts: "Se llama %name% y tiene %glyphs% glifos, de los que %reached% son accesibles por un código. Cada glifo cabe en una caja, cuyas esquinas son %low% y %high%; un fuste vertical tiene %stem% milésimas de eme de grosor, y un subrayado va %underline% bajo la línea. Ninguna de esas cifras se escribe en esta página: cada una se lee del programa.",
weight: "Lo que pesa",
weighed: "El programa mismo ocupa %program% bytes en disco, y una página que compone con él las tres líneas de arriba pesa %document% bytes, con el programa recortado a los glifos que dibujan esas líneas. Las dos cifras las mide el programa que creó esta página, no se escriben a mano.",
travels: "Lo que viaja con ella",
carried: "Como cualquier otro tipo de fuente que lleva esta biblioteca, un programa Type 1 viaja ligero: solo los glifos que una página dibuja de verdad entran en el archivo, así que una página que dice una palabra lleva las letras de esa palabra y no un alfabeto entero. El formato hace de eso un trabajo propio: cada glifo va cifrado por separado dentro del programa, así que la biblioteca descifra el programa, reescribe su lista de glifos para que solo tenga .notdef, los glifos dibujados y los glifos con que se construye una letra acentuada, y lo vuelve a cifrar. Cada glifo conservado se copia byte a byte, aún bajo su propio cifrado, y los trozos de contorno que comparten varios glifos se conservan enteros. La descripción de la fuente nombra entonces cada glifo que tiene el archivo. En una fuente de unas decenas de glifos eso ahorra poco; en una grande es casi todo el programa: una fuente de sesenta mil bytes compuesta en tres palabras lleva los glifos de esas tres palabras y no el resto.",
caveat: "Una fuente Type 1 es una fuente simple: sus códigos son de un byte, así que como mucho 256 de sus glifos son accesibles a la vez y nunca compone texto hacia abajo. Qué código lleva a qué glifo es cosa del programa: este archivo repite los nombres que el propio programa les da.",
glyph_by_glyph: "El programa, glifo a glifo",
geometry: "Su matriz es [{matrix}]; se inclina {slant}°; {pitch}; {codes}.",
pitch: [
"todos los glifos tienen un mismo ancho",
"cada glifo tiene su propio ancho",
],
codes: [
"sus códigos siguen la tabla que publica el formato",
"detalla sus propios códigos, {count} en total",
],
reached_by: "{character} se alcanza con el código {code}, el glifo {name}, que avanza {advance}",
widened: "La línea de arriba se compone una vez con los anchos que indica el programa, y otra con un archivo de métricas escrito encima que ensancha cada glifo un cuarto: a 15 puntos mide {program} puntos, luego {metrics}.",
decimal: '.',
kerned: "Con interletraje de su archivo de métricas",
pairs: PAIRS,
kerning: "Los mismos pares dos veces: la primera línea se compone sin interletraje, la segunda con los pares del archivo de métricas que la biblioteca lee junto al programa. Cada par cuyos lados enfrentados se inclinan o quedan abiertos se acerca, hasta un cuarto de eme, y el movimiento se escribe en la propia línea, así que todo programa de lectura dibuja las letras donde se compusieron.",
};
/// The page in German.
const GERMAN: Words = Words {
title: "Eine in einer Type-1-Schrift gesetzte Seite",
lead: "Die drei Zeilen unten sind aus einem Schriftprogramm in Adobes erstem Format gezeichnet. Es ist keine Schriftdatei mit Tabellen, in denen man einen Glyphen nachschlägt: Es ist ein PostScript-Programm, seine Glyphen einzeln verschlüsselt, und die Bibliothek liest es, um herauszufinden, welcher Code jeden von ihnen erreicht. Jeder Glyph wird so breit gesetzt, wie die zum Programm gehörende Metrikdatei sagt, oder, ohne sie, wie das Programm selbst sagt. Das Programm wird dann, auf die Glyphen dieser Seite gekürzt, in diese Datei mitgenommen, und die Lesesoftware zeichnet sie aus genau diesen Bytes.",
shown: "Aus dem Programm gezeichnet",
samples: [
"HQF DEVELOPMENT",
"ABCDEFGHIJKLM NOPQRSTUVWXYZ 0123456789",
"EIN STRICHSCHNITT, GELESEN UND ZUGESCHNITTEN.",
],
stated: "Was das Programm über sich angibt",
facts: "Es nennt sich %name% und enthält %glyphs% Glyphen, von denen %reached% über einen Code erreichbar sind. Jeder Glyph steht in einem Rahmen mit den Ecken %low% und %high%; ein senkrechter Stamm ist %stem% Tausendstel Geviert dick, und eine Unterstreichung liegt %underline% unter der Zeile. Keine dieser Zahlen ist in diese Seite geschrieben: Jede wird aus dem Programm gelesen.",
weight: "Was es wiegt",
weighed: "Das Programm selbst hat %program% Bytes auf der Platte, und eine Seite, die die drei Zeilen oben darin setzt, wiegt %document% Bytes, das Programm auf die Glyphen dieser Zeilen gekürzt. Beide Zahlen misst das Programm, das diese Seite erstellt hat, keine ist von Hand eingetragen.",
travels: "Was mitreist",
carried: "Wie jede andere Schriftart, die diese Bibliothek mitführt, reist ein Type-1-Programm mit leichtem Gepäck: Nur die Glyphen, die eine Seite tatsächlich zeichnet, kommen in die Datei, also trägt eine Seite, die ein Wort sagt, die Buchstaben dieses Worts und nicht ein ganzes Alphabet. Das Format macht daraus eine eigene Arbeit: Jeder Glyph ist im Programm einzeln verschlüsselt, also entschlüsselt die Bibliothek das Programm, schreibt seine Glyphenliste so um, dass sie nur .notdef, die gezeichneten Glyphen und die Glyphen enthält, aus denen ein Buchstabe mit Akzent gebaut ist, und verschlüsselt es wieder. Jeder behaltene Glyph wird Byte für Byte kopiert, noch unter seiner eigenen Verschlüsselung, und die Konturteile, die mehrere Glyphen teilen, bleiben ganz. Die Beschreibung der Schrift nennt dann jeden Glyphen, den die Datei enthält. Bei einem Schnitt von ein paar Dutzend Glyphen spart das wenig; bei einem großen ist es der größte Teil des Programms: Ein Schnitt von sechzigtausend Bytes, in drei Wörtern gesetzt, trägt die Glyphen dieser drei Wörter und nicht den Rest.",
caveat: "Eine Type-1-Schrift ist eine einfache Schrift: Ihre Codes sind ein Byte, also sind höchstens 256 ihrer Glyphen zugleich erreichbar, und sie setzt nie senkrecht. Welcher Code welchen Glyphen erreicht, ist Sache des Programms — diese Datei wiederholt die eigenen Namen, die das Programm ihnen gibt.",
glyph_by_glyph: "Das Programm, Glyph für Glyph",
geometry: "Seine Matrix ist [{matrix}]; es neigt sich {slant}°; {pitch}; {codes}.",
pitch: [
"alle Glyphen haben dieselbe Breite",
"jeder Glyph hat seine eigene Breite",
],
codes: [
"seine Codes folgen der Tabelle, die das Format veröffentlicht",
"es schreibt seine eigenen Codes aus, {count} davon",
],
reached_by: "{character} erreicht über Code {code} den Glyphen {name}, der um {advance} vorrückt",
widened: "Die Zeile oben ist einmal nach den Breiten gesetzt, die das Programm angibt, und einmal mit einer darübergelegten Metrikdatei, die jeden Glyphen um ein Viertel verbreitert: bei 15 Punkt läuft sie {program} Punkt, dann {metrics}.",
decimal: '.',
kerned: "Nach seiner Metrikdatei unterschnitten",
pairs: PAIRS,
kerning: "Dieselben Paare zweimal: Die erste Zeile ist ohne Unterschneidung gesetzt, die zweite nach den Paaren der Metrikdatei unterschnitten, die die Bibliothek neben dem Programm liest. Jedes Paar, dessen einander zugewandte Seiten schräg stehen oder offen sind, rückt zusammen, um bis zu ein Viertel Geviert, und die Verschiebung wird in die Zeile selbst geschrieben, also zeichnet jeder Leser die Buchstaben dort, wo sie gesetzt wurden.",
};
/// 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 LEFT: f64 = 72.0;
/// How wide a block of text is.
const WIDTH: f64 = 450.0;
/// Sets a block of words with its first baseline at `top`, and hands back the
/// ordinate the block ends at.
fn block(
c: &mut Content,
handle: &FontHandle,
size: f64,
top: 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))
}
/// What a page holding nothing but the three samples weighs.
///
/// It is measured on a page of its own: a page that states its own weight never
/// settles, because writing the figure changes the figure.
fn weighed(words: &Words, face: &Type1Font) -> Result<usize, Box<dyn std::error::Error>> {
let mut doc = Document::new();
doc.set_license(licence::licensed());
let handle = doc.add_type1_font(face.clone());
let mut c = Content::new();
let mut top = 760.0;
for sample in words.samples {
top = block(&mut c, &handle, 15.0, top, sample)? - 10.0;
}
let mut page = Page::a4();
page.content = c.into_bytes();
doc.add_page(page)?;
Ok(doc.to_bytes()?.len())
}
/// How many of the program's glyphs a code reaches.
fn reached(face: &Type1Font) -> usize {
(0..=u8::MAX)
.filter(|code| face.character_at(*code).is_some())
.count()
}
/// The two corners of the box a face states, as the page writes them.
fn corners(face: &Type1Font) -> (String, String) {
let [llx, lly, urx, ury] = face.bbox();
(format!("{llx} · {lly}"), format!("{urx} · {ury}"))
}
/// Draws the whole page.
fn build(
words: &Words,
face: &Type1Font,
bare: &Type1Font,
prose: Font,
program: usize,
document: usize,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut doc = Document::new();
doc.set_license(licence::licensed());
let text = doc.add_font(prose);
let drawn = doc.add_type1_font(face.clone());
let mut c = Content::new();
let mut top = 760.0;
top = block(&mut c, &text, 18.0, top, words.title)? - 14.0;
top = block(&mut c, &text, 10.0, top, words.lead)? - 24.0;
top = block(&mut c, &text, 12.0, top, words.shown)? - 14.0;
for sample in words.samples {
top = block(&mut c, &drawn, 15.0, top, sample)? - 10.0;
}
top -= 14.0;
top = block(&mut c, &text, 12.0, top, words.stated)? - 10.0;
let stated = words
.facts
.replace("%name%", face.postscript_name())
.replace("%glyphs%", &face.glyph_names().count().to_string())
.replace("%reached%", &reached(face).to_string())
.replace("%low%", &corners(face).0)
.replace("%high%", &corners(face).1)
.replace(
"%stem%",
&face
.stem_v()
.map_or_else(|| "—".to_owned(), |v| v.to_string()),
)
.replace(
"%underline%",
&face
.underline()
.map_or_else(|| "—".to_owned(), |stated| (-stated.position).to_string()),
);
top = block(&mut c, &text, 10.0, top, &stated)? - 20.0;
top = block(&mut c, &text, 12.0, top, words.weight)? - 10.0;
let said = words
.weighed
.replace("%program%", &spaced(program))
.replace("%document%", &spaced(document));
top = block(&mut c, &text, 10.0, top, &said)? - 20.0;
top = block(&mut c, &text, 12.0, top, words.travels)? - 10.0;
top = block(&mut c, &text, 10.0, top, words.carried)? - 20.0;
c.set_fill(Rgb::gray(0.35))?;
block(&mut c, &text, 9.0, top, words.caveat)?;
let mut page = Page::a4();
page.content = c.into_bytes();
doc.add_page(page)?;
let page = second_page(&mut doc, &text, bare, words)?;
doc.add_page(page)?;
let kerned = drawn.clone().kerning();
let mut c = Content::new();
let mut top = 760.0;
top = block(&mut c, &text, 12.0, top, words.kerned)? - 14.0;
top = block(&mut c, &drawn, 15.0, top, words.pairs)? - 10.0;
top = block(&mut c, &kerned, 15.0, top, words.pairs)? - 20.0;
block(&mut c, &text, 10.0, top, words.kerning)?;
let mut page = Page::a4();
page.content = c.into_bytes();
doc.add_page(page)?;
Ok(doc.to_bytes()?)
}
/// The line the second page reads glyph by glyph and sets twice, drawn in the
/// capitals and figures the face holds.
const LINE: &str = "HQF 2038";
/// A metrics file for `face` that states every glyph a quarter wider than the
/// program does.
fn widening(face: &Type1Font) -> String {
let mut lines = Vec::new();
for name in face.glyph_names() {
let width = face.advance_of(name).unwrap_or_default() * 1.25;
lines.push(format!("C -1 ; WX {width:.0} ; N {name} ;"));
}
format!(
"StartFontMetrics 4.1\nFontName {}\nStartCharMetrics {}\n{}\nEndCharMetrics\nEndFontMetrics\n",
face.postscript_name(),
lines.len(),
lines.join("\n")
)
}
/// Where the codes of a font reading `encoding` come from, said in `words`.
#[allow(
clippy::match_same_arms,
reason = "an encoding the library adds later reads as the published one"
)]
fn codes_said(encoding: &BuiltinEncoding, words: &Words) -> String {
match encoding {
BuiltinEncoding::Standard => words.codes[0].to_owned(),
BuiltinEncoding::Own(table) => words.codes[1].replace("{count}", &table.len().to_string()),
_ => words.codes[0].to_owned(),
}
}
/// The second page: the program's geometry, the line read glyph by glyph, and
/// the line set twice, the second time under a metrics file that widens it.
fn second_page(
doc: &mut Document,
text: &FontHandle,
face: &Type1Font,
words: &Words,
) -> Result<Page, Box<dyn std::error::Error>> {
let mut c = Content::new();
let mut top = 760.0;
top = block(&mut c, text, 18.0, top, words.glyph_by_glyph)? - 14.0;
let entries = face.matrix().map(|entry| entry.to_string()).join(" ");
let origin = codes_said(face.encoding(), words);
let geometry = words
.geometry
.replace("{matrix}", &entries)
.replace("{slant}", &face.italic_angle().to_string())
.replace("{pitch}", words.pitch[usize::from(!face.fixed_pitch())])
.replace("{codes}", &origin);
top = block(&mut c, text, 10.0, top, &geometry)? - 12.0;
for each in LINE.chars().filter(|each| *each != ' ') {
let Some(reaching) = face.code_of(each) else {
continue;
};
let glyph = face.name_at(reaching).unwrap_or_default();
let moves = face
.advance_of(glyph)
.unwrap_or_else(|| face.advance_at(reaching));
let said = words
.reached_by
.replace("{character}", &each.to_string())
.replace("{code}", &reaching.to_string())
.replace("{name}", glyph)
.replace("{advance}", &moves.to_string());
top = block(&mut c, text, 9.5, top, &said)? - 2.0;
}
let widened = face.clone().with_metrics(widening(face))?;
let handles = [
doc.add_type1_font(face.clone()),
doc.add_type1_font(widened),
];
top -= 16.0;
let mut runs = Vec::new();
for handle in &handles {
top = block(&mut c, handle, 15.0, top, LINE)? - 8.0;
let run = format!("{:.1}", handle.measure(LINE, 15.0));
runs.push(run.replace('.', &words.decimal.to_string()));
}
let said = words
.widened
.replace("{program}", &runs[0])
.replace("{metrics}", &runs[1]);
block(&mut c, text, 10.0, top - 8.0, &said)?;
let mut page = Page::a4();
page.content = c.into_bytes();
Ok(page)
}
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 mut args = env::args().skip(1);
// A named file is written as named; the default one carries the language,
// so the two languages do not overwrite each other in `tmp/`.
let out = args
.next()
.unwrap_or_else(|| language.file_name(&out::default_path("type_one_font")));
let (face_path, metrics_path) = args.next().map_or_else(
|| (default_face(), Some(default_metrics())),
|face| (PathBuf::from(face), args.next().map(PathBuf::from)),
);
let bytes = fs::read(&face_path)?;
let bare = Type1Font::parse(&bytes)?;
let face = match metrics_path {
Some(metrics) => bare.clone().with_metrics(fs::read(metrics)?)?,
None => bare.clone(),
};
let prose = Font::parse(fs::read(prose_face())?)?;
let document = weighed(words, &face)?;
let drawn = build(words, &face, &bare, prose, bytes.len(), document)?;
if let Some(parent) = Path::new(&out).parent() {
fs::create_dir_all(parent)?;
}
fs::write(&out, &drawn)?;
println!("wrote {out}: {} bytes", drawn.len());
Ok(())
}
/// A count with a space between its thousands, which is how every language this
/// example is written in writes one.
fn spaced(count: usize) -> String {
let digits = count.to_string();
let mut out = String::with_capacity(digits.len() + digits.len() / 3);
for (index, digit) in digits.chars().enumerate() {
if index > 0 && (digits.len() - index).is_multiple_of(3) {
out.push('\u{00A0}');
}
out.push(digit);
}
out
}
#[cfg(test)]
mod tests {
use super::{Language, PAIRS, WORDS, default_face, default_metrics, language, spaced};
use hqf_pdf::Type1Font;
/// The lines two languages are allowed to write the same way: a mark, a run
/// of letters and figures, and the name of the firm are the same whoever
/// reads them.
const SPARED: [&str; 3] = [
"\"HQF DEVELOPMENT\"",
"\"ABCDEFGHIJKLM NOPQRSTUVWXYZ 0123456789\"",
"pairs: \"AT AV AW AY FA LT LV LW LY PA TA VA WA YA\"",
];
#[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:?}"
);
}
#[test]
fn every_line_the_page_shows_is_one_the_face_can_draw() {
// The face draws capitals, figures and the marked letters French needs,
// and nothing else. A sample holding anything more would be drawn with
// a hole in it.
let face = Type1Font::parse(std::fs::read(default_face()).expect("the face is committed"))
.expect("the face reads");
for language in [Language::English, Language::French] {
let words = super::Words::of(language);
for sample in words.samples.into_iter().chain([words.pairs]) {
let missing: Vec<char> = sample
.chars()
.filter(|character| face.code_of(*character).is_none())
.collect();
assert!(missing.is_empty(), "{sample:?} needs {missing:?}");
}
}
}
#[test]
fn every_pair_the_third_page_shows_is_one_the_metrics_file_kerns() {
let face = Type1Font::parse(std::fs::read(default_face()).expect("the face is committed"))
.expect("the face reads")
.with_metrics(std::fs::read(default_metrics()).expect("its metrics are committed"))
.expect("its metrics read");
for pair in PAIRS.split(' ') {
let (first, second) = pair.split_at(1);
assert!(face.kerning_between(first, second) < 0.0, "{pair}");
}
}
#[test]
fn a_count_is_written_with_a_space_between_its_thousands() {
assert_eq!(spaced(7), "7");
assert_eq!(spaced(999), "999");
assert_eq!(spaced(1000), "1\u{00A0}000");
assert_eq!(spaced(8117), "8\u{00A0}117");
}
}
|