write_form_cells.py

Die Python-Datei des Beispiels „Eine Seite und ein Stempel in Tabellenzellen“. Eine Prüfseite für ein Paket, das jemand anderes geschickt hat: Jede Zeile zeigt eine seiner Seiten, vollständig aus der Datei genommen, in der sie ankam, neben einem Stempel, der einmal gezeichnet und in jede Zeile gesetzt wird, die ihn erhielt. Beide werden in die Zelle eingepasst, die die Tabelle ihnen lässt, ohne Breite von ihrer Spalte oder Höhe von ihrer Zeile zu beanspruchen.

Python 576 Zeilen

Wofür dieses Beispiel da ist

Ein Lieferant schickt ein Paket — einen Lieferschein, eine Packliste, ein Zertifikat, einen Wiegeschein —, und jemand muss Seite für Seite sagen, was auf jeder gefunden wurde. Die Zusammenfassung ist meist ein Bildschirmfoto jeder Seite oder die abgetippte Seite. Beides heißt, dass Zusammenfassung und Papier voneinander abweichen können, und an dem Tag, an dem sie es tun, glaubt man der Zusammenfassung.

Was dieses Beispiel zeigt

  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
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
"""Writes a checking sheet for a pack somebody else sent: one row per page of it,
each showing that page and the stamp it was given.

The Python twin of the `write_form_cells` example in Rust. Two kinds of form stand
in the cells, and they behave alike. The first column holds a page taken whole out
of another document; the second holds a stamp drawn here, once, and placed in every
row that was given it. Neither asks its column for a width nor its row for a height:
each is fitted whole and undistorted into the rectangle the table leaves it, which
is the bargain a picture cell makes. Both are given a rectangle taller than their
own proportions need, so the width runs out first and each stands in the middle of
the height it does not fill.

A page brought in belongs to the sheets it is put on rather than to the document, so
every page the table lands on is handed the pages it shows.

The pack is written by the example a moment before it is read, so this runs on any
machine with nothing to fetch.

What the sheet says is held in `Words`, once per language, and `HQF_PDF_LANG` picks
which set is printed. What a page is called is also what a reader hears in its place:
the alternative text of a form cell reads the same field as the line beside it, so
the two cannot disagree.

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# A4's width, which every page here is drawn across.
PAGE_WIDTH = 595.276

# The margin the checking sheet and the pack both keep.
MARGIN = 56.0

# The width left between the margins, which the table is fitted across.
TABLE_WIDTH = PAGE_WIDTH - 2 * MARGIN

# The top of the table on the page, and the lowest it may reach.
TOP = 736.0
BOTTOM = 64.0

# How many pages the pack holds, which is how many rows the table takes.
SHEETS = 4

# How many stamps are drawn. A row names one of them; two rows may name the same one.
MARKS = 3

# The width of the column the pages are shown in, and of the column the stamps stand in.
SHEET_COLUMN = 96.0
MARK_COLUMN = 76.0

# The height every row of the checking sheet takes.
ROW_HEIGHT = 140.0

# The colour the heading is painted on.
HEAD_FILL = hqf_pdf.Rgb.gray(0.86)

# The ink the sheet is written in, and the grey of its second lines.
INK = hqf_pdf.Rgb(0.1, 0.1, 0.12)
MUTED = hqf_pdf.Rgb(0.42, 0.42, 0.45)

# The colour the sender's stationery is printed in.
SENDER_INK = hqf_pdf.Rgb(0.11, 0.33, 0.55)

# The ink of each stamp, in the order the stamps are drawn.
MARK_INKS = [
    hqf_pdf.Rgb(0.09, 0.42, 0.24),
    hqf_pdf.Rgb(0.65, 0.38, 0.04),
    hqf_pdf.Rgb(0.36, 0.36, 0.42),
]

# The size of a stamp, in points, and the size its word is set at.
STAMP = (62.0, 24.0)
STAMP_TEXT = 9.0

# Who sent the pack, and where they are. A firm writes its name one way, whatever
# language reads it.
SENDER_NAME = "Meridian Optics SAS"
SENDER_TOWN = "18 avenue des Peupliers, 69100 Villeurbanne"

# What each page of the pack is filed as. A reference is not language.
REFERENCES = ["DN-4471", "PL-4471", "CC-4471", "WB-4471"]

# The stamp each page was given, in the order the rows are printed. The first page and
# the third carry the same one, which is drawn once.
MARK_OF = [0, 1, 0, 2]

# How many lines of the sender's own writing each page of the pack carries.
LINES_OF = [24, 19, 26, 15]

# The share of the measure each line of the sender's writing runs to, taken in turn: a
# page of writing is not a block, and the short line ends a paragraph.
LINE_SHARES = [1.0, 0.94, 0.98, 0.9, 0.96, 0.52]


@dataclass(frozen=True)
class Words:
    """Every word the checking sheet prints, in one language."""

    # The line at the head of the sheet.
    title: str
    # The two lines under it, saying what each row shows.
    intro: tuple[str, str]
    # The heading of each column, in the order the columns are set.
    headings: tuple[str, str, str, str]
    # What each page of the pack is called, in the order they arrived. A form cell
    # carries the same words as the words a reader hears in its place.
    sheets: tuple[str, str, str, str]
    # What was found on each of them.
    notes: tuple[str, str, str, str]
    # What each stamp says, which is also what it is called.
    marks: tuple[str, str, str]
    # The line under the table, saying what a stamp placed twice costs.
    foot: str


# The checking sheet in English.
ENGLISH = Words(
    title="What arrived, page by page",
    intro=(
        "Each row shows a page of the pack as it arrived, and the stamp that page "
        "was given.",
        "Both are forms, and both are fitted to the cell the table leaves them, "
        "whole and undistorted.",
    ),
    headings=("Sheet", "Stamp", "What was found", "Reference"),
    sheets=(
        "Delivery note",
        "Packing list",
        "Certificate of conformity",
        "Weighbridge ticket",
    ),
    notes=(
        "The whole page, at a sixth of its size: nothing was redrawn, and nothing "
        "was left out.",
        "Two lines disagree with the delivery note, so the sheet waits on the "
        "sender.",
        "Signed and dated, and filed with the batch it covers.",
        "The weight reads, the time does not, so the sheet is held until a clean "
        "copy arrives.",
    ),
    marks=("Accepted", "Query", "On hold"),
    foot=(
        "The first row and the third carry one stamp: a drawing costs its "
        "operators once, wherever it lands."
    ),
)

# The checking sheet in French.
FRENCH = Words(
    title="Documents reçus, feuille par feuille",
    intro=(
        ("Chaque ligne montre une feuille du lot telle qu’elle est arrivée, et le "
         "tampon qu’on y a apposé."),
        "Les deux sont des formulaires, ajustés à la cellule que le tableau leur "
        "laisse, entiers et sans déformation.",
    ),
    headings=("Feuille", "Tampon", "Ce qui a été relevé", "Référence"),
    sheets=(
        "Bon de livraison",
        "Liste de colisage",
        "Certificat de conformité",
        "Ticket de pesée",
    ),
    notes=(
        ("La page entière, au sixième de sa taille : rien n’a été refait, rien n’a été"
         " laissé de côté."),
        ("Deux lignes ne concordent pas avec le bon de livraison : la feuille attend "
         "la réponse de l’expéditeur."),
        "Signé et daté, classé avec le lot qu’il couvre.",
        ("Le poids se lit, l’heure non : la feuille est retenue jusqu’à l’arrivée "
         "d’une copie nette."),
    ),
    marks=("Accepté", "À vérifier", "En attente"),
    foot=(
        (("Le même tampon figure aux lignes un et trois : ses instructions ne sont "
          "écrites qu’une seule fois."))
    ),
)

# The checking sheet in Dutch.
DUTCH = Words(
    title="Wat er binnenkwam, pagina voor pagina",
    intro=(
        "Elke rij toont een pagina van het pakket zoals ze binnenkwam, en de stempel "
        "die die pagina kreeg.",
        "Beide zijn formulieren, en beide passen in de cel die de tabel ze geeft, "
        "heel en onvervormd.",
    ),
    headings=("Blad", "Stempel", "Wat er gevonden is", "Referentie"),
    sheets=(
        "Pakbon",
        "Paklijst",
        "Conformiteitsverklaring",
        "Weegbon",
    ),
    notes=(
        "De hele pagina, op een zesde van haar grootte: niets is opnieuw getekend en "
        "niets weggelaten.",
        "Twee regels kloppen niet met de pakbon, dus wacht het blad op de afzender.",
        "Getekend en gedateerd, en opgeborgen bij de partij die het dekt.",
        "Het gewicht is leesbaar, de tijd niet, dus wordt het blad aangehouden tot er "
        "een schone kopie komt.",
    ),
    marks=("Aanvaard", "Navraag", "Uitgesteld"),
    foot=(
        "De eerste en de derde rij hebben dezelfde stempel: de operatoren van een "
        "tekening staan er één keer in."
    ),
)

# The checking sheet in Portuguese.
PORTUGUESE = Words(
    title="O que chegou, página a página",
    intro=(
        "Cada linha mostra uma página do lote tal como chegou, e o carimbo que essa "
        "página recebeu.",
        "Ambos são formulários, e ambos se ajustam à célula que a tabela lhes deixa, "
        "inteiros e sem deformação.",
    ),
    headings=("Folha", "Carimbo", "O que se encontrou", "Referência"),
    sheets=(
        "Guia de remessa",
        "Lista de volumes",
        "Certificado de conformidade",
        "Talão de pesagem",
    ),
    notes=(
        "A página inteira, a um sexto do tamanho: nada foi redesenhado e nada ficou "
        "de fora.",
        "Duas linhas não batem com a guia de remessa, por isso a folha espera pelo "
        "remetente.",
        "Assinado e datado, e arquivado com o lote que abrange.",
        "O peso lê-se, a hora não, por isso a folha fica retida até chegar uma cópia "
        "limpa.",
    ),
    marks=("Aceite", "Dúvida", "Retido"),
    foot=(
        "A primeira linha e a terceira levam o mesmo carimbo: os operadores de um "
        "desenho contam uma só vez."
    ),
)

# The checking sheet in Italian.
ITALIAN = Words(
    title="Ciò che è arrivato, pagina per pagina",
    intro=(
        "Ogni riga mostra una pagina del plico com’è arrivata, e il timbro che ha "
        "ricevuto.",
        "Entrambi sono moduli e si adattano alla cella che la tabella lascia loro, "
        "interi e senza deformazioni.",
    ),
    headings=("Foglio", "Timbro", "Ciò che si è trovato", "Riferimento"),
    sheets=(
        "Documento di trasporto",
        "Distinta colli",
        "Certificato di conformità",
        "Scontrino di pesatura",
    ),
    notes=(
        "La pagina intera, a un sesto della sua dimensione: niente è stato "
        "ridisegnato e niente omesso.",
        "Due righe non corrispondono al documento di trasporto, così il foglio "
        "attende il mittente.",
        "Firmato e datato, e archiviato con il lotto che copre.",
        "Il peso si legge, l’ora no, così il foglio resta sospeso finché non arriva "
        "una copia pulita.",
    ),
    marks=("Accettato", "Da chiarire", "In sospeso"),
    foot=(
        "La prima riga e la terza portano un solo timbro: un disegno costa i suoi "
        "operatori una volta, ovunque cada."
    ),
)

# The checking sheet in Spanish.
SPANISH = Words(
    title="Lo que llegó, página a página",
    intro=(
        "Cada fila muestra una página del lote tal como llegó, y el sello que recibió.",
        "Ambos son formularios, y ambos encajan en la celda que les deja la tabla, "
        "enteros y sin deformar.",
    ),
    headings=("Hoja", "Sello", "Lo que se encontró", "Referencia"),
    sheets=(
        "Albarán",
        "Lista de bultos",
        "Certificado de conformidad",
        "Tique de báscula",
    ),
    notes=(
        "La página entera, a una sexta parte de su tamaño: nada se redibujó y nada se "
        "omitió.",
        "Dos líneas no cuadran con el albarán, así que la hoja espera al remitente.",
        "Firmado y fechado, y archivado con el lote que cubre.",
        "El peso se lee, la hora no, así que la hoja se retiene hasta que llegue una "
        "copia limpia.",
    ),
    marks=("Aceptado", "Consulta", "Retenido"),
    foot=(
        "La primera fila y la tercera llevan un sello: un dibujo cuesta sus "
        "operadores una vez, caiga donde caiga."
    ),
)

# The checking sheet in German.
GERMAN = Words(
    title="Was eingegangen ist, Seite für Seite",
    intro=(
        "Jede Zeile zeigt eine Seite der Sendung, wie sie ankam, und ihren Stempel.",
        "Beides sind Formulare, ganz und unverzerrt in die Zelle eingepasst, die die "
        "Tabelle lässt.",
    ),
    headings=("Blatt", "Stempel", "Befund", "Referenz"),
    sheets=(
        "Lieferschein",
        "Packliste",
        "Konformitätsbescheinigung",
        "Wiegeschein",
    ),
    notes=(
        "Die ganze Seite, auf ein Sechstel verkleinert: nichts neu gezeichnet, nichts "
        "weggelassen.",
        "Zwei Zeilen widersprechen dem Lieferschein, also wartet das Blatt auf den "
        "Absender.",
        "Unterschrieben und datiert und bei der Charge abgelegt, zu der es gehört.",
        "Das Gewicht ist lesbar, die Uhrzeit nicht, also wird das Blatt bis zu einer "
        "sauberen Kopie zurückgehalten.",
    ),
    marks=("Geprüft", "Rückfrage", "Wartet"),
    foot=(
        "Zeilen eins und drei tragen denselben Stempel: Eine Zeichnung steht nur "
        "einmal in der Datei."
    ),
)


# 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 text(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    size: float,
    x: float,
    y: float,
    ink: hqf_pdf.Rgb,
    line: str,
) -> None:
    """Draws one line of text in `ink`, with its left edge at `x` and its baseline
    at `y`."""
    content.set_fill(ink)
    content.draw_text(font, size, x, y, line)


def wash(ink: hqf_pdf.Rgb) -> hqf_pdf.Rgb:
    """The pale wash a stamp is printed on, which is its own ink most of the way to
    white."""
    return hqf_pdf.Rgb(
        ink.r * 0.16 + 0.84,
        ink.g * 0.16 + 0.84,
        ink.b * 0.16 + 0.84,
    )


def stamp(font: hqf_pdf.FontHandle, word: str, ink: hqf_pdf.Rgb) -> hqf_pdf.Drawing:
    """One stamp: a washed box, a rule around it, and the word it carries in the
    middle."""
    width, height = STAMP
    content = hqf_pdf.Content()

    content.set_fill(wash(ink))
    content.rect(0.0, 0.0, width, height)
    content.fill()

    content.set_stroke(ink)
    content.set_line_width(1.2)
    content.rect(0.6, 0.6, width - 1.2, height - 1.2)
    content.stroke()

    measured = font.measure(word, STAMP_TEXT)
    text(content, font, STAMP_TEXT, (width - measured) / 2.0, 8.6, ink, word)

    return hqf_pdf.Drawing(content, hqf_pdf.Rect(0.0, 0.0, width, height))


def sheet_page(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    title: str,
    reference: str,
    lines: int,
) -> None:
    """One page of the pack: the sender's letterhead, what the page is, its
    reference, and the sender's own writing under it.

    The writing is a block of rules rather than words. What this example needs of the
    pack is that its pages arrive whole, at whatever they were made.
    """
    content.set_fill(wash(SENDER_INK))
    content.rect(0.0, 742.0, PAGE_WIDTH, 100.0)
    content.fill()
    content.set_fill(SENDER_INK)
    content.rect(0.0, 738.0, PAGE_WIDTH, 4.0)
    content.fill()

    text(content, font, 20.0, MARGIN, 788.0, INK, SENDER_NAME)
    text(content, font, 9.0, MARGIN, 766.0, MUTED, SENDER_TOWN)

    text(content, font, 26.0, MARGIN, 690.0, SENDER_INK, title)
    text(content, font, 11.0, MARGIN, 666.0, MUTED, reference)

    content.set_stroke(hqf_pdf.Rgb.gray(0.74))
    content.set_line_width(3.4)
    y = 630.0
    for line in range(lines):
        share = LINE_SHARES[line % len(LINE_SHARES)]
        content.move_to(MARGIN, y)
        content.line_to(share * TABLE_WIDTH + MARGIN, y)
        content.stroke()
        y -= 22.0

    text(content, font, 8.0, MARGIN, 60.0, MUTED, SENDER_NAME)


def a_pack(font_path: Path, words: Words) -> bytes:
    """The pack the checking sheet reads: one page per sheet, as its sender made
    them."""
    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    font = document.add_font(hqf_pdf.Font.from_path(font_path))

    for index, title in enumerate(words.sheets):
        content = hqf_pdf.Content()
        sheet_page(content, font, title, REFERENCES[index], LINES_OF[index])

        page = hqf_pdf.Page.a4()
        page.set_content(content)
        document.add_page(page)

    return document.to_bytes()


def checking_sheet(
    font: hqf_pdf.FontHandle,
    words: Words,
    sheets: list[hqf_pdf.ImportedPage],
    stamps: list[hqf_pdf.DrawingHandle],
) -> hqf_pdf.Table:
    """The checking sheet: a heading, then one row per page of the pack, holding that
    page and its stamp."""
    # The two form columns are fixed widths, the note takes what is left, and the
    # reference is as wide as its own widest cell.
    columns = hqf_pdf.Columns(
        [
            hqf_pdf.ColumnWidth.points(SHEET_COLUMN),
            hqf_pdf.ColumnWidth.points(MARK_COLUMN),
            hqf_pdf.ColumnWidth.fraction(1.0),
            hqf_pdf.ColumnWidth.content(),
        ],
        TABLE_WIDTH,
    )

    table = hqf_pdf.Table(columns)
    table.header(1)
    table.rule(hqf_pdf.Rule.frame(), hqf_pdf.Stroke(0.8))
    hairline = hqf_pdf.Stroke(0.25, color=hqf_pdf.Rgb.gray(0.75))
    table.rule(hqf_pdf.Rule.horizontal_other(), hairline)
    table.rule(hqf_pdf.Rule.vertical_other(), hairline)
    table.rule(hqf_pdf.Rule.horizontal(1), hqf_pdf.Stroke(0.8))

    pad = hqf_pdf.Padding.symmetric(6.0, 5.0)

    table.push(
        hqf_pdf.Row(
            cells=[
                hqf_pdf.Cell(font, 9.0, label, padding=pad, fill=HEAD_FILL)
                for label in words.headings
            ],
            min_height=20.0,
        )
    )

    for index, name in enumerate(words.sheets):
        mark = MARK_OF[index]
        table.push(
            hqf_pdf.Row(
                cells=[
                    hqf_pdf.Cell.imported_page(
                        sheets[index],
                        alt=name,
                        valign=hqf_pdf.VAlign.Middle,
                        padding=pad,
                    ),
                    hqf_pdf.Cell.drawing(
                        stamps[mark],
                        alt=words.marks[mark],
                        valign=hqf_pdf.VAlign.Middle,
                        padding=pad,
                    ),
                    hqf_pdf.Cell(
                        font, 9.0, f"{name}\n{words.notes[index]}", padding=pad
                    ),
                    hqf_pdf.Cell(font, 9.0, REFERENCES[index], padding=pad),
                ],
                min_height=ROW_HEIGHT,
            )
        )

    return table


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_cells.pdf", language)).stem)
    font_path = _out.font_path()

    pack = hqf_pdf.Reader(a_pack(font_path, words))

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    font = document.add_font(hqf_pdf.Font.from_path(font_path))

    # Each page of the pack comes in as a form, and each stamp is drawn once.
    sheets = [document.import_page(pack, index) for index in range(SHEETS)]
    stamps = [
        document.add_drawing(stamp(font, word, ink))
        for word, ink in zip(words.marks, MARK_INKS)
    ]

    content = hqf_pdf.Content()
    text(content, font, 15.0, MARGIN, 786.0, INK, words.title)
    y = 768.0
    for line in words.intro:
        text(content, font, 9.0, MARGIN, y, MUTED, line)
        y -= 12.0

    table = checking_sheet(font, words, sheets, stamps)
    placed = table.fit(MARGIN, TOP, TOP - BOTTOM, 0)
    placed.draw(content)

    text(content, font, 9.0, MARGIN, placed.bottom - 18.0, MUTED, words.foot)

    page = hqf_pdf.Page.a4()
    page.set_content(content)
    # A page may only draw what its resources name, and a page brought in from elsewhere
    # belongs to the sheets it is put on.
    for sheet in sheets:
        sheet.add_to(page)
    document.add_page(page)

    written = document.write(out)
    print(
        f"wrote {out}: {written} bytes, {len(sheets)} pages brought in, "
        f"{MARKS} stamps drawn"
    )


if __name__ == "__main__":
    main()