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 | //! 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};
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;
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 de la boîte que le moteur a rendue, sans \
aucune mesure à la main. Les mêmes boîtes 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",
};
/// 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)];
fn main() -> 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(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; 2] = [
// 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""#,
];
#[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:?}"
);
}
}
|