diff --git a/contributors/people.toml b/contributors/people.toml index 3f4dc1d1..625fcdb7 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -91,3 +91,7 @@ sync_github_name = false [people.krinry] display_name = "krinry" sync_github_name = true + +[people.Silvertree2010] +display_name = "Netter" +sync_github_name = false diff --git a/crates/script/src/tests_services.rs b/crates/script/src/tests_services.rs index c0ecfab4..f5ba0bf3 100644 --- a/crates/script/src/tests_services.rs +++ b/crates/script/src/tests_services.rs @@ -2,7 +2,7 @@ //! that keeps files in memory (the web editor's browser storage) answers for its own files. use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, PoisonError}; use effectcraft_engine::{Services, Session}; @@ -13,14 +13,19 @@ struct Mem(Mutex>>); impl Services for Mem { fn read_file(&self, path: &str) -> std::io::Result> { - self.0.lock().unwrap().get(path).cloned().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, path.to_string())) + self.0 + .lock() + .unwrap_or_else(PoisonError::into_inner) + .get(path) + .cloned() + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, path.to_string())) } fn write_file(&self, path: &str, data: &[u8]) -> std::io::Result<()> { - self.0.lock().unwrap().insert(path.into(), data.into()); + self.0.lock().unwrap_or_else(PoisonError::into_inner).insert(path.into(), data.into()); Ok(()) } fn exists(&self, path: &str) -> bool { - self.0.lock().unwrap().contains_key(path) + self.0.lock().unwrap_or_else(PoisonError::into_inner).contains_key(path) } fn is_dir(&self, path: &str) -> bool { path == "/browser/footage" diff --git a/crates/text/src/fonts.rs b/crates/text/src/fonts.rs index 9ebf92e2..aeabf187 100644 --- a/crates/text/src/fonts.rs +++ b/crates/text/src/fonts.rs @@ -514,12 +514,37 @@ pub fn resolve(family: &str, style: &str) -> Resolved { return r; } } + if let Some(r) = resolve_alias(family, style) { + return r; + } // Inter is bundled; face 0 (the first bundled face) is the last resort. let mut r = resolve_in(DEFAULT_FAMILY, style).unwrap_or(Resolved { face: 0, synth_bold: false, synth_italic: false, missing: true, instance: None }); r.missing = !family.is_empty() && !family.eq_ignore_ascii_case(DEFAULT_FAMILY); r } +/// Metric-compatible substitutes for common proprietary families, in order of preference (the +/// same pairs fontconfig's `30-metric-aliases.conf` uses). Text keeps its widths and line breaks. +const METRIC_ALIASES: &[(&str, &[&str])] = &[ + ("Calibri", &["Carlito"]), + ("Cambria", &["Caladea"]), + ("Arial", &["Liberation Sans", "Arimo"]), + ("Helvetica", &["Liberation Sans", "Arimo"]), + ("Arial Narrow", &["Liberation Sans Narrow"]), + ("Times New Roman", &["Liberation Serif", "Tinos"]), + ("Times", &["Liberation Serif", "Tinos"]), + ("Courier New", &["Liberation Mono", "Cousine"]), + ("Courier", &["Liberation Mono", "Cousine"]), +]; + +/// An installed metric-compatible substitute for a missing family. The result stays `missing`, +/// so Find Missing Fonts still lists the requested name. +fn resolve_alias(family: &str, style: &str) -> Option { + let family = family.trim(); + let (_, subs) = METRIC_ALIASES.iter().find(|(name, _)| name.eq_ignore_ascii_case(family))?; + subs.iter().find_map(|s| resolve_in(s, style)).map(|r| Resolved { missing: true, ..r }) +} + fn resolve_in(family: &str, style: &str) -> Option { let faces = all_faces(); let fam: Vec<&Arc> = faces.iter().filter(|f| f.info.family.eq_ignore_ascii_case(family)).collect(); @@ -1150,6 +1175,11 @@ mod tests { /// A minimal TrueType font built in code (no font file in the repo): U+0020 and U+E000 map to /// an empty glyph, U+E001 to a triangle. fn blank_glyph_font() -> Vec { + test_font("Blank Glyph Test") + } + + /// The blank-glyph test font under another family name. + fn test_font(family: &str) -> Vec { fn u16s(v: &[u16]) -> Vec { v.iter().flat_map(|x| x.to_be_bytes()).collect() } @@ -1177,7 +1207,7 @@ mod tests { hhea.extend(u16s(&[0, 3])); let hmtx = u16s(&[600, 0, 600, 0, 600, 0]); let maxp = u16s(&[1, 0, 3, 3, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0]); - let strings: Vec> = ["Blank Glyph Test", "Regular"].iter().map(|s| s.encode_utf16().flat_map(|u| u.to_be_bytes()).collect()).collect(); + let strings: Vec> = [family, "Regular"].iter().map(|s| s.encode_utf16().flat_map(|u| u.to_be_bytes()).collect()).collect(); let mut name = u16s(&[0, 2, 30]); let mut off = 0; for (id, st) in strings.iter().enumerate() { @@ -1221,6 +1251,23 @@ mod tests { assert_eq!(fallback_for('\u{e001}', inter), id, "fallback finds the drawn one"); } + /// A Microsoft family that isn't installed renders in its installed metric-compatible + /// substitute instead of Inter, and still reports itself missing (#1677). + #[test] + fn microsoft_families_resolve_to_metric_compatible_substitutes() { + add_font_data(test_font("Carlito")); + add_font_data(test_font("Caladea")); + let calibri = resolve("Calibri", "Regular"); + assert_eq!(face(calibri.face).info.family, "Carlito"); + assert!(calibri.missing, "the original name stays missing for Find Missing Fonts"); + let cambria = resolve("Cambria", "Bold"); + assert_eq!(face(cambria.face).info.family, "Caladea"); + assert!(cambria.missing); + let unknown = resolve("No Such Family 1677", "Regular"); + assert_eq!(face(unknown.face).info.family, DEFAULT_FAMILY); + assert!(unknown.missing); + } + #[test] fn the_han_order_follows_the_cjk_locale() { // Chinese first for any Chinese tag, whether or not it names a script.