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 | |
| 3 | use crate::face::{ |
| 4 | Face, |
| 5 | Role, |
| 6 | }; |
| 7 | use crate::font::Font; |
| 8 | |
| 9 | use 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 | |
| 19 | const BODY: &[u8] = include_bytes!("../fonts/NotoSans-Regular.ttf"); |
| 20 | const BOLD: &[u8] = include_bytes!("../fonts/NotoSans-Bold.ttf"); |
| 21 | const ITALIC: &[u8] = include_bytes!("../fonts/NotoSans-Italic.ttf"); // a real italic, drawn, not sheared |
| 22 | const BOLD_ITALIC: &[u8] = include_bytes!("../fonts/NotoSans-BoldItalic.ttf"); |
| 23 | const 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. |
| 28 | const WIDE: &[u8] = include_bytes!("../fonts/DejaVuSans.ttf"); |
| 29 | const WIDE_MONO: &[u8] = include_bytes!("../fonts/DejaVuSansMono.ttf"); // the same, behind mono |
| 30 | |
| 31 | /// The reader's typefaces, one chain per role. |
| 32 | pub struct FontSet { |
| 33 | body: Font, |
| 34 | bold: Font, |
| 35 | italic: Font, |
| 36 | bold_italic: Font, |
| 37 | mono: Font, |
| 38 | } |
| 39 | |
| 40 | impl 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)] |
| 86 | mod 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 | } |