Add math.accent support for flac and dtls OpenType features (#5202)

This commit is contained in:
Max 2024-10-30 15:29:15 +00:00 committed by GitHub
parent 2634a8402c
commit f85faf957f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 110 additions and 52 deletions

View File

@ -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;

View File

@ -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<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)
}
}

View File

@ -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![],

View File

@ -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
}
}
}

View File

@ -991,5 +991,5 @@ pub(crate) const SYM: &[(&str, Symbol)] = typst_macros::symbols! {
kelvin: '',
Re: '',
Im: '',
dotless: [i: '𝚤', j: '𝚥'],
dotless: [i: 'ı', j: 'ȷ'],
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

View File

@ -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)$

View File

@ -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)) \