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 | //! Stamps a page with what it is: DRAFT, COPY, PAID.
//!
//! A stamp is scaled to the box it is given rather than set in a size, so the
//! same line stamps a whole page, a table cell and a form field without a
//! single size being worked out by the caller. It is turned first and scaled
//! second, so that what stays inside the box is the line as the reader sees it.
//! The page below draws four of them across boxes of very different shapes.
//!
//! The stamp takes whatever colour it is given and nothing else: it is text,
//! and a caller that wants it under the page's own ink draws it first.
//!
//! The stamped words, the title and the four captions are held in `Words`, once
//! per language, and `HQF_PDF_LANG` picks which set is drawn. Each set keeps
//! its last word the longest, because the last box is the one that shows a long
//! word coming out smaller rather than cut.
//!
//! Usage: `cargo run --example write_stamp -- tmp/stamp.pdf`
//! `HQF_PDF_LANG=fr cargo run --example write_stamp -- tmp/tampon.pdf`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
use hqf_pdf::layout::{Slant, Stamp};
use hqf_pdf::{Document, Font, FontHandle, Page, Rgb};
#[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.
fn default_font() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fonts")
.join("DejaVuSans.ttf")
}
/// The margin the boxes are laid out inside.
const MARGIN: f64 = 56.0;
/// How many boxes the page stamps.
const BOX_COUNT: usize = 4;
/// One box to stamp: where it is, how big, and which way the line runs.
struct Stamped {
x: f64,
y: f64,
width: f64,
height: f64,
slant: Slant,
color: Rgb,
}
/// The boxes, from the top of the page down.
const STAMPED: [Stamped; BOX_COUNT] = [
Stamped {
x: MARGIN,
y: 606.0,
width: 483.0,
height: 150.0,
slant: Slant::Up,
color: Rgb::new(0.75, 0.16, 0.16),
},
Stamped {
x: MARGIN,
y: 300.0,
width: 150.0,
height: 260.0,
slant: Slant::Down,
color: Rgb::new(0.25, 0.35, 0.7),
},
Stamped {
x: 268.0,
y: 300.0,
width: 271.0,
height: 260.0,
slant: Slant::Flat,
color: Rgb::new(0.1, 0.45, 0.25),
},
Stamped {
x: MARGIN,
y: 130.0,
width: 483.0,
height: 110.0,
slant: Slant::Up,
color: Rgb::new(0.45, 0.45, 0.45),
},
];
/// The words the page is written in, one set per language.
///
/// The stamped words are language: a reader stamps a document in the language
/// they read it in. Each set keeps its last word the longest, because the last
/// box is the one that shows a long word coming out smaller rather than cut.
#[derive(Debug)]
struct Words {
/// The line at the head of the page.
title: &'static str,
/// What each box is stamped with, in the order `STAMPED` lays them out.
stamps: [&'static str; BOX_COUNT],
/// What is written under each box.
captions: [&'static str; BOX_COUNT],
}
impl Words {
/// The words the page is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
}
/// The page in English.
const ENGLISH: Words = Words {
title: "One stamp, four boxes",
stamps: ["DRAFT", "COPY", "PAID", "CANCELLED"],
captions: [
"A wide box: the line runs up its diagonal, which is nearly flat.",
"A tall box, stamped the other way.",
"Flat across the middle, and wide enough that the width is what binds.",
"A longer word in a shorter box: the size follows, nothing is cut.",
],
};
/// The page in French.
const FRENCH: Words = Words {
title: "Un tampon, quatre boîtes",
stamps: ["BROUILLON", "COPIE", "PAYÉ", "ANNULATION"],
captions: [
"Une boîte large : le mot monte le long de sa diagonale, presque à plat.",
"Une boîte haute, tamponnée dans l'autre sens.",
"À plat au milieu, et assez large pour que ce soit la largeur qui limite.",
"Un mot plus long dans une boîte plus basse : la taille suit, rien n'est coupé.",
],
};
/// 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)];
/// Draws the outline of a box, so that what the stamp was fitted to can be
/// seen.
fn outline(content: &mut Content, stamped: &Stamped) -> Result<(), hqf_pdf::Error> {
content.save_state();
content.set_stroke(Rgb::gray(0.8))?;
content.set_line_width(0.5)?;
content.rect(stamped.x, stamped.y, stamped.width, stamped.height)?;
content.stroke();
content.restore_state();
Ok(())
}
/// Writes the caption under a box.
fn caption(
content: &mut Content,
font: &FontHandle,
stamped: &Stamped,
text: &str,
) -> Result<(), hqf_pdf::Error> {
content.save_state();
content.set_fill(Rgb::gray(0.35))?;
content.begin_text();
content.set_font(font.name(), 8.5)?;
content.text_origin(stamped.x, stamped.y - 14.0)?;
content.show_glyphs(&font.glyphs(text));
content.end_text();
content.restore_state();
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let language = Language::from_environment()?;
let words = Words::of(language);
// 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 = env::args()
.nth(1)
.unwrap_or_else(|| language.file_name(&out::default_path("stamp")));
let mut doc = Document::new();
doc.set_license(licence::licensed());
let font = doc.add_font(Font::parse(fs::read(default_font())?)?);
let mut content = Content::new();
content.begin_text();
content.set_font(font.name(), 14.0)?;
content.text_origin(MARGIN, 780.0)?;
content.show_glyphs(&font.glyphs(words.title));
content.end_text();
for ((stamped, text), caption_text) in STAMPED.iter().zip(words.stamps).zip(words.captions) {
outline(&mut content, stamped)?;
Stamp::new(&font, text)
.slant(stamped.slant)
.color(stamped.color)
.draw(
&mut content,
stamped.x,
stamped.y,
stamped.width,
stamped.height,
)?;
caption(&mut content, &font, stamped, caption_text)?;
}
let mut page = Page::a4();
page.content = content.into_bytes();
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, {} stamps",
bytes.len(),
STAMPED.len()
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::{WORDS, language};
/// The lines two languages are allowed to write the same way. There are
/// none: every word on the page is a word of the language it is in.
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 page says these in more than one language: {untranslated:?}"
);
}
/// The last box is shorter than the first and takes the longest word: that
/// is what its caption points at, so a language that lost the ordering
/// would draw a caption its own page disproves.
#[test]
fn every_language_stamps_its_longest_word_in_the_last_box() {
for (language, words) in WORDS {
let longest = words
.stamps
.iter()
.map(|stamp| stamp.chars().count())
.max()
.expect("a page stamps at least one box");
assert_eq!(
words.stamps[words.stamps.len() - 1].chars().count(),
longest,
"in {} the last box does not take the longest word",
language.code()
);
}
}
}
|