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 | //! Sets text inside three shapes on one page: a column with a note set into its
//! top right-hand corner, the text going round the note; a circle; and a
//! six-sided shape with a picture box in its middle, each line meeting the box
//! set in two parts, left of it then right of it. The outline of every shape is
//! drawn in grey, and a caption says what each one shows.
//!
//! Every line stands where the shape holds it whole from the top of its letters
//! to the bottom, so no letter crosses an edge; the shapes are given to the
//! flow, and nothing is measured by hand.
//!
//! The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks
//! which set the page is written in.
//!
//! Usage: `cargo run --example write_text_shapes -- tmp/text_shapes.pdf
//! [font.ttf]`
//! `HQF_PDF_LANG=fr cargo run --example write_text_shapes --
//! tmp/text_shapes_fr.pdf`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
use hqf_pdf::layout::{Rect, TextShape};
use hqf_pdf::{Document, Font, FontHandle, Page, TextFlow};
#[path = "shared/out.rs"]
mod out;
#[path = "shared/licence.rs"]
mod licence;
#[path = "shared/language.rs"]
mod language;
#[path = "shared/failure.rs"]
mod failure;
use language::Language;
/// The font the example draws with when none is given on the command line: 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")
}
/// How far in from the left edge of the sheet everything starts, in points.
const MARGIN: f64 = 56.0;
/// Where the baseline of the page's heading sits, in points up from the foot of
/// the sheet.
const HEAD_TOP: f64 = 790.0;
/// The size the heading is set at, in points.
const HEADING: f64 = 16.0;
/// The size the lines under the heading and the captions are set at, in points.
const BODY: f64 = 9.5;
/// The size the text inside the shapes is set at, in points.
const TEXT: f64 = 10.5;
/// The size the note's text is set at, in points.
const NOTE: f64 = 8.5;
/// How far below one line the next one sits, in points.
const LINE_STEP: f64 = 13.0;
/// The column the first text is set in.
const COLUMN: Rect = Rect::new(MARGIN, 420.0, 230.0, 320.0);
/// The note set into the column's top right-hand corner.
const NOTE_BOX: Rect = Rect::new(166.0, 620.0, 120.0, 120.0);
/// The room held clear round the note and round the picture box, in points.
const GAP: f64 = 8.0;
/// How far the note's text is kept from the note's edges, in points.
const NOTE_PADDING: f64 = 6.0;
/// The square the circle is drawn in.
const CIRCLE: Rect = Rect::new(309.0, 510.0, 230.0, 230.0);
/// The corners of the six-sided shape's outline, in order.
const HEXAGON: [(f64, f64); 6] = [
(116.0, 375.0),
(479.0, 375.0),
(539.0, 255.0),
(479.0, 135.0),
(116.0, 135.0),
(56.0, 255.0),
];
/// The corners of the six-sided shape the text is set in, eight points inside
/// the outline.
const HEXAGON_TEXT: [(f64, f64); 6] = [
(121.0, 367.0),
(474.0, 367.0),
(530.0, 255.0),
(474.0, 143.0),
(121.0, 143.0),
(65.0, 255.0),
];
/// How far inside the outline of the column and of the circle the text is set,
/// in points.
const INSET: f64 = 7.0;
/// The picture box in the middle of the six-sided shape.
const PICTURE: Rect = Rect::new(237.0, 265.0, 121.0, 80.0);
/// The words the page is written in, one set per language.
#[derive(Debug)]
struct Words {
/// The page's heading, and what the file says of itself.
title: &'static str,
/// What the page says under its heading, one sentence over two lines.
intro: [&'static str; 2],
/// What the note set into the column says.
note: &'static str,
/// The text set round the note.
around: &'static str,
/// The text set inside the circle.
circle: &'static str,
/// The text set inside the six-sided shape, round the picture box.
polygon: &'static str,
/// What the picture box is labelled with.
picture: &'static str,
/// The captions under the column, the circle and the six-sided shape.
captions: [&'static str; 3],
}
impl Words {
/// The words the page is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
}
/// The page in English.
const ENGLISH: Words = Words {
title: "Text that keeps to a shape",
intro: [
"Each block below is one text set line by line: a line stands only where the shape holds it",
"whole, and where a rectangle sits in its way, the line goes round it or carries on beyond it.",
],
note: "A note set into the column. The text keeps eight points clear of it.",
around: "A column of text with a note set into its top right-hand corner. The lines beside the note are shorter: each one stops eight points before the note's edge, and the line under it takes the whole width again. Nothing here was measured by hand. The flow is told where the column is and where the note stands, and it works out, line by line, how much room each line has. The same text in another language runs to a different number of lines, and the note still sits where it was placed, with the text going round it.",
circle: "Inside a circle, every line is as wide as the circle is at the height of its letters, measured where the circle is narrowest along that line, so no letter crosses the edge. The lines grow longer towards the middle and shorter towards the bottom, and each one is set flush left inside its own width.",
polygon: "A six-sided shape with a picture box in its middle. Where a line meets the box, it is set in two parts: the text fills the stretch to the left of the box, then carries on in the stretch to its right, and only then goes down to the next line. A word never sits in a stretch too narrow for it. Above and below the box, the lines take the whole width the shape gives them, and along the slanted sides they grow shorter, so the block follows the outline from its top to its bottom. The shape is given as a list of corners, and any outline whose sides do not cross can be filled the same way.",
picture: "Picture",
captions: [
"Around a note",
"Inside a circle",
"Inside a polygon, around a picture box",
],
};
/// The page in French.
const FRENCH: Words = Words {
title: "Un texte qui épouse une forme",
intro: [
"Chaque bloc ci-dessous est un seul texte composé ligne à ligne : une ligne ne se place que là où",
"la forme la contient entière, et quand un rectangle la gêne, elle le contourne ou continue au-delà.",
],
note: "Une note insérée dans la colonne. Le texte s’en écarte de huit points.",
around: "Une colonne de texte, avec une note insérée dans son coin supérieur droit. Les lignes voisines de la note sont plus courtes : chacune s’arrête huit points avant le bord de la note, et la ligne qui passe dessous retrouve toute la largeur. Rien n’a été mesuré à la main. On indique au flux où se trouvent la colonne et la note, et il calcule, ligne après ligne, la place dont chacune dispose. Dans une autre langue, le même texte occupe un autre nombre de lignes ; la note reste là où on l’a posée, et le texte continue de la contourner.",
circle: "Dans un cercle, chaque ligne a la largeur du cercle à la hauteur de ses lettres, prise là où le cercle est le plus étroit sur cette ligne : aucune lettre ne dépasse le bord. Les lignes s’allongent vers le milieu, raccourcissent vers le bas, et chacune est alignée à gauche dans sa propre largeur.",
polygon: "Une forme à six côtés, avec un cadre d’image en son milieu. Quand une ligne rencontre le cadre, elle est composée en deux parties : le texte remplit la portion à gauche du cadre, continue dans celle de droite, et ne passe qu’ensuite à la ligne suivante. Un mot ne se place jamais dans une portion trop étroite pour lui. Au-dessus et au-dessous du cadre, les lignes prennent toute la largeur que la forme leur laisse, et le long des côtés obliques elles raccourcissent : le bloc suit le contour de haut en bas. La forme est donnée par la liste de ses sommets, et tout contour dont les côtés ne se croisent pas se remplit de la même façon.",
picture: "Image",
captions: [
"Autour d’une note",
"Dans un cercle",
"Dans un polygone, autour d’un cadre d’image",
],
};
/// The page in Dutch.
const DUTCH: Words = Words {
title: "Tekst die een vorm volgt",
intro: [
"Elk blok hieronder is één tekst, regel voor regel gezet: een regel staat alleen waar de vorm hem",
"helemaal bevat; waar een rechthoek in de weg staat, gaat de regel eromheen of erachter verder.",
],
note: "Een notitie in de kolom. De tekst houdt er acht punten afstand van.",
around: "Een tekstkolom met een notitie in de hoek rechtsboven. De regels naast de notitie zijn korter: elke regel stopt acht punten vóór de rand van de notitie, en de regel eronder krijgt weer de volle breedte. Niets hiervan is met de hand gemeten. De tekstopmaak krijgt de plaats van de kolom en van de notitie mee, en berekent regel voor regel hoeveel ruimte elke regel heeft. Dezelfde tekst in een andere taal beslaat een ander aantal regels, en de notitie blijft staan waar ze is geplaatst, met de tekst eromheen.",
circle: "In een cirkel is elke regel zo breed als de cirkel op de hoogte van zijn letters, gemeten waar de cirkel langs die regel het smalst is, zodat geen letter over de rand komt. De regels worden naar het midden toe langer en naar onderen toe korter, en elke regel staat links uitgelijnd binnen zijn eigen breedte.",
polygon: "Een zeshoekige vorm met een afbeeldingskader in het midden. Waar een regel het kader raakt, wordt hij in twee delen gezet: de tekst vult het stuk links van het kader, gaat verder in het stuk rechts ervan en gaat pas daarna naar de volgende regel. Een woord komt nooit in een stuk dat er te smal voor is. Boven en onder het kader krijgen de regels de volle breedte die de vorm hun laat, en langs de schuine zijden worden ze korter, zodat het blok de omtrek van boven tot onder volgt. De vorm wordt opgegeven als een lijst hoekpunten, en elke omtrek waarvan de zijden elkaar niet kruisen, kan op dezelfde manier worden gevuld.",
picture: "Afbeelding",
captions: [
"Om een notitie heen",
"In een cirkel",
"In een veelhoek, om een afbeeldingskader heen",
],
};
/// The page in Portuguese.
const PORTUGUESE: Words = Words {
title: "Um texto que segue uma forma",
intro: [
"Cada bloco abaixo é um só texto composto linha a linha: uma linha só fica onde a forma a contém",
"inteira, e onde um retângulo lhe barra o caminho, a linha contorna-o ou continua para lá dele.",
],
note: "Uma nota inserida na coluna. O texto mantém-se a oito pontos dela.",
around: "Uma coluna de texto com uma nota inserida no canto superior direito. As linhas ao lado da nota são mais curtas: cada uma termina oito pontos antes do bordo da nota, e a linha por baixo dela volta a ter a largura toda. Nada foi medido à mão. O fluxo de texto recebe a posição da coluna e a da nota, e calcula, linha a linha, o espaço de que cada linha dispõe. O mesmo texto noutra língua ocupa outro número de linhas, e a nota continua onde foi colocada, com o texto a contorná-la.",
circle: "Dentro de um círculo, cada linha tem a largura do círculo à altura das suas letras, medida onde o círculo é mais estreito ao longo dessa linha, para que nenhuma letra ultrapasse o bordo. As linhas alongam-se até ao meio e encurtam daí para baixo, e cada uma fica alinhada à esquerda dentro da sua própria largura.",
polygon: "Uma forma de seis lados com uma moldura de imagem no meio. Quando uma linha encontra a moldura, é composta em duas partes: o texto preenche o troço à esquerda da moldura, continua no troço à direita, e só então desce para a linha seguinte. Uma palavra nunca fica num troço demasiado estreito para ela. Acima e abaixo da moldura, as linhas ocupam toda a largura que a forma lhes dá, e ao longo dos lados oblíquos encurtam, de modo que o bloco segue o contorno de cima a baixo. A forma é descrita por uma lista de vértices, e qualquer contorno cujos lados não se cruzem pode ser preenchido da mesma maneira.",
picture: "Imagem",
captions: [
"À volta de uma nota",
"Dentro de um círculo",
"Dentro de um polígono, à volta de uma moldura de imagem",
],
};
/// The page in Italian.
const ITALIAN: Words = Words {
title: "Un testo che segue una forma",
intro: [
"Ogni blocco qui sotto è un solo testo composto riga per riga: una riga sta solo dove la forma",
"la contiene intera, e dove un rettangolo è d’intralcio, la riga gli gira attorno o prosegue oltre.",
],
note: "Una nota inserita nella colonna. Il testo ne resta a otto punti di distanza.",
around: "Una colonna di testo con una nota inserita nell’angolo in alto a destra. Le righe accanto alla nota sono più corte: ognuna si ferma otto punti prima del bordo della nota, e la riga sotto di essa riprende tutta la larghezza. Qui nulla è stato misurato a mano. Il flusso riceve la posizione della colonna e quella della nota, e calcola riga per riga lo spazio di cui ciascuna dispone. Lo stesso testo in un’altra lingua occupa un numero diverso di righe, e la nota resta dove è stata collocata, con il testo che le gira attorno.",
circle: "Dentro un cerchio, ogni riga ha la larghezza del cerchio all’altezza delle sue lettere, presa nel punto in cui il cerchio è più stretto lungo quella riga, così nessuna lettera supera il bordo. Le righe si allungano verso il centro e si accorciano verso il basso, e ciascuna è allineata a sinistra entro la propria larghezza.",
polygon: "Una forma a sei lati con un riquadro per immagine al centro. Dove una riga incontra il riquadro, viene composta in due parti: il testo riempie il tratto a sinistra del riquadro, poi prosegue in quello a destra, e solo allora passa alla riga seguente. Una parola non finisce mai in un tratto troppo stretto per contenerla. Sopra e sotto il riquadro, le righe prendono tutta la larghezza che la forma concede, e lungo i lati obliqui si accorciano, così il blocco segue il contorno dall’alto in basso. La forma è descritta da un elenco di vertici, e qualsiasi contorno i cui lati non si incrocino si riempie allo stesso modo.",
picture: "Immagine",
captions: [
"Attorno a una nota",
"Dentro un cerchio",
"Dentro un poligono, attorno a un riquadro per immagine",
],
};
/// The page in Spanish.
const SPANISH: Words = Words {
title: "Texto que se ciñe a una forma",
intro: [
"Cada bloque de abajo es un solo texto compuesto línea a línea: una línea solo va donde la forma",
"la contiene entera, y donde un rectángulo se interpone, la línea lo rodea o sigue más allá.",
],
note: "Una nota insertada en la columna. El texto se mantiene a ocho puntos de ella.",
around: "Una columna de texto con una nota insertada en su esquina superior derecha. Las líneas junto a la nota son más cortas: cada una se detiene ocho puntos antes del borde de la nota, y la línea que pasa por debajo recupera todo el ancho. Nada se ha medido a mano. Al flujo se le indica dónde está la columna y dónde se sitúa la nota, y calcula, línea a línea, de cuánto espacio dispone cada una. El mismo texto en otro idioma ocupa otro número de líneas, y la nota sigue donde se colocó, rodeada por el texto.",
circle: "Dentro de un círculo, cada línea tiene el ancho del círculo a la altura de sus letras, medido donde el círculo es más estrecho a lo largo de esa línea, de modo que ninguna letra rebasa el borde. Las líneas se alargan hacia el centro y se acortan hacia abajo, y cada una se alinea a la izquierda dentro de su propio ancho.",
polygon: "Una forma de seis lados con un marco de imagen en el centro. Donde una línea se encuentra con el marco, se compone en dos partes: el texto llena el tramo a la izquierda del marco, sigue en el tramo de la derecha y solo entonces baja a la línea siguiente. Una palabra nunca queda en un tramo demasiado estrecho para ella. Encima y debajo del marco, las líneas ocupan todo el ancho que la forma les deja, y a lo largo de los lados oblicuos se acortan, de modo que el bloque sigue el contorno de arriba abajo. La forma se define mediante una lista de vértices, y cualquier contorno cuyos lados no se crucen puede rellenarse de la misma manera.",
picture: "Imagen",
captions: [
"Alrededor de una nota",
"Dentro de un círculo",
"Dentro de un polígono, alrededor de un marco de imagen",
],
};
/// The page in German.
const GERMAN: Words = Words {
title: "Text, der einer Form folgt",
intro: [
"Jeder Block unten ist ein einziger Text, Zeile für Zeile gesetzt: Eine Zeile steht nur dort, wo die Form",
"sie ganz fasst, und wo ein Rechteck im Weg steht, umgeht sie es oder läuft rechts davon weiter.",
],
note: "Eine Notiz in der Spalte. Der Text hält acht Punkt Abstand zu ihr.",
around: "Eine Textspalte mit einer Notiz in der rechten oberen Ecke. Die Zeilen neben der Notiz sind kürzer: Jede endet acht Punkt vor dem Rand der Notiz, und die Zeile darunter hat wieder die volle Breite. Hier wurde nichts von Hand gemessen. Der Textfluss erfährt, wo die Spalte liegt und wo die Notiz steht, und berechnet daraus Zeile für Zeile, wie viel Platz jede Zeile hat. Derselbe Text ergibt in einer anderen Sprache eine andere Zeilenzahl, und die Notiz bleibt trotzdem dort, wo sie gesetzt wurde; der Text läuft um sie herum.",
circle: "In einem Kreis ist jede Zeile so breit wie der Kreis auf der Höhe ihrer Buchstaben, gemessen dort, wo der Kreis entlang dieser Zeile am schmalsten ist. So ragt kein Buchstabe über den Rand. Die Zeilen werden zur Mitte hin länger und nach unten hin kürzer, und jede steht linksbündig in ihrer eigenen Breite.",
polygon: "Eine sechseckige Form mit einem Bildrahmen in der Mitte. Wo eine Zeile auf den Rahmen trifft, wird sie in zwei Teilen gesetzt: Der Text füllt das Stück links vom Rahmen, läuft im Stück rechts davon weiter und geht erst dann in die nächste Zeile. Kein Wort steht in einem Stück, das zu schmal dafür ist. Über und unter dem Rahmen nutzen die Zeilen die volle Breite, die ihnen die Form lässt, und entlang der schrägen Seiten werden sie kürzer, sodass der Block dem Umriss von oben bis unten folgt. Die Form wird als Liste von Eckpunkten angegeben, und jeder Umriss, dessen Seiten sich nicht kreuzen, lässt sich ebenso füllen.",
picture: "Bild",
captions: [
"Um eine Notiz herum",
"In einem Kreis",
"In einem Vieleck, um einen Bildrahmen herum",
],
};
/// 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),
];
/// `area` with `inset` points taken off each side.
const fn inside(area: Rect, inset: f64) -> Rect {
Rect::new(
area.x() + inset,
area.y() + inset,
area.width() - 2.0 * inset,
area.height() - 2.0 * inset,
)
}
/// The column, round the note.
fn around_the_note() -> TextShape {
TextShape::rect(inside(COLUMN, INSET)).around(NOTE_BOX, GAP)
}
/// The circle.
const fn circle() -> TextShape {
TextShape::ellipse(inside(CIRCLE, INSET))
}
/// The six-sided shape, round the picture box.
fn hexagon() -> Result<TextShape, hqf_pdf::Error> {
Ok(TextShape::polygon(HEXAGON_TEXT)?.around(PICTURE, GAP))
}
/// `line` set at `(x, y)` in `font` at `size` points.
fn line_at(
content: &mut Content,
font: &FontHandle,
size: f64,
(x, y): (f64, f64),
line: &str,
) -> Result<(), hqf_pdf::Error> {
content.begin_text();
content.set_font(font, size)?;
content.text_origin(x, y)?;
content.show_glyphs(&font.glyphs(line));
content.end_text();
Ok(())
}
/// The grey outlines of the column, the circle and the six-sided shape.
fn outlines(content: &mut Content) -> Result<(), hqf_pdf::Error> {
content.save_state();
content.set_stroke_rgb(0.6, 0.6, 0.6)?.set_line_width(0.5)?;
content
.rect(COLUMN.x(), COLUMN.y(), COLUMN.width(), COLUMN.height())?
.stroke();
let radius = CIRCLE.width() / 2.0;
content
.circle(CIRCLE.x() + radius, CIRCLE.y() + radius, radius)?
.stroke();
let [(x, y), rest @ ..] = HEXAGON;
content.move_to(x, y)?;
for (x, y) in rest {
content.line_to(x, y)?;
}
content.close_path().stroke();
content.restore_state();
Ok(())
}
/// The note, a pale box with its text inside, and the picture box, a grey one
/// with its label.
fn note_and_picture(
content: &mut Content,
font: &FontHandle,
words: &Words,
) -> Result<(), hqf_pdf::Error> {
content.save_state();
content.set_fill_rgb(1.0, 0.97, 0.85)?;
content.set_stroke_rgb(0.6, 0.5, 0.2)?.set_line_width(0.5)?;
content
.rect(
NOTE_BOX.x(),
NOTE_BOX.y(),
NOTE_BOX.width(),
NOTE_BOX.height(),
)?
.fill_and_stroke();
content.set_fill_rgb(0.85, 0.85, 0.85)?;
content
.rect(PICTURE.x(), PICTURE.y(), PICTURE.width(), PICTURE.height())?
.fill();
content.restore_state();
let flow = TextFlow::new(font, NOTE);
let inner = NOTE_BOX.width() - NOTE_PADDING - NOTE_PADDING;
let lines = flow.break_lines(words.note, inner);
content.begin_text();
flow.draw(
content,
&lines,
NOTE_BOX.x() + NOTE_PADDING,
NOTE_BOX.y() + NOTE_BOX.height() - NOTE_PADDING,
inner,
)?;
content.end_text();
line_at(
content,
font,
BODY,
(
PICTURE.x() + 8.0,
PICTURE.y() + PICTURE.height() / 2.0 - 3.0,
),
words.picture,
)
}
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_shapes")));
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(hqf_pdf::cos::Name::new("Title"), words.title);
let font = doc.add_font(Font::parse(fs::read(&font_path)?)?);
let mut content = Content::new();
content.set_fill_rgb(0.0, 0.0, 0.0)?;
line_at(
&mut content,
&font,
HEADING,
(MARGIN, HEAD_TOP),
words.title,
)?;
let mut y = HEAD_TOP - 24.0;
for line in words.intro {
line_at(&mut content, &font, BODY, (MARGIN, y), line)?;
y -= LINE_STEP;
}
outlines(&mut content)?;
note_and_picture(&mut content, &font, words)?;
let flow = TextFlow::new(&font, TEXT);
for (text, shape) in [
(words.around, around_the_note()),
(words.circle, circle()),
(words.polygon, hexagon()?),
] {
flow.shape(text, &shape).draw(&mut content)?;
}
let [column, round, polygon] = words.captions;
line_at(
&mut content,
&font,
BODY,
(COLUMN.x(), COLUMN.y() - 14.0),
column,
)?;
line_at(
&mut content,
&font,
BODY,
(CIRCLE.x(), CIRCLE.y() - 14.0),
round,
)?;
line_at(&mut content, &font, BODY, (MARGIN, 118.0), polygon)?;
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 {} ({} bytes)", out, bytes.len());
Ok(())
}
#[cfg(test)]
mod tests {
use hqf_pdf::{Document, Font, TextFlow};
use super::{
BODY, COLUMN, HEADING, NOTE, NOTE_BOX, NOTE_PADDING, TEXT, WORDS, around_the_note, circle,
default_font, hexagon, language,
};
/// How wide the heading and the lines under it may run, in points.
const WIDTH: f64 = 483.0;
/// The lines two languages are allowed to write the same way.
const SPARED: [&str; 0] = [];
#[test]
fn every_language_writes_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:?}"
);
}
/// Every text fits whole in its shape, every line set on its own fits the
/// room it has, and the note fits its box.
#[test]
fn every_language_fits_the_room_it_has() {
let mut doc = Document::new();
let font = doc.add_font(
Font::parse(std::fs::read(default_font()).expect("the committed font is there"))
.expect("the committed font parses"),
);
let flow = TextFlow::new(&font, TEXT);
for (named, words) in WORDS {
let code = named.code();
for (text, shape) in [
(words.around, around_the_note()),
(words.circle, circle()),
(words.polygon, hexagon().expect("six corners")),
] {
assert!(
flow.shape(text, &shape).is_complete(),
"the {code} text does not fit its shape: {text:.40}"
);
}
let mut lines = vec![(HEADING, words.title, WIDTH)];
lines.extend(words.intro.map(|line| (BODY, line, WIDTH)));
lines.push((BODY, words.captions[0], COLUMN.width()));
lines.push((BODY, words.captions[1], WIDTH - COLUMN.width() - 23.0));
lines.push((BODY, words.captions[2], WIDTH));
for (size, line, room) in lines {
let measured = font.measure(line, size);
assert!(
measured <= room,
"the {code} page sets {line:?} over {measured:.1} points, and has {room:.1}"
);
}
let note = TextFlow::new(&font, NOTE);
let broken =
note.break_lines(words.note, NOTE_BOX.width() - NOTE_PADDING - NOTE_PADDING);
assert!(
note.height(&broken) <= NOTE_BOX.height() - NOTE_PADDING - NOTE_PADDING,
"the {code} note runs out of its box"
);
}
}
}
|