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 | //! Lays an invoice on a letterhead somebody else made.
//!
//! The letterhead is a PDF: a page with a header block on it, and nothing else.
//! It is read, brought in as a form, drawn at the foot of the new page's stack,
//! and the invoice is set on top of it. Nothing about the letterhead is
//! rewritten — its fonts, its colours and its marks come across as they are.
//! Under the invoice, the page lists what the letterhead says, read back off
//! it: each run of text with where it begins and the size it is set at.
//!
//! The invoice's labels, and the one label the stand-in letterhead carries, are
//! held in `Words`, once per language, and `HQF_PDF_LANG` picks which set is
//! drawn. The leader dots are counted rather than typed, so a longer label in
//! another language still ends its amount on the same character.
//!
//! Usage: `cargo run --example write_overlay -- tmp/overlay.pdf
//! [letterhead.pdf]`
//! `HQF_PDF_LANG=fr cargo run --example write_overlay --
//! tmp/surcharge.pdf`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
use hqf_pdf::read::{Reader, TextRun};
use hqf_pdf::{Document, Font, Page};
#[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.
fn default_font() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fonts")
.join("DejaVuSans.ttf")
}
/// How many billed lines the invoice sets.
const ITEM_COUNT: usize = 3;
/// How many characters a billed line runs to: its label, a space, the leader
/// dots, a space, then its amount. The dots take whatever is left, so the
/// amounts end on the same character however long the label before them is.
const LINE_CHARS: usize = 56;
/// The invoice's number, and what each billed line comes to. Neither is
/// language.
const NUMBER: &str = "1964413";
const AMOUNTS: [&str; ITEM_COUNT] = ["4 250.00 EUR", "850.00 EUR", "5 100.00 EUR"];
/// The letterhead's own name, address and registration number. None of them is
/// language: they belong to whoever the letterhead is for.
const COMPANY: &str = "ACME Ltd";
const COMPANY_ADDRESS: &str = "12 Fleet Street — London EC4Y 1AA";
const COMPANY_NUMBER: &str = "123 456 789";
/// The size each of the five invoice lines is set at, in the order they are
/// laid down.
const SIZES: [f64; 5] = [16.0, 11.0, 11.0, 11.0, 12.0];
/// The words the invoice is written in, one set per language.
///
/// What is not language stays out of it: the invoice's number, the three
/// amounts, and the letterhead's name, address and number.
#[derive(Debug)]
struct Words {
/// What stands on the letterhead before its registration number.
company_number_label: &'static str,
/// What stands before the invoice's number.
number_label: &'static str,
/// The line that gives the day the invoice was issued.
issued: &'static str,
/// What each billed line is for, in the order they are set.
items: [&'static str; ITEM_COUNT],
/// What stands above the runs read back off the letterhead.
read_back: &'static str,
/// What stands for a run whose characters the letterhead's file does not
/// state.
unread: &'static str,
}
impl Words {
/// The words the invoice is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
/// The five lines the invoice sets, in order. Each billed line is padded
/// out with leader dots until it runs to `LINE_CHARS`.
fn lines(&self) -> Vec<String> {
let mut lines = vec![
format!("{} {NUMBER}", self.number_label),
self.issued.to_owned(),
];
lines.extend(self.items.iter().zip(AMOUNTS).map(|(item, amount)| {
let dots = LINE_CHARS.saturating_sub(item.chars().count() + amount.chars().count() + 2);
format!("{item} {} {amount}", ".".repeat(dots))
}));
lines
}
/// The second line of the stand-in letterhead: an address, then the number
/// the company registers under, under whatever this language calls it.
fn letterhead_line(&self) -> String {
format!(
"{COMPANY_ADDRESS} — {} {COMPANY_NUMBER}",
self.company_number_label
)
}
}
/// The invoice in English.
const ENGLISH: Words = Words {
company_number_label: "Company No.",
number_label: "Invoice No.",
issued: "Issued 14 July 2026",
items: ["Rendering engine development", "VAT at 20 %", "Total due"],
read_back: "What the letterhead says, read back off its page:",
unread: "(a run whose characters the file does not state)",
};
/// The invoice in French.
const FRENCH: Words = Words {
company_number_label: "N° d’entreprise",
number_label: "Facture n°",
issued: "Émise le 14 juillet 2026",
items: [
"Développement du moteur de rendu",
"TVA 20 %",
"Total à payer",
],
read_back: "Ce que dit le papier à en-tête, relu sur sa page\u{a0}:",
unread: "(un passage dont le fichier ne donne pas les caractères)",
};
/// The invoice in Dutch.
const DUTCH: Words = Words {
company_number_label: "KvK-nr.",
number_label: "Factuur nr.",
issued: "Uitgegeven op 14 juli 2026",
items: ["Ontwikkeling van de opmaakmotor", "Btw 20 %", "Te betalen"],
read_back: "Wat het briefpapier zegt, teruggelezen van de pagina:",
unread: "(een stuk tekst waarvan het bestand de tekens niet vermeldt)",
};
/// The invoice in Portuguese.
const PORTUGUESE: Words = Words {
company_number_label: "NIPC",
number_label: "Fatura n.º",
issued: "Emitida a 14 de julho de 2026",
items: [
"Desenvolvimento do motor de paginação",
"IVA a 20 %",
"Total a pagar",
],
read_back: "O que diz o papel timbrado, relido na sua página:",
unread: "(um trecho cujos caracteres o ficheiro não indica)",
};
/// The invoice in Italian.
const ITALIAN: Words = Words {
company_number_label: "N. REA",
number_label: "Fattura n.",
issued: "Emessa il 14 luglio 2026",
items: [
"Sviluppo del motore di impaginazione",
"IVA al 20 %",
"Totale dovuto",
],
read_back: "Ciò che dice la carta intestata, riletto sulla sua pagina:",
unread: "(un tratto di cui il file non indica i caratteri)",
};
/// The invoice in Spanish.
const SPANISH: Words = Words {
company_number_label: "N.º de empresa",
number_label: "Factura n.º",
issued: "Emitida el 14 de julio de 2026",
items: [
"Desarrollo del motor de maquetación",
"IVA al 20 %",
"Total a pagar",
],
read_back: "Lo que dice el membrete, leído de nuevo en su página:",
unread: "(un fragmento cuyos caracteres el archivo no indica)",
};
/// The invoice in German.
const GERMAN: Words = Words {
company_number_label: "Handelsregister Nr.",
number_label: "Rechnung Nr.",
issued: "Ausgestellt am 14. Juli 2026",
items: [
"Entwicklung der Rendering-Engine",
"MwSt. 20 %",
"Zu zahlen",
],
read_back: "Was der Briefkopf sagt, von seiner Seite zurückgelesen:",
unread: "(ein Abschnitt, dessen Zeichen die Datei nicht angibt)",
};
/// 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 letterhead, for when the caller has none to hand.
fn a_letterhead(
font_path: &Path,
words: &'static Words,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut doc = Document::new();
doc.set_license(licence::licensed());
let font = doc.add_font(Font::parse(fs::read(font_path)?)?);
let mut content = Content::new();
content.save_state();
content.set_fill_rgb(0.91, 0.94, 0.98)?;
content.rect(0.0, 742.0, 595.276, 100.0)?;
content.fill();
content.set_fill_rgb(0.2, 0.35, 0.6)?;
content.rect(0.0, 738.0, 595.276, 4.0)?;
content.fill();
content.restore_state();
content.begin_text();
content.set_font(&font, 22.0)?;
content.text_origin(56.0, 786.0)?;
content.show_glyphs(&font.glyphs(COMPANY));
content.end_text();
content.begin_text();
content.set_font(&font, 9.0)?;
content.text_origin(56.0, 764.0)?;
content.show_glyphs(&font.glyphs(&words.letterhead_line()));
content.end_text();
let mut page = Page::a4();
page.content = content.into_bytes();
doc.add_page(page)?;
Ok(doc.to_bytes()?)
}
/// One run read back off the letterhead, as the page lists it: its text, then
/// where it begins and the size it is set at, where the file says so.
fn run_line(run: &TextRun, words: &Words) -> String {
let text = run.text.as_deref().unwrap_or(words.unread);
match (run.origin, run.size) {
(Some((x, y)), Some(size)) => format!("{text} · {x:.0}, {y:.0} · {size:.0} pt"),
_ => text.to_owned(),
}
}
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 = env::args().skip(1);
// 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("overlay")));
let font_path = default_font();
// The letterhead: the caller's, or one made here to stand in for it.
let letterhead = match args.next() {
Some(given) => fs::read(given)?,
None => a_letterhead(&font_path, words)?,
};
let reader = Reader::new(letterhead)?;
println!("the letterhead has {} page(s)", reader.pages()?.len());
let mut doc = Document::new();
doc.set_license(licence::licensed());
// The page comes in as a form, with everything it drew and everything it
// named. It is the same page, and it is now a thing this document can draw.
let template = doc.import_page(&reader, 0)?;
println!(
"imported it as {}: {:.0} × {:.0} points",
String::from_utf8_lossy(template.name().as_bytes()),
template.width(),
template.height()
);
let font = doc.add_font(Font::parse(fs::read(&font_path)?)?);
let mut content = Content::new();
content.draw_form(template.name(), 0.0, 0.0, 1.0)?;
let mut top = 680.0;
for (line, size) in words.lines().iter().zip(SIZES) {
content.begin_text();
content.set_font(&font, size)?;
content.text_origin(56.0, top)?;
content.show_glyphs(&font.glyphs(line));
content.end_text();
top = size.mul_add(-2.0, top);
}
// What the letterhead says, read back off its page, under a line of its
// own: each run with where it begins and the size it is set at.
let said = reader.page_text(0)?;
let mut read_back = vec![(11.0, words.read_back.to_owned())];
read_back.extend(said.runs.iter().map(|run| (9.0, run_line(run, words))));
top -= 12.0;
for (size, line) in read_back {
content.begin_text();
content.set_font(&font, size)?;
content.text_origin(56.0, top)?;
content.show_glyphs(&font.glyphs(&line));
content.end_text();
println!("{line}");
top = size.mul_add(-1.6, top);
}
let mut page = Page::new(template.width(), template.height());
page.content = content.into_bytes();
template.add_to(&mut page);
doc.add_page(page)?;
let bytes = doc.to_bytes()?;
if let Some(parent) = Path::new(&path).parent() {
fs::create_dir_all(parent)?;
}
fs::write(&path, &bytes)?;
println!("wrote {path}: {} bytes", bytes.len());
Ok(())
}
#[cfg(test)]
mod tests {
use super::{ENGLISH, FRENCH, LINE_CHARS, WORDS, language};
/// The lines two languages are allowed to write the same way, where two
/// languages spell a word alike. The letterhead's name and address, the
/// invoice's number and the three amounts are all held outside the words.
const SPARED: [&str; 2] = [
// Written the same way in Italian.
r#""IVA al 20 %""#,
// Written the same way in Portuguese.
r#""Total a pagar""#,
];
#[test]
fn every_language_draws_the_page_in_its_own_words() {
let untranslated = language::untranslated_lines(&WORDS, &SPARED);
assert!(
untranslated.is_empty(),
"the invoice says these in more than one language: {untranslated:?}"
);
}
/// The leader dots are counted, not typed, so every billed line ends its
/// amount on the same character in every language.
#[test]
fn every_billed_line_runs_to_the_same_width() {
for words in [&ENGLISH, &FRENCH] {
for line in words.lines().iter().skip(2) {
assert_eq!(line.chars().count(), LINE_CHARS, "{line}");
}
}
}
}
|