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 whose pages are numbered by their labels, not by their
//! position.
//!
//! Whether the labels work is not a question the bytes can answer: it is
//! answered by the page box of reading software, which is the one place they
//! show. So every page here says what reading software ought to be showing for
//! it — open the file and the two either agree or they do not.
//!
//! What each page says is written in the language `HQF_PDF_LANG` names. The
//! labels themselves are not: `i`, `1` and `A-1` are what reading software
//! shows in its page box, they come from the numbering style and the prefix the
//! file writes, and a page that translated them would no longer say what the
//! file asks for.
//!
//! Six runs are labelled by name: roman numerals in small and in capital
//! letters, the capitals carrying on the count at four; arabic figures, alone
//! and behind a prefix; letters, capital and small, the small ones behind a
//! prefix. What each page shows is worked out from its label. The back cover is
//! labelled by its position, with its prefix and no number, and names the
//! sheets the plates were put under, read back by their name.
//!
//! The file opens with the thumbnails shown, each under its label, two pages at
//! a time with the odd ones on the left.
//!
//! Usage: `cargo run --example write_page_labels -- tmp/labels.pdf [font.ttf]`
//! `cargo run --example write_page_labels -- tmp/labels.pdf --archival`
//! `HQF_PDF_LANG=fr cargo run --example write_page_labels`
use std::env;
use std::fs;
use std::num::NonZeroU32;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
use hqf_pdf::cos::Name;
use hqf_pdf::metadata::xmp::{Metadata, PdfA};
use hqf_pdf::viewer::{OpenMode, PageLayout};
use hqf_pdf::{Document, Font, LabelStyle, License, Page, PageLabel};
#[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")
}
/// How far in from the left edge of the sheet every line is set, in points.
const LEFT: f64 = 72.0;
/// How many sheets the document runs to, which each page says of itself.
const SHEETS: usize = 14;
/// The size each line of a page is set at, in points, in the order they are
/// drawn.
const SIZES: [f64; 5] = [12.0, 9.0, 9.0, 14.0, 20.0];
/// Where the baseline of each line sits, in points up from the foot of the
/// sheet, in the order they are drawn.
const BASELINES: [f64; 5] = [772.0, 752.0, 738.0, 700.0, 640.0];
/// The words each page is written in, one set per language.
///
/// What reading software shows in its page box is not among them: `i`, `1` and
/// `A-1` come from the numbering style and the prefix the file writes, and the
/// page carries them so that the two can be read against each other.
#[derive(Debug)]
struct Words {
/// What the document is called, in what the file says of itself.
title: &'static str,
/// The three lines at the head of every page, which say what page labels
/// are and where they show.
head: [&'static str; 3],
/// What stands before a sheet's number.
sheet: &'static str,
/// What stands between a sheet's number and the count of them.
of: &'static str,
/// What stands before the label reading software ought to be showing.
shows_as: &'static str,
/// The label of the back cover, which is its prefix alone.
back_cover: &'static str,
/// The line the back cover adds, naming the sheets the plates are put
/// under; `{sheets}` stands for them.
plates_at: &'static str,
}
impl Words {
/// The words each page is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
}
/// The pages in English.
const ENGLISH: Words = Words {
title: "A document numbered by its labels",
head: [
"Page labels number the pages for the reader.",
"Roman numerals, arabic figures, letters, a prefix, or the prefix alone \
on the back cover.",
"The label shows in reading software's page box, not the sheet number below.",
],
sheet: "Sheet",
of: "of",
shows_as: "Reading software shows this page as:",
back_cover: "Cover",
plates_at: "The plates are sheets {sheets}.",
};
/// The pages in French.
const FRENCH: Words = Words {
title: "Un document numéroté par ses étiquettes",
head: [
"Les étiquettes de page numérotent les pages pour le lecteur.",
"Chiffres romains, arabes, lettres, un préfixe, ou le préfixe seul en \
quatrième de couverture.",
"C’est l’étiquette qui s’affiche dans la case de page du logiciel de lecture, \
pas le numéro ci-dessous.",
],
sheet: "Feuille",
of: "sur",
shows_as: "Dans le logiciel de lecture :",
back_cover: "Couverture",
plates_at: "Les planches sont les feuilles {sheets}.",
};
/// The pages in Dutch.
const DUTCH: Words = Words {
title: "Een document genummerd door zijn labels",
head: [
"Paginalabels nummeren de pagina’s voor de lezer.",
"Romeinse cijfers, Arabische cijfers, letters, een voorvoegsel of alleen het voorvoegsel achterop.",
"Het label staat in het paginavak van de leessoftware, niet het bladnummer hieronder.",
],
sheet: "Blad",
of: "van",
shows_as: "Leessoftware toont deze pagina als:",
back_cover: "Omslag",
plates_at: "De platen zijn de bladen {sheets}.",
};
/// The pages in Portuguese.
const PORTUGUESE: Words = Words {
title: "Um documento numerado pelas suas etiquetas",
head: [
"As etiquetas de página numeram as páginas para o leitor.",
"Numeração romana, algarismos árabes, letras, um prefixo, ou só o prefixo na contracapa.",
"A etiqueta aparece na caixa de página do programa de leitura, não o número de folha abaixo.",
],
sheet: "Folha",
of: "de",
shows_as: "O programa de leitura mostra:",
back_cover: "Capa",
plates_at: "As estampas são as folhas {sheets}.",
};
/// The pages in Italian.
const ITALIAN: Words = Words {
title: "Un documento numerato dalle sue etichette",
head: [
"Le etichette di pagina numerano le pagine per il lettore.",
"Numeri romani, cifre arabe, lettere, un prefisso, o solo il prefisso sulla quarta di copertina.",
"La casella di pagina del programma di lettura mostra l’etichetta, non il numero di foglio qui sotto.",
],
sheet: "Foglio",
of: "di",
shows_as: "Il programma di lettura mostra:",
back_cover: "Copertina",
plates_at: "Le tavole sono i fogli {sheets}.",
};
/// The pages in Spanish.
const SPANISH: Words = Words {
title: "Un documento numerado por sus etiquetas",
head: [
"Las etiquetas de página numeran las páginas para el lector.",
"Números romanos, cifras arábigas, letras, un prefijo, o solo el prefijo en la contraportada.",
"La etiqueta sale en la casilla de página del programa de lectura, no el número de hoja de abajo.",
],
sheet: "Hoja",
of: "de",
shows_as: "El programa de lectura muestra:",
back_cover: "Portada",
plates_at: "Las láminas son las hojas {sheets}.",
};
/// The pages in German.
const GERMAN: Words = Words {
title: "Ein durch seine Seitenbeschriftungen nummeriertes Dokument",
head: [
"Seitenbeschriftungen nummerieren die Seiten für den Leser.",
"Römische und arabische Zahlen, Buchstaben, ein Präfix oder nur das Präfix auf der Rückseite.",
"Die Beschriftung steht im Seitenfeld der Lesesoftware, nicht die Blattnummer unten.",
],
sheet: "Blatt",
of: "von",
shows_as: "Lesesoftware zeigt diese Seite als:",
back_cover: "Umschlag",
plates_at: "Die Tafeln sind die Blätter {sheets}.",
};
/// 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),
];
/// A run of pages numbered by one label.
struct Run {
/// The name the run's pages are put under, which the label is addressed to.
name: &'static str,
/// How the run is numbered.
label: PageLabel,
/// How many pages the run holds.
pages: u32,
}
impl Run {
/// What reading software ought to show for each page of the run, worked out
/// from the label itself.
fn shown(&self) -> Vec<String> {
(0..self.pages)
.map(|offset| self.label.shown_at(offset))
.collect()
}
}
/// The document: a foreword in roman numerals, a body counting from one, plates
/// carrying on the roman count at four, appendices in capitals, notes in small
/// letters behind a prefix, and an annex behind another.
fn runs() -> Vec<Run> {
vec![
Run {
name: "foreword",
label: PageLabel::new(LabelStyle::LowerRoman),
pages: 2,
},
Run {
name: "body",
label: PageLabel::new(LabelStyle::Decimal),
pages: 3,
},
Run {
name: "plates",
label: PageLabel::new(LabelStyle::UpperRoman).start(NonZeroU32::MIN.saturating_add(3)),
pages: 2,
},
Run {
name: "appendices",
label: PageLabel::new(LabelStyle::UpperLetters),
pages: 2,
},
Run {
name: "notes",
label: PageLabel::new(LabelStyle::LowerLetters).prefix("n-"),
pages: 2,
},
Run {
name: "annex",
label: PageLabel::new(LabelStyle::Decimal).prefix("A-"),
pages: 2,
},
]
}
/// What one page says: the three lines that say what page labels are, which
/// sheet it is, and what reading software ought to be calling it.
fn lines(words: &Words, sheet: usize, shown: &str) -> [String; 5] {
let [first, second, third] = words.head;
[
first.to_owned(),
second.to_owned(),
third.to_owned(),
format!("{} {sheet} {} {SHEETS}", words.sheet, words.of),
format!("{} {shown}", words.shows_as),
]
}
/// A page that says which sheet it is and what reading software ought to call
/// it.
fn page(
font: &hqf_pdf::FontHandle,
words: &Words,
sheet: usize,
shown: &str,
note: Option<&str>,
) -> Result<Page, hqf_pdf::Error> {
let mut content = Content::new();
for ((line, size), top) in lines(words, sheet, shown).iter().zip(SIZES).zip(BASELINES) {
content.begin_text();
content.set_font(font, size)?;
content.text_origin(LEFT, top)?;
content.show_glyphs(&font.glyphs(line));
content.end_text();
}
if let Some(note) = note {
content.begin_text();
content.set_font(font, 12.0)?;
content.text_origin(LEFT, 600.0)?;
content.show_glyphs(&font.glyphs(note));
content.end_text();
}
let mut page = Page::a4();
page.content = content.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: Vec<String> = env::args().skip(1).collect();
let evaluation = args.iter().any(|arg| arg == "--evaluation");
// A labelled archival file, for a validator to be pointed at: the labels
// are a catalogue entry, and the catalogue is what PDF/A is strictest
// about.
let archival = args.iter().any(|arg| arg == "--archival");
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("page-labels")));
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::A3B);
let mut facts = Metadata::default();
facts.title = Some(words.title.to_owned());
facts.producer = Some("hqf-pdf".to_owned());
facts.created = Some("2026-07-16T09:30:00+02:00".to_owned());
doc.set_metadata(facts);
}
doc.set_info(Name::new("Title"), words.title);
let font = doc.add_font(Font::parse(fs::read(&font_path)?)?);
let mut sheet = 1;
for run in runs() {
let first = doc.page_count();
for shown in run.shown() {
doc.add_page(page(&font, words, sheet, &shown, None)?)?;
sheet += 1;
}
doc.name_pages(run.name, first..doc.page_count())?;
doc.label_named_pages(run.name, run.label)?;
}
// The back cover, labelled by its prefix alone, which spells no number; it
// names the sheets the plates were put under, read back by their name.
let plates = doc.named_pages("plates").unwrap_or_default();
let numbers: Vec<String> = plates.iter().map(|rank| (rank + 1).to_string()).collect();
let shown = format!(
"{}{}",
words.back_cover,
LabelStyle::PrefixOnly.spell(NonZeroU32::MIN)
);
let note = words.plates_at.replace("{sheets}", &numbers.join(", "));
doc.add_page(page(&font, words, sheet, &shown, Some(¬e))?)?;
doc.label_pages(
doc.page_count() - 1,
PageLabel::new(LabelStyle::PrefixOnly).prefix(words.back_cover),
);
doc.set_open_mode(OpenMode::Thumbnails);
doc.set_page_layout(PageLayout::TwoPagesOddLeft);
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::{LEFT, Run, SHEETS, SIZES, WORDS, default_font, language, lines, runs};
/// The width of the sheet, in points: A4, which every page is.
const SHEET_WIDTH: f64 = 595.276;
/// The room a line set from the left margin has before it reaches the
/// margin on the other side of the sheet.
const PAPER: f64 = SHEET_WIDTH - 2.0 * LEFT;
/// The lines two languages are allowed to write the same way, where two
/// languages spell a word alike. What reading software shows in its page
/// box is held outside the words.
const SPARED: [&str; 1] = [
// Written the same way in Portuguese.
r#"of: "de""#,
];
#[test]
fn every_language_writes_the_pages_in_its_own_words() {
let untranslated = language::untranslated_lines(&WORDS, &SPARED);
assert!(
untranslated.is_empty(),
"the pages say these in more than one language: {untranslated:?}"
);
}
/// Every page says which sheet of how many it is, and the count it gives is
/// the count the runs make: a run added or a page dropped would otherwise
/// leave seven written on eight sheets.
#[test]
fn the_count_every_page_gives_is_the_count_the_runs_make() {
let sheets: usize = runs().iter().map(|run| run.shown().len()).sum();
assert_eq!(
sheets + 1,
SHEETS,
"the pages say there are {SHEETS} of them"
);
}
/// Nothing on a page is broken to a width: a line longer than the paper
/// runs off the edge of it. The longest is measured on the last sheet,
/// whose label is the widest reading software is given.
#[test]
fn every_language_writes_lines_that_fit_the_room_they_have() {
let mut doc = Document::new();
let font = doc.add_font(
Font::parse(std::fs::read(default_font()).expect("the committed font is there"))
.expect("the committed font parses"),
);
for (named, words) in WORDS {
let mut shown: Vec<String> = runs().iter().flat_map(Run::shown).collect();
shown.push(words.back_cover.to_owned());
for (sheet, label) in (1..).zip(shown) {
for (line, size) in lines(words, sheet, &label).iter().zip(SIZES) {
let measured = font.measure(line, size);
assert!(
measured <= PAPER,
"the {} page draws {line:?} over {measured:.1} \
points, and it has {PAPER:.1}",
named.code()
);
}
}
}
}
}
|