write_articles.rs

Die Rust-Datei des Beispiels „Eine Geschichte, die weiter hinten fortgesetzt wird“. Zwei Artikel über zwei Seiten: Jeder beginnt in einer Spalte der Titelseite und geht in einer Spalte der zweiten weiter, und die beiden tauschen unterwegs die Seiten. Die Datei sagt, welche Spalte auf welche folgt, sodass die Anzeigesoftware mit dem Artikel fortfährt, bei dem der Leser ist.

Rust 624 Zeilen

Wofür dieses Beispiel da ist

Eine Zeitung bringt drei Geschichten auf ihre Titelseite und beendet keine davon dort. Auf Papier kommt das Auge zurecht: Man liest bis zum Fuß einer Spalte, blättert um und sucht die Überschrift wieder. Am Bildschirm ist diese Suche schlimmer, denn die Spalte unter der, die man gerade gelesen hat, gehört zu einer anderen Geschichte und sieht genauso aus.

Was dieses Beispiel zeigt

  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
//! Writes a two-story newsletter whose reading order is not its page order.
//!
//! Each story begins in a column of the front page and carries on in a column
//! of the second — and the two swap sides on the way, so the column below the
//! one being read is the other story. Nothing drawn on the page says which
//! carries on which. The articles say it: each names the patches of page it is
//! read through, in order, and reading software that follows one goes from the
//! foot of a patch to the head of the next wherever it falls.
//!
//! Both stories are held in `Words`, once per language, and `HQF_PDF_LANG`
//! picks which set is set. The grid is not language: the same four columns hold
//! whichever words are poured into them.
//!
//! The pages open two columns wide, the odd pages on the left, so the first
//! spread shows the first two pages side by side.
//!
//! Each article also names the newsletter as its subject and the keywords it is
//! filed under, which reading software's panel shows beside its title and
//! author.
//!
//! Usage: `cargo run --example write_articles -- tmp/articles.pdf [font.ttf]`
//!        `HQF_PDF_LANG=fr cargo run --example write_articles`

use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use hqf_pdf::content::Content;
use hqf_pdf::cos::Name;
use hqf_pdf::layout::Align;
use hqf_pdf::viewer::PageLayout;
use hqf_pdf::{Article, 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 words the newsletter is written in, one set per language.
#[derive(Debug)]
struct Words {
    /// What stands across the head of the front page.
    masthead: &'static str,
    /// The keywords every story is filed under, which reading software's panel
    /// shows beside its title and author.
    keywords: &'static str,
    /// The line under it, which says what the file is showing.
    standfirst: &'static str,
    /// The first story: its heading and the passage it is told in.
    first: Story,
    /// The second story, told beside it.
    second: Story,
    /// What stands after a heading where a story is taken up again.
    continued: &'static str,
}

/// One story of the newsletter.
#[derive(Debug)]
struct Story {
    /// What the reader sees over it, and what the article is called in reading
    /// software's panel.
    heading: &'static str,
    /// Who wrote it.
    author: &'static str,
    /// The passage poured through the story's two columns.
    passage: &'static str,
}

impl Words {
    /// The words the newsletter is written in, in `language`.
    fn of(language: Language) -> &'static Self {
        language::pick(&WORDS, language)
    }

    /// The two stories, in the order they are laid out.
    const fn stories(&self) -> [&Story; 2] {
        [&self.first, &self.second]
    }
}

/// The newsletter in English.
const ENGLISH: Words = Words {
    masthead: "The Thursday Sheet",
    keywords: "newsletter, workshop, weekly",
    standfirst: "Two stories, four columns, and a reading order of their own.",
    first: Story {
        heading: "The bridge reopens",
        author: "Claire Vasseur",
        passage: "The bridge reopened on Tuesday, eleven months after the \
river took the middle of it. The county surveyor walked the deck at dawn and \
declared it sound, which is the only ceremony anyone asked for. Traffic \
crossed within the hour. What took the eleven months was not the deck but the \
two piers under it, each rebuilt from the bed up while the water ran round \
them. The contractor counted nineteen days lost to the weather and none at all \
to the design, which is a rarer thing than it sounds. The old approach road \
stays shut until the spring, so the lorries still go the long way round by the \
mill. The parish council has asked for a footway on the upstream side and been \
told it will wait its turn. Meanwhile the bus takes four minutes off its run \
and the school on the far bank has moved its bell back to where it stood \
before the flood. Nobody has yet said what becomes of the \
temporary crossing downstream, which cost more to keep open than to build and \
which the anglers would like left where it is.",
    },
    second: Story {
        heading: "The harvest, county by county",
        author: "Martin Aubert",
        passage: "The wheat came in ten days early and heavier than anyone \
had allowed for, and the barns of three parishes were full before the second \
week of August. The dry June is what did it, say the growers, and the wet \
April before that. Not every field agrees: the high ground east of the ridge \
lost a third of what it stood to a wind that came through on a single \
afternoon. The merchants have held their price so far, which nobody expects to \
last past the sales, and the millers are already asking what the winter sowing \
will look like on ground this hard. The straw is the other half of the story: \
there is a great deal of it, it is dry, and the men who bale it have been \
working the fields two counties away as well as their own. Two of the larger \
farms have put their second cut off a fortnight in the hope of rain. The \
weighbridge at the co-operative has been open since six every morning and the \
queue at seven is longer than it was last year at nine.",
    },
    continued: "(continued)",
};

/// The newsletter in French.
const FRENCH: Words = Words {
    masthead: "La Feuille du jeudi",
    keywords: "lettre d’information, atelier, hebdomadaire",
    standfirst: "Deux articles, quatre colonnes et un ordre de lecture qui leur est propre.",
    first: Story {
        heading: "Le pont rouvre",
        author: "Claire Vasseur",
        passage: "Le pont a rouvert mardi, onze mois après que la rivière en a emporté le \
milieu. L’ingénieur des routes a parcouru le tablier à l’aube et l’a déclaré \
sûr : c’est la seule cérémonie que l’on ait réclamée. La circulation a repris \
dans l’heure. Ce qui a pris onze mois, ce n’est pas le tablier mais les deux \
piles qui le soutiennent, refaites depuis le lit pendant que l’eau courait \
autour. L’entreprise a compté dix-neuf jours perdus à cause du temps et pas un \
à cause des plans, ce qui est plus rare qu’il n’y paraît. L’ancienne route \
d’accès reste fermée jusqu’au printemps : les camions font donc encore le \
détour par le moulin. Le conseil municipal a demandé un trottoir côté amont ; \
on lui a répondu que ce projet attendrait son tour. En attendant, le car gagne \
quatre minutes sur son trajet et l’école de l’autre rive a remis sa sonnerie à \
l’heure qu’elle avait avant la crue. Personne n’a encore dit ce que deviendra \
la passerelle provisoire en aval, qui a coûté plus cher à entretenir qu’à \
monter et que les pêcheurs aimeraient voir rester en place.",
    },
    second: Story {
        heading: "La moisson, canton par canton",
        author: "Martin Aubert",
        passage: "Le blé est rentré dix jours en avance et plus lourd que prévu, et les granges \
de trois communes étaient pleines avant la deuxième semaine d’août. C’est la \
sécheresse de juin qui l’explique, disent les cultivateurs, et avant elle les \
pluies d’avril. Toutes les parcelles n’en disent pas autant : les hauteurs à \
l’est de la crête ont perdu un tiers de leur récolte sur pied sous un coup de \
vent d’un seul après-midi. Les négociants tiennent leur prix pour l’instant, \
ce qui, de l’avis général, ne durera pas au-delà des ventes, et les meuniers \
se demandent déjà ce que donneront les semis d’hiver sur une terre aussi dure. \
La paille est l’autre moitié de l’affaire : il y en a beaucoup, elle est \
sèche, et ceux qui la mettent en balles travaillent jusqu’à deux cantons plus \
loin en plus de leurs propres champs. Deux des plus grosses fermes ont \
repoussé leur seconde coupe de quinze jours dans l’espoir qu’il pleuve. Le \
pont-bascule de la coopérative ouvre chaque matin à six heures, et la file de \
sept heures est plus longue que celle de neuf heures l’an dernier.",
    },
    continued: "(suite)",
};

/// The newsletter in Dutch.
const DUTCH: Words = Words {
    masthead: "Het Donderdagblad",
    keywords: "nieuwsbrief, werkplaats, wekelijks",
    standfirst: "Twee verhalen, vier kolommen en een eigen leesvolgorde.",
    first: Story {
        heading: "De brug gaat weer open",
        author: "Claire Vasseur",
        passage: "De brug is dinsdag weer opengegaan, elf maanden nadat de rivier het middenstuk had meegenomen. De provinciale landmeter liep bij dageraad over het dek en verklaarde het veilig, de enige plechtigheid waar iemand om had gevraagd. Binnen een uur reed het verkeer eroverheen. Wat de elf maanden kostte, was niet het dek maar de twee pijlers eronder, elk vanaf de bedding opnieuw gebouwd terwijl het water eromheen stroomde. De aannemer telde negentien dagen verlies door het weer en geen enkele door het ontwerp. De oude toegangsweg blijft tot het voorjaar dicht, dus de vrachtwagens rijden nog om via de molen. De gemeenteraad vroeg om een voetpad aan de stroomopwaartse kant en moet op zijn beurt wachten. Intussen wint de bus vier minuten en heeft de school aan de overkant de bel teruggehangen waar hij vóór de overstroming hing. Niemand heeft nog gezegd wat er gebeurt met de noodoversteek stroomafwaarts, die meer kostte om open te houden dan om te bouwen en die de vissers graag zouden laten liggen waar hij ligt.",
    },
    second: Story {
        heading: "De oogst, streek per streek",
        author: "Martin Aubert",
        passage: "De tarwe kwam tien dagen vroeg binnen en zwaarder dan iemand had verwacht, en de schuren van drie parochies zaten vol vóór de tweede week van augustus. Het was de droge juni, zeggen de telers, en de natte april daarvoor. Niet elk veld is het daarmee eens: het hoge land ten oosten van de heuvelrug verloor een derde van wat er stond door een wind die op één middag langskwam. De handelaren hebben hun prijs tot nu toe vastgehouden, wat niemand na de veilingen nog verwacht, en de molenaars vragen zich al af hoe de wintertarwe op zulke harde grond zal staan. Het stro is de andere helft van het verhaal: er is veel van, het is droog, en de mannen die het persen hebben naast hun eigen velden ook die van twee streken verderop gedaan. Twee van de grotere boerderijen hebben hun tweede snede twee weken uitgesteld in de hoop op regen. De weegbrug van de coöperatie is elke ochtend vanaf zes uur open en de rij om zeven is langer dan vorig jaar om negen.",
    },
    continued: "(vervolg)",
};

/// The newsletter in Portuguese.
const PORTUGUESE: Words = Words {
    masthead: "A Folha de Quinta",
    keywords: "boletim, oficina, semanal",
    standfirst: "Duas notícias, quatro colunas e uma ordem de leitura própria.",
    first: Story {
        heading: "A ponte reabre",
        author: "Claire Vasseur",
        passage: "A ponte reabriu na terça-feira, onze meses depois de o rio lhe ter levado o tramo central. O engenheiro do concelho percorreu o tabuleiro de madrugada e declarou-o sólido, a única cerimónia que alguém pediu. O trânsito passou em menos de uma hora. O que levou os onze meses não foi o tabuleiro mas os dois pilares por baixo, refeitos a partir do leito enquanto a água corria à volta. O empreiteiro contou dezanove dias perdidos com o tempo e nenhum com o projeto, coisa mais rara do que parece. A antiga estrada de acesso continua fechada até à primavera, por isso os camiões continuam a dar a volta pelo moinho. A junta pediu um passeio do lado de montante e ouviu que terá de esperar a sua vez. Entretanto o autocarro ganha quatro minutos no percurso e a escola da outra margem pôs o sino de volta onde estava antes da cheia. Ninguém disse ainda o que será da passagem provisória a jusante, que custou mais a manter aberta do que a construir e que os pescadores gostariam de deixar onde está.",
    },
    second: Story {
        heading: "A colheita, concelho a concelho",
        author: "Martin Aubert",
        passage: "O trigo chegou dez dias mais cedo e mais pesado do que se previa, e os celeiros de três freguesias estavam cheios antes da segunda semana de agosto. Foi o junho seco, dizem os agricultores, e o abril chuvoso antes dele. Nem todos os campos concordam: as terras altas a leste da serra perderam um terço do que tinham com um vento que passou numa só tarde. Os comerciantes mantiveram o preço até agora, coisa que ninguém espera que dure para lá das vendas, e os moleiros já perguntam como será a sementeira de inverno numa terra tão dura. A palha é a outra metade da história: há muita, está seca, e quem a enfarda tem trabalhado, além dos seus, os campos de dois concelhos vizinhos. Duas das maiores herdades adiaram quinze dias o segundo corte à espera de chuva. A báscula da cooperativa abre às seis todas as manhãs e a fila às sete é mais longa do que no ano passado às nove.",
    },
    continued: "(continua)",
};

/// The newsletter in Italian.
const ITALIAN: Words = Words {
    masthead: "Il Foglio del giovedì",
    keywords: "notiziario, laboratorio, settimanale",
    standfirst: "Due articoli, quattro colonne e un ordine di lettura tutto loro.",
    first: Story {
        heading: "Il ponte riapre",
        author: "Claire Vasseur",
        passage: "Il ponte ha riaperto martedì, undici mesi dopo che il fiume se ne era portato via la parte centrale. Il geometra della provincia ha percorso l’impalcato all’alba e lo ha dichiarato sicuro, unica cerimonia che qualcuno avesse chiesto. Il traffico è passato entro un’ora. Gli undici mesi non se li è presi l’impalcato ma le due pile sottostanti, ricostruite dal letto del fiume mentre l’acqua scorreva loro intorno. L’impresa ha contato diciannove giorni persi per il maltempo e nessuno per il progetto, cosa più rara di quanto sembri. La vecchia strada d’accesso resta chiusa fino alla primavera, così i camion fanno ancora il giro lungo dal mulino. Il consiglio comunale ha chiesto un marciapiede sul lato a monte e si è sentito dire che aspetterà il suo turno. Intanto l’autobus guadagna quattro minuti sul percorso e la scuola dell’altra sponda ha rimesso la campana dov’era prima dell’alluvione. Nessuno ha ancora detto che ne sarà del passaggio provvisorio a valle: tenerlo aperto è costato più che costruirlo, e i pescatori vorrebbero lasciarlo dov’è.",
    },
    second: Story {
        heading: "Il raccolto, provincia per provincia",
        author: "Martin Aubert",
        passage: "Il grano è arrivato con dieci giorni di anticipo e più pesante del previsto, e i granai di tre paesi erano pieni prima della seconda settimana di agosto. È stato il giugno secco, dicono i coltivatori, e l’aprile piovoso prima. Non tutti i campi sono d’accordo: le terre alte a est del crinale hanno perso un terzo di quanto avevano per un vento passato in un solo pomeriggio. I commercianti hanno tenuto il prezzo finora, cosa che nessuno si aspetta duri oltre le vendite, e i mugnai già si chiedono come sarà la semina invernale su una terra così dura. La paglia è l’altra metà della storia: ce n’è molta, è asciutta, e chi la imballa ha lavorato i campi di due province più in là oltre ai propri. Due delle aziende più grandi hanno rinviato di quindici giorni il secondo taglio sperando nella pioggia. La pesa della cooperativa apre alle sei ogni mattina e la coda alle sette è più lunga di quella dell’anno scorso alle nove.",
    },
    continued: "(continua)",
};

/// The newsletter in Spanish.
const SPANISH: Words = Words {
    masthead: "La Hoja del Jueves",
    keywords: "boletín, taller, semanal",
    standfirst: "Dos artículos, cuatro columnas y un orden de lectura propio.",
    first: Story {
        heading: "El puente reabre",
        author: "Claire Vasseur",
        passage: "El puente reabrió el martes, once meses después de que el río se llevara su tramo central. El aparejador del condado recorrió el tablero al amanecer y lo declaró sólido, única ceremonia que nadie pidió. El tráfico cruzó en menos de una hora. Lo que costó los once meses no fue el tablero sino las dos pilas de debajo, rehechas desde el lecho mientras el agua corría a su alrededor. El contratista contó diecinueve días perdidos por el tiempo y ninguno por el proyecto, cosa más rara de lo que parece. La antigua carretera de acceso sigue cerrada hasta la primavera, así que los camiones siguen dando el rodeo por el molino. El ayuntamiento pidió una acera en el lado de aguas arriba y le dijeron que esperaría su turno. Mientras tanto el autobús ahorra cuatro minutos en su recorrido y la escuela de la otra orilla ha vuelto a poner su campana donde estaba antes de la riada. Nadie ha dicho aún qué será del paso provisional de aguas abajo, que costó más mantener abierto que construir y que los pescadores querrían dejar donde está.",
    },
    second: Story {
        heading: "La cosecha, comarca por comarca",
        author: "Martin Aubert",
        passage: "El trigo llegó diez días antes y más pesado de lo previsto, y los graneros de tres parroquias estaban llenos antes de la segunda semana de agosto. Fue el junio seco, dicen los agricultores, y el abril lluvioso de antes. No todos los campos opinan igual: las tierras altas al este de la loma perdieron un tercio de lo que tenían por un viento que pasó en una sola tarde. Los comerciantes han mantenido su precio por ahora, cosa que nadie espera que dure más allá de las ventas, y los molineros ya preguntan cómo será la siembra de invierno en una tierra tan dura. La paja es la otra mitad de la historia: hay mucha, está seca, y los que la empacan han trabajado los campos de dos comarcas más allá además de los suyos. Dos de las fincas mayores han retrasado quince días su segundo corte con la esperanza de que llueva. La báscula de la cooperativa abre a las seis cada mañana y la cola a las siete es más larga que el año pasado a las nueve.",
    },
    continued: "(continúa)",
};

/// The newsletter in German.
const GERMAN: Words = Words {
    masthead: "Das Donnerstagsblatt",
    keywords: "Wöchentlicher Rundbrief der Werkstatt",
    standfirst: "Zwei Berichte, vier Spalten und eine eigene Lesereihenfolge.",
    first: Story {
        heading: "Die Brücke ist wieder offen",
        author: "Claire Vasseur",
        passage: "Die Brücke ist seit Dienstag wieder offen, elf Monate nachdem der Fluss ihr Mittelstück mitriss. Der Kreisbaumeister schritt im Morgengrauen die Fahrbahn ab und erklärte sie für tragfähig; mehr Feier hatte niemand verlangt. Binnen einer Stunde rollte der Verkehr. Die elf Monate steckten nicht in der Fahrbahn, sondern in den zwei Pfeilern darunter, vom Flussbett an neu gebaut, während das Wasser um sie strömte. Die Baufirma zählte neunzehn Tage Verlust durch das Wetter und keinen durch den Entwurf. Die alte Zufahrt bleibt bis zum Frühjahr gesperrt, also fahren die Lastwagen weiter den Umweg über die Mühle. Der Gemeinderat will einen Fußweg auf der Seite flussaufwärts und muss warten, bis er an der Reihe ist. Der Bus spart vier Minuten, und die Schule am anderen Ufer läutet ihre Glocke wieder wie vor dem Hochwasser. Was aus dem Behelfsübergang flussabwärts wird, hat noch niemand gesagt; die Angler hätten ihn gern behalten.",
    },
    second: Story {
        heading: "Die Ernte, Kreis für Kreis",
        author: "Martin Aubert",
        passage: "Der Weizen kam zehn Tage früher und schwerer als erwartet, und die Scheunen dreier Gemeinden waren vor der zweiten Augustwoche voll. Das habe der trockene Juni bewirkt, sagen die Bauern, und davor der nasse April. Nicht jedes Feld bestätigt das: Die Höhen östlich des Kamms verloren ein Drittel ihrer Ernte an einen Sturm, der an einem einzigen Nachmittag durchzog. Die Händler halten ihren Preis bisher, was nach den Auktionen niemand mehr erwartet hatte, und die Müller fragen schon nach der Wintersaat auf so hartem Boden. Das Stroh ist die andere Hälfte der Ernte: Es gibt viel davon, es ist trocken, und die Pressen arbeiten auch zwei Kreise weiter. Zwei größere Höfe haben ihren zweiten Schnitt um vierzehn Tage verschoben, in der Hoffnung auf Regen. Die Waage der Genossenschaft öffnet jeden Morgen um sechs, und die Schlange um sieben ist länger als im Vorjahr um neun.",
    },
    continued: "(Fortsetzung)",
};

/// 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 the column on the left of a page.
const LEFT: f64 = 70.0;
/// The left edge of the column on the right of a page.
const RIGHT: f64 = 310.0;
/// The width of one column.
const COLUMN: f64 = 215.0;
/// The top edge of the two columns of the front page, which the masthead stands
/// above.
const FRONT_TOP: f64 = 700.0;
/// The top edge of the two columns of the second page.
const INSIDE_TOP: f64 = 760.0;
/// How far down from its top edge a column reaches.
const HEIGHT: f64 = 190.0;
/// The size the passages are set at, in points.
const SIZE: f64 = 9.0;
/// The distance from one baseline to the next, in points.
const LEADING: f64 = 12.5;
/// The size a story's heading is set at, in points, or less where the heading
/// would not fit across its column.
const HEADING_SIZE: f64 = 12.0;
/// How much of a column's height the heading and the line under it take.
const HEADING_ROOM: f64 = 26.0;

/// One patch of page a story is read through: which page, and where on it.
#[derive(Debug, Clone, Copy)]
struct Patch {
    /// The page it is on, counting from zero.
    page: usize,
    /// The left edge of the column, in points.
    x: f64,
    /// The top edge of the column, in points up from the foot of the sheet.
    top: f64,
}

impl Patch {
    /// Where the passage starts, under the room the heading takes.
    const fn text_top(self) -> f64 {
        self.top - HEADING_ROOM
    }

    /// How much of the column the passage has, once the heading has had its
    /// room.
    const fn text_height() -> f64 {
        HEIGHT - HEADING_ROOM
    }
}

/// The two patches the first story is read through: the left of the front page,
/// then the right of the second.
const FIRST: [Patch; 2] = [
    Patch {
        page: 0,
        x: LEFT,
        top: FRONT_TOP,
    },
    Patch {
        page: 1,
        x: RIGHT,
        top: INSIDE_TOP,
    },
];

/// The two patches the second story is read through, which cross the first
/// story's on the way: the right of the front page, then the left of the
/// second.
const SECOND: [Patch; 2] = [
    Patch {
        page: 0,
        x: RIGHT,
        top: FRONT_TOP,
    },
    Patch {
        page: 1,
        x: LEFT,
        top: INSIDE_TOP,
    },
];

/// The patches each story is read through, in the order the stories are laid
/// out.
const fn patches() -> [[Patch; 2]; 2] {
    [FIRST, SECOND]
}

/// The heading one patch carries: the story's own on the patch it begins in,
/// and the story's own with a word after it on every later patch.
fn heading(words: &Words, story: &Story, first: bool) -> String {
    if first {
        story.heading.to_owned()
    } else {
        format!("{} {}", story.heading, words.continued)
    }
}

/// Draws the masthead across the head of the front page.
fn masthead(content: &mut Content, font: &FontHandle, words: &Words) -> Result<(), hqf_pdf::Error> {
    content.begin_text();
    content.set_font(font, 22.0)?;
    content.text_origin(LEFT, 780.0)?;
    content.show_glyphs(&font.glyphs(words.masthead));
    content.end_text();

    content.begin_text();
    content.set_font(font, SIZE)?;
    content.text_origin(LEFT, 762.0)?;
    content.show_glyphs(&font.glyphs(words.standfirst));
    content.end_text();

    content
        .set_line_width(1.0)?
        .move_to(LEFT, 752.0)?
        .line_to(RIGHT + COLUMN, 752.0)?
        .stroke();
    Ok(())
}

/// Draws one patch: its frame, its heading, and the slice of the passage it
/// holds. Returns how many lines it took.
fn draw_patch(
    content: &mut Content,
    font: &FontHandle,
    flow: &TextFlow<'_>,
    patch: Patch,
    title: &str,
    lines: &[hqf_pdf::layout::Line],
) -> Result<usize, hqf_pdf::Error> {
    content.save_state();
    content
        .set_stroke_rgb(0.75, 0.75, 0.75)?
        .set_line_width(0.5)?
        .rect(patch.x, patch.top - HEIGHT, COLUMN, HEIGHT)?
        .stroke();
    content.restore_state();

    content.begin_text();
    content.set_font(font, font.fit(title, COLUMN - 12.0, HEADING_SIZE))?;
    content.text_origin(patch.x + 6.0, patch.top - 16.0)?;
    content.show_glyphs(&font.glyphs(title));
    content.end_text();

    let taken = flow.fit(lines, Patch::text_height(), 0);
    content.begin_text();
    flow.draw(
        content,
        &lines[..taken],
        patch.x + 6.0,
        patch.text_top(),
        COLUMN - 12.0,
    )?;
    content.end_text();
    Ok(taken)
}

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("articles")));
    let font_path = args.next().map_or_else(default_font, PathBuf::from);

    let mut doc = Document::new();
    doc.set_license(licence::licensed());
    doc.set_info(Name::new("Title"), words.masthead);
    let font = doc.add_font(Font::parse(fs::read(&font_path)?)?);
    let flow = TextFlow::new(&font, SIZE)
        .leading(LEADING)
        .align(Align::Justify);

    // One content stream per page, drawn into as each story reaches it.
    let mut sheets = [Content::new(), Content::new()];
    masthead(&mut sheets[0], &font, words)?;

    for (story, patches) in words.stories().into_iter().zip(patches()) {
        let broken = flow.break_lines(story.passage, COLUMN - 12.0);
        let mut from = 0;
        for (index, patch) in patches.into_iter().enumerate() {
            let taken = draw_patch(
                &mut sheets[patch.page],
                &font,
                &flow,
                patch,
                &heading(words, story, index == 0),
                &broken[from..],
            )?;
            from += taken;
        }
        let article = patches.into_iter().fold(
            Article::new(story.heading)
                .with_author(story.author)
                .with_subject(words.masthead)
                .with_keywords(words.keywords),
            |article, patch| {
                article.on_page(patch.page, patch.x, patch.top - HEIGHT, COLUMN, HEIGHT)
            },
        );
        println!(
            "{}: {} patches, keywords {}",
            article.title(),
            article.patches(),
            article.keywords().unwrap_or_default()
        );
        doc.add_article(article);
    }

    for content in sheets {
        let mut page = Page::a4();
        page.content = content.into_bytes();
        doc.add_page(page)?;
    }

    doc.set_page_layout(PageLayout::TwoColumnsOddLeft);

    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::layout::Align;
    use hqf_pdf::{Document, Font, TextFlow};

    use super::{COLUMN, LEADING, Patch, SIZE, WORDS, Words, default_font, language, patches};

    /// The lines two languages are allowed to write the same way: the two
    /// bylines, which are the names of people rather than words to translate.
    const SPARED: [&str; 3] = [
        "author: \"Claire Vasseur\"",
        "author: \"Martin Aubert\"",
        // Written the same way in Portuguese.
        r#"continued: "(continua)""#,
    ];

    /// How many of a passage's lines each of a story's patches takes, and how
    /// many the passage broke into.
    fn poured(passage: &str) -> (usize, Vec<usize>) {
        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 flow = TextFlow::new(&handle, SIZE)
            .leading(LEADING)
            .align(Align::Justify);
        let lines = flow.break_lines(passage, COLUMN - 12.0);

        let mut from = 0;
        let mut taken = Vec::new();
        for _ in 0..2 {
            let fitted = flow.fit(&lines[from..], Patch::text_height(), 0);
            taken.push(fitted);
            from += fitted;
        }
        (lines.len(), taken)
    }

    #[test]
    fn every_language_writes_the_newsletter_in_its_own_words() {
        let untranslated = language::untranslated_lines(&WORDS, &SPARED);

        assert!(
            untranslated.is_empty(),
            "the newsletter says these in more than one language: {untranslated:?}"
        );
    }

    /// A story that stops inside its first patch never reaches the second, and
    /// the article would lead a reader to an empty column; one longer than the
    /// two loses its last lines with nothing on the page to say so.
    #[test]
    fn every_story_fills_both_of_its_patches_and_no_more() {
        for (named, words) in WORDS {
            for story in words.stories() {
                let (lines, taken) = poured(story.passage);
                let code = named.code();

                assert!(
                    taken[1] > 0,
                    "the {code} story {:?} never reaches its second column",
                    story.heading
                );
                assert_eq!(
                    taken.iter().sum::<usize>(),
                    lines,
                    "the {code} story {:?} leaves lines under the foot of its \
                     last column",
                    story.heading
                );
            }
        }
    }

    /// The two stories cross on the way: the column a story carries on in is
    /// not the column under the one it began in, which is the whole reason the
    /// file states a reading order at all.
    #[test]
    fn the_two_stories_swap_sides_between_the_pages() {
        let [first, second] = patches();

        assert_eq!(first[0].page, second[0].page);
        assert_eq!(first[1].page, second[1].page);
        assert!((first[0].x - second[1].x).abs() < f64::EPSILON);
        assert!((second[0].x - first[1].x).abs() < f64::EPSILON);
    }

    /// Every story names an author, which the thread carries beside its title.
    #[test]
    fn every_story_is_signed() {
        for (_, words) in WORDS {
            for story in words.stories() {
                assert!(!story.author.is_empty());
            }
        }
    }

    /// The words a language writes are the words the file is set in, whichever
    /// language that is.
    #[test]
    fn every_language_names_both_stories() {
        for (_, words) in WORDS {
            let [first, second]: [&super::Story; 2] = Words::stories(words);
            assert_ne!(first.heading, second.heading);
        }
    }
}