forked from 1b-agent/opencadstudio-upstream
feat(web): font_register control op for host-supplied font files
The web build has no font folder, so a text style whose TrueType file is
not a bundled Noto subset falls back to it. font_register takes the file
from the host page:
{"op":"font_register","file_name":"Brand-Regular.ttf","data_base64":"…"}
The faces join the font database, file_name resolves to the family of
the first face (case-insensitive, without directory, like installed
fonts), and open drawings lay their text out again (fallback glyph
cache cleared, text geometry invalidated per tab). The reply names the
family and the number of faces. Bytes without a face fail with
font_invalid. SysFonts::by_file becomes updatable for this.
Proposed in #1771.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
acb8c5ea54
commit
0998cb9e88
3 files changed
+183
-5
No files matched your search
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user