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 | //! Bulleted, numbered and nested lists, each built out of the indents and one
//! tab stop rather than out of a list engine.
//!
//! A list is a paragraph whose first line hangs out to the left: the marker
//! sits in the room the hanging indent opens, the tabulation after it carries
//! the pen to the stop the body begins at, and every line below the first
//! starts at that same stop. Nesting is the same block again, its marker where
//! the block above put its body.
//!
//! The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks
//! which set the page is set in. The indents are not language: the headings
//! read the very numbers the blocks are built from, so no language can announce
//! a room the page does not open.
//!
//! Usage: `cargo run --example write_lists -- tmp/lists.pdf [font.ttf]`
//! `HQF_PDF_LANG=fr cargo run --example write_lists -- tmp/listes.pdf`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
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 left edge every block is set from.
const X: f64 = 70.0;
/// The room the two lists at the top of the page each have.
const NARROW: f64 = 210.0;
/// The left edge of the second of them.
const RIGHT: f64 = 320.0;
/// The room the nested list at the foot of the page has.
const WIDE: f64 = 460.0;
/// The size every list is set at.
const SIZE: f64 = 10.5;
/// The room left between a heading and the block below it.
const DROP: f64 = 4.0;
/// The top the two lists at the head of the page are set from.
const TOP: f64 = 780.0;
/// The top the nested list is set from, headings and all.
const NEST_TOP: f64 = 640.0;
/// How far into the box the bodies of the bulleted list begin.
const BULLET_BODY: f64 = 14.0;
/// How far into the box the bodies of the numbered list begin, which is what
/// the widest of its markers needs rather than what the narrowest does.
const NUMBER_BODY: f64 = 22.0;
/// Where the markers and the bodies of the inner list stand: its markers begin
/// where the outer bodies do, and its bodies one step further in again.
const INNER_MARKER: f64 = NUMBER_BODY;
/// See [`INNER_MARKER`].
const INNER_BODY: f64 = NUMBER_BODY * 2.0;
/// The words the page is written in, one set per language.
#[derive(Debug)]
struct Words {
/// The heading over the bulleted list, on either side of the room its
/// bodies begin at.
bulleted_before: &'static str,
/// See [`Words::bulleted_before`].
bulleted_after: &'static str,
/// The items of the bulleted list, each behind its bullet and a tabulation.
bullets: &'static str,
/// The heading over the numbered list, on either side of the room its
/// bodies begin at.
numbered_before: &'static str,
/// See [`Words::numbered_before`].
numbered_after: &'static str,
/// The items of the numbered list, whose markers are wider than a bullet
/// and so ask for more room in front of the body.
numbers: &'static str,
/// The heading over the nested list.
nested: &'static str,
/// The outer items of the nested list, the first of which is broken down
/// further.
outer: [&'static str; 2],
/// What the first outer item breaks down into, set again one level in.
inner: &'static str,
}
impl Words {
/// The words the page is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
/// The heading over the bulleted list, with the room its bodies begin at
/// read off the very number the block is built from.
fn bulleted(&self) -> String {
format!(
"{}{BULLET_BODY:.0}{}",
self.bulleted_before, self.bulleted_after
)
}
/// The heading over the numbered list, likewise.
fn numbered(&self) -> String {
format!(
"{}{NUMBER_BODY:.0}{}",
self.numbered_before, self.numbered_after
)
}
}
/// The page in English.
const ENGLISH: Words = Words {
bulleted_before: "Bulleted: a bullet, a tabulation, and ",
bulleted_after: " points in front of the body.",
bullets: "\u{2022}\tEvery line below the first lines up with the words, not \
with the bullet.\n\
\u{2022}\tThe marker hangs in the room the negative first-line indent opens.\n\
\u{2022}\tA long item wraps inside the block and stays inside it.",
numbered_before: "Numbered: the same block, widened to ",
numbered_after: " points for the widest marker.",
numbers: "1.\tA number hangs out to the left exactly as a bullet does.\n\
2.\tThe room in front is what the widest marker needs, not the narrowest.\n\
10.\tA two-figure number still ends before the stop the body begins at.",
nested: "Nested: the inner list is the same block again, its markers \
standing where the outer bodies begin.",
outer: [
"1.\tPrepare the document, the font it draws through and the page it fills.",
"2.\tAdd each page, with the content stream it was built from.",
],
inner: "a.\tRead the font from its file and hand it to the document.\n\
b.\tName the page size, in points.",
};
/// The page in French.
const FRENCH: Words = Words {
bulleted_before: "À puces : une puce, une tabulation et ",
bulleted_after: " points devant le corps.",
bullets: "•\tToutes les lignes sous la première s’alignent sur les mots, pas sur la \
puce.\n•\tLe marqueur se loge dans l’espace que dégage le retrait négatif de \
première ligne.\n•\tUne entrée longue passe à la ligne dans le bloc et y \
reste.",
numbered_before: "Numérotée : le même bloc, élargi à ",
numbered_after: " points pour le marqueur le plus large.",
numbers: "1.\tUn numéro dépasse à gauche exactement comme une puce.\n2.\tLa place devant \
est celle qu’il faut au plus large des marqueurs, pas au plus étroit.\n10.\tUn \
numéro à deux chiffres finit toujours avant le taquet où commence le corps.",
nested: "Imbriquée : la liste intérieure est le même bloc, ses marqueurs posés là où \
commencent les corps de la liste extérieure.",
outer: [
"1.\tPréparer le document, la police de son texte et la page qu’il remplit.",
"2.\tAjouter chaque page, avec le flux de contenu qui la compose.",
],
inner: "a.\tLire la police dans son fichier et la donner au document.\n\
b.\tDonner la taille de la page, en points.",
};
/// The page in Dutch.
const DUTCH: Words = Words {
bulleted_before: "Met opsommingstekens: een teken, een tab en",
bulleted_after: " punten voor de hoofdtekst.",
bullets: "•\tElke volgregel lijnt uit met de woorden, niet met het teken.\n•\tHet teken hangt in de ruimte die de negatieve inspringing opent.\n•\tEen lang item breekt binnen het blok af en blijft erin.",
numbered_before: "Genummerd: hetzelfde blok, verbreed tot",
numbered_after: " punten voor het breedste teken.",
numbers: "1.\tEen nummer hangt links naar buiten, net als een teken.\n2.\tDe ruimte ervoor volgt het breedste teken.\n10.\tOok een nummer van twee cijfers eindigt vóór de tabstop.",
nested: "Genest: de binnenste lijst is weer hetzelfde blok, met de tekens op de plek waar de tekst van de buitenste begint.",
outer: [
"1.\tHet document voorbereiden, het lettertype waarmee het tekent en de pagina die het vult.",
"2.\tElke pagina toevoegen, met de inhoudsstroom waaruit ze is opgebouwd.",
],
inner: "a.\tHet lettertype uit zijn bestand lezen en aan het document geven.\nb.\tHet paginaformaat noemen, in punten.",
};
/// The page in Portuguese.
const PORTUGUESE: Words = Words {
bulleted_before: "Com marcas: uma marca, uma tabulação e",
bulleted_after: " pontos à frente do corpo.",
bullets: "•\tCada linha abaixo da primeira alinha pelas palavras, não pela marca.\n•\tA marca fica pendurada no espaço que abre o avanço negativo da primeira linha.\n•\tUm item longo muda de linha dentro do bloco e fica dentro dele.",
numbered_before: "Numerada: o mesmo bloco, alargado para",
numbered_after: " pontos para a marca mais larga.",
numbers: "1.\tUm número fica pendurado à esquerda tal como uma marca.\n2.\tO espaço à frente é o que a marca mais larga precisa, não a mais estreita.\n10.\tUm número de dois algarismos acaba ainda antes da paragem de tabulação onde começa o corpo.",
nested: "Aninhada: a lista interior é de novo o mesmo bloco, com as marcas onde começam os corpos exteriores.",
outer: [
"1.\tPreparar o documento, o tipo de letra com que desenha e a página que preenche.",
"2.\tAcrescentar cada página, com o fluxo de conteúdo a partir do qual foi construída.",
],
inner: "a.\tLer o tipo de letra do seu ficheiro e entregá-lo ao documento.\nb.\tIndicar o tamanho da página, em pontos.",
};
/// The page in Italian.
const ITALIAN: Words = Words {
bulleted_before: "Puntato: un pallino, una tabulazione e",
bulleted_after: " punti davanti al corpo.",
bullets: "•\tOgni riga sotto la prima si allinea alle parole, non al pallino.\n•\tIl segno pende nello spazio aperto dal rientro negativo della prima riga.\n•\tUna voce lunga va a capo dentro il blocco e ci resta.",
numbered_before: "Numerato: lo stesso blocco, allargato a",
numbered_after: " punti per il segno più largo.",
numbers: "1.\tUn numero pende a sinistra proprio come un pallino.\n2.\tLo spazio davanti è quello che serve al segno più largo, non al più stretto.\n10.\tUn numero a due cifre finisce comunque prima del fermo dove inizia il corpo.",
nested: "Annidato: l’elenco interno è di nuovo lo stesso blocco, con i segni dove iniziano i corpi esterni.",
outer: [
"1.\tPreparare il documento, il font con cui disegna e la pagina che riempie.",
"2.\tAggiungere ogni pagina, con il flusso di contenuto da cui è stata costruita.",
],
inner: "a.\tLeggere il font dal suo file e passarlo al documento.\nb.\tIndicare il formato della pagina, in punti.",
};
/// The page in Spanish.
const SPANISH: Words = Words {
bulleted_before: "Con viñetas: una viñeta, un tabulador y ",
bulleted_after: " puntos delante del cuerpo.",
bullets: "•\tCada línea bajo la primera se alinea con las palabras, no con la viñeta.\n•\tLa marca cuelga en el hueco que abre la sangría negativa de la primera línea.\n•\tUn elemento largo pasa de línea dentro del bloque y se queda en él.",
numbered_before: "Numerada: el mismo bloque, ensanchado a ",
numbered_after: " puntos para la marca más ancha.",
numbers: "1.\tUn número cuelga a la izquierda igual que una viñeta.\n2.\tEl hueco delante es el que necesita la marca más ancha, no la más estrecha.\n10.\tUn número de dos cifras sigue acabando antes del tope donde empieza el cuerpo.",
nested: "Anidada: la lista interior es otra vez el mismo bloque, con sus marcas donde empiezan los cuerpos exteriores.",
outer: [
"1.\tPreparar el documento, la fuente con que se dibuja y la página que llena.",
"2.\tAñadir cada página, con el flujo de contenido del que se construyó.",
],
inner: "a.\tLeer la fuente de su archivo y dársela al documento.\nb.\tNombrar el tamaño de página, en puntos.",
};
/// The page in German.
const GERMAN: Words = Words {
bulleted_before: "Mit Aufzählungszeichen: ein Aufzählungspunkt, ein Tabulator und ",
bulleted_after: " Punkt vor dem Text.",
bullets: "•\tJede Folgezeile richtet sich an den Wörtern aus, nicht am Zeichen.\n•\tDas Zeichen hängt im Raum, den der negative Erstzeileneinzug öffnet.\n•\tEin langer Eintrag bricht im Block um und bleibt darin.",
numbered_before: "Nummeriert: derselbe Block, verbreitert auf ",
numbered_after: " Punkt für das breiteste Zeichen.",
numbers: "1.\tEine Zahl hängt genau wie ein Zeichen nach links heraus.\n2.\tDer Raum davor richtet sich nach dem breitesten Zeichen.\n10.\tAuch eine zweistellige Zahl endet vor dem Tabstopp des Texts.",
nested: "Verschachtelt: Die innere Liste ist wieder derselbe Block, ihre Zeichen stehen dort, wo die äußeren Texte beginnen.",
outer: [
"1.\tDas Dokument vorbereiten, die Schrift, mit der es zeichnet, und die Seite, die es füllt.",
"2.\tJede Seite hinzufügen, mit dem Inhaltsstrom, aus dem sie gebaut wurde.",
],
inner: "a.\tDie Schrift aus ihrer Datei lesen und dem Dokument übergeben.\nb.\tDie Seitengröße nennen, in Punkt.",
};
/// 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),
];
/// A list block whose markers begin `marker` points into the box and whose
/// bodies begin `body` points into it.
///
/// One call sets the three settings a list is made of, and there is no list
/// engine under them: the left indent holds every line at the body, the
/// first-line indent hangs that one line back out to the marker, and the single
/// ruler stop is what the tabulation after the marker carries the pen to.
fn list(handle: &FontHandle, marker: f64, body: f64) -> TextFlow<'_> {
TextFlow::new(handle, SIZE)
.leading(14.0)
.hanging_list(marker, body)
}
/// Draws a heading across `width` and hands back the top of the block that goes
/// under it, however many lines the heading itself took.
fn heading(
c: &mut Content,
handle: &FontHandle,
top: f64,
x: f64,
width: f64,
label: &str,
) -> Result<f64, Box<dyn std::error::Error>> {
let flow = TextFlow::new(handle, 8.0);
let lines = flow.break_lines(label, width);
c.begin_text();
flow.draw(c, &lines, x, top, width)?;
c.end_text();
Ok(top - flow.height(&lines) - DROP)
}
/// Draws one block at `top` and hands back the top of whatever follows it.
fn block(
c: &mut Content,
flow: &TextFlow<'_>,
text: &str,
x: f64,
top: f64,
width: f64,
) -> Result<f64, Box<dyn std::error::Error>> {
let lines = flow.break_lines(text, width);
c.begin_text();
flow.draw(c, &lines, x, top, width)?;
c.end_text();
Ok(top - flow.height(&lines))
}
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("lists")));
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();
// A bullet is narrow, so the body begins close in.
let top = heading(&mut c, &handle, TOP, X, NARROW, &words.bulleted())?;
block(
&mut c,
&list(&handle, 0.0, BULLET_BODY),
words.bullets,
X,
top,
NARROW,
)?;
// "10." is wider than "1.", and the room in front is what the widest of
// them needs.
let top = heading(&mut c, &handle, TOP, RIGHT, NARROW, &words.numbered())?;
block(
&mut c,
&list(&handle, 0.0, NUMBER_BODY),
words.numbers,
RIGHT,
top,
NARROW,
)?;
// Nesting is the same block again with its marker where the block above put
// its body, each one set from the foot of the one before it.
let mut top = heading(&mut c, &handle, NEST_TOP, X, WIDE, words.nested)?;
let outer = list(&handle, 0.0, NUMBER_BODY);
let inner = list(&handle, INNER_MARKER, INNER_BODY);
for (index, item) in words.outer.iter().enumerate() {
top = block(&mut c, &outer, item, X, top, WIDE)?;
if index == 0 {
top = block(&mut c, &inner, words.inner, X, top, WIDE)?;
}
}
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 hqf_pdf::{Document, Font, TextFlow};
use super::{
BULLET_BODY, DROP, NARROW, NEST_TOP, NUMBER_BODY, SIZE, TOP, WORDS, default_font, language,
};
/// The lines two languages are allowed to write the same way. Every field
/// of the page is a sentence, and no two languages write one alike.
const SPARED: [&str; 0] = [];
/// The foot the head of the page reaches when its heading and its block are
/// set at `width` from `TOP`.
fn foot_of(heading: &str, body: &str, body_indent: f64, width: f64) -> f64 {
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);
let over = TextFlow::new(&handle, 8.0);
let heading_lines = over.break_lines(heading, width);
let top = TOP - over.height(&heading_lines) - DROP;
let flow = TextFlow::new(&handle, SIZE)
.leading(14.0)
.hanging_list(0.0, body_indent);
let lines = flow.break_lines(body, width);
top - flow.height(&lines)
}
#[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 two lists at the head of the page are set from a top of their own,
/// and the nested one from another: a language whose items run to one line
/// more would lay the first over the second with nothing on the page to say
/// so.
#[test]
fn every_language_keeps_the_lists_at_the_head_above_the_nested_one() {
for (named, words) in WORDS {
let code = named.code();
for (heading, body, indent) in [
(words.bulleted(), words.bullets, BULLET_BODY),
(words.numbered(), words.numbers, NUMBER_BODY),
] {
let foot = foot_of(&heading, body, indent, NARROW);
assert!(
foot >= NEST_TOP,
"a list at the head of the {code} page reaches {foot}, \
under the nested list at {NEST_TOP}"
);
}
}
}
}
|