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
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393 | """Writes a deed of deposit claiming the first part of the archival standard,
at level A.
Level A is the one that asks the page to say what it is made of: which run
of words is the heading, which are paragraphs, and which marks are no part
of the reading at all. A screen reader follows that tree; a page that only
looks right has nothing for it to follow.
The first part adds what the later editions dropped: the sheet is built on
PDF 1.4, nothing on it is painted so that what lies behind it shows through,
no part of it can be switched off, and no file travels inside it.
The rule and the page number at the foot are marked as what they are —
furniture, not text — so nothing reading the document aloud says them.
Whether the result really is PDF/A-1A is not for us to say. `veraPDF` says,
and `scripts/check_pdfa.sh` asks it.
The words of the page and of its metadata are held in `Words`, once per language,
and `HQF_PDF_LANG` picks which set the record is written in. The tree names the same
language, so a screen reader reads the page in it.
Usage: `python examples/write_pdfa1a.py [out.pdf] [font.ttf]`
`HQF_PDF_LANG=fr python examples/write_pdfa1a.py`
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import _language
import _licence
import _out
import hqf_pdf
# When the record was drawn up. The library never reads the clock, so the caller says
# when.
ISSUED = "2026-01-19T11:30:00+01:00"
# Who the file says wrote it, and what produced it, in every language alike.
AUTHOR = "HQF Development"
PRODUCER = "hqf-pdf-server"
@dataclass(frozen=True)
class Words:
"""Every word the deed is written in, in one language."""
# What the file says it is, in its metadata.
title: str
# What the file says it is about, in its metadata.
subject: str
# The language the tree says the page is written in, which a screen reader reads it
# in.
lang: str
# The heading, tagged `H1`.
heading: str
# The two paragraphs under the heading, each tagged `P`.
paragraphs: tuple[str, str]
# The page number at the foot, marked as furniture.
foot: str
# The deed in English.
ENGLISH = Words(
title="Deed of deposit, January 2026",
subject="A record that says what it is made of, under the first edition",
lang="en-GB",
heading="Deed of deposit",
paragraphs=(
(
"This deed claims the first part of the archival standard at its highest "
"level. On top of the promise that the page keeps its looks, and on top of "
"every mark of ink saying which letter it stands for, the file says what "
"it is made of: this line is a heading, that block is a paragraph, and the "
"rule across the foot is furniture that says nothing at all.\nSomething "
"reading the deed aloud follows the order the tree gives rather than the "
"order the ink was laid down in, and reading software on a telephone has "
"something to reflow."
),
(
"The papers listed above were deposited on 19 January 2026 and are held "
"for thirty years. The first part of the standard is what the depositary "
"asks for by name, so the sheet carries nothing the part closes: no "
"see-through paint, no part of the page that can be switched off, and no "
"file inside it."
),
),
foot="Page 1 of 1",
)
# The deed in French.
FRENCH = Words(
title="Acte de dépôt, janvier 2026",
subject=("Un acte qui dit de quoi il est fait, selon la première édition de la "
"norme"),
lang="fr-FR",
heading="Acte de dépôt",
paragraphs=(
(
("Cet acte déclare suivre la première partie de la norme d’archivage, à "
"son niveau le plus exigeant. En plus de la promesse que la page garde "
"son apparence, et de chaque trace d’encre qui dit quelle lettre elle "
"représente, le fichier dit de quoi il est fait : cette ligne est un "
"titre, ce bloc est un paragraphe, et le filet en pied de page est un "
"ornement qui ne dit rien.\nUn logiciel qui lit l’acte à voix haute suit "
"l’ordre que donne l’arbre de structure, et non celui dans lequel l’encre"
" a été posée ; sur un téléphone, une application de lecture a de quoi "
"recomposer le texte.")
),
(
("Les pièces décrites ci-dessus ont été déposées le 19 janvier 2026 et "
"sont conservées trente ans. Le dépositaire exige nommément la première "
"partie de la norme ; la feuille ne contient donc rien de ce qu’elle "
"exclut : aucune transparence, aucun élément que l’on puisse masquer, "
"aucun fichier joint.")
),
),
foot="Page 1 sur 1",
)
# The deed in German.
GERMAN = Words(
title="Hinterlegungsurkunde, Januar 2026",
subject="Ein Dokument nach dem ersten Teil der Norm, das angibt, woraus es besteht",
lang="de-DE",
heading="Hinterlegungsurkunde",
paragraphs=(
(
("Diese Urkunde erklärt Konformität mit dem ersten Teil der Archivnorm, "
"auf dessen höchster Stufe. Zugesagt ist, dass die Seite ihr Aussehen "
"behält und jede Druckspur angibt, für welchen Buchstaben sie steht; "
"darüber hinaus gibt die Datei an, woraus sie besteht: Diese Zeile ist "
"eine Überschrift, jener Block ein Absatz, und die Linie am Fuß ist "
"Zierrat ohne jede Aussage.\nEin Vorleseprogramm folgt der Ordnung des "
"Strukturbaums statt der Reihenfolge, in der die Tinte aufgetragen wurde,"
" und eine Lese-App auf dem Smartphone kann den Text neu umbrechen.")
),
(
"Die oben genannten Unterlagen wurden am 19. Januar 2026 hinterlegt und "
"werden dreißig Jahre lang aufbewahrt. Der Verwahrer verlangt ausdrücklich "
"den ersten Teil der Norm, daher enthält das Blatt nichts, was dieser Teil "
"ausschließt: keine durchscheinende Farbe, keinen ausblendbaren Teil der "
"Seite und keine eingebettete Datei."
),
),
foot="Seite 1 von 1",
)
# The deed in Spanish.
SPANISH = Words(
title="Acta de depósito, enero de 2026",
subject="Un documento que dice de qué está hecho, según la primera edición",
lang="es-ES",
heading="Acta de depósito",
paragraphs=(
(
("Esta acta declara cumplir la primera parte de la norma de archivo en su "
"nivel más alto. Además de prometer que la página conserva su aspecto y "
"que cada trazo de tinta dice qué letra representa, el fichero dice de "
"qué está hecho: esta línea es un título, ese bloque es un párrafo y el "
"filete del pie es un adorno que no dice nada.\nUn programa que lea el "
"acta en voz alta sigue el orden que da el árbol, no el orden en que se "
"puso la tinta, y una aplicación de lectura en el móvil tiene con qué "
"recomponer el texto.")
),
(
("Los documentos citados arriba se depositaron el 19 de enero de 2026 y se"
" conservan durante treinta años. El depositario exige expresamente la "
"primera parte de la norma, así que la hoja no contiene nada de lo que "
"esa parte excluye: ni transparencias, ni partes de la página que puedan "
"ocultarse, ni ficheros adjuntos.")
),
),
foot="Página 1 de 1",
)
# The deed in Italian.
ITALIAN = Words(
title="Atto di deposito, gennaio 2026",
subject="Un documento che dice di che cosa è fatto, secondo la prima edizione",
lang="it-IT",
heading="Atto di deposito",
paragraphs=(
(
("Quest’atto dichiara di rispettare la prima parte della norma di "
"archiviazione al suo livello più alto. Oltre alla promessa che la pagina"
" conservi il suo aspetto e che ogni segno d’inchiostro dica quale "
"lettera rappresenta, il file dice di che cosa è fatto: questa riga è un "
"titolo, quel blocco è un paragrafo e il filetto a piè di pagina è un "
"ornamento che non dice nulla.\nUna sintesi vocale legge l’atto seguendo "
"l’ordine dato dall’albero della struttura, non quello in cui è stato "
"steso l’inchiostro, e un’app di lettura sul telefono può reimpaginare il"
" testo.")
),
(
"I documenti indicati sopra sono stati depositati il 19 gennaio 2026 e "
"sono conservati per trent’anni. Il depositario chiede espressamente la "
"prima parte della norma, perciò il foglio non contiene nulla di ciò che "
"quella parte esclude: niente colore trasparente, nessuna parte della "
"pagina che si possa nascondere, nessun file allegato."
),
),
foot="Pagina 1 di 1",
)
# The deed in Portuguese.
PORTUGUESE = Words(
title="Auto de depósito, janeiro de 2026",
subject="Um documento que diz de que é feito, segundo a primeira edição",
lang="pt-PT",
heading="Auto de depósito",
paragraphs=(
(
("Este auto declara cumprir a primeira parte da norma de arquivo, no seu "
"nível mais alto. Além da promessa de que a página mantém o seu aspeto, e"
" de que cada traço de tinta diz que letra representa, o ficheiro diz de "
"que é feito: esta linha é um título, aquele bloco é um parágrafo e o "
"filete do rodapé é um ornamento que nada diz.\nUm programa que lê o auto"
" em voz alta segue a ordem dada pela árvore, e não a ordem em que a "
"tinta foi posta, e uma aplicação de leitura no telemóvel tem com que "
"recompor o texto.")
),
(
"Os documentos acima referidos foram depositados a 19 de janeiro de 2026 e "
"ficam guardados durante trinta anos. O depositário exige expressamente a "
"primeira parte da norma, por isso a folha não contém nada do que essa "
"parte exclui: nem tinta transparente, nem partes da página que se possam "
"ocultar, nem ficheiros anexos."
),
),
foot="Página 1 de 1",
)
# The deed in Dutch.
DUTCH = Words(
title="Akte van depot, januari 2026",
subject="Een document dat zegt waaruit het bestaat, volgens de eerste editie",
lang="nl-NL",
heading="Akte van depot",
paragraphs=(
(
("Deze akte verklaart te voldoen aan het eerste deel van de archiefnorm, "
"op het hoogste niveau. Naast de belofte dat de pagina haar uiterlijk "
"behoudt en dat elk inktspoor aangeeft voor welke letter het staat, "
"vertelt het bestand waaruit het bestaat: deze regel is een kop, dat blok"
" is een alinea en de lijn onderaan is opsmuk die niets zegt.\nEen "
"voorleesprogramma houdt de volgorde van de boom aan en niet die waarin "
"de inkt is neergezet, en een leesapp op een telefoon kan de tekst "
"herschikken.")
),
(
("De hierboven genoemde stukken zijn op 19 januari 2026 gedeponeerd en "
"worden dertig jaar bewaard. De bewaarnemer vraagt uitdrukkelijk om het "
"eerste deel van de norm; daarom bevat het blad niets wat dat deel "
"uitsluit: geen transparantie, geen deel van de pagina dat kan worden "
"uitgeschakeld en geen bijgevoegd bestand.")
),
),
foot="Pagina 1 van 1",
)
# 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,
}
# Where each block of what the page says goes, in the order it is read: the tag it
# stands under, the size it is set at, the box it goes in, and the leading where it
# states one. The blocks say the heading and the two paragraphs of `Words`, in that
# order.
READ = (
("H1", 20.0, 72.0, 782.0, 451.0, None),
("P", 11.0, 72.0, 730.0, 451.0, 16.0),
("P", 11.0, 72.0, 580.0, 451.0, 16.0),
)
# The rule at the foot of the page, and the box of the line under it. Neither is part of
# the reading, so both are marked as furniture.
RULE = (72.0, 70.0, 451.0, 1.0)
FOOT = (9.0, 72.0, 68.0, 451.0)
def foot_of_page(
content: hqf_pdf.Content, handle: hqf_pdf.FontHandle, text: str
) -> None:
"""Draws the rule and the page number, both marked as furniture so that
nothing reading the document aloud says them."""
left, low, width, height = RULE
content.begin_artifact(hqf_pdf.Artifact.Footer)
content.save_state()
# Rust says this as `set_fill_rgb`; Python takes the colour itself.
content.set_fill(hqf_pdf.Rgb(0.0, 0.0, 0.0))
content.rect(left, low, width, height)
content.fill()
content.restore_state()
content.end_marked()
size, left, top, width = FOOT
flow = hqf_pdf.TextFlow(
handle, size, first_baseline=hqf_pdf.FirstBaseline.points(0.0)
)
lines = flow.break_lines(text, width)
content.begin_artifact(hqf_pdf.Artifact.Footer)
# The Python side opens and closes the text object itself.
flow.draw(content, lines, left, top, width)
content.end_marked()
def sheet(handle: hqf_pdf.FontHandle, words: Words):
"""The one page, and the branch of the tree that says what is on it."""
content = hqf_pdf.Content()
marks = hqf_pdf.Marks()
branches = []
said = (words.heading, words.paragraphs[0], words.paragraphs[1])
for (tag, size, left, top, width, leading), text in zip(READ, said):
settings = {} if leading is None else {"leading": leading}
flow = hqf_pdf.TextFlow(
handle,
size,
first_baseline=hqf_pdf.FirstBaseline.points(0.0),
**settings,
)
lines = flow.break_lines(text, width)
runs = flow.draw_tagged(content, lines, left, top, width, tag, marks)
branches.append(
hqf_pdf.StructElement(tag, children=[(0, run) for run in runs])
)
foot_of_page(content, handle, words.foot)
page = hqf_pdf.Page.a4()
page.set_content(content)
return page, hqf_pdf.StructElement("Sect", children=branches)
def main() -> None:
"""Writes the file, and says where it went."""
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
# languages do not overwrite each other in `tmp/`.
out = _out.output_path(Path(_language.file_name("pdfa1a.pdf", language)).stem)
document = hqf_pdf.Document()
document.set_license(_licence.licensed())
document.set_version(hqf_pdf.Version.V1_4)
document.set_conformance(hqf_pdf.PdfA.A1A)
document.set_metadata(
hqf_pdf.Metadata(
title=words.title,
author=AUTHOR,
subject=words.subject,
producer=PRODUCER,
created=ISSUED,
)
)
handle = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))
page, sect = sheet(handle, words)
document.add_page(page)
document.set_structure(
hqf_pdf.StructureTree(
children=[hqf_pdf.StructElement("Document", children=[sect])],
lang=words.lang,
)
)
data = document.to_bytes()
out.parent.mkdir(parents=True, exist_ok=True)
out.write_bytes(data)
print(f"wrote {out}: {len(data)} bytes")
if __name__ == "__main__":
main()
|