1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887 | //! Writes a document that says what the marks on its pages stand for.
//!
//! Two things are drawn on each page: content, and furniture. The content is
//! marked and numbered, and the structure tree points at those numbers in
//! reading order, so that something reading the document aloud reads it in the
//! order it was written rather than the order it was drawn. The furniture — the
//! running head, the page number — is marked as an artifact instead, carries no
//! number, and is therefore invisible to everything that follows the tree.
//!
//! The page and the branch of the tree describing it are built together, by one
//! function, from one counter: that is what keeps a number in the stream and a
//! number in the tree from ever drifting apart.
//!
//! The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks
//! which set the document is written in. The tags are not language: `H1`, `P`
//! and `Figure` are the names the standard gives, and the tree declares which
//! tongue it is that the words under them are written in.
//!
//! Usage: `cargo run --example write_tagged -- tmp/tagged.pdf [font.ttf]`
//! `cargo run --example write_tagged -- tmp/tagged.pdf --archival`
//! `cargo run --example write_tagged -- tmp/tagged.pdf --archival-2`
//! `cargo run --example write_tagged -- tmp/tagged.pdf --archival-2u`
//! `cargo run --example write_tagged -- tmp/tagged.pdf --archival-2a`
//! `cargo run --example write_tagged -- tmp/tagged.pdf --archival-3u`
//! `cargo run --example write_tagged -- tmp/tagged.pdf --archival-4`
//! `cargo run --example write_tagged -- tmp/tagged.pdf --ua`
//! `cargo run --example write_tagged -- tmp/tagged.pdf --ua-2`
//! `HQF_PDF_LANG=fr cargo run --example write_tagged -- tmp/balise.pdf`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
use hqf_pdf::cos::Name;
use hqf_pdf::layout::{
Align, Cell, Columns, Padding, Row, Rule, Stroke, Table, TableFrame, TextFlow,
};
use hqf_pdf::metadata::xmp::{Metadata, PdfA, PdfUa};
use hqf_pdf::structure::{ListNumbering, PDF_2_NAMESPACE};
use hqf_pdf::{
Artifact, Bookmark, Document, Font, FontHandle, License, Link, LinkTarget, Marks, OpenAction,
Page, PageFit, Rgb, StructElement, StructureTree, Version,
};
#[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 font the example draws with when none is given on the command line.
fn default_font() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fonts")
.join("DejaVuSans.ttf")
}
/// The archival level each flag on the command line asks for. A command line
/// naming several takes the first of this list. `--archival` on its own is the
/// level that carries a tree, under the part that also lets a file travel
/// inside the document.
const ARCHIVAL_LEVELS: [(&str, PdfA); 6] = [
("--archival-2a", PdfA::A2A),
("--archival-2u", PdfA::A2U),
("--archival-2", PdfA::A2B),
("--archival-3u", PdfA::A3U),
("--archival-4", PdfA::A4),
("--archival", PdfA::A3A),
];
/// The archival level `args` asks for, if any. A command line naming several
/// takes the first of [`ARCHIVAL_LEVELS`]; a flag is matched whole, so
/// `--archival` never answers for `--archival-2`.
fn archival_level(args: &[String]) -> Option<PdfA> {
ARCHIVAL_LEVELS
.iter()
.find(|(flag, _)| args.iter().any(|arg| arg == flag))
.map(|(_, level)| *level)
}
/// The tag every heading of the document is a heading of.
const HEADING: &str = "H1";
/// Which of the sheets carries the table.
const TABLE_ON: usize = 1;
/// Which of the sheets carries the numbered list.
const LIST_ON: usize = 1;
/// The labels the items of the list carry: figures a numbering gives rather
/// than words a language writes.
const LABELS: [&str; 3] = ["1.", "2.", "3."];
/// The baseline the first item of the list is set on, and how far the next one
/// sits under it.
const LIST_TOP: f64 = 420.0;
/// See [`LIST_TOP`].
const LIST_STEP: f64 = 16.0;
/// Where the label of an item is drawn, and where the words beside it begin.
const LABEL_AT: f64 = 72.0;
/// See [`LABEL_AT`].
const ITEM_AT: f64 = 92.0;
/// The baseline the line leading from one sheet to the next is set on.
const LINK_ON: f64 = 380.0;
/// The identifier the section of the sheet at `index` is named by, which is
/// what a link leading to that section states.
fn section_id(index: usize) -> String {
format!("section-{index}")
}
/// How many of each part the table lists, and what that many come to: figures
/// an order gives rather than words a language writes.
const AMOUNTS: [[&str; 2]; 3] = [
["40", "480.00 EUR"],
["6", "1 260.00 EUR"],
["6", "2 510.00 EUR"],
];
/// One page of the document: what it says, and how it says it.
#[derive(Debug)]
struct Sheet {
/// The heading of the page.
heading: &'static str,
/// What the page says, its paragraphs separated by newlines. The flow
/// breaks them into lines and marks one run per paragraph.
body: &'static str,
/// What an illustration on the page says, for something that cannot see it.
/// The page draws one only if it has something to say about it.
figure: Option<&'static str>,
}
/// The words the document is written in, one set per language.
#[derive(Debug)]
struct Words {
/// The title the file states, which is also the running head every page
/// carries.
title: &'static str,
/// What a page number reads, in front of the number and between the two.
page_before: &'static str,
/// See [`Words::page_before`].
page_between: &'static str,
/// The language the structure tree declares, so that something reading the
/// document aloud reads it in the tongue it was written in.
lang: &'static str,
/// The two sheets.
sheets: [Sheet; 2],
/// What the line leading from the first sheet to the second reads, which is
/// also what a reader that cannot see the page is told about it.
cross_reference: &'static str,
/// What the panel row leading to the name the document registers reads.
contents: &'static str,
/// The three items of the numbered list, each drawn beside the label of
/// [`LABELS`] that numbers it.
steps: [&'static str; 3],
/// The headings of the table's three columns.
columns: [&'static str; 3],
/// What each of the parts in the table is called.
parts: [&'static str; 3],
}
impl Words {
/// The words the document is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
/// The rows of the table, its headings first, then each part beside the
/// figures the order gives for it.
fn table(&self) -> Vec<[&'static str; 3]> {
std::iter::once(self.columns)
.chain(
self.parts
.iter()
.zip(AMOUNTS)
.map(|(part, amount)| [*part, amount[0], amount[1]]),
)
.collect()
}
/// What the page number of the sheet at `index` reads.
fn page_number(&self, index: usize) -> String {
format!(
"{}{}{}{}",
self.page_before,
index + 1,
self.page_between,
self.sheets.len()
)
}
}
/// The document in English.
const ENGLISH: Words = Words {
title: "A tagged document",
page_before: "Page ",
page_between: " of ",
lang: "en-GB",
sheets: [
Sheet {
heading: "What a tagged document is",
body: "A tagged document says what each run of ink on its pages stands for: \
this run is a heading, that one is a paragraph, and the rule across the \
top is furniture that says nothing at all.\n\
Something reading the document aloud follows the order the tree gives, \
not the order the ink was laid down in, and a phone reflowing the page \
has something to reflow.",
figure: Some("A filled rectangle, standing in for an illustration"),
},
Sheet {
heading: "What the tree leaves out",
body: "The running head above and the page number below are artifacts. They \
are drawn, they are not content, and nothing reading the document aloud \
says them.\n\
An artifact carries no number, so the tree cannot point at it. That \
is the whole of how reading software tells furniture from \
what the document says.",
figure: None,
},
],
cross_reference: "Read on: what the tree leaves out",
contents: "Contents",
steps: [
"Mark the run as the page draws it",
"Number it from the page's own counter",
"Point the tree at that number",
],
columns: ["Part", "Quantity", "Amount"],
parts: ["Bearing, 12 mm", "Shaft, ground", "Housing, cast"],
};
/// The document in French.
const FRENCH: Words = Words {
title: "Un document balisé",
page_before: "Page ",
page_between: " sur ",
lang: "fr-FR",
sheets: [
Sheet {
heading: "Ce qu’est un document balisé",
body: "Un document balisé dit ce que représente chaque élément de ses pages : \
celui-ci est un titre, celui-là un paragraphe, et le filet du haut relève du \
décor et ne dit rien du tout.\nUn logiciel qui lit le document à voix haute \
suit l’ordre que donne l’arbre, et non l’ordre dans lequel l’encre a été \
posée ; un téléphone qui recompose la page a de quoi la recomposer.",
figure: Some("Un rectangle plein, à la place d’une illustration"),
},
Sheet {
heading: "Ce que l’arbre laisse de côté",
body: "Le titre courant au-dessus et le numéro de page en dessous sont des artefacts. \
Ils sont affichés, ce ne sont pas du contenu, et aucun lecteur d’écran ne les \
prononce.\nUn artefact n’a aucun numéro, donc l’arbre ne peut pas le désigner. \
C’est tout ce qui permet à un logiciel de lecture de distinguer le décor de ce \
que dit le document.",
figure: None,
},
],
cross_reference: "À suivre : ce que l’arbre laisse de côté",
contents: "Sommaire",
steps: [
"Marquer l’élément au moment où la page le trace",
"Le numéroter avec le compteur de la page",
"Faire pointer l’arbre sur ce numéro",
],
columns: ["Pièce", "Quantité", "Montant"],
parts: ["Roulement, 12 mm", "Arbre rectifié", "Carter en fonte"],
};
/// The document in Dutch.
const DUTCH: Words = Words {
title: "Een getagd document",
page_before: "Pagina",
page_between: " van",
lang: "nl-NL",
sheets: [
Sheet {
heading: "Wat een getagd document is",
body: "Een getagd document zegt waar elk stuk inkt op zijn pagina’s voor staat: dit stuk is een kop, dat een alinea, en de lijn bovenaan is meubilair dat helemaal niets zegt.\nSoftware die het document voorleest, volgt de volgorde die de boom geeft, niet de volgorde waarin de inkt is neergelegd, en een telefoon die de pagina herschikt, heeft iets om te herschikken.",
figure: Some("Een gevulde rechthoek, in plaats van een illustratie"),
},
Sheet {
heading: "Wat de boom weglaat",
body: "De kopregel hierboven en het paginanummer hieronder zijn artefacten. Ze worden getekend, ze zijn geen inhoud, en software die het document voorleest, slaat ze over.\nEen artefact heeft geen nummer, dus kan de boom er niet naar wijzen. Zo en niet anders onderscheidt leessoftware meubilair van wat het document zegt.",
figure: None,
},
],
cross_reference: "Lees verder: wat de boom weglaat",
contents: "Inhoud",
steps: [
"Het stuk markeren zoals de pagina het tekent",
"Het nummeren vanuit de eigen teller van de pagina",
"De boom naar dat nummer laten wijzen",
],
columns: ["Onderdeel", "Aantal", "Bedrag"],
parts: ["Lager, 12 mm", "As, geslepen", "Behuizing, gegoten"],
};
/// The document in Portuguese.
const PORTUGUESE: Words = Words {
title: "Um documento etiquetado",
page_before: "Página",
page_between: " de",
lang: "pt-PT",
sheets: [
Sheet {
heading: "O que é um documento etiquetado",
body: "Um documento etiquetado diz o que representa cada troço de tinta das suas páginas: este troço é um título, aquele é um parágrafo, e o filete no topo é mobília que não diz nada.\nO programa que lê o documento em voz alta segue a ordem que a árvore dá, não a ordem em que a tinta foi posta, e um telemóvel que reorganiza a página tem algo para reorganizar.",
figure: Some("Um retângulo preenchido, no lugar de uma ilustração"),
},
Sheet {
heading: "O que a árvore deixa de fora",
body: "O cabeçalho em cima e o número de página em baixo são artefactos. São desenhados, não são conteúdo, e nada que leia o documento em voz alta os diz.\nUm artefacto não leva número, por isso a árvore não pode apontar para ele. É exatamente assim que um programa de leitura distingue a mobília do que o documento diz.",
figure: None,
},
],
cross_reference: "Continue a ler: o que a árvore deixa de fora",
contents: "Índice",
steps: [
"Marcar o troço como a página o desenha",
"Numerá-lo pelo contador próprio da página",
"Apontar a árvore para esse número",
],
columns: ["Peça", "Quantidade", "Montante"],
parts: ["Rolamento, 12 mm", "Veio, retificado", "Caixa, fundida"],
};
/// The document in Italian.
const ITALIAN: Words = Words {
title: "Un documento marcato",
page_before: "Pagina",
page_between: " di",
lang: "it-IT",
sheets: [
Sheet {
heading: "Che cos’è un documento marcato",
body: "Un documento marcato dice cosa rappresenta ogni tratto d’inchiostro delle sue pagine: questo è un titolo, quello un paragrafo, e il filetto in alto è un ornamento che non dice nulla.\nCiò che legge il documento ad alta voce segue l’ordine dato dall’albero, non l’ordine in cui è stato steso l’inchiostro, e un telefono che reimpagina la pagina ha qualcosa da reimpaginare.",
figure: Some("Un rettangolo pieno, al posto di un’illustrazione"),
},
Sheet {
heading: "Ciò che l’albero lascia fuori",
body: "L’intestazione in alto e il numero di pagina in basso sono artefatti. Sono disegnati, non sono contenuto, e nulla che legga il documento ad alta voce li pronuncia.\nUn artefatto non porta numero, così l’albero non può indicarlo. È tutto qui il modo in cui un programma di lettura distingue gli ornamenti da ciò che dice il documento.",
figure: None,
},
],
cross_reference: "Continuate a leggere: ciò che l’albero lascia fuori",
contents: "Indice",
steps: [
"Marcare il tratto come lo disegna la pagina",
"Numerarlo con il contatore proprio della pagina",
"Far puntare l’albero a quel numero",
],
columns: ["Pezzo", "Quantità", "Importo"],
parts: ["Cuscinetto, 12 mm", "Albero, rettificato", "Carter, fuso"],
};
/// The document in Spanish.
const SPANISH: Words = Words {
title: "Un documento etiquetado",
page_before: "Página",
page_between: " de",
lang: "es-ES",
sheets: [
Sheet {
heading: "Qué es un documento etiquetado",
body: "Un documento etiquetado dice qué representa cada tramo de tinta de sus páginas: este tramo es un título, aquel es un párrafo, y el filete de arriba es decoración que no dice nada.\nUn programa que lee el documento en voz alta sigue el orden que da el árbol, no el orden en que se puso la tinta, y un teléfono que recompone la página tiene algo que recomponer.",
figure: Some("Un rectángulo relleno, en lugar de una ilustración"),
},
Sheet {
heading: "Lo que el árbol deja fuera",
body: "La cabecera de arriba y el número de página de abajo son artefactos. Se dibujan, no son contenido, y ningún programa que lea el documento en voz alta los pronuncia.\nUn artefacto no lleva número, así que el árbol no puede señalarlo. Así es, ni más ni menos, como un programa de lectura distingue la decoración de lo que dice el documento.",
figure: None,
},
],
cross_reference: "Siga leyendo: lo que el árbol deja fuera",
contents: "Índice",
steps: [
"Marcar el tramo como lo dibuja la página",
"Numerarlo con el contador propio de la página",
"Hacer que el árbol señale ese número",
],
columns: ["Pieza", "Cantidad", "Importe"],
parts: ["Rodamiento, 12 mm", "Eje, rectificado", "Carcasa, fundida"],
};
/// The document in German.
const GERMAN: Words = Words {
title: "Ein getaggtes Dokument",
page_before: "Seite ",
page_between: " von ",
lang: "de-DE",
sheets: [
Sheet {
heading: "Was ein getaggtes Dokument ist",
body: "Ein getaggtes Dokument sagt, wofür jeder Farbauftrag auf seinen Seiten steht: Dies ist eine Überschrift, das ein Absatz, und die Linie oben ist Beiwerk, das gar nichts sagt.\nWas beim Vorlesen zu hören ist, folgt der Reihenfolge, die der Baum vorgibt, nicht der, in der die Farbe aufgetragen wurde, und ein Telefon, das die Seite neu umbricht, hat etwas zum Umbrechen.",
figure: Some("Ein gefülltes Rechteck, stellvertretend für eine Abbildung"),
},
Sheet {
heading: "Was der Baum auslässt",
body: "Der Kolumnentitel oben und die Seitenzahl unten sind Artefakte. Sie werden gezeichnet, sind aber kein Inhalt, und beim Vorlesen werden sie nicht ausgesprochen.\nEin Artefakt trägt keine Nummer, also kann der Baum nicht darauf zeigen. Genau daran unterscheidet eine Lesesoftware das Beiwerk von dem, was das Dokument sagt.",
figure: None,
},
],
cross_reference: "Weiterlesen: was der Baum auslässt",
contents: "Inhalt",
steps: [
"Den Abschnitt so markieren, wie die Seite ihn zeichnet",
"Ihn mit dem eigenen Zähler der Seite nummerieren",
"Den Baum auf diese Nummer zeigen lassen",
],
columns: ["Teil", "Menge", "Betrag"],
parts: ["Lager, 12 mm", "Welle, geschliffen", "Gehäuse, gegossen"],
};
/// 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),
];
/// Draws a line of text at a point on the page.
fn text(
content: &mut Content,
font: &FontHandle,
size: f64,
x: f64,
y: f64,
line: &str,
) -> Result<(), hqf_pdf::Error> {
content.begin_text();
content.set_font(font, size)?;
content.text_origin(x, y)?;
content.show_glyphs(&font.glyphs(line));
content.end_text();
Ok(())
}
/// Draws the line that leads on to another sheet, and hands back the link over
/// it and the branch of the tree holding both.
///
/// The run of the line is held by a `Link` element that also names the
/// rectangle over it, so the tree and the annotation say the same thing about
/// the same words. The link leads to the element of the tree carrying `wanted`
/// rather than to a page, which is what the accessibility standard built on PDF
/// 2.0 asks of a destination inside the document.
fn leading_on(
content: &mut Content,
font: &FontHandle,
marks: &mut Marks,
index: usize,
words: &Words,
wanted: &str,
) -> Result<(Link, StructElement), hqf_pdf::Error> {
let id = marks.next_id();
content.begin_tagged(&Name::new("Span"), id);
text(content, font, 11.0, 72.0, LINK_ON, words.cross_reference)?;
content.end_marked();
let link = Link::new(
72.0,
LINK_ON - 3.0,
font.measure(words.cross_reference, 11.0),
14.0,
LinkTarget::Element(wanted.as_bytes().to_vec()),
)
.description(words.cross_reference);
let element = StructElement::new("Link")
.child(StructElement::new("Span").content(index, id))
.link(index, 0);
Ok((link, element))
}
/// Draws the numbered list and hands back the branch of the tree describing it.
///
/// Each item is drawn as two runs — the label, then the words beside it — and
/// the list states the scheme its labels are numbered by, which is what lets
/// something reading the document aloud number the items itself.
fn numbered_list(
content: &mut Content,
font: &FontHandle,
marks: &mut Marks,
index: usize,
words: &Words,
) -> Result<StructElement, hqf_pdf::Error> {
let mut list = StructElement::new("L").list_numbering(ListNumbering::Decimal);
let mut baseline = LIST_TOP;
for (label, step) in LABELS.iter().zip(words.steps) {
let numbered = marks.next_id();
content.begin_tagged(&Name::new("Lbl"), numbered);
text(content, font, 11.0, LABEL_AT, baseline, label)?;
content.end_marked();
let said = marks.next_id();
content.begin_tagged(&Name::new("LBody"), said);
text(content, font, 11.0, ITEM_AT, baseline, step)?;
content.end_marked();
list = list.child(
StructElement::new("LI")
.child(StructElement::new("Lbl").content(index, numbered))
.child(StructElement::new("LBody").content(index, said)),
);
baseline -= LIST_STEP;
}
Ok(list)
}
/// Builds one page and the branch of the tree that describes it.
///
/// The numbers come from the page's own counter, so no two runs of the page
/// share one, and the tree is handed the very numbers the stream was marked
/// with.
///
/// `leads_to` names the element of the tree the sheet's one link goes to, and a
/// sheet given none carries no link at all.
fn sheet(
font: &FontHandle,
index: usize,
words: &Words,
leads_to: Option<&str>,
) -> Result<(Page, StructElement), hqf_pdf::Error> {
let sheet = &words.sheets[index];
let mut marks = Marks::new();
let mut content = Content::new();
let mut section = StructElement::new("Sect");
// The running head: a rule and a title, drawn on every page and read on
// none.
content.begin_artifact(Artifact::Header);
content.rect(72.0, 780.0, 451.0, 1.0)?;
content.fill();
text(&mut content, font, 9.0, 72.0, 788.0, words.title)?;
content.end_marked();
let id = marks.next_id();
content.begin_tagged(&Name::new(HEADING), id);
text(&mut content, font, 18.0, 72.0, 730.0, sheet.heading)?;
content.end_marked();
section = section.child(StructElement::new(HEADING).content(index, id));
// The flow breaks the text into lines and marks one run per paragraph,
// handing back the number each run carries. The page never says where a
// line ends, and the tree gets one element per paragraph rather than one
// for the block.
let flow = TextFlow::new(font, 11.0).leading(16.0);
let lines = flow.break_lines(sheet.body, 451.0);
let mut top = 706.0;
content.begin_text();
let numbers = flow.draw_tagged(
&mut content,
&lines,
72.0,
top,
451.0,
&Name::new("P"),
&mut marks,
)?;
content.end_text();
top -= flow.height(&lines);
for number in numbers {
section = section.child(StructElement::new("P").content(index, number));
}
// The table describes itself: it marks each of its cells as it draws them
// and hands back the branch of the tree saying which row and which column
// each of them sits in. The counter it numbers from is the page's own, the
// same one every run above was numbered from.
if index == TABLE_ON {
let mut table = Table::new(Columns::equal(3, 451.0)?);
table.header(1);
table.rule(Rule::Frame, Stroke::black(0.8));
table.rule(Rule::HorizontalOther, Stroke::new(0.25, Rgb::gray(0.75)));
for row in words.table() {
let mut placed = Row::new();
for (column, cell) in row.iter().enumerate() {
let align = if column == 0 {
Align::Left
} else {
Align::Right
};
placed = placed.cell(
Cell::new(font, 10.0, *cell)
.align(align)
.padding(Padding::symmetric(6.0, 4.0)),
);
}
table.push(placed);
}
let placement = table.fit(TableFrame::new(72.0, top - 20.0, 300.0), 0)?;
section = section.child(placement.draw_tagged(&mut content, index, &mut marks)?);
}
if index == LIST_ON {
section = section.child(numbered_list(&mut content, font, &mut marks, index, words)?);
}
if let Some(alt) = sheet.figure {
let id = marks.next_id();
content.begin_tagged(&Name::new("Figure"), id);
content.rect(72.0, 420.0, 200.0, 120.0)?;
content.fill();
content.end_marked();
section = section.child(StructElement::new("Figure").alt(alt).content(index, id));
}
let mut carried = None;
if let Some(wanted) = leads_to {
let (link, element) = leading_on(&mut content, font, &mut marks, index, words, wanted)?;
carried = Some(link);
section = section.child(element);
}
// The page number: drawn, and no more part of what the document says than
// the staple would be.
content.begin_artifact(Artifact::Footer);
text(
&mut content,
font,
9.0,
72.0,
60.0,
&words.page_number(index),
)?;
content.end_marked();
let mut page = Page::a4();
page.content = content.into_bytes();
page.links.extend(carried);
Ok((page, section))
}
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: Vec<String> = env::args().skip(1).collect();
let evaluation = args.iter().any(|arg| arg == "--evaluation");
// A tagged archival file, for a validator to be pointed at, at each level
// the standard grades: B asks nothing of the marks, U asks every mark to
// stand for a letter, and A asks for the structure tree on top — the one
// claim in the packet that a file cannot make on its own. Part 4 grades
// nothing and carries no information dictionary.
let archival = archival_level(&args);
let archival_4 = archival == Some(PdfA::A4);
// The accessibility claim, which a validator judges against the tree, the
// artifacts, the embedded fonts and the title all at once.
let ua = args.iter().any(|arg| arg == "--ua");
// The same claim under the part built on PDF 2.0, whose tree is rooted in
// one `Document` element named from the PDF 2.0 set of tags.
let ua_2 = args.iter().any(|arg| arg == "--ua-2");
args.retain(|arg| !arg.starts_with("--"));
let mut args = args.into_iter();
// 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 path = args
.next()
.unwrap_or_else(|| language.file_name(&out::default_path("tagged")));
let font_path = args.next().map_or_else(default_font, PathBuf::from);
let mut doc = Document::new();
doc.set_license(if evaluation {
License::evaluation()
} else {
licence::licensed()
});
if let Some(level) = archival {
doc.set_conformance(level);
}
if ua {
doc.set_ua_conformance(PdfUa::One);
}
if ua_2 {
doc.set_ua_conformance(PdfUa::Two);
}
if archival_4 || ua_2 {
doc.set_version(Version::V2_0);
}
if archival.is_some() || ua || ua_2 {
let mut facts = Metadata::default();
facts.title = Some(words.title.to_owned());
facts.producer = Some("hqf-pdf".to_owned());
facts.created = Some("2026-07-21T09:30:00+02:00".to_owned());
doc.set_metadata(facts);
}
// Part 4 writes no information dictionary at all, and refuses an entry
// named by hand rather than dropping it.
if !archival_4 {
doc.set_info(Name::new("Title"), words.title);
}
let font = doc.add_font(Font::parse(fs::read(&font_path)?)?);
// The whole document is one element, holding one section per page. Reading
// software that follows the tree walks it in this order, whatever order the
// ink was laid down in.
let mut body = StructElement::new("Document");
if ua_2 {
body = body.namespace(PDF_2_NAMESPACE);
}
// Under the accessibility standard built on PDF 2.0, each section is named,
// and the first sheet carries a line leading to the section of the second:
// a destination inside such a document points at a piece of the structure
// rather than at a page, so the link holds wherever that section is laid
// out.
for index in 0..words.sheets.len() {
let leads_to = (ua_2 && index + 1 < words.sheets.len()).then(|| section_id(index + 1));
let (page, mut section) = sheet(&font, index, words, leads_to.as_deref())?;
if ua_2 {
section = section.id(section_id(index));
}
doc.add_page(page)?;
body = body.child(section);
}
// The panel beside the page leads to the sections themselves rather than to
// page numbers: under the accessibility standard built on PDF 2.0, every
// way into the document points at a piece of the structure. So does the
// view the document opens on, and so does the name it registers for another
// document to aim at.
if ua_2 {
for (index, sheet) in words.sheets.iter().enumerate() {
doc.add_bookmark(Bookmark::element(sheet.heading, section_id(index)));
}
doc.add_bookmark(Bookmark::named(words.contents, "contents"));
doc.set_open_action(OpenAction::element(section_id(0)));
doc.add_element_destination("contents", section_id(0), PageFit::WholePage)?;
}
doc.set_structure(StructureTree::new().lang(words.lang).child(body));
let bytes = doc.to_bytes()?;
if let Some(parent) = Path::new(&path).parent() {
fs::create_dir_all(parent)?;
}
fs::write(&path, &bytes)?;
println!("wrote {} ({} bytes)", path, bytes.len());
Ok(())
}
#[cfg(test)]
mod tests {
use hqf_pdf::metadata::xmp::PdfA;
use hqf_pdf::{Document, Font};
use super::{ARCHIVAL_LEVELS, WORDS, Words, archival_level, default_font, language};
/// The lines two languages are allowed to write the same way. A page number
/// opens on a word two languages spell alike, and so do a few names and
/// headings.
const SPARED: [&str; 6] = [
"page_before: \"Page \"",
// Written the same way in Portuguese.
r#"contents: "Índice""#,
r#"page_before: "Página""#,
r#"page_between: " de""#,
// Written the same way in Dutch.
r#""Lager, 12 mm""#,
r#"page_before: "Pagina""#,
];
/// A4's width, and the room a line the page draws whole has: it is not
/// broken to anything, so it only has to stay on the paper.
const PAGE_WIDTH: f64 = 595.276;
/// See [`PAGE_WIDTH`].
const PAPER: f64 = PAGE_WIDTH - 72.0;
#[test]
fn every_language_draws_the_page_in_its_own_words() {
let untranslated = language::untranslated_lines(&WORDS, &SPARED);
assert!(
untranslated.is_empty(),
"the document says these in more than one language: {untranslated:?}"
);
}
/// The running head, the headings, the page numbers and the line leading
/// from one sheet to the next are drawn whole rather than broken to the
/// measure the body is set to: any of them can run off the sheet.
#[test]
fn every_language_writes_lines_no_wider_than_the_paper() {
let mut doc = Document::new();
let font = Font::parse(std::fs::read(default_font()).expect("the committed font is there"))
.expect("the committed font parses");
let text = doc.add_font(font);
for (named, words) in WORDS {
let code = named.code();
let lines = std::iter::once((9.0, words.title.to_owned()))
.chain(
words
.sheets
.iter()
.map(|sheet| (18.0, sheet.heading.to_owned())),
)
.chain((0..words.sheets.len()).map(|index| (9.0, words.page_number(index))))
.chain(std::iter::once((11.0, words.cross_reference.to_owned())))
.chain(words.steps.iter().map(|step| (11.0, (*step).to_owned())));
for (size, line) in lines {
let measured = text.measure(&line, size);
assert!(
measured <= PAPER,
"the {code} document draws {line:?} over {measured:.1} points, \
and the paper leaves {PAPER:.1}"
);
}
}
}
/// Every flag asks for a level of its own, and each is matched whole: a
/// flag read as a prefix of another would silently write one level under
/// the name of the next, and only a validator would ever notice.
#[test]
fn every_archival_flag_asks_for_the_level_it_names() {
let expected = [
("--archival", PdfA::A3A),
("--archival-2", PdfA::A2B),
("--archival-2u", PdfA::A2U),
("--archival-2a", PdfA::A2A),
("--archival-3u", PdfA::A3U),
("--archival-4", PdfA::A4),
];
assert_eq!(ARCHIVAL_LEVELS.len(), expected.len());
for (flag, level) in expected {
assert_eq!(
archival_level(&[flag.to_owned()]),
Some(level),
"{flag} asks for another level"
);
}
assert_eq!(archival_level(&["--ua".to_owned()]), None);
assert_eq!(archival_level(&[]), None);
}
/// The tree gets one element per paragraph and one per illustration, so a
/// language that runs two paragraphs into one, or leaves an illustration
/// undescribed, hands a reader a different document under the same name.
#[test]
fn every_language_builds_the_same_tree() {
fn shape(words: &Words) -> Vec<(usize, bool)> {
words
.sheets
.iter()
.map(|sheet| {
(
sheet.body.split('\n').count(),
sheet.figure.is_some_and(|alt| !alt.is_empty()),
)
})
.collect()
}
let (first, leading) = WORDS[0];
let expected = shape(leading);
for (named, words) in WORDS {
assert_eq!(
shape(words),
expected,
"the {} document holds paragraphs or figures the {} one does not",
named.code(),
first.code()
);
}
}
}
|