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 | """Adds a signature area, a date, a line of one's own and a page at the end to an
agreement somebody else sent, then a place to sign over the signature area, without
moving a byte of it.
The Python twin of the `write_added_to` example in Rust. The agreement arrives as a PDF
of two pages. What is added is created as a document of its own: its first page is
drawn over the agreement's last page, and its second page is added after it. The
framed, empty signature field `FIELD` is then added over the signature area, in a
section of its own, through `IncrementalUpdate.to_bytes_with_signature_field`. The file
that comes out is the agreement, byte for byte, followed by the sections holding what
was added — an ordinary signature the agreement already carried still covers the bytes
it covered, and a reader that shows the file's earlier versions still shows the
agreement as it arrived. A signature certifying the agreement against change does not
survive: no permission ISO 32000-2 table 257 gives `/DocMDP` allows a page to be added
or drawn over, and nothing here looks for one.
The example creates the agreement itself first, so that it runs with nothing handed to
it. Every line both documents draw is held in `Words`, once per language, and
`HQF_PDF_LANG` picks which set is drawn.
Usage: python examples/write_added_to.py [out.pdf]
HQF_PDF_LANG=fr python examples/write_added_to.py
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import _language
import _licence
import _out
import hqf_pdf
# The margin every line starts from.
MARGIN = 56.0
# The size of a title.
TITLE_SIZE = 18.0
# The size of a line of the agreement and of the annex.
BODY_SIZE = 11.0
# How far apart two lines of the agreement and of the annex stand.
LEADING = 18.0
# The size of what is written in and under the signature area.
SMALL_SIZE = 9.0
# The signature area: its left edge, its bottom, its width and its height.
AREA = (320.0, 120.0, 219.0, 90.0)
# The name of the place to sign added over the signature area.
FIELD = "approval"
# The colour of the frame of the place to sign, and of the word written in the corner of
# the signature area.
FRAME = hqf_pdf.Rgb(0.2, 0.35, 0.7)
# The date the agreement is received on, written the same way in every language.
DATE = "2026-09-12"
@dataclass(frozen=True)
class Words:
"""The words both documents are written in, in one language."""
# The title of the agreement.
title: str
# The lines of the agreement's first page.
first: tuple[str, str, str, str, str]
# The lines of the agreement's second page.
second: tuple[str, str, str, str, str]
# What is written in the corner of the signature area.
signature: str
# What stands before the date under the signature area.
received_on: str
# The line of one's own, written above the signature area.
approval: str
# The title of the page added at the end.
annex_title: str
# The lines of the page added at the end. `{bytes}` stands for how many bytes the
# agreement arrived as.
annex: tuple[str, str, str, str]
# The documents in English.
ENGLISH = Words(
title="Maintenance agreement",
first=(
"1. The supplier services the two lifts of the building every quarter.",
"2. A fault reported before noon is looked at the same working day.",
"3. Parts are charged at the price list in force on the day they are fitted.",
"4. The agreement runs for one year and renews itself unless ended.",
"5. Either party ends it by letter, three months before it renews.",
),
second=(
"6. The customer keeps the machine rooms clear and reachable.",
"7. The supplier's staff sign the logbook at every visit.",
"8. An invoice is paid within thirty days of the date it bears.",
"9. Disputes go before the courts where the building stands.",
"Signed for the supplier, and sent to the customer to sign.",
),
signature="Signature",
received_on="Received on",
approval="Read and approved, subject to annex A.",
annex_title="Annex A",
annex=(
"The lift in the north wing is serviced every month, not every quarter.",
"This page, the signature area, the date and the line above it were",
"added after the agreement arrived. The first {bytes} bytes of this file",
"are the agreement exactly as it was sent.",
),
)
# The documents in French.
FRENCH = Words(
title="Contrat de maintenance",
first=(
"1. Le prestataire entretient les deux ascenseurs chaque trimestre.",
"2. Une panne signalée avant midi est examinée le même jour ouvré.",
"3. Les pièces sont facturées au tarif en vigueur le jour de leur pose.",
"4. Le contrat court un an et se renouvelle sauf résiliation.",
"5. Chaque partie peut le résilier par lettre, trois mois avant l’échéance.",
),
second=(
"6. Le client maintient les salles des machines dégagées et accessibles.",
"7. Le personnel du prestataire signe le registre à chaque visite.",
"8. Une facture est réglée dans les trente jours suivant sa date.",
"9. Les litiges relèvent des tribunaux du lieu de l’immeuble.",
"Signé pour le prestataire, et envoyé au client pour signature.",
),
signature="Signature",
received_on="Reçu le",
approval="Lu et approuvé, sous réserve de l’annexe A.",
annex_title="Annexe A",
annex=(
("L’ascenseur de l’aile nord est entretenu chaque mois, et non chaque "
"trimestre."),
"Cette page, la zone de signature, la date et la ligne au-dessus ont été",
"ajoutées après réception du contrat. Les {bytes} premiers octets de ce",
"fichier sont le contrat exactement tel qu’il a été envoyé.",
),
)
# The documents in Dutch.
DUTCH = Words(
title="Onderhoudscontract",
first=(
"1. De leverancier onderhoudt de twee liften van het gebouw elk kwartaal.",
"2. Een storing die vóór twaalf uur is gemeld, wordt nog dezelfde werkdag bekeken.",
"3. Onderdelen worden berekend volgens de prijslijst die geldt op de dag van montage.",
"4. Het contract loopt een jaar en wordt stilzwijgend verlengd.",
"5. Elke partij kan het per brief opzeggen, uiterlijk drie maanden voor de verlenging.",
),
second=(
"6. De klant houdt de machinekamers vrij en bereikbaar.",
"7. Het personeel van de leverancier tekent het logboek bij elk bezoek.",
"8. Een factuur wordt betaald binnen dertig dagen na de factuurdatum.",
"9. Geschillen worden voorgelegd aan de rechter van de plaats waar het gebouw staat.",
"Namens de leverancier ondertekend en ter ondertekening naar de klant gestuurd.",
),
signature="Handtekening",
received_on="Ontvangen op",
approval="Gelezen en goedgekeurd, onder voorbehoud van bijlage A.",
annex_title="Bijlage A",
annex=(
"De lift in de noordvleugel wordt elke maand onderhouden, niet elk kwartaal.",
"Deze pagina, het handtekeningvak, de datum en de regel erboven zijn",
"toegevoegd nadat het contract binnenkwam. De eerste {bytes} bytes van dit",
"bestand zijn het contract precies zoals het verstuurd is.",
),
)
# The documents in Portuguese.
PORTUGUESE = Words(
title="Contrato de manutenção",
first=(
"1. O fornecedor revê os dois elevadores do edifício todos os trimestres.",
"2. Uma avaria comunicada antes do meio-dia é atendida no mesmo dia útil.",
"3. As peças são cobradas pela tabela em vigor no dia da montagem.",
"4. O contrato dura um ano e renova-se automaticamente salvo denúncia.",
"5. Qualquer das partes pode denunciá-lo por carta, com três meses de antecedência.",
),
second=(
"6. O cliente mantém as casas das máquinas livres e acessíveis.",
"7. O pessoal do fornecedor assina o livro de registo em cada visita.",
"8. Uma fatura paga-se nos trinta dias seguintes à sua data.",
"9. Os litígios são submetidos ao tribunal da comarca onde fica o edifício.",
"Assinado pelo fornecedor e enviado ao cliente para assinar.",
),
signature="Assinatura",
received_on="Recebido em",
approval="Lido e aprovado, salvo o anexo A.",
annex_title="Anexo A",
annex=(
"O elevador da ala norte é revisto todos os meses, não a cada trimestre.",
"Esta página, a zona de assinatura, a data e a linha acima foram",
"acrescentadas depois de o contrato chegar. Os primeiros {bytes} bytes",
"deste ficheiro são o contrato tal como foi enviado.",
),
)
# The documents in Italian.
ITALIAN = Words(
title="Contratto di manutenzione",
first=(
"1. Il fornitore revisiona i due ascensori dell’edificio ogni trimestre.",
"2. Un guasto segnalato prima di mezzogiorno è esaminato nello stesso giorno.",
"3. I ricambi sono addebitati al listino in vigore il giorno del montaggio.",
"4. Il contratto dura un anno e si rinnova tacitamente salvo disdetta.",
"5. Ciascuna parte lo disdice per lettera, tre mesi prima del rinnovo.",
),
second=(
"6. Il cliente tiene i locali macchine sgombri e accessibili.",
"7. Il personale del fornitore firma il registro a ogni visita.",
"8. Una fattura si paga entro trenta giorni dalla sua data.",
"9. Le controversie vanno al foro del luogo in cui sorge l’edificio.",
"Firmato per il fornitore e inviato al cliente per la firma.",
),
signature="Firma",
received_on="Ricevuto il",
approval="Letto e approvato, salvo l’allegato A.",
annex_title="Allegato A",
annex=(
"L’ascensore dell’ala nord è revisionato ogni mese, non ogni trimestre.",
"Questa pagina, la zona della firma, la data e la riga sopra sono state",
"aggiunte dopo l’arrivo del contratto. I primi {bytes} byte di questo file",
"sono il contratto esattamente come è stato inviato.",
),
)
# The documents in Spanish.
SPANISH = Words(
title="Contrato de mantenimiento",
first=(
"1. El proveedor revisa los dos ascensores del edificio cada trimestre.",
"2. Una avería comunicada antes del mediodía se atiende el mismo día hábil.",
"3. Las piezas se cobran a la tarifa vigente el día de su montaje.",
"4. El contrato dura un año y se renueva solo salvo que se rescinda.",
"5. Cualquiera de las partes puede rescindirlo por carta con tres meses de antelación.",
),
second=(
"6. El cliente mantiene las salas de máquinas despejadas y accesibles.",
"7. El personal del proveedor firma el libro de registro en cada visita.",
"8. Cada factura se paga en los treinta días siguientes a su fecha.",
"9. Los litigios se someten a los tribunales del lugar donde se encuentra el edificio.",
"Firmado por el proveedor y enviado al cliente para su firma.",
),
signature="Firma",
received_on="Recibido el",
approval="Leído y conforme, salvo el anexo A.",
annex_title="Anexo A",
annex=(
"El ascensor del ala norte se revisa cada mes, no cada trimestre.",
"Esta página, la zona de firma, la fecha y la línea de arriba se",
"añadieron después de recibir el contrato. Los primeros {bytes} bytes",
"de este archivo son el contrato tal como se envió.",
),
)
# The documents in German.
GERMAN = Words(
title="Wartungsvertrag",
first=(
"1. Der Auftragnehmer wartet die beiden Aufzüge des Gebäudes jedes Quartal.",
"2. Eine vor zwölf Uhr gemeldete Störung wird am selben Werktag geprüft.",
"3. Ersatzteile werden zum Listenpreis am Tag des Einbaus berechnet.",
"4. Der Vertrag läuft ein Jahr und verlängert sich, sofern er nicht gekündigt wird.",
"5. Jede Partei kann ihn bis drei Monate vor der Verlängerung schriftlich kündigen.",
),
second=(
"6. Der Kunde hält die Maschinenräume frei und zugänglich.",
"7. Das Personal des Auftragnehmers unterschreibt bei jedem Besuch das Prüfbuch.",
"8. Eine Rechnung ist binnen dreißig Tagen ab ihrem Datum zu zahlen.",
"9. Gerichtsstand ist der Ort, an dem das Gebäude steht.",
"Für den Auftragnehmer unterschrieben und dem Kunden zur Unterschrift gesandt.",
),
signature="Unterschrift",
received_on="Erhalten am",
approval="Gelesen und genehmigt, vorbehaltlich Anlage A.",
annex_title="Anlage A",
annex=(
"Der Aufzug im Nordflügel wird jeden Monat gewartet, nicht jedes Quartal.",
"Diese Seite, das Unterschriftsfeld, das Datum und die Zeile darüber wurden",
"hinzugefügt, nachdem der Vertrag eingegangen war. Die ersten {bytes} Bytes",
"dieser Datei sind der Vertrag, genau so, wie er versandt wurde.",
),
)
# 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 grouped(number: int) -> str:
"""A whole number with its thousands set apart by a space."""
digits = str(number)
out = ""
for rank, digit in enumerate(digits):
if rank > 0 and (len(digits) - rank) % 3 == 0:
out += " "
out += digit
return out
def page_of_lines(
font: hqf_pdf.FontHandle, title: str, lines: list[str] | tuple[str, ...]
) -> hqf_pdf.Page:
"""A page holding a title and `lines` under it."""
content = hqf_pdf.Content()
content.draw_text(font, TITLE_SIZE, MARGIN, 770.0, title)
y = 730.0
for text in lines:
content.draw_text(font, BODY_SIZE, MARGIN, y, text)
y -= LEADING
page = hqf_pdf.Page.a4()
page.set_content(content)
return page
def agreement(words: Words, font: hqf_pdf.Font) -> bytes:
"""The agreement as it is sent: two pages, created by somebody else."""
document = hqf_pdf.Document()
document.set_license(_licence.licensed())
handle = document.add_font(font)
document.add_page(page_of_lines(handle, words.title, words.first))
document.add_page(page_of_lines(handle, words.title, words.second))
return document.to_bytes()
def additions(words: Words, font: hqf_pdf.Font, received: int) -> hqf_pdf.Document:
"""What is added: a page drawn over the agreement's last page, holding the signature
area, the date and the line of one's own, and the annex added after it."""
document = hqf_pdf.Document()
document.set_license(_licence.licensed())
handle = document.add_font(font)
x, y, width, height = AREA
over = hqf_pdf.Content()
over.save_state()
over.set_fill(hqf_pdf.Rgb(0.93, 0.96, 1.0))
over.rect(x, y, width, height)
over.fill()
over.set_fill(FRAME)
over.draw_text(handle, SMALL_SIZE, x + 8.0, y + height - 14.0, words.signature)
over.restore_state()
over.draw_text(handle, SMALL_SIZE, x, y - 16.0, f"{words.received_on} {DATE}")
over.draw_text(handle, BODY_SIZE, MARGIN, 240.0, words.approval)
page = hqf_pdf.Page.a4()
page.set_content(over)
document.add_page(page)
count = grouped(received)
annex = [text.replace("{bytes}", count) for text in words.annex]
document.add_page(page_of_lines(handle, words.annex_title, annex))
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("added_to.pdf", language)).stem)
font = hqf_pdf.Font.from_path(_out.DEFAULT_FONT)
received = agreement(words, font)
update = hqf_pdf.IncrementalUpdate(received)
update.set_license(_licence.licensed())
# The first page of the additions is drawn over the agreement's second page; the
# annex, which no entry names, comes after it.
added = update.to_added_bytes(additions(words, font, len(received)), [1])
# The place to sign goes on the page drawn over, the second of the file.
x, y, width, height = AREA
field = hqf_pdf.SignatureField(
FIELD, x, y, width, height, border_color=FRAME, border_width=1.0
)
update = hqf_pdf.IncrementalUpdate(added)
update.set_license(_licence.licensed())
written = update.to_bytes_with_signature_field(1, field)
Path(out).write_bytes(written)
print(
f"wrote {out}: {len(written)} bytes, "
f"the first {len(received)} of them the agreement as it arrived"
)
if __name__ == "__main__":
main()
|