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 | //! 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 --ua`
//! `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::content::Rgb;
use hqf_pdf::cos::Name;
use hqf_pdf::layout::{Align, Cell, Columns, Padding, Row, Rule, Stroke, Table, TextFlow};
use hqf_pdf::metadata::xmp::{Metadata, PdfA, PdfUa};
use hqf_pdf::{
Artifact, Document, Font, FontHandle, License, Marks, Page, StructElement, StructureTree,
};
#[path = "shared/out.rs"]
mod out;
#[path = "shared/licence.rs"]
mod licence;
#[path = "shared/language.rs"]
mod language;
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 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;
/// 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],
/// 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 a reader tells furniture from what the document says.",
figure: None,
},
],
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 trait d'encre sur ses \
pages : celui-ci est un titre, celui-là un paragraphe, et le filet du \
haut est du mobilier qui ne dit rien du tout.\n\
Ce 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 dessinés, ils ne sont pas du contenu, et rien de ce \
qui lit le document à voix haute ne les dit.\n\
Un artefact ne porte aucun numéro, donc l'arbre ne peut pas le \
désigner. C'est tout ce qui permet de distinguer le mobilier de ce que \
dit le document.",
figure: None,
},
],
columns: ["Désignation", "Quantité", "Montant"],
parts: ["Roulement, 12 mm", "Arbre rectifié", "Carter en fonte"],
};
/// 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); 2] =
[(Language::English, &ENGLISH), (Language::French, &FRENCH)];
/// 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.name(), size)?;
content.text_origin(x, y)?;
content.show_glyphs(&font.glyphs(line));
content.end_text();
Ok(())
}
/// 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.
fn sheet(
font: &FontHandle,
index: usize,
words: &Words,
) -> 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(72.0, top - 20.0, 300.0, 0)?;
section = section.child(placement.draw_tagged(&mut content, index, &mut marks)?);
}
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));
}
// 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();
Ok((page, section))
}
fn main() -> 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: level A is the
// level that demands the structure tree, and it is the one claim in the
// packet that a file cannot make on its own.
let archival = args.iter().any(|arg| arg == "--archival");
// 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");
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 archival {
doc.set_conformance(PdfA::A3A);
}
if ua {
doc.set_ua_conformance(PdfUa::One);
}
if archival || ua {
doc.set_metadata(Metadata {
title: Some(words.title.to_owned()),
producer: Some("hqf-pdf".to_owned()),
created: Some("2026-07-21T09:30:00+02:00".to_owned()),
..Metadata::default()
});
}
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. A reader
// that follows the tree walks it in this order, whatever order the ink was
// laid down in.
let mut body = StructElement::new("Document");
for index in 0..words.sheets.len() {
let (page, section) = sheet(&font, index, words)?;
doc.add_page(page)?;
body = body.child(section);
}
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::{Document, Font};
use super::{WORDS, Words, default_font, language};
/// The lines two languages are allowed to write the same way. A page number
/// opens on `Page ` in both; nothing else on the two sheets is shared.
const SPARED: [&str; 1] = ["page_before: \"Page \""];
/// 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 and the page numbers 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))));
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}"
);
}
}
}
/// 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()
);
}
}
}
|