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
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740 | //! Writes a run of personal letters into one file, and lets its first sheet
//! jump to any of them.
//!
//! A mailing house receives one PDF and has to know where each letter starts,
//! how many sheets it runs to, and which envelope it goes in. The file says it
//! itself: each recipient is a document part that claims the next so many
//! sheets, counted rather than numbered by hand. The index on the first sheet
//! then leads to the part rather than to a page, so a letter that gains a sheet
//! moves nothing.
//!
//! Both sets of words are held in `Words`, once per language, and
//! `HQF_PDF_LANG` picks which set is drawn.
//!
//! Usage: `cargo run --example write_one_letter_each -- tmp/letters.pdf
//! [font.ttf]`
//! `HQF_PDF_LANG=fr cargo run --example write_one_letter_each`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
use hqf_pdf::layout::Rect;
use hqf_pdf::{
Document, DocumentPart, DocumentParts, Error, Font, FontHandle, Link, LinkTarget, Page,
PartData, Rgb, TextFlow, Version,
};
#[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 page is set in when the caller names none: the one committed
/// for the tests, so 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 pages are written in, one set per language.
#[derive(Debug)]
struct Words {
/// What the file as a whole is called.
title: &'static str,
/// What the file carries, said once at the top of the first sheet.
lead: &'static str,
/// What the hierarchy says, said in grey at the foot of the first sheet.
note: &'static str,
/// The heading above the list of recipients.
index_heading: &'static str,
/// Who each of the four letters is for.
recipients: [&'static str; 4],
/// Where each of them is sent.
towns: [&'static str; 4],
/// The country every recipient is in.
country: &'static str,
/// The word a reference is introduced by.
reference: &'static str,
/// The word a subject is introduced by.
subject: &'static str,
/// What each of the four letters is about.
subjects: [&'static str; 4],
/// How a letter opens.
greeting: &'static str,
/// What every letter says.
body: &'static str,
/// How a letter closes.
closing: &'static str,
/// Who signs it.
signer: &'static str,
/// What the signer does.
signer_role: &'static str,
/// The heading of the sheet that follows the first letter.
enclosure_title: &'static str,
/// What that sheet says.
enclosure: &'static str,
/// The word a letter is called by.
letter: &'static str,
/// The word a sheet is counted by.
sheet: &'static str,
/// The word one sheet is counted by, in the middle of a line.
sheet_one: &'static str,
/// The word several sheets are counted by, in the middle of a line.
sheet_many: &'static str,
}
impl Words {
/// The words the pages are written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
}
/// The pages in English.
const ENGLISH: Words = Words {
title: "One letter each, in one file",
lead: "The five sheets that follow are four letters, and this index leads \
to them. No letter names a page number, and none had to: each \
recipient claims the next so many sheets, and the index leads to the \
recipient rather than to a sheet. Give the second letter another \
page and nothing above has to be counted again.",
note: "Each entry of the index is a go-to document part action: it names \
the part that claims the sheet, not the sheet itself, so reading \
software following it lands on that recipient's first page wherever that page \
has moved to. A mailing house reads the same parts to know where to \
cut and what to fold.",
index_heading: "Who this run is for",
recipients: [
"Baker and Sons",
"Halden Joinery",
"Westmill Dairy",
"Corran Bookbinders",
],
towns: ["Bristol", "Kendal", "Truro", "Oban"],
country: "United Kingdom",
reference: "Our reference:",
subject: "Subject:",
subjects: [
"Your delivery window from September",
"The crates returned in July",
"Collection moved to Tuesdays",
"The paper stock held for you",
],
greeting: "Dear Sir or Madam,",
body: "We are writing to every account served from this depot, because the \
round that reaches you changes at the end of the month. Nothing you \
have ordered is affected, and the prices agreed in March stand until \
they are next reviewed. Should the new arrangement not suit you, \
reply to this letter quoting the reference above and we will keep \
the old one for you until the end of the year.",
closing: "Yours faithfully,",
signer: "R. Alderton",
signer_role: "Depot manager",
enclosure_title: "What changes, and when",
enclosure: "The round leaves the depot an hour earlier and calls in the \
reverse order, so an address served last is now served first. \
Deliveries falling on a public holiday move to the working day \
after it rather than the one before. An order placed after four \
in the afternoon travels on the round after next, which is one \
day later than it used to.",
letter: "Letter",
sheet: "Sheet",
sheet_one: "sheet",
sheet_many: "sheets",
};
/// The pages in French.
const FRENCH: Words = Words {
title: "Une lettre chacun, dans un seul fichier",
lead: "Les cinq feuillets qui suivent forment quatre lettres, et le présent sommaire \
y mène. Aucune lettre n’indique de numéro de page, et aucune n’en a eu \
besoin : chaque destinataire prend les feuillets suivants, autant qu’il lui en \
faut, et le sommaire mène au destinataire plutôt qu’à un feuillet. Donnez une \
page de plus à la deuxième lettre : rien de ce qui précède n’est à recompter.",
note: "Chaque ligne du sommaire est une action « aller à une partie » : elle nomme la \
partie qui revendique le feuillet, et non le feuillet lui-même, de sorte qu’un \
logiciel de lecture qui la suit tombe sur la première page de ce destinataire, \
où qu’elle soit passée. Un routeur lit les mêmes parties pour savoir où couper \
et quoi plier.",
index_heading: "À qui ce lot s’adresse",
recipients: [
"Boulangerie Petit",
"Menuiserie Vallon",
"Laiterie du Coteau",
"Reliure du Port",
],
towns: ["Aix-en-Provence", "Annecy", "Quimper", "Sète"],
country: "France",
reference: "Nos références\u{00A0}:",
subject: "Objet\u{00A0}:",
subjects: [
"Votre créneau de livraison à partir de septembre",
"Les caisses rendues en juillet",
"Ramassage déplacé au mardi",
"Le papier tenu à votre disposition",
],
greeting: "Madame, Monsieur,",
body: "Nous écrivons à tous les clients rattachés à ce dépôt, car la tournée qui vous \
livre change à la fin du mois. Vos commandes ne sont pas concernées, et les \
prix convenus en mars restent valables jusqu’à leur prochaine révision. Si la \
nouvelle organisation ne vous convient pas, répondez à ce courrier en \
rappelant la référence ci-dessus : nous maintiendrons l’ancienne pour vous \
jusqu’à la fin de l’année.",
closing: "Veuillez agréer nos salutations distinguées.",
signer: "R. Alderton",
signer_role: "Responsable du dépôt",
enclosure_title: "Ce qui change, et quand",
enclosure: "La tournée quitte le dépôt une heure plus tôt et passe dans l’ordre inverse : \
une adresse desservie en dernier l’est désormais en premier. Une livraison qui \
tombe un jour férié est reportée au jour ouvré suivant plutôt qu’au précédent. \
Une commande passée après seize heures part avec la deuxième tournée qui suit, \
soit un jour plus tard qu’auparavant.",
letter: "Lettre",
sheet: "Feuillet",
sheet_one: "feuillet",
sheet_many: "feuillets",
};
/// The pages in Dutch.
const DUTCH: Words = Words {
title: "Voor elk een brief, in één bestand",
lead: "De vijf bladen die volgen zijn vier brieven, en deze index leidt ernaar. Geen brief noemt een paginanummer, en dat hoefde ook niet: elke ontvanger krijgt de eerstvolgende bladen, en de index leidt naar de ontvanger in plaats van naar een blad. Geef de tweede brief een extra pagina en niets hierboven hoeft opnieuw geteld te worden.",
note: "Elke regel van de index is een actie ga-naar-documentdeel: ze noemt het deel waartoe het blad behoort, niet het blad zelf, dus leessoftware die haar volgt, landt op de eerste pagina van die ontvanger waar die pagina ook naartoe is geschoven. Een verzendhuis leest dezelfde delen om te weten waar te snijden en wat te vouwen.",
index_heading: "Voor wie deze oplage is",
recipients: [
"Baker and Sons",
"Halden Joinery",
"Westmill Dairy",
"Corran Bookbinders",
],
towns: ["Bristol", "Kendal", "Truro", "Oban"],
country: "Verenigd Koninkrijk",
reference: "Ons kenmerk:",
subject: "Onderwerp:",
subjects: [
"Uw leveringsvenster vanaf september",
"De in juli teruggestuurde kratten",
"Ophaaldag verschoven naar dinsdag",
"Het voor u gereserveerde papier",
],
greeting: "Geachte heer, mevrouw,",
body: "Wij schrijven aan elke klant die vanuit dit depot wordt beleverd, omdat de route die u bereikt aan het einde van de maand verandert. Niets van wat u besteld hebt, verandert, en de in maart afgesproken prijzen blijven gelden tot de volgende herziening. Past de nieuwe regeling u niet, antwoord dan op deze brief met het kenmerk hierboven en wij houden de oude voor u aan tot het einde van het jaar.",
closing: "Met vriendelijke groet,",
signer: "R. Alderton",
signer_role: "Depotbeheerder",
enclosure_title: "Wat er verandert, en wanneer",
enclosure: "De route vertrekt een uur eerder uit het depot en rijdt in omgekeerde volgorde, dus een adres dat als laatste werd bediend, komt nu als eerste. Leveringen die op een feestdag vallen, schuiven naar de werkdag erna in plaats van de dag ervoor. Een bestelling na vier uur ’s middags gaat mee met de route na de volgende, een dag later dan vroeger.",
letter: "Brief",
sheet: "Blad",
sheet_one: "blad",
sheet_many: "bladen",
};
/// The pages in Portuguese.
const PORTUGUESE: Words = Words {
title: "Uma carta para cada um, num ficheiro",
lead: "As cinco folhas que se seguem são quatro cartas, e este índice leva a elas. Nenhuma carta nomeia um número de página, nem foi preciso: cada destinatário reclama as folhas que lhe cabem, e o índice leva ao destinatário e não a uma folha. Dê outra página à segunda carta e nada acima tem de ser recontado.",
note: "Cada entrada do índice é uma ação de ir para uma parte do documento: nomeia a parte que reclama a folha, não a folha em si, por isso o programa de leitura que a segue chega à primeira página desse destinatário onde quer que ela tenha ido parar. Uma empresa de envios lê as mesmas partes para saber onde cortar e o que dobrar.",
index_heading: "Para quem é esta tiragem",
recipients: [
"Baker and Sons",
"Halden Joinery",
"Westmill Dairy",
"Corran Bookbinders",
],
towns: ["Bristol", "Kendal", "Truro", "Oban"],
country: "Reino Unido",
reference: "A nossa referência:",
subject: "Assunto:",
subjects: [
"A sua janela de entrega a partir de setembro",
"As caixas devolvidas em julho",
"A recolha passa para as terças-feiras",
"O papel reservado para si",
],
greeting: "Exmos. Senhores,",
body: "Escrevemos a todos os clientes servidos a partir deste armazém, porque a volta que vos serve muda no fim do mês. Nada do que encomendaram é afetado, e os preços acordados em março mantêm-se até à próxima revisão. Se a nova organização não vos convier, respondam a esta carta citando a referência acima e manteremos a anterior até ao fim do ano.",
closing: "Com os melhores cumprimentos,",
signer: "R. Alderton",
signer_role: "Chefe de armazém",
enclosure_title: "O que muda, e quando",
enclosure: "A volta sai do armazém uma hora mais cedo e passa pela ordem inversa, por isso uma morada servida em último é agora servida em primeiro. As entregas que calham num feriado passam para o dia útil seguinte e não para o anterior. Uma encomenda feita depois das quatro da tarde vai na volta a seguir à próxima, um dia mais tarde do que antes.",
letter: "Carta",
sheet: "Folha",
sheet_one: "folha",
sheet_many: "folhas",
};
/// The pages in Italian.
const ITALIAN: Words = Words {
title: "Una lettera a testa, in un file",
lead: "I cinque fogli che seguono sono quattro lettere, e questo indice porta a esse. Nessuna lettera nomina un numero di pagina, né è servito: ogni destinatario rivendica i fogli che gli spettano, e l’indice porta al destinatario e non a un foglio. Date un’altra pagina alla seconda lettera e niente qui sopra va ricontato.",
note: "Ogni voce dell’indice è un’azione «vai a» verso una parte del documento: nomina la parte che rivendica il foglio, non il foglio stesso, così il programma di lettura che la segue arriva alla prima pagina di quel destinatario ovunque si sia spostata. Un’azienda di spedizioni legge le stesse parti per sapere dove tagliare e cosa piegare.",
index_heading: "Per chi è questa tiratura",
recipients: [
"Baker and Sons",
"Halden Joinery",
"Westmill Dairy",
"Corran Bookbinders",
],
towns: ["Bristol", "Kendal", "Truro", "Oban"],
country: "Regno Unito",
reference: "Nostro riferimento:",
subject: "Oggetto:",
subjects: [
"La vostra fascia di consegna da settembre",
"Le casse rese a luglio",
"Il ritiro passa al martedì",
"La carta tenuta per voi",
],
greeting: "Gentili Signori,",
body: "Scriviamo a tutti i clienti serviti da questo deposito, perché il giro che vi raggiunge cambia a fine mese. Nulla di quanto avete ordinato ne risente, e i prezzi concordati a marzo restano validi fino alla prossima revisione. Se la nuova organizzazione non vi conviene, rispondete a questa lettera citando il riferimento qui sopra e vi manterremo la precedente fino alla fine dell’anno.",
closing: "Distinti saluti,",
signer: "R. Alderton",
signer_role: "Responsabile del deposito",
enclosure_title: "Cosa cambia, e quando",
enclosure: "Il giro parte dal deposito un’ora prima e passa in ordine inverso, così un indirizzo servito per ultimo ora è servito per primo. Le consegne che cadono in un giorno festivo passano al giorno lavorativo successivo anziché al precedente. Un ordine fatto dopo le quattro del pomeriggio viaggia sul giro successivo al prossimo, un giorno più tardi di prima.",
letter: "Lettera",
sheet: "Foglio",
sheet_one: "foglio",
sheet_many: "fogli",
};
/// The pages in Spanish.
const SPANISH: Words = Words {
title: "Una carta para cada uno, en un archivo",
lead: "Las cinco hojas que siguen son cuatro cartas, y este índice lleva a ellas. Ninguna carta nombra un número de página, ni hizo falta: cada destinatario reclama las hojas que le tocan, y el índice lleva al destinatario y no a una hoja. Dé otra página a la segunda carta y no hay que volver a contar nada de arriba.",
note: "Cada entrada del índice es una acción de ir a una parte del documento: nombra la parte que reclama la hoja, no la hoja misma, así que el programa de lectura que la sigue llega a la primera página de ese destinatario dondequiera que se haya movido. Una empresa de envíos lee las mismas partes para saber dónde cortar y qué doblar.",
index_heading: "Para quién es esta tirada",
recipients: [
"Baker and Sons",
"Halden Joinery",
"Westmill Dairy",
"Corran Bookbinders",
],
towns: ["Bristol", "Kendal", "Truro", "Oban"],
country: "Reino Unido",
reference: "Nuestra referencia:",
subject: "Asunto:",
subjects: [
"Su franja de entrega a partir de septiembre",
"Las cajas devueltas en julio",
"La recogida pasa a los martes",
"El papel reservado para usted",
],
greeting: "Muy señores míos:",
body: "Escribimos a todas las cuentas servidas desde este almacén, porque la ruta que llega hasta ustedes cambia a final de mes. Nada de lo que han pedido se ve afectado, y los precios acordados en marzo se mantienen hasta su próxima revisión. Si el nuevo sistema no les conviene, respondan a esta carta citando la referencia de arriba y les mantendremos el anterior hasta final de año.",
closing: "Atentamente,",
signer: "R. Alderton",
signer_role: "Jefe de almacén",
enclosure_title: "Qué cambia, y cuándo",
enclosure: "La ruta sale del almacén una hora antes y pasa en orden inverso, así que una dirección servida la última ahora es la primera. Las entregas que caen en festivo pasan al día hábil siguiente y no al anterior. Un pedido hecho después de las cuatro de la tarde va en la ruta siguiente a la próxima, un día más tarde que antes.",
letter: "Carta",
sheet: "Hoja",
sheet_one: "hoja",
sheet_many: "hojas",
};
/// The pages in German.
const GERMAN: Words = Words {
title: "Ein Brief pro Empfänger, in einer Datei",
lead: "Die fünf folgenden Blätter sind vier Briefe, und dieses Verzeichnis führt zu ihnen. Kein Brief nennt eine Seitenzahl, und keiner musste es: Jeder Empfänger beansprucht die nächsten Blätter, und das Verzeichnis führt zum Empfänger statt zu einem Blatt. Bekommt der zweite Brief eine weitere Seite, muss oben nichts neu gezählt werden.",
note: "Jeder Eintrag des Verzeichnisses ist eine Aktion, die zu einem Dokumentteil springt: Sie nennt den Teil, der das Blatt beansprucht, nicht das Blatt selbst, also landet eine Lesesoftware, die ihr folgt, auf der ersten Seite dieses Empfängers, wohin diese Seite auch gerückt ist. Ein Versanddienstleister liest dieselben Teile, um zu wissen, wo zu schneiden und was zu falten ist.",
index_heading: "Für wen dieser Lauf ist",
recipients: [
"Baker and Sons",
"Halden Joinery",
"Westmill Dairy",
"Corran Bookbinders",
],
towns: ["Bristol", "Kendal", "Truro", "Oban"],
country: "Vereinigtes Königreich",
reference: "Unser Zeichen:",
subject: "Betreff:",
subjects: [
"Ihr Lieferfenster ab September",
"Die im Juli zurückgegebenen Kisten",
"Abholung auf dienstags verlegt",
"Der für Sie reservierte Papiervorrat",
],
greeting: "Sehr geehrte Damen und Herren,",
body: "wir schreiben allen Kunden, die von diesem Depot beliefert werden, weil sich die Tour, die Sie erreicht, zum Monatsende ändert. Nichts von dem, was Sie bestellt haben, ist betroffen, und die im März vereinbarten Preise gelten bis zu ihrer nächsten Überprüfung. Sollte Ihnen die neue Regelung nicht passen, antworten Sie auf diesen Brief unter Angabe des obigen Zeichens, und wir behalten die alte für Sie bis zum Jahresende bei.",
closing: "Mit freundlichen Grüßen",
signer: "R. Alderton",
signer_role: "Depotleitung",
enclosure_title: "Was sich ändert, und wann",
enclosure: "Die Tour verlässt das Depot eine Stunde früher und fährt in umgekehrter Reihenfolge, also wird eine bisher zuletzt belieferte Adresse jetzt zuerst beliefert. Lieferungen, die auf einen Feiertag fallen, rücken auf den Werktag danach statt auf den davor. Eine nach vier Uhr nachmittags aufgegebene Bestellung fährt mit der übernächsten Tour, einen Tag später als bisher.",
letter: "Brief",
sheet: "Blatt",
sheet_one: "Blatt",
sheet_many: "Blätter",
};
/// 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 left edge of everything on the pages.
const LEFT: f64 = 72.0;
/// How wide a block of text is.
const WIDTH: f64 = 451.0;
/// The reference each letter is written under.
const REFERENCES: [&str; 4] = ["CT-4711", "CT-4712", "CT-4713", "CT-4714"];
/// How many sheets each letter runs to. The first carries an enclosure; the
/// others are a single sheet.
const SHEETS: [u8; 4] = [2, 1, 1, 1];
/// The first sheet of each letter, counting the index as sheet zero.
///
/// This is the same running count the parts themselves are given, said once so
/// that the index and the hierarchy cannot drift apart.
fn first_sheets() -> [usize; 4] {
let mut first = [0_usize; 4];
let mut cursor = 1;
for (index, run) in SHEETS.iter().enumerate() {
first[index] = cursor;
cursor += usize::from(*run);
}
first
}
/// Sets a block of words with its first baseline at `top`, and hands back the
/// ordinate the block ends at.
fn block(
c: &mut Content,
handle: &FontHandle,
size: f64,
top: f64,
text: &str,
) -> Result<f64, Error> {
let flow = TextFlow::new(handle, size);
let lines = flow.break_lines(text, WIDTH);
c.begin_text();
flow.draw(c, &lines, LEFT, top, WIDTH)?;
c.end_text();
Ok(top - flow.height(&lines))
}
/// Sets one line of the index, and hands back the ordinate it ends at together
/// with the rectangle it covers.
fn index_line(
c: &mut Content,
handle: &FontHandle,
top: f64,
text: &str,
) -> Result<(f64, Rect), Error> {
let flow = TextFlow::new(handle, 10.5);
let lines = flow.break_lines(text, WIDTH);
let boxes = flow.line_boxes(&lines, LEFT, top, WIDTH);
c.begin_text();
flow.draw(c, &lines, LEFT, top, WIDTH)?;
c.end_text();
let covered = boxes
.first()
.copied()
.ok_or(Error::NoSuchPage { page: 0 })?;
Ok((top - flow.height(&lines), covered))
}
/// How a letter is announced in the index.
fn entry(words: &Words, index: usize) -> String {
let sheets = if SHEETS[index] > 1 {
words.sheet_many
} else {
words.sheet_one
};
format!(
"{} \u{2014} {} \u{2014} {} \u{2014} {} {sheets}",
words.recipients[index], words.towns[index], REFERENCES[index], SHEETS[index]
)
}
/// Draws the foot of a sheet: which letter it belongs to, and where it stands
/// in it.
fn foot(
c: &mut Content,
words: &Words,
text: &FontHandle,
index: usize,
sheet: usize,
) -> Result<(), Error> {
let flow = TextFlow::new(text, 8.0);
let line = format!(
"{} {}/{} \u{2014} {} \u{2014} {} {}/{}",
words.letter,
index + 1,
SHEETS.len(),
words.recipients[index],
words.sheet,
sheet + 1,
SHEETS[index]
);
c.set_fill(Rgb::gray(0.45))?;
c.begin_text();
flow.draw(c, &flow.break_lines(&line, WIDTH), LEFT, 56.0, WIDTH)?;
c.end_text();
c.set_fill(Rgb::gray(0.0))?;
Ok(())
}
/// Draws the index sheet, and hands back the links its entries carry.
fn index_sheet(words: &Words, text: &FontHandle) -> Result<(Vec<u8>, Vec<Link>), Error> {
let mut c = Content::new();
let mut top = block(&mut c, text, 17.0, 782.0, words.title)? - 12.0;
top = block(&mut c, text, 9.5, top, words.lead)? - 26.0;
top = block(&mut c, text, 13.0, top, words.index_heading)? - 14.0;
let mut links = Vec::with_capacity(SHEETS.len());
for (index, first) in first_sheets().into_iter().enumerate() {
let (below, covered) = index_line(&mut c, text, top, &entry(words, index))?;
// The link names the part that claims that sheet, not the sheet: the
// recipient is what a reader is after, and the part is what says where
// the recipient begins.
links.push(covered.link(LinkTarget::Part(first)));
top = below - 7.0;
}
c.set_fill(Rgb::gray(0.35))?;
block(&mut c, text, 8.5, top - 20.0, words.note)?;
c.set_fill(Rgb::gray(0.0))?;
Ok((c.into_bytes(), links))
}
/// Draws the sheet one letter is written on.
fn letter_sheet(words: &Words, text: &FontHandle, index: usize) -> Result<Vec<u8>, Error> {
let mut c = Content::new();
let mut top = block(&mut c, text, 13.0, 782.0, words.recipients[index])? - 6.0;
let whereabouts = format!("{}, {}", words.towns[index], words.country);
top = block(&mut c, text, 9.0, top, &whereabouts)? - 22.0;
let heading = format!("{} {}", words.reference, REFERENCES[index]);
top = block(&mut c, text, 9.0, top, &heading)? - 6.0;
let about = format!("{} {}", words.subject, words.subjects[index]);
top = block(&mut c, text, 10.0, top, &about)? - 20.0;
top = block(&mut c, text, 9.5, top, words.greeting)? - 12.0;
top = block(&mut c, text, 9.5, top, words.body)? - 22.0;
top = block(&mut c, text, 9.5, top, words.closing)? - 20.0;
top = block(&mut c, text, 9.5, top, words.signer)? - 4.0;
block(&mut c, text, 8.5, top, words.signer_role)?;
foot(&mut c, words, text, index, 0)?;
Ok(c.into_bytes())
}
/// Draws the sheet enclosed with the first letter.
fn enclosure_sheet(words: &Words, text: &FontHandle) -> Result<Vec<u8>, Error> {
let mut c = Content::new();
let top = block(&mut c, text, 13.0, 782.0, words.enclosure_title)? - 14.0;
block(&mut c, text, 9.5, top, words.enclosure)?;
foot(&mut c, words, text, 0, 1)?;
Ok(c.into_bytes())
}
/// What one letter states about itself, for whatever sends it.
fn stated(words: &Words, index: usize) -> Result<PartData, Error> {
PartData::new()
.set("Recipient", words.recipients[index])?
.set("Town", words.towns[index])?
.set("Reference", REFERENCES[index])?
.set("Sheets", SHEETS[index])
}
/// The hierarchy: the index, then one part per recipient, each taking the
/// sheets that follow rather than naming them.
fn hierarchy(words: &Words) -> Result<DocumentParts, Error> {
let mut run = DocumentPart::new().child(
DocumentPart::new()
.next_pages(1)
.data(PartData::new().set("Section", "Index")?),
);
for (index, run_of) in SHEETS.iter().enumerate() {
run = run.child(
DocumentPart::new()
.next_pages(usize::from(*run_of))
.data(stated(words, index)?),
);
}
Ok(DocumentParts::new(run))
}
/// Draws the index and the four letters, and says which sheets belong to whom.
fn build(words: &Words, font: &Path) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut doc = Document::new();
doc.set_license(licence::licensed());
// The hierarchy of document parts, and the action that leads to one, are
// written on PDF 2.0 and on nothing earlier.
doc.set_version(Version::V2_0);
let text = doc.add_font(Font::parse(fs::read(font)?)?);
let (drawn, links) = index_sheet(words, &text)?;
let mut index = Page::a4();
index.content = drawn;
index.links = links;
doc.add_page(index)?;
for (letter, run_of) in SHEETS.iter().enumerate() {
let mut sheet = Page::a4();
sheet.content = letter_sheet(words, &text, letter)?;
doc.add_page(sheet)?;
if *run_of > 1 {
let mut enclosed = Page::a4();
enclosed.content = enclosure_sheet(words, &text)?;
doc.add_page(enclosed)?;
}
}
doc.set_document_parts(hierarchy(words)?);
Ok(doc.to_bytes()?)
}
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("one_letter_each")));
let font_path = args.next().map_or_else(default_font, PathBuf::from);
let bytes = build(words, &font_path)?;
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::{Font, FontHandle};
use super::{REFERENCES, SHEETS, WIDTH, WORDS, default_font, first_sheets, language};
/// The font the pages are set in, to measure what they draw.
fn handle() -> FontHandle {
let mut doc = hqf_pdf::Document::new();
doc.add_font(
Font::parse(std::fs::read(default_font()).expect("the test font")).expect("a font"),
)
}
/// The lines two languages are allowed to write the same way: the person
/// who signs the letters has the same name in both.
const SPARED: [&str; 12] = [
"signer: \"R. Alderton\"",
// Written the same way in German.
r#""Baker and Sons""#,
r#""Bristol""#,
r#""Corran Bookbinders""#,
r#""Halden Joinery""#,
r#""Kendal""#,
r#""Oban""#,
r#""Truro""#,
r#""Westmill Dairy""#,
// Written the same way in Portuguese.
r#"country: "Reino Unido""#,
r#"letter: "Carta""#,
// Written the same way in Dutch.
r#"letter: "Brief""#,
];
#[test]
fn every_language_writes_the_run_in_its_own_words() {
let untranslated = language::untranslated_lines(&WORDS, &SPARED);
assert!(
untranslated.is_empty(),
"the pages say these in more than one language: {untranslated:?}"
);
}
#[test]
fn the_index_leads_to_the_sheet_each_letter_opens_on() {
let first = first_sheets();
// The index is sheet zero, so the first letter opens on sheet one and
// each one after it opens where the one before it stopped.
assert_eq!(first, [1, 3, 4, 5]);
for (index, opening) in first.into_iter().enumerate() {
let claimed = opening + usize::from(SHEETS[index]);
assert!(
index + 1 == SHEETS.len() || first[index + 1] == claimed,
"letter {index} ends at {claimed} and the next opens elsewhere"
);
}
}
#[test]
fn every_letter_has_a_recipient_a_town_a_subject_and_a_reference() {
for (_, words) in &WORDS {
assert_eq!(words.recipients.len(), SHEETS.len());
assert_eq!(words.towns.len(), SHEETS.len());
assert_eq!(words.subjects.len(), SHEETS.len());
}
assert_eq!(REFERENCES.len(), SHEETS.len());
}
#[test]
fn every_entry_of_the_index_is_set_on_one_line() {
let font = handle();
for (_, words) in &WORDS {
for index in 0..SHEETS.len() {
let entry = super::entry(words, index);
assert!(
font.measure(&entry, 10.5) <= WIDTH,
"{entry:?} needs {} of {WIDTH}",
font.measure(&entry, 10.5)
);
}
}
}
#[test]
fn every_subject_a_letter_states_fits_the_line_it_is_set_on() {
let font = handle();
for (_, words) in &WORDS {
for subject in words.subjects {
let line = format!("{} {subject}", words.subject);
assert!(
font.measure(&line, 10.0) <= WIDTH,
"{line:?} needs {} of {WIDTH}",
font.measure(&line, 10.0)
);
}
}
}
}
|