mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +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,
|
||||
) -> 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;
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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![],
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -991,5 +991,5 @@ pub(crate) const SYM: &[(&str, Symbol)] = typst_macros::symbols! {
|
||||
kelvin: 'K',
|
||||
Re: 'ℜ',
|
||||
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 ---
|
||||
// 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)$
|
||||
|
@ -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)) \
|
||||
|
Loading…
x
Reference in New Issue
Block a user