write_layers.rs

Die Rust-Datei des Beispiels „Eine Zeichnung, und jeder Leser sieht seine eigene“. Derselbe Grundriss auf sechs Ebenen, einzeln ein- und ausgeblendet: Möbel und Abstände eingeblendet, die internen Notizen ausgeblendet, bis sie gebraucht werden, und ein Entwurfs-Wasserzeichen für den Bildschirm und nicht für das Papier.

Rust 1046 Zeilen

Wofür dieses Beispiel da ist

Derselbe Grundriss muss drei Menschen erreichen: die Baustelle mit ihren Maßen, den Bauherrn ohne die internen Notizen, den Drucker ohne das Entwurfs-Wasserzeichen darüber. Drei Dateien zu erzeugen heißt, drei Dateien im Gleichklang zu halten, und an dem Tag, an dem eine davon neu erzeugt wird und die anderen nicht, baut jemand nach der falschen Zeichnung.

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
//! Draws one plan on six layers a reader can show and hide.
//!
//! The walls are the page's own ink and are always there. Everything else
//! belongs to a layer: the furniture and the dimensions start shown, the notes
//! start hidden, and the draft mark is looked at but never printed. The file
//! also says what three of the layers are: the draft mark is a watermark, a
//! grid laid over the plan says it is an aid for whoever drew it and asks to
//! start hidden, and the crop marks at the corners of the sheet are printer's
//! marks. The furniture layer names the program it came from, Plan Studio, and
//! says it holds a technical design. The chairs are one drawing placed six
//! times, and the drawing itself belongs to the furniture layer, so hiding that
//! layer hides every chair. Open the file and reading software's layers panel
//! lists all six; print it and the draft mark is gone without anyone touching
//! that panel. The name of the notes layer in the legend is a switch of its
//! own: clicking it shows the notes, and clicking it again hides them.
//!
//! The page is held in `Words`, once per language, and `HQF_PDF_LANG` picks
//! which set is drawn. A layer's name is language too: reading software's panel
//! shows it, and the legend under the plan names the same field, so the panel
//! and the page can never disagree.
//!
//! The second page sends the plan out as a sheet whose every part says what it
//! is: the paper tint is a background left out of the panel, the title block a
//! header locked under a heading of its own, the studio's mark a logo that says
//! who drew it, two stamps a family shown one at a time, and a trap line is for
//! the printer. Four marks are each shown under one policy over the title block
//! and the logo, a second set of states is offered for review, and one link
//! switches three layers at once.
//!
//! The file opens with the layers panel shown, and shows it again after full
//! screen.
//!
//! Usage: `cargo run --example write_layers -- tmp/layers.pdf [font.ttf]`
//!        `HQF_PDF_LANG=fr cargo run --example write_layers -- tmp/calques.pdf`

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

use hqf_pdf::action::{Action, LayerSwitch, LayerSwitches};
use hqf_pdf::content::Content;
use hqf_pdf::layout::{Rect, Slant, Stamp};
use hqf_pdf::metadata::xmp::{Metadata, Xmp};
use hqf_pdf::viewer::{OpenMode, SidePanel, ViewerPreferences};
use hqf_pdf::{
    Document, Drawing, Font, FontHandle, Layer, LayerConfiguration, LayerFamily, LayerHandle,
    LayerIntent, LayerPolicy, LayerPurpose, LayerRule, LayerWork, Link, Page, PageRole, 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")
}

/// The plan's outer wall: where it sits on the page and how big it is.
const PLAN: (f64, f64, f64, f64) = (80.0, 380.0, 435.0, 320.0);

/// How many layers the plan is drawn on.
const LAYER_COUNT: usize = 6;

/// How many notes the hidden layer holds.
const NOTE_COUNT: usize = 3;

/// Where each chair stands, by its lower left corner: three along each long
/// side of the table.
const CHAIRS: [(f64, f64); 6] = [
    (140.0, 662.0),
    (173.0, 662.0),
    (206.0, 662.0),
    (140.0, 578.0),
    (173.0, 578.0),
    (206.0, 578.0),
];

/// The corners of the trimmed sheet the crop marks point at.
const TRIM: (f64, f64, f64, f64) = (36.0, 36.0, 559.0, 806.0);

/// The two distances the plan is measured at. Neither is language.
const ALONG: &str = "7.25 m";
const ACROSS: &str = "5.00 m";

/// The words the page is written in, one set per language.
///
/// A layer's name is here: reading software's panel shows it, and so does the
/// legend. The two distances are not: a metre is a metre.
#[derive(Debug)]
struct Words {
    /// The line at the head of the page.
    title: &'static str,
    /// What stands under it, saying what a layer is.
    intro: &'static str,
    /// What each layer is called, in the order they are added. Reading
    /// software's panel lists these, and the legend names them again.
    layers: [&'static str; LAYER_COUNT],
    /// What the legend says each layer holds and how it starts.
    holdings: [&'static str; LAYER_COUNT],
    /// The notes the hidden layer holds, in the order they are placed.
    notes: [&'static str; NOTE_COUNT],
    /// The word the draft mark is stamped in.
    draft: &'static str,
    /// The two lines under the legend, saying that the draft mark takes itself
    /// off the printed page.
    printing: [&'static str; 2],
    /// The second page's title.
    sheet_title: &'static str,
    /// What the second page's layers are called: the paper, the title block,
    /// the logo, the trap line.
    sheet_layers: [&'static str; 4],
    /// The heading the title block is filed under in the panel.
    heading: &'static str,
    /// The two stamps, shown one at a time.
    stamps: [&'static str; 2],
    /// The name the panel offers the second set of layer states under.
    review: &'static str,
    /// What each of the four marks under a rule says of when it shows.
    rules: [&'static str; 4],
    /// What the title block says under the sheet's title.
    title_block: &'static str,
    /// The line that switches three layers at once.
    switches: &'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 plan, six layers",
    intro: "The walls are always there. Everything else is a layer the reader can show and hide.",
    layers: [
        "Furniture",
        "Dimensions",
        "Notes",
        "Draft",
        "Grid",
        "Crop marks",
    ],
    holdings: [
        "shown; turn it off and the rooms stand empty, chairs too",
        "shown; the distances around the plan",
        "hidden; click its name to show or hide them",
        "shown on screen, never printed; the file calls it a watermark",
        "hidden; an aid for whoever drew the plan, not part of it",
        "shown; the file calls them printer's marks",
    ],
    notes: [
        "Table seats six.",
        "Bed against the party wall.",
        "Counter, waste under.",
    ],
    draft: "DRAFT",
    printing: [
        "The draft mark carries its own answer for the printer, so nothing has to",
        "be switched off before the page is printed.",
    ],
    sheet_title: "The same plan, as a sheet that goes out",
    sheet_layers: ["Paper", "Title block", "Logo", "Traps"],
    heading: "Sheet",
    stamps: ["Approved", "Rejected"],
    review: "Review",
    rules: [
        "Shown while the title block and the logo are both shown",
        "Shown while either of them is shown",
        "Shown while either of them is hidden",
        "Shown while both of them are hidden",
    ],
    title_block: "Sheet 2 of 2, issued for review",
    switches: "Title block on, logo off, the rejection stamp flipped",
};

/// The page in French.
const FRENCH: Words = Words {
    title: "Un plan, six calques",
    intro: "Les murs sont toujours là. Tout le reste est un calque à afficher ou à masquer.",
    layers: [
        "Mobilier",
        "Cotes",
        "Notes",
        "Brouillon",
        "Grille",
        "Traits de coupe",
    ],
    holdings: [
        "affiché ; désactivez-le et les pièces sont vides, chaises comprises",
        "affiché ; les distances autour du plan",
        "masqué ; cliquez sur son nom pour les afficher ou les masquer",
        "affiché à l’écran, jamais imprimé ; le fichier le nomme filigrane",
        "masqué ; aide au tracé, qui ne fait pas partie du plan",
        "affiché ; le fichier les nomme repères d’imprimeur",
    ],
    notes: [
        "Table pour six personnes.",
        "Lit contre le mur mitoyen.",
        "Plan de travail, poubelle dessous.",
    ],
    draft: "BROUILLON",
    printing: [
        "La mention de brouillon contient sa propre consigne d’impression :",
        "rien n’est à masquer avant d’imprimer la page.",
    ],
    sheet_title: "Le même plan, en feuille émise",
    sheet_layers: ["Papier", "Cartouche", "Logo", "Grossi-maigri"],
    heading: "Feuille",
    stamps: ["Approuvé", "Refusé"],
    review: "Relecture",
    rules: [
        "Visible quand le cartouche et le logo sont tous deux visibles",
        "Visible quand l’un des deux est visible",
        "Visible quand l’un des deux est masqué",
        "Visible quand les deux sont masqués",
    ],
    title_block: "Feuille 2 sur 2, émise pour relecture",
    switches: "Cartouche affiché, logo masqué, tampon de refus basculé",
};

/// The page in Dutch.
const DUTCH: Words = Words {
    title: "Eén plattegrond, zes lagen",
    intro: "De muren zijn er altijd. Al het andere is een laag die de lezer kan tonen en verbergen.",
    layers: [
        "Meubels",
        "Maten",
        "Notities",
        "Concept",
        "Raster",
        "Snijtekens",
    ],
    holdings: [
        "getoond; zet haar uit en de kamers staan leeg, ook zonder stoelen",
        "getoond; de afstanden rond de plattegrond",
        "verborgen; klik op de naam om haar te tonen of te verbergen",
        "getoond op het scherm, nooit afgedrukt; het bestand noemt haar een watermerk",
        "verborgen; een hulp voor wie de plattegrond tekende, geen deel ervan",
        "getoond; het bestand noemt ze drukkerstekens",
    ],
    notes: [
        "De tafel heeft zes plaatsen.",
        "Bed tegen de scheidingsmuur.",
        "Aanrecht, afvalbak eronder.",
    ],
    draft: "CONCEPT",
    printing: [
        "De conceptmarkering bevat haar eigen instructie voor de printer, dus hoeft er",
        "niets uitgezet te worden voordat de pagina wordt afgedrukt.",
    ],
    sheet_title: "Dezelfde plattegrond, als blad dat de deur uitgaat",
    sheet_layers: ["Papier", "Titelblok", "Logo", "Valkuilen"],
    heading: "Blad",
    stamps: ["Goedgekeurd", "Afgekeurd"],
    review: "Beoordeling",
    rules: [
        "Getoond zolang titelblok en logo allebei getoond worden",
        "Getoond zolang een van beide getoond wordt",
        "Getoond zolang een van beide verborgen is",
        "Getoond zolang beide verborgen zijn",
    ],
    title_block: "Blad 2 van 2, uitgegeven ter beoordeling",
    switches: "Titelblok aan, logo uit, de afkeuringsstempel omgedraaid",
};

/// The page in Portuguese.
const PORTUGUESE: Words = Words {
    title: "Uma planta, seis camadas",
    intro: "As paredes estão sempre lá. Tudo o resto é uma camada que o leitor pode mostrar e ocultar.",
    layers: [
        "Mobiliário",
        "Cotas",
        "Notas",
        "Rascunho",
        "Grelha",
        "Marcas de corte",
    ],
    holdings: [
        "visível; desligue-a e as divisões ficam vazias, cadeiras incluídas",
        "visível; as distâncias à volta da planta",
        "oculta; clique no nome para a mostrar ou ocultar",
        "visível no ecrã, nunca impressa; o ficheiro chama-lhe marca de água",
        "oculta; uma ajuda para quem desenhou a planta, não parte dela",
        "visível; o ficheiro chama-lhes marcas de impressão",
    ],
    notes: [
        "A mesa tem seis lugares.",
        "Cama encostada à parede meeira.",
        "Bancada, caixote do lixo por baixo.",
    ],
    draft: "RASCUNHO",
    printing: [
        "A marca de rascunho leva a sua própria resposta para a impressora, por isso não é",
        "preciso desligar nada antes de imprimir a página.",
    ],
    sheet_title: "A mesma planta, como folha emitida",
    sheet_layers: ["Papel", "Legenda", "Logótipo", "Trapping"],
    heading: "Folha",
    stamps: ["Aprovado", "Rejeitado"],
    review: "Revisão",
    rules: [
        "Visível enquanto a legenda e o logótipo estão ambos visíveis",
        "Visível enquanto um dos dois está visível",
        "Visível enquanto um dos dois está oculto",
        "Visível enquanto os dois estão ocultos",
    ],
    title_block: "Folha 2 de 2, emitida para revisão",
    switches: "Legenda visível, logótipo oculto, o carimbo de rejeição invertido",
};

/// The page in Italian.
const ITALIAN: Words = Words {
    title: "Una pianta, sei livelli",
    intro: "I muri ci sono sempre. Tutto il resto è un livello che il lettore può mostrare e nascondere.",
    layers: [
        "Arredo",
        "Quote",
        "Note",
        "Bozza",
        "Griglia",
        "Segni di taglio",
    ],
    holdings: [
        "visibile; spegnetelo e le stanze restano vuote, sedie comprese",
        "visibile; le distanze intorno alla pianta",
        "nascosto; cliccate sul suo nome per mostrarlo o nasconderlo",
        "visibile a schermo, mai stampato; il file lo chiama filigrana",
        "nascosto; un aiuto per chi ha disegnato la pianta, non parte di essa",
        "visibile; il file li chiama segni di stampa",
    ],
    notes: [
        "Il tavolo ha sei posti.",
        "Letto contro il muro divisorio.",
        "Bancone, pattumiera sotto.",
    ],
    draft: "BOZZA",
    printing: [
        "Il segno di bozza porta la propria risposta per la stampante, così non c’è",
        "niente da spegnere prima di stampare la pagina.",
    ],
    sheet_title: "La stessa pianta, come tavola emessa",
    sheet_layers: ["Carta", "Cartiglio", "Logo", "Trapping"],
    heading: "Tavola",
    stamps: ["Approvato", "Respinto"],
    review: "Revisione",
    rules: [
        "Visibile quando il cartiglio e il logo sono entrambi visibili",
        "Visibile quando uno dei due è visibile",
        "Visibile quando uno dei due è nascosto",
        "Visibile quando entrambi sono nascosti",
    ],
    title_block: "Tavola 2 di 2, emessa per revisione",
    switches: "Cartiglio visibile, logo nascosto, il timbro di rifiuto capovolto",
};

/// The page in Spanish.
const SPANISH: Words = Words {
    title: "Un plano, seis capas",
    intro: "Los muros se ven siempre. Todo lo demás es una capa que el lector puede mostrar y ocultar.",
    layers: [
        "Muebles",
        "Cotas",
        "Notas",
        "Borrador",
        "Cuadrícula",
        "Marcas de corte",
    ],
    holdings: [
        "visible; al apagarla las habitaciones quedan vacías, sillas incluidas",
        "visible; las distancias alrededor del plano",
        "oculta; haga clic en su nombre para mostrarla u ocultarla",
        "visible en pantalla, nunca impresa; el archivo la llama marca de agua",
        "oculta; una ayuda para quien dibujó el plano, no parte de él",
        "visible; el archivo las llama marcas de imprenta",
    ],
    notes: [
        "La mesa tiene seis plazas.",
        "Cama contra el muro medianero.",
        "Encimera, cubo debajo.",
    ],
    draft: "BORRADOR",
    printing: [
        "La marca de borrador lleva su propia respuesta para la impresora, así que no hay",
        "que apagar nada antes de imprimir la página.",
    ],
    sheet_title: "El mismo plano, como hoja emitida",
    sheet_layers: ["Papel", "Cajetín", "Logotipo", "Reventado"],
    heading: "Hoja",
    stamps: ["Aprobado", "Rechazado"],
    review: "Revisión",
    rules: [
        "Visible mientras el cajetín y el logotipo están visibles",
        "Visible mientras uno de los dos está visible",
        "Visible mientras uno de los dos está oculto",
        "Visible mientras los dos están ocultos",
    ],
    title_block: "Hoja 2 de 2, emitida para revisión",
    switches: "Cajetín visible, logotipo oculto, el sello de rechazo invertido",
};

/// The page in German.
const GERMAN: Words = Words {
    title: "Ein Plan, sechs Ebenen",
    intro: "Die Wände sind immer da. Alles andere ist eine Ebene, die der Leser ein- und ausblenden kann.",
    layers: [
        "Möbel",
        "Maße",
        "Notizen",
        "Entwurf",
        "Raster",
        "Schnittmarken",
    ],
    holdings: [
        "sichtbar; ausgeschaltet stehen die Räume leer, nicht einmal Stühle bleiben",
        "sichtbar; die Abstände rund um den Plan",
        "verborgen; auf den Namen klicken, um sie ein- oder auszublenden",
        "auf dem Bildschirm sichtbar, nie gedruckt; die Datei nennt es ein Wasserzeichen",
        "verborgen; eine Hilfe für den Zeichner des Plans, nicht Teil davon",
        "sichtbar; die Datei nennt sie Druckermarken",
    ],
    notes: [
        "Tisch für sechs.",
        "Bett an der Brandwand.",
        "Theke, Mülleimer darunter.",
    ],
    draft: "ENTWURF",
    printing: [
        "Der Entwurfsvermerk bringt seine eigene Antwort für den Drucker mit, also muss",
        "vor dem Drucken der Seite nichts ausgeschaltet werden.",
    ],
    sheet_title: "Derselbe Plan als Blatt für den Versand",
    sheet_layers: ["Papier", "Schriftfeld", "Logo", "Überfüllungen"],
    heading: "Blatt",
    stamps: ["Freigegeben", "Abgelehnt"],
    review: "Prüfung",
    rules: [
        "Sichtbar, solange Schriftfeld und Logo beide sichtbar sind",
        "Sichtbar, solange eines von beiden sichtbar ist",
        "Sichtbar, solange eines von beiden verborgen ist",
        "Sichtbar, solange beide verborgen sind",
    ],
    title_block: "Blatt 2 von 2, zur Prüfung ausgegeben",
    switches: "Schriftfeld an, Logo aus, der Ablehnungsstempel umgeschaltet",
};

/// 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 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 walls: the outer room and the partition that splits it.
fn walls(content: &mut Content) -> Result<(), hqf_pdf::Error> {
    let (x, y, width, height) = PLAN;
    content.save_state();
    content.set_stroke(Rgb::gray(0.15))?;
    content.set_line_width(3.0)?;
    content.rect(x, y, width, height)?.stroke();
    content
        .move_to(x + 265.0, y)?
        .line_to(x + 265.0, y + 190.0)?;
    content.stroke();
    content.restore_state();
    Ok(())
}

/// Draws the furniture: a table, a bed and a counter, each a filled outline.
fn furniture(content: &mut Content) -> Result<(), hqf_pdf::Error> {
    let (x, y, _, height) = PLAN;
    let pieces = [
        (x + 40.0, y + height - 110.0, 120.0, 70.0),
        (x + 300.0, y + height - 130.0, 90.0, 110.0),
        (x + 290.0, y + 30.0, 110.0, 40.0),
    ];
    content.save_state();
    content.set_fill(Rgb::new(0.85, 0.88, 0.93))?;
    content.set_stroke(Rgb::new(0.35, 0.45, 0.6))?;
    content.set_line_width(1.0)?;
    for (px, py, pw, ph) in pieces {
        content.rect(px, py, pw, ph)?.fill();
        content.rect(px, py, pw, ph)?.stroke();
    }
    content.restore_state();
    Ok(())
}

/// Draws the grid: a line every 29 points across the plan and every 32 up it.
fn grid(content: &mut Content) -> Result<(), hqf_pdf::Error> {
    let (x, y, width, height) = PLAN;
    content.save_state();
    content.set_stroke(Rgb::new(0.55, 0.75, 0.9))?;
    content.set_line_width(0.3)?;
    let mut across = x;
    for _ in 1..15 {
        across += 29.0;
        content.move_to(across, y)?.line_to(across, y + height)?;
    }
    let mut up = y;
    for _ in 1..10 {
        up += 32.0;
        content.move_to(x, up)?.line_to(x + width, up)?;
    }
    content.stroke();
    content.restore_state();
    Ok(())
}

/// The chair every place at the table shows: a filled outline in the colours of
/// the furniture.
fn chair() -> Result<Drawing, hqf_pdf::Error> {
    let mut content = Content::new();
    content.set_fill(Rgb::new(0.85, 0.88, 0.93))?;
    content.set_stroke(Rgb::new(0.35, 0.45, 0.6))?;
    content.set_line_width(1.0)?;
    content.rect(0.5, 0.5, 13.0, 9.0)?.fill();
    content.rect(0.5, 0.5, 13.0, 9.0)?.stroke();
    Ok(Drawing::new(content, Rect::new(0.0, 0.0, 14.0, 10.0)))
}

/// Draws the crop marks: two short lines at each corner of the trimmed sheet,
/// standing off it.
fn crop_marks(content: &mut Content) -> Result<(), hqf_pdf::Error> {
    let (left, bottom, right, top) = TRIM;
    content.save_state();
    content.set_stroke(Rgb::gray(0.0))?;
    content.set_line_width(0.5)?;
    // Each corner, with the side of it the marks stand off on: a mark starts 6
    // points from the corner and ends 24 points from it.
    for (corner_x, corner_y, out_x, out_y) in [
        (left, bottom, -1.0, -1.0),
        (right, bottom, 1.0, -1.0),
        (left, top, -1.0, 1.0),
        (right, top, 1.0, 1.0),
    ] {
        let (near_x, far_x) = (6.0 * out_x, 24.0 * out_x);
        let (near_y, far_y) = (6.0 * out_y, 24.0 * out_y);
        content
            .move_to(corner_x + near_x, corner_y)?
            .line_to(corner_x + far_x, corner_y)?;
        content
            .move_to(corner_x, corner_y + near_y)?
            .line_to(corner_x, corner_y + far_y)?;
    }
    content.stroke();
    content.restore_state();
    Ok(())
}

/// Draws the dimensions: a line under the plan and one beside it, each with the
/// distance it stands for.
fn dimensions(content: &mut Content, font: &FontHandle) -> Result<(), hqf_pdf::Error> {
    let (x, y, width, height) = PLAN;
    let below = y - 26.0;
    let beside = x - 46.0;

    content.save_state();
    content.set_stroke(Rgb::new(0.2, 0.5, 0.35))?;
    content.set_fill(Rgb::new(0.2, 0.5, 0.35))?;
    content.set_line_width(0.8)?;

    content.move_to(x, below)?.line_to(x + width, below)?;
    content.move_to(x, below - 5.0)?.line_to(x, below + 5.0)?;
    content
        .move_to(x + width, below - 5.0)?
        .line_to(x + width, below + 5.0)?;
    content.move_to(beside, y)?.line_to(beside, y + height)?;
    content.move_to(beside - 5.0, y)?.line_to(beside + 5.0, y)?;
    content
        .move_to(beside - 5.0, y + height)?
        .line_to(beside + 5.0, y + height)?;
    content.stroke();

    text(
        content,
        font,
        9.0,
        x + width / 2.0 - 18.0,
        below + 8.0,
        ALONG,
    )?;
    text(content, font, 9.0, beside + 6.0, y + height / 2.0, ACROSS)?;
    content.restore_state();
    Ok(())
}

/// Draws the notes: what someone reading the plan wants said, and a reader who
/// is only looking at the rooms does not.
fn notes(
    content: &mut Content,
    font: &FontHandle,
    words: &'static Words,
) -> Result<(), hqf_pdf::Error> {
    let (x, y, _, height) = PLAN;
    let places = [
        (x + 44.0, y + height - 130.0),
        (x + 292.0, y + height - 148.0),
        (x + 292.0, y + 16.0),
    ];
    content.save_state();
    content.set_fill(Rgb::new(0.7, 0.25, 0.1))?;
    for ((nx, ny), line) in places.into_iter().zip(words.notes) {
        text(content, font, 8.0, nx, ny, line)?;
    }
    content.restore_state();
    Ok(())
}

/// The baseline of the legend's first row, and how far each row stands below
/// the one before.
const LEGEND_TOP: f64 = 300.0;
/// See [`LEGEND_TOP`].
const LEGEND_STEP: f64 = 18.0;

/// Draws the legend under the plan: what each layer holds and how it starts.
fn legend(
    content: &mut Content,
    font: &FontHandle,
    words: &'static Words,
) -> Result<(), hqf_pdf::Error> {
    let mut baseline = LEGEND_TOP;
    for (name, what) in words.layers.into_iter().zip(words.holdings) {
        text(content, font, 10.0, 80.0, baseline, name)?;
        content.save_state();
        content.set_fill(Rgb::gray(0.35))?;
        text(content, font, 10.0, 170.0, baseline, what)?;
        content.restore_state();
        baseline -= LEGEND_STEP;
    }

    content.save_state();
    content.set_fill(Rgb::gray(0.35))?;
    text(
        content,
        font,
        10.0,
        80.0,
        baseline - 12.0,
        words.printing[0],
    )?;
    text(
        content,
        font,
        10.0,
        80.0,
        baseline - 26.0,
        words.printing[1],
    )?;
    content.restore_state();
    Ok(())
}

/// Draws `body` inside `layer`, so reading software shows and hides all of it
/// at once.
fn on_layer<F>(content: &mut Content, layer: &LayerHandle, body: F) -> Result<(), hqf_pdf::Error>
where
    F: FnOnce(&mut Content) -> Result<(), hqf_pdf::Error>,
{
    content.begin_layer(layer);
    body(content)?;
    content.end_marked();
    Ok(())
}

/// The layers of the second page, and the rules and switches built on them.
struct Sheet {
    /// The paper tint behind everything, left out of the panel.
    paper: LayerHandle,
    /// The title block across the foot, locked and filed under a heading.
    title_block: LayerHandle,
    /// The studio's mark, which says what it is and who made it.
    logo: LayerHandle,
    /// The two stamps a reader shows one of at a time.
    stamps: [LayerHandle; 2],
    /// The trap line round the walls, for whoever prints the sheet.
    traps: LayerHandle,
    /// The four marks each shown under one policy over the title block and the
    /// logo.
    rules: [LayerHandle; 4],
}

/// Adds the second page's layers to `doc`, with the family, the configuration
/// and the four rules built on them.
fn second_sheet_layers(
    doc: &mut Document,
    words: &Words,
    notes: &LayerHandle,
) -> Result<Sheet, hqf_pdf::Error> {
    let paper = doc.add_layer(
        Layer::new(words.sheet_layers[0])
            .page_role(PageRole::Background)
            .listed(false),
    )?;
    let title_block = doc.add_layer(
        Layer::new(words.sheet_layers[1])
            .page_role(PageRole::HeaderFooter)
            .heading(words.heading)
            .locked(true)
            .exported(true),
    )?;
    let mut facts = Metadata::default();
    facts.title = Some(words.sheet_layers[2].to_owned());
    facts.creator = Some("HQF Development".to_owned());
    let logo = doc.add_layer(
        Layer::new(words.sheet_layers[2])
            .page_role(PageRole::Logo)
            .made_by("HQF Development", LayerWork::Artwork)
            .metadata(Xmp::Facts(Box::new(facts))),
    )?;
    let approved = doc.add_layer(Layer::new(words.stamps[0]).page_role(PageRole::Foreground))?;
    let rejected = doc.add_layer(
        Layer::new(words.stamps[1])
            .page_role(PageRole::Foreground)
            .visible(false),
    )?;
    let traps = doc.add_layer(
        Layer::new(words.sheet_layers[3])
            .purpose(LayerPurpose::Trapping)
            .intent(LayerIntent::View)
            .exported(false),
    )?;
    doc.add_layer_family(LayerFamily::new().holding(&approved).holding(&rejected))?;
    doc.add_layer_configuration(
        LayerConfiguration::new(words.review)
            .showing(notes)
            .showing(&rejected),
    )?;
    let mut rules = Vec::with_capacity(4);
    for policy in [
        LayerPolicy::AllVisible,
        LayerPolicy::AnyVisible,
        LayerPolicy::AnyHidden,
        LayerPolicy::AllHidden,
    ] {
        rules.push(doc.add_layer_rule(LayerRule::new(policy).over(&title_block).over(&logo))?);
    }
    let [all_shown, any_shown, any_hidden, all_hidden] =
        rules
            .try_into()
            .map_err(|_| hqf_pdf::Error::ImpossibleGeometry {
                what: "four rules, one per policy",
            })?;
    Ok(Sheet {
        paper,
        title_block,
        logo,
        stamps: [approved, rejected],
        traps,
        rules: [all_shown, any_shown, any_hidden, all_hidden],
    })
}

/// Draws `body` inside `layer`, with the graphics state saved around it so the
/// colours it sets stay inside the layer's run.
fn kept_on_layer<F>(
    content: &mut Content,
    layer: &LayerHandle,
    body: F,
) -> Result<(), hqf_pdf::Error>
where
    F: FnOnce(&mut Content) -> Result<(), hqf_pdf::Error>,
{
    on_layer(content, layer, |content| {
        content.save_state();
        body(content)?;
        content.restore_state();
        Ok(())
    })
}

/// The second page: a sheet whose paper, title block, logo, stamps and trap
/// line each sit on a layer that says what it is, four marks each shown under
/// its own rule over two of them, and a link that switches three at once.
fn second_sheet(font: &FontHandle, words: &Words, sheet: &Sheet) -> Result<Page, hqf_pdf::Error> {
    let mut content = Content::new();
    kept_on_layer(&mut content, &sheet.paper, |content| {
        content.set_fill(Rgb::new(0.97, 0.96, 0.92))?;
        content.rect(20.0, 20.0, 555.0, 802.0)?.fill();
        Ok(())
    })?;
    text(&mut content, font, 15.0, 80.0, 750.0, words.sheet_title)?;

    let (x, y, width, height) = PLAN;
    walls(&mut content)?;
    kept_on_layer(&mut content, &sheet.traps, |content| {
        content.set_stroke(Rgb::new(0.9, 0.2, 0.6))?;
        content.set_line_width(0.3)?;
        content
            .rect(x - 1.0, y - 1.0, width + 2.0, height + 2.0)?
            .stroke();
        Ok(())
    })?;

    for (layer, (word, color)) in sheet.stamps.iter().zip(
        words
            .stamps
            .into_iter()
            .zip([Rgb::new(0.1, 0.55, 0.2), Rgb::new(0.8, 0.15, 0.15)]),
    ) {
        kept_on_layer(&mut content, layer, |content| {
            content.set_stroke(color)?;
            content.set_line_width(2.0)?;
            content
                .rect(x + 20.0, y + height - 50.0, 140.0, 34.0)?
                .stroke();
            content.set_fill(color)?;
            text(content, font, 16.0, x + 32.0, y + height - 38.0, word)
        })?;
    }

    // The four marks, each shown only while its rule over the title block and
    // the logo holds.
    let mut top = y - 30.0;
    for (layer, said) in sheet.rules.iter().zip(words.rules) {
        kept_on_layer(&mut content, layer, |content| {
            content.set_fill(Rgb::new(0.15, 0.35, 0.8))?;
            content.rect(80.0, top - 2.0, 10.0, 10.0)?.fill();
            Ok(())
        })?;
        text(&mut content, font, 9.0, 98.0, top, said)?;
        top -= 18.0;
    }

    kept_on_layer(&mut content, &sheet.title_block, |content| {
        content.set_line_width(0.8)?;
        content.rect(80.0, 60.0, 435.0, 50.0)?.stroke();
        text(content, font, 11.0, 90.0, 90.0, words.sheet_title)?;
        text(content, font, 8.0, 90.0, 72.0, words.title_block)
    })?;
    kept_on_layer(&mut content, &sheet.logo, |content| {
        content.set_fill(Rgb::new(0.15, 0.3, 0.6))?;
        content.rect(455.0, 68.0, 34.0, 34.0)?.fill();
        content.set_fill(Rgb::gray(1.0))?;
        text(content, font, 11.0, 460.0, 80.0, "HQF")
    })?;

    text(&mut content, font, 10.0, 80.0, top - 10.0, words.switches)?;
    let mut page = Page::a4();
    page.content = content.into_bytes();
    page.links.push(Link::acting(
        78.0,
        top - 14.0,
        font.measure(words.switches, 10.0) + 4.0,
        16.0,
        Action::SwitchLayers(
            LayerSwitches::new()
                .switch(LayerSwitch::On, &sheet.title_block)
                .switch(LayerSwitch::Off, &sheet.logo)
                .switch(LayerSwitch::Toggle, &sheet.stamps[1])
                .ignoring_radio_groups(),
        ),
    ));
    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("layers")));
    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 furniture_layer =
        doc.add_layer(Layer::new(words.layers[0]).made_by("Plan Studio", LayerWork::Technical))?;
    let dimensions_layer = doc.add_layer(Layer::new(words.layers[1]))?;
    let notes_layer = doc.add_layer(Layer::new(words.layers[2]).visible(false))?;
    let draft_layer = doc.add_layer(
        Layer::new(words.layers[3])
            .on_screen(true)
            .printed(false)
            .purpose(LayerPurpose::Watermark),
    )?;
    let grid_layer = doc.add_layer(
        Layer::new(words.layers[4])
            .visible(false)
            .intent(LayerIntent::Design),
    )?;
    let marks_layer =
        doc.add_layer(Layer::new(words.layers[5]).purpose(LayerPurpose::PrintersMarks))?;
    let chair = doc.add_drawing(chair()?.layer(&furniture_layer));

    let mut content = Content::new();
    text(&mut content, &font, 15.0, 80.0, 750.0, words.title)?;
    content.save_state();
    content.set_fill(Rgb::gray(0.35))?;
    text(&mut content, &font, 10.0, 80.0, 730.0, words.intro)?;
    content.restore_state();

    walls(&mut content)?;
    on_layer(&mut content, &grid_layer, grid)?;
    on_layer(&mut content, &furniture_layer, furniture)?;
    // The chairs are placed outside any layer's run: the drawing names its
    // layer itself.
    for (x, y) in CHAIRS {
        content.draw_form(chair.name(), x, y, 1.0)?;
    }
    on_layer(&mut content, &dimensions_layer, |content| {
        dimensions(content, &font)
    })?;
    on_layer(&mut content, &notes_layer, |content| {
        notes(content, &font, words)
    })?;
    on_layer(&mut content, &draft_layer, |content| {
        let (x, y, width, height) = PLAN;
        Stamp::new(&font, words.draft)
            .slant(Slant::Up)
            .color(Rgb::new(0.8, 0.3, 0.3))
            .draw(content, x, y, width, height)
    })?;
    on_layer(&mut content, &marks_layer, crop_marks)?;
    legend(&mut content, &font, words)?;

    let mut page = Page::a4();
    page.content = content.into_bytes();
    // The notes layer's name in the legend, the third row, flips the layer.
    page.links.push(Link::acting(
        78.0,
        LEGEND_STEP.mul_add(-2.0, LEGEND_TOP) - 4.0,
        80.0,
        16.0,
        Action::SwitchLayers(LayerSwitches::new().toggle(&notes_layer)),
    ));
    doc.add_page(page)?;

    let sheet = second_sheet_layers(&mut doc, words, &notes_layer)?;
    doc.add_page(second_sheet(&font, words, &sheet)?)?;

    doc.set_open_mode(OpenMode::Layers);

    doc.set_viewer_preferences(ViewerPreferences::new().after_full_screen(SidePanel::Layers));

    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, {LAYER_COUNT} layers", bytes.len());
    Ok(())
}

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

    /// The lines two languages are allowed to write the same way. A note is a
    /// note in English and in French.
    const SPARED: [&str; 9] = [
        "\"Notes\"",
        "\"Logo\"",
        // Written the same way in German.
        r#""Papier""#,
        // Written the same way in Portuguese.
        r#""Cotas""#,
        r#""Marcas de corte""#,
        r#""Notas""#,
        r#""Papel""#,
        // Written the same way in Portuguese.
        r#""Trapping""#,
        // Written the same way in Dutch.
        r#""Raster""#,
    ];

    /// No line the second page sets runs past the paper's right margin.
    #[test]
    fn every_line_of_the_second_page_stays_on_the_paper() {
        let mut doc = hqf_pdf::Document::new();
        let font = doc.add_font(
            hqf_pdf::Font::parse(
                std::fs::read(super::default_font()).expect("the committed font is there"),
            )
            .expect("the committed font parses"),
        );
        for (_, words) in WORDS {
            let lines = words.rules.iter().map(|line| (9.0, 98.0, *line)).chain([
                (10.0, 80.0, words.switches),
                (15.0, 80.0, words.sheet_title),
                (8.0, 90.0, words.title_block),
            ]);
            for (size, left, line) in lines {
                let width = font.measure(line, size);
                assert!(left + width <= 515.0, "{line:?} runs to {}", left + width);
            }
        }
    }

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

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