From c29ae9f2f1e8c6ac4ff671067f1f5806c636201b Mon Sep 17 00:00:00 2001 From: Aaron Muir Hamilton Date: Sat, 13 Apr 2024 01:26:29 -0400 Subject: [PATCH] Add basic language and script fallback for the Android backend. --- src/fontique/backend/android.rs | 96 ++++++++++++++++++++++++++++++--- src/fontique/collection/mod.rs | 2 + 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/src/fontique/backend/android.rs b/src/fontique/backend/android.rs index 7270867b..0f4c2c0f 100644 --- a/src/fontique/backend/android.rs +++ b/src/fontique/backend/android.rs @@ -4,10 +4,11 @@ use std::{path::Path, str::FromStr, sync::Arc}; use hashbrown::HashMap; -use roxmltree::Document; +use icu_locid::LanguageIdentifier; +use roxmltree::{Document, Node}; use super::{ - scan, FallbackKey, FamilyId, FamilyInfo, FamilyNameMap, GenericFamily, GenericFamilyMap, + scan, FallbackKey, FamilyId, FamilyInfo, FamilyNameMap, GenericFamily, GenericFamilyMap, Script, }; // TODO: Use actual generic families here, where available, when fonts.xml is properly parsed. @@ -33,6 +34,8 @@ pub struct SystemFonts { pub name_map: Arc, pub generic_families: Arc, family_map: HashMap, + locale_fallback: Box<[(Box, FamilyId)]>, + script_fallback: Box<[(Script, FamilyId)]>, } impl SystemFonts { @@ -42,6 +45,7 @@ impl SystemFonts { let scan::ScannedCollection { family_names: mut name_map, families: family_map, + postscript_names, .. } = scan::ScannedCollection::from_paths(Path::new(&android_root).join("fonts").to_str(), 8); let mut generic_families = GenericFamilyMap::default(); @@ -55,6 +59,9 @@ impl SystemFonts { ); } + let mut locale_fallback = vec![]; + let mut script_fallback = vec![]; + // Try to get generic info from fonts.xml if let Ok(s) = std::fs::read_to_string(Path::new(&android_root).join("etc/fonts.xml")) { if let Ok(doc) = Document::parse(s.clone().as_str()) { @@ -90,11 +97,62 @@ impl SystemFonts { // smarter, or something dumb that meets expecta­ // tions on Android. } - } else if let Some(_langs) = child + } else if let Some(langs) = child .attribute("lang") .map(|s| s.split(',').collect::>()) { - // TODO: implement language fallback "family" elements + let (_has_for, hasnt_for): (Vec, Vec) = child + .children() + .partition(|c| c.attribute("fallbackFor").is_some()); + { + // general fallback families + let (ps_named, _ps_unnamed): (Vec, Vec) = + hasnt_for.iter().partition(|c| { + c.attribute("postScriptName").is_some() + }); + + if let Some(family) = ps_named.iter().find_map(|x| { + postscript_names + .get(x.attribute("postScriptName").unwrap()) + }) { + for lang in langs { + if let Some(scr) = lang.strip_prefix("und-") { + // Undefined lang for script-only fallbacks + script_fallback.push((scr.into(), *family)); + } else if let Ok(locale) = + LanguageIdentifier::try_from_bytes( + lang.as_bytes(), + ) + { + if let Some(scr) = locale.script { + // Also fallback for the script on its own + script_fallback + .push((scr.as_str().into(), *family)); + if "Hant" == scr.as_str() { + // This works around ambiguous han char­ + // acters going unmapped with current + // fallback code. This should be done in + // a locale-dependent manner, since that + // is the norm. + script_fallback + .push(("Hani".into(), *family)); + } + } + locale_fallback + .push((locale.to_string().into(), *family)); + } + } + } + + // TODO: handle mapping to family names from file names + // when postScriptName is unavailable. + } + + // family-specific fallback families, currently unimplemented + // because it requires a GenericFamily to be plumbed through + // the `RangedStyle` `font_stack` from `resolve` where it is + // currently thrown away. + {} } // TODO: interpret variant="compact" without fallbackFor as a // fallback for system-ui, as falling back to a @@ -112,15 +170,37 @@ impl SystemFonts { name_map: Arc::new(name_map), generic_families: Arc::new(generic_families), family_map, + locale_fallback: locale_fallback.into(), + script_fallback: script_fallback.into(), } } - pub fn family(&mut self, id: FamilyId) -> Option { + pub fn family(&self, id: FamilyId) -> Option { self.family_map.get(&id).cloned() } - pub fn fallback(&mut self, _key: impl Into) -> Option { - // FIXME: This is a stub - Some(self.generic_families.get(GenericFamily::SansSerif)[0]) + pub fn fallback(&self, key: impl Into) -> Option { + let key: FallbackKey = key.into(); + let script = key.script(); + + key.locale() + .and_then(|li| { + self.locale_fallback + .iter() + .find(|(lid, _)| li == lid.as_ref()) + .map(|(_, fid)| *fid) + }) + .or_else(|| { + self.script_fallback + .iter() + .find(|(s, _)| script == *s) + .map(|(_, fid)| *fid) + }) + .or_else(|| { + self.generic_families + .get(GenericFamily::SansSerif) + .first() + .copied() + }) } } diff --git a/src/fontique/collection/mod.rs b/src/fontique/collection/mod.rs index 0efa0410..46c55866 100644 --- a/src/fontique/collection/mod.rs +++ b/src/fontique/collection/mod.rs @@ -352,6 +352,8 @@ impl Inner { if let Some(families) = self.data.fallbacks.get(selector) { self.fallback_cache.set(script, lang_key, families); } else if let Some(system) = self.system.as_ref() { + // Some platforms don't need mut System + #[allow(unused_mut)] let mut system = system.fonts.lock().unwrap(); if let Some(family) = system.fallback(selector) { self.data.fallbacks.set(selector, core::iter::once(family));