summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMax <me@mkor.je>2024-10-30 15:29:15 +0000
committerGitHub <noreply@github.com>2024-10-30 15:29:15 +0000
commitf85faf957ff6067e0b6f20a3d27388cc4549a330 (patch)
tree5ece60cc7ffce75a76d1653f6aef4ec189612c85
parent2634a8402cdbd2443edf27abeb4ee6bafb70f5bd (diff)
Add `math.accent` support for `flac` and `dtls` OpenType features (#5202)
-rw-r--r--crates/typst-layout/src/math/accent.rs18
-rw-r--r--crates/typst-layout/src/math/fragment.rs60
-rw-r--r--crates/typst-layout/src/math/mod.rs36
-rw-r--r--crates/typst-layout/src/math/text.rs24
-rw-r--r--crates/typst-library/src/symbols/sym.rs2
-rw-r--r--tests/ref/math-accent-dotless.pngbin0 -> 1026 bytes
-rw-r--r--tests/ref/math-style-dotless.pngbin0 -> 660 bytes
-rw-r--r--tests/suite/math/accent.typ7
-rw-r--r--tests/suite/math/style.typ13
9 files changed, 109 insertions, 51 deletions
diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs
index 9fa7a5a0..4e26502e 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 19a4494e..8da7c077 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_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_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_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<Abs>
)
}
-/// Look up the script/scriptscript alternates for a glyph
-fn script_alternatives<'a>(
- ctx: &MathContext<'a, '_, '_>,
- id: GlyphId,
-) -> Option<AlternateSet<'a>> {
- 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<Self> {
+ pub fn new(gsub: Option<LayoutTable<'a>>, feature: Feature) -> Option<Self> {
+ 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::<SubstitutionSubtable>(0)?;
@@ -680,7 +688,11 @@ impl<'a> GlyphwiseSubsts<'a> {
}
}
- pub fn try_apply(&self, glyph_id: GlyphId) -> Option<GlyphId> {
+ pub fn try_apply(
+ &self,
+ glyph_id: GlyphId,
+ alt_value: Option<u32>,
+ ) -> Option<GlyphId> {
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 b3dde977..32059cef 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<ttf_parser::gsub::AlternateSubstitution<'a>>,
+ dtls_table: Option<GlyphwiseSubsts<'a>>,
+ flac_table: Option<GlyphwiseSubsts<'a>>,
+ ssty_table: Option<GlyphwiseSubsts<'a>>,
glyphwise_tables: Option<Vec<GlyphwiseSubsts<'a>>>,
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::<SubstitutionSubtable>(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 df80b45a..86d871a2 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 606e44ea..42f47715 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 00000000..81eb4fa2
--- /dev/null
+++ b/tests/ref/math-accent-dotless.png
Binary files differ
diff --git a/tests/ref/math-style-dotless.png b/tests/ref/math-style-dotless.png
new file mode 100644
index 00000000..6f4063c6
--- /dev/null
+++ b/tests/ref/math-style-dotless.png
Binary files differ
diff --git a/tests/suite/math/accent.typ b/tests/suite/math/accent.typ
index 87ed8158..5be4f576 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 09ddd3c1..e21cd4fd 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)) \