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 | """Sets one justified paragraph in a narrow column four times: first with no
break points inside its words, then breaking after a slash or a hyphen, then
breaking where its punctuation opens a break, so the paths and compounds that ran
past the edge can be seen wrapping in it, and last breaking where its punctuation
opens a break once each hyphen has given way to a non-breaking one, so the paths
still wrap and the compounds stay whole.
The Python twin of the `write_break_after` example in Rust. The text carries
file paths and hyphenated compounds whose tokens are wider than the column. The
first setting breaks only at spaces, so those tokens run past the column's right
edge; the second breaks after "/" and "-", keeping the character on the line, so
the tokens wrap inside the column; the third names no character and reads the
punctuation itself, breaking after a slash or a hyphen but never after one that
opens a path; the fourth reads the punctuation too, its text given with each
hyphen replaced by a non-breaking hyphen before the lines are broken.
The sentence and the four headings are held in `Words`, once per language, and
`HQF_PDF_LANG` picks which set is drawn. The two paths are addresses, and stand the same
in every language.
Usage: python examples/write_break_after.py [out.pdf] [font.ttf]
HQF_PDF_LANG=fr python examples/write_break_after.py
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import _language
import _licence
import _out
import hqf_pdf
# The two addresses the sentence carries, each wider than the column so that a break
# after a slash can be seen falling inside them.
RENDER_PATH = "/api/v2/organizations/documents/render"
PREVIEW_PATH = "/api/v2/organizations/documents/preview"
@dataclass(frozen=True)
class Words:
"""Every word the page draws, in one language.
What is not language stays out of it: the two paths are addresses a caller types,
and each set of words carries a hyphenated compound of its own so that every setting
has something to break in.
"""
# The heading over the setting that breaks at spaces only.
plain: str
# The heading over the setting that breaks after a slash and a hyphen.
breaking: str
# The heading over the setting that breaks where its punctuation opens a break.
punctuation: str
# The heading over the setting that breaks where its punctuation opens a break, each
# hyphen replaced by a non-breaking one.
whole: str
# The sentence, cut where the two paths go into it.
text_before: str
text_between: str
text_after: str
def text(self) -> str:
"""Return the sentence every setting is broken to, with the paths in it."""
return (
f"{self.text_before}{RENDER_PATH}"
f"{self.text_between}{PREVIEW_PATH}{self.text_after}"
)
# The page in English.
ENGLISH = Words(
plain="Without break points: the paths run past the column's edge.",
breaking="Breaking after slash and hyphen: the paths and compounds wrap.",
punctuation="Breaking where punctuation allows: paths and compounds wrap, no character named.",
whole="Breaking where punctuation allows, no-break hyphens: paths wrap, compounds stay whole.",
text_before="Send each request to ",
text_between=", then read the read-only copy from ",
text_after=" on the rate-limited tier.",
)
# The page in French.
FRENCH = Words(
plain="Sans point de coupure : les chemins débordent de la colonne.",
breaking="Coupure après la barre oblique et le trait d’union : chemins et mots composés se coupent.",
punctuation="Coupure à la ponctuation, sans nommer de caractère : chemins et mots composés se coupent.",
whole="Coupure à la ponctuation, trait d’union insécable : les chemins se coupent, pas les mots composés.",
text_before="Envoyez chaque requête à ",
text_between=", puis consultez le fac-similé en lecture seule à l’adresse ",
text_after=" avec l’offre à débit limité.",
)
# The page in Dutch.
DUTCH = Words(
plain="Zonder breekpunten: paden lopen over de rand van de kolom.",
breaking="Breken na schuine streep en koppelteken: paden en samenstellingen worden afgebroken.",
punctuation="Breken bij leestekens, geen teken opgegeven: paden en samenstellingen worden afgebroken.",
whole="Breken bij leestekens, vaste koppeltekens: paden worden afgebroken, samenstellingen niet.",
text_before="Stuur elk verzoek naar ",
text_between=" en lees daarna de alleen-lezenkopie van ",
text_after=" op het niveau met verzoeklimiet.",
)
# The page in Portuguese.
PORTUGUESE = Words(
plain="Sem pontos de quebra: os caminhos ultrapassam a margem da coluna.",
breaking="Quebra após a barra e o hífen: os caminhos e a palavra composta partem-se.",
punctuation="Quebra pela pontuação, sem indicar caracteres: os caminhos e a palavra composta partem-se.",
whole="Quebra pela pontuação, hífen inseparável: partem-se os caminhos, não a palavra composta.",
text_before="Envie cada pedido para ",
text_between=" e, depois, leia a cópia só de leitura em ",
text_after=", no escalão com taxa-limite de pedidos.",
)
# The page in Italian.
ITALIAN = Words(
plain="Senza punti di interruzione: i percorsi escono dal bordo della colonna.",
breaking="Interruzione dopo barra e trattino: percorsi e composti vanno a capo.",
punctuation="Interruzione sulla punteggiatura, senza indicare segni: percorsi e composti vanno a capo.",
whole="Interruzione sulla punteggiatura, trattini indivisibili: a capo i percorsi, non i composti.",
text_before="Inviare ogni richiesta a ",
text_between=", poi leggere da ",
text_after=" la copia in sola lettura, sul livello con soglia-limite di richieste.",
)
# The page in Spanish.
SPANISH = Words(
plain="Sin puntos de corte: las rutas se salen del borde de la columna.",
breaking="Corte tras la barra y el guion: las rutas y los compuestos se parten.",
punctuation="Corte según la puntuación, sin nombrar caracteres: las rutas y los compuestos se parten.",
whole="Corte según la puntuación con guiones inseparables: las rutas se parten, los compuestos no.",
text_before="Envíe cada petición a ",
text_between=" como pares clave-valor y después lea la copia de solo lectura desde ",
text_after=", en el nivel con límite de peticiones.",
)
# The page in German.
GERMAN = Words(
plain="Ohne Umbruchstellen: Pfade laufen über den Spaltenrand hinaus.",
breaking="Umbruch nach Schrägstrich und Bindestrich: Pfade und Komposita brechen um.",
punctuation="Umbruch nach Satzzeichen, ohne Zeichenvorgabe: Pfade und Komposita brechen um.",
whole="Umbruch nach Satzzeichen, Bindestriche geschützt: Pfade brechen um, Komposita bleiben ganz.",
text_before="Senden Sie jede Anfrage an ",
text_between=" und lesen Sie anschließend die Nur-Lese-Kopie unter ",
text_after=" im Tarif mit Ratenbegrenzung.",
)
# 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}
# The column every setting is broken to, narrow enough that the paths reach its edge.
COLUMN = 125.0
# What the last setting puts in place of each hyphen: a hyphen no line breaks after.
NON_BREAKING_HYPHEN = "\u2011"
# The width a heading is set in, on one line.
HEADING_WIDTH = 400.0
# The left edge of every setting, stacked one above the other.
X = 80.0
# Where the heading of the first setting of a page stands.
TOP = 760.0
# The room left between the bottom of one setting and the heading of the next, where the
# next one's place follows from the one above it.
GAP = 30.0
def panel(
content: hqf_pdf.Content,
handle: hqf_pdf.FontHandle,
label: str,
flow: hqf_pdf.TextFlow,
text: str,
top: float,
) -> float:
"""Set `label` at `top` and `text` below it in the column, broken by `flow`, with
the column's edge around it, and return where the setting ends."""
heading = hqf_pdf.TextFlow(handle, 8.0)
heading_lines = heading.break_lines(label, HEADING_WIDTH)
heading.draw(content, heading_lines, X, top, HEADING_WIDTH)
text_top = top - 20.0
lines = flow.break_lines(text, COLUMN)
flow.draw(content, lines, X, text_top, COLUMN)
# The column's edge, so the lines can be read against it.
height = flow.height(lines)
content.save_state()
content.set_stroke(hqf_pdf.Rgb.gray(0.8))
content.set_line_width(0.5)
content.rect(X, text_top - height, COLUMN, height)
content.stroke()
content.restore_state()
return text_top - height
def setting(
handle: hqf_pdf.FontHandle,
break_after: str = "",
punctuation: bool = False,
replacing: dict[str, hqf_pdf.Replacement] | None = None,
) -> hqf_pdf.TextFlow:
"""Return the flow every panel sets its text in, breaking as asked and making
the replacements `replacing` names first."""
return hqf_pdf.TextFlow(
handle,
11.0,
leading=15.0,
align=hqf_pdf.Align.Justify,
break_after=break_after,
break_at_punctuation=punctuation,
replacing=replacing or {},
)
def pages(
handle: hqf_pdf.FontHandle, words: Words
) -> list[list[tuple[str, hqf_pdf.TextFlow]]]:
"""Return the settings the page sets its text in, each under its heading, two to
a page: the last two share theirs, so that the compound the one breaks can be
seen whole in the other.
Every setting sets the same text; only where a word may break inside itself
differs between them.
"""
return [
[
(words.plain, setting(handle)),
(words.breaking, setting(handle, "/-")),
],
[
(words.punctuation, setting(handle, punctuation=True)),
(
words.whole,
setting(
handle,
punctuation=True,
replacing={"-": hqf_pdf.Replacement.each(NON_BREAKING_HYPHEN)},
),
),
],
]
def set_page(
content: hqf_pdf.Content,
handle: hqf_pdf.FontHandle,
panels: list[tuple[str, hqf_pdf.TextFlow]],
text: str,
) -> float:
"""Set each of `panels` one under the other from the top of a page, and return
where the last one ends."""
top = TOP
bottom = TOP
for label, flow in panels:
bottom = panel(content, handle, label, flow, text, top)
top = bottom - GAP
return bottom
def main() -> None:
language = _language.from_environment()
words = _language.words_of(WORDS, language)
text = words.text()
# 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("break_after.pdf", language)).stem)
document = hqf_pdf.Document()
document.set_license(_licence.licensed())
handle = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))
for panels in pages(handle, words):
content = hqf_pdf.Content()
set_page(content, handle, panels, text)
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()
|