Skip to main content

max / shop

17.2 KB · 445 lines History Blame Raw
1 //! Swash wrapper: parse fonts, shape a cluster, rasterize a glyph.
2 //!
3 //! Holds a SET of fonts, not one. Slot 0 is the bundled font and answers for
4 //! almost everything; the rest arrive one at a time from [`Fallback`] when a
5 //! character turns up that slot 0 does not have. A glyph is therefore
6 //! identified by (font, glyph id) rather than by glyph id, since ids only mean
7 //! something inside the font that issued them.
8
9 use std::collections::HashMap;
10
11 use std::collections::HashSet;
12
13 use swash::{
14 FontRef, GlyphId,
15 scale::{Render, ScaleContext, Source, image::Content},
16 shape::ShapeContext,
17 zeno::{Format, Transform},
18 };
19
20 use crate::fallback::Fallback;
21 use crate::metrics::CellMetrics;
22
23 /// Characters drawn to meet their neighbours rather than to sit inside their
24 /// own cell.
25 ///
26 /// Box drawing and block elements are the obvious ones: a border seams if the
27 /// bar does not reach both edges. Braille is here because a `Canvas` or a
28 /// sparkline paints one picture across many cells. The legacy-computing
29 /// sextants are blocks by another name. And the four powerline separators are
30 /// the classic visible case — a prompt with a hairline of background showing
31 /// through every chevron.
32 ///
33 /// Over-including is cheap and under-including is not: a glyph that did not
34 /// need snapping is scaled by well under a pixel, which nobody can see, while
35 /// a glyph that needed it and did not get it leaves a seam on every row.
36 pub(crate) fn is_cell_furniture(c: char) -> bool {
37 matches!(c,
38 '\u{2500}'..='\u{259F}' // box drawing, block elements
39 | '\u{2800}'..='\u{28FF}' // braille
40 | '\u{E0B0}'..='\u{E0B3}' // powerline separators
41 | '\u{1FB00}'..='\u{1FBFF}' // legacy computing
42 )
43 }
44
45 /// Which font in the set. 0 is always the bundled one.
46 pub(crate) type FontId = u16;
47
48 pub(crate) const PRIMARY: FontId = 0;
49
50 struct Face {
51 data: Vec<u8>,
52 offset: u32,
53 }
54
55 pub(crate) struct Shaper {
56 faces: Vec<Face>,
57 /// Which face has a character, remembered including the misses: a
58 /// character nothing has is a fontconfig query we only want to run once.
59 resolved: HashMap<char, Option<FontId>>,
60 /// Font file to the slot already holding it, so one fallback face serves
61 /// every character it covers rather than being loaded per character.
62 by_file: HashMap<(String, i32), FontId>,
63 fallback: Fallback,
64 scale_ctx: ScaleContext,
65 shape_ctx: ShapeContext,
66 px: f32,
67 /// Where on the primary face's weight axis to draw.
68 ///
69 /// Named rather than inherited, because a variable face's default instance
70 /// is whatever its designer set and is not always the weight anyone wants:
71 /// Quasi Mono is cut from a base whose default is `wght` 200, so a terminal
72 /// that took the file's own default would draw every prompt in ExtraLight.
73 ///
74 /// Applied to every face, not only the primary one. A static fallback has
75 /// no axes and swash drops the setting, so this needs no branch.
76 weight: f32,
77 /// The cell this shaper is drawing into, in physical pixels.
78 cell: CellMetrics,
79 /// Scale that maps the face's own cell onto the pixel one, applied to
80 /// [`is_cell_furniture`] glyphs at rasterization.
81 ///
82 /// Both factors are within a pixel of 1, because the pixel cell is the
83 /// face's own measurements rounded. That is the whole trick: the distortion
84 /// is too small to see and it is exactly enough to close the seam.
85 snap: (f32, f32),
86 /// Glyph ids known to be furniture, learned as characters are resolved.
87 ///
88 /// Kept by id rather than re-derived from a character, because
89 /// rasterization is reached with a glyph id and a font and nothing else —
90 /// which is also what the atlas cache is keyed on, so a glyph is snapped or
91 /// not for its whole life and never both.
92 furniture: HashSet<(FontId, GlyphId)>,
93 }
94
95 pub(crate) struct ShapedGlyph {
96 pub(crate) id: GlyphId,
97 /// Pen advance in pixels after this glyph.
98 pub(crate) advance: f32,
99 /// Position offsets relative to the pen origin.
100 pub(crate) x_offset: f32,
101 pub(crate) y_offset: f32,
102 }
103
104 pub(crate) struct Raster {
105 pub(crate) bitmap: Vec<u8>,
106 pub(crate) width: u32,
107 pub(crate) height: u32,
108 /// Left edge relative to the pen origin (positive = right of origin).
109 pub(crate) placement_left: i32,
110 /// Top edge relative to the baseline (positive = above baseline).
111 pub(crate) placement_top: i32,
112 }
113
114 impl Shaper {
115 pub(crate) fn new(
116 font_data: Vec<u8>,
117 px: f32,
118 cell: CellMetrics,
119 weight: f32,
120 ) -> anyhow::Result<Self> {
121 let font = FontRef::from_index(&font_data, 0)
122 .ok_or_else(|| anyhow::anyhow!("swash: not a font"))?;
123 let font_offset = font.offset;
124 // Measured here rather than taken from `cell`, and the difference is
125 // the point: `cell` is the grid's step in physical pixels, which is a
126 // logical cell times an integer scale, while this is what the face
127 // draws at the size this shaper rasterizes at. Snapping between the two
128 // absorbs the gap instead of leaving it on screen.
129 let own = CellMetrics::measure(&font_data, px)?;
130 let snap = (
131 safe_ratio(cell.advance, own.exact_advance),
132 safe_ratio(cell.height, own.exact_height),
133 );
134 Ok(Self {
135 faces: vec![Face {
136 data: font_data,
137 offset: font_offset,
138 }],
139 resolved: HashMap::new(),
140 by_file: HashMap::new(),
141 fallback: Fallback::new(),
142 scale_ctx: ScaleContext::new(),
143 shape_ctx: ShapeContext::new(),
144 px,
145 weight,
146 cell,
147 snap,
148 furniture: HashSet::new(),
149 })
150 }
151
152 fn face(&self, id: FontId) -> FontRef<'_> {
153 let f = &self.faces[id as usize];
154 FontRef {
155 data: &f.data,
156 offset: f.offset,
157 key: swash::CacheKey::new(),
158 }
159 }
160
161 /// Char → the font that has it and its glyph id there.
162 ///
163 /// Falls back to `(PRIMARY, 0)` when nothing has it, which draws whatever
164 /// the bundled font puts at `.notdef`. On Quasi Mono that is a filled cell
165 /// with four triangular counters, which reads as "this is not a character".
166 /// The cell still holds its columns, so a missing glyph costs the look of
167 /// the line and never its layout.
168 pub(crate) fn glyph_id_for(&mut self, c: char) -> (FontId, GlyphId) {
169 let id = self.face(PRIMARY).charmap().map(c);
170 if id != 0 {
171 if is_cell_furniture(c) {
172 self.furniture.insert((PRIMARY, id));
173 }
174 return (PRIMARY, id);
175 }
176 match self.resolve(c) {
177 Some(font) => {
178 let id = self.face(font).charmap().map(c);
179 if id == 0 {
180 (PRIMARY, 0)
181 } else {
182 if is_cell_furniture(c) {
183 self.furniture.insert((font, id));
184 }
185 (font, id)
186 }
187 }
188 None => (PRIMARY, 0),
189 }
190 }
191
192 /// Load (or reuse) the face the system names for `c`.
193 fn resolve(&mut self, c: char) -> Option<FontId> {
194 if let Some(known) = self.resolved.get(&c) {
195 return *known;
196 }
197 let found = self.load(c);
198 self.resolved.insert(c, found);
199 found
200 }
201
202 fn load(&mut self, c: char) -> Option<FontId> {
203 let m = self.fallback.find(c)?;
204 let key = (m.path.clone(), m.index);
205 if let Some(slot) = self.by_file.get(&key) {
206 return Some(*slot);
207 }
208 let data = std::fs::read(&m.path).ok()?;
209 let index = usize::try_from(m.index).unwrap_or(0);
210 let offset = FontRef::from_index(&data, index)?.offset;
211 let slot = FontId::try_from(self.faces.len()).ok()?;
212 self.faces.push(Face { data, offset });
213 self.by_file.insert(key, slot);
214 tracing::debug!(font = %m.path, slot, "loaded a fallback font");
215 Some(slot)
216 }
217
218 pub(crate) fn shape(&mut self, font: FontId, text: &str) -> Vec<ShapedGlyph> {
219 // Borrowed from the field rather than through `face()`, so the shared
220 // borrow of the font set and the exclusive one of the context stay on
221 // different fields and the borrow checker can see it.
222 let f = &self.faces[font as usize];
223 let face = FontRef {
224 data: &f.data,
225 offset: f.offset,
226 key: swash::CacheKey::new(),
227 };
228 let mut shaper = self
229 .shape_ctx
230 .builder(face)
231 .size(self.px)
232 .variations(&[("wght", self.weight)][..])
233 .build();
234 shaper.add_str(text);
235 let mut out = Vec::new();
236 shaper.shape_with(|cluster| {
237 for g in cluster.glyphs {
238 out.push(ShapedGlyph {
239 id: g.id,
240 advance: g.advance,
241 x_offset: g.x,
242 y_offset: g.y,
243 });
244 }
245 });
246 out
247 }
248
249 pub(crate) fn rasterize(&mut self, font: FontId, id: GlyphId) -> Option<Raster> {
250 let f = &self.faces[font as usize];
251 let face = FontRef {
252 data: &f.data,
253 offset: f.offset,
254 key: swash::CacheKey::new(),
255 };
256 let mut scaler = self
257 .scale_ctx
258 .builder(face)
259 .size(self.px)
260 .variations(&[("wght", self.weight)][..])
261 .hint(true)
262 .build();
263 let mut render = Render::new(&[Source::Outline]);
264 render.format(Format::Alpha);
265 // Furniture is drawn to the cell, not to its own advance. Scaled about
266 // the pen origin, which sits on the baseline at the cell's left edge,
267 // so the horizontal factor takes the bar out to both edges and the
268 // vertical one takes it to the top and bottom — given a baseline at
269 // `CellMetrics::baseline`, which is what `ascent` below reports.
270 if self.furniture.contains(&(font, id)) {
271 render.transform(Some(Transform::scale(self.snap.0, self.snap.1)));
272 }
273 let image = render.render(&mut scaler, id)?;
274 if image.content != Content::Mask {
275 return None;
276 }
277 Some(Raster {
278 bitmap: image.data,
279 width: image.placement.width,
280 height: image.placement.height,
281 placement_left: image.placement.left,
282 placement_top: image.placement.top,
283 })
284 }
285
286 /// The baseline, as a distance down from the top of the cell.
287 ///
288 /// Not the face's raw ascent: the two differ once the cell furniture is
289 /// snapped.
290 ///
291 /// One baseline for the whole grid, whatever font a given cell came from:
292 /// a fallback face with its own ascent would sit its glyphs on a different
293 /// line and make a row of mixed scripts wander.
294 ///
295 /// The cell's baseline rather than the face's raw ascent. The cell is the
296 /// line height rounded up, and putting the baseline proportionally inside
297 /// it shares that slack the way the face shares it rather than dropping all
298 /// of it under the descender. It is also the position the furniture snap is
299 /// derived against, so a full block reaches both edges of the cell exactly.
300 pub(crate) fn baseline(&self) -> f32 {
301 self.cell.baseline
302 }
303 }
304
305 /// `a / b`, or 1.0 when `b` is not a usable divisor.
306 ///
307 /// A face that measured to nothing would otherwise turn every glyph into a
308 /// division by zero; drawing it unsnapped is the harmless answer.
309 fn safe_ratio(a: f32, b: f32) -> f32 {
310 if b > f32::EPSILON { a / b } else { 1.0 }
311 }
312
313 #[cfg(test)]
314 mod tests {
315 use super::*;
316
317 const FONT: &[u8] = shop_font::FACE;
318
319 /// A shaper over the bundled face, drawing into the cell that face implies.
320 fn shaper(px: f32, cell: CellMetrics) -> Shaper {
321 Shaper::new(FONT.to_vec(), px, cell, shop_font::WEIGHT).expect("the bundled face loads")
322 }
323
324 /// The rasterized extent of `c`, as (left, right, top, bottom) in pixels
325 /// relative to the cell's top-left corner.
326 fn extent(shaper: &mut Shaper, c: char) -> (f32, f32, f32, f32) {
327 let (font, id) = shaper.glyph_id_for(c);
328 let baseline = shaper.baseline();
329 let r = shaper.rasterize(font, id).expect("the glyph rasterizes");
330 let left = r.placement_left as f32;
331 let top = baseline - r.placement_top as f32;
332 (left, left + r.width as f32, top, top + r.height as f32)
333 }
334
335 // The whole point. A full block has to ink the entire cell, or every row
336 // and every column of a filled region shows a hairline of background.
337 #[test]
338 fn a_full_block_inks_the_whole_cell() {
339 let cell = CellMetrics::measure(FONT, 14.0).unwrap();
340 let mut shaper = shaper(14.0, cell);
341 let (x0, x1, y0, y1) = extent(&mut shaper, '');
342 assert!(x0 <= 0.5, "left edge at {x0}");
343 assert!(
344 x1 >= cell.advance - 0.5,
345 "right edge at {x1} of {}",
346 cell.advance
347 );
348 assert!(y0 <= 0.5, "top edge at {y0}");
349 assert!(
350 y1 >= cell.height - 0.5,
351 "bottom edge at {y1} of {}",
352 cell.height
353 );
354 }
355
356 // The snap is doing real work and almost none of it, which is what makes
357 // it safe. The block ends up taller than the face's own line box, by the
358 // half pixel the cell rounding added, and the factor that did it is well
359 // inside a pixel — so nothing on screen is visibly distorted.
360 #[test]
361 fn the_snap_closes_the_rounding_and_nothing_more() {
362 let cell = CellMetrics::measure(FONT, 14.0).unwrap();
363 let mut shaper = shaper(14.0, cell);
364 let (sx, sy) = shaper.snap;
365 assert!(
366 (sx - 1.0).abs() < 0.05 && (sy - 1.0).abs() < 0.05,
367 "the snap is stretching by ({sx}, {sy}), which would be visible"
368 );
369 let (_, _, y0, y1) = extent(&mut shaper, '\u{2588}');
370 let inked = y1 - y0;
371 assert!(
372 inked > cell.exact_height,
373 "the block inks {inked}, which is no more than the face's own {} \
374 line box — so the cell's extra {} is still background",
375 cell.exact_height,
376 cell.height - cell.exact_height
377 );
378 }
379
380 // A rule has to reach both edges, or a horizontal border is a dashed line.
381 #[test]
382 fn a_horizontal_rule_reaches_both_edges_of_the_cell() {
383 let cell = CellMetrics::measure(FONT, 14.0).unwrap();
384 let mut shaper = shaper(14.0, cell);
385 let (x0, x1, _, _) = extent(&mut shaper, '');
386 assert!(x0 <= 0.5 && x1 >= cell.advance - 0.5, "spans {x0}..{x1}");
387 }
388
389 // And a vertical one has to reach the top and the bottom, which is the
390 // half-pixel the cell rounding created.
391 #[test]
392 fn a_vertical_rule_reaches_the_top_and_bottom_of_the_cell() {
393 let cell = CellMetrics::measure(FONT, 14.0).unwrap();
394 let mut shaper = shaper(14.0, cell);
395 let (_, _, y0, y1) = extent(&mut shaper, '');
396 assert!(y0 <= 0.5, "top at {y0}");
397 assert!(y1 >= cell.height - 0.5, "bottom at {y1} of {}", cell.height);
398 }
399
400 // Text is not furniture and must not be stretched: a letter scaled to the
401 // cell would be a different typeface.
402 #[test]
403 fn a_letter_is_left_alone() {
404 let cell = CellMetrics::measure(FONT, 14.0).unwrap();
405 let mut shaper = shaper(14.0, cell);
406 let (_, _, y0, y1) = extent(&mut shaper, 'x');
407 assert!(y0 > 1.0 && y1 < cell.height - 1.0, "`x` spans {y0}..{y1}");
408 }
409
410 // The case the snap was built for: a size where the advance is not a whole
411 // number of pixels, so the cell rounds up and the difference would be
412 // background down every column — the same shape of defect as the hardcoded
413 // 8.0, arrived at honestly.
414 //
415 // With the face shop bundles today this is not a special case but the only
416 // case: Quasi Mono's advance is 79/125 em and 79 is prime, so it lands
417 // whole at 125px and nowhere usable. IosevkaTerm was 1/2 and did it at
418 // every even size, which is why this test used to have to reach for 15px.
419 #[test]
420 fn a_size_where_the_advance_is_fractional_still_tiles() {
421 let cell = CellMetrics::measure(FONT, 15.0).unwrap();
422 assert!(
423 (cell.exact_advance - cell.exact_advance.floor()).abs() > 0.01,
424 "advance {} is whole, so this test is not testing the snap",
425 cell.exact_advance
426 );
427 assert!((cell.advance - cell.exact_advance.ceil()).abs() < f32::EPSILON);
428
429 let mut shaper = shaper(15.0, cell);
430 let (x0, x1, y0, y1) = extent(&mut shaper, '\u{2588}');
431 assert!(x0 <= 0.5 && x1 >= cell.advance - 0.5, "spans {x0}..{x1}");
432 assert!(y0 <= 0.5 && y1 >= cell.height - 0.5, "spans {y0}..{y1}");
433 }
434
435 #[test]
436 fn the_ranges_that_snap_are_the_ones_that_meet_their_neighbours() {
437 for c in ['', '', '', '', '', '', '', '\u{E0B0}', '\u{1FB00}'] {
438 assert!(is_cell_furniture(c), "{c} should snap");
439 }
440 for c in ['a', 'M', ' ', '', '', ''] {
441 assert!(!is_cell_furniture(c), "{c} should not snap");
442 }
443 }
444 }
445