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 | //! 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 a reader, which is the one place they show. So
//! every page here says what a reader 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 a reader 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.
//!
//! 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::path::{Path, PathBuf};
use hqf_pdf::content::Content;
use hqf_pdf::cos::Name;
use hqf_pdf::metadata::xmp::{Metadata, PdfA};
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;
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 = 7;
/// 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 a reader 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 a reader ought to be showing.
shows_as: &'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 for the front matter, arabic for the body, a prefix for \
the annex.",
"The label shows in a reader's page box, not the sheet number below.",
],
sheet: "Sheet",
of: "of",
shows_as: "A reader shows this page as:",
};
/// 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 pour les pages liminaires, arabes pour le corps, un \
préfixe pour l'annexe.",
"L'étiquette s'affiche dans la case du lecteur, pas le numéro de \
feuille ci-dessous.",
],
sheet: "Feuille",
of: "sur",
shows_as: "Un lecteur affiche cette page ainsi :",
};
/// 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)];
/// A run of pages numbered by one label.
struct Run {
/// The page the run starts on, counting from zero.
from: usize,
/// How the run is numbered.
label: PageLabel,
/// What a reader ought to show for each page of the run.
shown: &'static [&'static str],
}
/// The document: a foreword in roman numerals, a body counting from one, and an
/// annex behind a prefix.
fn runs() -> Vec<Run> {
vec![
Run {
from: 0,
label: PageLabel::new(LabelStyle::LowerRoman),
shown: &["i", "ii"],
},
Run {
from: 2,
label: PageLabel::new(LabelStyle::Decimal),
shown: &["1", "2", "3"],
},
Run {
from: 5,
label: PageLabel::new(LabelStyle::Decimal).prefix("A-"),
shown: &["A-1", "A-2"],
},
]
}
/// What one page says: the three lines that say what page labels are, which
/// sheet it is, and what a reader 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 a reader ought to call it.
fn page(
font: &hqf_pdf::FontHandle,
words: &Words,
sheet: usize,
shown: &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.name(), size)?;
content.text_origin(LEFT, top)?;
content.show_glyphs(&font.glyphs(line));
content.end_text();
}
let mut page = Page::a4();
page.content = content.into_bytes();
Ok(page)
}
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 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);
doc.set_metadata(Metadata {
title: Some(words.title.to_owned()),
producer: Some("hqf-pdf".to_owned()),
created: Some("2026-07-16T09: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)?)?);
let mut sheet = 1;
for run in runs() {
for shown in run.shown {
doc.add_page(page(&font, words, sheet, shown)?)?;
sheet += 1;
}
doc.label_pages(run.from, run.label);
}
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, 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. There are
/// none: what a reader shows in its page box is held outside the words.
const SPARED: [&str; 0] = [];
#[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, 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 a reader 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 sheet = 1;
for run in runs() {
for shown in run.shown {
for (line, size) in lines(words, sheet, shown).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()
);
}
sheet += 1;
}
}
}
}
}
|