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 | //! Hangs links and marks on text the engine has already placed, from the boxes
//! it reports rather than measured by hand.
//!
//! A flow and a table are drawn, and then asked where each line and each cell
//! landed. A pale band is painted behind a paragraph's first line, one line is
//! underlined and made clickable, and one table cell is boxed and made
//! clickable — every rectangle taken from `line_boxes` or `cell_box`, none of
//! it measured.
//!
//! The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks
//! which set is drawn. The addresses the two links go to, the product's name
//! and its version number stand the same in every language.
//!
//! Usage: `cargo run --example write_linked_text -- tmp/linked_text.pdf
//! [font.ttf]`
//! `HQF_PDF_LANG=fr cargo run --example write_linked_text --
//! tmp/texte_lie.pdf`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
use hqf_pdf::layout::{Cell, ColumnWidth, Columns, Padding, Row, Rule, Stroke, Table, TableFrame};
use hqf_pdf::{Document, Font, LinkTarget, Page, Rgb, TextFlow};
#[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")
}
/// The left edge every block shares.
const LEFT: f64 = 70.0;
/// The width every block is set to.
const WIDTH: f64 = 455.0;
/// Where the clickable line goes, and what it is drawn with after the words
/// that introduce it.
const CTA_URL: &str = "https://example.org/hqf-pdf/terms";
/// Where the boxed table cell goes.
const LICENCE_URL: &str = "https://example.org/hqf-pdf/licence";
/// What the table names in its first row of values, and the version under it.
const PRODUCT_NAME: &str = "hqf-pdf";
const VERSION_NUMBER: &str = "1.0";
/// The words the page is written in, one set per language.
///
/// What is not language stays out of it: the two addresses, the product's name
/// and its version number read the same wherever the page is read.
#[derive(Debug)]
struct Words {
/// The title set at the head of the page.
title: &'static str,
/// The paragraph whose first line is painted behind.
intro: &'static str,
/// What stands before the address, on the line that is underlined and made
/// clickable.
cta_before: &'static str,
/// The two column headings of the table.
field: &'static str,
value: &'static str,
/// What each row of the table is called.
product: &'static str,
licence: &'static str,
version: &'static str,
/// The value of the licence row, the cell the box makes clickable.
licence_value: &'static str,
}
impl Words {
/// The words the page is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
/// The line that is drawn, underlined and made clickable.
fn cta(&self) -> String {
format!("{}{CTA_URL}", self.cta_before)
}
}
/// The page in English.
const ENGLISH: Words = Words {
title: "Boxes over what the engine placed",
intro: "The engine knows where every line and every cell lands, down to the \
point. This paragraph was flowed into a column, and the pale band was painted \
behind its first line from the box the engine reported, not measured by hand. \
The same boxes make a line clickable and draw a rule exactly under it.",
cta_before: "Read the terms at ",
field: "Field",
value: "Value",
product: "Product",
licence: "Licence",
version: "Version",
licence_value: "See the licence online",
};
/// The page in French.
const FRENCH: Words = Words {
title: "Des cadres posés sur ce que le moteur a placé",
intro: "Le moteur sait où tombe chaque ligne et chaque cellule, au point près. Ce \
paragraphe a été coulé dans une colonne, et la bande pâle a été peinte \
derrière sa première ligne à partir du cadre que le moteur a indiqué, sans \
rien mesurer à la main. Les mêmes cadres rendent une ligne cliquable et \
tracent un filet exactement dessous.",
cta_before: "Lisez les conditions sur ",
field: "Champ",
value: "Valeur",
product: "Produit",
licence: "Licence",
version: "Version",
licence_value: "Voir la licence en ligne",
};
/// The page in Dutch.
const DUTCH: Words = Words {
title: "Kaders over wat de motor plaatste",
intro: "De motor weet waar elke regel en elke cel terechtkomt, tot op de punt. Deze alinea is in een kolom gegoten, en de lichte band is achter de eerste regel geschilderd vanuit het kader dat de motor teruggaf, niet met de hand gemeten. Dezelfde kaders maken een regel klikbaar en trekken er precies een lijn onder.",
cta_before: "Lees de voorwaarden op",
field: "Veld",
value: "Waarde",
product: "Product",
licence: "Licentie",
version: "Versie",
licence_value: "Bekijk de licentie online",
};
/// The page in Portuguese.
const PORTUGUESE: Words = Words {
title: "Caixas sobre o que o motor colocou",
intro: "O motor sabe onde cai cada linha e cada célula, ao ponto. Este parágrafo foi vertido numa coluna, e a faixa clara foi pintada por trás da primeira linha a partir da caixa que o motor devolveu, não medida à mão. As mesmas caixas tornam uma linha clicável e traçam um filete exatamente por baixo.",
cta_before: "Leia as condições em",
field: "Campo",
value: "Valor",
product: "Produto",
licence: "Licença",
version: "Versão",
licence_value: "Ver a licença online",
};
/// The page in Italian.
const ITALIAN: Words = Words {
title: "Riquadri su ciò che il motore ha collocato",
intro: "Il motore sa dove cade ogni riga e ogni cella, al punto tipografico. Questo paragrafo è stato versato in una colonna, e la fascia chiara è stata dipinta dietro la sua prima riga dal riquadro fornito dal motore, non misurato a mano. Gli stessi riquadri rendono cliccabile una riga e tracciano un filetto esattamente sotto.",
cta_before: "Leggete le condizioni su",
field: "Campo",
value: "Valore",
product: "Prodotto",
licence: "Licenza",
version: "Versione",
licence_value: "Vedere la licenza online",
};
/// The page in Spanish.
const SPANISH: Words = Words {
title: "Cajas sobre lo que colocó el motor",
intro: "El motor sabe dónde cae cada línea y cada celda, con precisión de un punto. Este párrafo se vertió en una columna, y la franja clara se pintó detrás de su primera línea con la caja que dio el motor, no medida a mano. Las mismas cajas hacen una línea clicable y trazan una raya justo debajo.",
cta_before: "Lea las condiciones en ",
field: "Campo",
value: "Valor",
product: "Producto",
licence: "Licencia",
version: "Versión",
licence_value: "Ver la licencia en línea",
};
/// The page in German.
const GERMAN: Words = Words {
title: "Rahmen über dem, was die Engine platziert hat",
intro: "Die Engine weiß auf den Punkt genau, wo jede Zeile und jede Zelle landet. Dieser Absatz wurde in eine Spalte gesetzt, und das blasse Band wurde hinter seine erste Zeile gemalt, nach dem Rahmen, den die Engine meldete, nicht von Hand gemessen. Dieselben Rahmen machen eine Zeile anklickbar und ziehen eine Linie genau darunter.",
cta_before: "Die Bedingungen stehen unter ",
field: "Feld",
value: "Wert",
product: "Produkt",
licence: "Lizenz",
version: "Version",
licence_value: "Die Lizenz online ansehen",
};
/// 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),
];
fn main() -> std::process::ExitCode {
failure::reported(run())
}
#[allow(clippy::expect_used, reason = "the licence value cell was placed")]
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 out = args
.next()
.unwrap_or_else(|| language.file_name(&out::default_path("linked_text")));
let font_path = args.next().map_or_else(default_font, PathBuf::from);
let font = Font::parse(fs::read(&font_path)?)?;
let mut doc = Document::new();
doc.set_license(licence::licensed());
let handle = doc.add_font(font);
let mut c = Content::new();
// The title.
let title = TextFlow::new(&handle, 18.0);
let title_lines = title.break_lines(words.title, WIDTH);
c.begin_text();
title.draw(&mut c, &title_lines, LEFT, 790.0, WIDTH)?;
c.end_text();
// A paragraph, with the box of its first line painted behind it.
let intro = TextFlow::new(&handle, 11.0).leading(15.0);
let intro_lines = intro.break_lines(words.intro, WIDTH);
let band = intro.line_boxes(&intro_lines, LEFT, 750.0, WIDTH)[0];
c.save_state();
c.set_fill(Rgb::new(1.0, 0.93, 0.6))?
.rect(band.x(), band.y(), band.width(), band.height())?
.fill();
c.restore_state();
c.begin_text();
intro.draw(&mut c, &intro_lines, LEFT, 750.0, WIDTH)?;
c.end_text();
// One line, drawn, then underlined and linked from its own box.
let cta = TextFlow::new(&handle, 12.0).color(Rgb::new(0.0, 0.2, 0.8));
let cta_lines = cta.break_lines(&words.cta(), WIDTH);
c.begin_text();
cta.draw(&mut c, &cta_lines, LEFT, 655.0, WIDTH)?;
c.end_text();
let link_box = cta.line_boxes(&cta_lines, LEFT, 655.0, WIDTH)[0];
c.save_state();
c.set_fill(Rgb::new(0.0, 0.2, 0.8))?
.rect(link_box.x(), link_box.y(), link_box.width(), 0.6)?
.fill();
c.restore_state();
// A table, then the box of one value cell drawn and linked.
let columns = Columns::new(
vec![ColumnWidth::Points(140.0), ColumnWidth::Points(315.0)],
WIDTH,
)?;
let pad = Padding::symmetric(6.0, 4.0);
let mut table = Table::new(columns);
table.header(1);
table.rule(Rule::Frame, Stroke::black(0.8));
table.rule(
Rule::HorizontalOther,
Stroke::new(0.25, Rgb::new(0.75, 0.75, 0.75)),
);
table.push(
Row::new()
.cell(Cell::new(&handle, 10.0, words.field).padding(pad))
.cell(Cell::new(&handle, 10.0, words.value).padding(pad)),
);
table.push(
Row::new()
.cell(Cell::new(&handle, 10.0, words.product).padding(pad))
.cell(Cell::new(&handle, 10.0, PRODUCT_NAME).padding(pad)),
);
table.push(
Row::new()
.cell(Cell::new(&handle, 10.0, words.licence).padding(pad))
.cell(Cell::new(&handle, 10.0, words.licence_value).padding(pad)),
);
table.push(
Row::new()
.cell(Cell::new(&handle, 10.0, words.version).padding(pad))
.cell(Cell::new(&handle, 10.0, VERSION_NUMBER).padding(pad)),
);
let placed = table.fit(TableFrame::new(LEFT, 610.0, 200.0), 0)?;
placed.draw(&mut c)?;
let cell = placed
.cell_box(2, 1)
.expect("the licence value cell was placed");
c.save_state();
c.set_stroke(Rgb::new(0.8, 0.0, 0.0))?
.set_line_width(1.0)?
.rect(cell.x(), cell.y(), cell.width(), cell.height())?
.stroke();
c.restore_state();
let mut page = Page::a4();
page.content = c.into_bytes();
page.links
.push(link_box.link(LinkTarget::Uri(CTA_URL.to_owned())));
page.links
.push(cell.link(LinkTarget::Uri(LICENCE_URL.to_owned())));
doc.add_page(page)?;
let bytes = doc.to_bytes()?;
if let Some(parent) = Path::new(&out).parent() {
fs::create_dir_all(parent)?;
}
fs::write(&out, &bytes)?;
println!("wrote {out}: {} bytes", bytes.len());
Ok(())
}
#[cfg(test)]
mod tests {
use super::{WORDS, language};
/// The lines two languages are allowed to write the same way.
const SPARED: [&str; 5] = [
// A permission to use is a licence in French as in English.
r#"licence: "Licence""#,
// So is the word over which release of a thing is described.
r#"version: "Version""#,
// Written the same way in Italian.
r#"field: "Campo""#,
// Written the same way in Portuguese.
r#"value: "Valor""#,
// Written the same way in Dutch.
r#"product: "Product""#,
];
#[test]
fn every_language_draws_the_page_in_its_own_words() {
let untranslated = language::untranslated_lines(&WORDS, &SPARED);
assert!(
untranslated.is_empty(),
"the page says these in more than one language: {untranslated:?}"
);
}
}
|