From af262d13ab24b5bf2c5f6d8198730313951e4867 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Wed, 29 Apr 2026 12:03:12 -0400 Subject: [PATCH] Merge remaining tables from FEA This implements merging of head, hhea, and vhea tables when they are manually declared in FEA. --- fontbe/src/features.rs | 6 +-- fontbe/src/head.rs | 9 ++++ fontbe/src/metrics_and_limits.rs | 13 +++++- fontbe/src/orchestration.rs | 16 ------- fontbe/src/vertical_metrics.rs | 12 +++++- fontc/src/lib.rs | 43 +++++++++++++++++++ .../FeaTableOverrides.ufo/features.fea | 22 ++++++++++ .../FeaTableOverrides.ufo/fontinfo.plist | 40 +++++++++++++++++ .../glyphs/contents.plist | 8 ++++ .../FeaTableOverrides.ufo/glyphs/space.glif | 5 +++ .../FeaTableOverrides.ufo/layercontents.plist | 10 +++++ .../testdata/FeaTableOverrides.ufo/lib.plist | 10 +++++ .../FeaTableOverrides.ufo/metainfo.plist | 10 +++++ 13 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 resources/testdata/FeaTableOverrides.ufo/features.fea create mode 100644 resources/testdata/FeaTableOverrides.ufo/fontinfo.plist create mode 100644 resources/testdata/FeaTableOverrides.ufo/glyphs/contents.plist create mode 100644 resources/testdata/FeaTableOverrides.ufo/glyphs/space.glif create mode 100644 resources/testdata/FeaTableOverrides.ufo/layercontents.plist create mode 100644 resources/testdata/FeaTableOverrides.ufo/lib.plist create mode 100644 resources/testdata/FeaTableOverrides.ufo/metainfo.plist diff --git a/fontbe/src/features.rs b/fontbe/src/features.rs index 317815ad2..47ad7cd36 100644 --- a/fontbe/src/features.rs +++ b/fontbe/src/features.rs @@ -663,11 +663,7 @@ impl Work for FeatureCompilationWork { // if fea generated tables other than GPOS/GSUB/GDEF, stash them // so we can merge later on if result.has_non_layout_tables() { - let extras = ExtraFeaTables::from(result); - // we're currently only handling 'name' and OS/2; if other tables are in - // here we probably need to do something with them too, so let's warn - extras.log_unhandled_extras(); - context.extra_fea_tables.set(extras); + context.extra_fea_tables.set(ExtraFeaTables::from(result)); } // Enables the assumption that if the file exists features were compiled diff --git a/fontbe/src/head.rs b/fontbe/src/head.rs index 9f5bf4a9c..8b2393451 100644 --- a/fontbe/src/head.rs +++ b/fontbe/src/head.rs @@ -114,6 +114,7 @@ impl Work for HeadWork { .variant(FeWorkId::StaticMetadata) .variant(WorkId::Glyf) .variant(WorkId::LocaFormat) + .variant(WorkId::ExtraFeaTables) .build() } @@ -134,6 +135,14 @@ impl Work for HeadWork { ); apply_created_modified(&mut head, static_metadata.misc.created); apply_macstyle(&mut head, static_metadata.misc.selection_flags); + + // Apply FEA `table head { ... }` override (only FontRevision is settable) + if let Some(extra_tables) = context.extra_fea_tables.try_get() + && let Some(font_rev) = extra_tables.head.as_ref().map(|h| h.font_revision) + { + head.font_revision = font_rev + } + context.head.set(head); // Defer x/y Min/Max to metrics and limits job diff --git a/fontbe/src/metrics_and_limits.rs b/fontbe/src/metrics_and_limits.rs index f93b46f5d..2e635f398 100644 --- a/fontbe/src/metrics_and_limits.rs +++ b/fontbe/src/metrics_and_limits.rs @@ -290,6 +290,7 @@ impl Work for MetricAndLimitWork { .variant(WorkId::ALL_GLYF_FRAGMENTS) // We need composite bboxes to be calculated: .variant(WorkId::Glyf) + .variant(WorkId::ExtraFeaTables) .build() } @@ -350,7 +351,7 @@ impl Work for MetricAndLimitWork { let metrics = builder.build(); // Build and send horizontal metrics tables out into the world - let hhea = Hhea { + let mut hhea = Hhea { ascender: FWord::new(default_metrics.hhea_ascender.into_inner().ot_round()), descender: FWord::new(default_metrics.hhea_descender.into_inner().ot_round()), line_gap: FWord::new(default_metrics.hhea_line_gap.into_inner().ot_round()), @@ -368,6 +369,16 @@ impl Work for MetricAndLimitWork { } })?, }; + // Apply FEA `table hhea { ... }` overrides. + // FEA can only set Ascender, Descender, LineGap, CaretOffset. + if let Some(extra_tables) = context.extra_fea_tables.try_get() + && let Some(fea_hhea) = extra_tables.hhea.as_ref() + { + hhea.ascender = fea_hhea.ascender; + hhea.descender = fea_hhea.descender; + hhea.line_gap = fea_hhea.line_gap; + hhea.caret_offset = fea_hhea.caret_offset; + } context.hhea.set(hhea); let hmtx = Hmtx::new(metrics.long_metrics, metrics.first_side_bearings); diff --git a/fontbe/src/orchestration.rs b/fontbe/src/orchestration.rs index a2fb0546f..61626478e 100644 --- a/fontbe/src/orchestration.rs +++ b/fontbe/src/orchestration.rs @@ -266,22 +266,6 @@ impl From for ExtraFeaTables { } } -impl ExtraFeaTables { - pub(crate) fn log_unhandled_extras(&self) { - for (name, unhandled) in [ - ("head", self.head.is_some()), - ("hhea", self.hhea.is_some()), - ("vhea", self.vhea.is_some()), - ("base", self.base.is_some()), - ("stat", self.stat.is_some()), - ] { - if unhandled { - log::warn!("FEA generated unused table '{name}'"); - } - } - } -} - // we could use serde here but it produces really big outputs; so instead // we can use fontwrite on each table, and then serialize an array of Option> impl Persistable for ExtraFeaTables { diff --git a/fontbe/src/vertical_metrics.rs b/fontbe/src/vertical_metrics.rs index f6396d54c..9132b8931 100644 --- a/fontbe/src/vertical_metrics.rs +++ b/fontbe/src/vertical_metrics.rs @@ -36,6 +36,7 @@ impl Work for VerticalMetricsWork { .variant(WorkId::ALL_GLYF_FRAGMENTS) // We need composite bboxes to be calculated: .variant(WorkId::Glyf) + .variant(WorkId::ExtraFeaTables) .build() } @@ -97,7 +98,7 @@ impl Work for VerticalMetricsWork { let metrics = builder.build(); // Build and send vertical metrics tables out into the world - let vhea = Vhea { + let mut vhea = Vhea { ascender: FWord::new(default_metrics.vhea_ascender.into_inner().ot_round()), descender: FWord::new(default_metrics.vhea_descender.into_inner().ot_round()), line_gap: FWord::new(default_metrics.vhea_line_gap.into_inner().ot_round()), @@ -118,6 +119,15 @@ impl Work for VerticalMetricsWork { } })?, }; + // Apply FEA `table vhea { ... }` overrides. + // FEA can only set VertTypoAscender, VertTypoDescender, VertTypoLineGap. + if let Some(extra_tables) = context.extra_fea_tables.try_get() + && let Some(fea_vhea) = extra_tables.vhea.as_ref() + { + vhea.ascender = fea_vhea.ascender; + vhea.descender = fea_vhea.descender; + vhea.line_gap = fea_vhea.line_gap; + } context.vhea.set(vhea); let vmtx = Vmtx::new(metrics.long_metrics, metrics.first_side_bearings); diff --git a/fontc/src/lib.rs b/fontc/src/lib.rs index ca4b3badb..14aabd810 100644 --- a/fontc/src/lib.rs +++ b/fontc/src/lib.rs @@ -5537,4 +5537,47 @@ mod tests { .join("\n") ); } + + // FEA `table` block overrides: verify that explicit values in FEA win over + // fontinfo-derived values for head, hhea, and OS/2 tables. + + #[test] + fn fea_table_overrides_head() { + let result = TestCompile::compile_source("FeaTableOverrides.ufo"); + let head = result.font().head().unwrap(); + // FEA says FontRevision 2.5; fontinfo says versionMajor=1 versionMinor=0 + assert_eq!( + write_fonts::types::Fixed::from_f64(2.5), + head.font_revision() + ); + } + + #[test] + fn fea_table_overrides_hhea() { + let result = TestCompile::compile_source("FeaTableOverrides.ufo"); + let hhea = result.font().hhea().unwrap(); + // FEA: Ascender 950, Descender -250, LineGap 0, CaretOffset 20 + // fontinfo: 800, -200, 0, (default) + assert_eq!(write_fonts::types::FWord::new(950), hhea.ascender()); + assert_eq!(write_fonts::types::FWord::new(-250), hhea.descender()); + assert_eq!(write_fonts::types::FWord::new(0), hhea.line_gap()); + assert_eq!(20, hhea.caret_offset()); + } + + #[test] + fn fea_table_overrides_os2() { + let result = TestCompile::compile_source("FeaTableOverrides.ufo"); + let os2 = result.font().os2().unwrap(); + // FEA: FSType 0, TypoAscender 950, TypoDescender -250, TypoLineGap 0, + // winAscent 1100, winDescent 250, WeightClass 700 + // fontinfo: FSType 4 (bit 2), TypoAscender 800, TypoDescender -200, + // TypoLineGap 200, winAscent 1000, winDescent 200, WeightClass 400 + assert_eq!(0, os2.fs_type()); + assert_eq!(950, os2.s_typo_ascender()); + assert_eq!(-250, os2.s_typo_descender()); + assert_eq!(0, os2.s_typo_line_gap()); + assert_eq!(1100, os2.us_win_ascent()); + assert_eq!(250, os2.us_win_descent()); + assert_eq!(700, os2.us_weight_class()); + } } diff --git a/resources/testdata/FeaTableOverrides.ufo/features.fea b/resources/testdata/FeaTableOverrides.ufo/features.fea new file mode 100644 index 000000000..e835460b3 --- /dev/null +++ b/resources/testdata/FeaTableOverrides.ufo/features.fea @@ -0,0 +1,22 @@ +languagesystem DFLT dflt; + +table head { + FontRevision 2.5; +} head; + +table hhea { + Ascender 950; + Descender -250; + LineGap 0; + CaretOffset 20; +} hhea; + +table OS/2 { + FSType 0; + TypoAscender 950; + TypoDescender -250; + TypoLineGap 0; + winAscent 1100; + winDescent 250; + WeightClass 700; +} OS/2; diff --git a/resources/testdata/FeaTableOverrides.ufo/fontinfo.plist b/resources/testdata/FeaTableOverrides.ufo/fontinfo.plist new file mode 100644 index 000000000..36e34f546 --- /dev/null +++ b/resources/testdata/FeaTableOverrides.ufo/fontinfo.plist @@ -0,0 +1,40 @@ + + + + + familyName + FeaTableOverrides + unitsPerEm + 1000 + ascender + 800 + descender + -200 + openTypeHheaAscender + 800 + openTypeHheaDescender + -200 + openTypeHheaLineGap + 0 + openTypeOS2TypoAscender + 800 + openTypeOS2TypoDescender + -200 + openTypeOS2TypoLineGap + 200 + openTypeOS2WinAscent + 1000 + openTypeOS2WinDescent + 200 + openTypeOS2WeightClass + 400 + openTypeOS2Type + + 2 + + versionMajor + 1 + versionMinor + 0 + + diff --git a/resources/testdata/FeaTableOverrides.ufo/glyphs/contents.plist b/resources/testdata/FeaTableOverrides.ufo/glyphs/contents.plist new file mode 100644 index 000000000..d35e3daed --- /dev/null +++ b/resources/testdata/FeaTableOverrides.ufo/glyphs/contents.plist @@ -0,0 +1,8 @@ + + + + + space + space.glif + + diff --git a/resources/testdata/FeaTableOverrides.ufo/glyphs/space.glif b/resources/testdata/FeaTableOverrides.ufo/glyphs/space.glif new file mode 100644 index 000000000..4d7d7c206 --- /dev/null +++ b/resources/testdata/FeaTableOverrides.ufo/glyphs/space.glif @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/testdata/FeaTableOverrides.ufo/layercontents.plist b/resources/testdata/FeaTableOverrides.ufo/layercontents.plist new file mode 100644 index 000000000..b9c1a4f27 --- /dev/null +++ b/resources/testdata/FeaTableOverrides.ufo/layercontents.plist @@ -0,0 +1,10 @@ + + + + + + public.default + glyphs + + + diff --git a/resources/testdata/FeaTableOverrides.ufo/lib.plist b/resources/testdata/FeaTableOverrides.ufo/lib.plist new file mode 100644 index 000000000..d8fc173e3 --- /dev/null +++ b/resources/testdata/FeaTableOverrides.ufo/lib.plist @@ -0,0 +1,10 @@ + + + + + public.glyphOrder + + space + + + diff --git a/resources/testdata/FeaTableOverrides.ufo/metainfo.plist b/resources/testdata/FeaTableOverrides.ufo/metainfo.plist new file mode 100644 index 000000000..7b8b34ac6 --- /dev/null +++ b/resources/testdata/FeaTableOverrides.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + com.github.fonttools.ufoLib + formatVersion + 3 + +