write_shrink_to_fit.py

Die Python-Datei des Beispiels „Namensschilder in einer Größe, Namen jeder Länge“. Acht gleich große Namensschilder mit Namen, die von zwei kurzen Wörtern bis zu einem Doppelnamen reichen. Die Bibliothek findet für jeden Namen die größte Schriftgröße, die in sein Rechteck passt, und diese Größe wird darunter gedruckt.

Python 563 Zeilen

Wofür dieses Beispiel da ist

Eine Konferenz bestellt ihre Namensschilder Wochen, bevor sie weiß, wer kommt. Die Karte hat eine Größe, die Gästeliste nicht: zwei kurze Wörter für einen Besucher, ein Doppelname und ein zweiter Vorname für den nächsten. Wählt man eine Größe für den längsten Namen, verlieren sich die kurzen in der Mitte ihrer Karten; wählt man eine für die kurzen Namen, laufen die langen über den Rand. Am Abend vor der Eröffnung kürzt jemand Namen von Hand.

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
"""Prints a sheet of conference badges whose boxes are all one size and whose names
are not.

The Python twin of the `write_shrink_to_fit` example in Rust. A badge is a rectangle a
printer cut before anyone knew who would wear it. The name that goes in it is whatever
the register holds: two short words for one guest, a double-barrelled surname for the
next. Asking for a size that suits the longest name would leave the short ones lost in
their boxes, and asking for one that suits the short names would push the long ones out
of theirs.

Each name here is therefore handed its box and asked what size it fits at. The sizes
step down by a quarter of a point until the whole of the name stays inside the box on
both counts, and the badge is set at the first size that holds. The size each one
settled on is printed under its badge, so the sheet says what it did.

A floor stops the shrinking, and the last panel shows what happens at it: a name too
long for a luggage tag comes back at the floor whether it fits there or not, the tag is
drawn clipped to its own edges, and the caller decides what to do about it.

The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which set is
drawn. The guests' names are the same in both.

Usage: python examples/write_shrink_to_fit.py [out.pdf] [font.ttf]
       HQF_PDF_LANG=fr python examples/write_shrink_to_fit.py
"""

from __future__ import annotations

import sys
from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The guests the sheet is printed for, in the order the badges are laid out. A name is a
# name in every language, so these stay out of `Words`. They run from two short words to
# a double-barrelled surname longer than the box.
GUESTS = (
    "Ana Ruiz",
    "Wei Chen",
    "Marguerite Vandenbossche",
    "Jean-Baptiste Delaunay",
    "Konstantinos Papadopoulos",
    "Aleksandra Wiśniewska-Kowalczyk",
    "Åsa Lindqvist",
    "Mohammed Al-Rashid ibn Saleh",
)

# The name on the luggage tag, which no size fits.
TAGGED = "Bartholomew Christopher Fitzwilliam-Harrington"


@dataclass(frozen=True)
class Words:
    """Every word the sheet draws, in one language."""

    title: str
    lead: str
    sheet: str
    roles: tuple[str, ...]
    settled: str
    floor: str
    floored: str
    clipped: str
    caveat: str


# The sheet in English.
ENGLISH = Words(
    title="Badges of one size, names of every length",
    lead=(
        "The eight boxes below are the same box, cut before anyone knew who would "
        "wear it. Each name is handed the box it has to fit and asked what size it "
        "fits at: the sizes step down by a quarter of a point, each one broken to the "
        "width of the box and weighed against its height, and the name is set at the "
        "first size that holds. Nothing here is measured by hand, and no name is "
        "shortened."
    ),
    sheet="One box, eight names",
    roles=(
        "Speaker",
        "Attendee",
        "Session chair",
        "Press",
        "Workshop host",
        "Organiser",
        "Volunteer",
        "Exhibitor",
    ),
    settled="Set at %size% points",
    floor="Where the shrinking stops",
    floored=(
        "A floor is given along with the box, and the size never goes under it: a name "
        "shrunk until it disappears is worse than a name that runs over. The luggage "
        "tag below is far too small for the name it carries, so the floor is what "
        "comes back, and the tag is drawn at the floor and clipped to its own edges. "
        "What to do about a box no size fills is the caller's to decide: draw it "
        "clipped, as here, print the name on a second line, or hand the register back "
        "a name it has to shorten."
    ),
    clipped="At the floor, clipped to the tag",
    caveat=(
        "The size that comes back is never larger than the one asked for, so a short "
        "name is set at the size the design chose and only a long one comes out "
        "smaller. Every badge on this sheet went through the same call, and the size "
        "printed under each is the one it handed back."
    ),
)

# The sheet in French.
FRENCH = Words(
    title="Des badges d’une seule taille, des noms de toutes les longueurs",
    lead=(
        ("Les huit cadres ci-dessous sont un seul et même cadre, découpé avant qu’on "
         "sache à qui il irait. Chaque nom reçoit le cadre qu’il doit remplir, et on "
         "cherche la taille à laquelle il y tient : les tailles descendent d’un quart "
         "de point, le nom est coupé en lignes à la largeur du cadre puis comparé à sa"
         " hauteur, et il est composé à la première taille qui convient. Rien ici "
         "n’est mesuré à la main, et aucun nom n’est raccourci.")
    ),
    sheet="Un cadre, huit noms",
    roles=(
        "Conférencière",
        "Participant",
        "Présidente de séance",
        "Presse",
        "Animateur d’atelier",
        "Organisatrice",
        "Bénévole",
        "Exposant",
    ),
    settled="Composé à %size% points",
    floor="Là où la réduction s’arrête",
    floored=(
        ("Un plancher est donné avec le cadre, et la taille ne descend jamais en "
         "dessous : un nom réduit jusqu’à disparaître vaut moins qu’un nom qui "
         "déborde. L’étiquette de bagage ci-dessous est bien trop petite pour le nom "
         "qu’elle doit recevoir : c’est donc le plancher qui revient, et l’étiquette "
         "est composée au plancher puis rognée à ses propres bords. Que faire d’un "
         "cadre qu’aucune taille ne remplit, c’est à l’appelant d’en décider : le "
         "composer rogné, comme ici, reporter le nom sur une seconde ligne, ou "
         "renvoyer au registre un nom à abréger.")
    ),
    clipped="Au plancher, rogné à l’étiquette",
    caveat=(
        ("La taille rendue n’est jamais plus grande que celle demandée : un nom court "
         "est composé à la taille choisie par la maquette, et seul un nom long sort "
         "plus petit. Chaque badge de cette planche est passé par le même appel, et la"
         " taille imprimée sous chacun est celle que l’appel a rendue.")
    ),
)

# The sheet in Dutch.
DUTCH = Words(
    title="Badges van één formaat, namen van elke lengte",
    lead=(
        "De acht kaders hieronder zijn hetzelfde kader, gesneden voordat iemand wist "
        "wie het zou dragen. Elke naam krijgt het kader waarin hij moet passen en er "
        "wordt gevraagd op welke grootte hij past: de groottes zakken per kwart punt, "
        "elk afgebroken op de breedte van het kader en afgewogen tegen zijn hoogte, "
        "en de naam wordt gezet op de eerste grootte die past. Niets hier is met de "
        "hand gemeten, en geen naam is ingekort."
    ),
    sheet="Eén kader, acht namen",
    roles=(
        "Spreker",
        "Deelnemer",
        "Voorzitter",
        "Pers",
        "Workshopleider",
        "Organisator",
        "Vrijwilliger",
        "Exposant",
    ),
    settled="Gezet op %size% punten",
    floor="Waar het verkleinen ophoudt",
    floored=(
        "Samen met het kader wordt een ondergrens gegeven, en de grootte zakt er "
        "nooit onder: een naam die krimpt tot hij verdwijnt, is erger dan een naam "
        "die overloopt. Het bagagelabel hieronder is veel te klein voor de naam die "
        "erop staat, dus komt de ondergrens terug, en het label wordt op die grootte "
        "getekend en tot zijn eigen randen bijgesneden. Wat te doen met een kader dat "
        "geen grootte vult, beslist de aanroeper: bijgesneden tekenen, zoals hier, de "
        "naam op een tweede regel zetten, of de registratie een naam teruggeven die "
        "ingekort moet worden."
    ),
    clipped="Op de ondergrens, bijgesneden tot het label",
    caveat=(
        "De grootte die terugkomt, is nooit groter dan de gevraagde, dus een korte "
        "naam wordt gezet op de grootte die het ontwerp koos en alleen een lange komt "
        "kleiner uit. Elke badge op dit blad ging door dezelfde aanroep, en de "
        "grootte onder elke badge is degene die hij teruggaf."
    ),
)

# The sheet in Portuguese.
PORTUGUESE = Words(
    title="Crachás de um só tamanho, nomes de todos os comprimentos",
    lead=(
        "As oito caixas abaixo são a mesma caixa, cortada antes de se saber quem a "
        "usaria. A cada nome dá-se a caixa em que tem de caber e pergunta-se a que "
        "tamanho cabe: os tamanhos descem um quarto de ponto de cada vez, cada um "
        "partido à largura da caixa e comparado com a sua altura, e o nome é composto "
        "ao primeiro tamanho que cabe. Nada aqui é medido à mão, e nenhum nome é "
        "encurtado."
    ),
    sheet="Uma caixa, oito nomes",
    roles=(
        "Orador",
        "Participante",
        "Moderador",
        "Imprensa",
        "Anfitrião da oficina",
        "Organizador",
        "Voluntário",
        "Expositor",
    ),
    settled="Composto a %size% pontos",
    floor="Onde para a redução",
    floored=(
        "Com a caixa dá-se um mínimo, e o tamanho nunca desce abaixo dele: um nome "
        "reduzido até desaparecer é pior do que um nome que transborda. A etiqueta de "
        "bagagem abaixo é pequena demais para o nome que leva, por isso volta o "
        "mínimo, e a etiqueta é desenhada nesse tamanho e recortada pelas suas "
        "margens. O que fazer com uma caixa que nenhum tamanho preenche decide-o o "
        "programa que chama a biblioteca: desenhá-la recortada, como aqui, imprimir o "
        "nome numa segunda linha, ou devolver ao registo um nome que tem de encurtar."
    ),
    clipped="No mínimo, recortado à etiqueta",
    caveat=(
        "O tamanho devolvido nunca é maior do que o pedido, por isso um nome curto é "
        "composto ao tamanho que o design escolheu e só um longo sai mais pequeno. "
        "Cada crachá desta folha passou pela mesma chamada, e o tamanho impresso por "
        "baixo de cada um é o que ela devolveu."
    ),
)

# The sheet in Italian.
ITALIAN = Words(
    title="Badge di una sola misura, nomi di ogni lunghezza",
    lead=(
        "Gli otto riquadri qui sotto sono lo stesso riquadro, tagliato prima che si "
        "sapesse chi l’avrebbe portato. A ogni nome si dà il riquadro in cui deve "
        "stare e gli si chiede a quale corpo ci sta: i corpi scendono di un quarto di "
        "punto, ciascuno spezzato alla larghezza del riquadro e confrontato con la "
        "sua altezza, e il nome è composto al primo corpo che ci sta. Niente è "
        "misurato a mano, e nessun nome è accorciato."
    ),
    sheet="Un riquadro, otto nomi",
    roles=(
        "Relatore",
        "Partecipante",
        "Moderatore",
        "Stampa",
        "Conduttore del laboratorio",
        "Organizzatore",
        "Volontario",
        "Espositore",
    ),
    settled="Composto a %size% punti",
    floor="Dove si ferma la riduzione",
    floored=(
        "Con il riquadro si dà un minimo, e il corpo non scende mai sotto: un nome "
        "ridotto fino a sparire è peggio di un nome che sborda. L’etichetta bagaglio "
        "qui sotto è troppo piccola per il nome che porta, così torna il minimo, e "
        "l’etichetta è disegnata a quel corpo e ritagliata ai propri bordi. Cosa fare "
        "di un riquadro che nessun corpo riempie lo decide chi chiama: disegnarlo "
        "ritagliato, come qui, stampare il nome su una seconda riga, o restituire al "
        "registro un nome da accorciare."
    ),
    clipped="Al minimo, ritagliato all’etichetta",
    caveat=(
        "Il corpo restituito non è mai più grande di quello chiesto, così un nome "
        "corto è composto al corpo scelto dal progetto e solo uno lungo esce più "
        "piccolo. Ogni badge di questo foglio è passato per la stessa chiamata, e il "
        "corpo stampato sotto ciascuno è quello che ha restituito."
    ),
)

# The sheet in Spanish.
SPANISH = Words(
    title="Acreditaciones de un tamaño, nombres de todas las longitudes",
    lead=(
        "Las ocho cajas de abajo son la misma caja, cortada antes de que nadie "
        "supiera quién la llevaría. A cada nombre se le da la caja en que debe caber "
        "y se le pregunta a qué tamaño cabe: los tamaños bajan de cuarto en cuarto de "
        "punto, cada uno cortado al ancho de la caja y comparado con su altura, y el "
        "nombre se compone al primer tamaño que cabe. Nada se mide a mano y ningún "
        "nombre se acorta."
    ),
    sheet="Una caja, ocho nombres",
    roles=(
        "Ponente",
        "Asistente",
        "Moderador",
        "Prensa",
        "Anfitrión del taller",
        "Organizador",
        "Voluntario",
        "Expositor",
    ),
    settled="Compuesto a %size% puntos",
    floor="Dónde para la reducción",
    floored=(
        "Con la caja se da un mínimo, y el tamaño nunca baja de él: un nombre "
        "reducido hasta desaparecer es peor que un nombre que se sale. La etiqueta de "
        "equipaje de abajo es demasiado pequeña para el nombre que lleva, así que "
        "vuelve el mínimo, y la etiqueta se dibuja a ese tamaño recortada a sus "
        "propios bordes. Qué hacer con una caja que ningún tamaño llena lo decide el "
        "programa que pide el tamaño: dibujarla recortada, como aquí, imprimir el "
        "nombre en una segunda línea o devolver al registro un nombre que tiene que "
        "acortar."
    ),
    clipped="Al mínimo, recortado a la etiqueta",
    caveat=(
        "El tamaño que vuelve nunca es mayor que el pedido, así que un nombre corto "
        "se compone al tamaño que eligió el diseño y solo uno largo sale más pequeño. "
        "Cada acreditación de esta hoja pasó por la misma llamada, y el tamaño "
        "impreso debajo de cada una es el que devolvió."
    ),
)

# The sheet in German.
GERMAN = Words(
    title="Namensschilder einer Größe, Namen jeder Länge",
    lead=(
        "Die acht Rahmen unten sind derselbe Rahmen, zugeschnitten, bevor jemand "
        "wusste, wer ihn tragen würde. Jedem Namen wird der Rahmen übergeben, in den "
        "er passen muss, und er wird gefragt, in welcher Größe er passt: Die Größen "
        "sinken um je einen Viertelpunkt, jede wird auf die Breite des Rahmens "
        "umbrochen und gegen seine Höhe gewogen, und der Name wird in der ersten "
        "Größe gesetzt, die passt. Nichts hier ist von Hand gemessen, und kein Name "
        "wird gekürzt."
    ),
    sheet="Ein Rahmen, acht Namen",
    roles=(
        "Vortrag",
        "Teilnahme",
        "Sitzungsleitung",
        "Presse",
        "Workshopleitung",
        "Organisation",
        "Helferteam",
        "Ausstellung",
    ),
    settled="Gesetzt in %size% Punkt",
    floor="Wo das Verkleinern aufhört",
    floored=(
        "Mit dem Rahmen wird eine Untergrenze angegeben, und die Größe fällt nie "
        "darunter: Ein bis zum Verschwinden verkleinerter Name ist schlimmer als "
        "einer, der überläuft. Der Kofferanhänger unten ist viel zu klein für seinen "
        "Namen, also kommt die Untergrenze zurück, und der Anhänger wird in dieser "
        "Größe gezeichnet und an seinen eigenen Rändern beschnitten. Was mit einem "
        "Rahmen geschieht, den keine Größe füllt, entscheidet der Aufrufer: "
        "beschnitten zeichnen wie hier, den Namen auf eine zweite Zeile setzen oder "
        "der Anmeldung einen Namen zurückgeben, den sie kürzen muss."
    ),
    clipped="An der Untergrenze, auf den Anhänger beschnitten",
    caveat=(
        "Die zurückgegebene Größe ist nie größer als die verlangte, also wird ein "
        "kurzer Name in der Größe gesetzt, die das Design gewählt hat, und nur ein "
        "langer kommt kleiner heraus. Jedes Schild auf diesem Bogen lief durch "
        "denselben Aufruf, und die darunter gedruckte Größe ist die, die er zurückgab."
    ),
)

# Every language the example is written in. A language is added by writing its own set
# of words and naming it here.
WORDS = {_language.ENGLISH: ENGLISH, _language.FRENCH: FRENCH, _language.GERMAN: GERMAN, _language.SPANISH: SPANISH, _language.ITALIAN: ITALIAN, _language.PORTUGUESE: PORTUGUESE, _language.DUTCH: DUTCH}

# The left edge of everything on the page.
LEFT = 72.0

# How wide a block of text is, and how wide the two columns of badges are together.
WIDTH = 451.0

# How wide one badge is.
BADGE_WIDTH = 216.0

# How tall one badge is.
BADGE_HEIGHT = 58.0

# How far apart the two columns of badges stand.
BADGE_GAP = WIDTH - 2 * BADGE_WIDTH

# How far a badge's contents sit inside its edges.
INSET = 10.0

# The box a name is fitted into, inside the badge.
NAME_HEIGHT = 27.0

# The size a name is asked for before it is asked to fit.
NAME_SIZE = 22.0

# The size a name is never set below.
FLOOR = 8.0

# How wide the luggage tag is.
TAG_WIDTH = 100.0

# How tall the luggage tag is.
TAG_HEIGHT = 20.0

# The grey the sheet draws its second-rank words in.
GREY = hqf_pdf.Rgb.gray(0.42)

# The grey the badges are outlined in.
OUTLINE = hqf_pdf.Rgb.gray(0.55)


def block(
    content: hqf_pdf.Content,
    flow: hqf_pdf.TextFlow,
    x: float,
    top: float,
    width: float,
    text: str,
) -> float:
    """Sets a block of words at `top`, and hands back the ordinate it ends at."""
    lines = flow.break_lines(text, width)
    # The binding opens the text object itself, so opening another here would write a
    # pair of operators its twin in Rust does not.
    flow.draw(content, lines, x, top, width)
    return top - flow.height(lines)


def points(size: float) -> str:
    """A size in points, as the sheet prints it: two decimals, so the quarter-point
    step the sizes come in is visible."""
    return f"{size:.2f}"


def badge_size(font: hqf_pdf.FontHandle, index: int) -> float:
    """The size the name of guest `index` is set at, inside a badge."""
    flow = hqf_pdf.TextFlow(font, NAME_SIZE, align=hqf_pdf.Align.Center)
    return flow.fit_size(GUESTS[index], BADGE_WIDTH - 2 * INSET, NAME_HEIGHT, FLOOR)


def badge(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    words: Words,
    index: int,
    x: float,
    top: float,
) -> float:
    """Draws one badge, and hands back the ordinate its caption ends at."""
    content.set_stroke(OUTLINE)
    content.set_line_width(0.5)
    content.rect(x, top - BADGE_HEIGHT, BADGE_WIDTH, BADGE_HEIGHT)
    content.stroke()

    size = badge_size(font, index)
    name = hqf_pdf.TextFlow(font, NAME_SIZE, align=hqf_pdf.Align.Center).resized(size)
    inner = BADGE_WIDTH - 2 * INSET
    block(content, name, x + INSET, top - INSET, inner, GUESTS[index])

    role = hqf_pdf.TextFlow(font, 9.0, align=hqf_pdf.Align.Center, color=GREY)
    block(content, role, x + INSET, top - INSET - NAME_HEIGHT, inner, words.roles[index])

    caption = hqf_pdf.TextFlow(font, 8.0, align=hqf_pdf.Align.Center, color=GREY)
    said = words.settled.replace("%size%", points(size))
    return block(content, caption, x, top - BADGE_HEIGHT - 4.0, BADGE_WIDTH, said)


def tag(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    words: Words,
    x: float,
    top: float,
) -> float:
    """Draws the luggage tag no size fits, and hands back the ordinate its caption ends
    at."""
    asked = hqf_pdf.TextFlow(font, NAME_SIZE, align=hqf_pdf.Align.Center)
    # The floor is one the library has nothing to say about: a floor under six points
    # would come back with a warning beside the size.
    size, said = asked.fit_size_reporting(TAGGED, TAG_WIDTH - 2.0, TAG_HEIGHT, FLOOR)
    if said is not None:
        print(said, file=sys.stderr)

    content.save_state()
    content.rect(x, top - TAG_HEIGHT, TAG_WIDTH, TAG_HEIGHT)
    content.clip()
    content.end_path()
    name = hqf_pdf.TextFlow(font, size, align=hqf_pdf.Align.Center)
    block(content, name, x + 1.0, top, TAG_WIDTH - 2.0, TAGGED)
    content.restore_state()

    content.set_stroke(OUTLINE)
    content.set_line_width(0.5)
    content.rect(x, top - TAG_HEIGHT, TAG_WIDTH, TAG_HEIGHT)
    content.stroke()

    caption = hqf_pdf.TextFlow(font, 8.0, color=GREY)
    return block(
        content,
        caption,
        x + TAG_WIDTH + 12.0,
        top - 4.0,
        WIDTH - TAG_WIDTH - 12.0,
        words.clipped,
    )


def build(document: hqf_pdf.Document, font: hqf_pdf.FontHandle, words: Words) -> None:
    """Draws the whole sheet."""
    title = hqf_pdf.TextFlow(font, 18.0)
    lead = hqf_pdf.TextFlow(font, 10.0)
    label = hqf_pdf.TextFlow(font, 12.0)

    content = hqf_pdf.Content()
    top = 790.0
    top = block(content, title, LEFT, top, WIDTH, words.title) - 12.0
    top = block(content, lead, LEFT, top, WIDTH, words.lead) - 22.0
    top = block(content, label, LEFT, top, WIDTH, words.sheet) - 14.0

    for pair in range(len(GUESTS) // 2):
        lowest = top
        for column in range(2):
            x = LEFT + (BADGE_WIDTH + BADGE_GAP) * column
            lowest = min(badge(content, font, words, pair * 2 + column, x, top), lowest)
        top = lowest - 12.0

    top -= 10.0
    top = block(content, label, LEFT, top, WIDTH, words.floor) - 12.0
    top = block(content, lead, LEFT, top, WIDTH, words.floored) - 20.0
    top = tag(content, font, words, LEFT, top) - 22.0

    closing = hqf_pdf.TextFlow(font, 9.0, color=GREY)
    block(content, closing, LEFT, top, WIDTH, words.caveat)

    page = hqf_pdf.Page.a4()
    page.set_content(content)
    document.add_page(page)


def main() -> None:
    language = _language.from_environment()
    words = _language.words_of(WORDS, 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/`.
    out = _out.output_path(
        Path(_language.file_name("shrink_to_fit.pdf", language)).stem
    )

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    font = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))

    build(document, font, words)

    written = document.write(out)
    print(f"wrote {out}: {written} bytes")


if __name__ == "__main__":
    main()