write_named_regions.rs

Die Rust-Datei des Beispiels „Eine Vorlage, deren Bereiche per Name gefüllt werden“. Ein Lieferetikett, auf dem ein Gestalter vier Rechtecke benannt hat, aus seiner eigenen Datei zurückgelesen, viermal platziert und allein über diese Namen gefüllt: eine Anschrift, eine Bestellnummer, ein aus einer SVG-Datei gezeichnetes Logo und ein Rücksendeschein.

Rust 781 Zeilen

Wofür dieses Beispiel da ist

Ein Gestalter entwirft ein Lieferetikett und markiert, wohin die Dinge kommen: die Anschrift des Empfängers, das Logo des Absenders, die Bestellreferenz, eine Seite eines Faltblatts. Das Programm, das die Etiketten ausfüllt, schreibt jemand anderes, und meist kopiert es jede dieser Positionen als Zahlen in Punkt in seinen eigenen Code. An dem Tag, an dem der Gestalter das Logo verschiebt, druckt das Programm es weiterhin dort, wo es früher war.

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
//! Marks named regions on a template, reads them back out of the template's
//! file, places the template four times on a sheet with every region found
//! where it landed, and fills every region by its name.
//!
//! A designer lays a delivery label out and marks four rectangles on it: where
//! the recipient's address goes, where the sender's logo goes, where the order
//! reference goes and where a leaflet page goes. Each has a name, a kind and
//! the settings it is filled with, and the list travels inside the label's own
//! file, under the page's `/PieceInfo`.
//!
//! The program that fills the labels never sees the layout. It opens the file,
//! reads the regions back, and draws the label four times on an A4 sheet at
//! three quarters of its size. For every placement it asks where each region
//! landed, and a coloured frame is drawn there: it falls on the grey frame the
//! designer drew, placement after placement, although the label's own
//! coordinates start from a crop box ten points in from its sheet.
//!
//! Each label is then filled by the names of its regions alone: an address of
//! its own, an order number, a logo read from a drawing file in SVG, its disc
//! shaded by a gradient and its bar's corners rounded by a clip path, and
//! placed as vector artwork, and a return slip taken from another PDF. The slip
//! marks a region of its own, which lands inside the label's and is filled in
//! turn. The third label's order number is set in red, a setting the fill
//! states over the region's own.
//!
//! The last label is a proof. The program fills none of its regions: each one
//! shows the sample the designer left in it. A region of text is set with its
//! sample text; a region of a picture or of a page hands back the name of its
//! file, and the program fills it with the file of that name: the logo there is
//! the same design, as pixels.
//!
//! The terminal lists the regions read back from the file, by name, and how
//! many were filled.
//!
//! Usage: `cargo run --example write_named_regions -- tmp/named_regions.pdf`
//!        `HQF_PDF_LANG=fr cargo run --example write_named_regions -- tmp/zones.pdf`

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

use hqf_pdf::content::Content;
use hqf_pdf::image::Image;
use hqf_pdf::layout::TextFlow;
use hqf_pdf::read::{ImportedPage, PageBoundary, PageImport, Reader};
use hqf_pdf::region::{
    Region, RegionFonts, RegionKind, RegionSettings, Regions, TextFill, Unfilled,
};
use hqf_pdf::{
    Align, Anchor, Color, Document, DrawingHandle, FitMode, Font, FontHandle, ImageHandle, Page,
    PartData, Rect, Rgb,
};

#[path = "shared/out.rs"]
mod out;

#[path = "shared/licence.rs"]
mod licence;

#[path = "shared/language.rs"]
mod language;

#[path = "shared/failure.rs"]
mod failure;

use language::Language;

/// The font the example draws with.
fn default_font() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fonts")
        .join("DejaVuSans.ttf")
}

/// The margin the page is laid out inside.
const MARGIN: f64 = 56.0;

/// How far the page runs across, between the margins.
const WIDTH: f64 = 483.0;

/// The label's sheet, and the crop box it is seen at, ten points in.
const SHEET: (f64, f64) = (320.0, 220.0);
const CROP: Rect = Rect::new(10.0, 10.0, 300.0, 200.0);

/// The line round the label, half its width inside the crop box so that none of
/// it is cut away.
const EDGE: Rect = Rect::new(10.5, 10.5, 299.0, 199.0);

/// When the designer last changed the regions, in ISO 8601.
const MARKED: &str = "2026-09-28T10:00:00Z";

/// Each region: its name, its kind, and its rectangle in the label's own space,
/// as left, bottom, width and height.
const REGIONS: [(&str, RegionKind, [f64; 4]); 4] = [
    ("recipient", RegionKind::Text, [22.0, 90.0, 170.0, 70.0]),
    ("logo", RegionKind::Image, [210.0, 120.0, 88.0, 70.0]),
    ("reference", RegionKind::Text, [22.0, 26.0, 170.0, 28.0]),
    ("insert", RegionKind::Page, [210.0, 26.0, 88.0, 80.0]),
];

/// The scale the label is placed at on the sheet.
const SCALE: f64 = 0.75;

/// Where the corner of the label's crop box lands, for each placement.
const PLACED: [(f64, f64); 4] = [
    (MARGIN, 512.0),
    (314.0, 512.0),
    (MARGIN, 346.0),
    (314.0, 346.0),
];

/// The return slip's sheet, as wide for its height as the region it fills.
const SLIP: (f64, f64) = (220.0, 200.0);

/// The region the return slip marks for its code, as left, bottom, width and
/// height.
const SLIP_CODE: [f64; 4] = [20.0, 20.0, 180.0, 40.0];

/// The logo's width and height, in pixels.
const LOGO: (u32, u32) = (96, 64);

/// Who each label the program fills is addressed to, one line after another, in
/// the order the labels are placed.
const RECIPIENTS: [&str; 3] = [
    "Anna Keller\nSeestrasse 12\n8002 Zürich",
    "Tomás Ortega\nCalle Mayor 7\n28013 Madrid",
    "Claire Martin\n4 rue des Lilas\n69003 Lyon",
];

/// The order each label the program fills ships, in the order the labels are
/// placed.
const ORDERS: [&str; 3] = ["0917-1", "0917-2", "0917-3"];

/// The code each filled label's return slip carries, in the order the labels
/// are placed.
const RETURNS: [&str; 3] = ["RET-0917-1", "RET-0917-2", "RET-0917-3"];

/// The order number the designer leaves as a sample.
const SAMPLE_ORDER: &str = "0000-0";

/// The return code the designer leaves as a sample.
const SAMPLE_RETURN: &str = "RET-0000-0";

/// The file the logo region names as its sample.
const LOGO_FILE: &str = "logo.png";

/// The logo as a drawing file in SVG: the green disc and grey bar of its
/// pixels, as shapes, the disc shaded by a radial gradient lit from its upper
/// left and the bar's corners rounded by a clip path.
const LOGO_SVG: &str = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"96\" height=\"64\"><defs><radialGradient id=\"lit\" fx=\"0.35\" fy=\"0.35\"><stop offset=\"0\" stop-color=\"rgb(120, 210, 150)\"/><stop offset=\"1\" stop-color=\"rgb(26, 140, 77)\"/></radialGradient><clipPath id=\"corners\"><rect x=\"68\" y=\"16\" width=\"28\" height=\"32\" rx=\"6\"/></clipPath></defs><circle cx=\"32\" cy=\"32\" r=\"26\" fill=\"url(#lit) rgb(26, 140, 77)\"/><rect x=\"68\" y=\"16\" width=\"28\" height=\"32\" fill=\"rgb(60, 60, 60)\" clip-path=\"url(#corners)\"/></svg>";

/// The file the insert region names as its sample.
const SLIP_FILE: &str = "return-slip.pdf";

/// The colour the third label's order number is set in, over the region's own.
const URGENT: Rgb = Rgb::new(0.8, 0.1, 0.1);

/// The colour a region of each kind is framed in on the sheet.
const FRAMED: [(RegionKind, Rgb); 3] = [
    (RegionKind::Text, Rgb::new(0.1, 0.3, 0.8)),
    (RegionKind::Image, Rgb::new(0.1, 0.55, 0.3)),
    (RegionKind::Page, Rgb::new(0.85, 0.45, 0.1)),
];

/// The words the pages are written in, one set per language.
#[derive(Debug)]
struct Words {
    /// The line at the head of the sheet.
    title: &'static str,
    /// The paragraph under it.
    body: &'static str,
    /// The line at the head of the label.
    label: &'static str,
    /// The line over the list of regions.
    heading: &'static str,
    /// What each kind of region is filled with, in the order of
    /// [`RegionKind::Text`], [`RegionKind::Image`] and [`RegionKind::Page`].
    kinds: [&'static str; 3],
    /// The paragraph under the list.
    caption: &'static str,
    /// The address the designer leaves as a sample, one line after another.
    addressee: &'static str,
    /// The word before an order's number.
    order: &'static str,
    /// The line at the head of the return slip.
    slip: &'static str,
}

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

    /// The word for what a region of `kind` is filled with.
    const fn kind(&self, kind: RegionKind) -> &'static str {
        match kind {
            RegionKind::Image => self.kinds[1],
            RegionKind::Page => self.kinds[2],
            _ => self.kinds[0],
        }
    }
}

/// The pages in English.
const ENGLISH: Words = Words {
    title: "Named regions, read back from a template",
    body: "A designer marked four rectangles on this delivery label and named them. \
           Each one says what it is filled with and how, and the list travels inside \
           the label's own file. The program below never saw the layout: it read the \
           regions back from the file, placed the label four times, and asked where \
           each region landed. Every coloured frame is its answer.",
    label: "Delivery label",
    heading: "The regions the label's file carries",
    kinds: ["text", "picture", "page"],
    caption: "Each coloured frame falls on the grey frame the designer drew, although \
              the label is drawn at three quarters of its size and its own coordinates \
              start from a crop box ten points in from its sheet. Filling a region is \
              then a matter of its name alone. That is how the program filled each \
              label: an address, an order number, a logo and a return slip taken from \
              another PDF, each set as its region says. The last label is a proof: the \
              program filled none of its regions, so each one shows the sample the \
              designer left in it. The address and the order number are placeholder \
              text, and the logo and the return slip are the files their regions name.",
    addressee: "Recipient's name\nStreet and number\nPostcode and town",
    order: "Order",
    slip: "Return slip",
};

/// The pages in French.
const FRENCH: Words = Words {
    title: "Zones nommées, relues dans un modèle",
    body: "Un graphiste a délimité quatre rectangles sur cette étiquette de livraison \
et leur a donné un nom. Chacun précise ce qu’il doit recevoir et de quelle manière, \
et la liste est enregistrée dans le fichier même de l’étiquette. Le programme \
ci-dessous n’a jamais vu la mise en page : il a relu les zones dans le fichier, \
posé l’étiquette quatre fois et demandé où chaque zone se retrouvait. Chaque cadre \
de couleur est sa réponse.",
    label: "Étiquette de livraison",
    heading: "Les zones que contient le fichier de l’étiquette",
    kinds: ["texte", "image", "page"],
    caption: "Chaque cadre de couleur se superpose au cadre gris tracé par le graphiste, \
bien que l’étiquette soit reproduite aux trois quarts de sa taille et que ses \
propres coordonnées partent d’un recadrage situé à dix points du bord de sa \
feuille. Remplir une zone ne demande alors que son nom. Le programme a ainsi \
rempli chaque étiquette : une adresse, un numéro de commande, un logo et un bon de \
retour tiré d’un autre PDF, chacun disposé comme le prévoit sa zone. La dernière étiquette sert d’épreuve : le programme n’a rempli aucune de ses zones, si bien que chacune affiche l’exemple que le graphiste y a laissé. L’adresse et le numéro de commande y sont du faux texte, et le logo comme le bon de retour sont les fichiers que leurs zones désignent.",
    addressee: "Nom du destinataire\nNuméro et rue\nCode postal et ville",
    order: "Commande",
    slip: "Bon de retour",
};

/// The pages in Dutch.
const DUTCH: Words = Words {
    title: "Benoemde gebieden, teruggelezen uit een sjabloon",
    body: "Een ontwerper markeerde vier rechthoeken op dit verzendetiket en gaf ze een naam. Elk zegt waarmee het gevuld wordt en hoe, en de lijst reist mee in het eigen bestand van het etiket. Het programma hieronder heeft de opmaak nooit gezien: het las de gebieden uit het bestand, plaatste het etiket vier keer en vroeg waar elk gebied terechtkwam. Elk gekleurd kader is zijn antwoord.",
    label: "Verzendetiket",
    heading: "De gebieden die het bestand van het etiket bevat",
    kinds: ["tekst", "afbeelding", "pagina"],
    caption: "Elk gekleurd kader valt op het grijze kader dat de ontwerper tekende, hoewel het etiket op driekwart van zijn grootte is getekend en zijn eigen coördinaten beginnen bij een uitsnijkader tien punten binnen het blad. Een gebied vullen is dan alleen een kwestie van zijn naam. Zo vulde het programma elk etiket: een adres, een bestelnummer, een logo en een retourstrook uit een andere pdf, elk zoals zijn gebied zegt. Het laatste etiket is een proef: het programma vulde geen van zijn gebieden, zodat elk gebied het voorbeeld toont dat de ontwerper erin achterliet. Het adres en het bestelnummer zijn voorbeeldtekst; het logo en de retourstrook zijn elk het bestand waarnaar hun gebied verwijst.",
    addressee: "Naam ontvanger\nStraat en huisnummer\nPostcode en plaats",
    order: "Bestelling",
    slip: "Retourstrook",
};

/// The pages in Portuguese.
const PORTUGUESE: Words = Words {
    title: "Regiões com nome, relidas de um modelo",
    body: "Um designer marcou quatro retângulos nesta etiqueta de envio e deu-lhes nome. Cada um diz com que é preenchido e como, e a lista viaja dentro do próprio ficheiro da etiqueta. O programa abaixo nunca viu a paginação: releu as regiões do ficheiro, colocou a etiqueta quatro vezes e perguntou onde caiu cada região. Cada moldura colorida é a sua resposta.",
    label: "Etiqueta de envio",
    heading: "As regiões que o ficheiro da etiqueta leva",
    kinds: ["texto", "imagem", "página"],
    caption: "Cada moldura colorida cai sobre a moldura cinzenta que o designer desenhou, embora a etiqueta seja desenhada a três quartos do tamanho e as suas coordenadas partam de uma caixa de recorte dez pontos para dentro da folha. Preencher uma região é então só uma questão de nome. Foi assim que o programa preencheu cada etiqueta: uma morada, um número de encomenda, um logótipo e um talão de devolução tirado de outro PDF, cada um como a sua região diz. A última etiqueta é uma prova: o programa não preencheu nenhuma das suas regiões, por isso cada uma mostra o conteúdo de exemplo que o designer lá deixou. A morada e o número de encomenda são texto de exemplo; o logótipo e o talão de devolução são os ficheiros indicados pelas respetivas regiões.",
    addressee: "Nome do destinatário\nRua e número\n0000-000 Localidade",
    order: "Encomenda",
    slip: "Devolução",
};

/// The pages in Italian.
const ITALIAN: Words = Words {
    title: "Regioni con nome, rilette da un modello",
    body: "Un grafico ha segnato quattro rettangoli su questa etichetta di spedizione e ha dato loro un nome. Ciascuno dice con cosa si riempie e come, e l’elenco viaggia dentro il file stesso dell’etichetta. Il programma qui sotto non ha mai visto l’impaginazione: ha riletto le regioni dal file, ha collocato l’etichetta quattro volte e ha chiesto dove è caduta ogni regione. Ogni cornice colorata è la sua risposta.",
    label: "Etichetta di spedizione",
    heading: "Le regioni che porta il file dell’etichetta",
    kinds: ["testo", "immagine", "pagina"],
    caption: "Ogni cornice colorata cade sulla cornice grigia disegnata dal grafico, anche se l’etichetta è disegnata a tre quarti della sua dimensione e le sue coordinate partono da un riquadro di ritaglio dieci punti dentro il foglio. Riempire una regione è allora questione del solo nome. Così il programma ha riempito ogni etichetta: un indirizzo, un numero d’ordine, un logo e un tagliando di reso preso da un altro PDF, ciascuno come dice la sua regione. L’ultima etichetta, invece, è una bozza: il programma non ha riempito nessuna delle sue regioni, quindi ciascuna mostra il contenuto d’esempio che il grafico vi ha lasciato. L’indirizzo e il numero d’ordine sono testo segnaposto, mentre il logo e il tagliando di reso sono i file indicati dalle rispettive regioni.",
    addressee: "Nome del destinatario\nVia e numero civico\nCAP e città",
    order: "Ordine",
    slip: "Buono di reso",
};

/// The pages in Spanish.
const SPANISH: Words = Words {
    title: "Regiones con nombre, leídas de una plantilla",
    body: "Un diseñador marcó cuatro rectángulos en esta etiqueta de envío y les dio nombre. Cada uno dice con qué se rellena y cómo, y la lista viaja dentro del propio archivo de la etiqueta. El programa de abajo nunca vio la maqueta: leyó las regiones del archivo, colocó la etiqueta cuatro veces y preguntó dónde cayó cada región. Cada marco de color es su respuesta.",
    label: "Etiqueta de envío",
    heading: "Las regiones que lleva el archivo de la etiqueta",
    kinds: ["texto", "imagen", "página"],
    caption: "Cada marco de color cae sobre el marco gris que dibujó el diseñador, aunque la etiqueta se dibuja a tres cuartos de su tamaño y sus coordenadas empiezan en una caja de recorte diez puntos dentro de su hoja. Rellenar una región es entonces cuestión solo de su nombre. Así rellenó el programa cada etiqueta: una dirección, un número de pedido, un logotipo y un volante de devolución tomado de otro PDF, cada uno como dice su región. La última etiqueta es una prueba: el programa no rellenó ninguna de sus regiones, así que cada una muestra el ejemplo que dejó en ella el diseñador. La dirección y el número de pedido son texto de ejemplo, y el logotipo y el volante de devolución, los archivos nombrados en sus regiones.",
    addressee: "Nombre y apellidos\nCalle y número\nC. P. y localidad",
    order: "Pedido",
    slip: "Devolución",
};

/// The pages in German.
const GERMAN: Words = Words {
    title: "Benannte Bereiche, aus einer Vorlage zurückgelesen",
    body: "Eine Gestalterin hat auf diesem Lieferetikett vier Rechtecke markiert und benannt. Jedes sagt, womit und wie es gefüllt wird, und die Liste reist in der eigenen Datei des Etiketts mit. Das Programm unten hat das Layout nie gesehen: Es hat die Bereiche aus der Datei zurückgelesen, das Etikett viermal platziert und gefragt, wo jeder Bereich gelandet ist. Jeder farbige Rahmen ist seine Antwort.",
    label: "Lieferetikett",
    heading: "Die Bereiche, die die Datei des Etiketts trägt",
    kinds: ["Text", "Bild", "Seite"],
    caption: "Jeder farbige Rahmen fällt auf den grauen Rahmen, den die Gestalterin gezeichnet hat, obwohl das Etikett auf drei Viertel seiner Größe verkleinert ist und seine eigenen Koordinaten bei einem Beschnittrahmen zehn Punkt innerhalb seines Blatts beginnen. Einen Bereich zu füllen ist dann allein eine Frage seines Namens. So hat das Programm jedes Etikett gefüllt: eine Adresse, eine Bestellnummer, ein Logo und ein Rücksendeschein aus einem anderen PDF, jedes so gesetzt, wie sein Bereich es sagt. Das letzte Etikett ist ein Probeabzug: Das Programm hat keinen seiner Bereiche gefüllt, sodass jeder das Muster zeigt, das die Gestalterin darin hinterlegt hat. Die Adresse und die Bestellnummer sind Platzhaltertext, das Logo und der Rücksendeschein sind die Dateien, auf die ihre Bereiche verweisen.",
    addressee: "Name des Empfängers\nStraße und Hausnummer\nPostleitzahl und Ort",
    order: "Bestellung",
    slip: "Rückschein",
};

/// 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 rectangle of `edges`: left, bottom, width and height.
const fn rect_of(edges: [f64; 4]) -> Rect {
    Rect::new(edges[0], edges[1], edges[2], edges[3])
}

/// The settings each region is marked with, by its name.
fn settings_of(name: &str) -> RegionSettings {
    let mut settings = RegionSettings::default();
    match name {
        "recipient" => {
            settings.font = Some("Helvetica".to_owned());
            settings.size = Some(10.0);
            settings.align = Some(Align::Left);
        }
        "logo" => {
            settings.fit = Some(FitMode::Contain);
            settings.anchor = Some(Anchor::TOP_RIGHT);
        }
        "reference" => {
            settings.font = Some("Helvetica-Bold".to_owned());
            settings.size = Some(12.0);
            settings.align = Some(Align::Right);
        }
        _ => settings.fit = Some(FitMode::Cover),
    }
    settings
}

/// The sample each region is marked with, by its name, in `words`.
fn sample_of(name: &str, words: &Words) -> String {
    match name {
        "recipient" => words.addressee.to_owned(),
        "logo" => LOGO_FILE.to_owned(),
        "reference" => format!("{} {SAMPLE_ORDER}", words.order),
        _ => SLIP_FILE.to_owned(),
    }
}

/// The regions the designer marks on the label, their samples in `words`.
fn marked(words: &Words) -> Result<Regions, hqf_pdf::Error> {
    let mut regions = Regions::new(MARKED)?;
    for (name, kind, edges) in REGIONS {
        let mut region = Region::new(name, kind, rect_of(edges))?
            .with_settings(settings_of(name))?
            .with_sample(sample_of(name, words))?;
        if name == "reference" {
            region = region.with_custom(PartData::new().set("column", "order_id")?);
        }
        regions = regions.with(region)?;
    }
    Ok(regions)
}

/// The regions the return slip marks: where its code goes.
fn slip_regions() -> Result<Regions, hqf_pdf::Error> {
    let mut settings = RegionSettings::default();
    settings.font = Some("Courier-Bold".to_owned());
    settings.size = Some(7.0);
    settings.align = Some(Align::Center);
    Regions::new(MARKED)?.with(
        Region::new("code", RegionKind::Text, rect_of(SLIP_CODE))?
            .with_settings(settings)?
            .with_sample(SAMPLE_RETURN)?,
    )
}

/// The return slip, a PDF of its own: a heading, a frame, and the region its
/// code goes in.
fn return_slip(words: &Words) -> Result<Vec<u8>, hqf_pdf::Error> {
    let mut doc = Document::new();
    doc.set_license(licence::licensed());
    let font = doc.add_font(Font::parse(fs::read(default_font())?)?);

    let mut content = Content::new();
    content.save_state();
    content.set_fill(Rgb::new(0.96, 0.92, 0.84))?;
    content.rect(0.0, 0.0, SLIP.0, SLIP.1)?;
    content.fill();
    content.restore_state();
    frame(
        &mut content,
        Rect::new(4.0, 4.0, 212.0, 192.0),
        Rgb::gray(0.3),
        4.0,
    )?;
    write(&mut content, &font, 20.0, 150.0, 24.0, words.slip)?;
    frame(&mut content, rect_of(SLIP_CODE), Rgb::gray(0.5), 3.0)?;

    let mut page = Page::new(SLIP.0, SLIP.1);
    page.content = content.into_bytes();
    page.regions = Some(slip_regions()?);
    doc.add_page(page)?;
    doc.to_bytes()
}

/// The logo's pixels, row after row from the top: a green disc on white beside
/// a grey bar.
fn logo_pixels() -> Vec<u8> {
    let (wide, tall) = LOGO;
    let mut pixels = Vec::new();
    for y in 0..tall {
        for x in 0..wide {
            let (dx, dy) = (i64::from(x) - 32, i64::from(y) - 32);
            let pixel = if dx * dx + dy * dy <= 676 {
                [26, 140, 77]
            } else if x >= 68 && (16..48).contains(&y) {
                [60, 60, 60]
            } else {
                [255, 255, 255]
            };
            pixels.extend(pixel);
        }
    }
    pixels
}

/// Refuses a text a region chain left words of.
fn all_placed(filled: &TextFill, what: &str) -> Result<(), Box<dyn std::error::Error>> {
    if filled.is_complete() {
        Ok(())
    } else {
        Err(format!("the {what} does not fit its region").into())
    }
}

/// What every label is filled with besides its own address, order and code.
struct Filling<'a> {
    /// The words the pages are written in.
    words: &'a Words,
    /// The logo, as pixels.
    logo: ImageHandle,
    /// The logo, as vector artwork.
    artwork: DrawingHandle,
    /// The return slip's file.
    slip: &'a Reader,
    /// The return slip's page, brought into the sheet's document.
    slip_form: &'a ImportedPage,
}

/// Fills the regions of the label placed `placement`th, each where `landed`
/// says it landed, and the region of the return slip filled into it; says how
/// many regions it filled.
fn fill_label(
    doc: &mut Document,
    content: &mut Content,
    fonts: &mut RegionFonts,
    landed: &Regions,
    placement: usize,
    filling: &Filling<'_>,
) -> Result<usize, Box<dyn std::error::Error>> {
    let none = RegionSettings::default();
    let address = landed.fill_text(
        doc,
        content,
        fonts,
        "recipient",
        RECIPIENTS[placement],
        &none,
    )?;
    all_placed(&address, "address")?;
    let mut reference = RegionSettings::default();
    if placement + 1 == RECIPIENTS.len() {
        reference.color = Some(Color::Rgb(URGENT));
    }
    let order = format!("{} {}", filling.words.order, ORDERS[placement]);
    let number = landed.fill_text(doc, content, fonts, "reference", &order, &reference)?;
    all_placed(&number, "order number")?;
    landed.fill_drawing(content, "logo", &filling.artwork, &none)?;
    let inserted = landed.fill_page(content, "insert", filling.slip_form, filling.slip, &none)?;
    let inner = inserted
        .regions()
        .ok_or("the return slip carries no regions")?;
    let code = inner.fill_text(doc, content, fonts, "code", RETURNS[placement], &none)?;
    all_placed(&code, "return code")?;
    Ok(landed.len() + inner.len())
}

/// Fills none of the regions of the proof label, each where `landed` says it
/// landed: every region of text shows its sample, and every region of a picture
/// or of a page is filled with the file its sample names. Says how many regions
/// it filled.
fn proof_label(
    doc: &mut Document,
    content: &mut Content,
    fonts: &mut RegionFonts,
    landed: &Regions,
    filling: &Filling<'_>,
) -> Result<usize, Box<dyn std::error::Error>> {
    let none = RegionSettings::default();
    let mut filled = 0;
    for region in landed.fill_samples(doc, content, fonts, &[], Unfilled::Sample)? {
        match region.sample() {
            Some(LOGO_FILE) => {
                landed.fill_image(content, region.name(), &filling.logo, &none)?;
            }
            Some(SLIP_FILE) => {
                let inserted = landed.fill_page(
                    content,
                    region.name(),
                    filling.slip_form,
                    filling.slip,
                    &none,
                )?;
                let inner = inserted
                    .regions()
                    .ok_or("the return slip carries no regions")?;
                inner.fill_samples(doc, content, fonts, &[], Unfilled::Sample)?;
                filled += inner.len();
            }
            _ => {}
        }
        filled += 1;
    }
    Ok(filled)
}

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

/// Writes a paragraph in a column the width of the page.
fn paragraph(
    content: &mut Content,
    font: &FontHandle,
    size: f64,
    top: f64,
    color: Rgb,
    text: &str,
) -> Result<(), hqf_pdf::Error> {
    let flow = TextFlow::new(font, size).leading(size * 1.4).color(color);
    let lines = flow.break_lines(text, WIDTH);
    content.begin_text();
    flow.draw(content, &lines, MARGIN, top, WIDTH)?;
    content.end_text();
    Ok(())
}

/// Frames `rect` in `color`, with a line `width` wide.
fn frame(content: &mut Content, rect: Rect, color: Rgb, width: f64) -> Result<(), hqf_pdf::Error> {
    content.save_state();
    content.set_stroke(color)?;
    content.set_line_width(width)?;
    content.rect(rect.x(), rect.y(), rect.width(), rect.height())?;
    content.stroke();
    content.restore_state();
    Ok(())
}

/// The label as the designer made it: its sheet, its crop box, a heading, a
/// grey frame round each region, and the regions themselves.
fn template(words: &Words) -> Result<Vec<u8>, hqf_pdf::Error> {
    let mut doc = Document::new();
    doc.set_license(licence::licensed());
    let font = doc.add_font(Font::parse(fs::read(default_font())?)?);

    let mut content = Content::new();
    frame(&mut content, EDGE, Rgb::gray(0.2), 1.0)?;
    write(&mut content, &font, 22.0, 186.0, 11.0, words.label)?;
    for (_, _, edges) in REGIONS {
        frame(&mut content, rect_of(edges), Rgb::gray(0.6), 2.0)?;
    }

    let mut page = Page::new(SHEET.0, SHEET.1);
    page.crop = Some(CROP);
    page.content = content.into_bytes();
    page.regions = Some(marked(words)?);
    doc.add_page(page)?;
    doc.to_bytes()
}

fn main() -> std::process::ExitCode {
    failure::reported(run())
}

fn run() -> Result<(), Box<dyn std::error::Error>> {
    let language = Language::from_environment()?;
    let words = Words::of(language);

    // A named file is written as named; the default one carries the language,
    // so the two languages do not overwrite each other in `tmp/`.
    let out = env::args()
        .nth(1)
        .unwrap_or_else(|| language.file_name(&out::default_path("named_regions")));

    let label = Reader::new(template(words)?)?;
    let found = label.regions(0)?.ok_or("the label carries no regions")?;
    let names: Vec<&str> = found.iter().map(Region::name).collect();
    println!(
        "the label's file carries {} regions: {}",
        found.len(),
        names.join(", ")
    );

    let mut doc = Document::new();
    doc.set_license(licence::licensed());
    let font = doc.add_font(Font::parse(fs::read(default_font())?)?);
    let form = doc.import_page_from(&label, &PageImport::new(0).boundary(PageBoundary::Crop))?;
    let corner = form.box_();
    let slip = Reader::new(return_slip(words)?)?;
    let slip_form = doc.import_page(&slip, 0)?;
    let filling = Filling {
        words,
        logo: doc.add_image(Image::from_rgb(LOGO.0, LOGO.1, &logo_pixels())?),
        artwork: doc.add_svg(LOGO_SVG)?.drawing().clone(),
        slip: &slip,
        slip_form: &slip_form,
    };
    let mut fonts = RegionFonts::new();
    let mut filled = 0;

    let mut content = Content::new();
    write(&mut content, &font, MARGIN, 780.0, 16.0, words.title)?;
    paragraph(&mut content, &font, 10.0, 766.0, Rgb::BLACK, words.body)?;

    for (placement, (x, y)) in PLACED.into_iter().enumerate() {
        // The form draws from the corner of the box it was taken at, so the
        // corner is taken off the point the crop box is meant to land on.
        let (at_x, at_y) = (corner.x().mul_add(-SCALE, x), corner.y().mul_add(-SCALE, y));
        content.draw_form(form.name(), at_x, at_y, SCALE)?;
        let landed = form
            .regions(&label, at_x, at_y, SCALE)?
            .ok_or("the label carries no regions")?;
        for region in &landed {
            let color = FRAMED
                .iter()
                .find(|(kind, _)| *kind == region.kind())
                .map_or(Rgb::BLACK, |(_, color)| *color);
            frame(&mut content, region.rect(), color, 0.75)?;
        }
        filled += if placement < RECIPIENTS.len() {
            fill_label(
                &mut doc,
                &mut content,
                &mut fonts,
                &landed,
                placement,
                &filling,
            )?
        } else {
            proof_label(&mut doc, &mut content, &mut fonts, &landed, &filling)?
        };
    }

    write(&mut content, &font, MARGIN, 318.0, 11.0, words.heading)?;
    let mut line = 300.0;
    for region in &found {
        let rect = region.rect();
        let said = format!(
            "{}  ·  {}  ·  {:.0} {:.0} {:.0} × {:.0}",
            region.name(),
            words.kind(region.kind()),
            rect.x(),
            rect.y(),
            rect.width(),
            rect.height()
        );
        write(&mut content, &font, MARGIN, line, 9.0, &said)?;
        line -= 14.0;
    }
    paragraph(
        &mut content,
        &font,
        8.5,
        232.0,
        Rgb::gray(0.35),
        words.caption,
    )?;

    let mut page = Page::a4();
    page.content = content.into_bytes();
    form.add_to(&mut page);
    filling.slip_form.add_to(&mut page);
    doc.add_page(page)?;

    let bytes = doc.to_bytes()?;
    if let Some(parent) = Path::new(&out).parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(&out, &bytes)?;
    println!(
        "wrote {out}: {} bytes, the label placed {} times, {filled} regions filled",
        bytes.len(),
        PLACED.len()
    );
    Ok(())
}

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

    /// The lines two languages write the same way: a page is a page in both.
    const SPARED: [&str; 4] = [
        "\"page\"",
        // Written the same way in Portuguese.
        r#""página""#,
        r#""texto""#,
        // Written the same way in Dutch.
        r#""pagina""#,
    ];

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

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

    /// The title, the heading and the label's own heading are drawn where they
    /// are put, and a line past its room runs off the sheet or over the label.
    #[test]
    fn every_language_keeps_each_line_within_the_room_it_has() {
        let mut document = Document::new();
        let font = document.add_font(
            Font::parse(std::fs::read(default_font()).expect("the font is committed"))
                .expect("the font parses"),
        );
        for (named, words) in WORDS {
            for (line, size, room) in [
                (words.title, 16.0, WIDTH),
                (words.heading, 11.0, WIDTH),
                (words.label, 11.0, 186.0),
                (words.slip, 24.0, 180.0),
            ] {
                let measured = font.measure(line, size);
                assert!(
                    measured <= room,
                    "the {} page draws {line:?} over {measured:.1} points, and it has \
                     {room:.1}",
                    named.code()
                );
            }
        }
    }
}