Du texte trop long dans une cellule étroite

Du texte trop long dans une cellule de tableau étroite : laissé déborder de la colonne, ou coupé à sa largeur.

Python write_cell_overflow.py 293 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
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
"""Lays a day's meals out in a table whose dish column is narrower than some of the
dishes named in it, twice: above as the names come, below cut to the column.

The Python twin of the `write_cell_overflow` example in Rust.

The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which set
the page is set in. A dish is not only a word here: two of them are single compounds
wider than the column they are put in, which is the whole of what the two tables differ
over, so every language has to name its meals with compounds of its own.

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# When a meal is taken, and what it comes to in kilocalories. What is on the plate is a
# word, and stands in `Words` beside them.
MEALS = [
    ("07:30", 420),
    ("10:00", 210),
    ("12:30", 540),
    ("16:00", 180),
    ("19:30", 610),
    ("21:00", 0),
]

# What the energies are counted in, which is the same wherever the page is read.
UNIT = "kcal"


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

    # The heading over each of the two tables.
    headings: tuple[str, str]
    # What the time column and the dish column are called. The third heading is the unit
    # the energies are counted in.
    columns: tuple[str, str]
    # What is on the plate at each meal of `MEALS`. A compound name is one word to a
    # line breaker, however many words it reads as, and two of these are compounds wider
    # than the column they are put in.
    dishes: tuple[str, str, str, str, str, str]
    # What the boxed-off row at the foot of the table is called.
    total: str
    # The notes under each table.
    notes: tuple[str, str, str]


# The page in English.
ENGLISH = Words(
    headings=(
        "Overflow: a dish wider than its column runs past the edge, over the "
        "kcal beside it.",
        "Break: the same dish is cut after its last glyph that fits, and "
        "carries on the next line.",
    ),
    columns=("Time", "Dish"),
    dishes=(
        "Wholemeal-sourdough-with-almond-butter",
        "Greek yoghurt, walnuts",
        "Cauliflower-and-broccoli-gratin-with-hazelnuts",
        "Apple, oat biscuits",
        "Pan-fried-mackerel-with-buckwheat",
        "Camomile infusion",
    ),
    total="Total",
    notes=(
        "Drink 1.5–2 litres of water over the day, between the meals rather "
        "than with them.",
        "Swap any dish for another of ≈ the same energy.",
        "Weigh the portions dry, not cooked.",
    ),
)

# The page in French.
FRENCH = Words(
    headings=(
        "Débordement : un plat plus large que sa colonne passe le bord et mord "
        "sur les kcal.",
        "Coupure : le même plat est coupé après son dernier signe qui tient, et "
        "reprend en dessous.",
    ),
    columns=("Heure", "Plat"),
    dishes=(
        "Pain-au-levain-complet-et-purée-d'amande",
        "Yaourt grec, noix",
        "Gratin-de-chou-fleur-et-brocolis-aux-noisettes",
        "Pomme, biscuits à l'avoine",
        "Maquereau-poêlé-au-sarrasin",
        "Infusion de camomille",
    ),
    total="Total",
    notes=(
        "Buvez 1.5–2 litres d'eau dans la journée, entre les repas plutôt que "
        "pendant.",
        "Remplacez un plat par un autre qui apporte à peu près autant.",
        "Pesez les portions crues, pas cuites.",
    ),
)

# 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 left edge of both tables, the width the plan is fitted across -- a card, not a
# page -- and the two columns that do not take what is left.
X = 80.0
TABLE_WIDTH = 250.0
TIME_WIDTH = 44.0
ENERGY_WIDTH = 46.0
# How far a note is set from the bullet that introduces it.
BULLET_INDENT = 10.0
# The size everything in the table is set at.
BODY = 8.5


def plan(
    font: hqf_pdf.FontHandle, words: Words, overlong: hqf_pdf.Overlong
) -> hqf_pdf.Table:
    """The plan, laid out with the given answer to a dish too wide for its column."""
    # The dish takes whatever the time and the energy leave it.
    columns = hqf_pdf.Columns(
        [
            hqf_pdf.ColumnWidth.points(TIME_WIDTH),
            hqf_pdf.ColumnWidth.fraction(1.0),
            hqf_pdf.ColumnWidth.points(ENERGY_WIDTH),
        ],
        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(4.0, 3.0)
    shade = hqf_pdf.Rgb.gray(0.88)

    def heading(label: str, align: hqf_pdf.Align = hqf_pdf.Align.Left) -> hqf_pdf.Cell:
        return hqf_pdf.Cell(
            font,
            BODY,
            label,
            padding=pad,
            fill=shade,
            align=align,
            valign=hqf_pdf.VAlign.Middle,
        )

    table.push(
        hqf_pdf.Row(
            [
                heading(words.columns[0]),
                heading(words.columns[1]),
                heading(UNIT, hqf_pdf.Align.Right),
            ],
            min_height=18.0,
        )
    )

    for (time, energy), dish in zip(MEALS, words.dishes):
        table.push(
            hqf_pdf.Row(
                [
                    hqf_pdf.Cell(font, BODY, time, padding=pad),
                    hqf_pdf.Cell(font, BODY, dish, padding=pad, overlong=overlong),
                    hqf_pdf.Cell(
                        font,
                        BODY,
                        str(energy),
                        padding=pad,
                        align=hqf_pdf.Align.Right,
                    ),
                ],
                min_height=16.0,
            )
        )

    # The day's energy, boxed off the grid so that it reads as a total rather than as
    # one more meal. The label carries the box's left edge and the figure its right, so
    # the two cells trace one box between them.
    rule = hqf_pdf.Stroke(0.8)
    box_margin = hqf_pdf.Margin.symmetric(0.0, 2.0)
    total = sum(energy for _, energy in MEALS)
    table.push(
        hqf_pdf.Row(
            [
                hqf_pdf.Cell(
                    font,
                    BODY,
                    words.total,
                    span=2,
                    align=hqf_pdf.Align.Right,
                    padding=pad,
                    margin=box_margin,
                    border=hqf_pdf.Border(top=rule, bottom=rule, left=rule),
                ),
                hqf_pdf.Cell(
                    font,
                    BODY,
                    str(total),
                    align=hqf_pdf.Align.Right,
                    padding=pad,
                    margin=box_margin,
                    border=hqf_pdf.Border(top=rule, bottom=rule, right=rule),
                ),
            ],
            min_height=16.0,
        )
    )

    return table


def draw_notes(
    content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words, top: float
) -> None:
    """Draws the notes under the plan as a bullet list, the first baseline one leading
    below `top`.

    The bullet sits at the left edge and the note is flowed beside it, so a note long
    enough to run on lines up under itself rather than under its bullet.
    """
    flow = hqf_pdf.TextFlow(font, 7.5, leading=10.0)
    bullet = flow.break_lines("•", BULLET_INDENT)
    width = TABLE_WIDTH - BULLET_INDENT

    y = top
    for note in words.notes:
        lines = flow.break_lines(note, width)
        flow.draw(content, bullet, X, y, BULLET_INDENT)
        flow.draw(content, lines, X + BULLET_INDENT, y, width)
        y -= flow.height(lines)


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

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

    content = hqf_pdf.Content()

    for (top, overlong), label in zip(
        (
            (790.0, hqf_pdf.Overlong.Overflow),
            (540.0, hqf_pdf.Overlong.Break),
        ),
        words.headings,
    ):
        heading = hqf_pdf.TextFlow(font, 8.0)
        lines = heading.break_lines(label, 430.0)
        heading.draw(content, lines, X, top, 430.0)

        table_top = top - 30.0
        placed = plan(font, words, overlong).fit(X, table_top, 220.0, 0)
        placed.draw(content)

        # The notes follow the table, which is taller when it cuts.
        draw_notes(content, font, words, table_top - placed.height - 12.0)

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

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


if __name__ == "__main__":
    main()