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 | //! Shrinks headings to a fixed column, and turns a label up the page's spine.
//!
//! Two things the layout does so the caller does not have to measure anything.
//! A heading of unknown length is set with [`FontHandle::fit`], which gives the
//! largest size at which it still fits its column: three headings of very
//! different lengths come out at three sizes, each filling the same width. And
//! a label is turned a quarter turn with [`Content::rotate`], which sets a
//! block of text on its side about a pivot the caller names — here, up the left
//! margin like the title on the spine of a book.
//!
//! The headings and the spine label are held in `Words`, once per language, and
//! `HQF_PDF_LANG` picks which set is drawn. Each set keeps three headings of
//! very different lengths, which is what the page is about; the name of the
//! firm on the spine is the same in every language.
//!
//! Usage: `cargo run --example write_shrink_and_turn -- tmp/shrink_and_turn.pdf
//! [font.ttf]`
//! `HQF_PDF_LANG=fr cargo run --example write_shrink_and_turn --
//! tmp/reduire_et_tourner.pdf`
use std::env;
use std::f64::consts::FRAC_PI_2;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
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 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")
}
const PAGE_WIDTH: f64 = 595.276;
const MARGIN: f64 = 64.0;
/// The column the headings are fitted into, well inside the spine label.
const COLUMN_LEFT: f64 = 120.0;
const COLUMN_WIDTH: f64 = PAGE_WIDTH - COLUMN_LEFT - MARGIN;
/// How many headings the column is filled with.
const HEADING_COUNT: usize = 3;
/// The firm the statement is drawn up for, which keeps its name in every
/// language.
const FIRM: &str = "ACME LTD";
/// The words the page is written in, one set per language.
///
/// What is not language stays out of it: the firm keeps its name. Each set
/// keeps three headings of very different lengths, because what the page shows
/// is three lengths coming out at three sizes on one right edge.
#[derive(Debug)]
struct Words {
/// The headings, longest to shortest: each is set as large as it can be and
/// still fill the column, so all three end at the same right edge.
headings: [&'static str; HEADING_COUNT],
/// What the spine says after the firm's name.
spine: &'static str,
}
impl Words {
/// The words the page is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
/// The label turned up the left margin, the firm's name in front of it.
fn spine_label(&self) -> String {
format!("{FIRM} — {}", self.spine)
}
}
/// The page in English.
const ENGLISH: Words = Words {
headings: [
"Consolidated statement of professional services rendered",
"Summary of accounts",
"Total due",
],
spine: "ANNUAL STATEMENT",
};
/// The page in French.
const FRENCH: Words = Words {
headings: [
"Relevé consolidé des prestations de services réalisées",
"Récapitulatif des comptes",
"Total à payer",
],
spine: "RELEVÉ ANNUEL",
};
/// 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)];
/// Sets a heading as large as it fits the column, its right edge on the
/// column's.
fn heading(
content: &mut Content,
font: &FontHandle,
text: &str,
baseline: f64,
) -> Result<(), hqf_pdf::Error> {
// The size at which the heading fills the column, never larger than 30pt.
let size = font.fit(text, COLUMN_WIDTH, 30.0);
let right = COLUMN_LEFT + COLUMN_WIDTH;
content.begin_text();
content.set_font(font.name(), size)?;
content.text_origin(right - font.measure(text, size), baseline)?;
content.show_glyphs(&font.glyphs(text));
content.end_text();
Ok(())
}
/// Turns a label a quarter turn up the left margin, about its own start.
fn spine_label(content: &mut Content, font: &FontHandle, text: &str) -> Result<(), hqf_pdf::Error> {
let pivot_x = MARGIN;
let pivot_y = MARGIN;
content.save_state();
content.set_fill(Rgb::gray(0.55))?;
// Turned a quarter turn about the pivot, the label is then drawn as if it
// ran along the bottom from there: the turn carries it up the left margin.
content.rotate(FRAC_PI_2, pivot_x, pivot_y)?;
content.begin_text();
content.set_font(font.name(), 12.0)?;
content.text_origin(pivot_x, pivot_y)?;
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);
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("shrink_and_turn")));
let font_path = args.next().map_or_else(default_font, PathBuf::from);
let mut doc = Document::new();
doc.set_license(licence::licensed());
let font = doc.add_font(Font::parse(fs::read(&font_path)?)?);
let mut content = Content::new();
// A hairline down the right edge of the column, so the shared right edge of
// the fitted headings can be seen.
let right = COLUMN_LEFT + COLUMN_WIDTH;
content.save_state();
content.set_stroke(Rgb::gray(0.85))?;
content.set_line_width(0.5)?;
content.move_to(right, 760.0)?;
content.line_to(right, 560.0)?;
content.stroke();
content.restore_state();
let mut baseline = 740.0;
for text in words.headings {
heading(&mut content, &font, text, baseline)?;
baseline -= 56.0;
}
spine_label(&mut content, &font, &words.spine_label())?;
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, {} headings fitted",
bytes.len(),
words.headings.len()
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::{WORDS, language};
/// The lines two languages are allowed to write the same way. There are
/// none: the firm's name is held outside the words.
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:?}"
);
}
}
|