feat(fonts): A family's kind without keeping it loaded

ScopedFonts::family_kind reads a family's format, class, PANOSE and file
date from its Regular face. A loaded face is asked; an installed family
that isn't loaded is read from its file, which is let go afterwards,
instead of loading every family the font menu's filters look at for the
rest of the session. Files over MAX_DOCUMENT_FONT_BYTES are skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
moppeeer 2026-10-12 00:13:57 +02:00 committed by paaspaas00
parent 03a8d4b955
commit 6baa57b503
2 changed files with 54 additions and 0 deletions

View File

@ -1306,6 +1306,43 @@ impl ScopedFonts<'_> {
None
}
/// `family`'s kind (format, class, PANOSE) and when its file was added, read from its Regular
/// face (else its first), for the font menus' filters, which look at every family. A face
/// already loaded is asked; an installed family that isn't loaded is read from its file, which
/// is then let go rather than kept loaded. None for an unknown family or an unreadable file.
pub fn family_kind(&self, family: &str) -> Option<(crate::kind::FaceKind, Option<std::time::SystemTime>)> {
let styles = self.styles(family);
let style = styles.iter().find(|s| *s == "Regular").or(styles.first())?.clone();
if self.own().iter().chain(self.db.read_faces().iter()).any(|f| f.family.eq_ignore_ascii_case(family)) {
let f = self.find(family, &style)?;
return Some((f.kind(), f.added()));
}
#[cfg(not(target_arch = "wasm32"))]
{
let path = {
let cat = self.db.read_catalog();
let of_family = |c: &&CatalogEntry| c.family.eq_ignore_ascii_case(family);
let entry = cat.iter().filter(of_family).find(|c| norm(&c.style) == norm(&style)).or_else(|| cat.iter().find(of_family));
entry.map(|c| c.path.clone())?
};
if std::fs::metadata(&path).ok()?.len() > MAX_DOCUMENT_FONT_BYTES {
return None;
}
let data = Arc::new(std::fs::read(&path).ok()?);
let found = enumerate_faces(&data);
let pick = found
.iter()
.find(|(_, f, st, _)| f.eq_ignore_ascii_case(family) && norm(st) == norm(&style))
.or_else(|| found.iter().find(|(_, f, _, _)| f.eq_ignore_ascii_case(family)))?
.clone();
let (index, fam, st, coords) = pick;
let face = make_face(FontBytes::Owned(data.clone()), FontSource::Installed(path), index, fam, st, coords)?;
Some((face.kind(), face.added()))
}
#[cfg(target_arch = "wasm32")]
None
}
/// Is `family` available (the document's, loaded, or installed on the system), as named or
/// without a format suffix (see [`ScopedFonts::face`])?
pub fn has_family(&self, family: &str) -> bool {

View File

@ -126,6 +126,23 @@ fn a_link_back_to_a_parent_folder_ends_the_scan() {
assert_eq!(db.load_system_fonts(), 2);
}
#[test]
fn a_familys_kind_is_read_without_keeping_it_loaded() {
let dir = font_dir("kind");
let db = FontDb::with_font_dirs(vec![dir]);
db.load_system_fonts();
let fonts = db.scoped(0);
let (kind, added) = fonts.family_kind(FAMILY).expect("an installed family");
assert_eq!(kind.class, crate::kind::FontClass::SansSerif);
assert!(added.is_some(), "the file's date");
assert!(!db.is_loaded(FAMILY), "the file was read and let go");
// A loaded family is asked directly, with the same answer.
let _ = fonts.face(FAMILY, "Regular");
assert!(db.is_loaded(FAMILY));
assert_eq!(fonts.family_kind(FAMILY).map(|(k, _)| k.class), Some(crate::kind::FontClass::SansSerif));
assert!(fonts.family_kind("No Such Family").is_none());
}
#[test]
fn the_scan_reads_names_without_loading_the_font() {
let dir = font_dir("names");