write_stamp.rs

Die Rust-Datei des Beispiels „An ein Rechteck angepasste Stempel“. Was ein Dokument über sich sagt — ENTWURF, KOPIE, BEZAHLT —, über Rechtecke von vier verschiedenen Formen gelegt und an jedes angepasst, dann MUSTER, dem Dokument einmal gegeben und auf jede gerade Seite gelegt.

Rust 644 Zeilen

Wofür dieses Beispiel da ist

ENTWURF, KOPIE, BEZAHLT, MUSTER: die Wörter, die ein Dokument über sich selbst sagt, und das Erste, wonach ein Prüfer sucht. Sie müssen unübersehbar sein und quer über dem liegen, was sie kennzeichnen — heute eine ganze Seite, morgen eine Tabellenzelle, nächsten Monat ein übersetztes Wort, anderthalbmal so lang —, ihre Größe kann also nie im Voraus feststehen.

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
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
//! Stamps a page with what it is: DRAFT, COPY, PAID.
//!
//! A stamp is scaled to the box it is given rather than set in a size, so the
//! same line stamps a whole page, a table cell and a form field without a
//! single size being worked out by the caller. It is turned first and scaled
//! second, so that what stays inside the box is the line as the reader sees it.
//! The first page draws four of them across boxes of very different shapes.
//!
//! The stamp takes whatever colour it is given and nothing else: it is text,
//! and a caller that wants it under the page's own ink draws it first.
//!
//! Under each caption, a second line says how far the stamp turned its line.
//!
//! The three pages after it carry watermarks instead, handed to the document
//! once rather than drawn page by page. The first runs from page two to the
//! end, even pages only, set at 96 points whatever the page measures and
//! painted at 35 per cent opacity. The second runs from page three, odd pages
//! only, under the page's own ink, fitted to half the page and a shadow behind
//! it. The third lies flat over every page from the fourth on, small and pale.
//! Each later page says how many of the three it carries, and page three how
//! far its shadow is offset. The last page carries a stamp of another kind
//! besides: APPROVED pressed above the page rather than drawn into it, which a
//! reader can hide, delete or leave off the paper.
//!
//! The stamped words, the title, the four captions, the watermark and the line
//! heading each later page are held in `Words`, once per language, and
//! `HQF_PDF_LANG` picks which set is drawn. Each set keeps its last word the
//! longest, because the last box is the one that shows a long word coming out
//! smaller rather than cut.
//!
//! Usage: `cargo run --example write_stamp -- tmp/stamp.pdf`
//!        `HQF_PDF_LANG=fr cargo run --example write_stamp -- tmp/tampon.pdf`

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

use hqf_pdf::annotation::{Stamp as Pressed, StampIcon};
use hqf_pdf::content::Content;
use hqf_pdf::layout::{PageParity, Shadow, Slant, Stacking, Stamp, Watermark};
use hqf_pdf::{Document, ExtGState, Font, FontHandle, GStateHandle, Page, Rgb};

#[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.
fn default_font() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fonts")
        .join("DejaVuSans.ttf")
}

/// The margin the boxes are laid out inside.
const MARGIN: f64 = 56.0;

/// How many boxes the page stamps.
const BOX_COUNT: usize = 4;

/// How many pages follow the stamped one.
const LATER_PAGES: usize = 3;

/// The size the watermark is set in, in points, on every page it covers.
const WATERMARK_SIZE: f64 = 96.0;

/// The share of its colour the watermark is painted with.
const WATERMARK_ALPHA: f64 = 0.35;

/// One box to stamp: where it is, how big, and which way the line runs.
struct Stamped {
    x: f64,
    y: f64,
    width: f64,
    height: f64,
    slant: Slant,
    color: Rgb,
}

/// The size the first page's title is set in, in points.
const TITLE_SIZE: f64 = 14.0;

/// The size the line heading each later page is set in, in points: a sentence
/// rather than a title, kept inside the margins in every language.
const LATER_SIZE: f64 = 11.0;

/// The colour the stamp drawn above the last page reads in.
const APPROVED_GREEN: Rgb = Rgb::new(0.1, 0.5, 0.2);

/// The boxes, from the top of the page down.
const STAMPED: [Stamped; BOX_COUNT] = [
    Stamped {
        x: MARGIN,
        y: 606.0,
        width: 483.0,
        height: 150.0,
        slant: Slant::Up,
        color: Rgb::new(0.75, 0.16, 0.16),
    },
    Stamped {
        x: MARGIN,
        y: 300.0,
        width: 150.0,
        height: 260.0,
        slant: Slant::Down,
        color: Rgb::new(0.25, 0.35, 0.7),
    },
    Stamped {
        x: 268.0,
        y: 300.0,
        width: 271.0,
        height: 260.0,
        slant: Slant::Flat,
        color: Rgb::new(0.1, 0.45, 0.25),
    },
    Stamped {
        x: MARGIN,
        y: 130.0,
        width: 483.0,
        height: 110.0,
        slant: Slant::Up,
        color: Rgb::new(0.45, 0.45, 0.45),
    },
];

/// The words the page is written in, one set per language.
///
/// The stamped words are language: a reader stamps a document in the language
/// they read it in. Each set keeps its last word the longest, because the last
/// box is the one that shows a long word coming out smaller rather than cut.
#[derive(Debug)]
struct Words {
    /// The line at the head of the page.
    title: &'static str,
    /// What each box is stamped with, in the order `STAMPED` lays them out.
    stamps: [&'static str; BOX_COUNT],
    /// What is written under each box.
    captions: [&'static str; BOX_COUNT],
    /// The watermark laid on the even pages after the first.
    watermark: &'static str,
    /// The watermark laid under the odd pages from the third.
    under: &'static str,
    /// The watermark laid flat over every page from the fourth.
    flat: &'static str,
    /// How far a stamp turned its line; `{degrees}` stands for the angle.
    turned: &'static str,
    /// How many watermarks a page carries; `{count}` stands for them.
    carried: &'static str,
    /// How far the shadow of the second watermark is offset; `{dx}` and `{dy}`
    /// stand for the offsets.
    shadowed: &'static str,
    /// The line at the head of each page after the first.
    later: [&'static str; LATER_PAGES],
    /// The word the stamp drawn above the last page reads.
    approved: &'static str,
    /// What the last page says that stamp is, the first of the two lines a
    /// reader that cannot see the page is told it says.
    above: &'static str,
    /// What the last page says a reader can do with that stamp, the second of
    /// the two lines.
    offered: &'static str,
}

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

/// The page in English.
const ENGLISH: Words = Words {
    title: "One stamp, four boxes",
    stamps: ["DRAFT", "COPY", "PAID", "CANCELLED"],
    captions: [
        "A wide box: the line runs up its diagonal, which is nearly flat.",
        "A tall box, stamped the other way.",
        "Flat across the middle, and wide enough that the width is what binds.",
        "A longer word in a shorter box: the size follows, nothing is cut.",
    ],
    watermark: "SPECIMEN",
    under: "INTERNAL",
    flat: "ARCHIVED",
    turned: "Turned {degrees}°.",
    carried: "Watermarks on this page: {count}.",
    shadowed: "The shadow is offset by ({dx}, {dy}) pt.",
    later: [
        "Page two is even: the first watermark lies across it, at 96 points.",
        "Page three is odd: the second lies under it, fitted to half the page.",
        "Page four is even again: the first once more, and the third over both.",
    ],
    approved: "APPROVED",
    above: "The green stamp “Approved” lies above the page.",
    offered: "It can be hidden, deleted or left off the paper.",
};

/// The page in French.
const FRENCH: Words = Words {
    title: "Un tampon, quatre boîtes",
    stamps: ["BROUILLON", "COPIE", "PAYÉ", "ANNULATION"],
    captions: [
        "Une boîte large : le mot monte le long de sa diagonale, presque à plat.",
        "Une boîte haute, tamponnée dans l’autre sens.",
        "À plat au milieu, et assez large pour que ce soit la largeur qui limite.",
        "Un mot plus long dans une boîte plus basse : la taille suit, rien n’est coupé.",
    ],
    watermark: "SPÉCIMEN",
    under: "INTERNE",
    flat: "ARCHIVÉ",
    turned: "Tourné de {degrees}°.",
    carried: "Filigranes sur cette page : {count}.",
    shadowed: "L’ombre est décalée de ({dx} ; {dy}) pt.",
    later: [
        "La page deux est paire : le premier filigrane la traverse, en corps 96.",
        "La page trois est impaire : le deuxième passe dessous, à demi-page.",
        "La page quatre est paire : le premier à nouveau, et le troisième par-dessus.",
    ],
    approved: "APPROUVÉ",
    above: "Le tampon vert « Approuvé » est posé sur la page.",
    offered: "On peut le masquer, l’effacer ou ne pas l’imprimer.",
};

/// The page in Dutch.
const DUTCH: Words = Words {
    title: "Eén stempel, vier kaders",
    stamps: ["CONCEPT", "KOPIE", "BETAALD", "GEANNULEERD"],
    captions: [
        "Een breed kader: de regel loopt langs de diagonaal, die bijna vlak is.",
        "Een hoog kader, de andere kant op gestempeld.",
        "Vlak door het midden, en breed genoeg dat de breedte beslist.",
        "Een langer woord in een korter kader: de grootte volgt, er wordt niets afgesneden.",
    ],
    watermark: "SPECIMEN",
    under: "INTERN",
    flat: "GEARCHIVEERD",
    turned: "Gedraaid {degrees}°.",
    carried: "Watermerken op deze pagina: {count}.",
    shadowed: "De schaduw is verschoven met ({dx}, {dy}) pt.",
    later: [
        "Pagina twee is even: het eerste watermerk ligt eroverheen, op 96 punten.",
        "Pagina drie is oneven: het tweede ligt eronder, passend op een halve pagina.",
        "Pagina vier is weer even: nog eens het eerste, en het derde over beide.",
    ],
    approved: "GOEDGEKEURD",
    above: "De groene stempel ‘Goedgekeurd’ ligt over de pagina heen.",
    offered: "Je kunt hem verbergen, verwijderen of buiten de afdruk laten.",
};

/// The page in Portuguese.
const PORTUGUESE: Words = Words {
    title: "Um carimbo, quatro caixas",
    stamps: ["RASCUNHO", "CÓPIA", "PAGO", "CANCELADO"],
    captions: [
        "Uma caixa larga: a linha sobe pela diagonal, que é quase plana.",
        "Uma caixa alta, carimbada no outro sentido.",
        "Plano pelo meio, e tão largo que é a largura que manda.",
        "Uma palavra mais longa numa caixa mais curta: o tamanho acompanha, nada é cortado.",
    ],
    watermark: "ESPÉCIME",
    under: "INTERNO",
    flat: "ARQUIVADO",
    turned: "Rodado {degrees}°.",
    carried: "Marcas de água nesta página: {count}.",
    shadowed: "A sombra está deslocada ({dx}, {dy}) pt.",
    later: [
        "A página dois é par: a primeira marca de água atravessa-a, a 96 pontos.",
        "A página três é ímpar: a segunda fica por baixo, ajustada a meia página.",
        "A página quatro volta a ser par: de novo a primeira, e a terceira por cima de ambas.",
    ],
    approved: "APROVADO",
    above: "O carimbo verde «Aprovado» fica por cima da página.",
    offered: "Um leitor de PDF pode ocultá-lo, apagá-lo ou deixá-lo fora da impressão.",
};

/// The page in Italian.
const ITALIAN: Words = Words {
    title: "Un timbro, quattro riquadri",
    stamps: ["BOZZA", "COPIA", "PAGATO", "ANNULLATO"],
    captions: [
        "Un riquadro largo: la riga sale lungo la diagonale, quasi piatta.",
        "Un riquadro alto, timbrato nell’altro senso.",
        "Piatto al centro, e largo abbastanza da essere vincolato dalla larghezza.",
        "Una parola più lunga in un riquadro più corto: il corpo segue, niente è tagliato.",
    ],
    watermark: "CAMPIONE",
    under: "INTERNO",
    flat: "ARCHIVIATO",
    turned: "Ruotato di {degrees}°.",
    carried: "Filigrane su questa pagina: {count}.",
    shadowed: "L’ombra è spostata di ({dx}, {dy}) pt.",
    later: [
        "La pagina due è pari: la prima filigrana la attraversa, a 96 punti.",
        "La pagina tre è dispari: la seconda sta sotto, adattata a mezza pagina.",
        "La pagina quattro è di nuovo pari: ancora la prima, e la terza sopra entrambe.",
    ],
    approved: "APPROVATO",
    above: "Il timbro verde «Approvato» è sovrapposto alla pagina.",
    offered: "Lo si può nascondere, eliminare o escludere dalla stampa.",
};

/// The page in Spanish.
const SPANISH: Words = Words {
    title: "Un sello, cuatro cajas",
    stamps: ["BORRADOR", "COPIA", "PAGADO", "CANCELADO"],
    captions: [
        "Una caja ancha: la línea sube por su diagonal, que es casi plana.",
        "Una caja alta, sellada en el otro sentido.",
        "Plano por el centro, y tan ancho que lo que manda es el ancho.",
        "Una palabra más larga en una caja más corta: el tamaño se adapta, nada se corta.",
    ],
    watermark: "MUESTRA",
    under: "INTERNO",
    flat: "ARCHIVADO",
    turned: "Girado {degrees}°.",
    carried: "Marcas de agua en esta página: {count}.",
    shadowed: "La sombra está desplazada ({dx}, {dy}) pt.",
    later: [
        "La página dos es par: la primera marca de agua la cruza, a 96 puntos.",
        "La página tres es impar: la segunda va debajo, ajustada a media página.",
        "La página cuatro vuelve a ser par: otra vez la primera, y la tercera sobre ambas.",
    ],
    approved: "APROBADO",
    above: "Sobre la página hay un sello verde «Aprobado».",
    offered: "Un lector de PDF puede ocultarlo, eliminarlo o excluirlo de la impresión.",
};

/// The page in German.
const GERMAN: Words = Words {
    title: "Ein Stempel, vier Rahmen",
    stamps: ["ENTWURF", "KOPIE", "BEZAHLT", "STORNIERT"],
    captions: [
        "Ein breiter Rahmen: Die Zeile läuft seine fast flache Diagonale hinauf.",
        "Ein hoher Rahmen, andersherum gestempelt.",
        "Flach durch die Mitte und so breit, dass die Breite die Größe bestimmt.",
        "Ein längeres Wort in einem kürzeren Rahmen: Die Größe folgt, nichts wird abgeschnitten.",
    ],
    watermark: "MUSTER",
    under: "INTERN",
    flat: "ARCHIVIERT",
    turned: "Um {degrees}° gedreht.",
    carried: "Wasserzeichen auf dieser Seite: {count}.",
    shadowed: "Der Schatten ist um ({dx}, {dy}) pt versetzt.",
    later: [
        "Seite zwei ist gerade: Das erste Wasserzeichen liegt quer darüber, in 96 Punkt.",
        "Seite drei ist ungerade: Das zweite liegt darunter, auf die halbe Seite eingepasst.",
        "Seite vier ist wieder gerade: noch einmal das erste, und das dritte über beiden.",
    ],
    approved: "GENEHMIGT",
    above: "Der grüne Stempel „Genehmigt“ liegt über der Seite.",
    offered: "Man kann ihn ausblenden, löschen oder beim Drucken weglassen.",
};

/// 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),
];

/// Draws the outline of a box, so that what the stamp was fitted to can be
/// seen.
fn outline(content: &mut Content, stamped: &Stamped) -> Result<(), hqf_pdf::Error> {
    content.save_state();
    content.set_stroke(Rgb::gray(0.8))?;
    content.set_line_width(0.5)?;
    content.rect(stamped.x, stamped.y, stamped.width, stamped.height)?;
    content.stroke();
    content.restore_state();
    Ok(())
}

/// Writes `text` at the head of a page, `size` points high.
fn heading(
    content: &mut Content,
    font: &FontHandle,
    size: f64,
    text: &str,
) -> Result<(), hqf_pdf::Error> {
    content.begin_text();
    content.set_font(font, size)?;
    content.text_origin(MARGIN, 780.0)?;
    content.show_glyphs(&font.glyphs(text));
    content.end_text();
    Ok(())
}

/// Writes line `row` of the caption under a box, the first one at row zero.
fn caption(
    content: &mut Content,
    font: &FontHandle,
    stamped: &Stamped,
    row: f64,
    text: &str,
) -> Result<(), hqf_pdf::Error> {
    content.save_state();
    content.set_fill(Rgb::gray(0.35))?;
    content.begin_text();
    content.set_font(font, 8.5)?;
    content.text_origin(stamped.x, row.mul_add(-11.0, stamped.y - 14.0))?;
    content.show_glyphs(&font.glyphs(text));
    content.end_text();
    content.restore_state();
    Ok(())
}

/// The three watermarks the pages after the first carry: the first on the even
/// pages from page two, the second under the odd pages from page three with
/// `shadow` behind it, the third flat over every page from page four. The first
/// and the third are drawn under `translucent`.
fn watermarks(
    font: &FontHandle,
    words: &Words,
    translucent: &GStateHandle,
    shadow: &Shadow,
) -> [Watermark; 3] {
    [
        Watermark::new(font, words.watermark)
            .pages(1..)
            .parity(PageParity::Even)
            .size(WATERMARK_SIZE)
            .color(Rgb::new(0.75, 0.16, 0.16))
            .state(translucent),
        Watermark::new(font, words.under)
            .pages(2..)
            .parity(PageParity::Odd)
            .stacking(Stacking::Under)
            .span(0.5)
            .slant(Slant::Down)
            .color(Rgb::gray(0.88))
            .shadow(shadow.clone()),
        Watermark::new(font, words.flat)
            .pages(3..)
            .parity(PageParity::Every)
            .stacking(Stacking::Over)
            .size(28.0)
            .slant(Slant::Flat)
            .color(Rgb::new(0.2, 0.35, 0.65))
            .state(translucent),
    ]
}

/// Writes the two lines saying what the stamp above the last page is, from
/// `baseline` down, and presses that stamp under them, above the page.
fn press_approved(
    page: &mut Page,
    content: &mut Content,
    font: &FontHandle,
    words: &Words,
    mut baseline: f64,
) -> Result<(), hqf_pdf::Error> {
    for text in [words.above, words.offered] {
        content.begin_text();
        content.set_font(font, 10.0)?;
        content.text_origin(MARGIN, baseline)?;
        content.show_glyphs(&font.glyphs(text));
        content.end_text();
        baseline -= 14.0;
    }
    page.annotations.push(
        Pressed::new(MARGIN, baseline - 56.0, 200.0, 56.0)
            .icon(StampIcon::Approved)
            .contents(format!("{} {}", words.above, words.offered))
            .words(
                &Stamp::new(font, words.approved)
                    .slant(Slant::Flat)
                    .color(APPROVED_GREEN),
            )?
            .into(),
    );
    Ok(())
}

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);

    // 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 = env::args()
        .nth(1)
        .unwrap_or_else(|| language.file_name(&out::default_path("stamp")));

    let mut doc = Document::new();
    doc.set_license(licence::licensed());
    let font = doc.add_font(Font::parse(fs::read(default_font())?)?);

    let translucent = doc.add_ext_gstate(ExtGState::new().fill_alpha(WATERMARK_ALPHA));
    let shadow = Shadow::new(3.0, -3.0, Rgb::gray(0.7));
    let marks = watermarks(&font, words, &translucent, &shadow);
    let carried: Vec<usize> = (1..=LATER_PAGES)
        .map(|index| marks.iter().filter(|mark| mark.covers(index)).count())
        .collect();
    for mark in marks {
        doc.add_watermark(mark)?;
    }

    let mut content = Content::new();
    heading(&mut content, &font, TITLE_SIZE, words.title)?;

    for ((stamped, text), caption_text) in STAMPED.iter().zip(words.stamps).zip(words.captions) {
        outline(&mut content, stamped)?;
        Stamp::new(&font, text)
            .slant(stamped.slant)
            .color(stamped.color)
            .draw(
                &mut content,
                stamped.x,
                stamped.y,
                stamped.width,
                stamped.height,
            )?;
        caption(&mut content, &font, stamped, 0.0, caption_text)?;
        let angle = Stamp::new(&font, text)
            .slant(stamped.slant)
            .angle(stamped.width, stamped.height);
        let turned = words
            .turned
            .replace("{degrees}", &format!("{:.0}", angle.to_degrees()));
        caption(&mut content, &font, stamped, 1.0, &turned)?;
    }

    let mut page = Page::a4();
    page.content = content.into_bytes();
    doc.add_page(page)?;

    for (index, line) in words.later.iter().enumerate() {
        let mut content = Content::new();
        heading(&mut content, &font, LATER_SIZE, line)?;
        let mut said = vec![
            words
                .carried
                .replace("{count}", &carried[index].to_string()),
        ];
        if index == 1 {
            said.push(
                words
                    .shadowed
                    .replace("{dx}", &format!("{:.0}", shadow.dx()))
                    .replace("{dy}", &format!("{:.0}", shadow.dy())),
            );
        }
        let mut baseline = 760.0;
        for text in said {
            content.begin_text();
            content.set_font(&font, 10.0)?;
            content.text_origin(MARGIN, baseline)?;
            content.show_glyphs(&font.glyphs(&text));
            content.end_text();
            baseline -= 14.0;
        }
        let mut page = Page::a4();
        if index == LATER_PAGES - 1 {
            press_approved(&mut page, &mut content, &font, words, baseline)?;
        }
        page.content = content.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, {} stamps, {} pages",
        bytes.len(),
        STAMPED.len(),
        1 + LATER_PAGES
    );
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{WORDS, language};

    /// The lines two languages are allowed to write the same way, where two
    /// languages spell a word alike. Every word on the page is a word of the
    /// language it is in.
    const SPARED: [&str; 7] = [
        // Written the same way in Italian.
        r#""COPIA""#,
        r#"under: "INTERNO""#,
        // Written the same way in Portuguese.
        r#""ANULADO""#,
        // Written the same way in Portuguese.
        r#""CANCELADO""#,
        // Written the same way in Dutch.
        r#""KOPIE""#,
        r#"under: "INTERN""#,
        r#"watermark: "SPECIMEN""#,
    ];

    #[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 last box is shorter than the first and takes the longest word: that
    /// is what its caption points at, so a language that lost the ordering
    /// would draw a caption its own page disproves.
    #[test]
    fn every_language_stamps_its_longest_word_in_the_last_box() {
        for (language, words) in WORDS {
            let longest = words
                .stamps
                .iter()
                .map(|stamp| stamp.chars().count())
                .max()
                .expect("a page stamps at least one box");

            assert_eq!(
                words.stamps[words.stamps.len() - 1].chars().count(),
                longest,
                "in {} the last box does not take the longest word",
                language.code()
            );
        }
    }
}