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 | """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
# 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 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 a reader tells furniture from what the document says."
),
figure=None,
),
),
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,
),
),
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 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 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. A reader 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()
|