diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index 751f36ca..58f7e1cd 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -393,6 +393,22 @@ fn legacy_names(f: &skrifa::FontRef<'_>, family: &str) -> Option<(String, String Some((legacy, name(f, &[StringId::SUBFAMILY_NAME]).unwrap_or_else(|| "Regular".into()))) } +/// The Windows family and style a variable font's named instance answers to. Windows lists each +/// instance other than Regular, Italic, Bold and Bold Italic as a family of its own, as the +/// family's static files name themselves: "Montserrat ExtraBold" / "Regular" for the ExtraBold +/// instance, "Montserrat ExtraBold" / "Italic" for ExtraBold Italic. +fn instance_legacy_names(family: &str, style: &str) -> Option<(String, String)> { + if style.contains('{') { + return None; + } + let weight: Vec<&str> = style.split_whitespace().filter(|w| !matches!(norm(w).as_str(), "italic" | "oblique")).collect(); + let weight = weight.join(" "); + if matches!(norm(&weight).as_str(), "" | "regular" | "bold") { + return None; + } + Some((format!("{family} {weight}"), if style_italic(style) { "Italic" } else { "Regular" }.into())) +} + /// The tables skrifa reads glyph outlines from: `glyf` (TrueType), `CFF `, `CFF2` and `VARC`. For a /// face with none of them (bitmaps only, or outlines in Apple's `hvgl` table), [`FontDb::outline`] /// returns an empty path for every glyph. @@ -686,7 +702,7 @@ fn make_face(bytes: FontBytes, index: u32, family: String, style: String, coords .map(|v| v >= 0.5) .or(axis(b"slnt").map(|v| v.abs() > 0.1)) .unwrap_or(!matches!(a.style, skrifa::attribute::Style::Normal) || style_italic(&style)); - let legacy = if coords.is_empty() { legacy_names(&f, &family) } else { None }; + let legacy = if coords.is_empty() { legacy_names(&f, &family) } else { instance_legacy_names(&family, &style) }; Some(FontFace { id: NEXT_ID.fetch_add(1, Ordering::Relaxed), family, diff --git a/crates/fonts/src/tests.rs b/crates/fonts/src/tests.rs index bb7b726a..a6fca4c4 100644 --- a/crates/fonts/src/tests.rs +++ b/crates/fonts/src/tests.rs @@ -147,6 +147,36 @@ fn windows_family_names_find_their_faces() { } } +#[test] +fn variable_font_instances_answer_to_their_windows_names() { + // Windows lists a variable font's named instances other than Regular, Italic, Bold and + // Bold Italic as families of their own, and documents name them that way ("Montserrat + // ExtraBold"). The bundled Montserrat is variable, so these names found no face and drew + // in Inter Regular, even with the family's static files installed (their faces are + // skipped as already present). + let db = FontDb::with_font_dirs(Vec::new()); + db.set_system_fallback(false); + if !db.is_loaded("Montserrat") { + return; // built without craft-fonts + } + for (name, style, weight, italic) in [ + ("Montserrat ExtraBold", "Regular", 800.0, false), + ("Montserrat SemiBold", "Regular", 600.0, false), + ("Montserrat Medium", "Regular", 500.0, false), + ("Montserrat Medium", "Italic", 500.0, true), + ("Montserrat Thin", "Bold", 100.0, false), + ] { + assert!(db.has_family(name), "{name}"); + let f = db.face(name, style); + assert_eq!((f.family.as_str(), f.weight, f.italic), ("Montserrat", weight, italic), "{name} {style}"); + assert_eq!(db.document_face(name, style).family, "Montserrat", "{name}: installed, so not Calibri"); + } + // Regular and Bold are the family itself, not families of their own. + assert!(!db.has_family("Montserrat Regular")); + assert!(!db.has_family("Montserrat Bold")); + assert_eq!(db.face("Montserrat", "Bold").weight, 700.0); +} + #[test] fn faces_without_outlines_are_not_cataloged() { // Rename the Semibold's `glyf` table, as in fonts whose glyphs are bitmaps or are stored in