A picture fitted into a box

One picture fitted into a box of one size eight ways: brought to four different sizes, stood against an edge, turned by a quarter and by an angle of its own, and mirrored — every panel drawn from the placement the rule worked out.

Python write_fitted.py 221 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
"""Fits one picture into boxes of one size eight ways, and labels each.

The Python twin of the `write_fitted` example in Rust. A `FitBox` says how a picture
is sized to a box, where it sits in it, how far the box is inset first, and how far
the picture is turned and mirrored on its way in. Every panel hands the same picture
and the same box to a different policy, and draws what comes back the same way:
concatenate the matrix the policy gives, then draw the picture at the origin at its
natural size. Nothing measures anything by hand.

The panel that covers its box is the one case that overflows on purpose, so it is
clipped to the box it covers; every other panel stays inside.

The title and the eight captions are held in `Words`, once per language, and
`HQF_PDF_LANG` picks which set is drawn. Each caption opens with the name the library
gives the policy it describes, which is the same in every language.

Usage: python examples/write_fitted.py [out.pdf]
       HQF_PDF_LANG=fr python examples/write_fitted.py
"""

from __future__ import annotations

import math
from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The picture every panel fits: the JPEG committed for the tests.
IMAGES = Path(__file__).resolve().parents[2] / "hqf-pdf" / "tests" / "images"
PHOTO = IMAGES / "photo.jpg"

PAGE_WIDTH = 595.276
MARGIN = 56.0
# The gap between the two columns of panels.
GAP = 24.0
# The box every panel fits its picture into.
BOX_WIDTH = (PAGE_WIDTH - 2.0 * MARGIN - GAP) / 2.0
BOX_HEIGHT = 130.0
# The top of the first row of boxes, and the drop from one row to the next.
FIRST_ROW = 750.0
ROW_PITCH = 176.0
# How far under a box its caption's baseline sits.
CAPTION_DROP = 14.0


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

    Each caption opens with the name the library gives its policy — ``contain``,
    ``cover``, ``stretch`` — which is not language.
    """

    # The line at the head of the page.
    title: str
    # What is written under each box, in the order the panels are laid out.
    captions: tuple[str, str, str, str, str, str, str, str]


# The page in English.
ENGLISH = Words(
    title="One picture, one box, eight fitting policies",
    captions=(
        "contain: as large as fits, its shape kept",
        "cover: no gap left, the overflow clipped",
        "stretch: the box filled, its shape lost",
        "contain, top left, inset 12 points",
        "a quarter turn anticlockwise",
        "a quarter turn clockwise",
        "turned 20 degrees, still inside its box",
        "mirrored left to right",
    ),
)

# The page in French.
FRENCH = Words(
    title="Une image, une boîte, huit façons de l'ajuster",
    captions=(
        "contain : la plus grande qui tient, sans déformer",
        "cover : aucun vide laissé, le débordement rogné",
        "stretch : la boîte remplie, les proportions perdues",
        "contain, en haut à gauche, 12 points de retrait",
        "un quart de tour vers la gauche",
        "un quart de tour vers la droite",
        "tournée de 20 degrés, toujours dans sa boîte",
        "retournée de gauche à droite",
    ),
)


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


@dataclass(frozen=True)
class Panel:
    """What a panel says it does, the policy that does it, and whether what it
    draws has to be held to the box."""

    caption: str
    fitted: hqf_pdf.FitBox
    clipped: bool = False


def panels(words: Words) -> list[Panel]:
    """The eight policies, in the order they are laid out: the five ways of sizing
    first, then the turns and the mirror, each under the caption `words` gives it."""
    fitted = [
        (hqf_pdf.FitBox(hqf_pdf.FitMode.Contain), False),
        (hqf_pdf.FitBox(hqf_pdf.FitMode.Cover), True),
        (hqf_pdf.FitBox(hqf_pdf.FitMode.Stretch), False),
        (
            hqf_pdf.FitBox(
                hqf_pdf.FitMode.Contain,
                anchor=hqf_pdf.Anchor.top_left(),
                margin=12.0,
            ),
            False,
        ),
        (hqf_pdf.FitBox(hqf_pdf.FitMode.Contain, turn=hqf_pdf.Turn.left()), False),
        (hqf_pdf.FitBox(hqf_pdf.FitMode.Contain, turn=hqf_pdf.Turn.right()), False),
        (
            hqf_pdf.FitBox(
                hqf_pdf.FitMode.Contain,
                turn=hqf_pdf.Turn.angle(math.radians(20.0)),
            ),
            False,
        ),
        (
            hqf_pdf.FitBox(hqf_pdf.FitMode.Contain, mirror=hqf_pdf.Mirror.LeftToRight),
            False,
        ),
    ]
    return [
        Panel(caption, box, clipped=clipped)
        for (box, clipped), caption in zip(fitted, words.captions)
    ]


def panel_box(index: int) -> tuple[float, float, float, float]:
    """The box the panel at `index` fits into: two to a row, filled left to right."""
    column = index % 2
    row = index // 2
    x = MARGIN + column * (BOX_WIDTH + GAP)
    top = FIRST_ROW - row * ROW_PITCH
    return (x, top - BOX_HEIGHT, BOX_WIDTH, BOX_HEIGHT)


def draw_panel(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    image: hqf_pdf.ImageHandle,
    panel: Panel,
    box: tuple[float, float, float, float],
) -> None:
    """Draws one panel: its box's outline, the picture fitted into it, and its
    caption underneath."""
    x, y, width, height = box

    content.save_state()
    content.set_stroke(hqf_pdf.Rgb.gray(0.75))
    content.set_line_width(0.5)
    content.rect(x, y, width, height)
    content.stroke()
    content.restore_state()

    size = image.size
    content.save_state()
    if panel.clipped:
        content.rect(x, y, width, height)
        content.clip()
        content.end_path()
    content.transform(panel.fitted.matrix(size, box))
    content.draw_image(image, 0.0, 0.0, size[0], size[1])
    content.restore_state()

    content.save_state()
    content.set_fill(hqf_pdf.Rgb.gray(0.35))
    content.draw_text(font, 8.5, x, y - CAPTION_DROP, panel.caption)
    content.restore_state()


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

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    font = document.add_font(hqf_pdf.Font.from_path(_out.DEFAULT_FONT))
    image = document.add_image(hqf_pdf.Image.from_path(PHOTO))

    content = hqf_pdf.Content()
    content.draw_text(font, 16.0, MARGIN, 790.0, words.title)

    drawn = panels(words)
    for index, panel in enumerate(drawn):
        draw_panel(content, font, image, panel, panel_box(index))

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

    written = document.write(out)
    print(
        f"wrote {out}: {written} bytes, {len(drawn)} panels of one "
        f"{image.width}x{image.height} picture"
    )


if __name__ == "__main__":
    main()