From 568f25fa9f2b2e50097bb1f76499f24355b18fa1 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Thu, 12 Nov 2026 06:27:53 -0500 Subject: [PATCH] Fonts: cap decoded names, bound outline flattening, cache fallback coverage * fonts: cap decoded names, outline flatten tolerance by size, coverage cache for system fallback * Apply review fixes - crates/fonts/src/fontdb/system.rs - crates/fonts/src/fontdb/names.rs * Apply review fixes - crates/fonts/src/fontdb/system.rs * address review: cap flattened edges in has_serifs * address review: check alias blankness before capping --------- Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> --- crates/fonts/src/fontdb/fallback.rs | 9 +++ crates/fonts/src/fontdb/names.rs | 40 +++++++++++++- crates/fonts/src/fontdb/outline.rs | 26 ++++++++- crates/fonts/src/fontdb/system.rs | 85 +++++++++++++++++++++++++++-- 4 files changed, 150 insertions(+), 10 deletions(-) diff --git a/crates/fonts/src/fontdb/fallback.rs b/crates/fonts/src/fontdb/fallback.rs index 73a2e0a25..7291cb343 100644 --- a/crates/fonts/src/fontdb/fallback.rs +++ b/crates/fonts/src/fontdb/fallback.rs @@ -10,8 +10,17 @@ pub(super) struct SysFallback { pub(super) misses: std::collections::HashSet, /// CJK chain families already looked up in the catalog (loaded, or not installed). pub(super) chain_tried: std::collections::HashSet<&'static str>, + /// The code points each installed file covers (as ranges), by path, valid for the file's + /// stamp: one character after another is looked up without reading every file again. + pub(super) coverage: std::collections::HashMap>)>, + /// Ranges held in `coverage` (it stops growing at [`MAX_COVERAGE_RANGES`]). + pub(super) coverage_ranges: usize, } +/// Most coverage ranges kept across all files (8 bytes each). +#[cfg(not(target_arch = "wasm32"))] +pub(super) const MAX_COVERAGE_RANGES: usize = 4 * 1024 * 1024; + #[cfg(all(test, target_os = "linux"))] pub(super) struct FallbackSnapshotHook { pub(super) reached: std::sync::mpsc::SyncSender<()>, diff --git a/crates/fonts/src/fontdb/names.rs b/crates/fonts/src/fontdb/names.rs index af5f14cc3..6c2610824 100644 --- a/crates/fonts/src/fontdb/names.rs +++ b/crates/fonts/src/fontdb/names.rs @@ -3,16 +3,42 @@ use super::face::FontFace; use super::system::CatalogEntry; use super::{Arc, MetadataProvider, StringId}; +/// Characters kept of one decoded name: a name record can be 64 KB, and a font has many of them, +/// kept for every face (a real family or style name is far shorter). +const MAX_NAME_CHARS: usize = 256; + +fn capped(name: impl std::fmt::Display) -> String { + use std::fmt::Write as _; + let mut out = String::new(); + // Stops decoding at the cap: the name is never built in full. + let mut room = MAX_NAME_CHARS; + struct Cap<'a>(&'a mut String, &'a mut usize); + impl std::fmt::Write for Cap<'_> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + for c in s.chars() { + if *self.1 == 0 { + return Err(std::fmt::Error); + } + *self.1 -= 1; + self.0.push(c); + } + Ok(()) + } + } + let _ = write!(Cap(&mut out, &mut room), "{name}"); + out +} + pub(super) fn aliases(font: &skrifa::FontRef<'_>, ids: &[StringId]) -> Vec { let mut names: Vec = - ids.iter().flat_map(|id| font.localized_strings(*id)).take(256).map(|name| name.to_string()).filter(|name| !name.trim().is_empty()).collect(); + ids.iter().flat_map(|id| font.localized_strings(*id)).take(256).filter(|name| !name.chars().all(char::is_whitespace)).map(capped).collect(); names.sort(); names.dedup(); names } pub(super) fn name(font: &skrifa::FontRef<'_>, ids: &[StringId]) -> Option { - ids.iter().find_map(|id| font.localized_strings(*id).english_or_first().map(|s| s.to_string()).filter(|s| !s.is_empty())) + ids.iter().find_map(|id| font.localized_strings(*id).english_or_first().map(capped).filter(|s| !s.is_empty())) } /// A face's family and style names (the default instance's style for a variable font). @@ -116,3 +142,13 @@ pub(super) fn style_italic(style: &str) -> bool { let s = norm(style); s.contains("italic") || s.contains("oblique") || s == "it" } + +#[cfg(test)] +mod tests { + #[test] + fn decoded_names_are_capped() { + assert_eq!(super::capped("Arial"), "Arial"); + let long = "é".repeat(10_000); + assert_eq!(super::capped(&long).chars().count(), super::MAX_NAME_CHARS); + } +} diff --git a/crates/fonts/src/fontdb/outline.rs b/crates/fonts/src/fontdb/outline.rs index 30e328be4..0b07f3b3f 100644 --- a/crates/fonts/src/fontdb/outline.rs +++ b/crates/fonts/src/fontdb/outline.rs @@ -11,28 +11,48 @@ pub(super) fn has_serifs(face: &FontFace) -> Option { if bbox.height().is_nan() || bbox.height() <= 1.0 { return None; } + // Flattened segments kept: an "l" has a few dozen; a hostile outline has no such limit of its own. + const MAX_EDGES: usize = 1 << 20; let mut edges = Vec::new(); + let mut over = false; let mut last = None; let mut start = None; - kurbo::flatten(&path, 1.0, |el| match el { + // A tolerance of one unit is fine for outlines of normal size; a transform that blows one up + // gets a tolerance in proportion, so the subdivision stays bounded (4096 units stay at 1). + let tol = (bbox.width().max(bbox.height()) / 4096.0).max(1.0); + if !tol.is_finite() { + return None; + } + kurbo::flatten(&path, tol, |el| match el { kurbo::PathEl::MoveTo(p) => { last = Some(p); start = Some(p); } kurbo::PathEl::LineTo(p) => { if let Some(a) = last { - edges.push((a, p)); + if edges.len() < MAX_EDGES { + edges.push((a, p)); + } else { + over = true; + } } last = Some(p); } kurbo::PathEl::ClosePath => { if let (Some(a), Some(b)) = (last, start) { - edges.push((a, b)); + if edges.len() < MAX_EDGES { + edges.push((a, b)); + } else { + over = true; + } } last = start; } _ => {} }); + if over { + return None; + } // The outline is y-down: the foot is near bbox.y1 (the baseline). let width_at = |y: f64| { let xs: Vec = edges diff --git a/crates/fonts/src/fontdb/system.rs b/crates/fonts/src/fontdb/system.rs index 2a367b894..84916807e 100644 --- a/crates/fonts/src/fontdb/system.rs +++ b/crates/fonts/src/fontdb/system.rs @@ -38,6 +38,60 @@ fn file_stamp(path: &std::path::Path) -> Option { std::fs::metadata(path).ok().map(|m| (m.len(), m.modified().ok())) } +/// The code points any face of a font file maps, as sorted inclusive ranges. +#[cfg(not(target_arch = "wasm32"))] +fn coverage_ranges(data: &[u8]) -> Vec<(u32, u32)> { + // One bit per code point: memory stays fixed however many faces (or named instances of one face) map however much. + let mut bits = vec![0u64; 0x110000 / 64]; + let mut last = None; + for (i, _, _, _) in enumerate_faces(data) { + if last.replace(i) == Some(i) { + continue; + } + if let Ok(f) = skrifa::FontRef::from_index(data, i) { + for (cp, _) in f.charmap().mappings() { + if let Some(w) = bits.get_mut((cp / 64) as usize) { + *w |= 1u64 << (cp % 64); + } + } + } + } + let mut out: Vec<(u32, u32)> = Vec::new(); + for (wi, &w) in bits.iter().enumerate() { + if w == 0 { + continue; + } + for b in 0..64u32 { + if (w >> b) & 1 == 0 { + continue; + } + let cp = wi as u32 * 64 + b; + match out.last_mut() { + Some(last) if last.1 + 1 == cp => last.1 = cp, + _ => out.push((cp, cp)), + } + } + } + out +} + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod coverage_tests { + #[test] + fn coverage_ranges_match_the_charmap() { + let data = include_bytes!("../../../../assets/fonts/SourceSans3-Regular.ttf"); + let ranges = super::coverage_ranges(data); + let f = skrifa::FontRef::new(data).unwrap(); + use skrifa::MetadataProvider as _; + let in_ranges = |c: char| ranges.iter().any(|&(lo, hi)| (lo..=hi).contains(&u32::from(c))); + assert!(in_ranges('a') && !in_ranges('\u{10FFFE}')); + for c in ['a', 'Z', 'é', '\u{4E2D}', '\u{1F600}'] { + assert_eq!(in_ranges(c), f.charmap().map(c).is_some(), "{c:?}"); + } + assert!(ranges.windows(2).all(|w| w[0].1 + 1 < w[1].0), "sorted and merged"); + } +} + /// What [`FontDb::reload_fonts`] did: the faces cataloged by the rescan, and the installed font /// files that changed or were removed since they were read (their faces are read again, from the /// new file, the next time they are looked up). @@ -272,6 +326,8 @@ impl FontDb { sys.generation = sys.generation.wrapping_add(1); sys.misses.clear(); sys.chain_tried.clear(); + sys.coverage.clear(); + sys.coverage_ranges = 0; count } @@ -564,13 +620,32 @@ impl FontDb { } } for p in paths { - if std::fs::metadata(&p).map(|m| m.len() > 40 << 20).unwrap_or(true) { + let Some(stamp) = file_stamp(&p).filter(|s| s.0 <= 40 << 20) else { continue }; + let cached = + self.sys.lock().unwrap_or_else(PoisonError::into_inner).coverage.get(&p).filter(|(s, _)| *s == stamp).map(|(_, r)| r.clone()); + let mut data = None; + let ranges = match cached { + Some(r) => r, + None => { + let Some(d) = self.read_installed(&p) else { continue }; + let r = Arc::new(coverage_ranges(&d)); + let mut sys = self.sys.lock().unwrap_or_else(PoisonError::into_inner); + if sys.coverage_ranges + r.len() <= super::fallback::MAX_COVERAGE_RANGES { + sys.coverage_ranges += r.len(); + if let Some((_, old)) = sys.coverage.insert(p.clone(), (stamp, r.clone())) { + sys.coverage_ranges -= old.len(); + } + } + data = Some(d); + r + } + }; + let cp = u32::from(c); + if !ranges.iter().any(|&(lo, hi)| (lo..=hi).contains(&cp)) { continue; } - let Some(data) = self.read_installed(&p) else { continue }; - let hit = - enumerate_faces(&data).iter().any(|(i, _, _, _)| skrifa::FontRef::from_index(&data, *i).is_ok_and(|f| f.charmap().map(c).is_some())); - if hit && self.add_faces(data, FontSource::Installed(p)) > 0 && covered(self) { + let Some(data) = data.or_else(|| self.read_installed(&p)) else { continue }; + if self.add_faces(data, FontSource::Installed(p)) > 0 && covered(self) { return true; } }