From cba200d4eca5846e94733e8b18ee3e69d1d9199c Mon Sep 17 00:00:00 2001 From: bluebear94 Date: Thu, 6 Jul 2023 04:11:42 -0400 Subject: [PATCH] Handle single and alternate substs for single glyphs in math mode (#1592) --- crates/typst-library/src/math/ctx.rs | 32 ++++++----- crates/typst-library/src/math/fragment.rs | 66 +++++++++++++++++++++- crates/typst-library/src/text/shaping.rs | 2 +- tests/ref/math/font-features.png | Bin 0 -> 3005 bytes tests/typ/math/font-features.typ | 10 ++++ 5 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 tests/ref/math/font-features.png create mode 100644 tests/typ/math/font-features.typ 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 0000000000000000000000000000000000000000..1fff3547032a73daa6ee2e7cd165e8c7cd107e29 GIT binary patch literal 3005 zcmZ`*c{tPy8~riE{3IHCk<1vCA|}@`WSwg#yP+Xb_T`35gPEIc8j-b7P1!;sTe7c{ zHpR$pvJ4|zi;#qDU-$lVzo+|s&vV}AdEfKL``3BSIq}w(#sYgJ_W%GOU}|Du3jkcu zpQ8`{B!KGg$$S97Pc=2rw+|axc;_3MG%U$jCM}2(9eeNl`Jim7app{tuT7aNrT|Ps z42#Cd6AJxiB_$giX4FnP^xHnyD!^ed;(=#Z;FrKVCI+C&y_L3ZziwZ-qWIZd~? z+%~85QtQ^xi1)&F!0$KYO#i|mbyagXRR90NA_l&YuMtD+4w_KwS2f-*yhq^2$E2m} zr{o;VLahC%C;xOelzA&eKsBpzMN~*m*!xpUo;=1|-m%g5r`lf9Skip@=KjfcZA?!5 zZ?}t7RE;L%HB>d*V{XhRS_gZ%V4Mgx{!~ZZbw8jhpa9bjDp%C>1lA9MO(pjA%$kt6 z8l&8E5Odp6;G5C@;qJPAZ=d9zl^c5n>s3DUbjy*MzSR&BwMp_wK8- zFQO0VryKa=9XcBZ_L@O z60wrS3(LQW5hTb^hfS^O0D-aKV8qgNXy#toE|cfclR}WBQOE+Dm!3imsu7yUYZ z!oCH=5W)5T%*?V=s+FjH!ISeu&)app?X@21iSpDfF(9&MZWM{UP2SbqK3WGFx#6jDM6q z(NNmY?A*$vSJ`cxed=mV+P_B6xHt7IFm4!U1;*;U~+BsH@bUzlL_0hUb>&} zY2OH%?wMAX1N%s2ta=MvP~1q6E$YgBc2(m_aC1GX((r(i&15rrH{xZT=$Iv{fQ3T0 zqf5pa;iuLX-o*8&WE;O>d7sz;4+se4i$o6XP{38?JIgFeZKCZ;LSoQL<1sjBSMu$# ztZeC4<*JGuqR0GL3fT}>|1PKeY*%zpQKoHd0(EJob#vy(T}brrh3O;C@;LK-&V~N( zh8&fF)E8#NmvSKVk-Ewr!}{Psd{#>>Ci5OUkah;%?9W#0yNt+tuJxS`ai?tfB+%eL z47Oe`KhOgeRz)n$6AxYdrY-FZ;7qi)-giY+J{l0dC!vB3a+;ic&4U;dJlHHDg1Gkx zO>47`?L4BTc69iQ-;8o-pPvr33xE2{%x;fW1u0Iae0kdcO%VZUp-fJ1_Ov`MR3y?~ zA}ln0>_RYckShr;xK%dC)*ac{rdRf!FjFWT@}mZAXv|b6+>u0$|XzSK;dJhl5I1P6Ob+b5#Ko9^>#IFEY%yDx6tD`$N2jQX-Hpqm6lu8Krgi1Ekw& zsqF3Ya|&hf+APbJgs4f$kJ0fPTu6)@Cu(c4%6IDHB>H_h-E>#<%!sRw1 zr-1$}-2$(AWz`BZAWG_7c*!l8ykCfCOs4R|fhe=HzVpw zIL>I`{E!`!wC!h(6P?Ve$<$hq?AK*Nt3Fmk?|*VQDtC+(bUyNd=|O4!V6d%*vk}*=5%I;h+yc>(xKh3L>a{&%7;~mkB{WdUY^_ zwI`Hwq8Ve*2pI^uKK9g(^xtR!TG>Mn;ed*nH5j?E#?&bzHk&? z5L%j1s=>STrp5)l9__aSFYN{{ey8y(T|&CUUr6ThSDL)gNeF5!eirpbcNT|*)^Cew zHYgGrLw!3N8M>~tJ8j|@p)zi1!K*gZ0ZBoKt8NBA2Rb!3Tn~&Evt|>_;+SI!C@gX@Hg|xJ$#0eZ(r+5IL1ZU0OAMpB`j;#qjl?7inDfX_o zS+|{$PsDZjmDo!ugpUe66ycWWJchYR2}B(IG-W5+SGDp&I$F#uLksL4ML&pNKEYEr zdfV}{(0q}7(kS-g``Hc(m?a!u@a^=S7c$6g&qB(^lkaoCKa6~x_-Z!4FylvK2e@|e zE_d{XS%!wA0rgrG{?qAb2H6brMp3vuP`9~>4nc146dH-~Aw!Lgt_IKQxvh!3nbFGw zvoEqDO5y_pr6G*7LkC`2#4tPt(Jj{07307`_Ic+NPc9%!%1xXa{q~&lPw#CQXwx_^ zNPXuiXs1d&tmN38q{h?!L|$GxnMwLs$DCEJo;a@qHHW1PY{>`?ej0ZVD>R?>@3t?4&)#BNu}jaT zU+d*__m3Bw4%}Aw7^sTFy4J3J(AB(*@LMU$etW#IwI4*bHinG!uVHXb?5E*RW*i9F zvpo9F)H12y(7a^tikyz`kzUPSpuW(g5AiyzHqa8PEMAq&2~kgvd8#hFBW{VYCzJL$qHM&uCh{8jjeG)XWGRR*BF)uJNFEpQcX z-By9p1vExAR1CJYbs6y8i8yq(Z0`GKjUyyjgC1WD;$MyMSD#&qyoda|49l6OyxRq= XF`u%t;co^1>gA?}mImcGx0wF`Xn|nk literal 0 HcmV?d00001 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 $