mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Fonts: a variable font's default face is named and weighed by its default instance (#2521)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
2a5dfa6a94
commit
b55d277b49
@@ -892,11 +892,22 @@ pub(crate) fn face_styles(f: &skrifa::FontRef<'_>) -> Vec<FaceStyle> {
|
||||
let native_family = native_family_name(f, &family);
|
||||
let mut keys = FaceKeys::of(f, &family, &style);
|
||||
let traits = FontTraits::of(f, &keys);
|
||||
let mut instances = named_instances(f, &family, &style, &mut keys, native_family.clone());
|
||||
let (mut instances, renamed) = named_instances(f, &family, &style, &mut keys, native_family.clone());
|
||||
for i in &mut instances {
|
||||
i.traits = traits;
|
||||
}
|
||||
let face = FaceStyle { family, native_family, style, keys, variations: vec![], weight: None, italic: false, traits };
|
||||
// A variable font's default face renders at its `wght` default, whatever its name table and
|
||||
// OS/2 table say (Noto Sans Hebrew: named `Light`, weight class 300, default instance Regular
|
||||
// at 400): weigh it so, and name it by its default instance when that is another weight.
|
||||
let weight = f
|
||||
.axes()
|
||||
.iter()
|
||||
.find(|a| a.tag().to_be_bytes() == *b"wght")
|
||||
.map(|a| a.default_value())
|
||||
.filter(|w| w.is_finite())
|
||||
.map(|w| w.clamp(1.0, 1000.0));
|
||||
let style = renamed.unwrap_or(style);
|
||||
let face = FaceStyle { family, native_family, style, keys, variations: vec![], weight, italic: false, traits };
|
||||
std::iter::once(face).chain(instances).collect()
|
||||
}
|
||||
|
||||
@@ -905,17 +916,40 @@ pub(crate) fn face_styles(f: &skrifa::FontRef<'_>) -> Vec<FaceStyle> {
|
||||
/// names already taken. The default instance's name, when it isn't the face's style name (a face
|
||||
/// `Regular` whose default instance is `Book`), names the face too (`keys`). Damaged tables give
|
||||
/// fewer instances or none; the counts read are capped.
|
||||
fn named_instances(f: &skrifa::FontRef<'_>, family: &str, style: &str, keys: &mut FaceKeys, native_family: Option<String>) -> Vec<FaceStyle> {
|
||||
fn named_instances(
|
||||
f: &skrifa::FontRef<'_>,
|
||||
family: &str,
|
||||
style: &str,
|
||||
keys: &mut FaceKeys,
|
||||
native_family: Option<String>,
|
||||
) -> (Vec<FaceStyle>, Option<String>) {
|
||||
let axes = f.axes();
|
||||
if axes.is_empty() || axes.len() > MAX_AXES {
|
||||
return vec![];
|
||||
return (vec![], None);
|
||||
}
|
||||
let tags: Vec<Tag> = axes.iter().map(|a| a.tag()).collect();
|
||||
let defaults: Vec<f32> = axes.iter().map(|a| a.default_value()).collect();
|
||||
let is_default = |coords: &[f32]| coords.len() == defaults.len() && coords.iter().zip(&defaults).all(|(c, d)| (c - d).abs() < 1e-3);
|
||||
// The face renders as its default instance. When that instance is another weight than the
|
||||
// name table says (`Light` face, Regular default instance), the instance names the face.
|
||||
let renamed = tags.iter().any(|t| t.to_be_bytes() == *b"wght").then(|| {
|
||||
f.named_instances()
|
||||
.iter()
|
||||
.take(MAX_INSTANCES)
|
||||
.filter(|inst| is_default(&inst.user_coords().take(MAX_AXES).collect::<Vec<f32>>()))
|
||||
.find_map(|inst| name(f, &[inst.subfamily_name_id()]))
|
||||
.filter(|d| style_weight(d) != style_weight(style))
|
||||
});
|
||||
let renamed = renamed.flatten();
|
||||
// Instances without a PostScript name of their own are named `<prefix>-<style>` (Adobe
|
||||
// Technical Note #5902), as PDF and `.ai` files name them.
|
||||
let prefix = name(f, &[StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX]).unwrap_or_else(|| family.to_string());
|
||||
let mut taken = vec![norm(style)];
|
||||
if let Some(new) = &renamed {
|
||||
let old = norm(style);
|
||||
keys.styles.retain(|k| *k != old);
|
||||
keys.styles.push(norm(new));
|
||||
}
|
||||
let mut taken = vec![norm(renamed.as_deref().unwrap_or(style))];
|
||||
let mut out = Vec::new();
|
||||
for inst in f.named_instances().iter().take(MAX_INSTANCES) {
|
||||
let coords: Vec<f32> = inst.user_coords().take(MAX_AXES).collect();
|
||||
@@ -960,7 +994,7 @@ fn named_instances(f: &skrifa::FontRef<'_>, family: &str, style: &str, keys: &mu
|
||||
traits: FontTraits::default(),
|
||||
});
|
||||
}
|
||||
out
|
||||
(out, renamed)
|
||||
}
|
||||
|
||||
/// Parse every face in `data` (a font file or collection) and the named instances of its variable
|
||||
|
||||
@@ -280,3 +280,43 @@ fn a_variable_full_name_names_its_default_face() {
|
||||
let bold = db.resolve("VaritestSans-Bold", "Regular").unwrap().0;
|
||||
assert_eq!(bold.variations(), [(*b"wght", 700.0)]);
|
||||
}
|
||||
|
||||
/// #2491: a variable font whose name table calls the default face `Light` (and whose OS/2 weight
|
||||
/// says 300) while its default instance is Regular at `wght` 400 lists, resolves and weighs the
|
||||
/// default face as Regular, as it renders.
|
||||
#[test]
|
||||
fn a_variable_default_face_is_named_and_weighed_by_its_default_instance() {
|
||||
let bytes = font();
|
||||
let source = skrifa::FontRef::new(&bytes).unwrap();
|
||||
let family = "Hebtest Sans";
|
||||
let names = name_table(&[
|
||||
(1, "Hebtest Sans Light"),
|
||||
(2, "Regular"),
|
||||
(4, "Hebtest Sans Light"),
|
||||
(6, "HebtestSans-Light"),
|
||||
(16, family),
|
||||
(17, "Light"),
|
||||
(256, "Weight"),
|
||||
(257, "Regular"),
|
||||
(258, "SemiBold"),
|
||||
(259, "Bold"),
|
||||
])
|
||||
.unwrap();
|
||||
let mut os2 = source.table_data(skrifa::raw::types::Tag::new(b"OS/2")).unwrap().as_bytes().to_vec();
|
||||
os2[4..6].copy_from_slice(&300_u16.to_be_bytes());
|
||||
let mut builder = write_fonts::FontBuilder::new();
|
||||
builder.add_raw(write_fonts::types::Tag::new(b"name"), names);
|
||||
builder.add_raw(write_fonts::types::Tag::new(b"OS/2"), os2);
|
||||
for record in source.table_directory.table_records() {
|
||||
let tag = write_fonts::types::Tag::new(&record.tag().to_be_bytes());
|
||||
if !builder.contains(tag) {
|
||||
builder.add_raw(tag, source.table_data(record.tag()).unwrap().as_bytes());
|
||||
}
|
||||
}
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
assert_eq!(db.add_font(builder.build()), 3);
|
||||
assert_eq!(db.styles(family), ["Regular", "SemiBold", "Bold"]);
|
||||
let (face, matched) = db.resolve(family, "Regular").unwrap();
|
||||
assert_eq!((face.style.as_str(), face.weight, matched), ("Regular", 400.0, FontMatch::Exact));
|
||||
assert!(face.variations().is_empty());
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha
|
||||
| 2026-10-13 | minor | Fit Headline no longer pushes the tracking to its ceiling and loses glyphs when the frame holds one line (#1445, fixes #1444); text frames, wrap shapes and type paths with coordinates beyond a billion points hold no text, and layout limits its search for room, instead of hanging (#1451); scores and hours unchanged |
|
||||
| 2026-10-12 | minor | Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged |
|
||||
| 2026-10-12 | minor | A character no installed font had is looked for again after the fonts are rescanned, so installing a font fixes it without a restart (#1376, from #992); scores and hours unchanged |
|
||||
| 2026-10-16 | minor | A variable font whose name table says Light but whose default instance is Regular (Noto Sans Hebrew) lists and weighs its default face as Regular (#2521); scores and hours unchanged |
|
||||
| 2026-10-12 | minor | Full-name font matching and Missing Fonts searches (#1258): synthetic static/variable font regressions; Windows font-service QA pending; scores and hours unchanged |
|
||||
| 2026-10-11 | minor | Middle Eastern: digit types done (Character panel menu › Digits: Default, Arabic, Hindi, Farsi; `text.setFormat` / `text.setRangeStyle {digits}`); Character Direction, kashidas, composers and the split caret still missing; scores unchanged at this rounding |
|
||||
| 2026-10-11 | minor | Thai, Lao, Khmer and Myanmar words for the caret (Ctrl/Alt+arrows) and double-click; combining marks count as part of a word; score unchanged (estimated) |
|
||||
|
||||
Reference in New Issue
Block a user