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 | """Shows the same field of stripes through two clipping paths: a disc set with
the nonzero rule, and a frame set with the even-odd rule over two nested
rectangles, so only the ring between them lets the stripes through.
The Python twin of the `write_clip` example in Rust. Each panel saves the
graphic state, states a path, marks it as the clip and ends it without a mark of
its own, then fills the stripes and restores the state so the clip lasts no
longer than the panel. The clip's own outline is drawn afterwards, so the window
it opened can be seen against the stripes.
Both labels are held in `Words`, once per language, and `HQF_PDF_LANG` picks which set
is drawn.
Usage: python examples/write_clip.py [out.pdf] [font.ttf]
HQF_PDF_LANG=fr python examples/write_clip.py
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import _language
import _licence
import _out
import hqf_pdf
@dataclass(frozen=True)
class Words:
"""Every word the page draws, in one language."""
# The label above the disc panel.
disc: str
# The label above the frame panel.
frame: str
# The page in English.
ENGLISH = Words(
disc="Clipped to a disc.",
frame="Clipped even-odd to a frame.",
)
# The page in French.
FRENCH = Words(
disc="Découpé selon un disque.",
frame="Découpé selon un cadre, règle pair-impair.",
)
# 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 distance a control point sits from the end of a quarter circle for the cubic that
# draws it, as a fraction of the radius.
KAPPA = 0.5522847498307939
# The left edge of both panels' fields.
X = 90.0
# The side of the square field the stripes fill.
SIDE = 150.0
# The height of one stripe, a divisor of the field's side.
STRIPE = 15.0
def stripes(content: hqf_pdf.Content, x: float, bottom: float) -> None:
"""Fills the square field whose bottom-left corner is (x, bottom) with
horizontal stripes in two alternating colours."""
band = 0
y = bottom
while y < bottom + SIDE:
if band % 2 == 0:
color = hqf_pdf.Rgb(0.15, 0.35, 0.75)
else:
color = hqf_pdf.Rgb(0.85, 0.90, 0.98)
content.set_fill(color)
content.rect(x, y, SIDE, STRIPE)
content.fill()
y += STRIPE
band += 1
def circle(content: hqf_pdf.Content, cx: float, cy: float, r: float) -> None:
"""Traces a circle of radius r about (cx, cy) as four cubic quarters."""
k = r * KAPPA
content.move_to(cx + r, cy)
content.curve_to(cx + r, cy + k, cx + k, cy + r, cx, cy + r)
content.curve_to(cx - k, cy + r, cx - r, cy + k, cx - r, cy)
content.curve_to(cx - r, cy - k, cx - k, cy - r, cx, cy - r)
content.curve_to(cx + k, cy - r, cx + r, cy - k, cx + r, cy)
content.close_path()
def heading(
content: hqf_pdf.Content, handle: hqf_pdf.FontHandle, top: float, label: str
) -> None:
"""Sets a one-line label with its baseline at top, above a panel."""
flow = hqf_pdf.TextFlow(handle, 9.0)
lines = flow.break_lines(label, 400.0)
flow.draw(content, lines, X, top, 400.0)
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("clip.pdf", language)).stem)
document = hqf_pdf.Document()
document.set_license(_licence.licensed())
handle = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))
content = hqf_pdf.Content()
# The disc: the nonzero rule lets the stripes through the whole circle.
disc_bottom = 560.0
heading(content, handle, disc_bottom + SIDE + 12.0, words.disc)
content.save_state()
circle(content, X + SIDE / 2.0, disc_bottom + SIDE / 2.0, SIDE / 2.0)
content.clip()
content.end_path()
stripes(content, X, disc_bottom)
content.restore_state()
content.set_stroke(hqf_pdf.Rgb.gray(0.4))
content.set_line_width(0.5)
circle(content, X + SIDE / 2.0, disc_bottom + SIDE / 2.0, SIDE / 2.0)
content.stroke()
# The frame: the even-odd rule counts the inner square as outside, so the stripes
# show only in the ring between the two.
frame_bottom = 320.0
heading(content, handle, frame_bottom + SIDE + 12.0, words.frame)
inset = SIDE / 4.0
content.save_state()
content.rect(X, frame_bottom, SIDE, SIDE)
content.rect(X + inset, frame_bottom + inset, SIDE - inset * 2.0, SIDE - inset * 2.0)
content.clip_even_odd()
content.end_path()
stripes(content, X, frame_bottom)
content.restore_state()
content.set_stroke(hqf_pdf.Rgb.gray(0.4))
content.set_line_width(0.5)
content.rect(X, frame_bottom, SIDE, SIDE)
content.rect(X + inset, frame_bottom + inset, SIDE - inset * 2.0, SIDE - inset * 2.0)
content.stroke()
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()
|