mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
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>
This commit is contained in:
1 parent
665fe2a8e4
commit
568f25fa9f
4 files changed
+150
-10
No files matched your search
@@ -10,8 +10,17 @@ pub(super) struct SysFallback {
|
||||
pub(super) misses: std::collections::HashSet<char>,
|
||||
/// 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<std::path::PathBuf, (super::system::FileStamp, std::sync::Arc<Vec<(u32, u32)>>)>,
|
||||
/// 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<()>,
|
||||
|
||||
@@ -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<String> {
|
||||
let mut names: Vec<String> =
|
||||
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<String> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -11,28 +11,48 @@ pub(super) fn has_serifs(face: &FontFace) -> Option<bool> {
|
||||
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<f64> = edges
|
||||
|
||||
@@ -38,6 +38,60 @@ fn file_stamp(path: &std::path::Path) -> Option<FileStamp> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user