Un formulaire interactif

Un formulaire que le lecteur remplit : champs texte, case à cocher, liste déroulante, boutons radio.

Python write_form.py 227 lignes
  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
"""Writes a document with a small form: labelled boxes a reader fills in, a
check box a reader ticks, a drop-down a reader picks from, a set of radio
buttons a reader chooses one of, and a field a reader signs.

The Python twin of the `write_form` example in Rust: the same document, through
the binding rather than through the library directly.

Each text field is a labelled rectangle the reader types into. The label is
drawn on the page, as any other text; the box beside it is the field, empty but
for any value it starts with. The check box under them is ticked on or off, the
drop-down under that offers a set of options, the radio buttons under that let
one of several be chosen, and the signature field under those is a place the
reader signs. A reader's software draws what is typed, the tick, the chosen
option, and the dot itself.

Every field carries a border, so the boxes a reader clicks are plain to see. The
standard draws none by default.

The labels, the options and the document's title are held in `Words`, once per
language, and `HQF_PDF_LANG` picks which set is drawn. A field's name is not among
them: it is what a filled form's data comes back under, so it stays the same
whichever language the page is drawn in.

Usage: python examples/write_form.py [out.pdf] [font.ttf]
       HQF_PDF_LANG=fr python examples/write_form.py
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The text fields the form asks for: the name a filled form's data comes back under, and
# the value the field starts with.
FIELDS = [("Name", ""), ("Email", ""), ("Country", "France")]

# The names the two radio buttons send back, in the order they are drawn.
PAYMENT_VALUES = ("card", "transfer")


@dataclass(frozen=True)
class Words:
    """Every word the form draws, in one language.

    A field's name is not here: it is the name a filled form's data comes back under,
    and a reader that changed language would send back something else.
    """

    # The line at the head of the page.
    title: str
    # The label drawn beside each text field, in the order `FIELDS` names them.
    labels: tuple[str, str, str]
    # The line beside the check box.
    subscribe: str
    # The label beside the drop-down.
    plan: str
    # The options the drop-down offers; the second is the one it starts on.
    plans: tuple[str, str]
    # The label beside the radio buttons.
    payment: str
    # The line beside each radio button, in the order they are drawn.
    payments: tuple[str, str]
    # The label beside the signature field.
    signature: str
    # The title the document carries in its information dictionary.
    document_title: str


# The page in English.
ENGLISH = Words(
    title="Please fill in",
    labels=("Name", "Email", "Country"),
    subscribe="Subscribe to the newsletter",
    plan="Plan",
    plans=("Monthly", "Yearly"),
    payment="Payment",
    payments=("Card", "Transfer"),
    signature="Signature",
    document_title="A document with a form",
)

# The page in French.
FRENCH = Words(
    title="Merci de remplir ce formulaire",
    labels=("Nom", "Courriel", "Pays"),
    subscribe="S'abonner à la lettre d'information",
    plan="Formule",
    plans=("Mensuelle", "Annuelle"),
    payment="Paiement",
    payments=("Carte", "Virement"),
    signature="Signature",
    document_title="Un document avec un formulaire",
)


# 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}

# The size the labels and the field text are set at.
SIZE = 12.0

# The colour every field's border is stroked in.
BORDER = hqf_pdf.Rgb.gray(0.6)
# The width every field's border is stroked at, in points.
BORDER_WIDTH = 0.75


def form_page(handle: hqf_pdf.FontHandle, words: Words) -> hqf_pdf.Page:
    """The form page: a title, then a label and a fillable box for each field."""
    content = hqf_pdf.Content()
    page = hqf_pdf.Page.a4()

    content.draw_text(handle, 20.0, 72.0, 760.0, words.title)

    top = 700.0
    for (name, value), label in zip(FIELDS, words.labels):
        content.draw_text(handle, SIZE, 72.0, top, label)
        page.add_field(
            hqf_pdf.FormField(
                name,
                180.0,
                top - 3.0,
                240.0,
                SIZE + 4.0,
                handle,
                SIZE,
                value=value or None,
                border_color=BORDER,
                border_width=BORDER_WIDTH,
            )
        )
        top -= SIZE * 3.0

    content.draw_text(handle, SIZE, 90.0, top, words.subscribe)
    page.add_checkbox(hqf_pdf.CheckBox(
            "Subscribe",
            72.0,
            top - 2.0,
            SIZE,
            SIZE,
            checked=True,
            border_color=BORDER,
            border_width=BORDER_WIDTH,
        ))
    top -= SIZE * 3.0

    content.draw_text(handle, SIZE, 72.0, top, words.plan)
    page.add_choice(
        hqf_pdf.ChoiceField(
            "Plan",
            180.0,
            top - 3.0,
            240.0,
            SIZE + 4.0,
            handle,
            SIZE,
            options=list(words.plans),
            selected=words.plans[1],
            combo=True,
            border_color=BORDER,
            border_width=BORDER_WIDTH,
        )
    )
    top -= SIZE * 3.0

    content.draw_text(handle, SIZE, 72.0, top, words.payment)
    page.add_radio(
        hqf_pdf.RadioGroup(
            "Payment",
            [
                (PAYMENT_VALUES[0], 180.0, top - 3.0, SIZE, SIZE),
                (PAYMENT_VALUES[1], 320.0, top - 3.0, SIZE, SIZE),
            ],
            selected=PAYMENT_VALUES[0],
            border_color=BORDER,
            border_width=BORDER_WIDTH,
        )
    )
    content.draw_text(handle, SIZE, 198.0, top, words.payments[0])
    content.draw_text(handle, SIZE, 338.0, top, words.payments[1])
    top -= SIZE * 4.0

    content.draw_text(handle, SIZE, 72.0, top, words.signature)
    page.add_signature(
        hqf_pdf.SignatureField(
            "Signature",
            180.0,
            top - 12.0,
            240.0,
            40.0,
            border_color=BORDER,
            border_width=BORDER_WIDTH,
        )
    )

    page.set_content(content)
    return page


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("form.pdf", language)).stem)

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    document.set_info("Title", words.document_title)

    handle = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))
    document.add_page(form_page(handle, words))

    written = document.write(out)
    print(f"wrote {out} ({written} bytes)")


if __name__ == "__main__":
    main()