write_added_to.py

Le fichier Python de l'exemple « Un contrat reçu, complété sans déplacer un octet ». Un contrat de maintenance envoyé par quelqu'un d'autre, avec un emplacement de signature, une date et une ligne à soi posés sur sa dernière page et une annexe ajoutée après elle, chaque octet du contrat laissé à sa place.

Python 226 lignes

À quoi sert cet exemple

Un fournisseur envoie un contrat de maintenance en PDF de deux pages. Il doit repartir avec un emplacement de signature, la date, une ligne qui dit qui signe, et une annexe d'une page à la fin. Ouvrir le contrat et le réécrire rendrait un autre fichier : une signature qu'il portait déjà ne tiendrait plus, et personne ne pourrait prouver que le texte est toujours celui qui a été envoyé.

Ce que montre cet exemple

  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
"""Adds a signature area, a date, a line of one's own and a page at the end to an
agreement somebody else sent, 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 file
that comes out is the agreement, byte for byte, followed by a section 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 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 jour ouvré même.",
        "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 y met fin par lettre, trois mois avant son terme.",
    ),
    second=(
        "6. Le client garde les locaux des machines dégagés 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é.",
    ),
)

# 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 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.set_stroke(hqf_pdf.Rgb(0.2, 0.35, 0.7))
    over.set_line_width(1.0)
    over.rect(x, y, width, height)
    over.fill_and_stroke()
    over.set_fill(hqf_pdf.Rgb(0.2, 0.35, 0.7))
    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.
    written = update.to_added_bytes(additions(words, font, len(received)), [1])

    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()