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 | //! 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.
//!
//! 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: `cargo run --example write_clip -- tmp/clip.pdf [font.ttf]`
//! `HQF_PDF_LANG=fr cargo run --example write_clip -- tmp/decoupe.pdf`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
use hqf_pdf::{Document, Font, Page, Rgb, TextFlow};
#[path = "shared/out.rs"]
mod out;
#[path = "shared/licence.rs"]
mod licence;
#[path = "shared/language.rs"]
mod language;
use language::Language;
/// The font the example draws with when none is given on the command line: the
/// one committed for the tests, so that the example runs on any machine.
fn default_font() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fonts")
.join("DejaVuSans.ttf")
}
/// The words the page is written in, one set per language.
#[derive(Debug)]
struct Words {
/// The label above the disc panel.
disc: &'static str,
/// The label above the frame panel.
frame: &'static str,
}
impl Words {
/// The words the page is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
}
/// The page in English.
const ENGLISH: Words = Words {
disc: "Clipped to a disc.",
frame: "Clipped even-odd to a frame.",
};
/// The page in French.
const FRENCH: Words = 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.
static WORDS: [(Language, &Words); 2] =
[(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.
const KAPPA: f64 = 0.552_284_749_830_793_9;
/// The left edge of both panels' fields, and the width and height of each.
const X: f64 = 90.0;
/// The side of the square field the stripes fill.
const SIDE: f64 = 150.0;
/// The height of one stripe.
const STRIPE: f64 = 15.0;
/// How many stripes cover the field: its side divided by one stripe's height.
const STRIPES: usize = 10;
/// Fills the square field whose bottom-left corner is `(x, bottom)` with
/// horizontal stripes in two alternating colours.
fn stripes(c: &mut Content, x: f64, bottom: f64) -> Result<(), Box<dyn std::error::Error>> {
let mut y = bottom;
for band in 0..STRIPES {
let color = if band % 2 == 0 {
Rgb::new(0.15, 0.35, 0.75)
} else {
Rgb::new(0.85, 0.90, 0.98)
};
c.set_fill(color)?.rect(x, y, SIDE, STRIPE)?.fill();
y += STRIPE;
}
Ok(())
}
/// Traces a circle of radius `r` about `(cx, cy)` as four cubic quarters.
fn circle(c: &mut Content, cx: f64, cy: f64, r: f64) -> Result<(), Box<dyn std::error::Error>> {
let k = r * KAPPA;
c.move_to(cx + r, cy)?
.curve_to(cx + r, cy + k, cx + k, cy + r, cx, cy + r)?
.curve_to(cx - k, cy + r, cx - r, cy + k, cx - r, cy)?
.curve_to(cx - r, cy - k, cx - k, cy - r, cx, cy - r)?
.curve_to(cx + k, cy - r, cx + r, cy - k, cx + r, cy)?
.close_path();
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let language = Language::from_environment()?;
let words = Words::of(language);
let mut args = env::args().skip(1);
// A named file is written as named; the default one carries the language,
// so the two languages do not overwrite each other in `tmp/`.
let out = args
.next()
.unwrap_or_else(|| language.file_name(&out::default_path("clip")));
let font_path = args.next().map_or_else(default_font, PathBuf::from);
let font = Font::parse(fs::read(&font_path)?)?;
let mut doc = Document::new();
doc.set_license(licence::licensed());
let handle = doc.add_font(font);
let mut c = Content::new();
// The disc: the nonzero rule lets the stripes through the whole circle.
let disc_bottom = 560.0;
heading(&mut c, &handle, disc_bottom + SIDE + 12.0, words.disc)?;
c.save_state();
circle(&mut c, X + SIDE / 2.0, disc_bottom + SIDE / 2.0, SIDE / 2.0)?;
c.clip().end_path();
stripes(&mut c, X, disc_bottom)?;
c.restore_state();
c.set_stroke(Rgb::gray(0.4))?.set_line_width(0.5)?;
circle(&mut c, X + SIDE / 2.0, disc_bottom + SIDE / 2.0, SIDE / 2.0)?;
c.stroke();
// The frame: the even-odd rule counts the inner square as outside, so the
// stripes show only in the ring between the two.
let frame_bottom = 320.0;
heading(&mut c, &handle, frame_bottom + SIDE + 12.0, words.frame)?;
let inset = SIDE / 4.0;
c.save_state();
c.rect(X, frame_bottom, SIDE, SIDE)?
.rect(
X + inset,
frame_bottom + inset,
SIDE - inset * 2.0,
SIDE - inset * 2.0,
)?
.clip_even_odd()
.end_path();
stripes(&mut c, X, frame_bottom)?;
c.restore_state();
c.set_stroke(Rgb::gray(0.4))?
.set_line_width(0.5)?
.rect(X, frame_bottom, SIDE, SIDE)?
.rect(
X + inset,
frame_bottom + inset,
SIDE - inset * 2.0,
SIDE - inset * 2.0,
)?
.stroke();
let mut page = Page::a4();
page.content = c.into_bytes();
doc.add_page(page)?;
let bytes = doc.to_bytes()?;
if let Some(parent) = Path::new(&out).parent() {
fs::create_dir_all(parent)?;
}
fs::write(&out, &bytes)?;
println!("wrote {out}: {} bytes", bytes.len());
Ok(())
}
/// Sets a one-line label with its baseline at `top`, above a panel.
fn heading(
c: &mut Content,
handle: &hqf_pdf::FontHandle,
top: f64,
label: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let flow = TextFlow::new(handle, 9.0);
let lines = flow.break_lines(label, 400.0);
c.begin_text();
flow.draw(c, &lines, X, top, 400.0)?;
c.end_text();
Ok(())
}
#[cfg(test)]
mod tests {
use super::{WORDS, language};
/// The lines two languages are allowed to write the same way. There are
/// none: every word is a word of the language it is written in.
const SPARED: [&str; 0] = [];
#[test]
fn every_language_draws_the_page_in_its_own_words() {
let untranslated = language::untranslated_lines(&WORDS, &SPARED);
assert!(
untranslated.is_empty(),
"the page says these in more than one language: {untranslated:?}"
);
}
}
|