diff --git a/crates/typst-library/src/math/ctx.rs b/crates/typst-library/src/math/ctx.rs index a1dc6cf4d..a1684ffa7 100644 --- a/crates/typst-library/src/math/ctx.rs +++ b/crates/typst-library/src/math/ctx.rs @@ -1,9 +1,11 @@ +use ttf_parser::gsub::SubstitutionSubtable; use ttf_parser::math::MathValue; use typst::font::{FontStyle, FontWeight}; use typst::model::realize; use unicode_segmentation::UnicodeSegmentation; use super::*; +use crate::text::tags; macro_rules! scaled { ($ctx:expr, text: $text:ident, display: $display:ident $(,)?) => { @@ -32,6 +34,7 @@ pub struct MathContext<'a, 'b, 'v> { pub table: ttf_parser::math::Table<'a>, pub constants: ttf_parser::math::Constants<'a>, pub ssty_table: Option>, + pub glyphwise_tables: Option>>, pub space_width: Em, pub fragments: Vec, pub local: Styles, @@ -49,29 +52,31 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { font: &'a Font, block: bool, ) -> Self { - let table = font.ttf().tables().math.unwrap(); - let constants = table.constants.unwrap(); + let math_table = font.ttf().tables().math.unwrap(); + let gsub_table = font.ttf().tables().gsub; + let constants = math_table.constants.unwrap(); - let ssty_table = font - .ttf() - .tables() - .gsub + 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| ssty.subtables.get::(0)) .and_then(|ssty| match ssty { - ttf_parser::gsub::SubstitutionSubtable::Alternate(alt_glyphs) => { - Some(alt_glyphs) - } + SubstitutionSubtable::Alternate(alt_glyphs) => Some(alt_glyphs), _ => None, }); + let features = tags(styles); + let glyphwise_tables = gsub_table.map(|gsub| { + features + .into_iter() + .filter_map(|feature| GlyphwiseSubsts::new(gsub, feature)) + .collect() + }); + let size = TextElem::size_in(styles); let ttf = font.ttf(); let space_width = ttf @@ -86,9 +91,10 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { regions: Regions::one(regions.base(), Axes::splat(false)), font, ttf: font.ttf(), - table, + table: math_table, constants, ssty_table, + glyphwise_tables, space_width, fragments: vec![], local: Styles::new(), diff --git a/crates/typst-library/src/math/fragment.rs b/crates/typst-library/src/math/fragment.rs index 139ce07b1..a1702aefc 100644 --- a/crates/typst-library/src/math/fragment.rs +++ b/crates/typst-library/src/math/fragment.rs @@ -1,5 +1,10 @@ +use rustybuzz::Feature; +use ttf_parser::gsub::{ + AlternateSet, AlternateSubstitution, SingleSubstitution, SubstitutionSubtable, +}; +use ttf_parser::opentype_layout::LayoutTable; + use super::*; -use ttf_parser::gsub::AlternateSet; #[derive(Debug, Clone)] pub enum MathFragment { @@ -174,12 +179,14 @@ pub struct GlyphFragment { impl GlyphFragment { pub fn new(ctx: &MathContext, c: char, span: Span) -> Self { let id = ctx.ttf.glyph_index(c).unwrap_or_default(); + let id = Self::adjust_glyph_index(ctx, id); Self::with_id(ctx, c, id, span) } pub fn try_new(ctx: &MathContext, c: char, span: Span) -> Option { let c = ctx.style.styled_char(c); let id = ctx.ttf.glyph_index(c)?; + let id = Self::adjust_glyph_index(ctx, id); Some(Self::with_id(ctx, c, id, span)) } @@ -209,6 +216,15 @@ impl GlyphFragment { fragment } + /// Apply GSUB substitutions. + fn adjust_glyph_index(ctx: &MathContext, id: GlyphId) -> GlyphId { + if let Some(glyphwise_tables) = &ctx.glyphwise_tables { + glyphwise_tables.iter().fold(id, |id, table| table.apply(id)) + } else { + id + } + } + /// Sets element id and boxes in appropriate way without changing other /// styles. This is used to replace the glyph with a stretch variant. pub fn set_id(&mut self, ctx: &MathContext, id: GlyphId) { @@ -412,3 +428,51 @@ fn kern_at_height( Some(kern.kern(i)?.scaled(ctx)) } + +/// An OpenType substitution table that is applicable to glyph-wise substitutions. +pub enum GlyphwiseSubsts<'a> { + Single(SingleSubstitution<'a>), + Alternate(AlternateSubstitution<'a>, u32), +} + +impl<'a> GlyphwiseSubsts<'a> { + pub fn new(gsub: LayoutTable<'a>, feature: Feature) -> Option { + let ssty = gsub + .features + .find(feature.tag) + .and_then(|feature| feature.lookup_indices.get(0)) + .and_then(|index| gsub.lookups.get(index))?; + let ssty = ssty.subtables.get::(0)?; + match ssty { + SubstitutionSubtable::Single(single_glyphs) => { + Some(Self::Single(single_glyphs)) + } + SubstitutionSubtable::Alternate(alt_glyphs) => { + Some(Self::Alternate(alt_glyphs, feature.value)) + } + _ => None, + } + } + + pub fn try_apply(&self, glyph_id: GlyphId) -> Option { + match self { + Self::Single(single) => match single { + SingleSubstitution::Format1 { coverage, delta } => coverage + .get(glyph_id) + .map(|_| GlyphId(glyph_id.0.wrapping_add(*delta as u16))), + SingleSubstitution::Format2 { coverage, substitutes } => { + coverage.get(glyph_id).and_then(|idx| substitutes.get(idx)) + } + }, + Self::Alternate(alternate, value) => alternate + .coverage + .get(glyph_id) + .and_then(|idx| alternate.alternate_sets.get(idx)) + .and_then(|set| set.alternates.get(*value as u16)), + } + } + + pub fn apply(&self, glyph_id: GlyphId) -> GlyphId { + self.try_apply(glyph_id).unwrap_or(glyph_id) + } +} diff --git a/crates/typst-library/src/text/shaping.rs b/crates/typst-library/src/text/shaping.rs index ec8812fe6..5be223905 100644 --- a/crates/typst-library/src/text/shaping.rs +++ b/crates/typst-library/src/text/shaping.rs @@ -858,7 +858,7 @@ pub fn families(styles: StyleChain) -> impl Iterator + Clone } /// Collect the tags of the OpenType features to apply. -fn tags(styles: StyleChain) -> Vec { +pub fn tags(styles: StyleChain) -> Vec { let mut tags = vec![]; let mut feat = |tag, value| { tags.push(Feature::new(Tag::from_bytes(tag), value, ..)); diff --git a/tests/ref/math/font-features.png b/tests/ref/math/font-features.png new file mode 100644 index 000000000..1fff35470 Binary files /dev/null and b/tests/ref/math/font-features.png differ diff --git a/tests/typ/math/font-features.typ b/tests/typ/math/font-features.typ new file mode 100644 index 000000000..ffdd1924a --- /dev/null +++ b/tests/typ/math/font-features.typ @@ -0,0 +1,10 @@ +// Test that setting font features in math.equation has an effect. + +--- +$ nothing $ +$ "hi ∅ hey" $ +$ sum_(i in NN) 1 + i $ +#show math.equation: set text(features: ("cv01",), fallback: false) +$ nothing $ +$ "hi ∅ hey" $ +$ sum_(i in NN) 1 + i $