write_clipping_text.py

Le fichier Python de l’exemple « Lettres qui détourent ce qui les suit ». Un mot peint de quatre façons, et chaque fois les rayures posées après lui ne se voient que dans ses lettres : c’est ainsi qu’on coule un motif ou une photographie dans un titre.

Python 297 lignes

À quoi sert cet exemple

Un titre rempli d’une photographie, un mot traversé par une texture, un tampon dont les lettres laissent voir le papier : c’est chaque fois le même procédé. Les lettres d’un mot sont posées en premier, et tout ce qui est dessiné après elles ne se voit qu’à l’intérieur de leurs contours. Le mot n’est pas une image découpée à la main ; il reste du texte, composé dans sa police, qu’on peut encore rechercher et copier.

Ce que montre cet exemple

  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
"""Writes a page of one word painted the four ways a text can clip what follows it.

The Python twin of the `write_clipping_text` example in Rust: the same page, through
the binding rather than through the library directly.

A text set in a clipping rendering mode adds the outlines of its letters to the
clipping path, and whatever is drawn after it, until the graphics state is restored,
shows only inside those letters. The word is filled, outlined, both, or not painted at
all, and each time stripes are laid across it after it is set: they show only where its
letters are.

Every word is held in `Words`, once per language, and `HQF_PDF_LANG` picks which set is
set.

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# Where the page's text begins, in points from the left edge of the page.
LEFT = 56.0
# How wide it runs.
WIDTH = 483.0
# The size the heading is set in.
TITLE_SIZE = 22.0
# The baseline of the heading.
TITLE_TOP = 800.0
# The size the line under the heading and the name of each way are set in.
NOTE_SIZE = 10.5
# The baseline of the line under the heading.
STANDFIRST_TOP = 776.0
# The gutter between the two words of a row.
GUTTER = 24.0
# The width of the pen that outlines the letters.
HOLLOW_PEN = 0.9

# The four ways a text clips what follows it, in the order the page sets them.
MODES = (
    hqf_pdf.TextRendering.fill_and_clip(),
    hqf_pdf.TextRendering.stroke_and_clip(),
    hqf_pdf.TextRendering.fill_stroke_and_clip(),
    hqf_pdf.TextRendering.clip(),
)

# Where the name of the first row's ways sits, and the drop from one row to the next.
MODE_TOP = 740.0
MODE_STEP = 150.0

# The size the word is painted at.
MODE_SIZE = 54.0

# How far under its name a painted word's baseline sits.
MODE_DROP = 70.0

# How far under the baseline the first stripe starts, how far apart the stripes are, how
# thick each is, and how many there are.
STRIPE_DROP = 14.0
STRIPE_STEP = 4.0
STRIPE = 2.0
STRIPES = 16


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

    # What stands across the head of the page, and what the file says of itself.
    title: str
    # The line under the heading, which says what the page shows.
    standfirst: str
    # The word the page paints each way.
    painted: str
    # What each way is called, in the order `MODES` lists them.
    modes: tuple[str, str, str, str]


# The page in English.
ENGLISH = Words(
    title="Letters that clip what follows them",
    standfirst=(
        "Each word is painted, then the stripes laid after it show only inside its "
        "letters."
    ),
    painted="Ink",
    modes=(
        "filled, then clipping the stripes",
        "outlined, then clipping the stripes",
        "filled, outlined, then clipping",
        "clipping alone: only the stripes show",
    ),
)

# The page in French.
FRENCH = Words(
    title="Lettres qui détourent ce qui les suit",
    standfirst=(
        "Chaque mot est peint, puis les rayures posées après lui ne se voient que dans "
        "ses lettres."
    ),
    painted="Encre",
    modes=(
        "rempli, puis détoure les rayures",
        "contouré, puis détoure les rayures",
        "rempli, contouré, puis détoure",
        "invisible : seules les rayures se voient",
    ),
)

# The page in Dutch.
DUTCH = Words(
    title="Letters die uitsnijden wat erna komt",
    standfirst=(
        "Elk woord wordt ingekleurd; de strepen daarna zijn alleen binnen de letters "
        "te zien."
    ),
    painted="Inkt",
    modes=(
        "gevuld, dan de strepen uitsnijden",
        "omlijnd, dan de strepen uitsnijden",
        "gevuld, omlijnd, dan uitsnijden",
        "alleen uitknippen: je ziet enkel de strepen",
    ),
)

# The page in Portuguese.
PORTUGUESE = Words(
    title="Letras que recortam o que as segue",
    standfirst=(
        "Cada palavra é pintada, e as riscas postas depois só se veem dentro das suas "
        "letras."
    ),
    painted="Tinta",
    modes=(
        "preenchida, depois recorta as riscas",
        "contornada, depois recorta as riscas",
        "preenchida, contornada, depois recorta",
        "só recorte: só se veem as riscas",
    ),
)

# The page in Italian.
ITALIAN = Words(
    title="Lettere che ritagliano ciò che segue",
    standfirst=(
        "Ogni parola è dipinta, poi le strisce posate dopo si vedono solo dentro le "
        "sue lettere."
    ),
    painted="Colore",
    modes=(
        "pieno, poi ritaglia le strisce",
        "contornato, poi ritaglia le strisce",
        "pieno, contornato, poi ritaglia",
        "solo ritaglio: si vedono solo le strisce",
    ),
)

# The page in Spanish.
SPANISH = Words(
    title="Letras que recortan lo que les sigue",
    standfirst=(
        "Cada palabra se pinta y las rayas puestas después solo se ven dentro de sus "
        "letras."
    ),
    painted="Tinta",
    modes=(
        "rellena, luego recorta las rayas",
        "contorneada, luego recorta las rayas",
        "rellena, contorneada, luego recorta",
        "solo recorte: solo se ven las rayas",
    ),
)

# The page in German.
GERMAN = Words(
    title="Buchstaben als Schnittmaske",
    standfirst=(
        "Jedes Wort wird gemalt, dann zeigen sich die folgenden Streifen nur in "
        "seinen Buchstaben."
    ),
    painted="Tinte",
    modes=(
        "gefüllt, dann die Streifen beschneidend",
        "umrandet, dann die Streifen beschneidend",
        "gefüllt, umrandet, dann beschneidend",
        "nur beschneidend: nur Streifen",
    ),
)

# Every language the page is written in.
WORDS = {_language.ENGLISH: ENGLISH, _language.FRENCH: FRENCH, _language.GERMAN: GERMAN, _language.SPANISH: SPANISH, _language.ITALIAN: ITALIAN, _language.PORTUGUESE: PORTUGUESE, _language.DUTCH: DUTCH}


def word_ink() -> hqf_pdf.Rgb:
    """The colour the word is filled in."""
    return hqf_pdf.Rgb(0.72, 0.16, 0.16)


def stripe_ink() -> hqf_pdf.Rgb:
    """The colour of the stripes a clipping word lets through."""
    return hqf_pdf.Rgb(0.95, 0.55, 0.1)


def line_at(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    text: str,
    size: float,
    at: tuple[float, float],
) -> None:
    """Sets `text` on one line from `x`, its baseline at `y`."""
    content.begin_text()
    content.text_position(at[0], at[1])
    content.set_font(font, size)
    content.show_runs(font.runs(text), size)
    content.end_text()


def clipped(content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words) -> None:
    """Draws the page: the heading, the line under it, and the word painted each of the
    four ways, two to a row, each under what the way is called and followed by stripes
    across it, held to its letters."""
    line_at(content, font, words.title, TITLE_SIZE, (LEFT, TITLE_TOP))
    content.save_state()
    content.set_fill(hqf_pdf.Rgb.gray(0.35))
    line_at(content, font, words.standfirst, NOTE_SIZE, (LEFT, STANDFIRST_TOP))
    content.restore_state()

    half = WIDTH / 2.0
    top = MODE_TOP
    for index, (mode, label) in enumerate(zip(MODES, words.modes)):
        x = LEFT if index % 2 == 0 else LEFT + half
        content.save_state()
        content.set_fill(hqf_pdf.Rgb.gray(0.35))
        line_at(content, font, label, NOTE_SIZE, (x, top))
        content.restore_state()

        # The clipping path the letters add holds until the state is restored, so the
        # stripes are drawn inside the same saved state.
        baseline = top - MODE_DROP
        content.save_state()
        content.set_fill(word_ink())
        content.set_stroke(hqf_pdf.Rgb.gray(0.1))
        content.set_line_width(HOLLOW_PEN)
        content.begin_text()
        content.text_position(x, baseline)
        content.set_font(font, MODE_SIZE)
        content.set_text_rendering(mode)
        content.show_runs(font.runs(words.painted), MODE_SIZE)
        content.end_text()
        content.set_fill(stripe_ink())
        y = baseline - STRIPE_DROP
        for __ in range(STRIPES):
            content.rect(x, y, half - GUTTER, STRIPE)
            y += STRIPE_STEP
        content.fill()
        content.restore_state()
        if index % 2 == 1:
            top -= MODE_STEP


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

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    document.set_info("Title", words.title)
    font = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))

    content = hqf_pdf.Content()
    clipped(content, font, words)
    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()