Une page et un tampon dans un tableau

Une feuille de contrôle pour un lot envoyé par quelqu'un d'autre : chaque ligne porte une de ses pages, prise entière dans le fichier où elle est arrivée, à côté d'un tampon dessiné une seule fois et posé sur chaque ligne qui l'a reçu. Les deux s'ajustent à la cellule que le tableau leur laisse, sans réclamer de largeur à leur colonne ni de hauteur à leur ligne.

Cette page est le programme entier, pour celui qui écrit vos logiciels. Il n'y a rien d'autre à y lire. Revenir au document qu'il écrit.

Rust write_form_cells.rs 671 lignes
  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
//! Draws a checking sheet for a pack somebody else sent: one row per page of
//! it, each showing that page and the stamp it was given.
//!
//! Two kinds of form stand in the cells, and they behave alike. The first
//! column holds a page taken whole out of another document; the second holds a
//! stamp drawn here, once, and placed in every row that was given it. Neither
//! asks its column for a width nor its row for a height: each is fitted whole
//! and undistorted into the rectangle the table leaves it, which is the bargain
//! a picture cell makes. Both are given a rectangle taller than their own
//! proportions need, so the width runs out first and each stands in the middle
//! of the height it does not fill.
//!
//! A page brought in belongs to the sheets it is put on rather than to the
//! document, so every page the table lands on is handed the pages it shows.
//!
//! The pack is written by the example a moment before it is read, so this runs
//! on any machine with nothing to fetch.
//!
//! What the sheet says is held in `Words`, once per language, and
//! `HQF_PDF_LANG` picks which set is printed. What a page is called is also
//! what a reader hears in its place: the alternative text of a form cell reads
//! the same field as the line beside it, so the two cannot disagree.
//!
//! Usage: `cargo run --example write_form_cells -- tmp/form_cells.pdf
//! [font.ttf]`
//!        `HQF_PDF_LANG=fr cargo run --example write_form_cells --
//! tmp/checking.pdf`

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

use hqf_pdf::content::Content;
use hqf_pdf::layout::{Cell, ColumnWidth, Columns, Padding, Row, Rule, Stroke, Table, VAlign};
use hqf_pdf::read::{ImportedPage, Reader};
use hqf_pdf::{Document, Drawing, DrawingHandle, Font, FontHandle, Page, Rect, 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 when none is given on the command line.
fn default_font() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fonts")
        .join("DejaVuSans.ttf")
}

/// A4's width, which every page here is drawn across.
const PAGE_WIDTH: f64 = 595.276;

/// The margin the checking sheet and the pack both keep.
const MARGIN: f64 = 56.0;

/// The width left between the margins, which the table is fitted across.
const TABLE_WIDTH: f64 = PAGE_WIDTH - 2.0 * MARGIN;

/// The top of the table on the page, and the lowest it may reach.
const TOP: f64 = 736.0;
const BOTTOM: f64 = 64.0;

/// How many pages the pack holds, which is how many rows the table takes.
const SHEETS: usize = 4;

/// How many stamps are drawn. A row names one of them; two rows may name the
/// same one.
const MARKS: usize = 3;

/// The width of the column the pages are shown in, and of the column the stamps
/// stand in.
const SHEET_COLUMN: f64 = 96.0;
const MARK_COLUMN: f64 = 76.0;

/// The height every row of the checking sheet takes.
const ROW_HEIGHT: f64 = 140.0;

/// The colour the heading is painted on.
const HEAD_FILL: Rgb = Rgb::gray(0.86);

/// The ink the sheet is written in, and the grey of its second lines.
const INK: Rgb = Rgb {
    r: 0.1,
    g: 0.1,
    b: 0.12,
};
const MUTED: Rgb = Rgb {
    r: 0.42,
    g: 0.42,
    b: 0.45,
};

/// The colour the sender's stationery is printed in.
const SENDER_INK: Rgb = Rgb {
    r: 0.11,
    g: 0.33,
    b: 0.55,
};

/// The ink of each stamp, in the order the stamps are drawn.
const MARK_INKS: [Rgb; MARKS] = [
    Rgb {
        r: 0.09,
        g: 0.42,
        b: 0.24,
    },
    Rgb {
        r: 0.65,
        g: 0.38,
        b: 0.04,
    },
    Rgb {
        r: 0.36,
        g: 0.36,
        b: 0.42,
    },
];

/// The size of a stamp, in points, and the size its word is set at.
const STAMP: (f64, f64) = (62.0, 24.0);
const STAMP_TEXT: f64 = 9.0;

/// The size a page of the pack is headed at.
const PACK_TITLE: f64 = 26.0;

/// Who sent the pack, and where they are. A firm writes its name one way,
/// whatever language reads it.
const SENDER_NAME: &str = "Meridian Optics SAS";
const SENDER_TOWN: &str = "18 avenue des Peupliers, 69100 Villeurbanne";

/// What each page of the pack is filed as. A reference is not language.
const REFERENCES: [&str; SHEETS] = ["DN-4471", "PL-4471", "CC-4471", "WB-4471"];

/// The stamp each page was given, in the order the rows are printed. The first
/// page and the third carry the same one, which is drawn once.
const MARK_OF: [usize; SHEETS] = [0, 1, 0, 2];

/// How many lines of the sender's own writing each page of the pack carries.
const LINES_OF: [usize; SHEETS] = [24, 19, 26, 15];

/// The share of the measure each line of the sender's writing runs to, taken in
/// turn: a page of writing is not a block, and the short line ends a paragraph.
const LINE_SHARES: [f64; 6] = [1.0, 0.94, 0.98, 0.9, 0.96, 0.52];

/// The words the checking sheet is printed in, one set per language.
#[derive(Debug)]
struct Words {
    /// The line at the head of the sheet.
    title: &'static str,
    /// The two lines under it, saying what each row shows.
    intro: [&'static str; 2],
    /// The heading of each column, in the order the columns are set.
    headings: [&'static str; 4],
    /// What each page of the pack is called, in the order they arrived. A form
    /// cell carries the same words as the words a reader hears in its place.
    sheets: [&'static str; SHEETS],
    /// What was found on each of them.
    notes: [&'static str; SHEETS],
    /// What each stamp says, which is also what it is called.
    marks: [&'static str; MARKS],
    /// The line under the table, saying what a stamp placed twice costs.
    foot: &'static str,
}

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

/// The checking sheet in English.
const ENGLISH: Words = Words {
    title: "What arrived, page by page",
    intro: [
        "Each row shows a page of the pack as it arrived, and the stamp that page was given.",
        "Both are forms, and both are fitted to the cell the table leaves them, whole and undistorted.",
    ],
    headings: ["Sheet", "Stamp", "What was found", "Reference"],
    sheets: [
        "Delivery note",
        "Packing list",
        "Certificate of conformity",
        "Weighbridge ticket",
    ],
    notes: [
        "The whole page, at a sixth of its size: nothing was redrawn, and nothing \
was left out.",
        "Two lines disagree with the delivery note, so the sheet waits on the \
sender.",
        "Signed and dated, and filed with the batch it covers.",
        "The weight reads, the time does not, so the sheet is held until a clean \
copy arrives.",
    ],
    marks: ["Accepted", "Query", "On hold"],
    foot: "The first row and the third carry one stamp: a drawing costs its \
operators once, wherever it lands.",
};

/// The checking sheet in French.
const FRENCH: Words = Words {
    title: "Ce qui est arrivé, feuille par feuille",
    intro: [
        "Chaque ligne montre une feuille du lot telle qu'elle est arrivée, et le tampon qu'elle a reçu.",
        "Les deux sont des formulaires, ajustés à la cellule que le tableau leur laisse, entiers et sans déformation.",
    ],
    headings: ["Feuille", "Tampon", "Ce qui a été relevé", "Référence"],
    sheets: [
        "Bon de livraison",
        "Liste de colisage",
        "Certificat de conformité",
        "Ticket de pesée",
    ],
    notes: [
        "La page entière, au sixième de sa taille : rien n'a été redessiné, rien \
n'a été laissé de côté.",
        "Deux lignes ne concordent pas avec le bon de livraison, la feuille attend \
donc l'expéditeur.",
        "Signé et daté, classé avec le lot qu'il couvre.",
        "Le poids se lit, l'heure non : la feuille est retenue jusqu'à l'arrivée \
d'une copie nette.",
    ],
    marks: ["Accepté", "À vérifier", "En attente"],
    foot: "Le même tampon est en première et en troisième ligne : un dessin ne \
coûte ses instructions qu'une fois.",
};

/// 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); 2] =
    [(Language::English, &ENGLISH), (Language::French, &FRENCH)];

/// Draws one line of text in `ink`, with its left edge at `x` and its baseline
/// at `y`.
fn text(
    content: &mut Content,
    font: &FontHandle,
    size: f64,
    x: f64,
    y: f64,
    ink: Rgb,
    line: &str,
) -> Result<(), hqf_pdf::Error> {
    content.set_fill(ink)?;
    content.begin_text();
    content.set_font(font.name(), size)?;
    content.text_origin(x, y)?;
    content.show_glyphs(&font.glyphs(line));
    content.end_text();
    Ok(())
}

/// The pale wash a stamp is printed on, which is its own ink most of the way to
/// white.
fn wash(ink: Rgb) -> Rgb {
    Rgb::new(
        ink.r.mul_add(0.16, 0.84),
        ink.g.mul_add(0.16, 0.84),
        ink.b.mul_add(0.16, 0.84),
    )
}

/// One stamp: a washed box, a rule around it, and the word it carries in the
/// middle.
fn stamp(font: &FontHandle, word: &str, ink: Rgb) -> Result<Drawing, hqf_pdf::Error> {
    let (width, height) = STAMP;
    let mut content = Content::new();

    content.set_fill(wash(ink))?;
    content.rect(0.0, 0.0, width, height)?.fill();

    content.set_stroke(ink)?;
    content.set_line_width(1.2)?;
    content.rect(0.6, 0.6, width - 1.2, height - 1.2)?.stroke();

    let measured = font.measure(word, STAMP_TEXT);
    text(
        &mut content,
        font,
        STAMP_TEXT,
        (width - measured) / 2.0,
        8.6,
        ink,
        word,
    )?;

    Ok(Drawing::new(content, Rect::new(0.0, 0.0, width, height)))
}

/// One page of the pack: the sender's letterhead, what the page is, its
/// reference, and the sender's own writing under it.
///
/// The writing is a block of rules rather than words. What this example needs
/// of the pack is that its pages arrive whole, at whatever they were made.
fn sheet_page(
    content: &mut Content,
    font: &FontHandle,
    title: &str,
    reference: &str,
    lines: usize,
) -> Result<(), hqf_pdf::Error> {
    content.set_fill(wash(SENDER_INK))?;
    content.rect(0.0, 742.0, PAGE_WIDTH, 100.0)?.fill();
    content.set_fill(SENDER_INK)?;
    content.rect(0.0, 738.0, PAGE_WIDTH, 4.0)?.fill();

    text(content, font, 20.0, MARGIN, 788.0, INK, SENDER_NAME)?;
    text(content, font, 9.0, MARGIN, 766.0, MUTED, SENDER_TOWN)?;

    text(content, font, PACK_TITLE, MARGIN, 690.0, SENDER_INK, title)?;
    text(content, font, 11.0, MARGIN, 666.0, MUTED, reference)?;

    content.set_stroke(Rgb::gray(0.74))?;
    content.set_line_width(3.4)?;
    let mut y = 630.0;
    for line in 0..lines {
        let share = LINE_SHARES[line % LINE_SHARES.len()];
        content.move_to(MARGIN, y)?;
        content.line_to(share.mul_add(TABLE_WIDTH, MARGIN), y)?;
        content.stroke();
        y -= 22.0;
    }

    text(content, font, 8.0, MARGIN, 60.0, MUTED, SENDER_NAME)
}

/// The pack the checking sheet reads: one page per sheet, as its sender made
/// them.
fn a_pack(font_path: &Path, words: &Words) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    let mut doc = Document::new();
    doc.set_license(licence::licensed());
    let font = doc.add_font(Font::parse(fs::read(font_path)?)?);

    for (index, title) in words.sheets.iter().enumerate() {
        let mut content = Content::new();
        sheet_page(
            &mut content,
            &font,
            title,
            REFERENCES[index],
            LINES_OF[index],
        )?;

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

    Ok(doc.to_bytes()?)
}

/// The checking sheet: a heading, then one row per page of the pack, holding
/// that page and its stamp.
fn checking_sheet<'f>(
    font: &'f FontHandle,
    words: &'static Words,
    sheets: &[ImportedPage],
    stamps: &[DrawingHandle],
) -> Result<Table<'f>, hqf_pdf::Error> {
    // The two form columns are fixed widths, the note takes what is left, and
    // the reference is as wide as its own widest cell.
    let columns = Columns::new(
        vec![
            ColumnWidth::Points(SHEET_COLUMN),
            ColumnWidth::Points(MARK_COLUMN),
            ColumnWidth::Fraction(1.0),
            ColumnWidth::Content,
        ],
        TABLE_WIDTH,
    )?;

    let mut table = Table::new(columns);
    table.header(1);
    table
        .rule(Rule::Frame, Stroke::black(0.8))
        .rule(Rule::HorizontalOther, Stroke::new(0.25, Rgb::gray(0.75)))
        .rule(Rule::VerticalOther, Stroke::new(0.25, Rgb::gray(0.75)))
        .rule(Rule::Horizontal(1), Stroke::black(0.8));

    let pad = Padding::symmetric(6.0, 5.0);

    let mut heading = Row::new();
    for label in words.headings {
        heading = heading.cell(Cell::new(font, 9.0, label).padding(pad).fill(HEAD_FILL));
    }
    table.push(heading.min_height(20.0));

    for (index, name) in words.sheets.iter().enumerate() {
        let mark = MARK_OF[index];
        table.push(
            Row::new()
                .cell(
                    Cell::imported_page(&sheets[index])
                        .valign(VAlign::Middle)
                        .alt(*name)
                        .padding(pad),
                )
                .cell(
                    Cell::drawing(&stamps[mark])
                        .valign(VAlign::Middle)
                        .alt(words.marks[mark])
                        .padding(pad),
                )
                .cell(Cell::new(font, 9.0, format!("{name}\n{}", words.notes[index])).padding(pad))
                .cell(Cell::new(font, 9.0, REFERENCES[index]).padding(pad))
                .min_height(ROW_HEIGHT),
        );
    }

    Ok(table)
}

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

    let pack = Reader::new(a_pack(&font_path, words)?)?;

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

    // Each page of the pack comes in as a form, and each stamp is drawn once.
    let mut sheets = Vec::with_capacity(SHEETS);
    for index in 0..SHEETS {
        sheets.push(doc.import_page(&pack, index)?);
    }
    let mut stamps = Vec::with_capacity(MARKS);
    for (word, ink) in words.marks.iter().zip(MARK_INKS) {
        let drawn = stamp(&font, word, ink)?;
        stamps.push(doc.add_drawing(drawn));
    }

    let mut content = Content::new();
    text(&mut content, &font, 15.0, MARGIN, 786.0, INK, words.title)?;
    let mut y = 768.0;
    for line in words.intro {
        text(&mut content, &font, 9.0, MARGIN, y, MUTED, line)?;
        y -= 12.0;
    }

    let table = checking_sheet(&font, words, &sheets, &stamps)?;
    let placed = table.fit(MARGIN, TOP, TOP - BOTTOM, 0)?;
    placed.draw(&mut content)?;

    text(
        &mut content,
        &font,
        9.0,
        MARGIN,
        placed.bottom() - 18.0,
        MUTED,
        words.foot,
    )?;

    let mut page = Page::a4();
    page.content = content.into_bytes();
    // A page may only draw what its resources name, and a page brought in from
    // elsewhere belongs to the sheets it is put on.
    for sheet in &sheets {
        sheet.add_to(&mut page);
    }
    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, {} pages brought in, {MARKS} stamps drawn",
        bytes.len(),
        sheets.len()
    );
    Ok(())
}

#[cfg(test)]
mod tests {
    use hqf_pdf::{Document, Font, FontHandle};

    use super::{
        BOTTOM, MARK_COLUMN, MARK_OF, MARKS, PACK_TITLE, REFERENCES, ROW_HEIGHT, SHEET_COLUMN,
        SHEETS, STAMP, STAMP_TEXT, TABLE_WIDTH, TOP, WORDS, default_font, language,
    };

    /// The lines two languages are allowed to write the same way. Every line
    /// here is a heading, a name, a sentence or a stamp, and no two languages
    /// write one alike.
    const SPARED: [&str; 0] = [];

    /// How much of a cell its content leaves free on each side, and above and
    /// below.
    const CELL_PAD: (f64, f64) = (6.0, 5.0);

    /// The font every measurement here is taken in, which is the one the
    /// example draws with.
    fn font(document: &mut Document) -> FontHandle {
        document.add_font(
            Font::parse(std::fs::read(default_font()).expect("the committed font is there"))
                .expect("the committed font parses"),
        )
    }

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

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

    /// The lines outside the table are drawn where they are put: one longer
    /// than the room between the margins runs towards the edge of the paper.
    #[test]
    fn every_language_keeps_each_drawn_line_between_the_margins() {
        let mut document = Document::new();
        let font = font(&mut document);

        for (named, words) in WORDS {
            let mut lines = vec![(words.title, 15.0), (words.foot, 9.0)];
            lines.extend(words.intro.map(|line| (line, 9.0)));

            for (line, size) in lines {
                let measured = font.measure(line, size);

                assert!(
                    measured <= TABLE_WIDTH,
                    "the {} sheet draws {line:?} over {measured:.1} points, and it \
                     has {TABLE_WIDTH:.1}",
                    named.code()
                );
            }
        }
    }

    /// A stamp is fitted to the cell it stands in, and the word it carries is
    /// drawn at the size the stamp was made at: a word wider than its stamp
    /// runs out of the box drawn around it.
    #[test]
    fn every_language_writes_stamps_that_stay_in_their_box() {
        let mut document = Document::new();
        let font = font(&mut document);
        let room = STAMP.0 - 4.0;

        for (named, words) in WORDS {
            for word in words.marks {
                let measured = font.measure(word, STAMP_TEXT);

                assert!(
                    measured <= room,
                    "the {} sheet sets the stamp {word:?} over {measured:.1} \
                     points, and its box holds {room:.1}",
                    named.code()
                );
            }
        }
    }

    /// The heading of every row, and the rows themselves, at the height they
    /// are given.
    const HEADING_HEIGHT: f64 = 20.0;

    /// The table is laid out in one go, so every row of it has to fit between
    /// the top it is given and the bottom of the page.
    #[test]
    fn the_whole_pack_fits_on_one_page() {
        assert_eq!(SHEETS, 4, "the sheet is laid out for a pack of four pages");

        let table = ROW_HEIGHT.mul_add(4.0, HEADING_HEIGHT);
        let room = TOP - BOTTOM;

        assert!(
            table <= room,
            "the table stands {table:.1} points tall, and it is given {room:.1}"
        );
    }

    /// The last column is as wide as its own widest cell, the two form columns
    /// are set widths, and the note takes what the three of them leave. Every
    /// heading has to fit the column it stands over.
    #[test]
    fn every_language_fits_each_heading_in_its_column() {
        let mut document = Document::new();
        let font = font(&mut document);
        let inside = |column: f64| CELL_PAD.0.mul_add(-2.0, column);

        for (named, words) in WORDS {
            // The column of references is as wide as the widest thing in it,
            // which is either a reference or the heading over them.
            let widest = REFERENCES
                .iter()
                .chain(std::iter::once(&words.headings[3]))
                .map(|line| font.measure(line, 9.0))
                .fold(0.0_f64, f64::max);
            let references = CELL_PAD.0.mul_add(2.0, widest);
            let note = TABLE_WIDTH - SHEET_COLUMN - MARK_COLUMN - references;

            for (heading, column) in
                words
                    .headings
                    .into_iter()
                    .zip([SHEET_COLUMN, MARK_COLUMN, note, references])
            {
                let measured = font.measure(heading, 9.0);
                let room = inside(column);

                assert!(
                    measured <= room,
                    "the {} sheet heads a column {heading:?} over {measured:.1} \
                     points, and the column holds {room:.1}",
                    named.code()
                );
            }
        }
    }

    /// The heading of a page of the pack is drawn where it is put, like every
    /// other line: one wider than the sender's margins runs towards the edge of
    /// the sheet, and the thumbnail shows it doing so.
    #[test]
    fn every_language_heads_each_page_of_the_pack_within_its_margins() {
        let mut document = Document::new();
        let font = font(&mut document);

        for (named, words) in WORDS {
            for title in words.sheets {
                let measured = font.measure(title, PACK_TITLE);

                assert!(
                    measured <= TABLE_WIDTH,
                    "the {} pack heads a page {title:?} over {measured:.1} points, \
                     and it has {TABLE_WIDTH:.1}",
                    named.code()
                );
            }
        }
    }

    /// Every row names a stamp that was drawn.
    #[test]
    fn every_row_names_a_stamp_that_exists() {
        for mark in MARK_OF {
            assert!(mark < MARKS, "row {mark} names a stamp that is not drawn");
        }
    }
}