Pictures inside table cells

A catalogue whose first column is a picture rather than text: four pictures of very different sizes and proportions, each fitted whole and undistorted into the cell the table gives it and standing against a different corner of it, claiming no width from its column and no height from its row.

Python write_picture_cells.py 260 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
"""Draws a catalogue whose first column is a picture rather than text.

The Python twin of the `write_picture_cells` example in Rust. The four pictures
are wildly different sizes — one is nine hundred pixels across, another sixteen
— and no two share the same proportions, yet the column they sit in is the width
the table gave it and every row is as tall as its own text. A picture asks its
column for no width and its row for no height: it is fitted, whole and
undistorted, into whatever rectangle it ends up with, and where it sits in that
rectangle is the alignment the cell already had.

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

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The margins, and the box the table is fitted into.
MARGIN = 56.0
TOP = 760.0
BOTTOM = 64.0
PAGE_WIDTH = 595.276
TABLE_WIDTH = PAGE_WIDTH - 2 * MARGIN

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

# The height every row of the catalogue takes, whatever its text needs.
ROW_HEIGHT = 54.0

# The pictures the catalogue shows, committed with the tests.
PICTURES = Path(__file__).resolve().parents[2] / "hqf-pdf" / "tests" / "images"


@dataclass(frozen=True)
class Item:
    """One line of the catalogue, save what it is called and what is said about it.

    A picture, a corner and a price are the same in every language; the line reads its
    words from `Words` instead.
    """

    picture: str
    across: hqf_pdf.Align
    down: hqf_pdf.VAlign
    reference: str
    price: str


# The catalogue's lines, in the order they are printed. The pictures are shown against
# each edge of their cells in turn, so that the room a picture does not fill can be seen
# going to one side or the other.
ITEMS = [
    Item(
        "hqf_development.png",
        hqf_pdf.Align.Left,
        hqf_pdf.VAlign.Top,
        "BN-900",
        "148.00",
    ),
    Item(
        "photo.jpg",
        hqf_pdf.Align.Center,
        hqf_pdf.VAlign.Middle,
        "PH-160",
        "24.50",
    ),
    Item(
        "logo.png",
        hqf_pdf.Align.Right,
        hqf_pdf.VAlign.Bottom,
        "SQ-96",
        "9.90",
    ),
    Item(
        "bilevel.png",
        hqf_pdf.Align.Center,
        hqf_pdf.VAlign.Middle,
        "RL-16",
        "3.75",
    ),
]


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

    # The heading of each column, in the order the columns are set.
    headings: tuple[str, str, str, str]
    # What each item is called, in the order the lines are printed. A picture cell
    # carries the same words as the words a reader hears in its place.
    names: tuple[str, str, str, str]
    # What is said about each item.
    notes: tuple[str, str, str, str]


# The catalogue in English.
ENGLISH = Words(
    headings=("Picture", "Item", "Reference", "Price, €"),
    names=("Wide banner", "Photograph", "Square mark", "Two-colour rule"),
    notes=(
        "Nine hundred pixels across and half as tall, drawn against the top "
        "left of its cell.",
        "Four to three, in the middle of its cell — the room it does not fill "
        "is shared either side.",
        "As tall as it is wide, against the bottom right. The height runs out "
        "first, so nothing is left below it.",
        "Sixteen pixels by eight, drawn as large as the cell allows: a picture "
        "is fitted to its cell, not to its own pixels.",
    ),
)

# The catalogue in French.
FRENCH = Words(
    headings=("Image", "Article", "Référence", "Prix, €"),
    names=("Bandeau large", "Photographie", "Marque carrée", "Filet bicolore"),
    notes=(
        "Neuf cents pixels de large et deux fois moins haut, calé en haut "
        "à gauche de sa cellule.",
        "Quatre pour trois, au milieu de sa cellule : la place qu'elle ne "
        "prend pas se partage des deux côtés.",
        "Aussi haute que large, calée en bas à droite. C'est la hauteur qui "
        "manque en premier, donc rien ne reste en dessous.",
        "Seize pixels sur huit, dessiné aussi grand que la cellule le "
        "permet : une image s'ajuste à sa cellule, pas à ses propres pixels.",
    ),
)


# 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 catalogue(
    document: hqf_pdf.Document, font: hqf_pdf.FontHandle, words: Words
) -> hqf_pdf.Table:
    """The catalogue: a heading, then one row per item, the first cell of each a
    picture."""
    # The picture column is a fixed width, the note takes what is left, and the
    # reference and the price are each as wide as their own widest cell.
    columns = hqf_pdf.Columns(
        [
            hqf_pdf.ColumnWidth.points(96.0),
            hqf_pdf.ColumnWidth.fraction(1.0),
            hqf_pdf.ColumnWidth.content(),
            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, words.headings[0], padding=pad, fill=HEAD_FILL
                ),
                hqf_pdf.Cell(
                    font, 9.0, words.headings[1], padding=pad, fill=HEAD_FILL
                ),
                hqf_pdf.Cell(
                    font, 9.0, words.headings[2], padding=pad, fill=HEAD_FILL
                ),
                hqf_pdf.Cell(
                    font,
                    9.0,
                    words.headings[3],
                    align=hqf_pdf.Align.Right,
                    padding=pad,
                    fill=HEAD_FILL,
                ),
            ],
            min_height=20.0,
        )
    )

    for item, name, note in zip(ITEMS, words.names, words.notes):
        image = document.add_image(hqf_pdf.Image.from_path(PICTURES / item.picture))
        table.push(
            hqf_pdf.Row(
                [
                    hqf_pdf.Cell.image(
                        image,
                        alt=name,
                        align=item.across,
                        valign=item.down,
                        padding=pad,
                    ),
                    hqf_pdf.Cell(font, 9.0, f"{name}\n{note}", padding=pad),
                    hqf_pdf.Cell(font, 9.0, item.reference, padding=pad),
                    hqf_pdf.Cell(
                        font,
                        9.0,
                        item.price,
                        align=hqf_pdf.Align.Right,
                        align_on=".",
                        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("picture_cells.pdf", language)).stem
    )

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

    table = catalogue(document, font, words)
    placed = table.fit(MARGIN, TOP, TOP - BOTTOM, 0)

    content = hqf_pdf.Content()
    placed.draw(content)

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

    written = document.write(out)
    print(f"wrote {out}: {written} bytes, {table.row_count} rows")


if __name__ == "__main__":
    main()