Les mêmes nombres, à travers des espaces qui disent ce qu'ils veulent dire

Six couleurs et un dégradé de gris peints trois fois avec exactement les mêmes nombres : d'abord laissés à l'appareil, puis dits de deux façons qui précisent contre quel blanc ils sont mesurés, quelle courbe leur est appliquée et jusqu'où vont les couleurs de base. Tout cela tient en une poignée de nombres, sans aucun profil à ranger dans le fichier.

Python write_calibrated_color.py 290 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
"""The same numbers, painted through spaces that say what they mean.

The Python twin of the `write_calibrated_color` example in Rust. A calibrated
space says what white the numbers are measured against, what exponent they are
raised to and — for colour — what the three primaries actually reach, so the same
numbers are the same colour wherever the file is opened.

What the page reads is held in `Words`, once per language, and `HQF_PDF_LANG` picks
which set is drawn. The numbers the bands are painted from are not language: they are
the same colours whoever reads the page.

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The left edge of everything, and how wide a row runs.
LEFT = 70.0
RUN = 455.0

# The white every calibrated space on the page is measured against, as CIE XYZ.
WHITE = (0.9505, 1.0, 1.089)

# The six colours the first three rows paint, each as three numbers.
COLORS = [
    [0.80, 0.10, 0.10],
    [0.90, 0.50, 0.10],
    [0.90, 0.85, 0.20],
    [0.15, 0.55, 0.30],
    [0.15, 0.35, 0.70],
    [0.45, 0.20, 0.55],
]

# The size of one colour swatch, and the gap to the next.
SWATCH = (70.0, 55.0)
SWATCH_GAP = 7.0

# How many steps the grey ramp is cut into, and how tall it stands.
STEPS = 10
RAMP_HEIGHT = 45.0


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

    The numbers the bands are painted from are not here: three numbers are the same
    three numbers in every language.
    """

    # The line at the head of the page.
    title: str
    # The two lines under it, saying that nothing below was painted twice.
    intro: tuple[str, str]
    # The heading of each panel, and the line under it.
    panels: tuple[tuple[str, str], tuple[str, str]]
    # The caption each band stands under, in the order the bands are painted.
    captions: tuple[str, str, str, str, str, str]
    # The three lines at the foot of the page.
    closing: tuple[str, str, str]


# The page in English.
ENGLISH = Words(
    title="The same numbers, through spaces that say what they mean",
    intro=(
        "Nothing below was painted twice with different numbers. Each band "
        "holds the very same numbers as",
        "the one above it, and what changes is the space they are said in.",
    ),
    panels=(
        (
            "Six colours, three ways of meaning them",
            "The three numbers are the same in every band. The primaries under them "
            "are not.",
        ),
        (
            "One grey ramp, three exponents",
            "Ten steps from 0.05 to 0.95, painted with the same ten numbers each time.",
        ),
    ),
    captions=(
        "Left to the device",
        "Through one set of primaries",
        "Through a wider set of primaries",
        "Left to the device",
        "Measured, raised to 1",
        "Measured, raised to 2.2",
    ),
    closing=(
        "A calibrated space carries its measurement in a handful of numbers "
        "rather than in a profile,",
        "so a file states what its colours are without embedding anything "
        "and without asking the",
        "reader to have the right profile installed.",
    ),
)

# The page in French.
FRENCH = Words(
    title="Les mêmes nombres, dans des espaces qui disent ce qu'ils valent",
    intro=(
        "Rien, plus bas, n'a été peint deux fois avec des nombres différents. "
        "Chaque bande porte les mêmes",
        "nombres que celle du dessus, et ce qui change est l'espace dans lequel "
        "on les dit.",
    ),
    panels=(
        (
            "Six couleurs, trois façons de les définir",
            "Les trois nombres sont les mêmes dans chaque bande. Les primaires sous "
            "eux, non.",
        ),
        (
            "Une rampe de gris, trois exposants",
            "Dix marches de 0.05 à 0.95, peintes à chaque fois avec les mêmes dix "
            "nombres.",
        ),
    ),
    captions=(
        "Laissé à l'appareil",
        "À travers un jeu de primaires",
        "À travers un jeu de primaires plus large",
        "Laissé à l'appareil",
        "Mesuré, élevé à 1",
        "Mesuré, élevé à 2.2",
    ),
    closing=(
        "Un espace calibré porte sa mesure dans une poignée de nombres plutôt "
        "que dans un profil,",
        "si bien qu'un fichier dit ce que sont ses couleurs sans rien embarquer "
        "et sans exiger",
        "que le lecteur ait le bon profil installé.",
    ),
)


# 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 caption(content, font, y, line):
    """Write the small caption a band stands under."""
    content.save_state()
    content.set_fill(hqf_pdf.Color.gray(0.45))
    content.draw_text(font, 8.0, LEFT, y, line)
    content.restore_state()


def panel_heading(content, font, y, title, under):
    """Draw one panel's heading, with the line under it."""
    content.save_state()
    content.set_fill(hqf_pdf.Color.gray(0.2))
    content.draw_text(font, 11.0, LEFT, y, title)
    content.set_fill(hqf_pdf.Color.gray(0.45))
    content.draw_text(font, 8.0, LEFT, y - 15.0, under)
    content.restore_state()


def band(content, space, bottom):
    """Paint the six colours across, in `space`; without one, left to the device."""
    x = LEFT
    for color in COLORS:
        content.save_state()
        if space is None:
            content.set_fill(hqf_pdf.Rgb(color[0], color[1], color[2]))
        else:
            content.set_fill_space(space)
            content.set_fill_components(color)
        content.rect(x, bottom, SWATCH[0], SWATCH[1])
        content.fill()
        content.restore_state()
        x += SWATCH[0] + SWATCH_GAP


def ramp(content, space, bottom):
    """Paint the grey ramp across, in `space`; without one, left to the device."""
    width = RUN / STEPS
    x = LEFT
    grey = 0.05
    for _ in range(STEPS):
        content.save_state()
        if space is None:
            content.set_fill(hqf_pdf.Color.gray(grey))
        else:
            content.set_fill_space(space)
            content.set_fill_components([grey])
        content.rect(x, bottom, width, RAMP_HEIGHT)
        content.fill()
        content.restore_state()
        x += width
        grey += 0.1


def heading(content, font, words):
    """Draw the title and the two lines saying what the page is showing."""
    content.draw_text(font, 15.0, LEFT, 780.0, words.title)
    content.save_state()
    content.set_fill(hqf_pdf.Color.gray(0.35))
    content.draw_text(font, 10.0, LEFT, 762.0, words.intro[0])
    content.draw_text(font, 10.0, LEFT, 748.0, words.intro[1])
    content.restore_state()


def closing(content, font, words):
    """Draw the closing note."""
    content.save_state()
    content.set_fill(hqf_pdf.Color.gray(0.35))
    for y, line in zip([140.0, 126.0, 112.0], words.closing):
        content.draw_text(font, 9.0, LEFT, y, line)
    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("calibrated.pdf", language)).stem)

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

    ordinary = document.add_color_space(
        hqf_pdf.CalRgb(WHITE)
        .gamma((2.2, 2.2, 2.2))
        .primaries(
            (0.4124, 0.2126, 0.0193),
            (0.3576, 0.7152, 0.1192),
            (0.1805, 0.0722, 0.9505),
        )
    )
    wider = document.add_color_space(
        hqf_pdf.CalRgb(WHITE)
        .gamma((2.2, 2.2, 2.2))
        .primaries(
            (0.5767, 0.2974, 0.0270),
            (0.1856, 0.6273, 0.0707),
            (0.1882, 0.0753, 0.9911),
        )
    )
    straight = document.add_color_space(hqf_pdf.CalGray(WHITE).gamma(1.0))
    bent = document.add_color_space(hqf_pdf.CalGray(WHITE).gamma(2.2))

    content = hqf_pdf.Content()
    heading(content, font, words)

    panel_heading(content, font, 706.0, words.panels[0][0], words.panels[0][1])
    caption(content, font, 668.0, words.captions[0])
    band(content, None, 610.0)
    caption(content, font, 594.0, words.captions[1])
    band(content, ordinary, 536.0)
    caption(content, font, 520.0, words.captions[2])
    band(content, wider, 462.0)

    panel_heading(content, font, 420.0, words.panels[1][0], words.panels[1][1])
    caption(content, font, 382.0, words.captions[3])
    ramp(content, None, 334.0)
    caption(content, font, 318.0, words.captions[4])
    ramp(content, straight, 270.0)
    caption(content, font, 254.0, words.captions[5])
    ramp(content, bent, 206.0)
    closing(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, {len(COLORS)} colours and {STEPS} greys "
        f"painted three times each"
    )


if __name__ == "__main__":
    main()