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 | //! Sets one justified paragraph in a narrow column twice: above with no break
//! points inside its words, below breaking after a slash or a hyphen, so the
//! paths and compounds that ran past the edge can be seen wrapping in it.
//!
//! The text carries file paths and hyphenated compounds whose tokens are wider
//! than the column. The upper setting breaks only at spaces, so those tokens
//! run past the column's right edge; the lower one breaks after `/` and `-`,
//! keeping the character on the line, so the tokens wrap inside the column.
//!
//! The sentence and the two headings are held in `Words`, once per language,
//! and `HQF_PDF_LANG` picks which set is drawn. The two paths are addresses,
//! and stand the same in every language.
//!
//! Usage: `cargo run --example write_break_after -- tmp/break_after.pdf
//! [font.ttf]`
//! `HQF_PDF_LANG=fr cargo run --example write_break_after --
//! tmp/coupure_apres.pdf`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
use hqf_pdf::layout::Align;
use hqf_pdf::{Document, Font, Page, 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 two addresses the sentence carries, each wider than the column so that a
/// break after a slash can be seen falling inside them.
const RENDER_PATH: &str = "/api/v2/organizations/documents/render";
const PREVIEW_PATH: &str = "/api/v2/organizations/documents/preview";
/// The words the page is written in, one set per language.
///
/// What is not language stays out of it: the two paths are addresses a caller
/// types, and each set of words carries a hyphenated compound of its own so
/// that both settings have something to break in.
#[derive(Debug)]
struct Words {
/// The heading over the setting that breaks at spaces only.
plain: &'static str,
/// The heading over the setting that breaks after a slash and a hyphen.
breaking: &'static str,
/// The sentence, cut where the two paths go into it.
text_before: &'static str,
text_between: &'static str,
text_after: &'static str,
}
impl Words {
/// The words the page is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
/// The sentence both settings are broken to, with the paths in it.
fn text(&self) -> String {
format!(
"{}{RENDER_PATH}{}{PREVIEW_PATH}{}",
self.text_before, self.text_between, self.text_after
)
}
}
/// The page in English.
const ENGLISH: Words = Words {
plain: "Without break points: the paths run past the column's edge.",
breaking: "Breaking after slash and hyphen: the paths and compounds wrap.",
text_before: "Send each request to ",
text_between: ", then read the read-only copy from ",
text_after: " on the rate-limited tier.",
};
/// The page in French.
const FRENCH: Words = Words {
plain: "Sans point de coupure : les chemins débordent de la colonne.",
breaking: "Coupure après la barre oblique et le trait d'union : tout rentre.",
text_before: "Envoyez chaque requête à ",
text_between: ", puis relisez la copie en lecture seule depuis ",
text_after: " sur l'offre en libre-service.",
};
/// 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 column both settings are broken to, narrow enough that the paths reach
/// its edge.
const COLUMN: f64 = 150.0;
/// The left edge of both settings, stacked one above the other.
const X: f64 = 80.0;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let language = Language::from_environment()?;
let words = Words::of(language);
let text = words.text();
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("break_after")));
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();
// Both panels set the same text; only whether a slash or a hyphen is a
// break point differs between them.
for (top, label, break_after) in [(760.0, words.plain, ""), (600.0, words.breaking, "/-")] {
let heading = TextFlow::new(&handle, 8.0);
let heading_lines = heading.break_lines(label, 400.0);
c.begin_text();
heading.draw(&mut c, &heading_lines, X, top, 400.0)?;
c.end_text();
let text_top = top - 20.0;
let flow = TextFlow::new(&handle, 11.0)
.leading(15.0)
.align(Align::Justify)
.break_after(break_after);
let lines = flow.break_lines(&text, COLUMN);
c.begin_text();
flow.draw(&mut c, &lines, X, text_top, COLUMN)?;
c.end_text();
// The column's edge, so the lines can be read against it.
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();
}
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(())
}
#[cfg(test)]
mod tests {
use super::{WORDS, language};
/// The lines two languages are allowed to write the same way. There are
/// none: the two paths are 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:?}"
);
}
}
|