//! Swash wrapper: parse fonts, shape a cluster, rasterize a glyph. //! //! Holds a SET of fonts, not one. Slot 0 is the bundled font and answers for //! almost everything; the rest arrive one at a time from [`Fallback`] when a //! character turns up that slot 0 does not have. A glyph is therefore //! identified by (font, glyph id) rather than by glyph id, since ids only mean //! something inside the font that issued them. use std::collections::HashMap; use std::collections::HashSet; use swash::{ FontRef, GlyphId, scale::{Render, ScaleContext, Source, image::Content}, shape::ShapeContext, zeno::{Format, Transform}, }; use crate::fallback::Fallback; use crate::metrics::CellMetrics; /// Characters drawn to meet their neighbours rather than to sit inside their /// own cell. /// /// Box drawing and block elements are the obvious ones: a border seams if the /// bar does not reach both edges. Braille is here because a `Canvas` or a /// sparkline paints one picture across many cells. The legacy-computing /// sextants are blocks by another name. And the four powerline separators are /// the classic visible case — a prompt with a hairline of background showing /// through every chevron. /// /// Over-including is cheap and under-including is not: a glyph that did not /// need snapping is scaled by well under a pixel, which nobody can see, while /// a glyph that needed it and did not get it leaves a seam on every row. pub(crate) fn is_cell_furniture(c: char) -> bool { matches!(c, '\u{2500}'..='\u{259F}' // box drawing, block elements | '\u{2800}'..='\u{28FF}' // braille | '\u{E0B0}'..='\u{E0B3}' // powerline separators | '\u{1FB00}'..='\u{1FBFF}' // legacy computing ) } /// Which font in the set. 0 is always the bundled one. pub(crate) type FontId = u16; pub(crate) const PRIMARY: FontId = 0; struct Face { data: Vec, offset: u32, } pub(crate) struct Shaper { faces: Vec, /// Which face has a character, remembered including the misses: a /// character nothing has is a fontconfig query we only want to run once. resolved: HashMap>, /// Font file to the slot already holding it, so one fallback face serves /// every character it covers rather than being loaded per character. by_file: HashMap<(String, i32), FontId>, fallback: Fallback, scale_ctx: ScaleContext, shape_ctx: ShapeContext, px: f32, /// Where on the primary face's weight axis to draw. /// /// Named rather than inherited, because a variable face's default instance /// is whatever its designer set and is not always the weight anyone wants: /// Quasi Mono is cut from a base whose default is `wght` 200, so a terminal /// that took the file's own default would draw every prompt in ExtraLight. /// /// Applied to every face, not only the primary one. A static fallback has /// no axes and swash drops the setting, so this needs no branch. weight: f32, /// The cell this shaper is drawing into, in physical pixels. cell: CellMetrics, /// Scale that maps the face's own cell onto the pixel one, applied to /// [`is_cell_furniture`] glyphs at rasterization. /// /// Both factors are within a pixel of 1, because the pixel cell is the /// face's own measurements rounded. That is the whole trick: the distortion /// is too small to see and it is exactly enough to close the seam. snap: (f32, f32), /// Glyph ids known to be furniture, learned as characters are resolved. /// /// Kept by id rather than re-derived from a character, because /// rasterization is reached with a glyph id and a font and nothing else — /// which is also what the atlas cache is keyed on, so a glyph is snapped or /// not for its whole life and never both. furniture: HashSet<(FontId, GlyphId)>, } pub(crate) struct ShapedGlyph { pub(crate) id: GlyphId, /// Pen advance in pixels after this glyph. pub(crate) advance: f32, /// Position offsets relative to the pen origin. pub(crate) x_offset: f32, pub(crate) y_offset: f32, } pub(crate) struct Raster { pub(crate) bitmap: Vec, pub(crate) width: u32, pub(crate) height: u32, /// Left edge relative to the pen origin (positive = right of origin). pub(crate) placement_left: i32, /// Top edge relative to the baseline (positive = above baseline). pub(crate) placement_top: i32, } impl Shaper { pub(crate) fn new( font_data: Vec, px: f32, cell: CellMetrics, weight: f32, ) -> anyhow::Result { let font = FontRef::from_index(&font_data, 0) .ok_or_else(|| anyhow::anyhow!("swash: not a font"))?; let font_offset = font.offset; // Measured here rather than taken from `cell`, and the difference is // the point: `cell` is the grid's step in physical pixels, which is a // logical cell times an integer scale, while this is what the face // draws at the size this shaper rasterizes at. Snapping between the two // absorbs the gap instead of leaving it on screen. let own = CellMetrics::measure(&font_data, px)?; let snap = ( safe_ratio(cell.advance, own.exact_advance), safe_ratio(cell.height, own.exact_height), ); Ok(Self { faces: vec![Face { data: font_data, offset: font_offset, }], resolved: HashMap::new(), by_file: HashMap::new(), fallback: Fallback::new(), scale_ctx: ScaleContext::new(), shape_ctx: ShapeContext::new(), px, weight, cell, snap, furniture: HashSet::new(), }) } fn face(&self, id: FontId) -> FontRef<'_> { let f = &self.faces[id as usize]; FontRef { data: &f.data, offset: f.offset, key: swash::CacheKey::new(), } } /// Char → the font that has it and its glyph id there. /// /// Falls back to `(PRIMARY, 0)` when nothing has it, which draws whatever /// the bundled font puts at `.notdef`. On Quasi Mono that is a filled cell /// with four triangular counters, which reads as "this is not a character". /// The cell still holds its columns, so a missing glyph costs the look of /// the line and never its layout. pub(crate) fn glyph_id_for(&mut self, c: char) -> (FontId, GlyphId) { let id = self.face(PRIMARY).charmap().map(c); if id != 0 { if is_cell_furniture(c) { self.furniture.insert((PRIMARY, id)); } return (PRIMARY, id); } match self.resolve(c) { Some(font) => { let id = self.face(font).charmap().map(c); if id == 0 { (PRIMARY, 0) } else { if is_cell_furniture(c) { self.furniture.insert((font, id)); } (font, id) } } None => (PRIMARY, 0), } } /// Load (or reuse) the face the system names for `c`. fn resolve(&mut self, c: char) -> Option { if let Some(known) = self.resolved.get(&c) { return *known; } let found = self.load(c); self.resolved.insert(c, found); found } fn load(&mut self, c: char) -> Option { let m = self.fallback.find(c)?; let key = (m.path.clone(), m.index); if let Some(slot) = self.by_file.get(&key) { return Some(*slot); } let data = std::fs::read(&m.path).ok()?; let index = usize::try_from(m.index).unwrap_or(0); let offset = FontRef::from_index(&data, index)?.offset; let slot = FontId::try_from(self.faces.len()).ok()?; self.faces.push(Face { data, offset }); self.by_file.insert(key, slot); tracing::debug!(font = %m.path, slot, "loaded a fallback font"); Some(slot) } pub(crate) fn shape(&mut self, font: FontId, text: &str) -> Vec { // Borrowed from the field rather than through `face()`, so the shared // borrow of the font set and the exclusive one of the context stay on // different fields and the borrow checker can see it. let f = &self.faces[font as usize]; let face = FontRef { data: &f.data, offset: f.offset, key: swash::CacheKey::new(), }; let mut shaper = self .shape_ctx .builder(face) .size(self.px) .variations(&[("wght", self.weight)][..]) .build(); shaper.add_str(text); let mut out = Vec::new(); shaper.shape_with(|cluster| { for g in cluster.glyphs { out.push(ShapedGlyph { id: g.id, advance: g.advance, x_offset: g.x, y_offset: g.y, }); } }); out } pub(crate) fn rasterize(&mut self, font: FontId, id: GlyphId) -> Option { let f = &self.faces[font as usize]; let face = FontRef { data: &f.data, offset: f.offset, key: swash::CacheKey::new(), }; let mut scaler = self .scale_ctx .builder(face) .size(self.px) .variations(&[("wght", self.weight)][..]) .hint(true) .build(); let mut render = Render::new(&[Source::Outline]); render.format(Format::Alpha); // Furniture is drawn to the cell, not to its own advance. Scaled about // the pen origin, which sits on the baseline at the cell's left edge, // so the horizontal factor takes the bar out to both edges and the // vertical one takes it to the top and bottom — given a baseline at // `CellMetrics::baseline`, which is what `ascent` below reports. if self.furniture.contains(&(font, id)) { render.transform(Some(Transform::scale(self.snap.0, self.snap.1))); } let image = render.render(&mut scaler, id)?; if image.content != Content::Mask { return None; } Some(Raster { bitmap: image.data, width: image.placement.width, height: image.placement.height, placement_left: image.placement.left, placement_top: image.placement.top, }) } /// The baseline, as a distance down from the top of the cell. /// /// Not the face's raw ascent: the two differ once the cell furniture is /// snapped. /// /// One baseline for the whole grid, whatever font a given cell came from: /// a fallback face with its own ascent would sit its glyphs on a different /// line and make a row of mixed scripts wander. /// /// The cell's baseline rather than the face's raw ascent. The cell is the /// line height rounded up, and putting the baseline proportionally inside /// it shares that slack the way the face shares it rather than dropping all /// of it under the descender. It is also the position the furniture snap is /// derived against, so a full block reaches both edges of the cell exactly. pub(crate) fn baseline(&self) -> f32 { self.cell.baseline } } /// `a / b`, or 1.0 when `b` is not a usable divisor. /// /// A face that measured to nothing would otherwise turn every glyph into a /// division by zero; drawing it unsnapped is the harmless answer. fn safe_ratio(a: f32, b: f32) -> f32 { if b > f32::EPSILON { a / b } else { 1.0 } } #[cfg(test)] mod tests { use super::*; const FONT: &[u8] = shop_font::FACE; /// A shaper over the bundled face, drawing into the cell that face implies. fn shaper(px: f32, cell: CellMetrics) -> Shaper { Shaper::new(FONT.to_vec(), px, cell, shop_font::WEIGHT).expect("the bundled face loads") } /// The rasterized extent of `c`, as (left, right, top, bottom) in pixels /// relative to the cell's top-left corner. fn extent(shaper: &mut Shaper, c: char) -> (f32, f32, f32, f32) { let (font, id) = shaper.glyph_id_for(c); let baseline = shaper.baseline(); let r = shaper.rasterize(font, id).expect("the glyph rasterizes"); let left = r.placement_left as f32; let top = baseline - r.placement_top as f32; (left, left + r.width as f32, top, top + r.height as f32) } // The whole point. A full block has to ink the entire cell, or every row // and every column of a filled region shows a hairline of background. #[test] fn a_full_block_inks_the_whole_cell() { let cell = CellMetrics::measure(FONT, 14.0).unwrap(); let mut shaper = shaper(14.0, cell); let (x0, x1, y0, y1) = extent(&mut shaper, '█'); assert!(x0 <= 0.5, "left edge at {x0}"); assert!( x1 >= cell.advance - 0.5, "right edge at {x1} of {}", cell.advance ); assert!(y0 <= 0.5, "top edge at {y0}"); assert!( y1 >= cell.height - 0.5, "bottom edge at {y1} of {}", cell.height ); } // The snap is doing real work and almost none of it, which is what makes // it safe. The block ends up taller than the face's own line box, by the // half pixel the cell rounding added, and the factor that did it is well // inside a pixel — so nothing on screen is visibly distorted. #[test] fn the_snap_closes_the_rounding_and_nothing_more() { let cell = CellMetrics::measure(FONT, 14.0).unwrap(); let mut shaper = shaper(14.0, cell); let (sx, sy) = shaper.snap; assert!( (sx - 1.0).abs() < 0.05 && (sy - 1.0).abs() < 0.05, "the snap is stretching by ({sx}, {sy}), which would be visible" ); let (_, _, y0, y1) = extent(&mut shaper, '\u{2588}'); let inked = y1 - y0; assert!( inked > cell.exact_height, "the block inks {inked}, which is no more than the face's own {} \ line box — so the cell's extra {} is still background", cell.exact_height, cell.height - cell.exact_height ); } // A rule has to reach both edges, or a horizontal border is a dashed line. #[test] fn a_horizontal_rule_reaches_both_edges_of_the_cell() { let cell = CellMetrics::measure(FONT, 14.0).unwrap(); let mut shaper = shaper(14.0, cell); let (x0, x1, _, _) = extent(&mut shaper, '─'); assert!(x0 <= 0.5 && x1 >= cell.advance - 0.5, "spans {x0}..{x1}"); } // And a vertical one has to reach the top and the bottom, which is the // half-pixel the cell rounding created. #[test] fn a_vertical_rule_reaches_the_top_and_bottom_of_the_cell() { let cell = CellMetrics::measure(FONT, 14.0).unwrap(); let mut shaper = shaper(14.0, cell); let (_, _, y0, y1) = extent(&mut shaper, '│'); assert!(y0 <= 0.5, "top at {y0}"); assert!(y1 >= cell.height - 0.5, "bottom at {y1} of {}", cell.height); } // Text is not furniture and must not be stretched: a letter scaled to the // cell would be a different typeface. #[test] fn a_letter_is_left_alone() { let cell = CellMetrics::measure(FONT, 14.0).unwrap(); let mut shaper = shaper(14.0, cell); let (_, _, y0, y1) = extent(&mut shaper, 'x'); assert!(y0 > 1.0 && y1 < cell.height - 1.0, "`x` spans {y0}..{y1}"); } // The case the snap was built for: a size where the advance is not a whole // number of pixels, so the cell rounds up and the difference would be // background down every column — the same shape of defect as the hardcoded // 8.0, arrived at honestly. // // With the face shop bundles today this is not a special case but the only // case: Quasi Mono's advance is 79/125 em and 79 is prime, so it lands // whole at 125px and nowhere usable. IosevkaTerm was 1/2 and did it at // every even size, which is why this test used to have to reach for 15px. #[test] fn a_size_where_the_advance_is_fractional_still_tiles() { let cell = CellMetrics::measure(FONT, 15.0).unwrap(); assert!( (cell.exact_advance - cell.exact_advance.floor()).abs() > 0.01, "advance {} is whole, so this test is not testing the snap", cell.exact_advance ); assert!((cell.advance - cell.exact_advance.ceil()).abs() < f32::EPSILON); let mut shaper = shaper(15.0, cell); let (x0, x1, y0, y1) = extent(&mut shaper, '\u{2588}'); assert!(x0 <= 0.5 && x1 >= cell.advance - 0.5, "spans {x0}..{x1}"); assert!(y0 <= 0.5 && y1 >= cell.height - 0.5, "spans {y0}..{y1}"); } #[test] fn the_ranges_that_snap_are_the_ones_that_meet_their_neighbours() { for c in ['─', '┼', '╬', '█', '▄', '░', '⠿', '\u{E0B0}', '\u{1FB00}'] { assert!(is_cell_furniture(c), "{c} should snap"); } for c in ['a', 'M', ' ', '★', '→', '✘'] { assert!(!is_cell_furniture(c), "{c} should not snap"); } } }