diff --git a/fontbe/src/fvar.rs b/fontbe/src/fvar.rs index d31e9f9c9..d65b4aad2 100644 --- a/fontbe/src/fvar.rs +++ b/fontbe/src/fvar.rs @@ -37,7 +37,7 @@ fn generate_fvar(static_metadata: &StaticMetadata) -> Option { // To match fontmake we should use the font-specific name range and not reuse // a well-known name, even if the name matches. .filter(|(key, _)| key.name_id.to_u16() > 255) - .map(|(key, name)| (name, key.name_id)) + .map(|(key, name)| (name.as_str(), key.name_id)) .collect(); let axes_and_instances = AxisInstanceArrays::new( @@ -50,7 +50,7 @@ fn generate_fvar(static_metadata: &StaticMetadata) -> Option { min_value: ir_axis.min.into(), default_value: ir_axis.default.into(), max_value: ir_axis.max.into(), - axis_name_id: *reverse_names.get(&ir_axis.name).unwrap(), + axis_name_id: *reverse_names.get(ir_axis.ui_label_name()).unwrap(), ..Default::default() }; if ir_axis.hidden { @@ -63,7 +63,7 @@ fn generate_fvar(static_metadata: &StaticMetadata) -> Option { .named_instances .iter() .map(|ni| InstanceRecord { - subfamily_name_id: *reverse_names.get(&ni.name).unwrap(), + subfamily_name_id: *reverse_names.get(ni.name.as_str()).unwrap(), coordinates: static_metadata .axes .iter() diff --git a/fontbe/src/stat.rs b/fontbe/src/stat.rs index 6fcb40cc8..745394a52 100644 --- a/fontbe/src/stat.rs +++ b/fontbe/src/stat.rs @@ -51,7 +51,7 @@ impl Work for StatWork { // To match fontmake we should use the font-specific name range and not reuse // a well-known name, even if the name matches. .filter(|(key, _)| key.name_id.to_u16() > 255) - .map(|(key, name)| (name, key.name_id)) + .map(|(key, name)| (name.as_str(), key.name_id)) .collect(); context.stat.set_unconditionally( @@ -62,7 +62,7 @@ impl Work for StatWork { .enumerate() .map(|(idx, a)| AxisRecord { axis_tag: a.tag, - axis_name_id: *reverse_names.get(&a.name).unwrap(), + axis_name_id: *reverse_names.get(a.ui_label_name()).unwrap(), axis_ordering: idx as u16, }) .collect::>() diff --git a/fontc/src/lib.rs b/fontc/src/lib.rs index 1905121f3..432e92f11 100644 --- a/fontc/src/lib.rs +++ b/fontc/src/lib.rs @@ -1729,6 +1729,51 @@ mod tests { }) } + #[test] + fn standard_axis_names() { + // test that we match fonttools' naming of standard axes + // https://github.com/googlefonts/fontc/issues/1020 + let result = TestCompile::compile_source("glyphs3/StandardAxisNames.glyphs"); + let static_metadata = result.fe_context.static_metadata.get(); + + assert_eq!( + vec![ + "weight".to_string(), + "width".to_string(), + "italic".to_string(), + "slant".to_string(), + "optical".to_string(), + "foobarbaz".to_string(), + ], + static_metadata + .axes + .iter() + .map(|axis| axis.name.clone()) + .collect::>() + ); + + let font = result.font(); + let name = font.name().unwrap(); + let fvar = font.fvar().unwrap(); + + assert_eq!( + vec![ + "Weight".to_string(), + "Width".to_string(), + "Italic".to_string(), + "Slant".to_string(), + "Optical Size".to_string(), + // This axis is not 'standard' so its UI label was not renamed + "foobarbaz".to_string(), + ], + fvar.axes() + .unwrap() + .iter() + .map(|axis| resolve_name(&name, axis.axis_name_id()).unwrap()) + .collect::>() + ) + } + fn assert_named_instances(source: &str, expected: Vec<(String, Vec<(&str, f32)>)>) { let result = TestCompile::compile_source(source); let font = result.font(); diff --git a/fontdrasil/src/types.rs b/fontdrasil/src/types.rs index e02dc6b1b..c30600b23 100644 --- a/fontdrasil/src/types.rs +++ b/fontdrasil/src/types.rs @@ -105,6 +105,36 @@ impl Axis { pub fn default_converter(&self) -> CoordConverter { CoordConverter::default_normalization(self.min, self.default, self.max) } + + /// Display name for the axis. + /// + /// Some frontends (e.g. designspace) support the notion of an axis' UI label name distinct + /// from the axis name; the former is used for displaying the axis in UI and can be + /// localised, whereas the latter is only used for internal cross-references. + /// In Designspace documents, these are stored in the axes' `` elements. + /// FontTools uses these to build the name records associated with axis names referenced + /// by fvar and STAT tables, or else falls back to the axis.name. fontc doesn't know about + /// them until norad is able to parse them (). + /// But even when the labelnames are ommited, there's a special group of registered + /// axis names that were common in old MutatorMath source files before the `` + /// element itself got standardised, which continue to receive a special treatment in + /// fonttools: i.e., the lowercase, shortened name gets replaced with a title-case, + /// expanded one (e.g. 'weight' => 'Weight', 'optical' => 'Optical Size' etc.). + /// For the sake of matching fontmake (which uses fonttools), we do the same here. + /// + /// For additional info see: + pub fn ui_label_name(&self) -> &str { + // TODO: support localised labelnames when norad does + let axis_name = self.name.as_str(); + match axis_name { + "weight" => "Weight", + "width" => "Width", + "slant" => "Slant", + "optical" => "Optical Size", + "italic" => "Italic", + _ => axis_name, + } + } } // OS/2 width class diff --git a/fontir/src/ir.rs b/fontir/src/ir.rs index 6b6eb481f..a6db62ec5 100644 --- a/fontir/src/ir.rs +++ b/fontir/src/ir.rs @@ -422,14 +422,14 @@ impl StaticMetadata { variable_axes .iter() - .map(|axis| &axis.name) - .chain(named_instances.iter().map(|ni| &ni.name)) + .map(|axis| axis.ui_label_name()) + .chain(named_instances.iter().map(|ni| ni.name.as_ref())) .for_each(|name| { if !visited.insert(name) { return; } name_id_gen += 1; - key_to_name.insert(NameKey::new(name_id_gen.into(), name), name.clone()); + key_to_name.insert(NameKey::new(name_id_gen.into(), name), name.to_string()); }); let variation_model = VariationModel::new(global_locations, variable_axes.clone())?; diff --git a/resources/testdata/glyphs3/StandardAxisNames.glyphs b/resources/testdata/glyphs3/StandardAxisNames.glyphs new file mode 100644 index 000000000..856ca8029 --- /dev/null +++ b/resources/testdata/glyphs3/StandardAxisNames.glyphs @@ -0,0 +1,193 @@ +{ +.appVersion = "3324"; +.formatVersion = 3; +axes = ( +{ +name = weight; +tag = wght; +}, +{ +name = width; +tag = wdth; +}, +{ +name = italic; +tag = ital; +}, +{ +name = slant; +tag = slnt; +}, +{ +name = optical; +tag = opsz; +}, +{ +name = foobarbaz; +tag = FOOB; +} +); +date = "2024-10-10 14:11:11 +0000"; +familyName = "New Font"; +fontMaster = ( +{ +axesValues = ( +0, +0, +0, +0, +0, +0 +); +id = m01; +metricValues = ( +{ +} +); +name = Regular; +}, +{ +axesValues = ( +0, +0, +0, +0, +10, +0 +); +id = "1FD21B2D-09AF-4981-B811-2D872E70C4D7"; +metricValues = ( +{ +} +); +name = Display; +}, +{ +axesValues = ( +10, +0, +0, +0, +0, +0 +); +id = "FEF7F46C-F4CB-4286-8DAB-0F5B4E3112C4"; +metricValues = ( +{ +} +); +name = Bold; +}, +{ +axesValues = ( +0, +10, +0, +0, +0, +0 +); +id = "35AAE790-B0B9-4EC3-B9E7-BFB27ABD6F7D"; +metricValues = ( +{ +} +); +name = Expanded; +}, +{ +axesValues = ( +0, +0, +1, +0, +0, +0 +); +id = "19C9E9CF-9763-4DDF-A379-0445DCA72D4F"; +metricValues = ( +{ +pos = 10; +} +); +name = Italic; +}, +{ +axesValues = ( +0, +0, +0, +10, +0, +0 +); +id = "AD527805-E4EB-47AA-92AC-2C0C755E4536"; +metricValues = ( +{ +pos = 10; +} +); +name = Slanted; +}, +{ +axesValues = ( +0, +0, +0, +0, +0, +10 +); +id = "A4969238-40F2-4056-9F02-70F1E4A1FE86"; +metricValues = ( +{ +} +); +name = "Foo Bar Baz"; +} +); +glyphs = ( +{ +glyphname = space; +lastChange = "2024-10-14 10:45:18 +0000"; +layers = ( +{ +layerId = m01; +width = 200; +}, +{ +layerId = "1FD21B2D-09AF-4981-B811-2D872E70C4D7"; +width = 600; +}, +{ +layerId = "FEF7F46C-F4CB-4286-8DAB-0F5B4E3112C4"; +width = 600; +}, +{ +layerId = "35AAE790-B0B9-4EC3-B9E7-BFB27ABD6F7D"; +width = 600; +}, +{ +layerId = "19C9E9CF-9763-4DDF-A379-0445DCA72D4F"; +width = 600; +}, +{ +layerId = "AD527805-E4EB-47AA-92AC-2C0C755E4536"; +width = 600; +}, +{ +layerId = "A4969238-40F2-4056-9F02-70F1E4A1FE86"; +width = 600; +} +); +unicode = 32; +} +); +metrics = ( +{ +type = "italic angle"; +} +); +unitsPerEm = 1000; +versionMajor = 1; +versionMinor = 0; +}