diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index 500192e2e..b9d0dc969 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -93,6 +93,9 @@ pub struct FontFace { pub family: String, /// Typographic style name (e.g. "Semibold", "Italic"). pub style: String, + /// The WWS family and style names (name IDs 21 and 22) when they differ from the typographic + /// ones: InDesign names an optical-size face by them ("Kepler Std Display" / "Light"). + pub wws: Option<(String, String)>, /// usWeightClass-style weight (400 = regular). pub weight: f32, pub italic: bool, @@ -347,6 +350,7 @@ impl FontFace { struct CatalogEntry { family: String, style: String, + wws: Option<(String, String)>, path: std::path::PathBuf, /// The font menu group and native family name, read by the scan. group: FontGroup, @@ -360,6 +364,7 @@ struct CatalogEntry { struct ScannedFace { family: String, style: String, + wws: Option<(String, String)>, group: FontGroup, native: Option, postscript: Option, @@ -574,6 +579,31 @@ fn has_face(faces: &[Arc], family: &str, style: &str) -> bool { faces.iter().any(|f| f.family.eq_ignore_ascii_case(family) && f.style.eq_ignore_ascii_case(style)) } +/// A face's WWS family and style names (name IDs 21 and 22), if it has a WWS family and it differs +/// from `family`/`style`. +fn wws_names(f: &skrifa::FontRef<'_>, family: &str, style: &str) -> Option<(String, String)> { + let wf = name(f, &[StringId::WWS_FAMILY_NAME])?; + let ws = name(f, &[StringId::WWS_SUBFAMILY_NAME]).unwrap_or_else(|| "Regular".into()); + (!wf.eq_ignore_ascii_case(family) || !ws.eq_ignore_ascii_case(style)).then_some((wf, ws)) +} + +/// The family and style a face is known by: its typographic names, or its WWS names (if it has +/// them) when `by_wws`. +fn known_as<'a>(family: &'a str, style: &'a str, wws: Option<&'a (String, String)>, by_wws: bool) -> Option<(&'a str, &'a str)> { + if by_wws { wws.map(|w| (w.0.as_str(), w.1.as_str())) } else { Some((family, style)) } +} + +/// Is `family` the WWS family name of `f`? +fn has_wws_family(f: &FontFace, family: &str) -> bool { + f.wws.as_ref().is_some_and(|w| w.0.eq_ignore_ascii_case(family)) +} + +/// Is `family` the typographic or the WWS family name of `c`? +#[cfg(not(target_arch = "wasm32"))] +fn in_catalog_family(c: &CatalogEntry, family: &str) -> bool { + c.family.eq_ignore_ascii_case(family) || c.wws.as_ref().is_some_and(|w| w.0.eq_ignore_ascii_case(family)) +} + fn face_names(f: &skrifa::FontRef<'_>) -> Option<(String, String)> { let family = name(f, &[StringId::TYPOGRAPHIC_FAMILY_NAME, StringId::FAMILY_NAME])?; let style = name(f, &[StringId::TYPOGRAPHIC_SUBFAMILY_NAME, StringId::SUBFAMILY_NAME]).unwrap_or_else(|| "Regular".into()); @@ -640,7 +670,8 @@ fn file_face_names(path: &std::path::Path) -> Vec { } }; let postscript = name(&f, &[StringId::POSTSCRIPT_NAME]); - Some(ScannedFace { family, style, group, native, postscript }) + let wws = wws_names(&f, &family, &style); + Some(ScannedFace { family, style, wws, group, native, postscript }) }) .collect() } @@ -752,6 +783,7 @@ fn make_face(bytes: FontBytes, source: FontSource, index: u32, family: String, s let m = f.metrics(Size::unscaled(), &location); let upem = m.units_per_em.max(1) as f64; let a = f.attributes(); + let wws = wws_names(&f, &family, &style); let hb = harfrust::FontRef::from_index(data, index).ok()?; let shaper = harfrust::ShaperData::new(&hb); let instance = (!coords.is_empty()) @@ -766,6 +798,7 @@ fn make_face(bytes: FontBytes, source: FontSource, index: u32, family: String, s id: NEXT_ID.fetch_add(1, Ordering::Relaxed), family, style, + wws, weight, italic, upem, @@ -1085,6 +1118,7 @@ impl FontDb { found.push(CatalogEntry { family: f.family, style: f.style, + wws: f.wws, path: p.clone(), group: f.group, native: f.native, @@ -1104,7 +1138,7 @@ impl FontDb { fn load_cataloged(&self, family: &str) -> bool { let paths: Vec = { let cat = self.read_catalog(); - let mut p: Vec<_> = cat.iter().filter(|c| c.family.eq_ignore_ascii_case(family)).map(|c| c.path.clone()).collect(); + let mut p: Vec<_> = cat.iter().filter(|c| in_catalog_family(c, family)).map(|c| c.path.clone()).collect(); p.sort(); p.dedup(); p @@ -1264,7 +1298,7 @@ impl FontDb { } fn is_loaded(&self, family: &str) -> bool { - self.read_faces().iter().any(|f| f.family.eq_ignore_ascii_case(family)) + self.read_faces().iter().any(|f| f.family.eq_ignore_ascii_case(family) || has_wws_family(f, family)) } /// A shared variable font's axes (see [`ScopedFonts::axes`]). @@ -1464,17 +1498,30 @@ impl ScopedFonts<'_> { } fn exact_style_traits(&self, family: &str) -> Vec<(bool, f32, String)> { + let v = self.named_style_traits(family, false); + // No face of that typographic family: the family may be a WWS name (see `find`). + if v.is_empty() { self.named_style_traits(family, true) } else { v } + } + + /// [`ScopedFonts::exact_style_traits`] by the faces' typographic names, or by their WWS names + /// when `by_wws`. + fn named_style_traits(&self, family: &str, by_wws: bool) -> Vec<(bool, f32, String)> { let mut v: Vec<(bool, f32, String)> = self .own() .iter() .chain(self.db.read_faces().iter()) - .filter(|f| f.family.eq_ignore_ascii_case(family) && !f.style.contains('{')) - .map(|f| (f.italic, f.weight, f.style.clone())) + .filter(|f| !f.style.contains('{')) + .filter_map(|f| known_as(&f.family, &f.style, f.wws.as_ref(), by_wws).map(|n| (f, n))) + .filter(|(_, (fam, _))| fam.eq_ignore_ascii_case(family)) + .map(|(f, (_, style))| (f.italic, f.weight, style.to_string())) .collect(); #[cfg(not(target_arch = "wasm32"))] for c in self.db.read_catalog().iter() { - if c.family.eq_ignore_ascii_case(family) && !v.iter().any(|(_, _, s)| s.eq_ignore_ascii_case(&c.style)) { - v.push((style_italic(&c.style), style_weight(&c.style), c.style.clone())); + if let Some((fam, style)) = known_as(&c.family, &c.style, c.wws.as_ref(), by_wws) + && fam.eq_ignore_ascii_case(family) + && !v.iter().any(|(_, _, s)| s.eq_ignore_ascii_case(style)) + { + v.push((style_italic(style), style_weight(style), style.to_string())); } } v.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.total_cmp(&b.1)).then(a.2.cmp(&b.2))); @@ -1565,11 +1612,11 @@ impl ScopedFonts<'_> { } fn has_exact_family(&self, family: &str) -> bool { - if self.own().iter().any(|f| f.family.eq_ignore_ascii_case(family)) || self.db.is_loaded(family) { + if self.own().iter().any(|f| f.family.eq_ignore_ascii_case(family) || has_wws_family(f, family)) || self.db.is_loaded(family) { return true; } #[cfg(not(target_arch = "wasm32"))] - if self.db.read_catalog().iter().any(|c| c.family.eq_ignore_ascii_case(family)) { + if self.db.read_catalog().iter().any(|c| in_catalog_family(c, family)) { return true; } false @@ -1636,12 +1683,20 @@ impl ScopedFonts<'_> { let faces = self.db.read_faces(); // The document's fonts first: they stand in for other fonts of the same name. let named = |f: &&Arc| f.family.eq_ignore_ascii_case(family); - let cands: Vec<&Arc> = self.own().iter().filter(named).chain(faces.iter().filter(named)).collect(); + let mut cands: Vec<&Arc> = self.own().iter().filter(named).chain(faces.iter().filter(named)).collect(); + // No face of that typographic family: the family may be a WWS name, whose faces carry + // their WWS style names. + let by_wws = cands.is_empty(); + if by_wws { + let aliased = |f: &&Arc| has_wws_family(f, family); + cands = self.own().iter().filter(aliased).chain(faces.iter().filter(aliased)).collect(); + } if cands.is_empty() { return None; } + let style_of = |f: &FontFace| -> String { norm(f.wws.as_ref().filter(|_| by_wws).map_or(&f.style, |w| &w.1)) }; let ns = norm(style); - if let Some(f) = cands.iter().find(|f| norm(&f.style) == ns) { + if let Some(f) = cands.iter().find(|f| style_of(f) == ns) { return Some((*f).clone()); } let (tw, ti) = (style_weight(style), style_italic(style)); diff --git a/crates/fonts/src/testing.rs b/crates/fonts/src/testing.rs index 830f3d358..4ce75acf5 100644 --- a/crates/fonts/src/testing.rs +++ b/crates/fonts/src/testing.rs @@ -67,6 +67,30 @@ pub fn font_mapping(family: &str, glyphs: &[(char, char)]) -> Option> { assemble(base, tables) } +/// [`font_with`] for an optical-size face: typographic names `family` / `style` (name IDs 16 and +/// 17) and WWS names `wws_family` / `wws_style` (IDs 21 and 22), the way InDesign names it +/// ("Kepler Std" / "Light Display" and "Kepler Std Display" / "Light"). `None` if the bundled font +/// can't be read. +pub fn font_with_wws(family: &str, style: &str, wws_family: &str, wws_style: &str, chars: &[char]) -> Option> { + let base = font_with(family, chars)?; + let entries = [(1u16, family), (2, style), (16, family), (17, style), (21, wws_family), (22, wws_style)]; + let count = u16::try_from(entries.len()).ok()?; + let mut name = Vec::new(); + for v in [0u16, count, 6 + count * 12] { + name.extend_from_slice(&v.to_be_bytes()); + } + let mut at = 0usize; + for (id, s) in entries { + let len = s.encode_utf16().count() * 2; + for v in [3u16, 1, 0x409, id, u16::try_from(len).ok()?, u16::try_from(at).ok()?] { + name.extend_from_slice(&v.to_be_bytes()); + } + at += len; + } + entries.iter().for_each(|(_, s)| name.extend(s.encode_utf16().flat_map(u16::to_be_bytes))); + with_tables(&base, vec![(*b"name", name)]) +} + /// Every table of the font file `font`: (tag, bytes). fn tables_of(font: &[u8]) -> Option)>> { let be16 = |b: &[u8], at: usize| b.get(at..at + 2).and_then(|s| s.try_into().ok()).map(u16::from_be_bytes); diff --git a/crates/fonts/src/tests_docfonts.rs b/crates/fonts/src/tests_docfonts.rs index f2f73bca7..f7bad7947 100644 --- a/crates/fonts/src/tests_docfonts.rs +++ b/crates/fonts/src/tests_docfonts.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; use super::*; -use crate::testing::{font_with, with_fs_type}; +use crate::testing::{font_with, font_with_wws, with_fs_type}; /// An empty temporary folder unique to `name` and this process. fn temp_dir(name: &str) -> PathBuf { @@ -190,3 +190,23 @@ fn the_access_check_vets_the_folder_and_every_file_before_it_is_opened() { assert_eq!((r.faces, r.scope, r.skipped), (0, 0, vec!["no".to_string()])); let _ = std::fs::remove_dir_all(&folder); } + +#[test] +fn optical_size_face_is_found_by_its_wws_names() { + let folder = temp_dir("wws"); + let bytes = font_with_wws("DocFont Kepler", "Light Display", "DocFont Kepler Display", "Light", &['k']).unwrap(); + std::fs::write(folder.join("light-display.otf"), bytes).unwrap(); + let db = FontDb::with_font_dirs(vec![]); + let r = db.load_document_fonts(&folder, &|_| Ok(())); + assert_eq!((r.faces, r.skipped.len()), (1, 0), "{:?}", r.skipped); + let fonts = db.scoped(r.scope); + assert!(fonts.has_family("DocFont Kepler Display"), "the WWS family counts as available"); + assert_eq!(fonts.face("DocFont Kepler Display", "Light").style, "Light Display"); + // `font.list` checks the style against these: "Light" isn't a missing style. + assert_eq!(fonts.styles("DocFont Kepler Display"), ["Light"]); + // The typographic names still find it. + assert!(fonts.has_family("DocFont Kepler")); + assert_eq!(fonts.face("DocFont Kepler", "Light Display").style, "Light Display"); + assert_eq!(fonts.styles("DocFont Kepler"), ["Light Display"]); + let _ = std::fs::remove_dir_all(&folder); +}