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
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494 | //! Writes a shelf card that marks its lines the three ways a text can be
//! marked, and sets its heading a second time behind itself.
//!
//! A rule under the words, a rule through the letters, a rule over the top: the
//! engine takes all three from the font rather than from a coordinate, so they
//! follow whatever size the text is set in. The heading and the mark across the
//! foot each carry a shadow — the same words set once more, offset and paler,
//! under the words themselves.
//!
//! Every word is held in `Words`, once per language, and `HQF_PDF_LANG` picks
//! which set is set. The prices are not words: they stand in one table, shared
//! by both languages.
//!
//! Usage: `cargo run --example write_text_ornaments -- tmp/ornaments.pdf
//! [font.ttf]`
//! `HQF_PDF_LANG=fr cargo run --example write_text_ornaments`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::color::Rgb;
use hqf_pdf::content::Content;
use hqf_pdf::cos::Name;
use hqf_pdf::layout::{Align, Shadow, Slant, Stamp};
use hqf_pdf::{Document, Font, FontHandle, Page, RichText, Style, 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: 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")
}
/// Where the card's text begins, in points from the left edge of the page.
const LEFT: f64 = 56.0;
/// How wide it runs.
const WIDTH: f64 = 483.0;
/// The size the heading is set in.
const TITLE_SIZE: f64 = 32.0;
/// The size the standfirst and the closing note are set in.
const NOTE_SIZE: f64 = 10.5;
/// The size a line of the shelf is set in.
const ROW_SIZE: f64 = 13.0;
/// The top of the heading's box.
const TITLE_TOP: f64 = 800.0;
/// The top of the standfirst.
const STANDFIRST_TOP: f64 = 748.0;
/// Where the line under the standfirst is drawn.
const HEAD_RULE: f64 = 730.0;
/// The top of the first line of the shelf.
const FIRST_ROW: f64 = 720.0;
/// How far apart the lines of the shelf sit.
const ROW_STEP: f64 = 26.0;
/// The top of the line that adds the three together.
const TOTAL_TOP: f64 = 626.0;
/// The top of the first line of the legend.
const LEGEND_TOP: f64 = 570.0;
/// How far apart the lines of the legend sit.
const LEGEND_STEP: f64 = 24.0;
/// How far into the box the legend's marked words are set.
const LEGEND_INDENT: f64 = 24.0;
/// The top of the closing note.
const NOTE_TOP: f64 = 470.0;
/// The box the mark is laid across: its lower-left corner and its size.
const MARK_BOX: [f64; 4] = [LEFT, 120.0, WIDTH, 250.0];
/// The words the card is written in, one set per language.
#[derive(Debug)]
struct Words {
/// What stands across the head of the card.
title: &'static str,
/// The line under it, which says what the card is showing.
standfirst: &'static str,
/// What each line of the shelf is called, in the order they are set.
goods: [&'static str; 3],
/// What stands against the sum of the three.
total_label: &'static str,
/// What opens the legend.
legend: &'static str,
/// The three marks, each named by words set the way the name says.
under: &'static str,
through: &'static str,
over: &'static str,
/// The note at the foot, which says where each mark comes from.
note: &'static str,
/// What the mark laid across the card reads.
mark: &'static str,
}
impl Words {
/// The words the card is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
/// The three marks, in the order the legend sets them.
const fn marks(&self) -> [&str; 3] {
[self.under, self.through, self.over]
}
}
/// The card in English.
const ENGLISH: Words = Words {
title: "The half-price shelf",
standfirst: "What a shop marks by hand, set by the engine instead.",
goods: [
"Atlas of the northern coast",
"Field guide to the estuary birds",
"The lighthouse keepers, a history",
],
total_label: "The three together",
legend: "The three marks, each set the way it names:",
under: "a rule under the words",
through: "a rule through the letters",
over: "a rule over the top",
note: "No rule on this card was placed by hand. The one under the words \
and the one through the letters sit where the face itself says they go, and \
the one over the top rides the ascender the face states; all three follow the \
size the text is set in. The heading carries a copy of itself behind it, \
offset and paler, which is what keeps large lettering readable over a \
photograph. The mark laid across the foot carries one too, turned with it, so \
the copy falls the same way on the sheet as it does under the heading.",
mark: "HALF PRICE",
};
/// The card in French.
const FRENCH: Words = Words {
title: "L'étagère à moitié prix",
standfirst: "Ce qu'un commerçant marque à la main, posé par le moteur.",
goods: [
"Atlas de la côte nord",
"Guide des oiseaux de l'estuaire",
"Les gardiens du phare, une histoire",
],
total_label: "Les trois ensemble",
legend: "Les trois marques, chacune posée comme elle se nomme :",
under: "un trait sous les mots",
through: "un trait au milieu des lettres",
over: "un trait au-dessus",
note: "Aucun trait de cette fiche n'a été placé à la main. Celui du \
dessous et celui du milieu sont là où le dessin de la lettre les met, et celui \
du haut suit l'ascendante que la police annonce ; tous trois suivent la taille \
du texte. Le titre porte une copie de lui-même derrière lui, décalée et plus \
pâle, ce qui garde un grand lettrage lisible sur une photographie. La mention \
posée en travers du pied en porte une aussi, tournée avec elle, si bien que la \
copie tombe sur la feuille du même côté que sous le titre.",
mark: "MOITIÉ PRIX",
};
/// Every language the card is written in.
static WORDS: [(Language, &Words); 2] =
[(Language::English, &ENGLISH), (Language::French, &FRENCH)];
/// What each line of the shelf was marked at and what it is marked at now.
///
/// Figures are not words: both languages set the same ones, and the engine
/// writes a decimal point in every language it is asked for.
const PRICES: [(&str, &str); 3] = [
("24.00 EUR", "12.00 EUR"),
("18.00 EUR", "9.00 EUR"),
("30.00 EUR", "15.00 EUR"),
];
/// What the three come to together.
const TOTAL: &str = "36.00 EUR";
/// The colour every shadow on the card is drawn in.
const fn shadow_ink() -> Rgb {
Rgb::gray(0.78)
}
/// Draws the heading, which sets itself once more behind itself.
fn heading(content: &mut Content, font: &FontHandle, words: &Words) -> Result<(), hqf_pdf::Error> {
let style = Style::new(font, TITLE_SIZE);
let text = RichText::new()
.shadow(Shadow::new(1.5, -1.5, shadow_ink()))
.push(words.title, &style);
let lines = text.break_lines(WIDTH);
text.draw(content, &lines, LEFT, TITLE_TOP, WIDTH)
}
/// Draws the standfirst and the line under it.
fn standfirst(
content: &mut Content,
font: &FontHandle,
words: &Words,
) -> Result<(), hqf_pdf::Error> {
let flow = TextFlow::new(font, NOTE_SIZE);
let lines = flow.break_lines(words.standfirst, WIDTH);
content.begin_text();
flow.draw(content, &lines, LEFT, STANDFIRST_TOP, WIDTH)?;
content.end_text();
content
.set_line_width(1.0)?
.move_to(LEFT, HEAD_RULE)?
.line_to(LEFT + WIDTH, HEAD_RULE)?
.stroke();
Ok(())
}
/// Draws one line of the shelf: what it is called, then what it was marked at
/// struck through and what it is marked at now.
fn shelf_line(
content: &mut Content,
font: &FontHandle,
name: &str,
prices: (&str, &str),
top: f64,
) -> Result<(), hqf_pdf::Error> {
let plain = Style::new(font, ROW_SIZE);
let struck = Style::new(font, ROW_SIZE).strikethrough();
let left = RichText::new().push(name, &plain);
let lines = left.break_lines(WIDTH);
left.draw(content, &lines, LEFT, top, WIDTH)?;
let right = RichText::new()
.align(Align::Right)
.push(prices.0, &struck)
.push(" ", &plain)
.push(prices.1, &plain);
let lines = right.break_lines(WIDTH);
right.draw(content, &lines, LEFT, top, WIDTH)
}
/// Draws the line that adds the three together, the sum ruled over the top the
/// way a column of figures is ruled before it is totalled.
fn total(content: &mut Content, font: &FontHandle, words: &Words) -> Result<(), hqf_pdf::Error> {
let plain = Style::new(font, ROW_SIZE);
let ruled = Style::new(font, ROW_SIZE).overline();
let left = RichText::new().push(words.total_label, &plain);
let lines = left.break_lines(WIDTH);
left.draw(content, &lines, LEFT, TOTAL_TOP, WIDTH)?;
let right = RichText::new().align(Align::Right).push(TOTAL, &ruled);
let lines = right.break_lines(WIDTH);
right.draw(content, &lines, LEFT, TOTAL_TOP, WIDTH)
}
/// Draws the legend: a line that says what follows, then the three marks, each
/// named by words set the way the name says.
fn legend(content: &mut Content, font: &FontHandle, words: &Words) -> Result<(), hqf_pdf::Error> {
let plain = Style::new(font, ROW_SIZE);
let opening = RichText::new().push(words.legend, &plain);
let lines = opening.break_lines(WIDTH);
opening.draw(content, &lines, LEFT, LEGEND_TOP, WIDTH)?;
let marked = [
Style::new(font, ROW_SIZE).underline(),
Style::new(font, ROW_SIZE).strikethrough(),
Style::new(font, ROW_SIZE).overline(),
];
// The tops step down one line at a time rather than being multiplied out,
// so the Python twin adds the same numbers in the same order.
let mut top = LEGEND_TOP;
for (name, style) in words.marks().into_iter().zip(marked) {
top -= LEGEND_STEP;
let line = RichText::new().push(name, &style);
let lines = line.break_lines(WIDTH - LEGEND_INDENT);
line.draw(
content,
&lines,
LEFT + LEGEND_INDENT,
top,
WIDTH - LEGEND_INDENT,
)?;
}
Ok(())
}
/// Draws the closing note, which says where each mark comes from.
fn note(content: &mut Content, font: &FontHandle, words: &Words) -> Result<(), hqf_pdf::Error> {
let flow = TextFlow::new(font, NOTE_SIZE).align(Align::Justify);
let lines = flow.break_lines(words.note, WIDTH);
content.begin_text();
flow.draw(content, &lines, LEFT, NOTE_TOP, WIDTH)?;
content.end_text();
Ok(())
}
/// Draws the mark laid across the foot of the card, which carries a shadow of
/// its own.
fn mark(content: &mut Content, font: &FontHandle, words: &Words) -> Result<(), hqf_pdf::Error> {
let [x, y, width, height] = MARK_BOX;
Stamp::new(font, words.mark)
.slant(Slant::Up)
.color(Rgb::new(0.72, 0.16, 0.16))
.shadow(Shadow::new(2.5, -2.5, shadow_ink()))
.draw(content, x, y, width, height)
}
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 out = args
.next()
.unwrap_or_else(|| language.file_name(&out::default_path("text_ornaments")));
let font_path = args.next().map_or_else(default_font, PathBuf::from);
let mut doc = Document::new();
doc.set_license(licence::licensed());
doc.set_info(Name::new("Title"), words.title);
let font = doc.add_font(Font::parse(fs::read(&font_path)?)?);
let mut content = Content::new();
heading(&mut content, &font, words)?;
standfirst(&mut content, &font, words)?;
// The tops step down one line at a time rather than being multiplied out,
// so the Python twin adds the same numbers in the same order.
let mut top = FIRST_ROW;
for (name, prices) in words.goods.into_iter().zip(PRICES) {
shelf_line(&mut content, &font, name, prices, top)?;
top -= ROW_STEP;
}
total(&mut content, &font, words)?;
legend(&mut content, &font, words)?;
note(&mut content, &font, words)?;
mark(&mut content, &font, words)?;
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", bytes.len());
Ok(())
}
#[cfg(test)]
mod tests {
use hqf_pdf::{Document, Font, RichText, Style, TextFlow};
use super::{
LEGEND_INDENT, NOTE_SIZE, PRICES, ROW_SIZE, TOTAL, WIDTH, WORDS, default_font, language,
};
/// The test font, added to a document so that a style can be built on it.
fn handle() -> (Document, hqf_pdf::FontHandle) {
let mut doc = Document::new();
let font = Font::parse(std::fs::read(default_font()).expect("the committed font is there"))
.expect("the committed font parses");
let handle = doc.add_font(font);
(doc, handle)
}
#[test]
fn every_language_writes_the_card_in_its_own_words() {
let untranslated = language::untranslated_lines(&WORDS, &[]);
assert!(
untranslated.is_empty(),
"the card says these in more than one language: {untranslated:?}"
);
}
/// A line of the shelf that wrapped would set its name over the prices
/// beside it, and the two would read as one.
#[test]
fn every_line_of_the_shelf_stands_on_a_line_of_its_own() {
let (_doc, handle) = handle();
let plain = Style::new(&handle, ROW_SIZE);
for (_, words) in WORDS {
for name in words.goods {
let text = RichText::new().push(name, &plain);
assert_eq!(
text.break_lines(WIDTH).len(),
1,
"{name:?} does not fit the card"
);
}
let label = RichText::new().push(words.total_label, &plain);
assert_eq!(label.break_lines(WIDTH).len(), 1);
}
}
/// The prices are set against the right edge, so a price wider than the box
/// would run off the left of it rather than wrap.
#[test]
fn the_prices_and_the_sum_fit_the_room_they_are_set_in() {
let (_doc, handle) = handle();
let plain = Style::new(&handle, ROW_SIZE);
for (was, now) in PRICES {
let text = RichText::new()
.push(was, &plain)
.push(" ", &plain)
.push(now, &plain);
assert_eq!(text.break_lines(WIDTH).len(), 1);
}
assert_eq!(
RichText::new().push(TOTAL, &plain).break_lines(WIDTH).len(),
1
);
}
/// Each of the three names is set in the mark it names, so a name that
/// wrapped would leave a rule hanging under a second line.
#[test]
fn every_name_of_a_mark_is_set_on_one_line() {
let (_doc, handle) = handle();
let styles = [
Style::new(&handle, ROW_SIZE).underline(),
Style::new(&handle, ROW_SIZE).strikethrough(),
Style::new(&handle, ROW_SIZE).overline(),
];
for (_, words) in WORDS {
for (name, style) in words.marks().into_iter().zip(styles.clone()) {
let text = RichText::new().push(name, &style);
assert_eq!(
text.break_lines(WIDTH - LEGEND_INDENT).len(),
1,
"{name:?} does not fit the legend"
);
}
}
}
/// The three marks stand one above the other at the size the card sets
/// them: the rule under the words below the baseline, the one through the
/// letters over it, and the one over the top above them both.
#[test]
fn the_three_marks_the_card_shows_are_three_different_heights() {
let (_doc, handle) = handle();
let under = handle.underline(ROW_SIZE);
let through = handle.strikethrough(ROW_SIZE);
let over = handle.overline(ROW_SIZE);
assert!(under.position < 0.0, "the rule under the words is under it");
assert!(
under.position < through.position,
"the rule through the letters stands over it"
);
assert!(
through.position < over.position,
"and the rule over the top over them both"
);
}
/// The closing note is the longest thing on the card, and it has the foot
/// of the sheet to fill; a note that ran past the mark would be set over
/// it.
#[test]
fn every_language_writes_a_note_that_stops_short_of_the_mark() {
let (_doc, handle) = handle();
let flow = TextFlow::new(&handle, NOTE_SIZE);
for (named, words) in WORDS {
let lines = flow.break_lines(words.note, WIDTH);
let bottom = super::NOTE_TOP - flow.height(&lines);
assert!(
bottom > super::MARK_BOX[1] + super::MARK_BOX[3],
"the {} note reaches {bottom}, under the top of the mark",
named.code()
);
}
}
}
|