Merge pull request #1452

Fonts: a variable font's named instances answer to their Windows family names
This commit is contained in:
Brandon Thomas
2026-10-30 15:48:32 -04:00
2 changed files with 47 additions and 1 deletions
+17 -1
View File
@@ -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,
+30
View File
@@ -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