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 | """Sets text in an embedded font, and writes the result to disk.
The Python twin of the `write_text` example in Rust: the same PDF, the same
words, through the binding rather than through the library directly.
`pdftotext` must be able to pull the words back out of the result. If it cannot,
the map back to characters is wrong and the text is unsearchable and uncopyable
even though the page looks right.
Under the invoice, four lines are set with the text state rather than with coordinates:
they step down by the leading, one has its letters pushed apart, one its glyphs
narrowed, and the rule under them is a stream of its own appended to the page's.
Once the file is written, the font is told to keep every glyph, and the terminal says
what the same document weighs with the face carried whole rather than cut down to what
the page draws.
The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which set is
drawn. The number, the issuer and the three amounts are the same in every language.
Usage: python examples/write_text.py [out.pdf] [font.ttf]
HQF_PDF_LANG=fr python examples/write_text.py
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import _language
import _licence
import _out
import hqf_pdf
# The invoice's number.
NUMBER = "1964413"
# Who issued it.
ISSUER = "HQF Development, Cabriès"
# What each of the three billed lines comes to, in the order they are set.
AMOUNTS = ("4 250.00 EUR", "850.00 EUR", "5 100.00 EUR")
# How many characters a billed line runs to: its label, a space, the leader dots, a
# space, then its amount. The dots take whatever is left, so the amounts end on the same
# character however long the label before them is.
LINE_CHARS = 57
@dataclass(frozen=True)
class Words:
"""Every word the page draws, in one language.
What is not language stays out of it: the invoice's number, who issued it and the
three amounts read the same wherever the page is read.
"""
# The document's title, which reading software shows over the page.
title: str
# What stands before the invoice's number.
number_label: str
# The line that gives the day it was issued and the term it falls due in.
dates: str
# What each billed line is called, in the order AMOUNTS prices them.
items: tuple[str, str, str]
# The line that says the lines under it step down by the leading.
leading: str
# The line set with its letters pushed apart.
spaced: str
# The line set with its glyphs narrowed.
narrowed: str
# The line that keeps the leading the move above it set.
stepped: str
def lines(self) -> list[str]:
"""Return the lines the sample page sets, which pdftotext must find again.
They are not all spelled out of the ASCII range for the look of it: an accent
and a dash are two glyphs a wrong ToUnicode map loses first, and a page that
only ever said "Total" would never catch it.
"""
lines = [f"{self.number_label} {NUMBER} — {ISSUER}", self.dates]
for item, amount in zip(self.items, AMOUNTS):
dots = max(0, LINE_CHARS - len(item) - len(amount) - 2)
lines.append(f"{item} {'.' * dots} {amount}")
return lines
# The page in English.
ENGLISH = Words(
title="hqf-pdf text sample",
number_label="Invoice No.",
dates="Issued 14 July 2026 — due within 30 days",
items=(
"Rendering engine development",
"VAT at 20 %",
"Total due",
),
leading="Each line below steps down by the leading, not by a coordinate.",
spaced="These letters stand one and a half points apart.",
narrowed="These glyphs are narrowed to eighty per cent of their width.",
stepped="This line keeps the leading the move above it set: twenty-two points.",
)
# The page in French.
FRENCH = Words(
title="Échantillon de texte hqf-pdf",
number_label="Facture n°",
dates="Émise le 14 juillet 2026 — payable sous 30 jours",
items=(
"Développement du moteur de rendu",
"TVA 20 %",
"Total à payer",
),
leading=(
"Chaque ligne ci-dessous descend de l’interligne, et non d’une coordonnée."
),
spaced="Ces lettres sont écartées d’un point et demi.",
narrowed="Ces glyphes sont ramenés à quatre-vingts pour cent de leur largeur.",
stepped=(
"Cette ligne garde l’interligne fixé par le déplacement du dessus : vingt-deux "
"points."
),
)
# The page in Dutch.
DUTCH = Words(
title="hqf-pdf-tekstvoorbeeld",
number_label="Factuur nr.",
dates="Uitgegeven op 14 juli 2026 — te betalen binnen 30 dagen",
items=(
"Ontwikkeling van de opmaakmotor",
"Btw 20 %",
"Te betalen",
),
leading="Elke regel hieronder zakt met de regelafstand, niet met een coördinaat.",
spaced="Deze letters staan anderhalve punt uit elkaar.",
narrowed="Deze glyphs zijn versmald tot tachtig procent van hun breedte.",
stepped="Deze regel houdt de regelafstand van de stap erboven: tweeëntwintig punten.",
)
# The page in Portuguese.
PORTUGUESE = Words(
title="Amostra de texto hqf-pdf",
number_label="Fatura n.º",
dates="Emitida a 14 de julho de 2026 — vence a 30 dias",
items=(
"Desenvolvimento do motor de paginação",
"IVA a 20 %",
"Total a pagar",
),
leading="Cada linha abaixo desce pelo entrelinhamento, não por uma coordenada.",
spaced="Estas letras estão a ponto e meio umas das outras.",
narrowed="Estes glifos são estreitados para oitenta por cento da sua largura.",
stepped="Esta linha mantém o entrelinhamento do passo acima: vinte e dois pontos.",
)
# The page in Italian.
ITALIAN = Words(
title="Campione di testo hqf-pdf",
number_label="Fattura n.",
dates="Emessa il 14 luglio 2026 — scadenza a 30 giorni",
items=(
"Sviluppo del motore di impaginazione",
"IVA al 20 %",
"Totale dovuto",
),
leading="Ogni riga qui sotto scende dell’interlinea, non di una coordinata.",
spaced="Queste lettere stanno a un punto e mezzo l’una dall’altra.",
narrowed="Questi glifi sono ristretti all’ottanta per cento della loro larghezza.",
stepped="Questa riga conserva l’interlinea fissata sopra: ventidue punti.",
)
# The page in Spanish.
SPANISH = Words(
title="Muestra de texto de hqf-pdf",
number_label="Factura n.º",
dates="Emitida el 14 de julio de 2026 — vence a 30 días",
items=(
"Desarrollo del motor de maquetación",
"IVA al 20 %",
"Total a pagar",
),
leading="Cada línea de abajo baja según el interlineado, no según una coordenada.",
spaced="Estas letras están a punto y medio unas de otras.",
narrowed="Estos glifos se estrechan al ochenta por ciento de su ancho.",
stepped="Esta línea conserva el interlineado del paso de arriba: veintidós puntos.",
)
# The page in German.
GERMAN = Words(
title="hqf-pdf-Textbeispiel",
number_label="Rechnung Nr.",
dates="Ausgestellt am 14. Juli 2026 — zahlbar innerhalb von 30 Tagen",
items=(
"Entwicklung der Rendering-Engine",
"MwSt. 20 %",
"Zu zahlen",
),
leading="Jede Zeile unten rückt um den Zeilenabstand, nicht um eine Koordinate.",
spaced="Diese Buchstaben stehen anderthalb Punkt auseinander.",
narrowed="Diese Glyphen sind auf achtzig Prozent ihrer Breite verengt.",
stepped="Diese Zeile behält den Abstand des Schritts darüber: zweiundzwanzig Punkt.",
)
# 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}
def main() -> None:
language = _language.from_environment()
words = _language.words_of(WORDS, language)
lines = words.lines()
# 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("text.pdf", language)).stem)
font = hqf_pdf.Font.from_path(_out.font_path())
print(
f"font: {font.postscript_name} "
f"({font.glyph_count} glyphs, {font.units_per_em} units per em)"
)
document = hqf_pdf.Document()
document.set_license(_licence.licensed()) # no notice, so the text stands alone
document.set_info("Title", words.title)
handle = document.add_font(font)
content = hqf_pdf.Content()
content.begin_text()
content.set_font(handle, 14.0)
content.text_position(60.0, 760.0)
for line in lines:
content.show_glyphs(handle.glyphs(line))
# Move down one line. The step is relative to the start of the previous one.
content.text_position(0.0, -28.0)
content.end_text()
# A rule under the total, positioned from the measured width of the text, to prove
# the metrics agree with what was drawn.
width = handle.measure(lines[-1], 14.0)
content.set_line_width(0.8)
baseline = 760.0 - 28.0 * 5.0
content.rect(60.0, baseline, width, 0.0)
content.stroke()
# The same kind of lines set with the text state: the leading carries each line
# down, the second is spaced and the third narrowed, and a move that sets the
# leading changes the step of every line after it.
content.begin_text()
content.set_font(handle, 11.0)
content.set_leading(16.0)
content.text_position(60.0, 560.0)
content.show_glyphs(handle.glyphs(words.leading))
content.next_line()
content.set_character_spacing(1.5)
content.show_glyphs(handle.glyphs(words.spaced))
content.set_character_spacing(0.0)
content.next_line()
content.set_horizontal_scaling(80.0)
content.show_glyphs(handle.glyphs(words.narrowed))
content.set_horizontal_scaling(100.0)
content.text_position_with_leading(0.0, -22.0)
content.show_glyphs(handle.glyphs(words.stepped))
content.end_text()
# The rule under them is built as a stream of its own, then appended.
rule = hqf_pdf.Content()
rule.set_line_width(0.4)
rule.move_to(60.0, 494.0)
rule.line_to(535.0, 494.0)
rule.stroke()
content.append(rule)
page = hqf_pdf.Page.a4()
page.set_content(content)
document.add_page(page)
data = document.to_bytes()
out.parent.mkdir(parents=True, exist_ok=True)
out.write_bytes(data)
print(f"wrote {out}: {len(data)} bytes")
print(f"the last line measures {width:.1f} pt at 14 pt")
handle.keep_all()
whole = document.to_bytes()
print(f"with every glyph kept: {len(whole)} bytes")
if __name__ == "__main__":
main()
|