Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_font/src/set.rs

11.8 KiB, 48 runs

created by r1870400018:35702, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! A set of chains, one per role, and the faces the crate carries embedded.
2
3use crate::face::{
4 Face,
5 Role,
6};
7use crate::font::Font;
8
9use oxedyne_fe2o3_core::prelude::*;
10
11// ┌───────────────────────────────────────────────────────────────────────────┐
12// │ THE FACES THE CRATE CARRIES │
13// │ │
14// │ Change these seven lines and everything set with the embedded set wears a │
15// │ different typeface. Nothing else needs to know: the chain below keeps the │
16// │ coverage, so a face may be chosen for how it reads and for nothing else. │
17// └───────────────────────────────────────────────────────────────────────────┘
18
19const BODY: &[u8] = include_bytes!("../fonts/NotoSans-Regular.ttf");
20const BOLD: &[u8] = include_bytes!("../fonts/NotoSans-Bold.ttf");
21const ITALIC: &[u8] = include_bytes!("../fonts/NotoSans-Italic.ttf"); // a real italic, drawn, not sheared
22const BOLD_ITALIC: &[u8] = include_bytes!("../fonts/NotoSans-BoldItalic.ttf");
23const MONO: &[u8] = include_bytes!("../fonts/NotoSansMono-Regular.ttf");
24
25// The face every chain falls back to. DejaVu is not here to be read but for the arrows, mathematics,
26// Arabic and Hebrew a reading face lacks, so a `→` reaches ink rather than a box. Never tried first,
27// it shapes nothing a better face can draw.
28const WIDE: &[u8] = include_bytes!("../fonts/DejaVuSans.ttf");
29const WIDE_MONO: &[u8] = include_bytes!("../fonts/DejaVuSansMono.ttf"); // the same, behind mono
30
31/// The reader's typefaces, one chain per role.
32pub struct FontSet {
33 body: Font,
34 bold: Font,
35 italic: Font,
36 bold_italic: Font,
37 mono: Font,
38}
39
40impl FontSet {
41
42 /// The set the engine carries, so it renders standalone and identically anywhere. Every chain ends
43 /// in the wide face; the leaning chains fall back to the UPRIGHT wide face, since an arrow has no
44 /// italic form and an upright arrow in a leaning sentence is what every renderer shows.
45 pub fn embedded() -> Outcome<Self> {
46 let wide = || Face::new(WIDE.to_vec());
47 Ok(Self {
48 body: res!(Font::chain(vec![res!(Face::new(BODY.to_vec())), res!(wide())])),
49 bold: res!(Font::chain(vec![res!(Face::new(BOLD.to_vec())), res!(wide())])),
50 italic: res!(Font::chain(vec![res!(Face::new(ITALIC.to_vec())), res!(wide())])),
51 bold_italic: res!(Font::chain(vec![
52 res!(Face::new(BOLD_ITALIC.to_vec())),
53 res!(wide()),
54 ])),
55 mono: res!(Font::chain(vec![
56 res!(Face::new(MONO.to_vec())),
57 res!(Face::new(WIDE_MONO.to_vec())),
58 ])),
59 })
60 }
61
62 /// A set the reader supplies.
63 pub fn new(body: Font, bold: Font, italic: Font, bold_italic: Font, mono: Font) -> Self {
64 Self {
65 body,
66 bold,
67 italic,
68 bold_italic,
69 mono,
70 }
71 }
72
73 /// The font playing a role.
74 pub fn get(&self, role: Role) -> &Font {
75 match role {
76 Role::Body => &self.body,
77 Role::Bold => &self.bold,
78 Role::Italic => &self.italic,
79 Role::BoldItalic => &self.bold_italic,
80 Role::Mono => &self.mono,
81 }
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::shape::Dir;
89
90 #[test]
91 fn test_the_embedded_set_loads_00() -> Outcome<()> {
92 let fs = res!(FontSet::embedded());
93 let m = res!(fs.get(Role::Body).metrics(16.0));
94 assert!(m.ascent > 0.0, "the ascent should rise above the baseline, found {}", m.ascent);
95 assert!(m.descent > 0.0, "the descent should fall below it, found {}", m.descent);
96 assert!(m.line_height() > 16.0, "a line is taller than its type size");
97 Ok(())
98 }
99
100 #[test]
101 fn test_shaping_yields_a_glyph_per_letter_01() -> Outcome<()> {
102 let fs = res!(FontSet::embedded());
103 let run = res!(fs.get(Role::Body).shape("Hello", 16.0, Dir::Ltr));
104 assert_eq!(run.glyphs.len(), 5, "five letters, five glyphs, in a font with no ligature here");
105 assert!(run.advance > 0.0, "the pen must travel");
106 // The glyphs march rightwards.
107 for w in run.glyphs.windows(2) {
108 assert!(w[1].x > w[0].x, "glyphs should advance to the right");
109 }
110 Ok(())
111 }
112
113 #[test]
114 fn test_an_empty_string_shapes_to_nothing_02() -> Outcome<()> {
115 let fs = res!(FontSet::embedded());
116 let run = res!(fs.get(Role::Body).shape("", 16.0, Dir::Ltr));
117 assert!(run.is_empty());
118 assert_eq!(run.advance, 0.0);
119 Ok(())
120 }
121
122 #[test]
123 fn test_a_glyph_has_an_outline_03() -> Outcome<()> {
124 let fs = res!(FontSet::embedded());
125 let font = fs.get(Role::Body);
126 let run = res!(font.shape("H", 64.0, Dir::Ltr));
127 assert_eq!(run.glyphs.len(), 1);
128 let g = run.glyphs[0];
129 let path = res!(font.outline(g.face, g.id, 64.0));
130 assert!(!path.is_empty(), "the letter H has an outline");
131 Ok(())
132 }
133
134 #[test]
135 fn test_a_space_has_an_advance_but_no_ink_04() -> Outcome<()> {
136 let fs = res!(FontSet::embedded());
137 let font = fs.get(Role::Body);
138 let run = res!(font.shape(" ", 16.0, Dir::Ltr));
139 assert_eq!(run.glyphs.len(), 1);
140 assert!(run.advance > 0.0, "a space still moves the pen");
141 let g = run.glyphs[0];
142 let path = res!(font.outline(g.face, g.id, 16.0));
143 assert!(path.is_empty(), "but it lays down no ink");
144 Ok(())
145 }
146
147 /// The characters that sent me looking for a chain: each appears in real documents already held in
148 /// the library, the reading face draws none of them, and before the chain they were tofu.
149 #[test]
150 fn test_what_the_reading_face_lacks_is_drawn_by_the_face_behind_it_06() -> Outcome<()> {
151 let fs = res!(FontSet::embedded());
152 let font = fs.get(Role::Body);
153 for (ch, what) in [
154 ('\u{2192}', "an arrow"),
155 ('\u{2295}', "a circled plus"),
156 ('\u{2264}', "less than or equal to"),
157 ('\u{2265}', "greater than or equal to"),
158 ('\u{2248}', "approximately equal to"),
159 ] {
160 let run = res!(font.shape(&fmt!("{}", ch), 32.0, Dir::Ltr));
161 assert_eq!(run.glyphs.len(), 1, "{} shapes to one glyph", what);
162 let g = run.glyphs[0];
163 assert_ne!(g.id, 0, "{} must not be the 'not defined' glyph", what);
164 assert!(g.face > 0, "{} comes from a face behind the first, since the first lacks it", what);
165 let path = res!(font.outline(g.face, g.id, 32.0));
166 assert!(!path.is_empty(), "{} must reach ink", what);
167 }
168 Ok(())
169 }
170
171 /// The reading face draws what it can, and is not passed over for the wide one.
172 #[test]
173 fn test_ordinary_prose_is_drawn_by_the_reading_face_alone_07() -> Outcome<()> {
174 let fs = res!(FontSet::embedded());
175 let font = fs.get(Role::Body);
176 let run = res!(font.shape("The quick brown fox, jumps -- over 42 lazy dogs!", 16.0, Dir::Ltr));
177 for g in &run.glyphs {
178 assert_eq!(g.face, 0, "ordinary prose is the reading face's, and nothing else's");
179 }
180 Ok(())
181 }
182
183 /// A face already in hand keeps the spaces and punctuation around what it draws, so a mixed string
184 /// comes back in as few stretches as it has real changes of face -- not one per word.
185 #[test]
186 fn test_a_face_in_hand_keeps_what_it_can_draw_08() -> Outcome<()> {
187 let fs = res!(FontSet::embedded());
188 let font = fs.get(Role::Body);
189 // One change of face and back: the arrow, and nothing else.
190 let run = res!(font.shape("from here \u{2192} to there", 16.0, Dir::Ltr));
191 let faces: Vec<u8> = run.glyphs.iter().map(|g| g.face).collect();
192 let mut changes = 0;
193 for w in faces.windows(2) {
194 if w[0] != w[1] {
195 changes += 1;
196 }
197 }
198 assert_eq!(changes, 2, "into the wide face for the arrow and back out: {:?}", faces);
199 Ok(())
200 }
201
202 /// A glyph's cluster is a byte offset into the WHOLE string, not the stretch it was cut into --
203 /// what a caret is placed by, so a fallback in the middle does not misplace every caret after it.
204 #[test]
205 fn test_clusters_are_offsets_into_the_whole_string_09() -> Outcome<()> {
206 let fs = res!(FontSet::embedded());
207 let font = fs.get(Role::Body);
208 let text = "ab \u{2192} cd";
209 let run = res!(font.shape(text, 16.0, Dir::Ltr));
210 for g in &run.glyphs {
211 assert!(
212 text.is_char_boundary(g.cluster),
213 "cluster {} is a character boundary of {:?}", g.cluster, text,
214 );
215 }
216 // The clusters march forwards across the join, rather than restarting at it.
217 let clusters: Vec<usize> = run.glyphs.iter().map(|g| g.cluster).collect();
218 let mut sorted = clusters.clone();
219 sorted.sort();
220 assert_eq!(clusters, sorted, "clusters do not restart at a change of face: {:?}", clusters);
221 match clusters.last() {
222 Some(last) => assert!(*last > 3, "the text after the arrow is offset past it: {:?}", clusters),
223 None => return Err(err!("the string shaped to nothing"; Test, Bug)),
224 }
225 Ok(())
226 }
227
228 /// Emphasis is a real face, not a sheared one.
229 #[test]
230 fn test_the_set_carries_a_real_italic_and_a_real_bold_italic_10() -> Outcome<()> {
231 let fs = res!(FontSet::embedded());
232 // A true italic has different letterforms, so the same letter is a different SHAPE, not the
233 // same shape leaning. Comparing outlines is the only way to tell a real italic from a shear:
234 // an obliqued upright would have the same glyph id in the same face.
235 let upright = res!(fs.get(Role::Body).shape("a", 64.0, Dir::Ltr));
236 let leaning = res!(fs.get(Role::Italic).shape("a", 64.0, Dir::Ltr));
237 let bold_leaning = res!(fs.get(Role::BoldItalic).shape("a", 64.0, Dir::Ltr));
238 for (run, what) in [(&leaning, "the italic"), (&bold_leaning, "the bold italic")] {
239 assert_eq!(run.glyphs.len(), 1, "{} draws one letter", what);
240 let g = run.glyphs[0];
241 let path = res!(fs.get(Role::Italic).outline(g.face, g.id, 64.0));
242 assert!(!path.is_empty(), "{} 'a' has an outline", what);
243 }
244 // The italic 'a' is a different width from the upright one, which a shear cannot change: a
245 // shear moves the tops of the letters and leaves the advance exactly where it was.
246 assert!(
247 (upright.advance - leaning.advance).abs() > 0.01,
248 "a real italic is a drawn face, so its 'a' is not the upright's width: {} then {}",
249 upright.advance, leaning.advance,
250 );
251 Ok(())
252 }
253
254 #[test]
255 fn test_bigger_type_travels_further_05() -> Outcome<()> {
256 let fs = res!(FontSet::embedded());
257 let font = fs.get(Role::Body);
258 let small = res!(font.shape("Hello", 16.0, Dir::Ltr));
259 let big = res!(font.shape("Hello", 32.0, Dir::Ltr));
260 assert!(
261 (big.advance - 2.0 * small.advance).abs() < 0.5,
262 "twice the size should be twice the width: {} then {}", small.advance, big.advance,
263 );
264 Ok(())
265 }
266
267 /// A face reports the family its designer named and where it sits in it, so a document's
268 /// `font: "Name"` can be matched against the file rather than its filename.
269 #[test]
270 fn test_a_face_reports_its_family_weight_and_slant_11() -> Outcome<()> {
271 let fs = res!(FontSet::embedded());
272 let body = res!(fs.get(Role::Body).info());
273 assert_eq!(body.family, "Noto Sans");
274 assert_eq!(body.weight, 400);
275 assert!(!body.italic);
276 let bi = res!(fs.get(Role::BoldItalic).info());
277 assert_eq!(bi.family, "Noto Sans", "the bold italic belongs to the same family");
278 assert_eq!(bi.weight, 700);
279 assert!(bi.italic);
280 assert!(crate::face::FaceInfo::read(b"not a font").is_err(), "garbage is refused, not guessed");
281 Ok(())
282 }
283
284 /// A run shaped with a feature the face carries draws different glyphs from the plain run, and a run
285 /// shaped with no features is exactly the plain run.
286 #[test]
287 fn test_a_feature_changes_the_glyphs_it_governs_12() -> Outcome<()> {
288 use crate::shape::Feature;
289 let fs = res!(FontSet::embedded());
290 let font = fs.get(Role::Body);
291 let plain = res!(font.shape("small", 16.0, Dir::Ltr));
292 let none = res!(font.shape_with("small", 16.0, Dir::Ltr, &[]));
293 let ids = |r: &crate::shape::Run| r.glyphs.iter().map(|g| g.id).collect::<Vec<u32>>();
294 assert_eq!(ids(&plain), ids(&none), "no features is the plain shaping");
295 let smcp = res!(font.shape_with("small", 16.0, Dir::Ltr, &[Feature::SMALL_CAPS]));
296 assert_ne!(ids(&plain), ids(&smcp), "smcp swaps the lower-case letters for small capitals");
297 Ok(())
298 }
299}