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 | """Sets the same tabulated text under each of the three tab methods, then a
table of contents whose dots run out to a right-hand stop, so what each method
does to a column can be read off the page.
The Python twin of the `write_tab_stops` example in Rust. A grey rule is drawn
at every stop the block was told to use, so the runs can be seen landing on
them. The relative method steps the pen on from wherever it stands, so its
columns never line up; the typewriter method steps to the next multiple of the
tab size, so they do; a ruler sends each tabulation to a stop of its own and
sets the run on it, centred over it, or lined up on its decimal point.
The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which
set the page is labelled in. The step the typewriter panel works in is not one
of them: its label is set around the very number the panel is given, so no
language can announce a grid the page does not land on.
Usage: python examples/write_tab_stops.py [out.pdf] [font.ttf]
HQF_PDF_LANG=fr python examples/write_tab_stops.py
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Sequence
import _language
import _licence
import _out
import hqf_pdf
# The left edge every panel is set from.
X = 80.0
# The room every panel has to fill.
COLUMN = 430.0
# The distance from a panel's heading down to the block below it.
DROP = 20.0
# The reference, the quantity and the unit price of each row the first three panels set.
# What each row is a category of is a word, and stands in `Words` under the same order.
ROWS = (
("A-1180", "12", "18.50"),
("B-2050", "4", "7.05"),
("C-9004", "120", "1 250.00"),
)
# The page each entry of the table of contents is on.
PAGES = ("7", "18", "34", "61", "112")
# The step the typewriter panel works in, which is the grid its columns land on and the
# number its label spells out.
GRID = 96.0
# How much of a line one tabulation opens when a flow is told nothing, as the first
# panel's label spells it out.
SHARE = "7.5%"
# The stops the ruler panel sends its three tabulations to.
RULER = [130.0, 260.0, 380.0]
@dataclass(frozen=True)
class Words:
"""Every word the page is labelled in, in one language."""
# The label over the relative panel, set around the share of a line one tabulation
# opens under the default.
relative: tuple[str, str]
# The label over the typewriter panel, set around the step it works in.
typewriter: tuple[str, str]
# The label over the ruler panel.
ruler: str
# The label over the table of contents.
listing: str
# What each of the four columns is called.
columns: tuple[str, str, str, str]
# What each row of `ROWS` is a category of.
categories: tuple[str, str, str]
# The entries of the table of contents, each set on the page `PAGES` gives it.
contents: tuple[str, str, str, str, str]
def relative_label(self) -> str:
"""The label over the relative panel, around the share it opens."""
return f"{self.relative[0]}{SHARE}{self.relative[1]}"
def typewriter_label(self) -> str:
"""The label over the typewriter panel, around the step it works in."""
return f"{self.typewriter[0]}{GRID:g}{self.typewriter[1]}"
def rows(self) -> str:
"""The four columns the first three panels set, a tabulation between each pair
and a heading over them."""
lines = ["\t".join(self.columns)]
lines.extend(
f"{row[0]}\t{category}\t{row[1]}\t{row[2]}"
for row, category in zip(ROWS, self.categories)
)
return "\n".join(lines)
def listed(self) -> str:
"""The table of contents, each entry with its page number behind a
tabulation."""
return "\n".join(
f"{entry}\t{page}" for entry, page in zip(self.contents, PAGES)
)
# The page in English.
ENGLISH = Words(
relative=(
"Relative, the default: each tabulation steps the pen on by ",
" of the column from where it stands, so the rows do not line up.",
),
typewriter=(
"Typewriter: each tabulation lands on the next multiple of ",
" points, so the rows line up on that grid.",
),
ruler="Ruler: a stop of its own for each tabulation, the second centred on it "
"and the third lined up on its decimal point, the heading, which carries "
"none, ending on the stop.",
listing="A ruler stop at the far edge, the run ending on it and the leader "
"filling the room the tabulation opened.",
columns=("Reference", "Category", "Quantity", "Unit price"),
categories=("Binding", "Cover", "Paper"),
contents=(
"Getting started",
"The page model",
"Fonts and the text engine",
"Tables and their columns",
"Digital signatures",
),
)
# The page in French.
FRENCH = Words(
relative=(
"Relative, par défaut : chaque tabulation avance la plume de ",
" de la colonne depuis où elle est, donc les lignes ne s'alignent pas.",
),
typewriter=(
"Machine à écrire : chaque tabulation tombe sur le multiple suivant de ",
" points, donc les lignes s'alignent sur cette grille.",
),
ruler="Règle : un taquet par tabulation, le deuxième centré dessus et le "
"troisième aligné sur son point décimal, l'en-tête, qui n'en porte pas, "
"finissant sur le taquet.",
listing="Un taquet de règle au bord, la course finissant dessus et les points "
"de conduite remplissant la place que la tabulation a ouverte.",
columns=("Référence", "Catégorie", "Quantité", "Prix unitaire"),
categories=("Reliure", "Couverture", "Papier"),
contents=(
"Pour commencer",
"Le modèle de page",
"Les polices et le moteur de texte",
"Les tableaux et leurs colonnes",
"Les signatures numériques",
),
)
# 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 panel(
content: hqf_pdf.Content,
handle: hqf_pdf.FontHandle,
top: float,
label: str,
flow: hqf_pdf.TextFlow,
text: str,
stops: Sequence[float],
) -> None:
"""Draws one panel: its heading, the tabulated block below it, and a grey rule
at every stop the block was told to use."""
heading = hqf_pdf.TextFlow(handle, 8.0)
heading_lines = heading.break_lines(label, COLUMN)
heading.draw(content, heading_lines, X, top, COLUMN)
text_top = top - DROP
lines = flow.break_lines(text, COLUMN)
height = flow.height(lines)
# The stops first, so the text is read over them rather than under them.
content.save_state()
content.set_stroke(hqf_pdf.Rgb(0.78, 0.78, 0.82))
content.set_line_width(0.5)
for stop in stops:
content.move_to(X + stop, text_top)
content.line_to(X + stop, text_top - height)
content.stroke()
content.restore_state()
flow.draw(content, lines, X, text_top, COLUMN)
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("tab_stops.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()
rows = words.rows()
# Relative, which is what a flow does when it is told nothing: every tabulation
# steps the pen on by the tab size from wherever it stands, so a longer entry pushes
# the whole row along with it.
relative = hqf_pdf.TextFlow(handle, 11.0, leading=16.0)
panel(content, handle, 780.0, words.relative_label(), relative, rows, [])
# A typewriter's tab bar: every tabulation lands on the next multiple of the tab
# size, so the rows line up on a grid however wide their entries are.
typewriter = hqf_pdf.TextFlow(
handle,
11.0,
leading=16.0,
tab_method=hqf_pdf.TabMethod.Typewriter,
tab_size=hqf_pdf.TabSize.points(GRID),
)
panel(
content,
handle,
660.0,
words.typewriter_label(),
typewriter,
rows,
[GRID, GRID * 2.0, GRID * 3.0, GRID * 4.0],
)
# A ruler names its stops, and each one says how the run it opens sits about it: the
# quantities are centred, the prices line up on their decimal point.
ruler = hqf_pdf.TextFlow(
handle,
11.0,
leading=16.0,
tab_method=hqf_pdf.TabMethod.Ruler,
tab_ruler=RULER,
tab_alignments=[
hqf_pdf.TabAlign.Left,
hqf_pdf.TabAlign.Center,
hqf_pdf.TabAlign.Decimal,
],
)
panel(content, handle, 540.0, words.ruler, ruler, rows, RULER)
# The stop sits at the far edge and the run ends on it, so the numbers are flush
# right whatever they are worth, and the leader fills the room the tabulation
# opened.
contents = hqf_pdf.TextFlow(
handle,
11.0,
leading=16.0,
tab_method=hqf_pdf.TabMethod.Ruler,
tab_ruler=[COLUMN],
tab_alignments=[hqf_pdf.TabAlign.Right],
leader=".",
)
panel(content, handle, 420.0, words.listing, contents, words.listed(), [COLUMN])
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()
|