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 | //! Sets one justified paragraph in a narrow column four times: first with no
//! break points inside its words, then breaking after a slash or a hyphen, then
//! breaking where its punctuation opens a break, so the paths and compounds
//! that ran past the edge can be seen wrapping in it, and last breaking where
//! its punctuation opens a break once each hyphen has given way to a
//! non-breaking one, so the paths still wrap and the compounds stay whole.
//!
//! The text carries file paths and hyphenated compounds whose tokens are wider
//! than the column. The first setting breaks only at spaces, so those tokens
//! run past the column's right edge; the second breaks after `/` and `-`,
//! keeping the character on the line, so the tokens wrap inside the column; the
//! third names no character and reads the punctuation itself, breaking after a
//! slash or a hyphen but never after one that opens a path; the fourth reads
//! the punctuation too, its text given with each hyphen replaced by a
//! non-breaking hyphen before the lines are broken.
//!
//! The sentence and the four 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, Replacement};
use hqf_pdf::{Document, Font, FontHandle, Page, 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")
}
/// 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 every setting has 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 heading over the setting that breaks where its punctuation opens a
/// break.
punctuation: &'static str,
/// The heading over the setting that breaks where its punctuation opens a
/// break, each hyphen replaced by a non-breaking one.
whole: &'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 every setting is 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.",
punctuation: "Breaking where punctuation allows: paths and compounds wrap, no character named.",
whole: "Breaking where punctuation allows, no-break hyphens: paths wrap, compounds stay whole.",
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 : chemins et mots composés se coupent.",
punctuation: "Coupure à la ponctuation, sans nommer de caractère : chemins et mots composés se coupent.",
whole: "Coupure à la ponctuation, trait d’union insécable : les chemins se coupent, pas les mots composés.",
text_before: "Envoyez chaque requête à ",
text_between: ", puis consultez le fac-similé en lecture seule à l’adresse ",
text_after: " avec l’offre à débit limité.",
};
/// The page in Dutch.
const DUTCH: Words = Words {
plain: "Zonder breekpunten: paden lopen over de rand van de kolom.",
breaking: "Breken na schuine streep en koppelteken: paden en samenstellingen worden afgebroken.",
punctuation: "Breken bij leestekens, geen teken opgegeven: paden en samenstellingen worden afgebroken.",
whole: "Breken bij leestekens, vaste koppeltekens: paden worden afgebroken, samenstellingen niet.",
text_before: "Stuur elk verzoek naar ",
text_between: " en lees daarna de alleen-lezenkopie van ",
text_after: " op het niveau met verzoeklimiet.",
};
/// The page in Portuguese.
const PORTUGUESE: Words = Words {
plain: "Sem pontos de quebra: os caminhos ultrapassam a margem da coluna.",
breaking: "Quebra após a barra e o hífen: os caminhos e a palavra composta partem-se.",
punctuation: "Quebra pela pontuação, sem indicar caracteres: os caminhos e a palavra composta partem-se.",
whole: "Quebra pela pontuação, hífen inseparável: partem-se os caminhos, não a palavra composta.",
text_before: "Envie cada pedido para ",
text_between: " e, depois, leia a cópia só de leitura em ",
text_after: ", no escalão com taxa-limite de pedidos.",
};
/// The page in Italian.
const ITALIAN: Words = Words {
plain: "Senza punti di interruzione: i percorsi escono dal bordo della colonna.",
breaking: "Interruzione dopo barra e trattino: percorsi e composti vanno a capo.",
punctuation: "Interruzione sulla punteggiatura, senza indicare segni: percorsi e composti vanno a capo.",
whole: "Interruzione sulla punteggiatura, trattini indivisibili: a capo i percorsi, non i composti.",
text_before: "Inviare ogni richiesta a ",
text_between: ", poi leggere da ",
text_after: " la copia in sola lettura, sul livello con soglia-limite di richieste.",
};
/// The page in Spanish.
const SPANISH: Words = Words {
plain: "Sin puntos de corte: las rutas se salen del borde de la columna.",
breaking: "Corte tras la barra y el guion: las rutas y los compuestos se parten.",
punctuation: "Corte según la puntuación, sin nombrar caracteres: las rutas y los compuestos se parten.",
whole: "Corte según la puntuación con guiones inseparables: las rutas se parten, los compuestos no.",
text_before: "Envíe cada petición a ",
text_between: " como pares clave-valor y después lea la copia de solo lectura desde ",
text_after: ", en el nivel con límite de peticiones.",
};
/// The page in German.
const GERMAN: Words = Words {
plain: "Ohne Umbruchstellen: Pfade laufen über den Spaltenrand hinaus.",
breaking: "Umbruch nach Schrägstrich und Bindestrich: Pfade und Komposita brechen um.",
punctuation: "Umbruch nach Satzzeichen, ohne Zeichenvorgabe: Pfade und Komposita brechen um.",
whole: "Umbruch nach Satzzeichen, Bindestriche geschützt: Pfade brechen um, Komposita bleiben ganz.",
text_before: "Senden Sie jede Anfrage an ",
text_between: " und lesen Sie anschließend die Nur-Lese-Kopie unter ",
text_after: " im Tarif mit Ratenbegrenzung.",
};
/// 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); 7] = [
(Language::English, &ENGLISH),
(Language::French, &FRENCH),
(Language::German, &GERMAN),
(Language::Spanish, &SPANISH),
(Language::Italian, &ITALIAN),
(Language::Portuguese, &PORTUGUESE),
(Language::Dutch, &DUTCH),
];
/// The column every setting is broken to, narrow enough that the paths reach
/// its edge.
const COLUMN: f64 = 125.0;
/// What the last setting puts in place of each hyphen: a hyphen no line breaks
/// after.
const NON_BREAKING_HYPHEN: &str = "\u{2011}";
/// The width a heading is set in, on one line.
const HEADING_WIDTH: f64 = 400.0;
/// The left edge of every setting, stacked one above the other.
const X: f64 = 80.0;
/// Where the heading of the first setting of a page stands.
const TOP: f64 = 760.0;
/// The room left between the bottom of one setting and the heading of the next,
/// where the next one's place follows from the one above it.
const GAP: f64 = 30.0;
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 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);
for panels in pages(&handle, words) {
let mut c = Content::new();
set_page(&mut c, &handle, &panels, &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(())
}
/// The settings the page sets its text in, each under its heading, two to a
/// page: the last two share theirs, so that the compound the one breaks can be
/// seen whole in the other.
///
/// Every setting sets the same text; only where a word may break inside itself
/// differs between them.
fn pages<'a>(handle: &'a FontHandle, words: &Words) -> [[(&'static str, TextFlow<'a>); 2]; 2] {
let setting = || {
TextFlow::new(handle, 11.0)
.leading(15.0)
.align(Align::Justify)
};
[
[
(words.plain, setting()),
(words.breaking, setting().break_after("/-")),
],
[
(words.punctuation, setting().break_at_punctuation(true)),
(
words.whole,
setting()
.break_at_punctuation(true)
.replacing('-', Replacement::Each(NON_BREAKING_HYPHEN.into())),
),
],
]
}
/// Sets each of `panels` one under the other from the top of a page, and hands
/// back where the last one ends.
fn set_page(
c: &mut Content,
handle: &FontHandle,
panels: &[(&str, TextFlow<'_>)],
text: &str,
) -> Result<f64, Box<dyn std::error::Error>> {
let mut top = TOP;
let mut bottom = TOP;
for (label, flow) in panels {
bottom = panel(c, handle, label, flow, text, top)?;
top = bottom - GAP;
}
Ok(bottom)
}
/// Sets `label` at `top` and `text` below it in the column, broken by `flow`,
/// with the column's edge around it, and hands back where the setting ends.
fn panel(
c: &mut Content,
handle: &FontHandle,
label: &str,
flow: &TextFlow<'_>,
text: &str,
top: f64,
) -> Result<f64, Box<dyn std::error::Error>> {
let heading = TextFlow::new(handle, 8.0);
let heading_lines = heading.break_lines(label, HEADING_WIDTH);
c.begin_text();
heading.draw(c, &heading_lines, X, top, HEADING_WIDTH)?;
c.end_text();
let text_top = top - 20.0;
let lines = flow.break_lines(text, COLUMN);
c.begin_text();
flow.draw(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();
Ok(text_top - height)
}
#[cfg(test)]
mod tests {
use hqf_pdf::content::Content;
use hqf_pdf::{Document, Font, FontHandle, TextFlow};
use super::{COLUMN, HEADING_WIDTH, WORDS, default_font, language, pages, set_page};
/// The lowest a setting may reach on its page.
const BOTTOM: f64 = 56.0;
/// 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] = [];
/// A handle on the font the example draws with.
fn handle(doc: &mut Document) -> FontHandle {
doc.add_font(Font::parse(std::fs::read(default_font()).unwrap()).unwrap())
}
#[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:?}"
);
}
#[test]
fn every_heading_holds_on_one_line_in_every_language() {
let mut doc = Document::new();
let handle = handle(&mut doc);
let heading = TextFlow::new(&handle, 8.0);
let overlong: Vec<_> = WORDS
.iter()
.flat_map(|(language, words)| {
[words.plain, words.breaking, words.punctuation, words.whole]
.map(|label| (language, label, handle.measure(label, 8.0)))
})
.filter(|(_, label, _)| heading.break_lines(label, HEADING_WIDTH).len() > 1)
.collect();
assert!(
overlong.is_empty(),
"set on more than one line: {overlong:?}"
);
}
#[test]
fn every_page_ends_above_its_bottom_in_every_language() {
let mut doc = Document::new();
let handle = handle(&mut doc);
let low: Vec<_> = WORDS
.iter()
.flat_map(|(language, words)| {
pages(&handle, words).map(|panels| {
let bottom =
set_page(&mut Content::new(), &handle, &panels, &words.text()).unwrap();
(language, bottom)
})
})
.filter(|(_, bottom)| *bottom < BOTTOM)
.collect();
assert!(low.is_empty(), "these pages end below {BOTTOM}: {low:?}");
}
#[test]
fn the_last_setting_keeps_whole_a_compound_the_one_before_breaks() {
let mut doc = Document::new();
let handle = handle(&mut doc);
let ends = |flow: &TextFlow<'_>, text: &str| -> Vec<usize> {
flow.break_lines(text, COLUMN)
.iter()
.map(|line| line.source().end)
.collect()
};
let alike: Vec<_> = WORDS
.iter()
.filter(|(_, words)| {
let [_, [(_, punctuation), (_, whole)]] = pages(&handle, words);
ends(&punctuation, &words.text()) == ends(&whole, &words.text())
})
.map(|(language, _)| language)
.collect();
assert!(
alike.is_empty(),
"the two settings break alike in {alike:?}"
);
}
#[test]
fn every_sentence_holds_a_hyphenated_compound() {
for (language, words) in &WORDS {
assert!(
words.text().contains('-'),
"{language:?} gives the last setting no hyphen to keep"
);
}
}
}
|