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 000000000..81eb4fa2b Binary files /dev/null and b/tests/ref/math-accent-dotless.png differ diff --git a/tests/ref/math-style-dotless.png b/tests/ref/math-style-dotless.png new file mode 100644 index 000000000..6f4063c60 Binary files /dev/null and b/tests/ref/math-style-dotless.png differ diff --git a/tests/suite/math/accent.typ b/tests/suite/math/accent.typ index 87ed81586..5be4f576f 100644 --- a/tests/suite/math/accent.typ +++ b/tests/suite/math/accent.typ @@ -35,3 +35,10 @@ $tilde(sum), tilde(sum, size: #50%), accent(H, hat, size: #200%)$ --- math-accent-sized-script --- // Test accent size in script size. $tilde(U, size: #1.1em), x^tilde(U, size: #1.1em), sscript(tilde(U, size: #1.1em))$ + +--- math-accent-dotless --- +// Test dotless glyph variants. +#let test(c) = $grave(#c), acute(sans(#c)), hat(frak(#c)), tilde(mono(#c)), + macron(bb(#c)), dot(cal(#c)), diaer(upright(#c)), breve(bold(#c)), + circle(bold(upright(#c))), caron(upright(sans(#c))), arrow(bold(frak(#c)))$ +$test(i) \ test(j)$ diff --git a/tests/suite/math/style.typ b/tests/suite/math/style.typ index 09ddd3c15..e21cd4fd8 100644 --- a/tests/suite/math/style.typ +++ b/tests/suite/math/style.typ @@ -12,6 +12,19 @@ $A, italic(A), upright(A), bold(A), bold(upright(A)), \ bb("hello") + bold(cal("world")), \ mono("SQRT")(x) wreath mono(123 + 456)$ +--- math-style-dotless --- +// Test styling dotless i and j. +$ dotless.i dotless.j, + upright(dotless.i) upright(dotless.j), + sans(dotless.i) sans(dotless.j), + bold(dotless.i) bold(dotless.j), + bb(dotless.i) bb(dotless.j), + cal(dotless.i) cal(dotless.j), + frak(dotless.i) frak(dotless.j), + mono(dotless.i) mono(dotless.j), + bold(frak(dotless.i)) upright(sans(dotless.j)), + italic(bb(dotless.i)) frak(sans(dotless.j)) $ + --- math-style-exceptions --- // Test a few style exceptions. $h, bb(N), cal(R), Theta, italic(Theta), sans(Theta), sans(italic(Theta)) \