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 | """Writes a table of quarterly takings whose every figure carries the headings that
reach it.
The Python twin of the `write_table_headers` example in Rust. A figure alone says
nothing: `118 400` is a number until something says which branch and which quarter it
belongs to. The table names its heading rows and its heading column, and the engine
works out, cell by cell, which headings reach each figure — so a reader that reads the
sheet aloud announces the town, the quarter and the year before the amount.
The table runs onto a second page, and it is one table there: the heading is drawn again
at the top of the page, and the document still says there is one of it.
Every word is held in `Words`, once per language, and `HQF_PDF_LANG` picks which set is
set. The branches and their takings are not words: they stand in one table, shared by
both languages.
Usage: python examples/write_table_headers.py [out.pdf] [font.ttf]
HQF_PDF_LANG=fr python examples/write_table_headers.py
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import _language
import _licence
import _out
import hqf_pdf
# Where the sheet's text begins, in points from the left edge of the page.
LEFT = 56.0
# How wide it runs.
WIDTH = 483.0
# The size the heading is set in.
TITLE_SIZE = 20.0
# The size the opening paragraph is set in.
INTRO_SIZE = 10.0
# The size a line of the table is set in.
ROW_SIZE = 9.0
# The size the foot of the page is set in.
FOOT_SIZE = 8.5
# The top of the heading's box.
TITLE_TOP = 790.0
# The top of the opening paragraph.
INTRO_TOP = 752.0
# The top of the table on the first page, under the opening paragraph.
FIRST_TOP = 668.0
# The top of the table on every page after it.
NEXT_TOP = 790.0
# How far down the page the table may reach.
FOOT = 90.0
# The baseline the foot of the page is set on.
FOOT_BASELINE = 60.0
# How wide each of the four quarter columns is, in points.
QUARTER_WIDTH = 90.0
# When the sheet was drawn up. The library never reads the clock: a document that
# stamped itself with the time would be a different file on every build.
DRAWN_UP = "2026-08-08T09:30:00+02:00"
@dataclass(frozen=True)
class Words:
"""The words the sheet is written in, one set per language."""
# The BCP 47 tag the document states.
lang: str
# What stands across the head of the sheet.
title: str
# The paragraph under it, which says what the sheet is showing.
intro: str
# The heading of the column the branches stand in.
branch: str
# What the four quarter columns are called.
quarters: tuple[str, str, str, str]
# What stands against the sum of every branch.
every_branch: str
# What opens the foot of the page, the page number following it.
foot: str
def page_number(self, index: int) -> str:
"""What the foot of page `index` reads, the pages counting from zero."""
return f"{self.foot} {index + 1}"
# The sheet in English.
ENGLISH = Words(
lang="en-GB",
title="Takings by branch, quarter by quarter",
intro=(
"Every figure below carries the headings that reach it: the branch "
"it stands beside, the quarter it stands under, and the year over the four "
"quarters together. Nothing here was written by hand — the sheet says which "
"rows and which column hold its headings, and the engine works out the rest. "
"Read aloud, the first figure of the first line is announced as the town, the "
"first quarter, the year, and then the amount."
),
branch="Branch",
quarters=("First quarter", "Second quarter", "Third quarter", "Fourth quarter"),
every_branch="Every branch together",
foot="Takings by branch, page",
)
# The sheet in French.
FRENCH = Words(
lang="fr-FR",
title="Recettes par agence, trimestre par trimestre",
intro=(
"Chaque montant ci-dessous porte les titres qui le concernent : "
"l'agence à côté de laquelle il se trouve, le trimestre au-dessus de lui, et "
"l'année qui couvre les quatre trimestres. Rien n'a été écrit à la main — la "
"feuille dit quelles lignes et quelle colonne portent ses titres, et le moteur "
"en déduit le reste. Lu à voix haute, le premier montant de la première ligne "
"s'annonce par la ville, le premier trimestre, l'année, puis la somme."
),
branch="Agence",
quarters=("1er trimestre", "2e trimestre", "3e trimestre", "4e trimestre"),
every_branch="Toutes agences réunies",
foot="Recettes par agence, page",
)
# Every language the sheet is written in.
WORDS = {_language.ENGLISH: ENGLISH, _language.FRENCH: FRENCH}
# The year the four quarter columns stand under.
#
# A year is not a word: both languages set the same one.
YEAR = "2026"
# What every branch took in each of the four quarters, in whole euros.
#
# Names and figures are not words either: both languages set the same table.
BRANCHES = (
("Ashcombe", (118_400, 126_900, 131_250, 142_800)),
("Barrowfield", (94_200, 88_750, 96_400, 103_900)),
("Coldharbour", (151_300, 148_200, 155_900, 167_400)),
("Dunwater", (72_600, 79_450, 81_200, 84_750)),
("Eastmarch", (133_800, 129_400, 138_600, 145_100)),
("Fernbridge", (86_950, 91_300, 89_700, 97_250)),
("Greyharbour", (164_500, 171_200, 168_900, 179_600)),
("Hollowmead", (58_300, 61_750, 64_200, 66_800)),
("Ivybridge", (107_600, 112_400, 109_850, 118_300)),
("Kelburn", (123_900, 118_700, 126_500, 134_200)),
("Langmere", (69_400, 73_100, 71_850, 76_900)),
("Marchford", (142_700, 147_300, 151_600, 158_400)),
("Netherby", (81_200, 84_900, 87_450, 92_100)),
("Oakhaven", (176_800, 182_400, 179_300, 191_700)),
("Penwick", (63_500, 66_200, 68_900, 71_400)),
("Quarrington", (98_700, 103_400, 101_250, 108_600)),
("Ravensmoor", (136_200, 141_800, 139_400, 148_900)),
("Saltmarsh", (77_300, 81_600, 79_950, 85_200)),
("Thornbury", (159_400, 164_900, 162_300, 173_500)),
("Upwell", (54_800, 57_200, 59_600, 62_100)),
("Vellacourt", (112_500, 117_800, 115_200, 123_700)),
("Westhaven", (188_300, 194_600, 191_400, 203_900)),
("Yarrowdale", (66_700, 69_300, 71_800, 74_500)),
("Applegarth", (104_200, 108_900, 106_400, 114_300)),
("Bramblewick", (89_600, 93_200, 91_700, 98_400)),
("Cotterill", (147_900, 152_600, 149_800, 160_200)),
("Dellmoor", (71_300, 74_800, 76_500, 79_900)),
("Eaglestone", (129_700, 134_200, 132_600, 141_300)),
("Foxholt", (83_400, 87_100, 85_600, 91_800)),
("Greenslade", (155_800, 161_400, 158_900, 169_700)),
)
def figure(amount: int) -> str:
"""An amount, with a space between its thousands."""
digits = str(amount)
out = ""
for index, digit in enumerate(digits):
if index > 0 and (len(digits) - index) % 3 == 0:
out += " "
out += digit
return out
def totals() -> list[int]:
"""What every branch took together in each of the four quarters."""
sums = [0, 0, 0, 0]
for _, quarters in BRANCHES:
for column, took in enumerate(quarters):
sums[column] += took
return sums
def columns() -> hqf_pdf.Columns:
"""The five columns: the branches, then one for each quarter."""
widths = [hqf_pdf.ColumnWidth.fraction(1.0)]
widths.extend(hqf_pdf.ColumnWidth.points(QUARTER_WIDTH) for _ in range(4))
return hqf_pdf.Columns(widths, WIDTH)
def cell(
font: hqf_pdf.FontHandle,
text: str,
align: hqf_pdf.Align,
span: int = 1,
span_rows: int = 1,
valign: hqf_pdf.VAlign = hqf_pdf.VAlign.Top,
) -> hqf_pdf.Cell:
"""A cell of the table, padded the way every cell of it is."""
return hqf_pdf.Cell(
font,
ROW_SIZE,
text,
align=align,
valign=valign,
padding=hqf_pdf.Padding.symmetric(6.0, 7.0),
span=span,
span_rows=span_rows,
)
def takings(font: hqf_pdf.FontHandle, words: Words) -> hqf_pdf.Table:
"""The table: two heading rows, one heading column, a line per branch, and a line
adding every branch together.
The heading of the branch column covers both heading rows, and the year covers the
four quarter columns. Neither of those can be read by looking up a column and along a
row, so the engine names the headings instead and writes, against every figure, which
of them reach it.
"""
table = hqf_pdf.Table(columns())
table.header(2)
table.row_headers(1)
table.fill(hqf_pdf.Area.header(), hqf_pdf.Rgb.gray(0.90))
table.rule(hqf_pdf.Rule.frame(), hqf_pdf.Stroke(0.8, hqf_pdf.Rgb.gray(0.0)))
hairline = hqf_pdf.Stroke(0.25, hqf_pdf.Rgb.gray(0.75))
table.rule(hqf_pdf.Rule.horizontal_other(), hairline)
table.rule(hqf_pdf.Rule.vertical_other(), hairline)
table.rule(hqf_pdf.Rule.horizontal(2), hqf_pdf.Stroke(0.8, hqf_pdf.Rgb.gray(0.0)))
table.push(
hqf_pdf.Row(
[
cell(
font,
words.branch,
hqf_pdf.Align.Left,
span_rows=2,
valign=hqf_pdf.VAlign.Middle,
),
cell(font, YEAR, hqf_pdf.Align.Center, span=4),
]
)
)
table.push(
hqf_pdf.Row([cell(font, name, hqf_pdf.Align.Right) for name in words.quarters])
)
for branch, took in BRANCHES:
line = [cell(font, branch, hqf_pdf.Align.Left)]
line.extend(cell(font, figure(amount), hqf_pdf.Align.Right) for amount in took)
table.push(hqf_pdf.Row(line))
total = [cell(font, words.every_branch, hqf_pdf.Align.Left)]
total.extend(cell(font, figure(amount), hqf_pdf.Align.Right) for amount in totals())
table.push(hqf_pdf.Row(total))
return table
def heading(
content: hqf_pdf.Content,
font: hqf_pdf.FontHandle,
words: Words,
marks: hqf_pdf.Marks,
) -> hqf_pdf.StructElement:
"""Draws the heading of the first page and hands back the element standing for it."""
flow = hqf_pdf.TextFlow(font, TITLE_SIZE)
lines = flow.break_lines(words.title, WIDTH)
mcid = marks.next_id()
content.begin_tagged("H1", mcid)
flow.draw(content, lines, LEFT, TITLE_TOP, WIDTH)
content.end_marked()
return hqf_pdf.StructElement("H1", children=[(0, mcid)])
def opening(
content: hqf_pdf.Content,
font: hqf_pdf.FontHandle,
words: Words,
marks: hqf_pdf.Marks,
) -> hqf_pdf.StructElement:
"""Draws the opening paragraph and hands back the element standing for it."""
flow = hqf_pdf.TextFlow(font, INTRO_SIZE, align=hqf_pdf.Align.Justify)
lines = flow.break_lines(words.intro, WIDTH)
mcid = marks.next_id()
content.begin_tagged("P", mcid)
flow.draw(content, lines, LEFT, INTRO_TOP, WIDTH)
content.end_marked()
return hqf_pdf.StructElement("P", children=[(0, mcid)])
def foot(
content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words, index: int
) -> None:
"""Draws the foot of a page, which belongs to the pagination and not to what the
document says."""
flow = hqf_pdf.TextFlow(font, FOOT_SIZE, align=hqf_pdf.Align.Right)
numbered = words.page_number(index)
lines = flow.break_lines(numbered, WIDTH)
content.begin_artifact(hqf_pdf.Artifact.Footer)
flow.draw(content, lines, LEFT, FOOT_BASELINE, WIDTH)
content.end_marked()
def build(words: Words, font_path: Path) -> hqf_pdf.Document:
"""Builds the whole document: the heading, the opening paragraph, the table over as
many pages as it needs, and the branch of the tree that says what all of it is."""
document = hqf_pdf.Document()
document.set_license(_licence.licensed())
# The accessibility claim, which is what a buyer's validator is pointed at: it is
# judged against the tree, the artifacts, the embedded font and the title all at
# once.
document.set_ua_conformance(hqf_pdf.PdfUa.One)
document.set_metadata(
hqf_pdf.Metadata(
title=words.title,
producer="hqf-pdf",
created=DRAWN_UP,
)
)
document.set_info("Title", words.title)
font = document.add_font(hqf_pdf.Font.from_path(font_path))
table = takings(font, words)
boxes = table.paginate(
hqf_pdf.TableFrame(LEFT, FIRST_TOP, FIRST_TOP - FOOT),
hqf_pdf.TableFrame(LEFT, NEXT_TOP, NEXT_TOP - FOOT),
)
# One table, however many pages draw it: every box is added to the same description,
# and the heading drawn again at the top of each page is the heading it already
# stands for.
described = hqf_pdf.TaggedTable("takings")
children = []
for index, placement in enumerate(boxes):
content = hqf_pdf.Content()
marks = hqf_pdf.Marks()
if index == 0:
children.append(heading(content, font, words, marks))
children.append(opening(content, font, words, marks))
described.add(placement, content, index, marks)
foot(content, font, words, index)
page = hqf_pdf.Page.a4()
page.set_content(content)
document.add_page(page)
children.append(described.finish())
document.set_structure(
hqf_pdf.StructureTree(
children=[hqf_pdf.StructElement("Document", children=children)],
lang=words.lang,
)
)
return document
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("table_headers.pdf", language)).stem
)
written = build(words, _out.font_path()).write(out)
print(f"wrote {out}: {written} bytes")
if __name__ == "__main__":
main()
|