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 | """Writes a document that says what the marks on its pages stand for.
The Python twin of the `write_tagged` example in Rust: the same document,
through the binding rather than through the library directly.
Two things are drawn on each page: content, and furniture. The content is marked
and numbered, and the structure tree points at those numbers in reading order, so
that something reading the document aloud reads it in the order it was written
rather than the order it was drawn. The furniture — the running head, the page
number — is marked as an artifact instead, carries no number, and is therefore
invisible to everything that follows the tree.
The page and the branch of the tree describing it are built together, by one
function, from one counter: that is what keeps a number in the stream and a number
in the tree from ever drifting apart.
The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which
set the document is written in. The tags are not language: `H1`, `P` and `Figure`
are the names the standard gives, and the tree declares which tongue it is that
the words under them are written in.
Usage: python examples/write_tagged.py [out.pdf] [font.ttf]
HQF_PDF_LANG=fr python examples/write_tagged.py
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import _language
import _licence
import _out
import hqf_pdf
# The tag every heading of the document is a heading of.
HEADING = "H1"
# Which of the sheets carries the table.
TABLE_ON = 1
# Which of the sheets carries the numbered list.
LIST_ON = 1
# The labels the items of the list carry: figures a numbering gives rather than words a
# language writes.
LABELS = ("1.", "2.", "3.")
# The baseline the first item of the list is set on, and how far the next one sits under
# it.
LIST_TOP = 420.0
LIST_STEP = 16.0
# Where the label of an item is drawn, and where the words beside it begin.
LABEL_AT = 72.0
ITEM_AT = 92.0
# How many of each part the table lists, and what that many come to: figures an order
# gives rather than words a language writes.
AMOUNTS = (
("40", "480.00 EUR"),
("6", "1 260.00 EUR"),
("6", "2 510.00 EUR"),
)
@dataclass(frozen=True)
class Sheet:
"""One page of the document: what it says, and how it says it."""
# The heading of the page.
heading: str
# What the page says, its paragraphs separated by newlines. The flow breaks them
# into lines and marks one run per paragraph.
body: str
# What an illustration on the page says, for something that cannot see it. The page
# draws one only if it has something to say about it.
figure: str | None
@dataclass(frozen=True)
class Words:
"""Every word the document draws, in one language."""
# The title the file states, which is also the running head every page carries.
title: str
# What a page number reads, in front of the number and between the two.
page_before: str
page_between: str
# The language the structure tree declares, so that something reading the document
# aloud reads it in the tongue it was written in.
lang: str
# The two sheets.
sheets: tuple[Sheet, Sheet]
# The three items of the numbered list, each drawn beside the label of `LABELS` that
# numbers it.
steps: tuple[str, str, str]
# The headings of the table's three columns.
columns: tuple[str, str, str]
# What each of the parts in the table is called.
parts: tuple[str, str, str]
def table(self) -> list[tuple[str, str, str]]:
"""The rows of the table, its headings first, then each part beside the
figures the order gives for it."""
return [self.columns] + [
(part, quantity, amount)
for part, (quantity, amount) in zip(self.parts, AMOUNTS)
]
def page_number(self, index: int) -> str:
"""What the page number of the sheet at `index` reads."""
return f"{self.page_before}{index + 1}{self.page_between}{len(self.sheets)}"
# The document in English.
ENGLISH = Words(
title="A tagged document",
page_before="Page ",
page_between=" of ",
lang="en-GB",
sheets=(
Sheet(
heading="What a tagged document is",
body=(
"A tagged document says what each run of ink on its pages stands for: "
"this run is a heading, that one is a paragraph, and the rule across the "
"top is furniture that says nothing at all.\n"
"Something reading the document aloud follows the order the tree gives, "
"not the order the ink was laid down in, and a phone reflowing the page "
"has something to reflow."
),
figure="A filled rectangle, standing in for an illustration",
),
Sheet(
heading="What the tree leaves out",
body=(
"The running head above and the page number below are artifacts. They "
"are drawn, they are not content, and nothing reading the document aloud "
"says them.\n"
"An artifact carries no number, so the tree cannot point at it. That "
"is the whole of how reading software tells furniture from what the "
"document says."
),
figure=None,
),
),
steps=(
"Mark the run as the page draws it",
"Number it from the page's own counter",
"Point the tree at that number",
),
columns=("Part", "Quantity", "Amount"),
parts=("Bearing, 12 mm", "Shaft, ground", "Housing, cast"),
)
# The document in French.
FRENCH = Words(
title="Un document balisé",
page_before="Page ",
page_between=" sur ",
lang="fr-FR",
sheets=(
Sheet(
heading="Ce qu’est un document balisé",
body=(
("Un document balisé dit ce que représente chaque élément de ses "
"pages : celui-ci est un titre, celui-là un paragraphe, et le filet "
"du haut relève du décor et ne dit rien du tout.\nUn logiciel qui lit"
" le document à voix haute suit l’ordre que donne l’arbre, et non "
"l’ordre dans lequel l’encre a été posée ; un téléphone qui recompose"
" la page a de quoi la recomposer.")
),
figure="Un rectangle plein, à la place d’une illustration",
),
Sheet(
heading="Ce que l’arbre laisse de côté",
body=(
("Le titre courant au-dessus et le numéro de page en dessous sont des "
"artefacts. Ils sont affichés, ce ne sont pas du contenu, et aucun "
"lecteur d’écran ne les prononce.\nUn artefact n’a aucun numéro, donc"
" l’arbre ne peut pas le désigner. C’est tout ce qui permet à un "
"logiciel de lecture de distinguer le décor de ce que dit le "
"document.")
),
figure=None,
),
),
steps=(
"Marquer l’élément au moment où la page le trace",
"Le numéroter avec le compteur de la page",
"Faire pointer l’arbre sur ce numéro",
),
columns=("Pièce", "Quantité", "Montant"),
parts=("Roulement, 12 mm", "Arbre rectifié", "Carter en fonte"),
)
# The document in Dutch.
DUTCH = Words(
title="Een getagd document",
page_before="Pagina",
page_between=" van",
lang="nl-NL",
sheets=(
Sheet(
heading="Wat een getagd document is",
body=(
"Een getagd document zegt waar elk stuk inkt op zijn pagina’s voor "
"staat: dit stuk is een kop, dat een alinea, en de lijn bovenaan is "
"meubilair dat helemaal niets zegt.\nSoftware die het document "
"voorleest, volgt de volgorde die de boom geeft, niet de volgorde "
"waarin de inkt is neergelegd, en een telefoon die de pagina "
"herschikt, heeft iets om te herschikken."
),
figure="Een gevulde rechthoek, in plaats van een illustratie",
),
Sheet(
heading="Wat de boom weglaat",
body=(
"De kopregel hierboven en het paginanummer hieronder zijn artefacten. "
"Ze worden getekend, ze zijn geen inhoud, en software die het "
"document voorleest, slaat ze over.\nEen artefact heeft geen nummer, "
"dus kan de boom er niet naar wijzen. Zo en niet anders onderscheidt "
"leessoftware meubilair van wat het document zegt."
),
figure=None,
),
),
steps=(
"Het stuk markeren zoals de pagina het tekent",
"Het nummeren vanuit de eigen teller van de pagina",
"De boom naar dat nummer laten wijzen",
),
columns=("Onderdeel", "Aantal", "Bedrag"),
parts=("Lager, 12 mm", "As, geslepen", "Behuizing, gegoten"),
)
# The document in Portuguese.
PORTUGUESE = Words(
title="Um documento etiquetado",
page_before="Página",
page_between=" de",
lang="pt-PT",
sheets=(
Sheet(
heading="O que é um documento etiquetado",
body=(
"Um documento etiquetado diz o que representa cada troço de tinta das "
"suas páginas: este troço é um título, aquele é um parágrafo, e o "
"filete no topo é mobília que não diz nada.\nO programa que lê o "
"documento em voz alta segue a ordem que a árvore dá, não a ordem em "
"que a tinta foi posta, e um telemóvel que reorganiza a página tem "
"algo para reorganizar."
),
figure="Um retângulo preenchido, no lugar de uma ilustração",
),
Sheet(
heading="O que a árvore deixa de fora",
body=(
"O cabeçalho em cima e o número de página em baixo são artefactos. "
"São desenhados, não são conteúdo, e nada que leia o documento em voz "
"alta os diz.\nUm artefacto não leva número, por isso a árvore não "
"pode apontar para ele. É exatamente assim que um programa de leitura "
"distingue a mobília do que o documento diz."
),
figure=None,
),
),
steps=(
"Marcar o troço como a página o desenha",
"Numerá-lo pelo contador próprio da página",
"Apontar a árvore para esse número",
),
columns=("Peça", "Quantidade", "Montante"),
parts=("Rolamento, 12 mm", "Veio, retificado", "Caixa, fundida"),
)
# The document in Italian.
ITALIAN = Words(
title="Un documento marcato",
page_before="Pagina",
page_between=" di",
lang="it-IT",
sheets=(
Sheet(
heading="Che cos’è un documento marcato",
body=(
"Un documento marcato dice cosa rappresenta ogni tratto d’inchiostro "
"delle sue pagine: questo è un titolo, quello un paragrafo, e il "
"filetto in alto è un ornamento che non dice nulla.\nCiò che legge il "
"documento ad alta voce segue l’ordine dato dall’albero, non l’ordine "
"in cui è stato steso l’inchiostro, e un telefono che reimpagina la "
"pagina ha qualcosa da reimpaginare."
),
figure="Un rettangolo pieno, al posto di un’illustrazione",
),
Sheet(
heading="Ciò che l’albero lascia fuori",
body=(
"L’intestazione in alto e il numero di pagina in basso sono "
"artefatti. Sono disegnati, non sono contenuto, e nulla che legga il "
"documento ad alta voce li pronuncia.\nUn artefatto non porta numero, "
"così l’albero non può indicarlo. È tutto qui il modo in cui un "
"programma di lettura distingue gli ornamenti da ciò che dice il "
"documento."
),
figure=None,
),
),
steps=(
"Marcare il tratto come lo disegna la pagina",
"Numerarlo con il contatore proprio della pagina",
"Far puntare l’albero a quel numero",
),
columns=("Pezzo", "Quantità", "Importo"),
parts=("Cuscinetto, 12 mm", "Albero, rettificato", "Carter, fuso"),
)
# The document in Spanish.
SPANISH = Words(
title="Un documento etiquetado",
page_before="Página",
page_between=" de",
lang="es-ES",
sheets=(
Sheet(
heading="Qué es un documento etiquetado",
body=(
"Un documento etiquetado dice qué representa cada tramo de tinta de "
"sus páginas: este tramo es un título, aquel es un párrafo, y el "
"filete de arriba es decoración que no dice nada.\nUn programa que "
"lee el documento en voz alta sigue el orden que da el árbol, no el "
"orden en que se puso la tinta, y un teléfono que recompone la página "
"tiene algo que recomponer."
),
figure="Un rectángulo relleno, en lugar de una ilustración",
),
Sheet(
heading="Lo que el árbol deja fuera",
body=(
"La cabecera de arriba y el número de página de abajo son artefactos. "
"Se dibujan, no son contenido, y ningún programa que lea el documento "
"en voz alta los pronuncia.\nUn artefacto no lleva número, así que el "
"árbol no puede señalarlo. Así es, ni más ni menos, como un programa "
"de lectura distingue la decoración de lo que dice el documento."
),
figure=None,
),
),
steps=(
"Marcar el tramo como lo dibuja la página",
"Numerarlo con el contador propio de la página",
"Hacer que el árbol señale ese número",
),
columns=("Pieza", "Cantidad", "Importe"),
parts=("Rodamiento, 12 mm", "Eje, rectificado", "Carcasa, fundida"),
)
# The document in German.
GERMAN = Words(
title="Ein getaggtes Dokument",
page_before="Seite ",
page_between=" von ",
lang="de-DE",
sheets=(
Sheet(
heading="Was ein getaggtes Dokument ist",
body=(
"Ein getaggtes Dokument sagt, wofür jeder Farbauftrag auf seinen "
"Seiten steht: Dies ist eine Überschrift, das ein Absatz, und die "
"Linie oben ist Beiwerk, das gar nichts sagt.\nWas beim Vorlesen zu "
"hören ist, folgt der Reihenfolge, die der Baum vorgibt, nicht der, "
"in der die Farbe aufgetragen wurde, und ein Telefon, das die Seite "
"neu umbricht, hat etwas zum Umbrechen."
),
figure="Ein gefülltes Rechteck, stellvertretend für eine Abbildung",
),
Sheet(
heading="Was der Baum auslässt",
body=(
"Der Kolumnentitel oben und die Seitenzahl unten sind Artefakte. Sie "
"werden gezeichnet, sind aber kein Inhalt, und beim Vorlesen werden "
"sie nicht ausgesprochen.\nEin Artefakt trägt keine Nummer, also kann "
"der Baum nicht darauf zeigen. Genau daran unterscheidet eine "
"Lesesoftware das Beiwerk von dem, was das Dokument sagt."
),
figure=None,
),
),
steps=(
"Den Abschnitt so markieren, wie die Seite ihn zeichnet",
"Ihn mit dem eigenen Zähler der Seite nummerieren",
"Den Baum auf diese Nummer zeigen lassen",
),
columns=("Teil", "Menge", "Betrag"),
parts=("Lager, 12 mm", "Welle, geschliffen", "Gehäuse, gegossen"),
)
# 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 numbered_list(
content: hqf_pdf.Content,
handle: hqf_pdf.FontHandle,
marks: hqf_pdf.Marks,
index: int,
words: Words,
) -> hqf_pdf.StructElement:
"""Draws the numbered list and hands back the branch of the tree describing it.
Each item is drawn as two runs — the label, then the words beside it — and the
list states the scheme its labels are numbered by, which is what lets something
reading the document aloud number the items itself.
"""
items = []
baseline = LIST_TOP
for label, step in zip(LABELS, words.steps):
numbered = marks.next_id()
content.begin_tagged("Lbl", numbered)
content.draw_text(handle, 11.0, LABEL_AT, baseline, label)
content.end_marked()
said = marks.next_id()
content.begin_tagged("LBody", said)
content.draw_text(handle, 11.0, ITEM_AT, baseline, step)
content.end_marked()
items.append(
hqf_pdf.StructElement(
"LI",
children=[
hqf_pdf.StructElement("Lbl", children=[(index, numbered)]),
hqf_pdf.StructElement("LBody", children=[(index, said)]),
],
)
)
baseline -= LIST_STEP
return hqf_pdf.StructElement(
"L", children=items, list_numbering=hqf_pdf.ListNumbering.Decimal
)
def sheet(
handle: hqf_pdf.FontHandle, index: int, words: Words
) -> tuple[hqf_pdf.Page, hqf_pdf.StructElement]:
"""Builds one page and the branch of the tree that describes it.
The numbers come from the page's own counter, so no two runs of the page share
one, and the tree is handed the very numbers the stream was marked with.
"""
page_of = words.sheets[index]
marks = hqf_pdf.Marks()
content = hqf_pdf.Content()
children: list[hqf_pdf.StructElement] = []
# The running head: a rule and a title, drawn on every page and read on none.
content.begin_artifact(hqf_pdf.Artifact.Header)
content.rect(72.0, 780.0, 451.0, 1.0)
content.fill()
content.draw_text(handle, 9.0, 72.0, 788.0, words.title)
content.end_marked()
number = marks.next_id()
content.begin_tagged(HEADING, number)
content.draw_text(handle, 18.0, 72.0, 730.0, page_of.heading)
content.end_marked()
children.append(hqf_pdf.StructElement(HEADING, children=[(index, number)]))
# The flow breaks the text into lines and marks one run per paragraph, handing back
# the number each run carries. The page never says where a line ends, and the tree
# gets one element per paragraph rather than one for the block.
flow = hqf_pdf.TextFlow(handle, 11.0, leading=16.0)
lines = flow.break_lines(page_of.body, 451.0)
top = 706.0
numbers = flow.draw_tagged(content, lines, 72.0, top, 451.0, "P", marks)
top -= flow.height(lines)
for number in numbers:
children.append(hqf_pdf.StructElement("P", children=[(index, number)]))
# The table describes itself: it marks each of its cells as it draws them and hands
# back the branch of the tree saying which row and which column each of them sits
# in. The counter it numbers from is the page's own, the same one every run above
# was numbered from.
if index == TABLE_ON:
table = hqf_pdf.Table(hqf_pdf.Columns.equal(3, 451.0))
table.header(1)
table.rule(hqf_pdf.Rule.frame(), hqf_pdf.Stroke(0.8))
table.rule(
hqf_pdf.Rule.horizontal_other(),
hqf_pdf.Stroke(0.25, color=hqf_pdf.Rgb.gray(0.75)),
)
padding = hqf_pdf.Padding.symmetric(6.0, 4.0)
for row in words.table():
cells = []
for column, cell in enumerate(row):
align = hqf_pdf.Align.Left if column == 0 else hqf_pdf.Align.Right
cells.append(
hqf_pdf.Cell(handle, 10.0, cell, align=align, padding=padding)
)
table.push(hqf_pdf.Row(cells=cells))
placement = table.fit(72.0, top - 20.0, 300.0)
children.append(placement.draw_tagged(content, index, marks))
if index == LIST_ON:
children.append(numbered_list(content, handle, marks, index, words))
if page_of.figure is not None:
number = marks.next_id()
content.begin_tagged("Figure", number)
content.rect(72.0, 420.0, 200.0, 120.0)
content.fill()
content.end_marked()
children.append(
hqf_pdf.StructElement(
"Figure", children=[(index, number)], alt=page_of.figure
)
)
# The page number: drawn, and no more part of what the document says than the staple
# would be.
content.begin_artifact(hqf_pdf.Artifact.Footer)
content.draw_text(handle, 9.0, 72.0, 60.0, words.page_number(index))
content.end_marked()
result = hqf_pdf.Page.a4()
result.set_content(content)
return result, hqf_pdf.StructElement("Sect", children=children)
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("tagged.pdf", language)).stem)
document = hqf_pdf.Document()
document.set_license(_licence.licensed())
document.set_info("Title", words.title)
handle = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))
# The whole document is one element, holding one section per page. Reading software
# that follows the tree walks it in this order, whatever order the ink was laid down
# in.
sections = []
for index in range(len(words.sheets)):
page, section = sheet(handle, index, words)
document.add_page(page)
sections.append(section)
document.set_structure(
hqf_pdf.StructureTree(
lang=words.lang,
children=[hqf_pdf.StructElement("Document", children=sections)],
)
)
written = document.write(out)
print(f"wrote {out} ({written} bytes)")
if __name__ == "__main__":
main()
|