Merge pull request #1774 from megabrain-agent/feat/font-register

feat(web): font_register control op for host-supplied font files
This commit is contained in:
Hakan Seven committed 2026-10-14 09:11:06 +03:00
commit 592289f4fd
3 files changed
+183 -5

No files matched your search

+70 -4
View File
@@ -22,8 +22,9 @@ struct SysFonts {
families: Mutex<Vec<String>>,
/// Installed face file name (lower-case) → the family that face holds.
/// A drawing references a TrueType style by *file* name, so this is what
/// makes such a style resolve — see [`family_for_reference`].
by_file: HashMap<String, String>,
/// makes such a style resolve — see [`family_for_reference`]. Files a host
/// registers at run time join it ([`register_font_file`]).
by_file: Mutex<HashMap<String, String>>,
}
static FONTS: OnceLock<SysFonts> = OnceLock::new();
@@ -60,7 +61,7 @@ fn fonts() -> &'static SysFonts {
SysFonts {
db: Mutex::new(db),
families: Mutex::new(families),
by_file,
by_file: Mutex::new(by_file),
}
})
}
@@ -320,7 +321,12 @@ fn family_for_file(name: &str) -> Option<String> {
if file.is_empty() {
return None;
}
fonts().by_file.get(&file.to_ascii_lowercase()).cloned()
fonts()
.by_file
.lock()
.unwrap()
.get(&file.to_ascii_lowercase())
.cloned()
}
/// The installed family a drawing's font reference names, or `None` when
@@ -371,10 +377,70 @@ pub fn register_web_font(bytes: &[u8]) {
canonical_cache().lock().unwrap().clear();
}
/// Add a font file a host supplies at run time (the `font_register` control
/// op): its faces join the database and `file_name` resolves to the family of
/// the first face, so a text style naming that file finds it. Returns that
/// family and the number of faces the file added, or `None` when the bytes
/// hold no face fontdb can read. Like [`register_web_font`], the family list
/// is rebuilt and the canonical-name memo cleared; callers invalidate the glyph
/// caches and re-layout text.
pub fn register_font_file(file_name: &str, bytes: &[u8]) -> Option<(String, usize)> {
let file = file_name.rsplit(['/', '\\']).next()?.trim();
if file.is_empty() {
return None;
}
let sys = fonts();
let (family, faces, names) = {
let mut db = sys.db.lock().unwrap();
let before: std::collections::HashSet<fontdb::ID> = db.faces().map(|f| f.id).collect();
db.load_font_data(bytes.to_vec());
let added: Vec<String> = db
.faces()
.filter(|face| !before.contains(&face.id))
.filter_map(|face| face.families.first().map(|(name, _)| name.clone()))
.collect();
let family = added.first()?.clone();
(family, added.len(), family_names(&db))
};
sys.by_file
.lock()
.unwrap()
.insert(file.to_ascii_lowercase(), family.clone());
*sys.families.lock().unwrap() = names;
canonical_cache().lock().unwrap().clear();
Some((family, faces))
}
#[cfg(test)]
mod tests {
use super::*;
/// A host-registered file resolves by its file name — any case, bare or
/// with a path — to the family inside it; bytes without a face are refused
/// and leave nothing behind.
#[test]
fn registered_font_file_resolves_by_file_name() {
let bytes = std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/web/fonts/greek.ttf"))
.expect("bundled web font");
let (family, faces) =
register_font_file("C:\\Fonts\\HostRegistered-Test.ttf", &bytes).expect("a face");
assert!(faces >= 1);
assert!(!family.is_empty());
assert_eq!(
family_for_reference("hostregistered-test.TTF").as_deref(),
Some(family.as_str())
);
assert_eq!(
family_for_reference("fonts/HostRegistered-Test.ttf").as_deref(),
Some(family.as_str())
);
assert!(has_family(&family));
assert_eq!(register_font_file("Broken-Test.ttf", b"not a font"), None);
assert_eq!(family_for_reference("Broken-Test.ttf"), None);
assert_eq!(register_font_file("", &bytes), None);
}
/// A DWG stores a style's TrueType font as a file name, so every installed
/// face has to be reachable by the name of its file — case-insensitively,
/// and whether it comes as a bare name or as a path.