write_lists.py

Die Python-Datei des Beispiels „Listen aus einem Einzug und einem Tabstopp“. Aufzählungen, nummerierte und verschachtelte Listen, jede aus einem nach links hängenden Einzug und einem Tabstopp gebaut statt aus einem eigenen Listenobjekt: Das Zeichen sitzt in dem Raum, den der Einzug öffnet, und jede Zeile unter der ersten beginnt dort, wo der Tabulator den Text hingesetzt hat.

Python 409 Zeilen

Wofür dieses Beispiel da ist

Das Erkennungszeichen eines von einem Programm zusammengesetzten Dokuments ist eine Liste, deren zweite Zeile unter dem Aufzählungszeichen beginnt statt unter den Wörtern. Es ist eine Kleinigkeit, und jede Textverarbeitung der Welt macht es richtig.

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
"""Bulleted, numbered and nested lists, each built out of the indents and one tab
stop rather than out of a list engine.

The Python twin of the `write_lists` example in Rust. A list is a paragraph whose
first line hangs out to the left: the marker sits in the room the hanging indent
opens, the tabulation after it carries the pen to the stop the body begins at,
and every line below the first starts at that same stop. Nesting is the same
block again, its marker where the block above put its body.

The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which
set the page is set in. The indents are not language: the headings read the very
numbers the blocks are built from, so no language can announce a room the page
does not open.

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The left edge every block is set from.
X = 70.0

# The room the two lists at the top of the page each have.
NARROW = 210.0

# The left edge of the second of them.
RIGHT = 320.0

# The room the nested list at the foot of the page has.
WIDE = 460.0

# The size every list is set at.
SIZE = 10.5

# The room left between a heading and the block below it.
DROP = 4.0

# The top the two lists at the head of the page are set from.
TOP = 780.0

# The top the nested list is set from, headings and all.
NEST_TOP = 640.0

# How far into the box the bodies of the bulleted list begin.
BULLET_BODY = 14.0

# How far into the box the bodies of the numbered list begin, which is what the widest
# of its markers needs rather than what the narrowest does.
NUMBER_BODY = 22.0

# Where the markers and the bodies of the inner list stand: its markers begin where the
# outer bodies do, and its bodies one step further in again.
INNER_MARKER = NUMBER_BODY
INNER_BODY = NUMBER_BODY * 2.0


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

    # The heading over the bulleted list, on either side of the room its bodies begin
    # at.
    bulleted_before: str
    bulleted_after: str
    # The items of the bulleted list, each behind its bullet and a tabulation.
    bullets: str
    # The heading over the numbered list, on either side of the room its bodies begin
    # at.
    numbered_before: str
    numbered_after: str
    # The items of the numbered list, whose markers are wider than a bullet and so ask
    # for more room in front of the body.
    numbers: str
    # The heading over the nested list.
    nested: str
    # The outer items of the nested list, the first of which is broken down further.
    outer: tuple[str, str]
    # What the first outer item breaks down into, set again one level in.
    inner: str

    def bulleted(self) -> str:
        """The heading over the bulleted list, with the room its bodies begin at read
        off the very number the block is built from."""
        return f"{self.bulleted_before}{BULLET_BODY:.0f}{self.bulleted_after}"

    def numbered(self) -> str:
        """The heading over the numbered list, likewise."""
        return f"{self.numbered_before}{NUMBER_BODY:.0f}{self.numbered_after}"


# The page in English.
ENGLISH = Words(
    bulleted_before="Bulleted: a bullet, a tabulation, and ",
    bulleted_after=" points in front of the body.",
    bullets=(
        "•\tEvery line below the first lines up with the words, not with the bullet.\n"
        "•\tThe marker hangs in the room the negative first-line indent opens.\n"
        "•\tA long item wraps inside the block and stays inside it."
    ),
    numbered_before="Numbered: the same block, widened to ",
    numbered_after=" points for the widest marker.",
    numbers=(
        "1.\tA number hangs out to the left exactly as a bullet does.\n"
        "2.\tThe room in front is what the widest marker needs, not the narrowest.\n"
        "10.\tA two-figure number still ends before the stop the body begins at."
    ),
    nested=(
        "Nested: the inner list is the same block again, its markers standing "
        "where the outer bodies begin."
    ),
    outer=(
        "1.\tPrepare the document, the font it draws through and the page it fills.",
        "2.\tAdd each page, with the content stream it was built from.",
    ),
    inner=(
        "a.\tRead the font from its file and hand it to the document.\n"
        "b.\tName the page size, in points."
    ),
)

# The page in French.
FRENCH = Words(
    bulleted_before="À puces : une puce, une tabulation et ",
    bulleted_after=" points devant le corps.",
    bullets=(
        ("•\tToutes les lignes sous la première s’alignent sur les mots, pas sur la "
         "puce.\n•\tLe marqueur se loge dans l’espace que dégage le retrait négatif de"
         " première ligne.\n•\tUne entrée longue passe à la ligne dans le bloc et y "
         "reste.")
    ),
    numbered_before="Numérotée : le même bloc, élargi à ",
    numbered_after=" points pour le marqueur le plus large.",
    numbers=(
        ("1.\tUn numéro dépasse à gauche exactement comme une puce.\n2.\tLa place "
         "devant est celle qu’il faut au plus large des marqueurs, pas au plus "
         "étroit.\n10.\tUn numéro à deux chiffres finit toujours avant le taquet où "
         "commence le corps.")
    ),
    nested=(
        ("Imbriquée : la liste intérieure est le même bloc, ses marqueurs posés là où "
         "commencent les corps de la liste extérieure.")
    ),
    outer=(
        "1.\tPréparer le document, la police de son texte et la page qu’il remplit.",
        "2.\tAjouter chaque page, avec le flux de contenu qui la compose.",
    ),
    inner=(
        "a.\tLire la police dans son fichier et la donner au document.\n"
        "b.\tDonner la taille de la page, en points."
    ),
)

# The page in Dutch.
DUTCH = Words(
    bulleted_before="Met opsommingstekens: een teken, een tab en",
    bulleted_after=" punten voor de hoofdtekst.",
    bullets=(
        "•\tElke volgregel lijnt uit met de woorden, niet met het teken.\n•\tHet "
        "teken hangt in de ruimte die de negatieve inspringing opent.\n•\tEen lang "
        "item breekt binnen het blok af en blijft erin."
    ),
    numbered_before="Genummerd: hetzelfde blok, verbreed tot",
    numbered_after=" punten voor het breedste teken.",
    numbers=(
        "1.\tEen nummer hangt links naar buiten, net als een teken.\n2.\tDe ruimte "
        "ervoor volgt het breedste teken.\n10.\tOok een nummer van twee cijfers "
        "eindigt vóór de tabstop."
    ),
    nested=(
        "Genest: de binnenste lijst is weer hetzelfde blok, met de tekens op de plek "
        "waar de tekst van de buitenste begint."
    ),
    outer=(
        "1.\tHet document voorbereiden, het lettertype waarmee het tekent en de pagina die het vult.",
        "2.\tElke pagina toevoegen, met de inhoudsstroom waaruit ze is opgebouwd.",
    ),
    inner=(
        "a.\tHet lettertype uit zijn bestand lezen en aan het document "
        "geven.\nb.\tHet paginaformaat noemen, in punten."
    ),
)

# The page in Portuguese.
PORTUGUESE = Words(
    bulleted_before="Com marcas: uma marca, uma tabulação e",
    bulleted_after=" pontos à frente do corpo.",
    bullets=(
        "•\tCada linha abaixo da primeira alinha pelas palavras, não pela "
        "marca.\n•\tA marca fica pendurada no espaço que abre o avanço negativo da "
        "primeira linha.\n•\tUm item longo muda de linha dentro do bloco e fica "
        "dentro dele."
    ),
    numbered_before="Numerada: o mesmo bloco, alargado para",
    numbered_after=" pontos para a marca mais larga.",
    numbers=(
        "1.\tUm número fica pendurado à esquerda tal como uma marca.\n2.\tO espaço à "
        "frente é o que a marca mais larga precisa, não a mais estreita.\n10.\tUm "
        "número de dois algarismos acaba ainda antes da paragem de tabulação onde "
        "começa o corpo."
    ),
    nested=(
        "Aninhada: a lista interior é de novo o mesmo bloco, com as marcas onde "
        "começam os corpos exteriores."
    ),
    outer=(
        "1.\tPreparar o documento, o tipo de letra com que desenha e a página que preenche.",
        "2.\tAcrescentar cada página, com o fluxo de conteúdo a partir do qual foi construída.",
    ),
    inner=(
        "a.\tLer o tipo de letra do seu ficheiro e entregá-lo ao "
        "documento.\nb.\tIndicar o tamanho da página, em pontos."
    ),
)

# The page in Italian.
ITALIAN = Words(
    bulleted_before="Puntato: un pallino, una tabulazione e",
    bulleted_after=" punti davanti al corpo.",
    bullets=(
        "•\tOgni riga sotto la prima si allinea alle parole, non al pallino.\n•\tIl "
        "segno pende nello spazio aperto dal rientro negativo della prima "
        "riga.\n•\tUna voce lunga va a capo dentro il blocco e ci resta."
    ),
    numbered_before="Numerato: lo stesso blocco, allargato a",
    numbered_after=" punti per il segno più largo.",
    numbers=(
        "1.\tUn numero pende a sinistra proprio come un pallino.\n2.\tLo spazio "
        "davanti è quello che serve al segno più largo, non al più stretto.\n10.\tUn "
        "numero a due cifre finisce comunque prima del fermo dove inizia il corpo."
    ),
    nested=(
        "Annidato: l’elenco interno è di nuovo lo stesso blocco, con i segni dove "
        "iniziano i corpi esterni."
    ),
    outer=(
        "1.\tPreparare il documento, il font con cui disegna e la pagina che riempie.",
        "2.\tAggiungere ogni pagina, con il flusso di contenuto da cui è stata costruita.",
    ),
    inner=(
        "a.\tLeggere il font dal suo file e passarlo al documento.\nb.\tIndicare il "
        "formato della pagina, in punti."
    ),
)

# The page in Spanish.
SPANISH = Words(
    bulleted_before="Con viñetas: una viñeta, un tabulador y ",
    bulleted_after=" puntos delante del cuerpo.",
    bullets=(
        "•\tCada línea bajo la primera se alinea con las palabras, no con la "
        "viñeta.\n•\tLa marca cuelga en el hueco que abre la sangría negativa de la "
        "primera línea.\n•\tUn elemento largo pasa de línea dentro del bloque y se "
        "queda en él."
    ),
    numbered_before="Numerada: el mismo bloque, ensanchado a ",
    numbered_after=" puntos para la marca más ancha.",
    numbers=(
        "1.\tUn número cuelga a la izquierda igual que una viñeta.\n2.\tEl hueco "
        "delante es el que necesita la marca más ancha, no la más estrecha.\n10.\tUn "
        "número de dos cifras sigue acabando antes del tope donde empieza el cuerpo."
    ),
    nested=(
        "Anidada: la lista interior es otra vez el mismo bloque, con sus marcas donde "
        "empiezan los cuerpos exteriores."
    ),
    outer=(
        "1.\tPreparar el documento, la fuente con que se dibuja y la página que llena.",
        "2.\tAñadir cada página, con el flujo de contenido del que se construyó.",
    ),
    inner=(
        "a.\tLeer la fuente de su archivo y dársela al documento.\nb.\tNombrar el "
        "tamaño de página, en puntos."
    ),
)

# The page in German.
GERMAN = Words(
    bulleted_before="Mit Aufzählungszeichen: ein Aufzählungspunkt, ein Tabulator und ",
    bulleted_after=" Punkt vor dem Text.",
    bullets=(
        "•\tJede Folgezeile richtet sich an den Wörtern aus, nicht am "
        "Zeichen.\n•\tDas Zeichen hängt im Raum, den der negative Erstzeileneinzug "
        "öffnet.\n•\tEin langer Eintrag bricht im Block um und bleibt darin."
    ),
    numbered_before="Nummeriert: derselbe Block, verbreitert auf ",
    numbered_after=" Punkt für das breiteste Zeichen.",
    numbers=(
        "1.\tEine Zahl hängt genau wie ein Zeichen nach links heraus.\n2.\tDer Raum "
        "davor richtet sich nach dem breitesten Zeichen.\n10.\tAuch eine zweistellige "
        "Zahl endet vor dem Tabstopp des Texts."
    ),
    nested=(
        "Verschachtelt: Die innere Liste ist wieder derselbe Block, ihre Zeichen "
        "stehen dort, wo die äußeren Texte beginnen."
    ),
    outer=(
        "1.\tDas Dokument vorbereiten, die Schrift, mit der es zeichnet, und die Seite, die es füllt.",
        "2.\tJede Seite hinzufügen, mit dem Inhaltsstrom, aus dem sie gebaut wurde.",
    ),
    inner=(
        "a.\tDie Schrift aus ihrer Datei lesen und dem Dokument übergeben.\nb.\tDie "
        "Seitengröße nennen, in Punkt."
    ),
)

# 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 a_list(
    handle: hqf_pdf.FontHandle, marker: float, body: float
) -> hqf_pdf.TextFlow:
    """A list block whose markers begin `marker` points into the box and whose
    bodies begin `body` points into it.

    One call sets the three settings a list is made of, and there is no list engine
    under them: the left indent holds every line at the body, the first-line indent
    hangs that one line back out to the marker, and the single ruler stop is what
    the tabulation after the marker carries the pen to.
    """
    return hqf_pdf.TextFlow(handle, SIZE, leading=14.0).hanging_list(marker, body)


def heading(
    content: hqf_pdf.Content,
    handle: hqf_pdf.FontHandle,
    top: float,
    x: float,
    width: float,
    label: str,
) -> float:
    """Draws a heading across `width` and hands back the top of the block that goes
    under it, however many lines the heading itself took."""
    flow = hqf_pdf.TextFlow(handle, 8.0)
    lines = flow.break_lines(label, width)
    flow.draw(content, lines, x, top, width)
    return top - flow.height(lines) - DROP


def block(
    content: hqf_pdf.Content,
    flow: hqf_pdf.TextFlow,
    text: str,
    x: float,
    top: float,
    width: float,
) -> float:
    """Draws one block at `top` and hands back the top of whatever follows it."""
    lines = flow.break_lines(text, width)
    flow.draw(content, lines, x, top, width)
    return top - flow.height(lines)


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

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

    content = hqf_pdf.Content()

    # A bullet is narrow, so the body begins close in.
    top = heading(content, handle, TOP, X, NARROW, words.bulleted())
    block(content, a_list(handle, 0.0, BULLET_BODY), words.bullets, X, top, NARROW)

    # "10." is wider than "1.", and the room in front is what the widest of them needs.
    top = heading(content, handle, TOP, RIGHT, NARROW, words.numbered())
    block(
        content, a_list(handle, 0.0, NUMBER_BODY), words.numbers, RIGHT, top, NARROW
    )

    # Nesting is the same block again with its marker where the block above put its
    # body, each one set from the foot of the one before it.
    top = heading(content, handle, NEST_TOP, X, WIDE, words.nested)
    outer = a_list(handle, 0.0, NUMBER_BODY)
    inner = a_list(handle, INNER_MARKER, INNER_BODY)

    for index, item in enumerate(words.outer):
        top = block(content, outer, item, X, top, WIDE)
        if index == 0:
            top = block(content, inner, words.inner, X, top, WIDE)

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

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


if __name__ == "__main__":
    main()