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 | //! Writes a document whose pages link out of themselves and to each other.
//!
//! A link draws nothing, so nothing on the page proves it is there: only a
//! reader does, by following it. The first page carries a table of contents
//! whose lines lead to the pages after it, and an address that leaves the
//! document; each of the pages after it leads back.
//!
//! The chapter titles and the lines around them are held in `Words`, once per
//! language, and `HQF_PDF_LANG` picks which set is drawn. A chapter title is
//! written once and read twice: it is what the contents shows, and what the
//! navigation panel shows.
//!
//! Usage: `cargo run --example write_links -- tmp/links.pdf [font.ttf]`
//! `cargo run --example write_links -- tmp/links.pdf --archival`
//! `HQF_PDF_LANG=fr cargo run --example write_links -- tmp/liens.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::metadata::xmp::{Metadata, PdfA};
use hqf_pdf::{Bookmark, Document, Font, FontHandle, License, Link, LinkTarget, Page};
#[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 many chapters the contents lists, each on the page after it.
const CHAPTER_COUNT: usize = 3;
/// The address the contents leaves the document by. It is not language.
const ADDRESS: &str = "https://example.org/hqf-pdf";
/// The words the document is written in, one set per language.
///
/// A chapter title is written once and read twice: the contents draws it, and
/// the navigation panel carries it. Both read the same field, so the two can
/// never fall out of step.
#[derive(Debug)]
struct Words {
/// The line at the head of the contents, and the name its bookmark takes.
contents: &'static str,
/// The line that says which text on the page can be clicked.
what_is_clickable: &'static str,
/// The chapters the contents lists, in the order they are laid out.
chapters: [&'static str; CHAPTER_COUNT],
/// The line each chapter page leads back by.
back: &'static str,
/// The title the document carries in its information dictionary, and in its
/// metadata when it is written as an archival file.
document_title: &'static str,
}
impl Words {
/// The words the document is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
}
/// The document in English.
const ENGLISH: Words = Words {
contents: "Contents",
what_is_clickable: "Blue text is clickable; black text is not.",
chapters: ["What a link is", "What a link draws", "Where a link goes"],
back: "Back to the contents",
document_title: "A document that links to itself",
};
/// The document in French.
const FRENCH: Words = Words {
contents: "Sommaire",
what_is_clickable: "Le texte bleu est cliquable, le noir ne l'est pas.",
chapters: [
"Ce qu'est un lien",
"Ce qu'un lien dessine",
"Où mène un lien",
],
back: "Retour au sommaire",
document_title: "Un document qui renvoie à lui-même",
};
/// 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)];
/// The size the contents and the chapter titles are set at.
const SIZE: f64 = 14.0;
/// Sets one line, with its baseline at `(x, y)`.
fn draw(
content: &mut Content,
font: &FontHandle,
size: f64,
x: f64,
y: f64,
text: &str,
) -> Result<(), hqf_pdf::Error> {
content.begin_text();
content.set_font(font.name(), size)?;
content.text_origin(x, y)?;
content.show_glyphs(&font.glyphs(text));
content.end_text();
Ok(())
}
/// The contents page: one line per chapter, each clickable, and an address that
/// leaves the document.
fn contents_page(font: &FontHandle, words: &'static Words) -> Result<Page, hqf_pdf::Error> {
let mut content = Content::new();
let mut page = Page::a4();
draw(&mut content, font, 20.0, 72.0, 760.0, words.contents)?;
// A black line that is not a link, beside the blue lines that are.
draw(
&mut content,
font,
10.0,
72.0,
735.0,
words.what_is_clickable,
)?;
let mut top = 700.0;
for (index, chapter) in words.chapters.iter().enumerate() {
let line = format!("{}. {chapter}", index + 1);
content.set_fill_rgb(0.0, 0.2, 0.8)?;
draw(&mut content, font, SIZE, 72.0, top, &line)?;
content.set_fill_rgb(0.0, 0.0, 0.0)?;
page.links.push(Link::new(
72.0,
top - 3.0,
font.measure(&line, SIZE),
SIZE,
// Chapter one is the page after this one, which is index 1.
LinkTarget::Page(index + 1),
));
top = SIZE.mul_add(-2.0, top);
}
content.set_fill_rgb(0.0, 0.2, 0.8)?;
draw(&mut content, font, SIZE, 72.0, top - 20.0, ADDRESS)?;
content.set_fill_rgb(0.0, 0.0, 0.0)?;
page.links.push(Link::new(
72.0,
top - 23.0,
font.measure(ADDRESS, SIZE),
SIZE,
LinkTarget::Uri(ADDRESS.to_owned()),
));
page.content = content.into_bytes();
Ok(page)
}
/// A chapter page, with a line leading back to the contents.
fn chapter_page(
font: &FontHandle,
words: &'static Words,
index: usize,
) -> Result<Page, hqf_pdf::Error> {
let mut content = Content::new();
let mut page = Page::a4();
let title = format!("{}. {}", index + 1, words.chapters[index]);
draw(&mut content, font, 20.0, 72.0, 760.0, &title)?;
content.set_fill_rgb(0.0, 0.2, 0.8)?;
draw(&mut content, font, SIZE, 72.0, 700.0, words.back)?;
content.set_fill_rgb(0.0, 0.0, 0.0)?;
page.links.push(Link::new(
72.0,
697.0,
font.measure(words.back, SIZE),
SIZE,
LinkTarget::Page(0),
));
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 linked archival file, for a validator to be pointed at: PDF/A has rules
// of its own about what an annotation may be.
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("links")));
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.document_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.document_title);
let font = doc.add_font(Font::parse(fs::read(&font_path)?)?);
doc.add_page(contents_page(&font, words)?)?;
for index in 0..CHAPTER_COUNT {
doc.add_page(chapter_page(&font, words, index)?)?;
}
// The same contents again, as the panel a reader navigates by.
let mut contents = Bookmark::new(words.contents, 0).open();
for (index, chapter) in words.chapters.iter().enumerate() {
contents = contents.child(Bookmark::new(
format!("{}. {chapter}", index + 1),
index + 1,
));
}
doc.add_bookmark(contents);
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 super::{WORDS, language};
/// The lines two languages are allowed to write the same way. There are
/// none: the address the contents leaves by is held outside the words.
const SPARED: [&str; 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:?}"
);
}
}
|