From f85faf957ff6067e0b6f20a3d27388cc4549a330 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 30 Oct 2024 15:29:15 +0000 Subject: [PATCH] Add `math.accent` support for `flac` and `dtls` OpenType features (#5202) --- crates/typst-layout/src/math/accent.rs | 18 +++++-- crates/typst-layout/src/math/fragment.rs | 62 ++++++++++++++--------- crates/typst-layout/src/math/mod.rs | 36 ++++++------- crates/typst-layout/src/math/text.rs | 24 +++++++-- crates/typst-library/src/symbols/sym.rs | 2 +- tests/ref/math-accent-dotless.png | Bin 0 -> 1026 bytes tests/ref/math-style-dotless.png | Bin 0 -> 660 bytes tests/suite/math/accent.typ | 7 +++ tests/suite/math/style.typ | 13 +++++ 9 files changed, 110 insertions(+), 52 deletions(-) create mode 100644 tests/ref/math-accent-dotless.png create mode 100644 tests/ref/math-style-dotless.png diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index 9fa7a5a08..4e26502e3 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -19,7 +19,12 @@ pub fn layout_accent( styles: StyleChain, ) -> SourceResult<()> { let cramped = style_cramped(); - let base = ctx.layout_into_fragment(elem.base(), styles.chain(&cramped))?; + let mut base = ctx.layout_into_fragment(elem.base(), styles.chain(&cramped))?; + + // Try to replace a glyph with its dotless variant. + if let MathFragment::Glyph(glyph) = &mut base { + glyph.make_dotless_form(ctx); + } // Preserve class to preserve automatic spacing. let base_class = base.class(); @@ -31,10 +36,17 @@ pub fn layout_accent( .at(scaled_font_size(ctx, styles)) .relative_to(base.width()); + let Accent(c) = elem.accent(); + let mut glyph = GlyphFragment::new(ctx, styles, *c, elem.span()); + + // Try to replace accent glyph with flattened variant. + let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); + if base.height() > flattened_base_height { + glyph.make_flattened_accent_form(ctx); + } + // Forcing the accent to be at least as large as the base makes it too // wide in many case. - let Accent(c) = elem.accent(); - let glyph = GlyphFragment::new(ctx, styles, *c, elem.span()); let short_fall = ACCENT_SHORT_FALL.at(glyph.font_size); let variant = glyph.stretch_horizontal(ctx, width, short_fall); let accent = variant.frame; diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index 19a4494ef..8da7c0776 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -2,9 +2,7 @@ use std::fmt::{self, Debug, Formatter}; use rustybuzz::Feature; use smallvec::SmallVec; -use ttf_parser::gsub::{ - AlternateSet, AlternateSubstitution, SingleSubstitution, SubstitutionSubtable, -}; +use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable}; use ttf_parser::opentype_layout::LayoutTable; use ttf_parser::{GlyphId, Rect}; use typst_library::foundations::StyleChain; @@ -390,20 +388,39 @@ impl GlyphFragment { frame } - pub fn make_scriptsize(&mut self, ctx: &MathContext) { + pub fn make_script_size(&mut self, ctx: &MathContext) { let alt_id = - script_alternatives(ctx, self.id).and_then(|alts| alts.alternates.get(0)); - + ctx.ssty_table.as_ref().and_then(|ssty| ssty.try_apply(self.id, None)); if let Some(alt_id) = alt_id { self.set_id(ctx, alt_id); } } - pub fn make_scriptscriptsize(&mut self, ctx: &MathContext) { - let alts = script_alternatives(ctx, self.id); - let alt_id = alts - .and_then(|alts| alts.alternates.get(1).or_else(|| alts.alternates.get(0))); + pub fn make_script_script_size(&mut self, ctx: &MathContext) { + let alt_id = ctx.ssty_table.as_ref().and_then(|ssty| { + // We explicitly request to apply the alternate set with value 1, + // as opposed to the default value in ssty, as the former + // corresponds to second level scripts and the latter corresponds + // to first level scripts. + ssty.try_apply(self.id, Some(1)) + .or_else(|| ssty.try_apply(self.id, None)) + }); + if let Some(alt_id) = alt_id { + self.set_id(ctx, alt_id); + } + } + pub fn make_dotless_form(&mut self, ctx: &MathContext) { + let alt_id = + ctx.dtls_table.as_ref().and_then(|dtls| dtls.try_apply(self.id, None)); + if let Some(alt_id) = alt_id { + self.set_id(ctx, alt_id); + } + } + + pub fn make_flattened_accent_form(&mut self, ctx: &MathContext) { + let alt_id = + ctx.flac_table.as_ref().and_then(|flac| flac.try_apply(self.id, None)); if let Some(alt_id) = alt_id { self.set_id(ctx, alt_id); } @@ -561,16 +578,6 @@ fn accent_attach(ctx: &MathContext, id: GlyphId, font_size: Abs) -> Option ) } -/// Look up the script/scriptscript alternates for a glyph -fn script_alternatives<'a>( - ctx: &MathContext<'a, '_, '_>, - id: GlyphId, -) -> Option> { - ctx.ssty_table.and_then(|ssty| { - ssty.coverage.get(id).and_then(|index| ssty.alternate_sets.get(index)) - }) -} - /// Look up whether a glyph is an extended shape. fn is_extended_shape(ctx: &MathContext, id: GlyphId) -> bool { ctx.table @@ -662,10 +669,11 @@ pub enum GlyphwiseSubsts<'a> { } impl<'a> GlyphwiseSubsts<'a> { - pub fn new(gsub: LayoutTable<'a>, feature: Feature) -> Option { + pub fn new(gsub: Option>, feature: Feature) -> Option { + let gsub = gsub?; let table = gsub .features - .find(ttf_parser::Tag(feature.tag.0)) + .find(feature.tag) .and_then(|feature| feature.lookup_indices.get(0)) .and_then(|index| gsub.lookups.get(index))?; let table = table.subtables.get::(0)?; @@ -680,7 +688,11 @@ impl<'a> GlyphwiseSubsts<'a> { } } - pub fn try_apply(&self, glyph_id: GlyphId) -> Option { + pub fn try_apply( + &self, + glyph_id: GlyphId, + alt_value: Option, + ) -> Option { match self { Self::Single(single) => match single { SingleSubstitution::Format1 { coverage, delta } => coverage @@ -694,11 +706,11 @@ impl<'a> GlyphwiseSubsts<'a> { .coverage .get(glyph_id) .and_then(|idx| alternate.alternate_sets.get(idx)) - .and_then(|set| set.alternates.get(*value as u16)), + .and_then(|set| set.alternates.get(alt_value.unwrap_or(*value) as u16)), } } pub fn apply(&self, glyph_id: GlyphId) -> GlyphId { - self.try_apply(glyph_id).unwrap_or(glyph_id) + self.try_apply(glyph_id, None).unwrap_or(glyph_id) } } diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index b3dde977c..32059cef9 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -13,7 +13,8 @@ mod stretch; mod text; mod underover; -use ttf_parser::gsub::SubstitutionSubtable; +use rustybuzz::Feature; +use ttf_parser::Tag; use typst_library::diag::{bail, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{Content, NativeElement, Packed, Resolve, StyleChain}; @@ -369,7 +370,9 @@ struct MathContext<'a, 'v, 'e> { ttf: &'a ttf_parser::Face<'a>, table: ttf_parser::math::Table<'a>, constants: ttf_parser::math::Constants<'a>, - ssty_table: Option>, + dtls_table: Option>, + flac_table: Option>, + ssty_table: Option>, glyphwise_tables: Option>>, space_width: Em, // Mutable. @@ -389,26 +392,17 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> { let gsub_table = font.ttf().tables().gsub; let constants = math_table.constants.unwrap(); - let ssty_table = gsub_table - .and_then(|gsub| { - gsub.features - .find(ttf_parser::Tag::from_bytes(b"ssty")) - .and_then(|feature| feature.lookup_indices.get(0)) - .and_then(|index| gsub.lookups.get(index)) - }) - .and_then(|ssty| ssty.subtables.get::(0)) - .and_then(|ssty| match ssty { - SubstitutionSubtable::Alternate(alt_glyphs) => Some(alt_glyphs), - _ => None, - }); + let feat = |tag: &[u8; 4]| { + GlyphwiseSubsts::new(gsub_table, Feature::new(Tag::from_bytes(tag), 0, ..)) + }; let features = features(styles); - let glyphwise_tables = gsub_table.map(|gsub| { + let glyphwise_tables = Some( features .into_iter() - .filter_map(|feature| GlyphwiseSubsts::new(gsub, feature)) - .collect() - }); + .filter_map(|feature| GlyphwiseSubsts::new(gsub_table, feature)) + .collect(), + ); let ttf = font.ttf(); let space_width = ttf @@ -422,10 +416,12 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> { locator, region: Region::new(base, Axes::splat(false)), font, - ttf: font.ttf(), + ttf, table: math_table, constants, - ssty_table, + dtls_table: feat(b"dtls"), + flac_table: feat(b"flac"), + ssty_table: feat(b"ssty"), glyphwise_tables, space_width, fragments: vec![], diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index df80b45ab..86d871a2a 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -26,20 +26,25 @@ pub fn layout_text( let span = elem.span(); let mut chars = text.chars(); let math_size = EquationElem::size_in(styles); - + let mut dtls = ctx.dtls_table.is_some(); let fragment: MathFragment = if let Some(mut glyph) = chars .next() .filter(|_| chars.next().is_none()) + .map(|c| dtls_char(c, &mut dtls)) .map(|c| styled_char(styles, c, true)) .and_then(|c| GlyphFragment::try_new(ctx, styles, c, span)) { // A single letter that is available in the math font. + if dtls { + glyph.make_dotless_form(ctx); + } + match math_size { MathSize::Script => { - glyph.make_scriptsize(ctx); + glyph.make_script_size(ctx); } MathSize::ScriptScript => { - glyph.make_scriptscriptsize(ctx); + glyph.make_script_script_size(ctx); } _ => (), } @@ -342,3 +347,16 @@ fn greek_exception( _ => return None, }) } + +/// Switch dotless character to non dotless character for use of the dtls +/// OpenType feature. +pub fn dtls_char(c: char, dtls: &mut bool) -> char { + match (c, *dtls) { + ('ı', true) => 'i', + ('ȷ', true) => 'j', + _ => { + *dtls = false; + c + } + } +} diff --git a/crates/typst-library/src/symbols/sym.rs b/crates/typst-library/src/symbols/sym.rs index 606e44ea0..42f47715d 100644 --- a/crates/typst-library/src/symbols/sym.rs +++ b/crates/typst-library/src/symbols/sym.rs @@ -991,5 +991,5 @@ pub(crate) const SYM: &[(&str, Symbol)] = typst_macros::symbols! { kelvin: 'K', Re: 'ℜ', Im: 'ℑ', - dotless: [i: '𝚤', j: '𝚥'], + dotless: [i: 'ı', j: 'ȷ'], }; diff --git a/tests/ref/math-accent-dotless.png b/tests/ref/math-accent-dotless.png new file mode 100644 index 0000000000000000000000000000000000000000..81eb4fa2bd7b48cba24fa3e1c8d6beda09bb6506 GIT binary patch literal 1026 zcmV+d1pWJoP)!pnd(F%+)=1U%322ab~{o< zb~#dUo>kU#>dKgohR#?sU%8u-ov46A)Y|UHkUJu7!>6_jFZhciN>^fA-ejn({IBSKG zm--yXft!0iY^CxR6kYBeEC5Pd&MWK-95DS4_l-b55gjmYHs9l6Gt8UAD_4QVZlHY{{<1_|CcqvbTm$|di}LtG-z@e4 zpapHPWJ0q0WYi8oa=B!bZa^|3*#byWbHayzfTSH(5+)ua@gCXaMWSlrIrVo+!OtW- z9e7G`W#9h>FMkixNEN_z4arkbWQ{{J)d18R09L9(zfD1SXz02wIgI{SnWZC(zWISW{C|gtbmO<>@a5KsA&+1fcYa%)&t%usS+wsMP=yV_k9I+A+p|?p}oC zu)XR!Qg#^NyKP9f-`@|?Pq|(|a(*0_?Ro>rd+ohW0QPQ0%2WZyUO%7#HuoQwjik#2 zG5~2CNI%rgfTZ7X4x}8Uj&M#n0E1~r=>qfh4h=A8+hhjz$*jJ$IULPeO@xWP=-3E8 zxkn4UeaTk`T&$qc1B+SMde-1N9|GSo15j20+?WwKSOw7c8qO939ybc?X$11~HNf$A z2pj`Gze95~xOXyKwgwy8AH(Ir@e;uA&Wr)Kn*?6U11Q*u`!uM7TXCKdc;N(a@aLrP zYLUJcEZE!LR&hOo8y+Gyn3%A93Zns*t%ctZ>xbI*?0lUQ9?YRP?FI`RHl$BN57swp za#4GkYR`~+@G?nZ8qE;*Fm}NMv*7o@d=#9!cFoFxhNB}aC}2F_4an{91~<%sy>b#l wA-HK+!M}p1bQHx4W2wJW3QoZ(_&I|A1%eFJ0H(ovsQ>@~07*qoM6N<$f<$=EKmY&$ literal 0 HcmV?d00001 diff --git a/tests/ref/math-style-dotless.png b/tests/ref/math-style-dotless.png new file mode 100644 index 0000000000000000000000000000000000000000..6f4063c60ada93e6f2e256b6b811a4a8acb7ef63 GIT binary patch literal 660 zcmV;F0&D$=P)m$FC`#;*8E8golu;BIc1SQ*dyz^cBvQ;5GASt>p+Ywb%`&$Z z%p%KO!?aedC7QBTH`yRo*K}uJ=bJtcyVN0&D7?&MP z1ptAH+&Z3hpDqPZ&DB`C8UT21$_21Bto8%oT7heooFB_;fS$Z30DMobW-Ld{p^i$( znham#C_Z`kP`+EgvJwmOBFOsKN3|!t8bdpj<1qB`@JISDLKadU^bUjgn_M@H9wU+l8Fp+-+i~aia zI>1?c3!ugf+%Ez)wX%`XZNR0}q*@6N?*j5u#^&MjMmT;9+A5GRO!>|Y!T`X?%wFWu zC$r#<4#40*VL9>`EH~`2iLbeSO?NNySp=;_ELoIdySxqSy7RD{K%p%T-z!tV@dKc- z_93$l08{obyhPMsI;9ip7rXx8fpF}cRwaMb1$a~d9_U2vAlq>=HAwKa*j4F03<