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 | //! Sets the same two paragraphs six ways, one shape to a block: indents, the
//! room around a paragraph, the alignment of its last line, and where its first
//! baseline sits.
//!
//! The paragraphs and the heading over each block are written in the language
//! `HQF_PDF_LANG` names: what a shape does to a paragraph is only visible on a
//! paragraph somebody can read.
//!
//! Usage: `cargo run --example write_paragraphs -- tmp/paragraphs.pdf
//! [font.ttf]`
//! `HQF_PDF_LANG=fr cargo run --example write_paragraphs`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
use hqf_pdf::layout::{Align, FirstBaseline, LastBaseline};
use hqf_pdf::{Document, Font, FontHandle, 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: the
/// one committed for the tests, so that the example runs on any machine.
fn default_font() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fonts")
.join("DejaVuSans.ttf")
}
/// The width of a block, and the gap between the two columns of them.
const COLUMN: f64 = 225.0;
/// The left edge of the two columns.
const LEFT: [f64; 2] = [60.0, 310.0];
/// The top of the first block of each column.
const TOP: f64 = 780.0;
/// How far apart the blocks of a column are stacked.
const STEP: f64 = 190.0;
/// The body size every block is set at, and the leading it is set on.
const SIZE: f64 = 10.0;
const LEADING: f64 = 13.0;
/// The size a block's heading is set at, and the leading it is set on.
const HEADING: f64 = 7.5;
const HEADING_LEADING: f64 = 9.5;
/// How far under its heading the text of a block starts.
const HEADING_GAP: f64 = 8.0;
/// The words the page is written in, one set per language.
#[derive(Debug)]
struct Words {
/// Two paragraphs, so that every block shows both what happens inside one
/// and what happens between two.
text: &'static str,
/// What each block is headed by, in the order they are set.
headings: [&'static str; 6],
}
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 {
text: "The deposit is held on the account named overleaf and is returned \
within thirty days of the keys being handed back.\nDeductions are itemised in \
writing, and anything not itemised is not deducted.",
headings: [
"Nothing set: the shape a flow has by default.",
"First line indented; the paragraphs are told apart by that alone.",
"First line hung out to the left of the block.",
"Room left above and below each paragraph.",
"Justified, with the last line of each paragraph centred.",
"First baseline on the top of the capitals, last line clear of its \
descenders.",
],
};
/// The page in French.
const FRENCH: Words = Words {
text: "Le dépôt de garantie est conservé sur le compte indiqué au verso et \
restitué dans les trente jours qui suivent la remise des clés.\nToute retenue \
est détaillée par écrit, et ce qui n'est pas détaillé n'est pas retenu.",
headings: [
"Rien de réglé : la forme qu'un bloc prend par défaut.",
"Première ligne en retrait ; c'est le seul signe qui sépare les \
paragraphes.",
"Première ligne sortie à gauche du bloc.",
"De l'air laissé au-dessus et au-dessous de chaque paragraphe.",
"Justifié, la dernière ligne de chaque paragraphe centrée.",
"Première ligne de base sur le haut des capitales, dernière ligne \
dégagée de ses jambages.",
],
};
/// 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 six blocks: what each is headed by, and the shape it sets the two
/// paragraphs in.
fn blocks<'a>(handle: &'a FontHandle, words: &'static Words) -> [(&'static str, TextFlow<'a>); 6] {
let plain = TextFlow::new(handle, SIZE).leading(LEADING);
[
(words.headings[0], plain.clone()),
(words.headings[1], plain.clone().first_line_indent(18.0)),
(
words.headings[2],
plain.clone().left_indent(24.0).first_line_indent(-24.0),
),
(
words.headings[3],
plain.clone().space_before(4.0).space_after(8.0),
),
(
words.headings[4],
plain
.clone()
.align(Align::Justify)
.last_align(Align::Center)
.right_indent(12.0),
),
(
words.headings[5],
plain
.clone()
.first_baseline(FirstBaseline::CapHeight)
.last_baseline(LastBaseline::Descender),
),
]
}
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("paragraphs")));
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();
for (index, (label, flow)) in blocks(&handle, words).iter().enumerate() {
let x = LEFT[index / 3];
let top = STEP.mul_add(-count(index % 3), TOP);
block(&mut c, &handle, flow, x, top, label, words.text)?;
}
let mut page = Page::a4();
page.content = c.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", bytes.len());
Ok(())
}
/// Converts a block count to a float. Six blocks never approach the point where
/// a `usize` loses precision as an `f64`.
#[expect(
clippy::cast_precision_loss,
reason = "a block index is far below f64's exact-integer range"
)]
const fn count(n: usize) -> f64 {
n as f64
}
/// Sets one block: its heading, the text under it, and the box the flow says it
/// fills.
fn block(
c: &mut Content,
handle: &FontHandle,
flow: &TextFlow<'_>,
x: f64,
top: f64,
label: &str,
text: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let heading = TextFlow::new(handle, HEADING)
.leading(HEADING_LEADING)
.color(Rgb::gray(0.35));
let heading_lines = heading.break_lines(label, COLUMN);
c.begin_text();
heading.draw(c, &heading_lines, x, top, COLUMN)?;
c.end_text();
let text_top = top - heading.height(&heading_lines) - HEADING_GAP;
let lines = flow.break_lines(text, COLUMN);
c.begin_text();
flow.draw(c, &lines, x, text_top, COLUMN)?;
c.end_text();
// The box the flow reports filling, so that the room it keeps around a
// paragraph is visible and not merely stated.
let height = flow.height(&lines);
c.save_state();
c.set_stroke_rgb(0.8, 0.8, 0.8)?
.set_line_width(0.5)?
.rect(x, text_top - height, COLUMN, height)?
.stroke();
c.restore_state();
Ok(())
}
#[cfg(test)]
mod tests {
use hqf_pdf::{Document, Font, Rgb, TextFlow};
use super::{
COLUMN, HEADING, HEADING_GAP, HEADING_LEADING, STEP, WORDS, blocks, default_font, language,
};
/// The lines two languages are allowed to write the same way. There are
/// none: the two paragraphs and all six headings are words a person reads.
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 blocks are stacked a fixed step apart, and nothing stops a block
/// from running into the one below it: a language whose paragraphs take one
/// line more would set the two on top of each other, and the page would
/// show what a shape does to a paragraph over the top of another paragraph.
#[test]
fn no_block_reaches_the_one_stacked_under_it() {
let mut doc = Document::new();
let handle = doc.add_font(
Font::parse(std::fs::read(default_font()).expect("the committed font is there"))
.expect("the committed font parses"),
);
for (named, words) in WORDS {
let heading = TextFlow::new(&handle, HEADING)
.leading(HEADING_LEADING)
.color(Rgb::gray(0.35));
for (label, flow) in blocks(&handle, words) {
let heading_lines = heading.break_lines(label, COLUMN);
let lines = flow.break_lines(words.text, COLUMN);
let height = heading.height(&heading_lines) + HEADING_GAP + flow.height(&lines);
assert!(
height <= STEP,
"the {} block headed {label:?} runs {height:.1} points, \
and the blocks are stacked {STEP:.1} apart",
named.code()
);
}
}
}
}
|