mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Add math.accent
support for flac
and dtls
OpenType features (#5202)
This commit is contained in:
parent
2634a8402c
commit
f85faf957f
@ -19,7 +19,12 @@ pub fn layout_accent(
|
|||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
let cramped = style_cramped();
|
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.
|
// Preserve class to preserve automatic spacing.
|
||||||
let base_class = base.class();
|
let base_class = base.class();
|
||||||
@ -31,10 +36,17 @@ pub fn layout_accent(
|
|||||||
.at(scaled_font_size(ctx, styles))
|
.at(scaled_font_size(ctx, styles))
|
||||||
.relative_to(base.width());
|
.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
|
// Forcing the accent to be at least as large as the base makes it too
|
||||||
// wide in many case.
|
// 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 short_fall = ACCENT_SHORT_FALL.at(glyph.font_size);
|
||||||
let variant = glyph.stretch_horizontal(ctx, width, short_fall);
|
let variant = glyph.stretch_horizontal(ctx, width, short_fall);
|
||||||
let accent = variant.frame;
|
let accent = variant.frame;
|
||||||
|
@ -2,9 +2,7 @@ use std::fmt::{self, Debug, Formatter};
|
|||||||
|
|
||||||
use rustybuzz::Feature;
|
use rustybuzz::Feature;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use ttf_parser::gsub::{
|
use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable};
|
||||||
AlternateSet, AlternateSubstitution, SingleSubstitution, SubstitutionSubtable,
|
|
||||||
};
|
|
||||||
use ttf_parser::opentype_layout::LayoutTable;
|
use ttf_parser::opentype_layout::LayoutTable;
|
||||||
use ttf_parser::{GlyphId, Rect};
|
use ttf_parser::{GlyphId, Rect};
|
||||||
use typst_library::foundations::StyleChain;
|
use typst_library::foundations::StyleChain;
|
||||||
@ -390,20 +388,39 @@ impl GlyphFragment {
|
|||||||
frame
|
frame
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_scriptsize(&mut self, ctx: &MathContext) {
|
pub fn make_script_size(&mut self, ctx: &MathContext) {
|
||||||
let alt_id =
|
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 {
|
if let Some(alt_id) = alt_id {
|
||||||
self.set_id(ctx, alt_id);
|
self.set_id(ctx, alt_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_scriptscriptsize(&mut self, ctx: &MathContext) {
|
pub fn make_script_script_size(&mut self, ctx: &MathContext) {
|
||||||
let alts = script_alternatives(ctx, self.id);
|
let alt_id = ctx.ssty_table.as_ref().and_then(|ssty| {
|
||||||
let alt_id = alts
|
// We explicitly request to apply the alternate set with value 1,
|
||||||
.and_then(|alts| alts.alternates.get(1).or_else(|| alts.alternates.get(0)));
|
// 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 {
|
if let Some(alt_id) = alt_id {
|
||||||
self.set_id(ctx, 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.
|
/// Look up whether a glyph is an extended shape.
|
||||||
fn is_extended_shape(ctx: &MathContext, id: GlyphId) -> bool {
|
fn is_extended_shape(ctx: &MathContext, id: GlyphId) -> bool {
|
||||||
ctx.table
|
ctx.table
|
||||||
@ -662,10 +669,11 @@ pub enum GlyphwiseSubsts<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> 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
|
let table = gsub
|
||||||
.features
|
.features
|
||||||
.find(ttf_parser::Tag(feature.tag.0))
|
.find(feature.tag)
|
||||||
.and_then(|feature| feature.lookup_indices.get(0))
|
.and_then(|feature| feature.lookup_indices.get(0))
|
||||||
.and_then(|index| gsub.lookups.get(index))?;
|
.and_then(|index| gsub.lookups.get(index))?;
|
||||||
let table = table.subtables.get::<SubstitutionSubtable>(0)?;
|
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 {
|
match self {
|
||||||
Self::Single(single) => match single {
|
Self::Single(single) => match single {
|
||||||
SingleSubstitution::Format1 { coverage, delta } => coverage
|
SingleSubstitution::Format1 { coverage, delta } => coverage
|
||||||
@ -694,11 +706,11 @@ impl<'a> GlyphwiseSubsts<'a> {
|
|||||||
.coverage
|
.coverage
|
||||||
.get(glyph_id)
|
.get(glyph_id)
|
||||||
.and_then(|idx| alternate.alternate_sets.get(idx))
|
.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 {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,8 @@ mod stretch;
|
|||||||
mod text;
|
mod text;
|
||||||
mod underover;
|
mod underover;
|
||||||
|
|
||||||
use ttf_parser::gsub::SubstitutionSubtable;
|
use rustybuzz::Feature;
|
||||||
|
use ttf_parser::Tag;
|
||||||
use typst_library::diag::{bail, SourceResult};
|
use typst_library::diag::{bail, SourceResult};
|
||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::{Content, NativeElement, Packed, Resolve, StyleChain};
|
use typst_library::foundations::{Content, NativeElement, Packed, Resolve, StyleChain};
|
||||||
@ -369,7 +370,9 @@ struct MathContext<'a, 'v, 'e> {
|
|||||||
ttf: &'a ttf_parser::Face<'a>,
|
ttf: &'a ttf_parser::Face<'a>,
|
||||||
table: ttf_parser::math::Table<'a>,
|
table: ttf_parser::math::Table<'a>,
|
||||||
constants: ttf_parser::math::Constants<'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>>>,
|
glyphwise_tables: Option<Vec<GlyphwiseSubsts<'a>>>,
|
||||||
space_width: Em,
|
space_width: Em,
|
||||||
// Mutable.
|
// Mutable.
|
||||||
@ -389,26 +392,17 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
|
|||||||
let gsub_table = font.ttf().tables().gsub;
|
let gsub_table = font.ttf().tables().gsub;
|
||||||
let constants = math_table.constants.unwrap();
|
let constants = math_table.constants.unwrap();
|
||||||
|
|
||||||
let ssty_table = gsub_table
|
let feat = |tag: &[u8; 4]| {
|
||||||
.and_then(|gsub| {
|
GlyphwiseSubsts::new(gsub_table, Feature::new(Tag::from_bytes(tag), 0, ..))
|
||||||
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 features = features(styles);
|
let features = features(styles);
|
||||||
let glyphwise_tables = gsub_table.map(|gsub| {
|
let glyphwise_tables = Some(
|
||||||
features
|
features
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|feature| GlyphwiseSubsts::new(gsub, feature))
|
.filter_map(|feature| GlyphwiseSubsts::new(gsub_table, feature))
|
||||||
.collect()
|
.collect(),
|
||||||
});
|
);
|
||||||
|
|
||||||
let ttf = font.ttf();
|
let ttf = font.ttf();
|
||||||
let space_width = ttf
|
let space_width = ttf
|
||||||
@ -422,10 +416,12 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
|
|||||||
locator,
|
locator,
|
||||||
region: Region::new(base, Axes::splat(false)),
|
region: Region::new(base, Axes::splat(false)),
|
||||||
font,
|
font,
|
||||||
ttf: font.ttf(),
|
ttf,
|
||||||
table: math_table,
|
table: math_table,
|
||||||
constants,
|
constants,
|
||||||
ssty_table,
|
dtls_table: feat(b"dtls"),
|
||||||
|
flac_table: feat(b"flac"),
|
||||||
|
ssty_table: feat(b"ssty"),
|
||||||
glyphwise_tables,
|
glyphwise_tables,
|
||||||
space_width,
|
space_width,
|
||||||
fragments: vec![],
|
fragments: vec![],
|
||||||
|
@ -26,20 +26,25 @@ pub fn layout_text(
|
|||||||
let span = elem.span();
|
let span = elem.span();
|
||||||
let mut chars = text.chars();
|
let mut chars = text.chars();
|
||||||
let math_size = EquationElem::size_in(styles);
|
let math_size = EquationElem::size_in(styles);
|
||||||
|
let mut dtls = ctx.dtls_table.is_some();
|
||||||
let fragment: MathFragment = if let Some(mut glyph) = chars
|
let fragment: MathFragment = if let Some(mut glyph) = chars
|
||||||
.next()
|
.next()
|
||||||
.filter(|_| chars.next().is_none())
|
.filter(|_| chars.next().is_none())
|
||||||
|
.map(|c| dtls_char(c, &mut dtls))
|
||||||
.map(|c| styled_char(styles, c, true))
|
.map(|c| styled_char(styles, c, true))
|
||||||
.and_then(|c| GlyphFragment::try_new(ctx, styles, c, span))
|
.and_then(|c| GlyphFragment::try_new(ctx, styles, c, span))
|
||||||
{
|
{
|
||||||
// A single letter that is available in the math font.
|
// A single letter that is available in the math font.
|
||||||
|
if dtls {
|
||||||
|
glyph.make_dotless_form(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
match math_size {
|
match math_size {
|
||||||
MathSize::Script => {
|
MathSize::Script => {
|
||||||
glyph.make_scriptsize(ctx);
|
glyph.make_script_size(ctx);
|
||||||
}
|
}
|
||||||
MathSize::ScriptScript => {
|
MathSize::ScriptScript => {
|
||||||
glyph.make_scriptscriptsize(ctx);
|
glyph.make_script_script_size(ctx);
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
@ -342,3 +347,16 @@ fn greek_exception(
|
|||||||
_ => return None,
|
_ => 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -991,5 +991,5 @@ pub(crate) const SYM: &[(&str, Symbol)] = typst_macros::symbols! {
|
|||||||
kelvin: 'K',
|
kelvin: 'K',
|
||||||
Re: 'ℜ',
|
Re: 'ℜ',
|
||||||
Im: 'ℑ',
|
Im: 'ℑ',
|
||||||
dotless: [i: '𝚤', j: '𝚥'],
|
dotless: [i: 'ı', j: 'ȷ'],
|
||||||
};
|
};
|
||||||
|
BIN
tests/ref/math-accent-dotless.png
Normal file
BIN
tests/ref/math-accent-dotless.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
BIN
tests/ref/math-style-dotless.png
Normal file
BIN
tests/ref/math-style-dotless.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 660 B |
@ -35,3 +35,10 @@ $tilde(sum), tilde(sum, size: #50%), accent(H, hat, size: #200%)$
|
|||||||
--- math-accent-sized-script ---
|
--- math-accent-sized-script ---
|
||||||
// Test accent size in script size.
|
// Test accent size in script size.
|
||||||
$tilde(U, size: #1.1em), x^tilde(U, size: #1.1em), sscript(tilde(U, size: #1.1em))$
|
$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)$
|
||||||
|
@ -12,6 +12,19 @@ $A, italic(A), upright(A), bold(A), bold(upright(A)), \
|
|||||||
bb("hello") + bold(cal("world")), \
|
bb("hello") + bold(cal("world")), \
|
||||||
mono("SQRT")(x) wreath mono(123 + 456)$
|
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 ---
|
--- math-style-exceptions ---
|
||||||
// Test a few style exceptions.
|
// Test a few style exceptions.
|
||||||
$h, bb(N), cal(R), Theta, italic(Theta), sans(Theta), sans(italic(Theta)) \
|
$h, bb(N), cal(R), Theta, italic(Theta), sans(Theta), sans(italic(Theta)) \
|
||||||
|
Loading…
x
Reference in New Issue
Block a user