From 6ad7f0b098fd1094f13e7d4894ecdb9f3c54b257 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Thu, 29 Oct 2026 16:27:06 +0300 Subject: [PATCH] Update head table bounds when instancing variable fonts Fixes https://github.com/typst/subsetter/issues/40 --- NOTICE | 1 + fonts/Astrolabe_subset.ttf | Bin 0 -> 1336 bytes src/glyf.rs | 9 ++ src/head.rs | 14 +- src/lib.rs | 4 + tests/data/fonttools.tests | 2 + tests/src/font_tools.rs | 1 + tests/ttx/Astrolabe_subset_1.ttx | 187 +++++++++++++++++++++++++++ tests/ttx/Cantarell-VF_1.ttx | 8 +- tests/ttx/Cantarell-VF_2.ttx | 10 +- tests/ttx/NotoSans-Regular_var_2.ttx | 10 +- tests/ttx/NotoSans-Regular_var_3.ttx | 10 +- 12 files changed, 236 insertions(+), 20 deletions(-) create mode 100644 fonts/Astrolabe_subset.ttf create mode 100644 tests/ttx/Astrolabe_subset_1.ttx diff --git a/NOTICE b/NOTICE index 80a9cdc0..1f058ed1 100644 --- a/NOTICE +++ b/NOTICE @@ -72,6 +72,7 @@ The SIL OPEN FONT LICENSE Version 1.1 applies to the following fonts: - fonts/NotoSansCJKsc-Bold-subset1.otf - fonts/NotoSansCJKsc-Regular.otf - fonts/Syne-Regular_subset.oft (Copyright 2017 The Syne Project Authors (https://gitlab.com/bonjour-monde/fonderie/syne-typeface)) +- fonts/Astrolabe_subset.ttf (Copyright 2024 The Astrolabe Project Authors (github.com/aliftype/astrolabe)) ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 diff --git a/fonts/Astrolabe_subset.ttf b/fonts/Astrolabe_subset.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b5329a0064985ebcd867cfbc18394f389c051acf GIT binary patch literal 1336 zcmZuxO-NKx6#nj=H{+WWeVICG7UMMjM2_Z6sZ^A&!bUJF6_Jqi<=;%rG&W;DLczF` zAP9J|(i=TK&>9}@i)H}vlXz@k&>rj?iN+bfOn*uT%w{>FWEn(?b|ZjCbjf%sHx(wS&_v`8#&61T_ba341+iQf?iW69Yo zuD56)eobs9QsWK|(1KW?@FblpGcZw493%FhcaoEhi7w73hv|Dclgi8Ni!p7!>R$Tk_N^NE{@wq?88 zTN(^h)s#qgWSh9LwQX-vXz5FJrD2-E`eM->4u>b+i!Ie&(_I%Vj+lgW-7z;d1}mz} zFvn4?5zNn^ks4;@QfE^&$CWgs^D-kGaOQdwaUk1^?2)d=Hmj zoF+-l%r_!mQ29}$5SNK5-m@^gwC{4~ZGm#4ye;|i9u{mDI`OPv8wlV>!FHoYloxCd z0-R6hvgHQRhr>u=26IRwj%ma&%l={P;@(Ybke*3u8yQB@KPdi*X@~ zC!9pcNw>r^sRT(v6DueW{Y_E65*?|fl#5u@p)Nin_D~=5avsajZu*!Y_?bFK)ly3y z8;G9N<(06QB~YPQ%C8w4DAm7&N>eozR$QHg8tntMTK=qLjq( glyph_data_fn: impl FnMut(u16, &Context<'a>) -> Result>, ) -> Result<()> { let subsetted_entries = subset_glyf_entries(ctx, glyph_data_fn)?; + ctx.font_bbox = subsetted_entries + .iter() + .filter_map(|entry| glyph_bbox(entry)) + .reduce(|a, b| [a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])]); let mut sub_glyf = Writer::new(); let mut sub_loca = Writer::new(); @@ -187,6 +191,11 @@ fn subset_glyf_entries<'a>( Ok(glyf_entries) } +fn glyph_bbox(glyph_data: &[u8]) -> Option<[i16; 4]> { + let mut r = Reader::new_at(glyph_data, 2); + Some([r.read()?, r.read()?, r.read()?, r.read()?]) +} + fn remap_component_glyph(mapper: &GlyphRemapper, data: &[u8]) -> Result> { let mut r = Reader::new(data); let mut w = Writer::with_capacity(data.len()); diff --git a/src/head.rs b/src/head.rs index 9814af83..78b5045e 100644 --- a/src/head.rs +++ b/src/head.rs @@ -1,6 +1,7 @@ //! The `head` table mostly contains information that can be reused from the //! old table, except for the `loca` format, which depends on the size of the -//! glyph data. The checksum will be recalculated in the very end. +//! glyph data, and the bounds which can change when instancing. The checksum +//! will be recalculated in the very end. use super::*; @@ -9,6 +10,17 @@ pub fn subset(ctx: &mut Context) -> Result<()> { let index_to_loc = head.get_mut(50..52).ok_or(MalformedFont)?; index_to_loc[0] = 0; index_to_loc[1] = ctx.long_loca as u8; + + // We are (potentially) instancing a variable font, recalculate the font bounds. + if ctx.interjector.is_skrifa() { + let [x_min, y_min, x_max, y_max] = ctx.font_bbox.unwrap_or_default(); + let bbox = head.get_mut(36..44).ok_or(MalformedFont)?; + bbox[0..2].copy_from_slice(&x_min.to_be_bytes()); + bbox[2..4].copy_from_slice(&y_min.to_be_bytes()); + bbox[4..6].copy_from_slice(&x_max.to_be_bytes()); + bbox[6..8].copy_from_slice(&y_max.to_be_bytes()); + } + ctx.push(Tag::HEAD, head); Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index fa9568f5..32f5ded7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -203,6 +203,7 @@ fn prepare_context<'a>( flavor, tables: vec![], long_loca: false, + font_bbox: None, }) } @@ -390,6 +391,9 @@ struct Context<'a> { pub(crate) custom_hmtx_data: Option>, /// Whether the long loca format was chosen. long_loca: bool, + /// The font bounding box (xMin, yMin, xMax, yMax), or `None` if there are + /// no glyphs with outlines. + font_bbox: Option<[i16; 4]>, } impl<'a> Context<'a> { diff --git a/tests/data/fonttools.tests b/tests/data/fonttools.tests index 7dba3823..55e41ea3 100644 --- a/tests/data/fonttools.tests +++ b/tests/data/fonttools.tests @@ -17,4 +17,6 @@ NotoSans-Regular_var.ttf;10,40,58,201-205;wght=800,wdth=70.0 // Note that the lsb values currently doesn't match fonttools Cantarell-VF.otf;1,15,30-35,40,103-105;wght=400 Cantarell-VF.otf;1,15,30-35,40,103-105;wght=800 +// Glyph 1 at MSHQ=200 is outside the head bounds of the font. +Astrolabe_subset.ttf;1;MSHQ=200 diff --git a/tests/src/font_tools.rs b/tests/src/font_tools.rs index d71d1ac0..b8c338b3 100644 --- a/tests/src/font_tools.rs +++ b/tests/src/font_tools.rs @@ -19,3 +19,4 @@ use crate::*; #[test] #[cfg(feature = "variable-fonts")] fn noto_sans_regular_var_3() {test_font_tools("NotoSans-Regular_var.ttf", "10,40,58,201-205", "wght=800,wdth=70.0", 3)} #[test] #[cfg(feature = "variable-fonts")] fn cantarell_v_f_1() {test_font_tools("Cantarell-VF.otf", "1,15,30-35,40,103-105", "wght=400", 1)} #[test] #[cfg(feature = "variable-fonts")] fn cantarell_v_f_2() {test_font_tools("Cantarell-VF.otf", "1,15,30-35,40,103-105", "wght=800", 2)} +#[test] #[cfg(feature = "variable-fonts")] fn astrolabe_subset_1() {test_font_tools("Astrolabe_subset.ttf", "1", "MSHQ=200", 1)} diff --git a/tests/ttx/Astrolabe_subset_1.ttx b/tests/ttx/Astrolabe_subset_1.ttx new file mode 100644 index 00000000..b458a8a4 --- /dev/null +++ b/tests/ttx/Astrolabe_subset_1.ttx @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright 2024 The Astrolabe Project Authors (github.com/aliftype/astrolabe) + + + Astrolabe + + + Regular + + + 1.000;ALIF;Astrolabe-Regular + + + Astrolabe Regular + + + Version 1.000 + + + Astrolabe-Regular + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/ttx/Cantarell-VF_1.ttx b/tests/ttx/Cantarell-VF_1.ttx index 5008c186..ec26a802 100644 --- a/tests/ttx/Cantarell-VF_1.ttx +++ b/tests/ttx/Cantarell-VF_1.ttx @@ -22,16 +22,16 @@ - + - + - - + + diff --git a/tests/ttx/Cantarell-VF_2.ttx b/tests/ttx/Cantarell-VF_2.ttx index 146b1c60..a46695e6 100644 --- a/tests/ttx/Cantarell-VF_2.ttx +++ b/tests/ttx/Cantarell-VF_2.ttx @@ -22,16 +22,16 @@ - + - - - - + + + + diff --git a/tests/ttx/NotoSans-Regular_var_2.ttx b/tests/ttx/NotoSans-Regular_var_2.ttx index b0a9199a..38e19394 100644 --- a/tests/ttx/NotoSans-Regular_var_2.ttx +++ b/tests/ttx/NotoSans-Regular_var_2.ttx @@ -23,16 +23,16 @@ - + - - - - + + + + diff --git a/tests/ttx/NotoSans-Regular_var_3.ttx b/tests/ttx/NotoSans-Regular_var_3.ttx index 70a97a48..329a5402 100644 --- a/tests/ttx/NotoSans-Regular_var_3.ttx +++ b/tests/ttx/NotoSans-Regular_var_3.ttx @@ -23,16 +23,16 @@ - + - - - - + + + +