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 | """Colour the document defines, next to colour it leaves to the device.
The Python twin of the `write_measured_color` example in Rust. Three numbers
said as they are leave what they look like to whatever shows them. The rows below
say colour two other ways: through the ICC profile the file carries, so the
numbers mean what they meant when the document was made, and as CIE 1976 L*a*b*,
where lightness and colour are separate numbers by construction.
What the page reads is held in `Words`, once per language, and `HQF_PDF_LANG` picks
which set is drawn. The numbers the swatches are painted from are not language: they
are the same colours whoever reads the page.
Usage: python examples/write_measured_color.py [out.pdf] [font.ttf]
HQF_PDF_LANG=fr python examples/write_measured_color.py
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Callable
import _language
import _licence
import _out
import hqf_pdf
# The size of one swatch, in points.
SWATCH = (70.0, 60.0)
# The left edge of the first swatch.
LEFT = 70.0
# The left edge of each swatch in a row: the first one, then every seventy-eight points
# across.
COLUMN_X = [70.0, 148.0, 226.0, 304.0, 382.0, 460.0]
# The six colours the first two rows are painted in, as three numbers each.
COLORS = [
[0.85, 0.20, 0.15],
[0.92, 0.58, 0.10],
[0.20, 0.55, 0.30],
[0.12, 0.40, 0.70],
[0.45, 0.25, 0.60],
[0.20, 0.20, 0.22],
]
# Six lightnesses at one colour, as `L*`, `a*` and `b*`.
LIGHTNESSES = [
[20.0, 40.0, 25.0],
[35.0, 40.0, 25.0],
[50.0, 40.0, 25.0],
[65.0, 40.0, 25.0],
[80.0, 40.0, 25.0],
[95.0, 40.0, 25.0],
]
# Six colours at one lightness, as `L*`, `a*` and `b*`.
HUES = [
[60.0, 60.0, 40.0],
[60.0, 20.0, 60.0],
[60.0, -50.0, 45.0],
[60.0, -40.0, -20.0],
[60.0, 10.0, -55.0],
[60.0, 50.0, -20.0],
]
@dataclass(frozen=True)
class Words:
"""Every word the page draws, in one language.
The numbers the swatches 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 what the first two rows hold.
intro: tuple[str, str]
# What each row of swatches shows, in the order the rows are drawn.
rows: tuple[str, str, str, str]
# The line under the stroked rule.
rule: str
# The three lines at the foot of the page.
closing: tuple[str, str, str]
# The page in English.
ENGLISH = Words(
title="Colour the document defines, not the device",
intro=(
"The first two rows hold the same six sets of three numbers. The second "
"row names a colour",
"space the file carries the profile for, so what those numbers mean "
"travels with the file.",
),
rows=(
"Three numbers said as they are, left to whatever shows them",
"The same three numbers, read through the sRGB profile the file carries",
"One colour at six lightnesses, said as L*a*b*: only the first number "
"changes",
"Six colours at one lightness: only the other two change",
),
rule="A rule stroked in the same space, by the stroking operators",
closing=(
"The first two rows look alike on a screen that is already sRGB, and "
"that is the point:",
"the difference is not what they look like here. It is that the second "
"row still names these",
"colours on a press, on another screen, and in ten years, because the "
"file says what it means.",
),
)
# The page in French.
FRENCH = Words(
title="La couleur que le document définit, pas celle de l'appareil",
intro=(
"Les deux premières rangées portent les mêmes six jeux de trois nombres. "
"La seconde",
"nomme un espace dont le fichier porte le profil : ces nombres gardent "
"leur sens partout.",
),
rows=(
"Trois nombres dits tels quels, laissés à ce qui les affiche",
"Les mêmes trois nombres, lus à travers le profil sRGB que porte le fichier",
"Une couleur à six clartés, dite en L*a*b* : seul le premier nombre change",
"Six couleurs à une même clarté : seuls les deux autres changent",
),
rule="Un filet tracé dans le même espace, par les opérateurs de trait",
closing=(
"Les deux premières rangées se ressemblent sur un écran déjà sRGB, et "
"c'est bien le point :",
"la différence n'est pas dans ce qu'on voit ici. C'est que la seconde "
"rangée nomme encore",
"ces couleurs sur une presse, sur un autre écran et dans dix ans, car le "
"fichier le dit.",
),
)
# 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 row(
content: hqf_pdf.Content,
font: hqf_pdf.FontHandle,
y: float,
caption: str,
count: int,
paint: Callable[[hqf_pdf.Content, int], None],
) -> None:
"""Draws a row of swatches, each painted by `paint`, and captions it."""
width, height = SWATCH
for column, x in enumerate(COLUMN_X[:count]):
content.save_state()
paint(content, column)
content.rect(x, y, width, height)
content.fill()
content.restore_state()
content.save_state()
content.set_fill(hqf_pdf.Rgb.gray(0.35))
content.draw_text(font, 9.0, LEFT, y - 14.0, caption)
content.restore_state()
def rows(
content: hqf_pdf.Content,
font: hqf_pdf.FontHandle,
words: Words,
profile: hqf_pdf.ColorSpaceHandle,
lab: hqf_pdf.ColorSpaceHandle,
) -> None:
"""Draws the four rows of swatches."""
def device(content: hqf_pdf.Content, column: int) -> None:
r, g, b = COLORS[column]
content.set_fill(hqf_pdf.Rgb(r, g, b))
def through_profile(content: hqf_pdf.Content, column: int) -> None:
content.set_fill_space(profile)
content.set_fill_components(COLORS[column])
def lightness(content: hqf_pdf.Content, column: int) -> None:
content.set_fill_space(lab)
content.set_fill_components(LIGHTNESSES[column])
def hue(content: hqf_pdf.Content, column: int) -> None:
content.set_fill_space(lab)
content.set_fill_components(HUES[column])
row(content, font, 660.0, words.rows[0], len(COLORS), device)
row(content, font, 560.0, words.rows[1], len(COLORS), through_profile)
row(content, font, 460.0, words.rows[2], len(LIGHTNESSES), lightness)
row(content, font, 360.0, words.rows[3], len(HUES), hue)
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("measured.pdf", language)).stem)
document = hqf_pdf.Document()
document.set_license(_licence.licensed())
font = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))
profile = document.add_color_space(
hqf_pdf.IccBased(hqf_pdf.srgb_icc_profile(), hqf_pdf.DeviceSpace.Rgb)
)
# The white of D65 daylight, as CIE XYZ: the light L*a*b* is measured against here.
lab = document.add_color_space(
hqf_pdf.Lab((0.9505, 1.0, 1.089)).range((-100.0, 100.0, -100.0, 100.0))
)
content = hqf_pdf.Content()
content.draw_text(font, 15.0, LEFT, 780.0, words.title)
content.save_state()
content.set_fill(hqf_pdf.Rgb.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()
rows(content, font, words, profile, lab)
# A stroke is said in its own space, by its own operators.
content.save_state()
content.set_stroke_space(lab)
content.set_stroke_components([45.0, 55.0, 35.0])
content.set_line_width(6.0)
content.move_to(LEFT, 300.0)
content.line_to(COLUMN_X[len(COLORS) - 1] + SWATCH[0], 300.0)
content.stroke()
content.restore_state()
content.save_state()
content.set_fill(hqf_pdf.Rgb.gray(0.35))
content.draw_text(font, 9.0, LEFT, 280.0, words.rule)
content.restore_state()
content.save_state()
content.set_fill(hqf_pdf.Rgb.gray(0.25))
for y, line in zip([235.0, 221.0, 207.0], words.closing):
content.draw_text(font, 10.0, LEFT, y, line)
content.restore_state()
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) * 3} swatches "
"in two spaces the document defines"
)
if __name__ == "__main__":
main()
|