A page and a stamp inside table cells

A checking sheet for a pack somebody else sent: every row holds one of its pages, taken whole out of the file it arrived in, beside a stamp drawn once and placed in every row that was given it. Both are fitted to the cell the table leaves them, claiming no width from their column and no height from their row.

This page is the whole program, for whoever writes your software. There is nothing here to read otherwise. Go back to the document it writes.

Python write_form_cells.py 420 lines
  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
"""Draws 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="Ce qui est arrivé, feuille par feuille",
    intro=(
        "Chaque ligne montre une feuille du lot telle qu'elle est arrivée, et le "
        "tampon qu'elle a reçu.",
        "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é redessiné, rien "
        "n'a été laissé de côté.",
        "Deux lignes ne concordent pas avec le bon de livraison, la feuille attend "
        "donc 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 est en première et en troisième ligne : un dessin ne "
        "coûte ses instructions qu'une fois."
    ),
)


# 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 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, 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(
            [
                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(
                [
                    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()