write_vertical_text.py

Die Python-Datei des Beispiels „Die Seite hinab gesetzter Text“. Drei schmale Schilder, deren Wörter Buchstabe unter Buchstabe gesetzt sind, jedes füllt seine Spalten anders, mit dem Maß jeder Spalte darunter.

Python 446 Zeilen

Wofür dieses Beispiel da ist

Ein Schild an einem schmalen Pfosten, der Rücken eines Ordners, ein Etikett an der Seite einer Kiste: Manchmal muss ein Wort von oben nach unten gelesen werden, ein Buchstabe unter dem nächsten. Hier werden drei Schilder so auf schmalen Tafeln gesetzt. Das erste füllt seine Spalten von rechts nach links und darf ein Wort überall trennen, wobei jede Spalte, die an einer Trennung endet, mit einem Bindestrich schließt; das zweite füllt sie von links nach rechts, unten beginnend, und das dritte zentriert seine Wörter in Blau, mit mehr Abstand zwischen den Spalten.

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
"""Sets text down the page, in columns: three signs painted on narrow boards, the
letters of each word stacked one under the next.

The Python twin of the `write_vertical_text` example in Rust: the same page, through the
binding rather than through the library directly.

Each board is a box of its own, and each sets its columns a different way: from the top
and right to left, the order Japanese and Chinese are set in, broken between any two
letters, a hyphen ending each column a cut inside a word closes; from the bottom and
left to right, broken only at a space; and centred down the
board, in a colour of its own and a wider leading. Only the columns that fit the board's
width are drawn.

Under each board, lines say how many of its columns fit and how many it broke into, how
tall the tallest runs, how many end a paragraph, what the first one holds, and how tall
the whole text would stand in one column. A grey outline marks the box every drawn
column covers.

The font is added to run down the page, which is what a flow down the page is refused
without. The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks
which set the page is written in.

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# How far in from the left edge of the sheet the heading starts, in points.
MARGIN = 72.0

# Where the baseline of the heading sits, in points up from the foot of the sheet.
HEAD_TOP = 790.0

# The size the heading is set at, in points.
HEADING = 16.0

# The size the lines under the heading and under the boards are set at, in points.
BODY = 9.0

# How far below one of those lines the next one sits, in points.
LINE_STEP = 12.0

# The left edges of the three boards, in points.
BOARDS = (72.0, 247.0, 422.0)

# How wide a board is, in points.
BOARD_WIDTH = 110.0

# How wide the label over a board and the lines under it may run, in points.
LABEL_WIDTH = 150.0

# Where the top of every board sits, in points up from the foot of the sheet.
BOARD_TOP = 720.0

# How tall a board is, in points.
BOARD_HEIGHT = 280.0

# The room kept between a board's edge and its outermost column, in points.
INSET = 6.0

# The size the signs are set at, in points.
SIGN = 18.0


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

    # The heading, and what the file says of itself.
    title: str
    # What the page says under its heading.
    intro: tuple[str, str]
    # What each board is labelled, left to right.
    labels: tuple[str, str, str]
    # What each board says, left to right; a newline starts a paragraph.
    signs: tuple[str, str, str]
    # How many columns fit and how many there are; `{shown}` and `{count}` stand for
    # them.
    shown: str
    # How tall the tallest column runs; `{tall}` stands for it.
    tallest: str
    # How many columns end a paragraph; `{ends}` stands for it.
    ends: str
    # What the first column holds; `{first}` stands for it.
    first: str
    # How tall the whole text would stand in one column; `{long}` stands for it.
    long: str
    # How far across the drawn columns reach; `{across}` stands for it.
    across: str


# The page in English.
ENGLISH = Words(
    title="Text set down the page",
    intro=(
        (
            "Three signs on narrow boards, each word's letters stacked one under the "
            "next,"
        ),
        (
            "each board setting its columns another way. Under each, what its columns "
            "measure."
        ),
    ),
    labels=(
        "From the top, right to left, a hyphen at each cut",
        "From the bottom, left to right",
        "Centred, in blue, wider apart",
    ),
    signs=(
        "HARBOUR VIEW HOTEL\nROOMS FREE\nBREAKFAST SERVED\nPARKING\nWIFI",
        "OPEN\nALL YEAR",
        "FERRY\nTO THE\nISLANDS",
    ),
    shown="{shown} of {count} columns fit",
    tallest="tallest: {tall} pt",
    ends="{ends} end a paragraph",
    first="first: {first}",
    long="in one column: {long} pt",
    across="across: {across} pt",
)

# The page in French.
FRENCH = Words(
    title="Du texte composé de haut en bas",
    intro=(
        (
            "Trois enseignes sur des planches étroites, les lettres de chaque mot "
            "empilées,"
        ),
        (
            "chaque planche posant ses colonnes autrement. Dessous, ce que mesurent "
            "ses colonnes."
        ),
    ),
    labels=(
        "Du haut, de droite à gauche, un trait d’union à chaque coupure",
        "Du bas, de gauche à droite",
        "Centré, en bleu, plus espacé",
    ),
    signs=(
        "HÔTEL DE LA MARINE\nCHAMBRES LIBRES\nPETIT DÉJEUNER\nPARKING\nWIFI",
        "OUVERT\nTOUTE L’ANNÉE",
        "BAC\nPOUR\nLES ÎLES",
    ),
    shown="{shown} colonnes sur {count} tiennent",
    tallest="la plus haute : {tall} pt",
    ends="{ends} finissent un paragraphe",
    first="la première : {first}",
    long="en une colonne : {long} pt",
    across="en largeur : {across} pt",
)

# The page in Dutch.
DUTCH = Words(
    title="Tekst gezet langs de pagina omlaag",
    intro=(
        (
            "Drie borden op smalle planken, de letters van elk woord onder elkaar "
            "gestapeld;"
        ),
        (
            "elk bord zet zijn kolommen op een andere manier. Eronder wat de kolommen "
            "meten."
        ),
    ),
    labels=(
        "Van boven, van rechts naar links, een streepje bij elke breuk",
        "Van onder, van links naar rechts",
        "Gecentreerd, in blauw, verder uit elkaar",
    ),
    signs=(
        "HOTEL HAVENZICHT\nKAMERS VRIJ\nONTBIJT\nPARKEREN\nWIFI",
        "OPEN\nHET HELE JAAR",
        "VEERBOOT\nNAAR DE\nEILANDEN",
    ),
    shown="{shown} van {count} kolommen passen",
    tallest="hoogste: {tall} pt",
    ends="{ends} sluiten een alinea af",
    first="eerste: {first}",
    long="in één kolom: {long} pt",
    across="in de breedte: {across} pt",
)

# The page in Portuguese.
PORTUGUESE = Words(
    title="Texto composto para baixo",
    intro=(
        (
            "Três letreiros em tábuas estreitas, as letras de cada palavra empilhadas "
            "umas sob as outras,"
        ),
        (
            "e cada tábua compõe as suas colunas de outra forma. Por baixo de cada "
            "uma, o que medem as colunas."
        ),
    ),
    labels=(
        "Da direita para a esquerda, um hífen em cada corte",
        "De baixo, da esquerda para a direita",
        "Centrado, a azul, mais espaçado",
    ),
    signs=(
        "HOTEL VISTA DO PORTO\nQUARTOS LIVRES\nPEQUENO-ALMOÇO\nESTACIONAMENTO\nWIFI",
        "ABERTO\nO ANO TODO",
        "FERRY\nPARA AS\nILHAS",
    ),
    shown="cabem {shown} de {count} colunas",
    tallest="a mais alta: {tall} pt",
    ends="{ends} acabam um parágrafo",
    first="primeira: {first}",
    long="numa coluna: {long} pt",
    across="na largura: {across} pt",
)

# The page in Italian.
ITALIAN = Words(
    title="Testo composto verso il basso",
    intro=(
        (
            "Tre insegne su assi strette, le lettere di ogni parola impilate l’una "
            "sotto l’altra,"
        ),
        (
            "e ogni asse compone le sue colonne in un altro modo. Sotto ciascuna, "
            "cosa misurano le sue colonne."
        ),
    ),
    labels=(
        "Dall’alto, da destra a sinistra, un trattino a ogni taglio",
        "Dal basso, da sinistra a destra",
        "Centrato, in blu, più spaziato",
    ),
    signs=(
        "HOTEL VISTA PORTO\nCAMERE LIBERE\nCOLAZIONE\nPARCHEGGIO\nWIFI",
        "APERTO\nTUTTO L’ANNO",
        "TRAGHETTO\nPER LE\nISOLE",
    ),
    shown="ci stanno {shown} colonne su {count}",
    tallest="la più alta: {tall} pt",
    ends="{ends} chiudono un paragrafo",
    first="prima: {first}",
    long="in una colonna: {long} pt",
    across="in larghezza: {across} pt",
)

# The page in Spanish.
SPANISH = Words(
    title="Texto compuesto hacia abajo",
    intro=(
        (
            "Tres carteles en tablas estrechas, las letras de cada palabra apiladas "
            "una bajo otra,"
        ),
        (
            "y cada tabla compone sus columnas de otra forma. Debajo de cada una, lo "
            "que miden sus columnas."
        ),
    ),
    labels=(
        "De derecha a izquierda, un guion en cada corte",
        "Desde abajo, de izquierda a derecha",
        "Centrado, en azul, más espaciado",
    ),
    signs=(
        "HOTEL VISTA AL PUERTO\nHABITACIONES LIBRES\nDESAYUNO\nAPARCAMIENTO\nWIFI",
        "ABIERTO\nTODO EL AÑO",
        "FERRY\nA LAS\nISLAS",
    ),
    shown="caben {shown} de {count} columnas",
    tallest="la más alta: {tall} pt",
    ends="{ends} acaban un párrafo",
    first="primera: {first}",
    long="en una columna: {long} pt",
    across="a lo ancho: {across} pt",
)

# The page in German.
GERMAN = Words(
    title="Senkrecht gesetzter Text",
    intro=(
        (
            "Drei Schilder auf schmalen Brettern, die Buchstaben jedes Worts "
            "untereinander gestapelt,"
        ),
        (
            "jedes Brett setzt seine Spalten anders. Darunter, was seine Spalten "
            "messen."
        ),
    ),
    labels=(
        "Von oben, rechts nach links, Bindestrich bei Umbruch",
        "Von unten, von links nach rechts",
        "Zentriert, in Blau, weiter auseinander",
    ),
    signs=(
        "HOTEL HAFENBLICK\nZIMMER FREI\nFRÜHSTÜCK\nPARKPLATZ\nWLAN",
        "GANZJÄHRIG\nGEÖFFNET",
        "FÄHRE\nZU DEN\nINSELN",
    ),
    shown="{shown} von {count} Spalten passen",
    tallest="am höchsten: {tall} pt",
    ends="{ends} beenden einen Absatz",
    first="erste: {first}",
    long="in einer Spalte: {long} pt",
    across="quer: {across} pt",
)

# 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}


def flows(upright) -> tuple[hqf_pdf.VerticalFlow, ...]:
    """The three ways the boards set their columns, left to right."""
    return (
        hqf_pdf.VerticalFlow(
            upright,
            SIGN,
            align=hqf_pdf.ColumnAlign.Top,
            order=hqf_pdf.ColumnOrder.RightToLeft,
            overlong=hqf_pdf.Overlong.Break,
            cut_mark="-",
        ),
        hqf_pdf.VerticalFlow(
            upright,
            SIGN,
            align=hqf_pdf.ColumnAlign.Bottom,
            order=hqf_pdf.ColumnOrder.LeftToRight,
            overlong=hqf_pdf.Overlong.Overflow,
        ),
        hqf_pdf.VerticalFlow(
            upright,
            SIGN,
            align=hqf_pdf.ColumnAlign.Center,
            leading=SIGN * 1.6,
            color=hqf_pdf.Rgb(0.0, 0.3, 0.65),
        ),
    )


def said(flow, columns, shown, text, words) -> list[str]:
    """What the board holding `columns` says of them under it, `shown` of them drawn
    and `text` their whole text."""
    count = len(columns)
    tall = max((columns.height(index) for index in range(count)), default=0.0)
    ends = sum(1 for index in range(count) if columns.last_of_paragraph(index))
    first = next(
        (columns.text(index) for index in range(count) if not columns.is_empty(index)),
        "",
    )
    whole = text.replace("\n", " ")
    return [
        words.shown.replace("{shown}", str(shown)).replace("{count}", str(count)),
        words.tallest.replace("{tall}", f"{tall:.0f}"),
        words.ends.replace("{ends}", str(ends)),
        words.first.replace("{first}", first),
        words.long.replace("{long}", f"{flow.measure(whole):.0f}"),
        words.across.replace("{across}", f"{flow.width(columns.sub(0, shown)):.0f}"),
    ]


def page(font, upright, words) -> hqf_pdf.Page:
    """The page: the heading, then each board with its label over it, its columns that
    fit, the outline of the box each covers, and what it says of them under it."""
    content = hqf_pdf.Content()
    content.set_fill(hqf_pdf.Rgb(0.0, 0.0, 0.0))
    content.draw_text(font, HEADING, MARGIN, HEAD_TOP, words.title)
    y = HEAD_TOP - 22.0
    for line in words.intro:
        content.draw_text(font, BODY, MARGIN, y, line)
        y -= LINE_STEP

    label = hqf_pdf.TextFlow(font, BODY, leading=LINE_STEP)
    outlines = hqf_pdf.Guides.hairline(hqf_pdf.Rgb.gray(0.6))
    for index, flow in enumerate(flows(upright)):
        left = BOARDS[index]
        labelled = label.break_lines(words.labels[index], LABEL_WIDTH)
        label.draw(content, labelled, left, BOARD_TOP + 30.0, LABEL_WIDTH)

        content.save_state()
        content.set_stroke(hqf_pdf.Rgb(0.2, 0.2, 0.2))
        content.set_line_width(0.8)
        content.rect(left, BOARD_TOP - BOARD_HEIGHT, BOARD_WIDTH, BOARD_HEIGHT)
        content.stroke()
        content.restore_state()

        text = words.signs[index]
        room = BOARD_HEIGHT - 2.0 * INSET
        columns = flow.break_columns(text, room)
        shown = flow.fit(columns, BOARD_WIDTH - 2.0 * INSET, 0)
        drawn = columns.sub(0, shown)
        # A column hangs from its middle: the first sits half an em in from the edge the
        # order starts from.
        if index == 1:
            x = left + INSET + SIGN / 2.0
        else:
            x = left + BOARD_WIDTH - INSET - SIGN / 2.0
        top = BOARD_TOP - INSET
        flow.draw(content, drawn, x, top, room)
        outlines.outline_all(content, flow.column_boxes(drawn, x, top, room))

        baseline = BOARD_TOP - BOARD_HEIGHT - 14.0
        content.set_fill(hqf_pdf.Rgb(0.0, 0.0, 0.0))
        for line in said(flow, columns, shown, text, words):
            content.draw_text(font, BODY, left, baseline, line)
            baseline -= LINE_STEP

    result = hqf_pdf.Page.a4()
    result.set_content(content)
    return result


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("vertical_text.pdf", language)).stem
    )

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    document.set_info("Title", words.title)
    font = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))
    upright = document.add_vertical_font(hqf_pdf.Font.from_path(_out.font_path()))
    document.add_page(page(font, upright, words))

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


if __name__ == "__main__":
    main()