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 | //! Sets one justified paragraph in a narrow column twice: above without its
//! soft hyphens, below with them, so the breaks and the hyphens they draw can
//! be seen against the loose lines above.
//!
//! The text carries soft hyphens (U+00AD) inside its long words and a no-break
//! space (U+00A0) inside the figure "1 200 EUR". The upper setting is the same
//! text with the soft hyphens taken out, so only the breaking differs between
//! the two; the no-break space holds the figure together in both.
//!
//! The paragraph and the two headings are held in `Words`, once per language,
//! and `HQF_PDF_LANG` picks which set is drawn. Every language has to carry its
//! own soft hyphens, in words its own spelling allows to break: a paragraph
//! translated without them would show nothing.
//!
//! Usage: `cargo run --example write_soft_hyphen -- tmp/soft_hyphen.pdf
//! [font.ttf]`
//! `HQF_PDF_LANG=fr cargo run --example write_soft_hyphen --
//! tmp/cesures.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 words the page is written in, one set per language.
#[derive(Debug)]
struct Words {
/// What stands over the setting whose soft hyphens have been taken out.
without: &'static str,
/// What stands over the setting that keeps them.
with: &'static str,
/// The paragraph, with soft hyphens marking where its long words may break
/// and a no-break space inside the figure so it never falls across two
/// lines. Each language marks the breaks its own spelling allows.
hyphenated: &'static str,
}
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 {
without: "Without soft hyphens: justify opens the lines to fill the column.",
with: "With soft hyphens: the long words break, each break drawing a hyphen.",
hyphenated: "Our inter\u{00AD}national sub\u{00AD}scription, billed at \
1\u{00A0}200\u{00A0}EUR, covers unlimited docu\u{00AD}ments, prior\u{00AD}ity \
support and quarterly state\u{00AD}ments.",
};
/// The page in French.
const FRENCH: Words = Words {
without: "Sans césures conditionnelles : la justification écarte les mots \
pour remplir la colonne.",
with: "Avec césures conditionnelles : les mots longs se coupent, et chaque \
coupure dessine un trait d'union.",
hyphenated: "Notre abonne\u{00AD}ment inter\u{00AD}national, factu\u{00AD}ré \
1\u{00A0}200\u{00A0}EUR, comprend un nombre illi\u{00AD}mité de docu\u{00AD}ments, \
une assis\u{00AD}tance prio\u{00AD}ritaire et des rele\u{00AD}vés \
trimes\u{00AD}triels.",
};
/// 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 long words
/// 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 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("soft_hyphen")));
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();
// The upper setting is the same text with its soft hyphens removed, so the
// two panels differ only in whether the words may break.
let plain = words.hyphenated.replace('\u{00AD}', "");
for (top, label, paragraph) in [
(760.0, words.without, plain.as_str()),
(600.0, words.with, words.hyphenated),
] {
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);
let lines = flow.break_lines(paragraph, 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::{COLUMN, WORDS, language};
/// The lines two languages are allowed to write the same way. There are
/// none.
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 page exists to show soft hyphens breaking long words and a no-break
/// space holding a figure together. A paragraph translated without them
/// would draw two settings that read the same.
#[test]
fn every_language_marks_its_own_breaks() {
for (code, words) in WORDS {
let breaks = words.hyphenated.matches('\u{00AD}').count();
assert!(
breaks >= 4,
"the {} paragraph marks {breaks} places to break",
code.code()
);
assert!(
words.hyphenated.contains('\u{00A0}'),
"the {} paragraph holds no figure together",
code.code()
);
}
}
/// A word that fits the column whole never breaks, so a paragraph whose
/// longest word is short would draw the same setting twice.
#[test]
fn every_language_carries_a_word_the_column_cannot_hold() {
for (code, words) in WORDS {
let longest = words
.hyphenated
.split_whitespace()
.map(|word| word.chars().filter(|c| *c != '\u{00AD}').count())
.max()
.unwrap_or(0);
assert!(
longest >= 12,
"the longest {} word runs to {longest} characters, and a {COLUMN}\
point column takes it whole",
code.code()
);
}
}
}
|