write_tagged.py

Le fichier Python de l'exemple « Un document qui se lit à voix haute ». Deux pages qui contiennent une copie d'elles-mêmes que personne ne voit : voici un titre, voici un tableau, et voici dans quel ordre les lire. Le logiciel qui lit le document à voix haute suit cette copie plutôt que l'ordre où les traits ont été dessinés, et saute le titre courant. Sur la page, rien ne change à l'œil.

Python 367 lignes

À quoi sert cet exemple

Quand une personne vous lit une lettre au téléphone, elle commence par le haut, elle suit les phrases dans l'ordre où elles ont été écrites, et elle ne s'arrête pas pour vous lire l'adresse imprimée au-dessus : elle voit bien où est le message. Un logiciel qui lit un document à voix haute ne voit rien de tout cela. Il n'a que les traits posés sur la page, dans l'ordre où le programme les a posés — souvent le titre, puis le numéro de page, puis le corps du texte — et une personne aveugle reçoit donc le numéro de page au milieu d'une phrase.

Ce que montre cet exemple

  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
"""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 trait d'encre sur ses "
                "pages : celui-ci est un titre, celui-là un paragraphe, et le filet du "
                "haut est du mobilier qui ne dit rien du tout.\n"
                "Ce 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 dessinés, ils ne sont pas du contenu, et rien de ce "
                "qui lit le document à voix haute ne les dit.\n"
                "Un artefact ne porte aucun numéro, donc l'arbre ne peut pas le "
                "désigner. C'est tout ce qui permet de distinguer le mobilier de ce que "
                "dit le document."
            ),
            figure=None,
        ),
    ),
    steps=(
        "Marquer le trait au moment où la page le pose",
        "Le numéroter avec le compteur de la page",
        "Faire pointer l'arbre sur ce numéro",
    ),
    columns=("Désignation", "Quantité", "Montant"),
    parts=("Roulement, 12 mm", "Arbre rectifié", "Carter en fonte"),
)

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


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, 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))
        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()