write_spot_color.rs

Die Rust-Datei des Beispiels „Druckfarben auf eigenen Platten“. Zwei Druckfarben, die die Druckmaschine auf je einer eigenen Platte führt, jede in zehn Stärken, eine Arbeit in beiden gesetzt, und die Passkreuze, mit denen die Platten ausgerichtet werden — gezeichnet in einer Druckfarbe, auf die jede Platte reagiert.

Rust 1234 Zeilen

Wofür dieses Beispiel da ist

Ein Zweifarbauftrag kostet weniger als ein Vierfarbauftrag, und die Farbe kommt genau richtig statt fast richtig heraus; deshalb wird so viel Geschäftspapier, Verpackung und Beschilderung so gedruckt. Es funktioniert, wenn die Datei die gewünschte Sonderfarbe nennt, sodass die Druckerei weiß, auf welche Platte diese Markierungen gehören, und als einzelne Zahl angibt, wie viel dieser Druckfarbe aufgetragen wird.

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
 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
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
//! Inks the press carries on plates of their own, at every tint.
//!
//! A spot colour is not mixed out of the process inks: it is an ink of its own,
//! and a colour in its space is one number — how much of it goes down. What
//! that number looks like where the ink is not to hand, on a screen or on a
//! press that does not carry it, is what the tint transform says.
//!
//! The second page sets a run list in those same two inks and in nothing else:
//! the heading on a full-strength plate, every other line on a tint of it, the
//! total on the second ink, the rules in both, and a closing paragraph whose
//! runs each name an ink. Everything that paints takes a colour the document
//! carries exactly where it takes a device one — a cell's background, a cell's
//! text, a row, a table's rules, a run of styled text, a stamp.
//!
//! The third and fourth pages are the first sheet separated: one page per
//! plate, each painting the tints of its own ink and saying which ink it is and
//! which sheet it belongs to, the blue plate also naming the space its tints
//! are read through.
//!
//! What the two sheets say is written in the language `HQF_PDF_LANG` names. The
//! names of the inks are not: a colorant is what the plate answers to, the file
//! writes it, and a page that renamed it would name a plate the press has not
//! got. Neither are the sheet counts, the prices or the tints.
//!
//! Usage: `cargo run --example write_spot_color -- tmp/spot.pdf [font.ttf]`
//!        `HQF_PDF_LANG=fr cargo run --example write_spot_color`

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

use hqf_pdf::content::Content;
use hqf_pdf::layout::{
    Area, Cell, ColumnWidth, Columns, Padding, RichText, Row, Rule, Stamp, Stroke, Style, Table,
    TableFrame, VAlign,
};
use hqf_pdf::{
    Align, Cmyk, ColorSpace, ColorSpaceHandle, DeviceSpace, Document, Exponential, Font,
    FontHandle, Page, PageSeparation, Paint, Rgb, Separation, 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.
fn default_font() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fonts")
        .join("DejaVuSans.ttf")
}

/// The left edge of everything, and the width of the sheet the page shows.
const LEFT: f64 = 70.0;
/// See [`LEFT`].
const WIDTH: f64 = 445.0;

/// The left edge of each tint swatch, and the size of one.
const TINT_X: &[f64] = &[
    70.0, 115.0, 160.0, 205.0, 250.0, 295.0, 340.0, 385.0, 430.0, 475.0,
];
/// See [`TINT_X`].
const TINT_SIZE: (f64, f64) = (40.0, 56.0);

/// The top of the run list on the second page.
const LIST_TOP: f64 = 700.0;

/// Where the head of a sheet sits, and where the two lines under it do.
const HEAD_BASELINE: f64 = 780.0;
const LEAD_BASELINES: [f64; 2] = [762.0, 748.0];

/// Where the rule at the foot of the second sheet is drawn.
const CLOSING_RULE: f64 = 92.0;

/// The size the closing paragraph is set at, and how far apart its lines sit.
const PROSE_SIZE: f64 = 10.5;
const PROSE_LEADING: f64 = 15.0;

/// Where the crosshairs stand around a sheet whose foot is at `bottom` and
/// which is `height` tall. A page says how many there are, so the count and the
/// word have to agree.
fn crosshairs(bottom: f64, height: f64) -> [(f64, f64); 4] {
    [
        (LEFT - 16.0, bottom - 16.0),
        (LEFT + WIDTH + 16.0, bottom - 16.0),
        (LEFT - 16.0, bottom + height + 16.0),
        (LEFT + WIDTH + 16.0, bottom + height + 16.0),
    ]
}

/// Which plates a line of the run list takes.
#[derive(Debug, Clone, Copy)]
enum Plates {
    First,
    Second,
    Both,
}

impl Plates {
    /// What this run of plates is called, in the words of a language.
    const fn named(self, words: &'static Words) -> &'static str {
        match self {
            Self::First => words.plates[0],
            Self::Second => words.plates[1],
            Self::Both => words.plates[2],
        }
    }
}

/// One line of the run list: how many sheets it runs to, which plates it takes
/// and what it comes to. What is being printed is a word, and it lives with the
/// other words of its language.
const ITEMS: &[(&str, Plates, &str)] = &[
    ("5 000", Plates::First, "412.00"),
    ("2 000", Plates::First, "196.00"),
    ("2 000", Plates::Both, "268.00"),
    ("3 000", Plates::Second, "184.00"),
    ("1 000", Plates::Both, "590.00"),
    ("750", Plates::Both, "445.00"),
    ("5 000", Plates::First, "388.00"),
    ("1 500", Plates::First, "242.00"),
    ("12", Plates::Both, "336.00"),
];

/// What the nine lines come to.
const TOTAL: &str = "3 061.00";

/// The ten tints each ink is shown at. What each is called is read off the tint
/// itself, so no language can label a swatch with a strength it is not laid at.
const TINTS: &[f64] = &[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0];

/// What one tint is called under its swatch.
fn tint_name(tint: f64) -> String {
    format!("{:.0}%", tint * 100.0)
}

/// The two inks the work is set in, and the colorant every plate answers to. A
/// colorant is what the plate is called on the press: the file writes it, and
/// no language renames it.
const BLUE: &str = "Corporate Blue";
const GOLD: &str = "Corporate Gold";
const ALL_PLATES: &str = "All";

/// The tint the panel behind the heading is laid at, which the caption under it
/// spells out in words.
const PANEL_TINT: f64 = 0.15;

/// The tint the shaded lines of the run list are laid at, which the note under
/// it spells out in words.
const SHADED_TINT: f64 = 0.08;

/// The strength the closing paragraph is drawn at, which it says of itself.
const PROSE_TINT: f64 = 0.9;

/// The sizes the two sheets are set in, in points.
const HEAD_SIZE: f64 = 15.0;
const LEAD_SIZE: f64 = 10.0;
const SWATCH_SIZE: f64 = 7.0;
const CAPTION_SIZE: f64 = 10.0;
const SHEET_TITLE_SIZE: f64 = 26.0;
const SHEET_LINE_SIZE: f64 = 11.0;
const MARK_SIZE: f64 = 9.0;
const CELL_SIZE: f64 = 9.0;
const TOTAL_SIZE: f64 = 9.5;
const CLOSING_SIZE: f64 = 8.5;

/// The words the two sheets are written in, one set per language.
///
/// The names of the inks are not among them, nor the sheet counts, the prices
/// or the tints.
#[derive(Debug)]
struct Words {
    /// What the first sheet is called.
    title: &'static str,
    /// The two lines under it, which say what a spot colour is.
    lead: [&'static str; 2],
    /// What stands after the name of the first ink over its ramp of tints.
    first_ramp: &'static str,
    /// What stands after the name of the second over its own.
    second_ramp: &'static str,
    /// What stands after the name of an ink over its ramp on its own plate.
    plate: &'static str,
    /// What the piece of work set in the two inks is called.
    sheet_title: &'static str,
    /// The line inside it, which says what is laid over what.
    sheet_line: &'static str,
    /// The line under it, which says what the crosshairs are for.
    marks: &'static str,
    /// What the second sheet is called.
    list_title: &'static str,
    /// The two lines under it, which say how far a carried colour reaches.
    list_lead: [&'static str; 2],
    /// What heads each column of the run list, left to right.
    headings: [&'static str; 4],
    /// What each line of the run list is printing, in the order [`ITEMS`] runs
    /// them.
    items: [&'static str; 9],
    /// What each run of plates is called: the first alone, the second alone,
    /// then both.
    plates: [&'static str; 3],
    /// What the last row of the run list is called.
    total: &'static str,
    /// The three runs of the note under the list, the middle one of which is
    /// set in the second ink.
    note: [&'static str; 3],
    /// The paragraph under it, drawn in one flow.
    prose: &'static str,
    /// The mark laid across the whole of the run list.
    stamp: &'static str,
    /// What stands before the name of the first ink in the closing line.
    quoted_before: &'static str,
    /// What stands between the two names.
    quoted_between: &'static str,
    /// What stands after the second.
    quoted_after: &'static str,
}

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

/// The two sheets in English.
const ENGLISH: Words = Words {
    title: "Inks the press carries, on plates of their own",
    lead: [
        "A spot colour is one number, not four: how much of that ink goes down. Where the ink is not",
        "to hand, the tint transform the file carries shows it — which is what these swatches are.",
    ],
    first_ramp: ", one plate, ten tints",
    second_ramp: ", a second plate",
    plate: ", sheet 1 separated: this page is the plate of that ink",
    sheet_title: "Two inks, one sheet",
    sheet_line: "Set in the second ink, over a fifteen percent tint of the first.",
    marks: "The four crosshairs are set in the colorant every plate answers to, which is what lines them up.",
    list_title: "The same two inks, through the layout",
    list_lead: [
        "A colour the document carries reaches a cell, a row, a rule and a run of styled text the",
        "way a colour the device answers for does. Not a drop of process ink goes down below.",
    ],
    headings: ["Item", "Sheets", "Plates", "Price"],
    items: [
        "Letterhead, A4",
        "Letterhead, A5",
        "Business cards, both sides",
        "Compliments slip",
        "Presentation folder",
        "Report cover, laminated",
        "Window envelope, DL",
        "Wallet envelope, C4",
        "Exhibition panel",
    ],
    plates: ["First", "Second", "Both"],
    total: "Total, nine items, two plates",
    note: [
        "The heading stands on a full plate of the first ink, ",
        "the total on the second",
        ", and the shaded lines on an eight percent tint of the first. One run of this \
         sentence is set in each of the two.",
    ],
    prose: "An ink of its own is what a house colour is bought as: the \
        press carries it on a plate of its own, so it goes down at the same strength \
        on every sheet of every run, which four process inks laid over each other \
        cannot promise. It costs a plate, so it is spent on the marks a reader \
        recognises the house by — the name, the rule under it, the panel behind a \
        heading — and the rest of the work is left to the process inks. This \
        paragraph is one flow of text, drawn in the first ink at ninety percent, and \
        the words of it were measured and broken to the width of the list above.",
    stamp: "SPECIMEN",
    quoted_before: "Quoted in two inks: ",
    quoted_between: " and ",
    quoted_after: ", each on a plate of its own.",
};

/// The two sheets in French.
const FRENCH: Words = Words {
    title: "Des encres en machine, chacune sur sa propre plaque",
    lead: [
        "Un ton direct est un seul nombre, pas quatre : la quantité d’encre déposée. Là où l’encre",
        "manque, la transformation de teinte que contient le fichier la simule : ce \
sont ces pastilles.",
    ],
    first_ramp: ", une plaque, dix teintes",
    second_ramp: ", une seconde plaque",
    plate: ", feuille 1 séparée : cette page est la plaque de cette encre",
    sheet_title: "Deux encres, une feuille",
    sheet_line: "Seconde encre, sur une teinte à quinze pour cent de la première.",
    marks: "Les croix, en couleur de repérage, s’impriment sur chaque plaque : c’est ce \
qui les cale.",
    list_title: "Les deux mêmes encres, dans toute la mise en page",
    list_lead: [
        "Une couleur définie par le document s’applique à une cellule, une ligne, un \
filet, un texte",
        "mis en forme, comme une couleur d’appareil. Ci-dessous, pas une goutte de quadrichromie.",
    ],
    headings: ["Article", "Feuilles", "Plaques", "Prix"],
    items: [
        "Papier à en-tête, A4",
        "Papier à en-tête, A5",
        "Cartes de visite, recto verso",
        "Carte de correspondance",
        "Chemise de présentation",
        "Couverture de rapport, pelliculée",
        "Enveloppe à fenêtre, DL",
        "Enveloppe à poche, C4",
        "Panneau d’exposition",
    ],
    plates: ["Première", "Seconde", "Les deux"],
    total: "Total, neuf articles, deux plaques",
    note: [
        "Le titre repose sur un aplat de la première encre, ",
        "le total sur la seconde",
        ", et les lignes ombrées sur une teinte à huit pour cent de la première. Un \
passage de cette phrase est composé dans chacune des deux.",
    ],
    prose: "Une encre à soi, voilà ce qu’on achète avec une couleur maison : la presse \
l’imprime sur sa propre plaque, et elle se dépose avec la même intensité sur \
chaque feuille de chaque tirage, ce que quatre encres de quadrichromie \
superposées ne peuvent pas garantir. Elle coûte une plaque : on la réserve \
donc aux signes auxquels un lecteur reconnaît la maison — le nom, le filet \
dessous, l’aplat derrière un titre — et on laisse le reste du travail aux \
encres de quadrichromie. Ce paragraphe est une seule coulée de texte, imprimée \
dans la première encre à quatre-vingt-dix pour cent, et ses mots ont été \
mesurés et coupés à la largeur de la liste ci-dessus.",
    stamp: "SPÉCIMEN",
    quoted_before: "Chiffré en deux encres : ",
    quoted_between: " et ",
    quoted_after: ", chacune sur sa plaque.",
};

/// The two sheets in Dutch.
const DUTCH: Words = Words {
    title: "Inkten die de pers meeneemt, op eigen platen",
    lead: [
        "Een steunkleur is één getal, geen vier: hoeveel van die inkt er neerkomt. Waar de inkt",
        "ontbreekt, toont de tintomzetting in het bestand haar, en dat zijn deze stalen.",
    ],
    first_ramp: ", één plaat, tien tinten",
    second_ramp: ", een tweede plaat",
    plate: ", blad 1 gescheiden: deze pagina is de plaat van die inkt",
    sheet_title: "Twee inkten, één blad",
    sheet_line: "Gezet in de tweede inkt, over een tint van vijftien procent van de eerste.",
    marks: "De vier paskruisen staan in de pasmerkkleur, die op elke plaat drukt, en daardoor lijnen ze uit.",
    list_title: "Dezelfde twee inkten, via de opmaak",
    list_lead: [
        "Een kleur die in het document zit, bereikt een cel, een rij, een lijn en een stuk opgemaakte tekst",
        "zoals een kleur waar het apparaat voor instaat. Hieronder komt geen druppel procesinkt neer.",
    ],
    headings: ["Artikel", "Bladen", "Platen", "Prijs"],
    items: [
        "Briefpapier, A4",
        "Briefpapier, A5",
        "Visitekaartjes, beide zijden",
        "Complimentkaart",
        "Presentatiemap",
        "Rapportomslag, gelamineerd",
        "Vensterenvelop, DL",
        "Akte-envelop, C4",
        "Beurspaneel",
    ],
    plates: ["Eerste", "Tweede", "Beide"],
    total: "Totaal, negen artikelen, twee platen",
    note: [
        "De kop staat op een volle plaat van de eerste inkt,",
        "het totaal op de tweede",
        ", en de gearceerde regels op een tint van acht procent van de eerste. Een stuk van deze zin staat in elk van beide.",
    ],
    prose: "Een eigen inkt is hoe een huiskleur wordt ingekocht: de pers drukt haar met een eigen plaat, dus komt ze met dezelfde sterkte op elk blad van elke oplage, wat vier over elkaar gelegde procesinkten niet kunnen beloven. Ze kost een plaat, dus wordt ze besteed aan de tekens waaraan een lezer het huis herkent (de naam, de lijn eronder, het vlak achter een kop) en de rest van het werk blijft voor de procesinkten. Deze alinea is één tekststroom, getekend in de eerste inkt op negentig procent, en haar woorden zijn gemeten en afgebroken op de breedte van de lijst hierboven.",
    stamp: "SPECIMEN",
    quoted_before: "Geoffreerd in twee inkten:",
    quoted_between: " en",
    quoted_after: ", elk op een eigen plaat.",
};

/// The two sheets in Portuguese.
const PORTUGUESE: Words = Words {
    title: "Tintas que a máquina leva, em chapas próprias",
    lead: [
        "Uma cor direta é um número, não quatro: quanta dessa tinta se põe. Onde a tinta não está",
        "à mão, a transformação de tom que o ficheiro leva mostra-a, e é isso que estas amostras são.",
    ],
    first_ramp: ", uma chapa, dez tons",
    second_ramp: ", uma segunda chapa",
    plate: ", folha 1 separada: esta página é a chapa dessa tinta",
    sheet_title: "Duas tintas, uma folha",
    sheet_line: "Na segunda tinta, sobre um tom de quinze por cento da primeira.",
    marks: "As quatro miras estão no colorante a que todas as chapas respondem, e é isso que as alinha.",
    list_title: "As mesmas duas tintas, através da paginação",
    list_lead: [
        "Uma cor que o documento leva chega a uma célula, uma linha, um filete e um troço de texto com estilo",
        "tal como uma cor a que o dispositivo responde. Por baixo não cai uma gota de quadricromia.",
    ],
    headings: ["Artigo", "Folhas", "Chapas", "Preço"],
    items: [
        "Papel timbrado, A4",
        "Papel timbrado, A5",
        "Cartões de visita, frente e verso",
        "Cartão de cumprimentos",
        "Pasta de apresentação",
        "Capa de relatório, plastificada",
        "Envelope de janela, DL",
        "Envelope saco, C4",
        "Painel de exposição",
    ],
    plates: ["Primeira", "Segunda", "Ambas"],
    total: "Total, nove artigos, duas chapas",
    note: [
        "O título assenta numa chapa cheia da primeira tinta,",
        "o total na segunda",
        ", e as linhas sombreadas num tom de oito por cento da primeira. Um troço desta frase está em cada uma das duas.",
    ],
    prose: "Uma tinta própria é a forma de comprar uma cor da casa: a máquina leva-a numa chapa própria, por isso cai com a mesma intensidade em cada folha de cada tiragem, o que quatro tintas de quadricromia sobrepostas não podem prometer. Custa uma chapa, por isso gasta-se nos sinais pelos quais um leitor reconhece a casa (o nome, o filete por baixo, o painel atrás de um título) e o resto do trabalho fica para a quadricromia. Este parágrafo é um só texto, desenhado na primeira tinta a noventa por cento, e as suas palavras foram medidas e partidas à largura da lista acima.",
    stamp: "ESPÉCIME",
    quoted_before: "Orçamentado em duas tintas:",
    quoted_between: " e",
    quoted_after: ", cada uma numa chapa própria.",
};

/// The two sheets in Italian.
const ITALIAN: Words = Words {
    title: "Inchiostri che porta la macchina, su lastre proprie",
    lead: [
        "Un colore spot è un numero, non quattro: quanto di quell’inchiostro si stende. Dove l’inchiostro non è",
        "a portata, la trasformazione di tinta che porta il file lo mostra, ed è ciò che sono questi campioni.",
    ],
    first_ramp: ", una lastra, dieci tinte",
    second_ramp: ", una seconda lastra",
    plate: ", foglio 1 separato: questa pagina è la lastra di quell’inchiostro",
    sheet_title: "Due inchiostri, un foglio",
    sheet_line: "Nel secondo inchiostro, su una tinta al quindici per cento del primo.",
    marks: "I quattro crocini sono nel colorante a cui rispondono tutte le lastre, ed è ciò che le allinea.",
    list_title: "Gli stessi due inchiostri, attraverso l’impaginazione",
    list_lead: [
        "Un colore che porta il documento arriva a una cella, una riga, un filetto e un tratto di testo con stile",
        "come un colore di cui risponde il dispositivo. Sotto non cade una goccia di quadricromia.",
    ],
    headings: ["Articolo", "Fogli", "Lastre", "Prezzo"],
    items: [
        "Carta intestata, A4",
        "Carta intestata, A5",
        "Biglietti da visita, fronte e retro",
        "Biglietto di cortesia",
        "Cartella di presentazione",
        "Copertina di rapporto, plastificata",
        "Busta a finestra, DL",
        "Busta a sacco, C4",
        "Pannello espositivo",
    ],
    plates: ["Primo", "Secondo", "Entrambi"],
    total: "Totale, nove articoli, due lastre",
    note: [
        "Il titolo sta su una lastra piena del primo inchiostro,",
        "il totale sul secondo",
        ", e le righe ombreggiate su una tinta all’otto per cento del primo. Un tratto di questa frase è in ciascuno dei due.",
    ],
    prose: "Un inchiostro proprio è come si compra un colore aziendale: la macchina lo porta su una lastra propria, così si stende con la stessa intensità su ogni foglio di ogni tiratura, cosa che quattro inchiostri di quadricromia sovrapposti non possono promettere. Costa una lastra, quindi si spende sui segni da cui il lettore riconosce la casa (il nome, il filetto sotto, il pannello dietro un titolo) e il resto del lavoro va alla quadricromia. Questo paragrafo è un solo testo, disegnato nel primo inchiostro al novanta per cento, e le sue parole sono state misurate e spezzate alla larghezza dell’elenco qui sopra.",
    stamp: "CAMPIONE",
    quoted_before: "Preventivato in due inchiostri:",
    quoted_between: " e",
    quoted_after: ", ciascuno su una lastra propria.",
};

/// The two sheets in Spanish.
const SPANISH: Words = Words {
    title: "Tintas que lleva la prensa, en planchas propias",
    lead: [
        "Un color directo es un número, no cuatro: cuánta de esa tinta se pone. Donde la tinta no está",
        "a mano, la transformación de tono que lleva el archivo la muestra, y eso son estas muestras.",
    ],
    first_ramp: ", una plancha, diez tonos",
    second_ramp: ", una segunda plancha",
    plate: ", hoja 1 separada: esta página es la plancha de esa tinta",
    sheet_title: "Dos tintas, una hoja",
    sheet_line: "En la segunda tinta, sobre un tono al quince por ciento de la primera.",
    marks: "Las cuatro cruces van en el colorante común a todas las planchas, que es lo que las alinea.",
    list_title: "Las mismas dos tintas, a través de la maqueta",
    list_lead: [
        "Un color que lleva el documento llega a una celda, una fila, un filete y un tramo de texto con estilo",
        "igual que un color del que responde el dispositivo. Abajo no cae ni una gota de cuatricromía.",
    ],
    headings: ["Artículo", "Hojas", "Planchas", "Precio"],
    items: [
        "Papel con membrete, A4",
        "Papel con membrete, A5",
        "Tarjetas de visita, dos caras",
        "Tarjeta de saludo",
        "Carpeta de presentación",
        "Portada de informe, plastificada",
        "Sobre con ventana, DL",
        "Sobre bolsa, C4",
        "Panel de exposición",
    ],
    plates: ["Primera", "Segunda", "Ambas"],
    total: "Total, nueve artículos, dos planchas",
    note: [
        "El título está sobre una plancha llena de la primera tinta,",
        "el total sobre la segunda",
        ", y las líneas sombreadas sobre un tono del ocho por ciento de la primera. Un tramo de esta frase va en cada una de las dos.",
    ],
    prose: "Una tinta propia es como se compra un color corporativo: la prensa la lleva en una plancha propia, así que cae con la misma intensidad en cada hoja de cada tirada, cosa que cuatro tintas de cuatricromía superpuestas no pueden prometer. Cuesta una plancha, así que se gasta en las señas por las que un lector reconoce la casa —el nombre, el filete debajo, el panel tras un título— y el resto del trabajo queda para la cuatricromía. Este párrafo es un solo texto, dibujado en la primera tinta al noventa por ciento, y sus palabras se midieron y cortaron al ancho de la lista de arriba.",
    stamp: "MUESTRA",
    quoted_before: "Presupuestado en dos tintas:",
    quoted_between: " y",
    quoted_after: ", cada una en una plancha propia.",
};

/// The two sheets in German.
const GERMAN: Words = Words {
    title: "Farben, die die Druckmaschine trägt, auf eigenen Platten",
    lead: [
        "Eine Schmuckfarbe ist eine Zahl, nicht vier: wie viel davon aufgetragen wird. Wo die Farbe",
        "nicht zur Hand ist, zeigt sie die mitgeführte Tonwertumrechnung: das sind diese Felder.",
    ],
    first_ramp: ", eine Platte, zehn Tonwerte",
    second_ramp: ", eine zweite Platte",
    plate: ", Bogen 1 separiert: Diese Seite ist die Platte dieser Farbe",
    sheet_title: "Zwei Farben, ein Bogen",
    sheet_line: "In der zweiten Farbe, über 15 % Tonwert der ersten.",
    marks: "Die vier Passkreuze stehen in der Farbe, auf die jede Platte antwortet, und das richtet sie aus.",
    list_title: "Dieselben zwei Farben, über das Layout",
    list_lead: [
        "Eine mitgeführte Farbe erreicht eine Zelle, eine Zeile, eine Linie und formatierten",
        "Text wie eine Farbe, für die das Gerät einsteht. Unten fällt kein Tropfen Prozessfarbe.",
    ],
    headings: ["Posten", "Bögen", "Platten", "Preis"],
    items: [
        "Briefpapier, A4",
        "Briefpapier, A5",
        "Visitenkarten, beidseitig",
        "Kurzmitteilung",
        "Präsentationsmappe",
        "Berichtsumschlag, laminiert",
        "Fensterumschlag, DL",
        "Versandtasche, C4",
        "Messetafel",
    ],
    plates: ["Erste", "Zweite", "Beide"],
    total: "Summe, neun Posten, zwei Platten",
    note: [
        "Die Überschrift steht auf einer vollen Platte der ersten Farbe, ",
        "die Summe auf der zweiten",
        ", und die schattierten Zeilen auf einem Tonwert von acht Prozent der ersten. Je ein Abschnitt dieses Satzes ist in einer der beiden gesetzt.",
    ],
    prose: "Eine eigene Farbe ist das, als was eine Hausfarbe gekauft wird: Die Druckmaschine trägt sie auf einer eigenen Platte, also kommt sie auf jedem Bogen jeder Auflage in derselben Stärke, was vier übereinandergelegte Prozessfarben nicht versprechen können. Sie kostet eine Platte, also wird sie für die Zeichen ausgegeben, an denen ein Leser das Haus erkennt — den Namen, die Linie darunter, die Fläche hinter einer Überschrift —, und der Rest der Arbeit bleibt den Prozessfarben. Dieser Absatz ist ein einziger Textfluss, in der ersten Farbe zu neunzig Prozent gezeichnet, und seine Wörter wurden auf die Breite der Liste oben gemessen und umbrochen.",
    stamp: "MUSTER",
    quoted_before: "In zwei Farben angeboten: ",
    quoted_between: " und ",
    quoted_after: ", jede auf ihrer eigenen Platte.",
};

/// 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 caption over a ramp of tints: the ink it shows, then what it shows of
/// it.
fn ramp_caption(ink: &str, after: &str) -> String {
    format!("{ink}{after}")
}

/// The closing line, which names the two inks the work is quoted in.
fn quoted(words: &Words) -> String {
    format!(
        "{}{BLUE}{}{GOLD}{}",
        words.quoted_before, words.quoted_between, words.quoted_after
    )
}

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

/// Draws the ten tints of one ink, each labelled with how much goes down.
fn tint_ramp(
    content: &mut Content,
    font: &FontHandle,
    ink: &ColorSpaceHandle,
    y: f64,
    caption: &str,
) -> Result<(), hqf_pdf::Error> {
    let (width, height) = TINT_SIZE;

    for (x, tint) in TINT_X.iter().zip(TINTS) {
        content.save_state();
        content.set_fill_space(ink);
        content.set_fill_components(&[*tint])?;
        content.rect(*x, y, width, height)?.fill();
        content.restore_state();

        content.save_state();
        content.set_fill(Rgb::gray(0.35))?;
        text(content, font, SWATCH_SIZE, *x, y - 11.0, &tint_name(*tint))?;
        content.restore_state();
    }

    content.save_state();
    content.set_fill(Rgb::gray(0.2))?;
    text(content, font, CAPTION_SIZE, LEFT, y + height + 8.0, caption)?;
    content.restore_state();
    Ok(())
}

/// Draws a printer's crosshair centred on `(x, y)`, in whatever space is in
/// force.
fn registration(content: &mut Content, x: f64, y: f64) -> Result<(), hqf_pdf::Error> {
    let arm = 7.0;
    content.move_to(x - arm, y)?;
    content.line_to(x + arm, y)?;
    content.move_to(x, y - arm)?;
    content.line_to(x, y + arm)?;
    content.stroke();

    content.rect(x - 4.0, y - 4.0, 8.0, 8.0)?.stroke();
    Ok(())
}

/// Draws a piece of work set in the two inks, with the marks a printer lines
/// the plates up by around it.
fn sheet(
    content: &mut Content,
    font: &FontHandle,
    words: &Words,
    blue: &ColorSpaceHandle,
    gold: &ColorSpaceHandle,
    registration_ink: &ColorSpaceHandle,
) -> Result<(), hqf_pdf::Error> {
    let (bottom, height) = (250.0, 190.0);
    content.save_state();
    content.set_stroke(Rgb::gray(0.75))?;
    content.set_line_width(0.5)?;
    content.rect(LEFT, bottom, WIDTH, height)?.stroke();
    content.restore_state();

    content.save_state();
    content.set_fill_space(blue);
    content.set_fill_components(&[1.0])?;
    text(
        content,
        font,
        SHEET_TITLE_SIZE,
        LEFT + 24.0,
        bottom + 130.0,
        words.sheet_title,
    )?;
    content.set_fill_components(&[PANEL_TINT])?;
    content
        .rect(LEFT + 24.0, bottom + 40.0, WIDTH - 48.0, 66.0)?
        .fill();
    content.restore_state();

    content.save_state();
    content.set_fill_space(gold);
    content.set_fill_components(&[1.0])?;
    content
        .rect(LEFT + 24.0, bottom + 118.0, 180.0, 3.0)?
        .fill();
    text(
        content,
        font,
        SHEET_LINE_SIZE,
        LEFT + 40.0,
        bottom + 68.0,
        words.sheet_line,
    )?;
    content.restore_state();

    content.save_state();
    content.set_stroke_space(registration_ink);
    content.set_stroke_components(&[1.0])?;
    content.set_line_width(0.5)?;
    for (x, y) in crosshairs(bottom, height) {
        registration(content, x, y)?;
    }
    content.restore_state();

    content.save_state();
    content.set_fill(Rgb::gray(0.35))?;
    text(content, font, MARK_SIZE, LEFT, bottom - 40.0, words.marks)?;
    content.restore_state();

    Ok(())
}

/// The run list, set in the two inks and in nothing else: the heading on a
/// full-strength plate of the first, every other line on an eight percent tint
/// of it, the total on the second, and every rule in one or the other.
fn run_list<'a>(
    font: &'a FontHandle,
    words: &'static Words,
    blue: &ColorSpaceHandle,
    gold: &ColorSpaceHandle,
) -> Result<Table<'a>, hqf_pdf::Error> {
    let columns = Columns::new(
        vec![
            ColumnWidth::Fraction(1.0),
            ColumnWidth::Points(54.0),
            ColumnWidth::Points(74.0),
            ColumnWidth::Points(66.0),
        ],
        WIDTH,
    )?;
    let mut table = Table::new(columns);
    table
        .fill(Area::EvenRows, Paint::named(blue, [SHADED_TINT]))
        .fill(Area::Header, Paint::named(blue, [1.0]))
        .rule(Rule::Frame, Stroke::new(0.8, Paint::named(blue, [0.55])))
        .rule(
            Rule::HorizontalOther,
            Stroke::new(0.3, Paint::named(blue, [0.3])),
        )
        .rule(
            Rule::Horizontal(1),
            Stroke::new(1.2, Paint::named(gold, [1.0])),
        )
        .header(1);

    let heading = |label: &'static str, align: Align| {
        Cell::new(font, CELL_SIZE, label)
            .align(align)
            .valign(VAlign::Middle)
            .color(Rgb::WHITE)
            .padding(Padding::symmetric(6.0, 5.0))
    };
    let [item, sheets, plates, price] = words.headings;
    table.push(
        Row::new()
            .cell(heading(item, Align::Left))
            .cell(heading(sheets, Align::Right))
            .cell(heading(plates, Align::Left))
            .cell(heading(price, Align::Right)),
    );

    for (index, (sheets, plates, price)) in ITEMS.iter().enumerate() {
        let line = |label: &'static str, align: Align| {
            Cell::new(font, CELL_SIZE, label)
                .align(align)
                .color(Paint::named(blue, [0.95]))
                .padding(Padding::symmetric(6.0, 5.0))
        };
        table.push(
            Row::new()
                .cell(line(words.items[index], Align::Left))
                .cell(line(sheets, Align::Right))
                .cell(line(plates.named(words), Align::Left))
                .cell(line(price, Align::Right)),
        );
    }

    let total = |label: &str, align: Align| {
        Cell::new(font, TOTAL_SIZE, label)
            .align(align)
            .color(Paint::named(blue, [1.0]))
            .padding(Padding::symmetric(6.0, 6.0))
    };
    table.push(
        Row::new()
            .cell(total(words.total, Align::Left))
            .cell(total("", Align::Right))
            .cell(total("", Align::Left))
            .cell(total(TOTAL, Align::Right))
            .fill(Paint::named(gold, [0.28])),
    );

    Ok(table)
}

/// Draws the run list, the note whose two runs each stand in the ink they name,
/// and a specimen mark laid across the whole of it.
fn run_sheet(
    content: &mut Content,
    font: &FontHandle,
    words: &'static Words,
    blue: &ColorSpaceHandle,
    gold: &ColorSpaceHandle,
) -> Result<(), hqf_pdf::Error> {
    content.save_state();
    content.set_fill_space(blue);
    content.set_fill_components(&[1.0])?;
    text(
        content,
        font,
        HEAD_SIZE,
        LEFT,
        HEAD_BASELINE,
        words.list_title,
    )?;
    content.restore_state();

    content.save_state();
    content.set_fill(Rgb::gray(0.35))?;
    for (line, y) in words.list_lead.iter().zip(LEAD_BASELINES) {
        text(content, font, LEAD_SIZE, LEFT, y, line)?;
    }
    content.restore_state();

    let table = run_list(font, words, blue, gold)?;
    let placed = table.fit(TableFrame::new(LEFT, LIST_TOP, LIST_TOP - 260.0), 0)?;
    let height = placed.height();
    placed.draw(content)?;

    let first_ink = Style::new(font, TOTAL_SIZE).color(Paint::named(blue, [PROSE_TINT]));
    let second_ink = Style::new(font, TOTAL_SIZE).color(Paint::named(gold, [1.0]));
    let note = RichText::new()
        .span(words.note[0], &first_ink)
        .span(words.note[1], &second_ink)
        .span(words.note[2], &first_ink);
    let lines = note.break_lines(WIDTH);
    let note_top = LIST_TOP - height - 26.0;
    note.draw(content, &lines, LEFT, note_top, WIDTH)?;

    let flow = TextFlow::new(font, PROSE_SIZE)
        .color(Paint::named(blue, [PROSE_TINT]))
        .leading(PROSE_LEADING);
    let prose = flow.break_lines(words.prose, WIDTH);
    content.begin_text();
    flow.draw(
        content,
        &prose,
        LEFT,
        note_top - RichText::height(&lines) - 26.0,
        WIDTH,
    )?;
    content.end_text();

    Stamp::new(font, words.stamp)
        .color(Paint::named(gold, [0.22]))
        .draw(content, LEFT, LIST_TOP - height, WIDTH, height)?;

    content.save_state();
    content.set_stroke_space(gold);
    content.set_stroke_components(&[1.0])?;
    content.set_line_width(0.8)?;
    content.move_to(LEFT, CLOSING_RULE)?;
    content.line_to(LEFT + WIDTH, CLOSING_RULE)?;
    content.stroke();
    content.set_fill_space(blue);
    content.set_fill_components(&[0.55])?;
    text(
        content,
        font,
        CLOSING_SIZE,
        LEFT,
        CLOSING_RULE - 14.0,
        &quoted(words),
    )?;
    content.restore_state();
    Ok(())
}

/// The plate of the first sheet printed in `ink`, whose space is `space`: its
/// ramp of tints alone, under what the plate is.
fn plate(
    font: &FontHandle,
    words: &Words,
    ink: &str,
    space: &ColorSpaceHandle,
    separation: PageSeparation,
) -> Result<Page, hqf_pdf::Error> {
    let mut content = Content::new();
    tint_ramp(
        &mut content,
        font,
        space,
        640.0,
        &ramp_caption(ink, words.plate),
    )?;
    let mut page = Page::a4();
    page.content = content.into_bytes();
    page.separation = Some(separation);
    Ok(page)
}

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

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

    let blue = doc.add_color_space(ColorSpace::Separation(Separation::ink(
        BLUE,
        Cmyk::new(0.92, 0.58, 0.0, 0.06),
    )));
    let gold = doc.add_color_space(Separation::ink(GOLD, Cmyk::new(0.0, 0.28, 0.95, 0.04)));

    // The colorant every plate answers to: a mark set in it goes down on all of
    // them, which is how a printer sees whether they line up. It is shown as
    // black where no press is involved, so the tint transform runs from the
    // paper to the ink.
    let registration_ink = doc.add_color_space(Separation::new(
        ALL_PLATES,
        DeviceSpace::Gray,
        Exponential::new([0.0, 1.0], 1.0)
            .endpoints(vec![1.0], vec![0.0])
            .range(vec![[0.0, 1.0]]),
    ));

    let mut content = Content::new();
    text(
        &mut content,
        &font,
        HEAD_SIZE,
        LEFT,
        HEAD_BASELINE,
        words.title,
    )?;
    content.save_state();
    content.set_fill(Rgb::gray(0.35))?;
    for (line, y) in words.lead.iter().zip(LEAD_BASELINES) {
        text(&mut content, &font, LEAD_SIZE, LEFT, y, line)?;
    }
    content.restore_state();

    tint_ramp(
        &mut content,
        &font,
        &blue,
        640.0,
        &ramp_caption(BLUE, words.first_ramp),
    )?;
    tint_ramp(
        &mut content,
        &font,
        &gold,
        530.0,
        &ramp_caption(GOLD, words.second_ramp),
    )?;

    sheet(&mut content, &font, words, &blue, &gold, &registration_ink)?;

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

    let mut second = Content::new();
    run_sheet(&mut second, &font, words, &blue, &gold)?;
    let mut page = Page::a4();
    page.content = second.into_bytes();
    doc.add_page(page)?;

    let separated = PageSeparation::new(BLUE, 1).in_space(&blue);
    doc.add_page(plate(&font, words, BLUE, &blue, separated)?)?;
    doc.add_page(plate(
        &font,
        words,
        GOLD,
        &gold,
        PageSeparation::new(GOLD, 1),
    )?)?;

    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, 4 pages, two inks at {} tints each, {} priced lines",
        bytes.len(),
        TINTS.len(),
        ITEMS.len()
    );
    Ok(())
}

#[cfg(test)]
mod tests {
    use hqf_pdf::layout::{RichText, Style, TableFrame};
    use hqf_pdf::{Cmyk, Document, Font, FontHandle, Paint, Separation, TextFlow};

    use super::{
        CELL_SIZE, CLOSING_RULE, CLOSING_SIZE, HEAD_SIZE, ITEMS, LEAD_SIZE, LEFT, LIST_TOP,
        MARK_SIZE, PANEL_TINT, PROSE_LEADING, PROSE_SIZE, PROSE_TINT, SHADED_TINT, SHEET_LINE_SIZE,
        SHEET_TITLE_SIZE, TINTS, TOTAL_SIZE, WIDTH, WORDS, Words, crosshairs, default_font,
        language, quoted, ramp_caption, run_list, tint_name,
    };

    /// The lines two languages are allowed to write the same way, where two
    /// languages spell a word alike. The names of the inks, the sheet counts
    /// and the prices are all held outside the words.
    const SPARED: [&str; 7] = [
        // Written the same way in Portuguese.
        r#""Ambas""#,
        r#""Segunda""#,
        r#"quoted_between: " e""#,
        // Written the same way in Dutch.
        r#""Beide""#,
        r#""Briefpapier, A4""#,
        r#""Briefpapier, A5""#,
        r#"stamp: "SPECIMEN""#,
    ];

    /// A4's width, in points, which is the sheet both pages are laid on.
    const PAGE_WIDTH: f64 = 595.276;

    /// The font every measurement here is taken in, which is the one the
    /// example draws with when the command line says nothing.
    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"),
        )
    }

    /// Every line the two sheets draw where they are asked to, each with the
    /// size it is set at and the room it has. What is flowed to a width is not
    /// here: it is broken, so it cannot run over.
    fn lines(words: &'static Words) -> Vec<(String, f64, f64)> {
        // The first column of the run list, less what a cell keeps clear on
        // each side.
        let item_column = WIDTH - 54.0 - 74.0 - 66.0 - 12.0;
        let plate_column = 74.0 - 12.0;

        // A line that overhangs nothing has only to stay on the sheet; one that
        // stands over a block has that block's width.
        let sheet = PAGE_WIDTH - LEFT;

        let mut measured = vec![
            (words.title.to_owned(), HEAD_SIZE, sheet),
            (words.list_title.to_owned(), HEAD_SIZE, sheet),
            (
                ramp_caption(super::BLUE, words.first_ramp),
                super::CAPTION_SIZE,
                WIDTH,
            ),
            (
                ramp_caption(super::GOLD, words.second_ramp),
                super::CAPTION_SIZE,
                WIDTH,
            ),
            (
                ramp_caption(super::BLUE, words.plate),
                super::CAPTION_SIZE,
                WIDTH,
            ),
            (
                ramp_caption(super::GOLD, words.plate),
                super::CAPTION_SIZE,
                WIDTH,
            ),
            (words.sheet_title.to_owned(), SHEET_TITLE_SIZE, WIDTH - 24.0),
            (words.sheet_line.to_owned(), SHEET_LINE_SIZE, WIDTH - 40.0),
            (words.marks.to_owned(), MARK_SIZE, WIDTH),
            (quoted(words), CLOSING_SIZE, WIDTH),
            (words.total.to_owned(), TOTAL_SIZE, item_column),
        ];
        for line in words.lead.iter().chain(&words.list_lead) {
            measured.push(((*line).to_owned(), LEAD_SIZE, sheet));
        }
        for item in words.items {
            measured.push((item.to_owned(), CELL_SIZE, item_column));
        }
        for plate in words.plates {
            measured.push((plate.to_owned(), CELL_SIZE, plate_column));
        }
        for (index, heading) in words.headings.into_iter().enumerate() {
            let room = if index == 0 {
                item_column
            } else {
                plate_column
            };
            measured.push((heading.to_owned(), CELL_SIZE, room));
        }
        measured
    }

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

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

    /// Every line above is drawn where it is asked for rather than broken to a
    /// width, so one longer than its room runs over the column beside it or off
    /// the sheet.
    #[test]
    fn every_language_writes_lines_that_fit_the_room_they_have() {
        let mut document = Document::new();
        let font = font(&mut document);

        for (named, words) in WORDS {
            for (line, size, room) in lines(words) {
                let measured = font.measure(&line, size);

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

    /// The closing paragraph is flowed, so it cannot run over on the right; it
    /// can run down over the rule at the foot of the sheet, and nothing else
    /// would say so. The list and the note above it are laid out here rather
    /// than guessed at: a language whose list stands taller pushes the
    /// paragraph down as surely as a longer paragraph does.
    #[test]
    fn every_language_keeps_the_closing_paragraph_above_the_rule() {
        let mut document = Document::new();
        let font = font(&mut document);
        let blue = document.add_color_space(Separation::ink(
            super::BLUE,
            Cmyk::new(0.92, 0.58, 0.0, 0.06),
        ));
        let gold = document.add_color_space(Separation::ink(
            super::GOLD,
            Cmyk::new(0.0, 0.28, 0.95, 0.04),
        ));

        for (named, words) in WORDS {
            let table = run_list(&font, words, &blue, &gold).expect("the columns divide the list");
            let list = table
                .fit(TableFrame::new(LEFT, LIST_TOP, LIST_TOP - 260.0), 0)
                .expect("the list is laid out from its first row")
                .height();

            let first_ink = Style::new(&font, TOTAL_SIZE).color(Paint::named(&blue, [PROSE_TINT]));
            let second_ink = Style::new(&font, TOTAL_SIZE).color(Paint::named(&gold, [1.0]));
            let note = RichText::new()
                .span(words.note[0], &first_ink)
                .span(words.note[1], &second_ink)
                .span(words.note[2], &first_ink);
            let note_lines = note.break_lines(WIDTH);

            let flow = TextFlow::new(&font, PROSE_SIZE).leading(PROSE_LEADING);
            let broken = flow.break_lines(words.prose, WIDTH);

            let top = LIST_TOP - list - 26.0 - RichText::height(&note_lines) - 26.0;
            let lowest = top - flow.height(&broken);

            assert!(
                lowest >= CLOSING_RULE,
                "the {} paragraph reaches down to {lowest:.1}, and the rule is at \
                 {CLOSING_RULE:.1}",
                named.code()
            );
        }
    }

    /// Three captions spell a tint out in words, which no language can work out
    /// from the number. Changing one of these settings has to send whoever
    /// changes it back to every language.
    #[test]
    fn the_tints_the_captions_spell_out_are_the_tints_that_are_laid_down() {
        assert!(
            (PANEL_TINT - 0.15).abs() < f64::EPSILON,
            "a caption calls the panel behind the heading fifteen percent"
        );
        assert!(
            (SHADED_TINT - 0.08).abs() < f64::EPSILON,
            "the note calls the shaded lines eight percent"
        );
        assert!(
            (PROSE_TINT - 0.9).abs() < f64::EPSILON,
            "the closing paragraph calls itself ninety percent"
        );
    }

    /// Three more captions spell a count out in words. Each is pinned to the
    /// table that produces it.
    #[test]
    fn the_counts_the_captions_spell_out_are_the_counts_that_are_drawn() {
        assert_eq!(TINTS.len(), 10, "a caption calls the ramp ten tints");
        assert_eq!(
            crosshairs(0.0, 0.0).len(),
            4,
            "a line under the sheet calls them the four crosshairs"
        );
        assert_eq!(ITEMS.len(), 9, "the last row calls the list nine items");
    }

    /// What a swatch is called is read off the tint it is laid at, so the ten
    /// of them run from a tenth to the whole plate.
    #[test]
    fn a_swatch_is_named_by_the_tint_it_is_laid_at() {
        let named: Vec<String> = TINTS.iter().map(|tint| tint_name(*tint)).collect();

        assert_eq!(named.first().map(String::as_str), Some("10%"));
        assert_eq!(named.last().map(String::as_str), Some("100%"));
    }
}