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
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267 | //! Writes a shelf card that marks its lines the three ways a text can be
//! marked, and sets its heading a second time behind itself.
//!
//! A rule under the words, a rule through the letters, a rule over the top: the
//! engine takes all three from the font rather than from a coordinate, so they
//! follow whatever size the text is set in. The rule over the total is the one
//! placed by hand: heavier, and standing clear of the figures. The heading and
//! the mark across the foot each carry a shadow — the same words set once more,
//! offset and paler, under the words themselves. The heading's letters are
//! pushed apart and the standfirst's narrowed, both counted where the lines are
//! broken.
//!
//! The back of the card sets the rest of what spaces a text: the words of one
//! line pushed apart, one paragraph set close and then set wide, a figure
//! lifted above the line to call a note, and the mark's words drawn hollow.
//! Word spacing moves the pen only at the byte a one-byte encoding gives the
//! space, so the face is added a second time, reached through such an encoding,
//! for the one line that shows it. Its last part rules a plain paragraph: one
//! line underlined, then hollow words set on a band laid down before them.
//!
//! A third page paints one word each of the four ways a text can be painted
//! without clipping what follows: filled, outlined, both, and not at all.
//!
//! Every word is held in `Words`, once per language, and `HQF_PDF_LANG` picks
//! which set is set. The prices are not words: they stand in one table, shared
//! by both languages.
//!
//! Usage: `cargo run --example write_text_ornaments -- tmp/ornaments.pdf
//! [font.ttf]`
//! `HQF_PDF_LANG=fr cargo run --example write_text_ornaments`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::color::Rgb;
use hqf_pdf::content::Content;
use hqf_pdf::cos::Name;
use hqf_pdf::font::encoding::{Encoding, SimpleEncoding};
use hqf_pdf::layout::{Align, Shadow, Slant, Stacking, Stamp};
use hqf_pdf::{Document, Font, FontHandle, Page, RichText, Style, TextFlow, TextRendering};
#[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: the
/// one committed for the tests, so that the example runs on any machine.
fn default_font() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fonts")
.join("DejaVuSans.ttf")
}
/// Where the card's text begins, in points from the left edge of the page.
const LEFT: f64 = 56.0;
/// How wide it runs.
const WIDTH: f64 = 483.0;
/// The size the heading is set in.
const TITLE_SIZE: f64 = 32.0;
/// What every letter of the heading takes on top of the width the face gives
/// it, in points.
const TITLE_TRACKING: f64 = 1.5;
/// The size the standfirst and the closing note are set in.
const NOTE_SIZE: f64 = 10.5;
/// How wide the letters of the standfirst are drawn, as a percentage of the
/// width the face gives them.
const STANDFIRST_SCALING: f64 = 90.0;
/// The size a line of the shelf is set in.
const ROW_SIZE: f64 = 13.0;
/// The top of the heading's box.
const TITLE_TOP: f64 = 800.0;
/// The top of the standfirst.
const STANDFIRST_TOP: f64 = 748.0;
/// Where the line under the standfirst is drawn.
const HEAD_RULE: f64 = 730.0;
/// The top of the first line of the shelf.
const FIRST_ROW: f64 = 720.0;
/// How far apart the lines of the shelf sit.
const ROW_STEP: f64 = 26.0;
/// The top of the line that adds the three together.
const TOTAL_TOP: f64 = 626.0;
/// The top of the first line of the legend.
const LEGEND_TOP: f64 = 570.0;
/// How far apart the lines of the legend sit.
const LEGEND_STEP: f64 = 24.0;
/// How far into the box the legend's marked words are set.
const LEGEND_INDENT: f64 = 24.0;
/// The top of the closing note.
const NOTE_TOP: f64 = 470.0;
/// The box the mark is laid across: its lower-left corner and its size.
const MARK_BOX: [f64; 4] = [LEFT, 100.0, WIDTH, 230.0];
/// The size the heading on the back of the card is set in.
const BACK_TITLE_SIZE: f64 = 22.0;
/// The top of the heading on the back of the card.
const BACK_TITLE_TOP: f64 = 800.0;
/// The top of the words the back of the card opens each of its five parts with.
const LABEL_TOPS: [f64; 5] = [740.0, 648.0, 490.0, 398.0, 288.0];
/// How far under its words each part sets what it shows.
const LABEL_GAP: f64 = 22.0;
/// What every blank of the spaced line takes on top of a space, in points.
const WORD_SPACING: f64 = 6.0;
/// How far apart the two lines of the first part sit.
const SAMPLE_STEP: f64 = 22.0;
/// The two leadings the paragraph is set at, close and then wide, in points.
const LEADINGS: [f64; 2] = [12.6, 21.0];
/// The gutter between the two settings of the paragraph.
const GUTTER: f64 = 24.0;
/// The size the figure calling the note is set in.
const CALL_SIZE: f64 = 8.0;
/// How far above the baseline the figure calling the note stands, in points.
const CALL_RISE: f64 = 5.0;
/// The figure that calls the note, the same in every language.
const CALL: &str = "1";
/// How far under the line that calls it the note stands.
const NOTE_STEP: f64 = 20.0;
/// The size the hollow words are set in.
const HOLLOW_SIZE: f64 = 44.0;
/// How far under its words the baseline of the hollow words stands.
const HOLLOW_DROP: f64 = 58.0;
/// The width of the pen that traces the hollow letters.
const HOLLOW_PEN: f64 = 0.9;
/// The size the words set on a band are set in.
const BAND_SIZE: f64 = 22.0;
/// The band under those words: its top this far above the baseline, in points,
/// and this thick.
const BAND: (f64, f64) = (17.0, 20.0);
/// How far under the underlined line the words set on a band stand.
const BAND_STEP: f64 = 24.0;
/// The top of the note at the foot of the back of the card.
const BACK_NOTE_TOP: f64 = 196.0;
/// The words the card is written in, one set per language.
#[derive(Debug)]
struct Words {
/// What stands across the head of the card.
title: &'static str,
/// The line under it, which says what the card is showing.
standfirst: &'static str,
/// What each line of the shelf is called, in the order they are set.
goods: [&'static str; 3],
/// What stands against the sum of the three.
total_label: &'static str,
/// What opens the legend.
legend: &'static str,
/// The three marks, each named by words set the way the name says.
under: &'static str,
through: &'static str,
over: &'static str,
/// The note at the foot, which says where each mark comes from.
note: &'static str,
/// What the mark laid across the card reads.
mark: &'static str,
/// What stands across the head of the back of the card.
back_title: &'static str,
/// What opens each of the five parts of the back: words, lines, the lifted
/// figure, the hollow letters and the rules.
labels: [&'static str; 5],
/// The line set once as the face spaces it and once with its words pushed
/// apart.
spaced: &'static str,
/// The paragraph set close and then set wide.
paragraph: &'static str,
/// The line the lifted figure calls a note from.
called: &'static str,
/// The note the lifted figure calls.
called_note: &'static str,
/// The line set underlined.
underlined: &'static str,
/// The words set hollow on a band.
banded: &'static str,
/// The note at the foot of the back, which says how each part is made.
back_note: &'static str,
/// What stands across the head of the third page.
modes_title: &'static str,
/// The word the third page paints each way.
painted: &'static str,
/// What each way of painting is called, in the order `MODES` lists them.
modes: [&'static str; 4],
}
impl Words {
/// The words the card is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
/// The three marks, in the order the legend sets them.
const fn marks(&self) -> [&str; 3] {
[self.under, self.through, self.over]
}
}
/// The card in English.
const ENGLISH: Words = Words {
title: "The half-price shelf",
standfirst: "What a shop marks by hand, set by the engine instead.",
goods: [
"Atlas of the northern coast",
"Field guide to the estuary birds",
"The lighthouse keepers, a history",
],
total_label: "The three together",
legend: "The three marks, each set the way it names:",
under: "a rule under the words",
through: "a rule through the letters",
over: "a rule over the top",
note: "One rule on this card was placed by hand: the heavier one over the \
total, standing clear of the figures. The one under the words and the one \
through the letters sit where the face itself says they go, and the one over \
the legend's last line rides the ascender the face states; those three follow \
the size the text is set in. The heading carries a copy of itself behind it, \
offset and paler, which is what keeps large lettering readable over a \
photograph. The mark laid across the foot carries one too, turned with it, so \
the copy falls the same way on the sheet as it does under the heading. The \
heading's letters are pushed a point and a half apart, and the standfirst is \
narrowed to nine tenths of the width the face gives it; each line is measured \
that way as well, so neither runs past the card.",
mark: "HALF PRICE",
back_title: "On the back: spaced, raised, hollow",
labels: [
"Words pushed apart: the same line, then six points more at every space.",
"Lines pushed apart: one paragraph, set close and then set wide.",
"A figure raised above the line, the way a note is called:",
"Letters drawn hollow: only their outline is traced.",
"Rules along the text: a line underlined, then a band under hollow letters.",
],
spaced: "Three books for the price of one and a half.",
paragraph: "Every book on this shelf came back unread or was opened once in the \
shop. The pages are clean, the spines unbroken, and the price is half the one \
printed on the cover.",
called: "Half price on every book of the shelf",
called_note: "While the shelf lasts.",
underlined: "Returns are accepted within thirty days.",
banded: "LAST COPIES",
back_note: "Nothing on this side is placed letter by letter. The room between \
words is a setting of the text state, and it moves the pen only at the byte a \
one-byte encoding gives the space, so the face is added a second time, reached \
through such an encoding. The leading is the distance from one baseline to the \
next. The lifted figure stands on a baseline raised five points, and is set \
smaller. The hollow letters are painted by their outline alone, in the colour \
and the width given to the pen. The underline and the band belong to the \
paragraph itself: the band goes down before the letters, which are painted \
over it.",
modes_title: "Four ways of painting a letter",
painted: "Ink",
modes: [
"filled",
"outlined",
"filled and outlined",
"invisible, yet there to be searched",
],
};
/// The card in French.
const FRENCH: Words = Words {
title: "L’étagère à moitié prix",
standfirst: "Ce qu’un commerçant marque d’ordinaire à la main, le moteur le compose ici.",
goods: [
"Atlas de la côte nord",
"Guide des oiseaux de l’estuaire",
"Les gardiens de phare, une histoire",
],
total_label: "Les trois ensemble",
legend: "Les trois traits, chacun tracé sur les mots qui le nomment :",
under: "un trait sous les mots",
through: "un trait qui barre les lettres",
over: "un trait au-dessus de la ligne",
note: "Un seul trait de cette fiche a été placé à la main : celui, plus épais, qui \
coiffe le total sans toucher les chiffres. Celui qui souligne les mots et \
celui qui barre les lettres se placent là où la police elle-même l’indique ; \
celui qui surmonte la dernière ligne de la légende repose sur la hauteur \
d’ascendante que déclare la police. Ces trois-là suivent la taille du texte. \
Le titre est doublé, derrière lui, d’une copie décalée et plus pâle, ce qui \
garde un grand lettrage lisible sur une photographie. Le tampon posé en \
travers du bas de la fiche en a une aussi, qui tourne avec lui : la copie \
tombe ainsi sur la feuille du même côté que sous le titre. Les lettres du \
titre sont espacées d’un point et demi de plus, et le sous-titre est ramené \
aux neuf dixièmes de la largeur que lui donne la police. Chaque ligne est \
mesurée avec ces réglages, si bien qu’aucune ne dépasse de la fiche.",
mark: "MOITIÉ PRIX",
back_title: "Au dos : espacé, en exposant, en creux",
labels: [
"Des mots espacés : la même ligne, puis six points de plus à chaque espace.",
"Des lignes espacées : un même paragraphe, d’abord serré, puis aéré.",
"Un chiffre en exposant, comme un appel de note :",
"Des lettres creuses : seul leur contour est tracé.",
"Des traits : une ligne soulignée, puis une bande sous des lettres creuses.",
],
spaced: "Trois livres pour le prix d’un et demi.",
paragraph: "Chaque livre de cette étagère a été rapporté sans avoir été lu, ou n’a été \
ouvert qu’une fois en librairie. Les pages sont propres, les dos intacts, et \
le prix est la moitié de celui qui est imprimé sur la couverture.",
called: "Moitié prix sur tous les livres de l’étagère",
called_note: "Dans la limite des livres disponibles.",
underlined: "Les retours sont acceptés sous trente jours.",
banded: "DERNIERS EXEMPLAIRES",
back_note: "Rien sur cette face n’est placé lettre à lettre. L’espace entre les mots \
est un réglage de l’état du texte ; il n’agit que sur l’octet qu’un encodage à un octet \
attribue à l’espace. La police est donc ajoutée une seconde fois, sous un encodage de ce \
type. L’interligne est la distance d’une ligne de base à la suivante. Le chiffre en \
exposant repose sur une ligne de base remontée de cinq points, dans un corps plus petit. \
Les lettres creuses ne sont peintes que par leur contour, avec la couleur et l’épaisseur \
de trait qui leur sont données. Le soulignement et la bande appartiennent au paragraphe \
lui-même : la bande est posée avant les lettres, qui sont peintes par-dessus.",
modes_title: "Quatre façons de peindre une lettre",
painted: "Encre",
modes: [
"rempli",
"contouré",
"rempli et contouré",
"invisible, mais présent pour la recherche",
],
};
/// The card in Dutch.
const DUTCH: Words = Words {
title: "De plank met halve prijzen",
standfirst: "Wat een winkel met de hand markeert, hier door de motor gezet.",
goods: [
"Atlas van de noordkust",
"Veldgids voor de vogels van het estuarium",
"De vuurtorenwachters, een geschiedenis",
],
total_label: "De drie samen",
legend: "De drie markeringen, elk gezet zoals haar naam zegt:",
under: "een lijn onder de woorden",
through: "een lijn door de letters",
over: "een lijn over de bovenkant",
note: "Eén lijn op deze kaart is met de hand geplaatst: de zwaardere boven het totaal, vrij van de cijfers. Die onder de woorden en die door de letters staan waar de letter zelf zegt dat ze horen, en die boven de laatste regel van het onderschrift rust op de stok die de letter opgeeft; die drie volgen de grootte waarin de tekst gezet is. De kop heeft een kopie van zichzelf erachter, verschoven en bleker, en die houdt grote letters leesbaar boven een foto. De markering schuin over de voet heeft er ook een, mee gedraaid, dus valt de kopie op het blad net zo als onder de kop. De letters van de kop staan anderhalve punt uit elkaar, en de intro is versmald tot negen tiende van de breedte die de letter haar geeft; elke regel is ook zo gemeten, dus loopt geen van beide over de kaart.",
mark: "HALVE PRIJS",
back_title: "Achterkant: gespatieerd, verhoogd, hol",
labels: [
"Woorden uit elkaar: dezelfde regel, dan zes punten extra bij elke spatie.",
"Regels uit elkaar: één alinea, krap gezet en dan ruim.",
"Een cijfer boven de regel, zoals een voetnoot wordt aangeroepen:",
"Letters hol getekend: alleen hun omtrek wordt getrokken.",
"Lijnen langs de tekst: een onderstreepte regel, dan een band onder holle letters.",
],
spaced: "Drie boeken voor de prijs van anderhalf.",
paragraph: "Elk boek op deze plank kwam ongelezen terug of is één keer in de winkel geopend. De pagina’s zijn schoon, de ruggen ongebroken, en de prijs is de helft van die op het omslag.",
called: "Halve prijs op elk boek van de plank",
called_note: "Zolang de voorraad strekt.",
underlined: "Retouren worden binnen dertig dagen aanvaard.",
banded: "LAATSTE EXEMPLAREN",
back_note: "Niets aan deze kant is letter voor letter geplaatst. De ruimte tussen woorden is een instelling van de tekststatus, en verplaatst de pen alleen bij de byte die een codering van één byte aan de spatie geeft, dus wordt de letter een tweede keer toegevoegd, via zo’n codering. De regelafstand is de afstand van de ene basislijn tot de volgende. Het verhoogde cijfer staat op een basislijn vijf punten hoger, en is kleiner gezet. De holle letters worden alleen door hun omtrek geschilderd, in de kleur en de dikte die de pen meekreeg. De onderstreping en de band horen bij de alinea zelf: de band komt vóór de letters, die eroverheen geschilderd worden.",
modes_title: "Vier manieren om een letter te schilderen",
painted: "Inkt",
modes: [
"gevuld",
"omlijnd",
"gevuld en omlijnd",
"onzichtbaar, maar vindbaar bij zoeken",
],
};
/// The card in Portuguese.
const PORTUGUESE: Words = Words {
title: "Prateleira a meio preço",
standfirst: "O que uma loja marca à mão, composto pelo motor.",
goods: [
"Atlas da costa norte",
"Guia de campo das aves do estuário",
"Os faroleiros, uma história",
],
total_label: "Os três juntos",
legend: "As três marcas, cada uma composta como o seu nome diz:",
under: "um filete sob as palavras",
through: "um filete através das letras",
over: "um filete por cima",
note: "Um filete deste cartão foi colocado à mão: o mais grosso por cima do total, afastado dos números. O que fica sob as palavras e o que atravessa as letras estão onde o próprio tipo diz que vão, e o que fica sobre a última linha da legenda apoia-se no ascendente que o tipo indica; esses três seguem o corpo do texto. O título leva por trás uma cópia de si mesmo, deslocada e mais pálida, que é o que mantém legíveis as letras grandes sobre uma fotografia. A marca posta ao longo do pé também leva uma, rodada com ela, por isso a cópia cai na folha da mesma forma que sob o título. As letras do título são afastadas ponto e meio, e o subtítulo é estreitado para nove décimos da largura que o tipo lhe dá; cada linha é medida assim também, por isso nenhuma sai do cartão.",
mark: "METADE DO PREÇO",
back_title: "No verso: espaçado, elevado, oco",
labels: [
"Palavras afastadas: a mesma linha, e depois seis pontos a mais em cada espaço.",
"Linhas afastadas: um parágrafo, composto apertado e depois largo.",
"Um número elevado acima da linha, como se chama uma nota:",
"Letras desenhadas ocas: só o seu contorno é traçado.",
"Filetes ao longo do texto: uma linha sublinhada, e depois uma faixa sob letras ocas.",
],
spaced: "Três livros pelo preço de um e meio.",
paragraph: "Cada livro desta prateleira voltou por ler ou foi aberto uma vez na loja. As páginas estão limpas, as lombadas intactas, e o preço é metade do impresso na capa.",
called: "Metade do preço em todos os livros da prateleira",
called_note: "Até esgotar.",
underlined: "Aceitam-se devoluções no prazo de trinta dias.",
banded: "ÚLTIMOS EXEMPLARES",
back_note: "Nada deste lado é colocado letra a letra. O espaço entre palavras é uma definição do estado de texto, e só move a caneta no byte que uma codificação de um byte dá ao espaço, por isso o tipo é acrescentado uma segunda vez, através dessa codificação. O entrelinhamento é a distância de uma linha de base à seguinte. O número elevado assenta numa linha de base cinco pontos mais alta, e em corpo menor. As letras ocas são pintadas só pelo seu contorno, na cor e na espessura dadas à caneta. O sublinhado e a faixa pertencem ao próprio parágrafo: a faixa é posta antes das letras, que são pintadas por cima.",
modes_title: "Quatro formas de pintar uma letra",
painted: "Tinta",
modes: [
"preenchida",
"contornada",
"preenchida e contornada",
"invisível, mas presente para a pesquisa",
],
};
/// The card in Italian.
const ITALIAN: Words = Words {
title: "Lo scaffale a metà prezzo",
standfirst: "Ciò che un negozio segna a mano, composto invece dal motore.",
goods: [
"Atlante della costa settentrionale",
"Guida da campo agli uccelli dell’estuario",
"I guardiani del faro, una storia",
],
total_label: "I tre insieme",
legend: "I tre segni, ciascuno composto come dice il suo nome:",
under: "un filetto sotto le parole",
through: "un filetto attraverso le lettere",
over: "un filetto sopra",
note: "Un filetto di questo cartoncino è stato collocato a mano: quello più spesso sopra il totale, staccato dalle cifre. Quello sotto le parole e quello attraverso le lettere stanno dove dice il carattere stesso, e quello sopra l’ultima riga della legenda poggia sull’ascendente indicato dal carattere; quei tre seguono il corpo del testo. Il titolo porta dietro una copia di sé, spostata e più chiara, ed è ciò che tiene leggibili le lettere grandi su una fotografia. Il segno che attraversa il piede ne porta una anche lui, ruotata con esso, così la copia cade sul foglio come sotto il titolo. Le lettere del titolo sono allargate di un punto e mezzo, e l’occhiello è ristretto a nove decimi della larghezza che gli dà il carattere; anche ogni riga è misurata così, perciò nessuna esce dal cartoncino.",
mark: "METÀ PREZZO",
back_title: "Sul retro: spaziato, sollevato, vuoto",
labels: [
"Parole allargate: la stessa riga, poi sei punti in più a ogni spazio.",
"Righe allargate: un paragrafo, composto stretto e poi largo.",
"Una cifra sollevata sopra la riga, come si richiama una nota:",
"Lettere disegnate vuote: se ne traccia solo il contorno.",
"Filetti lungo il testo: una riga sottolineata, poi una fascia sotto lettere vuote.",
],
spaced: "Tre libri al prezzo di uno e mezzo.",
paragraph: "Ogni libro di questo scaffale è tornato non letto o è stato aperto una volta in negozio. Le pagine sono pulite, i dorsi integri, e il prezzo è la metà di quello stampato in copertina.",
called: "Metà prezzo su ogni libro dello scaffale",
called_note: "Fino a esaurimento.",
underlined: "I resi si accettano entro trenta giorni.",
banded: "ULTIME COPIE",
back_note: "Niente su questo lato è collocato lettera per lettera. Lo spazio tra le parole è un’impostazione dello stato del testo, e sposta la penna solo al byte che una codifica a un byte dà allo spazio, così il carattere è aggiunto una seconda volta, attraverso una tale codifica. L’interlinea è la distanza da una linea di base alla successiva. La cifra sollevata sta su una linea di base alzata di cinque punti, ed è in corpo minore. Le lettere vuote sono dipinte solo dal loro contorno, nel colore e nello spessore dati alla penna. La sottolineatura e la fascia appartengono al paragrafo stesso: la fascia si stende prima delle lettere, che vi sono dipinte sopra.",
modes_title: "Quattro modi di dipingere una lettera",
painted: "Inchiostro",
modes: [
"piena",
"contornata",
"piena e contornata",
"invisibile, eppure lì da cercare",
],
};
/// The card in Spanish.
const SPANISH: Words = Words {
title: "Estante a medio precio",
standfirst: "Lo que una tienda marca a mano, compuesto por el motor.",
goods: [
"Atlas de la costa norte",
"Guía de campo de las aves del estuario",
"Los fareros, una historia",
],
total_label: "Los tres juntos",
legend: "Las tres marcas, cada una compuesta como dice su nombre:",
under: "una raya bajo las palabras",
through: "una raya a través de las letras",
over: "una raya por encima",
note: "Una raya de esta tarjeta se colocó a mano: la más gruesa sobre el total, separada de las cifras. La de bajo las palabras y la que cruza las letras están donde la propia fuente dice que van, y la que va sobre la última línea de la leyenda se apoya en el ascendente que indica la fuente; esas tres siguen el cuerpo del texto. El título lleva detrás una copia de sí mismo, desplazada y más pálida, que es lo que mantiene legibles las letras grandes sobre una fotografía. La marca que cruza el pie lleva otra, girada con ella, así que la copia cae igual en la hoja que bajo el título. Las letras del título se separan punto y medio, y la entradilla se estrecha a nueve décimas del ancho que le da la fuente; cada línea también se mide así, así que ninguna se sale de la tarjeta.",
mark: "MITAD DE PRECIO",
back_title: "Al dorso: espaciado, elevado, hueco",
labels: [
"Palabras separadas: la misma línea, y luego seis puntos más en cada espacio.",
"Líneas separadas: un párrafo, compuesto apretado y luego holgado.",
"Una cifra elevada sobre la línea, como se llama a una nota:",
"Letras dibujadas huecas: solo se traza su contorno.",
"Rayas a lo largo del texto: una línea subrayada, y luego una banda bajo letras huecas.",
],
spaced: "Tres libros por el precio de uno y medio.",
paragraph: "Cada libro de esta estantería volvió sin leer o se abrió una vez en la tienda. Las páginas están limpias, los lomos intactos, y el precio es la mitad del impreso en la cubierta.",
called: "Mitad de precio en todos los libros de la estantería",
called_note: "Hasta agotar existencias.",
underlined: "Se aceptan devoluciones en treinta días.",
banded: "ÚLTIMOS EJEMPLARES",
back_note: "Nada en esta cara se coloca letra a letra. El espacio entre palabras es un ajuste del estado de texto, y solo mueve la pluma en el byte que una codificación de un byte da al espacio, así que la fuente se añade una segunda vez, a través de esa codificación. El interlineado es la distancia de una línea base a la siguiente. La cifra elevada está sobre una línea base cinco puntos más alta, y en cuerpo menor. Las letras huecas se pintan solo por su contorno, con el color y el ancho dados a la pluma. El subrayado y la banda pertenecen al propio párrafo: la banda se pone antes que las letras, que se pintan encima.",
modes_title: "Cuatro formas de pintar una letra",
painted: "Tinta",
modes: [
"rellena",
"contorneada",
"rellena y contorneada",
"invisible, pero presente para las búsquedas",
],
};
/// The card in German.
const GERMAN: Words = Words {
title: "Das Halbpreisregal",
standfirst: "Was ein Laden von Hand auszeichnet, setzt hier die Engine.",
goods: [
"Atlas der Nordküste",
"Bestimmungsbuch der Vögel des Ästuars",
"Die Leuchtturmwärter, eine Geschichte",
],
total_label: "Alle drei zusammen",
legend: "Die drei Auszeichnungen, jede so gesetzt, wie sie heißt:",
under: "eine Linie unter den Wörtern",
through: "eine Linie durch die Buchstaben",
over: "eine Linie über den Buchstaben",
note: "Eine Linie auf dieser Karte wurde von Hand gesetzt: die kräftigere über der Summe, frei von den Ziffern. Die unter den Wörtern und die durch die Buchstaben stehen dort, wo der Schnitt selbst sie vorsieht, und die über der letzten Zeile der Legende liegt auf der Oberlänge, die der Schnitt angibt; diese drei folgen der Größe, in der der Text gesetzt ist. Die Überschrift trägt eine Kopie ihrer selbst dahinter, versetzt und blasser, was große Schrift über einem Foto lesbar hält. Die quer über den Fuß gelegte Auszeichnung trägt ebenfalls eine, mit ihr gedreht, sodass die Kopie auf dem Blatt genauso fällt wie unter der Überschrift. Die Buchstaben der Überschrift stehen anderthalb Punkt auseinander, und der Vorspann ist auf neun Zehntel der Breite verengt, die der Schnitt ihm gibt; jede Zeile wird ebenso gemessen, also läuft keine über die Karte hinaus.",
mark: "HALBER PREIS",
back_title: "Rückseite: gesperrt, gehoben, hohl",
labels: [
"Wörter gesperrt: dieselbe Zeile, dann sechs Punkt mehr je Leerzeichen.",
"Zeilen gesperrt: ein Absatz, eng und dann weit gesetzt.",
"Eine hochgestellte Ziffer, wie ein Fußnotenzeichen:",
"Hohle Buchstaben: Nur ihre Kontur wird gezogen.",
"Linien am Text: unterstrichen, dann ein Band unter hohlen Buchstaben.",
],
spaced: "Drei Bücher zum Preis von anderthalb.",
paragraph: "Jedes Buch in diesem Regal kam ungelesen zurück oder wurde einmal im Laden aufgeschlagen. Die Seiten sind sauber, die Rücken ungebrochen, und der Preis ist die Hälfte des auf dem Umschlag gedruckten.",
called: "Halber Preis für jedes Buch des Regals",
called_note: "Solange das Regal reicht.",
underlined: "Rückgabe binnen dreißig Tagen möglich.",
banded: "LETZTE EXEMPLARE",
back_note: "Nichts auf dieser Seite wird Buchstabe für Buchstabe platziert. Der Raum zwischen Wörtern ist eine Einstellung des Textzustands, und er bewegt den Stift nur bei dem Byte, das eine Ein-Byte-Kodierung dem Leerzeichen gibt, also wird der Schnitt ein zweites Mal hinzugefügt, über eine solche Kodierung erreicht. Der Zeilenabstand ist der Abstand von einer Grundlinie zur nächsten. Die gehobene Ziffer steht auf einer um fünf Punkt angehobenen Grundlinie und ist kleiner gesetzt. Die hohlen Buchstaben werden allein über ihre Kontur gemalt, in Farbe und Breite des Stifts. Unterstreichung und Band gehören zum Absatz selbst: Das Band kommt vor den Buchstaben, die darübergemalt werden.",
modes_title: "Vier Arten, einen Buchstaben zu malen",
painted: "Tinte",
modes: [
"gefüllt",
"umrandet",
"gefüllt und umrandet",
"unsichtbar, aber durchsuchbar",
],
};
/// Every language the card is written in.
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),
];
/// What each line of the shelf was marked at and what it is marked at now.
///
/// Figures are not words: both languages set the same ones, and the engine
/// writes a decimal point in every language it is asked for.
const PRICES: [(&str, &str); 3] = [
("24.00 EUR", "12.00 EUR"),
("18.00 EUR", "9.00 EUR"),
("30.00 EUR", "15.00 EUR"),
];
/// What the three come to together.
const TOTAL: &str = "36.00 EUR";
/// The colour every shadow on the card is drawn in.
const fn shadow_ink() -> Rgb {
Rgb::gray(0.78)
}
/// Draws the heading, which sets itself once more behind itself.
fn heading(content: &mut Content, font: &FontHandle, words: &Words) -> Result<(), hqf_pdf::Error> {
let style = Style::new(font, TITLE_SIZE).character_spacing(TITLE_TRACKING);
let text = RichText::new()
.shadow(Shadow::new(1.5, -1.5, shadow_ink()))
.span(words.title, &style);
let lines = text.break_lines(WIDTH);
text.draw(content, &lines, LEFT, TITLE_TOP, WIDTH)
}
/// Draws the standfirst and the line under it.
fn standfirst(
content: &mut Content,
font: &FontHandle,
words: &Words,
) -> Result<(), hqf_pdf::Error> {
let flow = TextFlow::new(font, NOTE_SIZE).horizontal_scaling(STANDFIRST_SCALING);
let lines = flow.break_lines(words.standfirst, WIDTH);
content.begin_text();
flow.draw(content, &lines, LEFT, STANDFIRST_TOP, WIDTH)?;
content.end_text();
content
.set_line_width(1.0)?
.move_to(LEFT, HEAD_RULE)?
.line_to(LEFT + WIDTH, HEAD_RULE)?
.stroke();
Ok(())
}
/// Draws one line of the shelf: what it is called, then what it was marked at
/// struck through and what it is marked at now.
fn shelf_line(
content: &mut Content,
font: &FontHandle,
name: &str,
prices: (&str, &str),
top: f64,
) -> Result<(), hqf_pdf::Error> {
let plain = Style::new(font, ROW_SIZE);
let struck = Style::new(font, ROW_SIZE).strikethrough();
let left = RichText::new().span(name, &plain);
let lines = left.break_lines(WIDTH);
left.draw(content, &lines, LEFT, top, WIDTH)?;
let right = RichText::new()
.align(Align::Right)
.span(prices.0, &struck)
.span(" ", &plain)
.span(prices.1, &plain);
let lines = right.break_lines(WIDTH);
right.draw(content, &lines, LEFT, top, WIDTH)
}
/// The rule over the total: its top this far above the baseline, in points, and
/// this thick.
const TOTAL_RULE: (f64, f64) = (ROW_SIZE, 1.2);
/// Draws the line that adds the three together, the sum ruled over the top the
/// way a column of figures is ruled before it is totalled.
fn total(content: &mut Content, font: &FontHandle, words: &Words) -> Result<(), hqf_pdf::Error> {
let plain = Style::new(font, ROW_SIZE);
let ruled = Style::new(font, ROW_SIZE).overline_at(TOTAL_RULE.0, TOTAL_RULE.1);
let left = RichText::new().span(words.total_label, &plain);
let lines = left.break_lines(WIDTH);
left.draw(content, &lines, LEFT, TOTAL_TOP, WIDTH)?;
let right = RichText::new().align(Align::Right).span(TOTAL, &ruled);
let lines = right.break_lines(WIDTH);
right.draw(content, &lines, LEFT, TOTAL_TOP, WIDTH)
}
/// Draws the legend: a line that says what follows, then the three marks, each
/// named by words set the way the name says.
fn legend(content: &mut Content, font: &FontHandle, words: &Words) -> Result<(), hqf_pdf::Error> {
let plain = Style::new(font, ROW_SIZE);
let opening = RichText::new().span(words.legend, &plain);
let lines = opening.break_lines(WIDTH);
opening.draw(content, &lines, LEFT, LEGEND_TOP, WIDTH)?;
let marked = [
Style::new(font, ROW_SIZE).underline(),
Style::new(font, ROW_SIZE).strikethrough(),
Style::new(font, ROW_SIZE).overline(),
];
// The tops step down one line at a time rather than being multiplied out,
// so the Python twin adds the same numbers in the same order.
let mut top = LEGEND_TOP;
for (name, style) in words.marks().into_iter().zip(marked) {
top -= LEGEND_STEP;
let line = RichText::new().span(name, &style);
let lines = line.break_lines(WIDTH - LEGEND_INDENT);
line.draw(
content,
&lines,
LEFT + LEGEND_INDENT,
top,
WIDTH - LEGEND_INDENT,
)?;
}
Ok(())
}
/// Draws the closing note, which says where each mark comes from.
fn note(content: &mut Content, font: &FontHandle, words: &Words) -> Result<(), hqf_pdf::Error> {
let flow = TextFlow::new(font, NOTE_SIZE).align(Align::Justify);
let lines = flow.break_lines(words.note, WIDTH);
content.begin_text();
flow.draw(content, &lines, LEFT, NOTE_TOP, WIDTH)?;
content.end_text();
Ok(())
}
/// Draws the mark laid across the foot of the card, which carries a shadow of
/// its own.
fn mark(content: &mut Content, font: &FontHandle, words: &Words) -> Result<(), hqf_pdf::Error> {
let [x, y, width, height] = MARK_BOX;
Stamp::new(font, words.mark)
.slant(Slant::Up)
.color(mark_ink())
.shadow(Shadow::new(2.5, -2.5, shadow_ink()))
.draw(content, x, y, width, height)
}
/// The colour of the mark, which the hollow letters take up on the back.
const fn mark_ink() -> Rgb {
Rgb::new(0.72, 0.16, 0.16)
}
/// Sets `text` in `flow` from `top`, and gives back where it ends.
fn block(
content: &mut Content,
flow: &TextFlow,
text: &str,
at: (f64, f64),
width: f64,
) -> Result<f64, hqf_pdf::Error> {
let (left, top) = at;
let lines = flow.break_lines(text, width);
content.begin_text();
flow.draw(content, &lines, left, top, width)?;
content.end_text();
Ok(top - flow.height(&lines))
}
/// Draws the first part of the back: one line as the face spaces it, then the
/// same line with its words pushed apart.
fn spaced_words(
content: &mut Content,
coded: &FontHandle,
words: &Words,
) -> Result<(), hqf_pdf::Error> {
let top = LABEL_TOPS[0] - LABEL_GAP;
let plain = TextFlow::new(coded, ROW_SIZE);
block(content, &plain, words.spaced, (LEFT, top), WIDTH)?;
let spaced = TextFlow::new(coded, ROW_SIZE).word_spacing(WORD_SPACING);
block(
content,
&spaced,
words.spaced,
(LEFT, top - SAMPLE_STEP),
WIDTH,
)?;
Ok(())
}
/// Draws the second part of the back: the paragraph at each of the two
/// leadings, side by side.
fn spaced_lines(
content: &mut Content,
font: &FontHandle,
words: &Words,
) -> Result<(), hqf_pdf::Error> {
let top = LABEL_TOPS[1] - LABEL_GAP;
let column = (WIDTH - GUTTER) / 2.0;
let mut left = LEFT;
for leading in LEADINGS {
let flow = TextFlow::new(font, NOTE_SIZE).leading(leading);
block(content, &flow, words.paragraph, (left, top), column)?;
left += column + GUTTER;
}
Ok(())
}
/// Draws the third part of the back: a line ending on a figure lifted above it,
/// then the note the figure calls, opening on the same lifted figure.
fn lifted_figure(
content: &mut Content,
font: &FontHandle,
words: &Words,
) -> Result<(), hqf_pdf::Error> {
let baseline = LABEL_TOPS[2] - LABEL_GAP - ROW_SIZE;
content.begin_text();
content.text_position(LEFT, baseline)?;
content.set_font(font, ROW_SIZE)?;
content.show_runs(&font.runs(words.called), ROW_SIZE)?;
lifted_call(content, font)?;
content.end_text();
content.begin_text();
content.text_position(LEFT, baseline - NOTE_STEP)?;
lifted_call(content, font)?;
content.set_font(font, NOTE_SIZE)?;
let note = format!(" {}", words.called_note);
content.show_runs(&font.runs(¬e), NOTE_SIZE)?;
content.end_text();
Ok(())
}
/// Shows the figure that calls the note, smaller and lifted, where the pen
/// stands, and sets the baseline back where it was.
fn lifted_call(content: &mut Content, font: &FontHandle) -> Result<(), hqf_pdf::Error> {
content.set_font(font, CALL_SIZE)?;
content.set_text_rise(CALL_RISE)?;
content.show_runs(&font.runs(CALL), CALL_SIZE)?;
content.set_text_rise(0.0)?;
Ok(())
}
/// Draws the fourth part of the back: the mark's words traced by their outline
/// alone.
fn hollow(content: &mut Content, font: &FontHandle, words: &Words) -> Result<(), hqf_pdf::Error> {
content.save_state();
content.set_stroke(mark_ink())?;
content.set_line_width(HOLLOW_PEN)?;
content.begin_text();
content.text_position(LEFT, LABEL_TOPS[3] - HOLLOW_DROP)?;
content.set_font(font, HOLLOW_SIZE)?;
content.set_text_rendering(TextRendering::Stroke);
content.show_runs(&font.runs(words.mark), HOLLOW_SIZE)?;
content.end_text();
content.restore_state();
Ok(())
}
/// The colour of the band the hollow words are set on.
const fn band_ink() -> Rgb {
Rgb::new(1.0, 0.84, 0.3)
}
/// Draws the fifth part of the back: a plain line with its rule filled after
/// its letters, then hollow words whose band is filled before them.
fn ruled_lines(
content: &mut Content,
font: &FontHandle,
words: &Words,
) -> Result<(), hqf_pdf::Error> {
let top = LABEL_TOPS[4] - LABEL_GAP;
let underlined = TextFlow::new(font, ROW_SIZE).underline();
let lines = underlined.break_lines(words.underlined, WIDTH);
content.begin_text();
underlined.draw(content, &lines, LEFT, top, WIDTH)?;
content.end_text();
underlined.draw_rules(content, &lines, LEFT, top, WIDTH)?;
let top = top - BAND_STEP;
let banded = TextFlow::new(font, BAND_SIZE)
.color(band_ink())
.outline(HOLLOW_PEN, mark_ink())
.strikethrough_at(BAND.0, BAND.1)
.rule_stacking(Stacking::Under);
let lines = banded.break_lines(words.banded, WIDTH);
banded.draw_rules(content, &lines, LEFT, top, WIDTH)?;
content.begin_text();
banded.draw(content, &lines, LEFT, top, WIDTH)?;
content.end_text();
Ok(())
}
/// Draws the back of the card: its heading, the five parts each under the words
/// that open it, and the note at the foot.
fn back(
content: &mut Content,
font: &FontHandle,
coded: &FontHandle,
words: &Words,
) -> Result<(), hqf_pdf::Error> {
let heading = TextFlow::new(font, BACK_TITLE_SIZE);
block(
content,
&heading,
words.back_title,
(LEFT, BACK_TITLE_TOP),
WIDTH,
)?;
let opening = TextFlow::new(font, NOTE_SIZE).color(Rgb::gray(0.35));
for (label, top) in words.labels.into_iter().zip(LABEL_TOPS) {
block(content, &opening, label, (LEFT, top), WIDTH)?;
}
spaced_words(content, coded, words)?;
spaced_lines(content, font, words)?;
lifted_figure(content, font, words)?;
hollow(content, font, words)?;
ruled_lines(content, font, words)?;
let note = TextFlow::new(font, NOTE_SIZE).align(Align::Justify);
block(
content,
¬e,
words.back_note,
(LEFT, BACK_NOTE_TOP),
WIDTH,
)?;
Ok(())
}
/// The four ways a text is painted without clipping what follows, in the order
/// the third page sets them.
const MODES: [TextRendering; 4] = [
TextRendering::Fill,
TextRendering::Stroke,
TextRendering::FillAndStroke,
TextRendering::Invisible,
];
/// Where the label of the third page's first row sits, and the drop from one
/// row to the next.
const MODE_TOP: f64 = 740.0;
const MODE_STEP: f64 = 150.0;
/// The size the third page's word is painted at.
const MODE_SIZE: f64 = 54.0;
/// How far under its label a painted word's baseline sits.
const MODE_DROP: f64 = 70.0;
/// Sets `text` on one line from `x`, its baseline at `y`.
fn line_at(
content: &mut Content,
font: &FontHandle,
text: &str,
size: f64,
(x, y): (f64, f64),
) -> Result<(), hqf_pdf::Error> {
content.begin_text();
content.text_position(x, y)?;
content.set_font(font, size)?;
content.show_runs(&font.runs(text), size)?;
content.end_text();
Ok(())
}
/// Draws the third page: the word painted each of the four ways, two to a row,
/// each under what the way is called.
fn painted(content: &mut Content, font: &FontHandle, words: &Words) -> Result<(), hqf_pdf::Error> {
line_at(
content,
font,
words.modes_title,
BACK_TITLE_SIZE,
(LEFT, BACK_TITLE_TOP),
)?;
let half = WIDTH / 2.0;
let mut top = MODE_TOP;
for (index, (mode, label)) in MODES.into_iter().zip(words.modes).enumerate() {
let x = if index % 2 == 0 { LEFT } else { LEFT + half };
content.save_state();
content.set_fill(Rgb::gray(0.35))?;
line_at(content, font, label, NOTE_SIZE, (x, top))?;
content.restore_state();
let baseline = top - MODE_DROP;
content.save_state();
content.set_fill(mark_ink())?;
content.set_stroke(Rgb::gray(0.1))?;
content.set_line_width(HOLLOW_PEN)?;
content.begin_text();
content.text_position(x, baseline)?;
content.set_font(font, MODE_SIZE)?;
content.set_text_rendering(mode);
content.show_runs(&font.runs(words.painted), MODE_SIZE)?;
content.end_text();
content.restore_state();
if index % 2 == 1 {
top -= MODE_STEP;
}
}
Ok(())
}
fn main() -> std::process::ExitCode {
failure::reported(run())
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let language = Language::from_environment()?;
let words = Words::of(language);
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("text_ornaments")));
let font_path = args.next().map_or_else(default_font, PathBuf::from);
let mut doc = Document::new();
doc.set_license(licence::licensed());
doc.set_info(Name::new("Title"), words.title);
let face = Font::parse(fs::read(&font_path)?)?;
let encoding = SimpleEncoding::fitting(&face, words.spaced)?;
let coded_face = face.clone().with_encoding(Encoding::Simple(encoding))?;
let font = doc.add_font(face);
let mut content = Content::new();
heading(&mut content, &font, words)?;
standfirst(&mut content, &font, words)?;
// The tops step down one line at a time rather than being multiplied out,
// so the Python twin adds the same numbers in the same order.
let mut top = FIRST_ROW;
for (name, prices) in words.goods.into_iter().zip(PRICES) {
shelf_line(&mut content, &font, name, prices, top)?;
top -= ROW_STEP;
}
total(&mut content, &font, words)?;
legend(&mut content, &font, words)?;
note(&mut content, &font, words)?;
mark(&mut content, &font, words)?;
let mut page = Page::a4();
page.content = content.into_bytes();
doc.add_page(page)?;
let coded = doc.add_font(coded_face);
let mut content = Content::new();
back(&mut content, &font, &coded, words)?;
let mut page = Page::a4();
page.content = content.into_bytes();
doc.add_page(page)?;
let mut content = Content::new();
painted(&mut content, &font, words)?;
let mut page = Page::a4();
page.content = content.into_bytes();
doc.add_page(page)?;
let bytes = doc.to_bytes()?;
if let Some(parent) = Path::new(&out).parent() {
fs::create_dir_all(parent)?;
}
fs::write(&out, &bytes)?;
println!("wrote {out}: {} bytes", bytes.len());
Ok(())
}
#[cfg(test)]
mod tests {
use hqf_pdf::{Document, Font, RichText, Style, TextFlow};
use hqf_pdf::font::encoding::{Encoding, SimpleEncoding};
use super::{
BACK_NOTE_TOP, BACK_TITLE_SIZE, BAND, BAND_SIZE, BAND_STEP, GUTTER, LABEL_GAP, LABEL_TOPS,
LEADINGS, LEFT, LEGEND_INDENT, NOTE_SIZE, PRICES, ROW_SIZE, STANDFIRST_SCALING, TITLE_SIZE,
TITLE_TRACKING, TOTAL, WIDTH, WORD_SPACING, WORDS, default_font, language,
};
/// The test font, added to a document so that a style can be built on it.
fn handle() -> (Document, hqf_pdf::FontHandle) {
let mut doc = Document::new();
let font = Font::parse(std::fs::read(default_font()).expect("the committed font is there"))
.expect("the committed font parses");
let handle = doc.add_font(font);
(doc, handle)
}
/// The lines two languages are allowed to write the same way.
const SPARED: [&str; 1] = [
// Written the same way in Portuguese.
r#"painted: "Tinta""#,
];
#[test]
fn every_language_writes_the_card_in_its_own_words() {
let untranslated = language::untranslated_lines(&WORDS, &SPARED);
assert!(
untranslated.is_empty(),
"the card says these in more than one language: {untranslated:?}"
);
}
/// A line of the shelf that wrapped would set its name over the prices
/// beside it, and the two would read as one.
#[test]
fn every_line_of_the_shelf_stands_on_a_line_of_its_own() {
let (_doc, handle) = handle();
let plain = Style::new(&handle, ROW_SIZE);
for (_, words) in WORDS {
for name in words.goods {
let text = RichText::new().span(name, &plain);
assert_eq!(
text.break_lines(WIDTH).len(),
1,
"{name:?} does not fit the card"
);
}
let label = RichText::new().span(words.total_label, &plain);
assert_eq!(label.break_lines(WIDTH).len(), 1);
}
}
/// The heading and the standfirst are each one line across the card, set
/// the way the card sets them: letters pushed apart, letters narrowed.
#[test]
fn the_heading_and_the_standfirst_each_take_one_line() {
let (_doc, handle) = handle();
let tracked = Style::new(&handle, TITLE_SIZE).character_spacing(TITLE_TRACKING);
let narrowed = TextFlow::new(&handle, NOTE_SIZE).horizontal_scaling(STANDFIRST_SCALING);
for (_, words) in WORDS {
let heading = RichText::new().span(words.title, &tracked);
assert_eq!(heading.break_lines(WIDTH).len(), 1, "{:?}", words.title);
assert_eq!(
narrowed.break_lines(words.standfirst, WIDTH).len(),
1,
"{:?}",
words.standfirst
);
}
}
/// The prices are set against the right edge, so a price wider than the box
/// would run off the left of it rather than wrap.
#[test]
fn the_prices_and_the_sum_fit_the_room_they_are_set_in() {
let (_doc, handle) = handle();
let plain = Style::new(&handle, ROW_SIZE);
for (was, now) in PRICES {
let text = RichText::new()
.span(was, &plain)
.span(" ", &plain)
.span(now, &plain);
assert_eq!(text.break_lines(WIDTH).len(), 1);
}
assert_eq!(
RichText::new().span(TOTAL, &plain).break_lines(WIDTH).len(),
1
);
}
/// Each of the three names is set in the mark it names, so a name that
/// wrapped would leave a rule hanging under a second line.
#[test]
fn every_name_of_a_mark_is_set_on_one_line() {
let (_doc, handle) = handle();
let styles = [
Style::new(&handle, ROW_SIZE).underline(),
Style::new(&handle, ROW_SIZE).strikethrough(),
Style::new(&handle, ROW_SIZE).overline(),
];
for (_, words) in WORDS {
for (name, style) in words.marks().into_iter().zip(styles.clone()) {
let text = RichText::new().span(name, &style);
assert_eq!(
text.break_lines(WIDTH - LEGEND_INDENT).len(),
1,
"{name:?} does not fit the legend"
);
}
}
}
/// The three marks stand one above the other at the size the card sets
/// them: the rule under the words below the baseline, the one through the
/// letters over it, and the one over the top above them both.
#[test]
fn the_three_marks_the_card_shows_are_three_different_heights() {
let (_doc, handle) = handle();
let under = handle.underline(ROW_SIZE);
let through = handle.strikethrough(ROW_SIZE);
let over = handle.overline(ROW_SIZE);
assert!(under.position < 0.0, "the rule under the words is under it");
assert!(
under.position < through.position,
"the rule through the letters stands over it"
);
assert!(
through.position < over.position,
"and the rule over the top over them both"
);
}
/// The closing note is the longest thing on the card, and it has the foot
/// of the sheet to fill; a note that ran past the mark would be set over
/// it.
#[test]
fn every_language_writes_a_note_that_stops_short_of_the_mark() {
let (_doc, handle) = handle();
let flow = TextFlow::new(&handle, NOTE_SIZE);
for (named, words) in WORDS {
let lines = flow.break_lines(words.note, WIDTH);
let bottom = super::NOTE_TOP - flow.height(&lines);
assert!(
bottom > super::MARK_BOX[1] + super::MARK_BOX[3],
"the {} note reaches {bottom}, under the top of the mark",
named.code()
);
}
}
/// The heading of the back and the words that open each of its parts are
/// one line each: a second line would be set over what follows.
#[test]
fn every_line_that_opens_the_back_takes_one_line() {
let (_doc, handle) = handle();
let heading = TextFlow::new(&handle, BACK_TITLE_SIZE);
let opening = TextFlow::new(&handle, NOTE_SIZE);
for (_, words) in WORDS {
let lines = heading.break_lines(words.back_title, WIDTH);
assert_eq!(lines.len(), 1, "{:?}", words.back_title);
for label in words.labels {
assert_eq!(opening.break_lines(label, WIDTH).len(), 1, "{label:?}");
}
}
}
/// The spaced line is shown twice, once as the face spaces it and once with
/// its words pushed apart; both are one line, so the two read as the same
/// words set two ways.
#[test]
fn the_spaced_line_takes_one_line_both_ways() {
for (_, words) in WORDS {
let face =
Font::parse(std::fs::read(default_font()).expect("the committed font is there"))
.expect("the committed font parses");
let encoding = SimpleEncoding::fitting(&face, words.spaced).expect("one line fits");
let coded = face
.with_encoding(Encoding::Simple(encoding))
.expect("the face takes the encoding");
let mut doc = Document::new();
let handle = doc.add_font(coded);
let plain = TextFlow::new(&handle, ROW_SIZE);
let spaced = TextFlow::new(&handle, ROW_SIZE).word_spacing(WORD_SPACING);
assert_eq!(plain.break_lines(words.spaced, WIDTH).len(), 1);
assert_eq!(spaced.break_lines(words.spaced, WIDTH).len(), 1);
}
}
/// The paragraph set wide is the taller of the two, and it has the room
/// down to the words of the next part; running past them would set the two
/// over each other.
#[test]
fn the_paragraph_set_wide_stops_short_of_the_next_part() {
let (_doc, handle) = handle();
let column = (WIDTH - GUTTER) / 2.0;
for (named, words) in WORDS {
let flow = TextFlow::new(&handle, NOTE_SIZE).leading(LEADINGS[1]);
let lines = flow.break_lines(words.paragraph, column);
let bottom = LABEL_TOPS[1] - LABEL_GAP - flow.height(&lines);
assert!(
bottom > LABEL_TOPS[2],
"the {} paragraph reaches {bottom}, under the next part",
named.code()
);
}
}
/// The underlined line and the words on a band are one line each, and the
/// band's words end above the note at the foot, which ends no nearer the
/// bottom of the sheet than the text is to its left edge.
#[test]
fn the_ruled_part_and_the_note_under_it_fit_the_back() {
let (_doc, handle) = handle();
let underlined = TextFlow::new(&handle, ROW_SIZE).underline();
let banded = TextFlow::new(&handle, BAND_SIZE).strikethrough_at(BAND.0, BAND.1);
let note = TextFlow::new(&handle, NOTE_SIZE);
for (named, words) in WORDS {
let lines = underlined.break_lines(words.underlined, WIDTH);
assert_eq!(lines.len(), 1, "{:?}", words.underlined);
let lines = banded.break_lines(words.banded, WIDTH);
assert_eq!(lines.len(), 1, "{:?}", words.banded);
let bottom = LABEL_TOPS[4] - LABEL_GAP - BAND_STEP - banded.height(&lines);
assert!(
bottom > BACK_NOTE_TOP,
"the {} band reaches {bottom}, under the top of the note",
named.code()
);
let lines = note.break_lines(words.back_note, WIDTH);
let bottom = BACK_NOTE_TOP - note.height(&lines);
assert!(
bottom > LEFT,
"the {} note reaches {bottom}, into the margin",
named.code()
);
}
}
}
|