summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst-layout/src/inline/mod.rs1
-rw-r--r--crates/typst-layout/src/inline/shaping.rs26
-rw-r--r--crates/typst-layout/src/math/accent.rs51
-rw-r--r--crates/typst-layout/src/math/attach.rs25
-rw-r--r--crates/typst-layout/src/math/frac.rs12
-rw-r--r--crates/typst-layout/src/math/fragment.rs778
-rw-r--r--crates/typst-layout/src/math/lr.rs33
-rw-r--r--crates/typst-layout/src/math/mat.rs32
-rw-r--r--crates/typst-layout/src/math/mod.rs56
-rw-r--r--crates/typst-layout/src/math/root.rs6
-rw-r--r--crates/typst-layout/src/math/shared.rs23
-rw-r--r--crates/typst-layout/src/math/stretch.rs298
-rw-r--r--crates/typst-layout/src/math/text.rs73
-rw-r--r--crates/typst-layout/src/math/underover.rs8
-rw-r--r--crates/typst-library/src/text/font/mod.rs15
-rw-r--r--crates/typst-library/src/text/item.rs18
-rw-r--r--crates/typst-library/src/text/mod.rs18
-rw-r--r--crates/typst-pdf/src/text.rs10
-rw-r--r--crates/typst-render/src/text.rs9
-rw-r--r--crates/typst-svg/src/text.rs23
-rw-r--r--tests/ref/math-accent-dotless-greedy.pngbin0 -> 710 bytes
-rw-r--r--tests/suite/math/accent.typ6
22 files changed, 721 insertions, 800 deletions
diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs
index 5ef820d0..6cafb9b0 100644
--- a/crates/typst-layout/src/inline/mod.rs
+++ b/crates/typst-layout/src/inline/mod.rs
@@ -9,6 +9,7 @@ mod prepare;
mod shaping;
pub use self::box_::layout_box;
+pub use self::shaping::create_shape_plan;
use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::SourceResult;
diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs
index ca723c0a..935a86b3 100644
--- a/crates/typst-layout/src/inline/shaping.rs
+++ b/crates/typst-layout/src/inline/shaping.rs
@@ -1,18 +1,16 @@
use std::borrow::Cow;
use std::fmt::{self, Debug, Formatter};
-use std::str::FromStr;
use std::sync::Arc;
use az::SaturatingAs;
-use ecow::EcoString;
use rustybuzz::{BufferFlags, ShapePlan, UnicodeBuffer};
use ttf_parser::Tag;
use typst_library::engine::Engine;
use typst_library::foundations::{Smart, StyleChain};
use typst_library::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size};
use typst_library::text::{
- families, features, is_default_ignorable, variant, Font, FontFamily, FontVariant,
- Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem,
+ families, features, is_default_ignorable, language, variant, Font, FontFamily,
+ FontVariant, Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem,
};
use typst_library::World;
use typst_utils::SliceExt;
@@ -295,6 +293,8 @@ impl<'a> ShapedText<'a> {
+ justification_left
+ justification_right,
x_offset: shaped.x_offset + justification_left,
+ y_advance: Em::zero(),
+ y_offset: Em::zero(),
range: (shaped.range.start - range.start).saturating_as()
..(shaped.range.end - range.start).saturating_as(),
span,
@@ -934,7 +934,7 @@ fn shape_segment<'a>(
/// Create a shape plan.
#[comemo::memoize]
-fn create_shape_plan(
+pub fn create_shape_plan(
font: &Font,
direction: rustybuzz::Direction,
script: rustybuzz::Script,
@@ -952,7 +952,7 @@ fn create_shape_plan(
/// Shape the text with tofus from the given font.
fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
- let x_advance = font.advance(0).unwrap_or_default();
+ let x_advance = font.x_advance(0).unwrap_or_default();
let add_glyph = |(cluster, c): (usize, char)| {
let start = base + cluster;
let end = start + c.len_utf8();
@@ -1044,20 +1044,8 @@ fn calculate_adjustability(ctx: &mut ShapingContext, lang: Lang, region: Option<
/// Difference between non-breaking and normal space.
fn nbsp_delta(font: &Font) -> Option<Em> {
- let space = font.ttf().glyph_index(' ')?.0;
let nbsp = font.ttf().glyph_index('\u{00A0}')?.0;
- Some(font.advance(nbsp)? - font.advance(space)?)
-}
-
-/// Process the language and region of a style chain into a
-/// rustybuzz-compatible BCP 47 language.
-fn language(styles: StyleChain) -> rustybuzz::Language {
- let mut bcp: EcoString = TextElem::lang_in(styles).as_str().into();
- if let Some(region) = TextElem::region_in(styles) {
- bcp.push('-');
- bcp.push_str(region.as_str());
- }
- rustybuzz::Language::from_str(&bcp).unwrap()
+ Some(font.x_advance(nbsp)? - font.space_width()?)
}
/// Returns true if all glyphs in `glyphs` have ranges within the range `range`.
diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs
index 30160646..159703b8 100644
--- a/crates/typst-layout/src/math/accent.rs
+++ b/crates/typst-layout/src/math/accent.rs
@@ -3,7 +3,10 @@ use typst_library::foundations::{Packed, StyleChain};
use typst_library::layout::{Em, Frame, Point, Size};
use typst_library::math::AccentElem;
-use super::{style_cramped, FrameFragment, GlyphFragment, MathContext, MathFragment};
+use super::{
+ style_cramped, style_dtls, style_flac, FrameFragment, GlyphFragment, MathContext,
+ MathFragment,
+};
/// How much the accent can be shorter than the base.
const ACCENT_SHORT_FALL: Em = Em::new(0.5);
@@ -15,40 +18,40 @@ pub fn layout_accent(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
- let cramped = style_cramped();
- let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?;
-
let accent = elem.accent;
let top_accent = !accent.is_bottom();
- // Try to replace base glyph with its dotless variant.
- if top_accent && elem.dotless(styles) {
- if let MathFragment::Glyph(glyph) = &mut base {
- glyph.make_dotless_form(ctx);
- }
- }
+ // Try to replace the base glyph with its dotless variant.
+ let dtls = style_dtls();
+ let base_styles =
+ if top_accent && elem.dotless(styles) { styles.chain(&dtls) } else { styles };
+
+ let cramped = style_cramped();
+ let base = ctx.layout_into_fragment(&elem.base, base_styles.chain(&cramped))?;
// Preserve class to preserve automatic spacing.
let base_class = base.class();
let base_attach = base.accent_attach();
- let mut glyph = GlyphFragment::new(ctx, styles, accent.0, elem.span());
+ // Try to replace the accent glyph with its flattened variant.
+ let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
+ let flac = style_flac();
+ let accent_styles = if top_accent && base.ascent() > flattened_base_height {
+ styles.chain(&flac)
+ } else {
+ styles
+ };
- // Try to replace accent glyph with its flattened variant.
- if top_accent {
- let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
- if base.ascent() > flattened_base_height {
- glyph.make_flattened_accent_form(ctx);
- }
- }
+ let mut glyph =
+ GlyphFragment::new_char(ctx.font, accent_styles, accent.0, elem.span())?;
- // Forcing the accent to be at least as large as the base makes it too
- // wide in many case.
+ // Forcing the accent to be at least as large as the base makes it too wide
+ // in many cases.
let width = elem.size(styles).relative_to(base.width());
- let short_fall = ACCENT_SHORT_FALL.at(glyph.font_size);
- let variant = glyph.stretch_horizontal(ctx, width - short_fall);
- let accent = variant.frame;
- let accent_attach = variant.accent_attach.0;
+ let short_fall = ACCENT_SHORT_FALL.at(glyph.item.size);
+ glyph.stretch_horizontal(ctx, width - short_fall);
+ let accent_attach = glyph.accent_attach.0;
+ let accent = glyph.into_frame();
let (gap, accent_pos, base_pos) = if top_accent {
// Descent is negative because the accent's ink bottom is above the
diff --git a/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs
index 90aad941..a7f3cad5 100644
--- a/crates/typst-layout/src/math/attach.rs
+++ b/crates/typst-layout/src/math/attach.rs
@@ -66,7 +66,6 @@ pub fn layout_attach(
let relative_to_width = measure!(t, width).max(measure!(b, width));
stretch_fragment(
ctx,
- styles,
&mut base,
Some(Axis::X),
Some(relative_to_width),
@@ -220,7 +219,6 @@ fn layout_attachments(
// Calculate the distance each pre-script extends to the left of the base's
// width.
let (tl_pre_width, bl_pre_width) = compute_pre_script_widths(
- ctx,
&base,
[tl.as_ref(), bl.as_ref()],
(tx_shift, bx_shift),
@@ -231,7 +229,6 @@ fn layout_attachments(
// base's width. Also calculate each post-script's kerning (we need this for
// its position later).
let ((tr_post_width, tr_kern), (br_post_width, br_kern)) = compute_post_script_widths(
- ctx,
&base,
[tr.as_ref(), br.as_ref()],
(tx_shift, bx_shift),
@@ -287,14 +284,13 @@ fn layout_attachments(
/// post-script's kerning value. The first tuple is for the post-superscript,
/// and the second is for the post-subscript.
fn compute_post_script_widths(
- ctx: &MathContext,
base: &MathFragment,
[tr, br]: [Option<&MathFragment>; 2],
(tr_shift, br_shift): (Abs, Abs),
space_after_post_script: Abs,
) -> ((Abs, Abs), (Abs, Abs)) {
let tr_values = tr.map_or_default(|tr| {
- let kern = math_kern(ctx, base, tr, tr_shift, Corner::TopRight);
+ let kern = math_kern(base, tr, tr_shift, Corner::TopRight);
(space_after_post_script + tr.width() + kern, kern)
});
@@ -302,7 +298,7 @@ fn compute_post_script_widths(
// need to shift the post-subscript left by the base's italic correction
// (see the kerning algorithm as described in the OpenType MATH spec).
let br_values = br.map_or_default(|br| {
- let kern = math_kern(ctx, base, br, br_shift, Corner::BottomRight)
+ let kern = math_kern(base, br, br_shift, Corner::BottomRight)
- base.italics_correction();
(space_after_post_script + br.width() + kern, kern)
});
@@ -317,19 +313,18 @@ fn compute_post_script_widths(
/// extends left of the base's width and the second being the distance the
/// pre-subscript extends left of the base's width.
fn compute_pre_script_widths(
- ctx: &MathContext,
base: &MathFragment,
[tl, bl]: [Option<&MathFragment>; 2],
(tl_shift, bl_shift): (Abs, Abs),
space_before_pre_script: Abs,
) -> (Abs, Abs) {
let tl_pre_width = tl.map_or_default(|tl| {
- let kern = math_kern(ctx, base, tl, tl_shift, Corner::TopLeft);
+ let kern = math_kern(base, tl, tl_shift, Corner::TopLeft);
space_before_pre_script + tl.width() + kern
});
let bl_pre_width = bl.map_or_default(|bl| {
- let kern = math_kern(ctx, base, bl, bl_shift, Corner::BottomLeft);
+ let kern = math_kern(base, bl, bl_shift, Corner::BottomLeft);
space_before_pre_script + bl.width() + kern
});
@@ -471,13 +466,7 @@ fn compute_script_shifts(
/// a negative value means shifting the script closer to the base. Requires the
/// distance from the base's baseline to the script's baseline, as well as the
/// script's corner (tl, tr, bl, br).
-fn math_kern(
- ctx: &MathContext,
- base: &MathFragment,
- script: &MathFragment,
- shift: Abs,
- pos: Corner,
-) -> Abs {
+fn math_kern(base: &MathFragment, script: &MathFragment, shift: Abs, pos: Corner) -> Abs {
// This process is described under the MathKernInfo table in the OpenType
// MATH spec.
@@ -502,8 +491,8 @@ fn math_kern(
// Calculate the sum of kerning values for each correction height.
let summed_kern = |height| {
- let base_kern = base.kern_at_height(ctx, pos, height);
- let attach_kern = script.kern_at_height(ctx, pos.inv(), height);
+ let base_kern = base.kern_at_height(pos, height);
+ let attach_kern = script.kern_at_height(pos.inv(), height);
base_kern + attach_kern
};
diff --git a/crates/typst-layout/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs
index 2567349d..091f328f 100644
--- a/crates/typst-layout/src/math/frac.rs
+++ b/crates/typst-layout/src/math/frac.rs
@@ -109,14 +109,14 @@ fn layout_frac_like(
frame.push_frame(denom_pos, denom);
if binom {
- let mut left = GlyphFragment::new(ctx, styles, '(', span)
- .stretch_vertical(ctx, height - short_fall);
- left.center_on_axis(ctx);
+ let mut left = GlyphFragment::new_char(ctx.font, styles, '(', span)?;
+ left.stretch_vertical(ctx, height - short_fall);
+ left.center_on_axis();
ctx.push(left);
ctx.push(FrameFragment::new(styles, frame));
- let mut right = GlyphFragment::new(ctx, styles, ')', span)
- .stretch_vertical(ctx, height - short_fall);
- right.center_on_axis(ctx);
+ let mut right = GlyphFragment::new_char(ctx.font, styles, ')', span)?;
+ right.stretch_vertical(ctx, height - short_fall);
+ right.center_on_axis();
ctx.push(right);
} else {
frame.push(
diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs
index 01fa6be4..eb85eeb5 100644
--- a/crates/typst-layout/src/math/fragment.rs
+++ b/crates/typst-layout/src/math/fragment.rs
@@ -1,28 +1,32 @@
use std::fmt::{self, Debug, Formatter};
-use rustybuzz::Feature;
-use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable};
-use ttf_parser::opentype_layout::LayoutTable;
-use ttf_parser::{GlyphId, Rect};
+use az::SaturatingAs;
+use rustybuzz::{BufferFlags, UnicodeBuffer};
+use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
+use ttf_parser::GlyphId;
+use typst_library::diag::{bail, warning, SourceResult};
use typst_library::foundations::StyleChain;
use typst_library::introspection::Tag;
use typst_library::layout::{
- Abs, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
+ Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
};
use typst_library::math::{EquationElem, MathSize};
-use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem};
-use typst_library::visualize::{FixedStroke, Paint};
+use typst_library::text::{features, language, Font, Glyph, TextElem, TextItem};
use typst_syntax::Span;
-use typst_utils::default_math_class;
+use typst_utils::{default_math_class, Get};
use unicode_math_class::MathClass;
-use super::{stretch_glyph, MathContext, Scaled};
+use super::MathContext;
+use crate::inline::create_shape_plan;
use crate::modifiers::{FrameModifiers, FrameModify};
+/// Maximum number of times extenders can be repeated.
+const MAX_REPEATS: usize = 1024;
+
+#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone)]
pub enum MathFragment {
Glyph(GlyphFragment),
- Variant(VariantFragment),
Frame(FrameFragment),
Spacing(Abs, bool),
Space(Abs),
@@ -33,13 +37,18 @@ pub enum MathFragment {
impl MathFragment {
pub fn size(&self) -> Size {
- Size::new(self.width(), self.height())
+ match self {
+ Self::Glyph(glyph) => glyph.size,
+ Self::Frame(fragment) => fragment.frame.size(),
+ Self::Spacing(amount, _) => Size::with_x(*amount),
+ Self::Space(amount) => Size::with_x(*amount),
+ _ => Size::zero(),
+ }
}
pub fn width(&self) -> Abs {
match self {
- Self::Glyph(glyph) => glyph.width,
- Self::Variant(variant) => variant.frame.width(),
+ Self::Glyph(glyph) => glyph.size.x,
Self::Frame(fragment) => fragment.frame.width(),
Self::Spacing(amount, _) => *amount,
Self::Space(amount) => *amount,
@@ -49,8 +58,7 @@ impl MathFragment {
pub fn height(&self) -> Abs {
match self {
- Self::Glyph(glyph) => glyph.height(),
- Self::Variant(variant) => variant.frame.height(),
+ Self::Glyph(glyph) => glyph.size.y,
Self::Frame(fragment) => fragment.frame.height(),
_ => Abs::zero(),
}
@@ -58,17 +66,15 @@ impl MathFragment {
pub fn ascent(&self) -> Abs {
match self {
- Self::Glyph(glyph) => glyph.ascent,
- Self::Variant(variant) => variant.frame.ascent(),
- Self::Frame(fragment) => fragment.frame.baseline(),
+ Self::Glyph(glyph) => glyph.ascent(),
+ Self::Frame(fragment) => fragment.frame.ascent(),
_ => Abs::zero(),
}
}
pub fn descent(&self) -> Abs {
match self {
- Self::Glyph(glyph) => glyph.descent,
- Self::Variant(variant) => variant.frame.descent(),
+ Self::Glyph(glyph) => glyph.descent(),
Self::Frame(fragment) => fragment.frame.descent(),
_ => Abs::zero(),
}
@@ -85,7 +91,6 @@ impl MathFragment {
pub fn class(&self) -> MathClass {
match self {
Self::Glyph(glyph) => glyph.class,
- Self::Variant(variant) => variant.class,
Self::Frame(fragment) => fragment.class,
Self::Spacing(_, _) => MathClass::Space,
Self::Space(_) => MathClass::Space,
@@ -98,7 +103,6 @@ impl MathFragment {
pub fn math_size(&self) -> Option<MathSize> {
match self {
Self::Glyph(glyph) => Some(glyph.math_size),
- Self::Variant(variant) => Some(variant.math_size),
Self::Frame(fragment) => Some(fragment.math_size),
_ => None,
}
@@ -106,8 +110,7 @@ impl MathFragment {
pub fn font_size(&self) -> Option<Abs> {
match self {
- Self::Glyph(glyph) => Some(glyph.font_size),
- Self::Variant(variant) => Some(variant.font_size),
+ Self::Glyph(glyph) => Some(glyph.item.size),
Self::Frame(fragment) => Some(fragment.font_size),
_ => None,
}
@@ -116,7 +119,6 @@ impl MathFragment {
pub fn set_class(&mut self, class: MathClass) {
match self {
Self::Glyph(glyph) => glyph.class = class,
- Self::Variant(variant) => variant.class = class,
Self::Frame(fragment) => fragment.class = class,
_ => {}
}
@@ -125,7 +127,6 @@ impl MathFragment {
pub fn set_limits(&mut self, limits: Limits) {
match self {
Self::Glyph(glyph) => glyph.limits = limits,
- Self::Variant(variant) => variant.limits = limits,
Self::Frame(fragment) => fragment.limits = limits,
_ => {}
}
@@ -149,7 +150,6 @@ impl MathFragment {
pub fn is_text_like(&self) -> bool {
match self {
Self::Glyph(glyph) => !glyph.extended_shape,
- Self::Variant(variant) => !variant.extended_shape,
MathFragment::Frame(frame) => frame.text_like,
_ => false,
}
@@ -158,7 +158,6 @@ impl MathFragment {
pub fn italics_correction(&self) -> Abs {
match self {
Self::Glyph(glyph) => glyph.italics_correction,
- Self::Variant(variant) => variant.italics_correction,
Self::Frame(fragment) => fragment.italics_correction,
_ => Abs::zero(),
}
@@ -167,7 +166,6 @@ impl MathFragment {
pub fn accent_attach(&self) -> (Abs, Abs) {
match self {
Self::Glyph(glyph) => glyph.accent_attach,
- Self::Variant(variant) => variant.accent_attach,
Self::Frame(fragment) => fragment.accent_attach,
_ => (self.width() / 2.0, self.width() / 2.0),
}
@@ -176,7 +174,6 @@ impl MathFragment {
pub fn into_frame(self) -> Frame {
match self {
Self::Glyph(glyph) => glyph.into_frame(),
- Self::Variant(variant) => variant.frame,
Self::Frame(fragment) => fragment.frame,
Self::Tag(tag) => {
let mut frame = Frame::soft(Size::zero());
@@ -190,7 +187,6 @@ impl MathFragment {
pub fn limits(&self) -> Limits {
match self {
MathFragment::Glyph(glyph) => glyph.limits,
- MathFragment::Variant(variant) => variant.limits,
MathFragment::Frame(fragment) => fragment.limits,
_ => Limits::Never,
}
@@ -198,11 +194,31 @@ impl MathFragment {
/// If no kern table is provided for a corner, a kerning amount of zero is
/// assumed.
- pub fn kern_at_height(&self, ctx: &MathContext, corner: Corner, height: Abs) -> Abs {
+ pub fn kern_at_height(&self, corner: Corner, height: Abs) -> Abs {
match self {
Self::Glyph(glyph) => {
- kern_at_height(ctx, glyph.font_size, glyph.id, corner, height)
- .unwrap_or_default()
+ // For glyph assemblies we pick either the start or end glyph
+ // depending on the corner.
+ let is_vertical =
+ glyph.item.glyphs.iter().all(|glyph| glyph.y_advance != Em::zero());
+ let glyph_index = match (is_vertical, corner) {
+ (true, Corner::TopLeft | Corner::TopRight) => {
+ glyph.item.glyphs.len() - 1
+ }
+ (false, Corner::TopRight | Corner::BottomRight) => {
+ glyph.item.glyphs.len() - 1
+ }
+ _ => 0,
+ };
+
+ kern_at_height(
+ &glyph.item.font,
+ GlyphId(glyph.item.glyphs[glyph_index].id),
+ corner,
+ Em::from_length(height, glyph.item.size),
+ )
+ .unwrap_or_default()
+ .at(glyph.item.size)
}
_ => Abs::zero(),
}
@@ -215,12 +231,6 @@ impl From<GlyphFragment> for MathFragment {
}
}
-impl From<VariantFragment> for MathFragment {
- fn from(variant: VariantFragment) -> Self {
- Self::Variant(variant)
- }
-}
-
impl From<FrameFragment> for MathFragment {
fn from(fragment: FrameFragment) -> Self {
Self::Frame(fragment)
@@ -229,266 +239,282 @@ impl From<FrameFragment> for MathFragment {
#[derive(Clone)]
pub struct GlyphFragment {
- pub id: GlyphId,
- pub c: char,
- pub font: Font,
- pub lang: Lang,
- pub region: Option<Region>,
- pub fill: Paint,
- pub stroke: Option<FixedStroke>,
- pub shift: Abs,
- pub width: Abs,
- pub ascent: Abs,
- pub descent: Abs,
+ // Text stuff.
+ pub item: TextItem,
+ pub base_glyph: Glyph,
+ // Math stuff.
+ pub size: Size,
+ pub baseline: Option<Abs>,
pub italics_correction: Abs,
pub accent_attach: (Abs, Abs),
- pub font_size: Abs,
- pub class: MathClass,
pub math_size: MathSize,
- pub span: Span,
- pub modifiers: FrameModifiers,
+ pub class: MathClass,
pub limits: Limits,
pub extended_shape: bool,
+ pub mid_stretched: Option<bool>,
+ // External frame stuff.
+ pub modifiers: FrameModifiers,
+ pub shift: Abs,
+ pub align: Abs,
}
impl GlyphFragment {
- pub fn new(ctx: &MathContext, styles: StyleChain, c: char, span: Span) -> Self {
- let id = ctx.ttf.glyph_index(c).unwrap_or_default();
- let id = Self::adjust_glyph_index(ctx, id);
- Self::with_id(ctx, styles, c, id, span)
- }
-
- pub fn try_new(
- ctx: &MathContext,
+ /// Calls `new` with the given character.
+ pub fn new_char(
+ font: &Font,
styles: StyleChain,
c: char,
span: Span,
- ) -> Option<Self> {
- let id = ctx.ttf.glyph_index(c)?;
- let id = Self::adjust_glyph_index(ctx, id);
- Some(Self::with_id(ctx, styles, c, id, span))
+ ) -> SourceResult<Self> {
+ Self::new(font, styles, c.encode_utf8(&mut [0; 4]), span)
}
- pub fn with_id(
- ctx: &MathContext,
+ /// Try to create a new glyph out of the given string. Will bail if the
+ /// result from shaping the string is not a single glyph or is a tofu.
+ #[comemo::memoize]
+ pub fn new(
+ font: &Font,
styles: StyleChain,
- c: char,
- id: GlyphId,
+ text: &str,
span: Span,
- ) -> Self {
+ ) -> SourceResult<GlyphFragment> {
+ let mut buffer = UnicodeBuffer::new();
+ buffer.push_str(text);
+ buffer.set_language(language(styles));
+ // TODO: Use `rustybuzz::script::MATH` once
+ // https://github.com/harfbuzz/rustybuzz/pull/165 is released.
+ buffer.set_script(
+ rustybuzz::Script::from_iso15924_tag(ttf_parser::Tag::from_bytes(b"math"))
+ .unwrap(),
+ );
+ buffer.set_direction(rustybuzz::Direction::LeftToRight);
+ buffer.set_flags(BufferFlags::REMOVE_DEFAULT_IGNORABLES);
+
+ let features = features(styles);
+ let plan = create_shape_plan(
+ font,
+ buffer.direction(),
+ buffer.script(),
+ buffer.language().as_ref(),
+ &features,
+ );
+
+ let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
+ if buffer.len() != 1 {
+ bail!(span, "did not get a single glyph after shaping {}", text);
+ }
+
+ let info = buffer.glyph_infos()[0];
+ let pos = buffer.glyph_positions()[0];
+
+ // TODO: add support for coverage and fallback, like in normal text shaping.
+ if info.glyph_id == 0 {
+ bail!(span, "current font is missing a glyph for {}", text);
+ }
+
+ let cluster = info.cluster as usize;
+ let c = text[cluster..].chars().next().unwrap();
+ let limits = Limits::for_char(c);
let class = EquationElem::class_in(styles)
.or_else(|| default_math_class(c))
.unwrap_or(MathClass::Normal);
- let mut fragment = Self {
- id,
- c,
- font: ctx.font.clone(),
- lang: TextElem::lang_in(styles),
- region: TextElem::region_in(styles),
+ let glyph = Glyph {
+ id: info.glyph_id as u16,
+ x_advance: font.to_em(pos.x_advance),
+ x_offset: font.to_em(pos.x_offset),
+ y_advance: font.to_em(pos.y_advance),
+ y_offset: font.to_em(pos.y_offset),
+ range: 0..text.len().saturating_as(),
+ span: (span, 0),
+ };
+
+ let item = TextItem {
+ font: font.clone(),
+ size: TextElem::size_in(styles),
fill: TextElem::fill_in(styles).as_decoration(),
stroke: TextElem::stroke_in(styles).map(|s| s.unwrap_or_default()),
- shift: TextElem::baseline_in(styles),
- font_size: TextElem::size_in(styles),
+ lang: TextElem::lang_in(styles),
+ region: TextElem::region_in(styles),
+ text: text.into(),
+ glyphs: vec![glyph.clone()],
+ };
+
+ let mut fragment = Self {
+ item,
+ base_glyph: glyph,
+ // Math
math_size: EquationElem::size_in(styles),
- width: Abs::zero(),
- ascent: Abs::zero(),
- descent: Abs::zero(),
- limits: Limits::for_char(c),
+ class,
+ limits,
+ mid_stretched: None,
+ // Math in need of updating.
+ extended_shape: false,
italics_correction: Abs::zero(),
accent_attach: (Abs::zero(), Abs::zero()),
- class,
- span,
+ size: Size::zero(),
+ baseline: None,
+ // Misc
+ align: Abs::zero(),
+ shift: TextElem::baseline_in(styles),
modifiers: FrameModifiers::get_in(styles),
- extended_shape: false,
};
- fragment.set_id(ctx, id);
- fragment
- }
-
- /// Apply GSUB substitutions.
- fn adjust_glyph_index(ctx: &MathContext, id: GlyphId) -> GlyphId {
- if let Some(glyphwise_tables) = &ctx.glyphwise_tables {
- glyphwise_tables.iter().fold(id, |id, table| table.apply(id))
- } else {
- id
- }
+ fragment.update_glyph();
+ Ok(fragment)
}
/// Sets element id and boxes in appropriate way without changing other
/// styles. This is used to replace the glyph with a stretch variant.
- pub fn set_id(&mut self, ctx: &MathContext, id: GlyphId) {
- let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default();
- let italics = italics_correction(ctx, id, self.font_size).unwrap_or_default();
- let bbox = ctx.ttf.glyph_bounding_box(id).unwrap_or(Rect {
- x_min: 0,
- y_min: 0,
- x_max: 0,
- y_max: 0,
- });
+ pub fn update_glyph(&mut self) {
+ let id = GlyphId(self.item.glyphs[0].id);
- let mut width = advance.scaled(ctx, self.font_size);
+ let extended_shape = is_extended_shape(&self.item.font, id);
+ let italics = italics_correction(&self.item.font, id).unwrap_or_default();
+ let width = self.item.width();
+ if !extended_shape {
+ self.item.glyphs[0].x_advance += italics;
+ }
+ let italics = italics.at(self.item.size);
+
+ let (ascent, descent) =
+ ascent_descent(&self.item.font, id).unwrap_or((Em::zero(), Em::zero()));
// The fallback for accents is half the width plus or minus the italics
// correction. This is similar to how top and bottom attachments are
// shifted. For bottom accents we do not use the accent attach of the
// base as it is meant for top acccents.
- let top_accent_attach =
- accent_attach(ctx, id, self.font_size).unwrap_or((width + italics) / 2.0);
+ let top_accent_attach = accent_attach(&self.item.font, id)
+ .map(|x| x.at(self.item.size))
+ .unwrap_or((width + italics) / 2.0);
let bottom_accent_attach = (width - italics) / 2.0;
- let extended_shape = is_extended_shape(ctx, id);
- if !extended_shape {
- width += italics;
- }
-
- self.id = id;
- self.width = width;
- self.ascent = bbox.y_max.scaled(ctx, self.font_size);
- self.descent = -bbox.y_min.scaled(ctx, self.font_size);
+ self.baseline = Some(ascent.at(self.item.size));
+ self.size = Size::new(
+ self.item.width(),
+ ascent.at(self.item.size) + descent.at(self.item.size),
+ );
self.italics_correction = italics;
self.accent_attach = (top_accent_attach, bottom_accent_attach);
self.extended_shape = extended_shape;
}
- pub fn height(&self) -> Abs {
- self.ascent + self.descent
- }
-
- pub fn into_variant(self) -> VariantFragment {
- VariantFragment {
- c: self.c,
- font_size: self.font_size,
- italics_correction: self.italics_correction,
- accent_attach: self.accent_attach,
- class: self.class,
- math_size: self.math_size,
- span: self.span,
- limits: self.limits,
- extended_shape: self.extended_shape,
- frame: self.into_frame(),
- mid_stretched: None,
- }
+ // Reset a GlyphFragment's text field and math properties back to its
+ // base_id's. This is used to return a glyph to its unstretched state.
+ pub fn reset_glyph(&mut self) {
+ self.align = Abs::zero();
+ self.item.glyphs = vec![self.base_glyph.clone()];
+ self.update_glyph();
}
- pub fn into_frame(self) -> Frame {
- let item = TextItem {
- font: self.font.clone(),
- size: self.font_size,
- fill: self.fill,
- stroke: self.stroke,
- lang: self.lang,
- region: self.region,
- text: self.c.into(),
- glyphs: vec![Glyph {
- id: self.id.0,
- x_advance: Em::from_length(self.width, self.font_size),
- x_offset: Em::zero(),
- range: 0..self.c.len_utf8() as u16,
- span: (self.span, 0),
- }],
- };
- let size = Size::new(self.width, self.ascent + self.descent);
- let mut frame = Frame::soft(size);
- frame.set_baseline(self.ascent);
- frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item));
- frame.modify(&self.modifiers);
- frame
+ pub fn baseline(&self) -> Abs {
+ self.ascent()
}
- pub fn make_script_size(&mut self, ctx: &MathContext) {
- let alt_id =
- 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);
- }
+ /// The distance from the baseline to the top of the frame.
+ pub fn ascent(&self) -> Abs {
+ self.baseline.unwrap_or(self.size.y)
}
- 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);
- }
+ /// The distance from the baseline to the bottom of the frame.
+ pub fn descent(&self) -> Abs {
+ self.size.y - self.ascent()
}
- 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);
- }
+ pub fn into_frame(self) -> Frame {
+ let mut frame = Frame::soft(self.size);
+ frame.set_baseline(self.baseline());
+ frame.push(
+ Point::with_y(self.ascent() + self.shift + self.align),
+ FrameItem::Text(self.item),
+ );
+ frame.modify(&self.modifiers);
+ frame
}
/// Try to stretch a glyph to a desired height.
- pub fn stretch_vertical(self, ctx: &mut MathContext, height: Abs) -> VariantFragment {
- stretch_glyph(ctx, self, height, Axis::Y)
+ pub fn stretch_vertical(&mut self, ctx: &mut MathContext, height: Abs) {
+ self.stretch(ctx, height, Axis::Y)
}
/// Try to stretch a glyph to a desired width.
- pub fn stretch_horizontal(
- self,
- ctx: &mut MathContext,
- width: Abs,
- ) -> VariantFragment {
- stretch_glyph(ctx, self, width, Axis::X)
- }
-}
+ pub fn stretch_horizontal(&mut self, ctx: &mut MathContext, width: Abs) {
+ self.stretch(ctx, width, Axis::X)
+ }
+
+ /// Try to stretch a glyph to a desired width or height.
+ ///
+ /// The resulting frame may not have the exact desired width or height.
+ pub fn stretch(&mut self, ctx: &mut MathContext, target: Abs, axis: Axis) {
+ self.reset_glyph();
+
+ // If the base glyph is good enough, use it.
+ let mut advance = self.size.get(axis);
+ if axis == Axis::X && !self.extended_shape {
+ // For consistency, we subtract the italics correction from the
+ // glyph's width if it was added in `update_glyph`.
+ advance -= self.italics_correction;
+ }
+ if target <= advance {
+ return;
+ }
-impl Debug for GlyphFragment {
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- write!(f, "GlyphFragment({:?})", self.c)
- }
-}
+ let id = GlyphId(self.item.glyphs[0].id);
+ let font = self.item.font.clone();
+ let Some(construction) = glyph_construction(&font, id, axis) else { return };
+
+ // Search for a pre-made variant with a good advance.
+ let mut best_id = id;
+ let mut best_advance = advance;
+ for variant in construction.variants {
+ best_id = variant.variant_glyph;
+ best_advance =
+ self.item.font.to_em(variant.advance_measurement).at(self.item.size);
+ if target <= best_advance {
+ break;
+ }
+ }
-#[derive(Clone)]
-pub struct VariantFragment {
- pub c: char,
- pub italics_correction: Abs,
- pub accent_attach: (Abs, Abs),
- pub frame: Frame,
- pub font_size: Abs,
- pub class: MathClass,
- pub math_size: MathSize,
- pub span: Span,
- pub limits: Limits,
- pub mid_stretched: Option<bool>,
- pub extended_shape: bool,
-}
+ // This is either good or the best we've got.
+ if target <= best_advance || construction.assembly.is_none() {
+ self.item.glyphs[0].id = best_id.0;
+ self.item.glyphs[0].x_advance =
+ self.item.font.x_advance(best_id.0).unwrap_or_default();
+ self.item.glyphs[0].x_offset = Em::zero();
+ self.item.glyphs[0].y_advance =
+ self.item.font.y_advance(best_id.0).unwrap_or_default();
+ self.item.glyphs[0].y_offset = Em::zero();
+ self.update_glyph();
+ return;
+ }
+
+ // Assemble from parts.
+ let assembly = construction.assembly.unwrap();
+ let min_overlap = min_connector_overlap(&self.item.font)
+ .unwrap_or_default()
+ .at(self.item.size);
+ assemble(ctx, self, assembly, min_overlap, target, axis);
+ }
-impl VariantFragment {
/// Vertically adjust the fragment's frame so that it is centered
/// on the axis.
- pub fn center_on_axis(&mut self, ctx: &MathContext) {
- self.align_on_axis(ctx, VAlignment::Horizon)
+ pub fn center_on_axis(&mut self) {
+ self.align_on_axis(VAlignment::Horizon);
}
/// Vertically adjust the fragment's frame so that it is aligned
/// to the given alignment on the axis.
- pub fn align_on_axis(&mut self, ctx: &MathContext, align: VAlignment) {
- let h = self.frame.height();
- let axis = ctx.constants.axis_height().scaled(ctx, self.font_size);
- self.frame.set_baseline(align.inv().position(h + axis * 2.0));
+ pub fn align_on_axis(&mut self, align: VAlignment) {
+ let h = self.size.y;
+ let axis = axis_height(&self.item.font).unwrap().at(self.item.size);
+ self.align += self.baseline();
+ self.baseline = Some(align.inv().position(h + axis * 2.0));
+ self.align -= self.baseline();
}
}
-impl Debug for VariantFragment {
+impl Debug for GlyphFragment {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- write!(f, "VariantFragment({:?})", self.c)
+ write!(f, "GlyphFragment({:?})", self.item.text)
}
}
@@ -566,46 +592,47 @@ impl FrameFragment {
}
}
+fn ascent_descent(font: &Font, id: GlyphId) -> Option<(Em, Em)> {
+ let bbox = font.ttf().glyph_bounding_box(id)?;
+ Some((font.to_em(bbox.y_max), -font.to_em(bbox.y_min)))
+}
+
/// Look up the italics correction for a glyph.
-fn italics_correction(ctx: &MathContext, id: GlyphId, font_size: Abs) -> Option<Abs> {
- Some(
- ctx.table
- .glyph_info?
- .italic_corrections?
- .get(id)?
- .scaled(ctx, font_size),
- )
+fn italics_correction(font: &Font, id: GlyphId) -> Option<Em> {
+ font.ttf()
+ .tables()
+ .math?
+ .glyph_info?
+ .italic_corrections?
+ .get(id)
+ .map(|value| font.to_em(value.value))
}
/// Loop up the top accent attachment position for a glyph.
-fn accent_attach(ctx: &MathContext, id: GlyphId, font_size: Abs) -> Option<Abs> {
- Some(
- ctx.table
- .glyph_info?
- .top_accent_attachments?
- .get(id)?
- .scaled(ctx, font_size),
- )
+fn accent_attach(font: &Font, id: GlyphId) -> Option<Em> {
+ font.ttf()
+ .tables()
+ .math?
+ .glyph_info?
+ .top_accent_attachments?
+ .get(id)
+ .map(|value| font.to_em(value.value))
}
/// Look up whether a glyph is an extended shape.
-fn is_extended_shape(ctx: &MathContext, id: GlyphId) -> bool {
- ctx.table
- .glyph_info
- .and_then(|info| info.extended_shapes)
- .and_then(|info| info.get(id))
+fn is_extended_shape(font: &Font, id: GlyphId) -> bool {
+ font.ttf()
+ .tables()
+ .math
+ .and_then(|math| math.glyph_info)
+ .and_then(|glyph_info| glyph_info.extended_shapes)
+ .and_then(|coverage| coverage.get(id))
.is_some()
}
/// Look up a kerning value at a specific corner and height.
-fn kern_at_height(
- ctx: &MathContext,
- font_size: Abs,
- id: GlyphId,
- corner: Corner,
- height: Abs,
-) -> Option<Abs> {
- let kerns = ctx.table.glyph_info?.kern_infos?.get(id)?;
+fn kern_at_height(font: &Font, id: GlyphId, corner: Corner, height: Em) -> Option<Em> {
+ let kerns = font.ttf().tables().math?.glyph_info?.kern_infos?.get(id)?;
let kern = match corner {
Corner::TopLeft => kerns.top_left,
Corner::TopRight => kerns.top_right,
@@ -614,11 +641,187 @@ fn kern_at_height(
}?;
let mut i = 0;
- while i < kern.count() && height > kern.height(i)?.scaled(ctx, font_size) {
+ while i < kern.count() && height > font.to_em(kern.height(i)?.value) {
i += 1;
}
- Some(kern.kern(i)?.scaled(ctx, font_size))
+ Some(font.to_em(kern.kern(i)?.value))
+}
+
+fn axis_height(font: &Font) -> Option<Em> {
+ Some(font.to_em(font.ttf().tables().math?.constants?.axis_height().value))
+}
+
+pub fn stretch_axes(font: &Font, id: u16) -> Axes<bool> {
+ let id = GlyphId(id);
+ let horizontal = font
+ .ttf()
+ .tables()
+ .math
+ .and_then(|math| math.variants)
+ .and_then(|variants| variants.horizontal_constructions.get(id))
+ .is_some();
+ let vertical = font
+ .ttf()
+ .tables()
+ .math
+ .and_then(|math| math.variants)
+ .and_then(|variants| variants.vertical_constructions.get(id))
+ .is_some();
+
+ Axes::new(horizontal, vertical)
+}
+
+fn min_connector_overlap(font: &Font) -> Option<Em> {
+ font.ttf()
+ .tables()
+ .math?
+ .variants
+ .map(|variants| font.to_em(variants.min_connector_overlap))
+}
+
+fn glyph_construction(font: &Font, id: GlyphId, axis: Axis) -> Option<GlyphConstruction> {
+ font.ttf()
+ .tables()
+ .math?
+ .variants
+ .map(|variants| match axis {
+ Axis::X => variants.horizontal_constructions,
+ Axis::Y => variants.vertical_constructions,
+ })?
+ .get(id)
+}
+
+/// Assemble a glyph from parts.
+fn assemble(
+ ctx: &mut MathContext,
+ base: &mut GlyphFragment,
+ assembly: GlyphAssembly,
+ min_overlap: Abs,
+ target: Abs,
+ axis: Axis,
+) {
+ // Determine the number of times the extenders need to be repeated as well
+ // as a ratio specifying how much to spread the parts apart
+ // (0 = maximal overlap, 1 = minimal overlap).
+ let mut full;
+ let mut ratio;
+ let mut repeat = 0;
+ loop {
+ full = Abs::zero();
+ ratio = 0.0;
+
+ let mut parts = parts(assembly, repeat).peekable();
+ let mut growable = Abs::zero();
+
+ while let Some(part) = parts.next() {
+ let mut advance = base.item.font.to_em(part.full_advance).at(base.item.size);
+ if let Some(next) = parts.peek() {
+ let max_overlap = base
+ .item
+ .font
+ .to_em(part.end_connector_length.min(next.start_connector_length))
+ .at(base.item.size);
+ if max_overlap < min_overlap {
+ // This condition happening is indicative of a bug in the
+ // font.
+ ctx.engine.sink.warn(warning!(
+ base.item.glyphs[0].span.0,
+ "glyph has assembly parts with overlap less than minConnectorOverlap";
+ hint: "its rendering may appear broken - this is probably a font bug";
+ hint: "please file an issue at https://github.com/typst/typst/issues"
+ ));
+ }
+
+ advance -= max_overlap;
+ growable += max_overlap - min_overlap;
+ }
+
+ full += advance;
+ }
+
+ if full < target {
+ let delta = target - full;
+ ratio = (delta / growable).min(1.0);
+ full += ratio * growable;
+ }
+
+ if target <= full || repeat >= MAX_REPEATS {
+ break;
+ }
+
+ repeat += 1;
+ }
+
+ let mut glyphs = vec![];
+ let mut parts = parts(assembly, repeat).peekable();
+ while let Some(part) = parts.next() {
+ let mut advance = base.item.font.to_em(part.full_advance).at(base.item.size);
+ if let Some(next) = parts.peek() {
+ let max_overlap = base
+ .item
+ .font
+ .to_em(part.end_connector_length.min(next.start_connector_length))
+ .at(base.item.size);
+ advance -= max_overlap;
+ advance += ratio * (max_overlap - min_overlap);
+ }
+ let (x, y) = match axis {
+ Axis::X => (Em::from_length(advance, base.item.size), Em::zero()),
+ Axis::Y => (Em::zero(), Em::from_length(advance, base.item.size)),
+ };
+ glyphs.push(Glyph {
+ id: part.glyph_id.0,
+ x_advance: x,
+ x_offset: Em::zero(),
+ y_advance: y,
+ y_offset: Em::zero(),
+ ..base.item.glyphs[0].clone()
+ });
+ }
+
+ match axis {
+ Axis::X => base.size.x = full,
+ Axis::Y => {
+ base.baseline = None;
+ base.size.y = full;
+ base.size.x = glyphs
+ .iter()
+ .map(|glyph| base.item.font.x_advance(glyph.id).unwrap_or_default())
+ .max()
+ .unwrap_or_default()
+ .at(base.item.size);
+ }
+ }
+
+ base.item.glyphs = glyphs;
+ base.italics_correction = base
+ .item
+ .font
+ .to_em(assembly.italics_correction.value)
+ .at(base.item.size);
+ if axis == Axis::X {
+ base.accent_attach = (full / 2.0, full / 2.0);
+ }
+ base.mid_stretched = None;
+ base.extended_shape = true;
+}
+
+/// Return an iterator over the assembly's parts with extenders repeated the
+/// specified number of times.
+fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ {
+ assembly.parts.into_iter().flat_map(move |part| {
+ let count = if part.part_flags.extender() { repeat } else { 1 };
+ std::iter::repeat_n(part, count)
+ })
+}
+
+pub fn has_dtls_feat(font: &Font) -> bool {
+ font.ttf()
+ .tables()
+ .gsub
+ .and_then(|gsub| gsub.features.index(ttf_parser::Tag::from_bytes(b"dtls")))
+ .is_some()
}
/// Describes in which situation a frame should use limits for attachments.
@@ -671,56 +874,3 @@ impl Limits {
fn is_integral_char(c: char) -> bool {
('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c)
}
-
-/// An OpenType substitution table that is applicable to glyph-wise substitutions.
-pub enum GlyphwiseSubsts<'a> {
- Single(SingleSubstitution<'a>),
- Alternate(AlternateSubstitution<'a>, u32),
-}
-
-impl<'a> GlyphwiseSubsts<'a> {
- pub fn new(gsub: Option<LayoutTable<'a>>, feature: Feature) -> Option<Self> {
- let gsub = gsub?;
- let table = gsub
- .features
- .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)?;
- match table {
- SubstitutionSubtable::Single(single_glyphs) => {
- Some(Self::Single(single_glyphs))
- }
- SubstitutionSubtable::Alternate(alt_glyphs) => {
- Some(Self::Alternate(alt_glyphs, feature.value))
- }
- _ => None,
- }
- }
-
- 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
- .get(glyph_id)
- .map(|_| GlyphId(glyph_id.0.wrapping_add(*delta as u16))),
- SingleSubstitution::Format2 { coverage, substitutes } => {
- coverage.get(glyph_id).and_then(|idx| substitutes.get(idx))
- }
- },
- Self::Alternate(alternate, value) => alternate
- .coverage
- .get(glyph_id)
- .and_then(|idx| alternate.alternate_sets.get(idx))
- .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, None).unwrap_or(glyph_id)
- }
-}
diff --git a/crates/typst-layout/src/math/lr.rs b/crates/typst-layout/src/math/lr.rs
index bf823541..e0caf417 100644
--- a/crates/typst-layout/src/math/lr.rs
+++ b/crates/typst-layout/src/math/lr.rs
@@ -45,20 +45,20 @@ pub fn layout_lr(
// Scale up fragments at both ends.
match inner_fragments {
- [one] => scale(ctx, styles, one, relative_to, height, None),
+ [one] => scale(ctx, one, relative_to, height, None),
[first, .., last] => {
- scale(ctx, styles, first, relative_to, height, Some(MathClass::Opening));
- scale(ctx, styles, last, relative_to, height, Some(MathClass::Closing));
+ scale(ctx, first, relative_to, height, Some(MathClass::Opening));
+ scale(ctx, last, relative_to, height, Some(MathClass::Closing));
}
_ => {}
}
- // Handle MathFragment::Variant fragments that should be scaled up.
+ // Handle MathFragment::Glyph fragments that should be scaled up.
for fragment in inner_fragments.iter_mut() {
- if let MathFragment::Variant(ref mut variant) = fragment {
- if variant.mid_stretched == Some(false) {
- variant.mid_stretched = Some(true);
- scale(ctx, styles, fragment, relative_to, height, Some(MathClass::Large));
+ if let MathFragment::Glyph(ref mut glyph) = fragment {
+ if glyph.mid_stretched == Some(false) {
+ glyph.mid_stretched = Some(true);
+ scale(ctx, fragment, relative_to, height, Some(MathClass::Large));
}
}
}
@@ -95,18 +95,9 @@ pub fn layout_mid(
let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?;
for fragment in &mut fragments {
- match fragment {
- MathFragment::Glyph(glyph) => {
- let mut new = glyph.clone().into_variant();
- new.mid_stretched = Some(false);
- new.class = MathClass::Fence;
- *fragment = MathFragment::Variant(new);
- }
- MathFragment::Variant(variant) => {
- variant.mid_stretched = Some(false);
- variant.class = MathClass::Fence;
- }
- _ => {}
+ if let MathFragment::Glyph(ref mut glyph) = fragment {
+ glyph.mid_stretched = Some(false);
+ glyph.class = MathClass::Fence;
}
}
@@ -117,7 +108,6 @@ pub fn layout_mid(
/// Scale a math fragment to a height.
fn scale(
ctx: &mut MathContext,
- styles: StyleChain,
fragment: &mut MathFragment,
relative_to: Abs,
height: Rel<Abs>,
@@ -132,7 +122,6 @@ fn scale(
let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default());
stretch_fragment(
ctx,
- styles,
fragment,
Some(Axis::Y),
Some(relative_to),
diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs
index e509cecc..278b1343 100644
--- a/crates/typst-layout/src/math/mat.rs
+++ b/crates/typst-layout/src/math/mat.rs
@@ -9,8 +9,8 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape};
use typst_syntax::Span;
use super::{
- alignments, delimiter_alignment, style_for_denominator, AlignmentResult,
- FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL,
+ alignments, style_for_denominator, AlignmentResult, FrameFragment, GlyphFragment,
+ LeftRightAlternator, MathContext, DELIM_SHORT_FALL,
};
const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
@@ -183,8 +183,12 @@ fn layout_body(
// We pad ascent and descent with the ascent and descent of the paren
// to ensure that normal matrices are aligned with others unless they are
// way too big.
- let paren =
- GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached());
+ let paren = GlyphFragment::new_char(
+ ctx.font,
+ styles.chain(&denom_style),
+ '(',
+ Span::detached(),
+ )?;
for (column, col) in columns.iter().zip(&mut cols) {
for (cell, (ascent, descent)) in column.iter().zip(&mut heights) {
@@ -202,8 +206,8 @@ fn layout_body(
));
}
- ascent.set_max(cell.ascent().max(paren.ascent));
- descent.set_max(cell.descent().max(paren.descent));
+ ascent.set_max(cell.ascent().max(paren.ascent()));
+ descent.set_max(cell.descent().max(paren.descent()));
col.push(cell);
}
@@ -312,19 +316,19 @@ fn layout_delimiters(
let target = height + VERTICAL_PADDING.of(height);
frame.set_baseline(height / 2.0 + axis);
- if let Some(left) = left {
- let mut left = GlyphFragment::new(ctx, styles, left, span)
- .stretch_vertical(ctx, target - short_fall);
- left.align_on_axis(ctx, delimiter_alignment(left.c));
+ if let Some(left_c) = left {
+ let mut left = GlyphFragment::new_char(ctx.font, styles, left_c, span)?;
+ left.stretch_vertical(ctx, target - short_fall);
+ left.center_on_axis();
ctx.push(left);
}
ctx.push(FrameFragment::new(styles, frame));
- if let Some(right) = right {
- let mut right = GlyphFragment::new(ctx, styles, right, span)
- .stretch_vertical(ctx, target - short_fall);
- right.align_on_axis(ctx, delimiter_alignment(right.c));
+ if let Some(right_c) = right {
+ let mut right = GlyphFragment::new_char(ctx.font, styles, right_c, span)?;
+ right.stretch_vertical(ctx, target - short_fall);
+ right.center_on_axis();
ctx.push(right);
}
diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs
index 708a4443..5fd22e57 100644
--- a/crates/typst-layout/src/math/mod.rs
+++ b/crates/typst-layout/src/math/mod.rs
@@ -13,8 +13,6 @@ mod stretch;
mod text;
mod underover;
-use rustybuzz::Feature;
-use ttf_parser::Tag;
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{
@@ -30,7 +28,7 @@ use typst_library::math::*;
use typst_library::model::ParElem;
use typst_library::routines::{Arenas, RealizationKind};
use typst_library::text::{
- families, features, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem,
+ families, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem,
};
use typst_library::World;
use typst_syntax::Span;
@@ -38,11 +36,11 @@ use typst_utils::Numeric;
use unicode_math_class::MathClass;
use self::fragment::{
- FrameFragment, GlyphFragment, GlyphwiseSubsts, Limits, MathFragment, VariantFragment,
+ has_dtls_feat, stretch_axes, FrameFragment, GlyphFragment, Limits, MathFragment,
};
use self::run::{LeftRightAlternator, MathRun, MathRunFrameBuilder};
use self::shared::*;
-use self::stretch::{stretch_fragment, stretch_glyph};
+use self::stretch::stretch_fragment;
/// Layout an inline equation (in a paragraph).
#[typst_macros::time(span = elem.span())]
@@ -58,7 +56,7 @@ pub fn layout_equation_inline(
let font = find_math_font(engine, styles, elem.span())?;
let mut locator = locator.split();
- let mut ctx = MathContext::new(engine, &mut locator, styles, region, &font);
+ let mut ctx = MathContext::new(engine, &mut locator, region, &font);
let scale_style = style_for_script_scale(&ctx);
let styles = styles.chain(&scale_style);
@@ -113,7 +111,7 @@ pub fn layout_equation_block(
let font = find_math_font(engine, styles, span)?;
let mut locator = locator.split();
- let mut ctx = MathContext::new(engine, &mut locator, styles, regions.base(), &font);
+ let mut ctx = MathContext::new(engine, &mut locator, regions.base(), &font);
let scale_style = style_for_script_scale(&ctx);
let styles = styles.chain(&scale_style);
@@ -374,14 +372,7 @@ struct MathContext<'a, 'v, 'e> {
region: Region,
// Font-related.
font: &'a Font,
- ttf: &'a ttf_parser::Face<'a>,
- table: ttf_parser::math::Table<'a>,
constants: ttf_parser::math::Constants<'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.
fragments: Vec<MathFragment>,
}
@@ -391,46 +382,20 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
fn new(
engine: &'v mut Engine<'e>,
locator: &'v mut SplitLocator<'a>,
- styles: StyleChain<'a>,
base: Size,
font: &'a Font,
) -> Self {
- let math_table = font.ttf().tables().math.unwrap();
- let gsub_table = font.ttf().tables().gsub;
- let constants = math_table.constants.unwrap();
-
- let feat = |tag: &[u8; 4]| {
- GlyphwiseSubsts::new(gsub_table, Feature::new(Tag::from_bytes(tag), 0, ..))
- };
-
- let features = features(styles);
- let glyphwise_tables = Some(
- features
- .into_iter()
- .filter_map(|feature| GlyphwiseSubsts::new(gsub_table, feature))
- .collect(),
- );
-
- let ttf = font.ttf();
- let space_width = ttf
- .glyph_index(' ')
- .and_then(|id| ttf.glyph_hor_advance(id))
- .map(|advance| font.to_em(advance))
- .unwrap_or(THICK);
+ // These unwraps are safe as the font given is one returned by the
+ // find_math_font function, which only returns fonts that have a math
+ // constants table.
+ let constants = font.ttf().tables().math.unwrap().constants.unwrap();
Self {
engine,
locator,
region: Region::new(base, Axes::splat(false)),
font,
- ttf,
- table: math_table,
constants,
- dtls_table: feat(b"dtls"),
- flac_table: feat(b"flac"),
- ssty_table: feat(b"ssty"),
- glyphwise_tables,
- space_width,
fragments: vec![],
}
}
@@ -529,7 +494,8 @@ fn layout_realized(
if let Some(elem) = elem.to_packed::<TagElem>() {
ctx.push(MathFragment::Tag(elem.tag.clone()));
} else if elem.is::<SpaceElem>() {
- ctx.push(MathFragment::Space(ctx.space_width.resolve(styles)));
+ let space_width = ctx.font.space_width().unwrap_or(THICK);
+ ctx.push(MathFragment::Space(space_width.resolve(styles)));
} else if elem.is::<LinebreakElem>() {
ctx.push(MathFragment::Linebreak);
} else if let Some(elem) = elem.to_packed::<HElem>() {
diff --git a/crates/typst-layout/src/math/root.rs b/crates/typst-layout/src/math/root.rs
index 32f52719..91b9b16a 100644
--- a/crates/typst-layout/src/math/root.rs
+++ b/crates/typst-layout/src/math/root.rs
@@ -49,9 +49,9 @@ pub fn layout_root(
// Layout root symbol.
let target = radicand.height() + thickness + gap;
- let sqrt = GlyphFragment::new(ctx, styles, '√', span)
- .stretch_vertical(ctx, target)
- .frame;
+ let mut sqrt = GlyphFragment::new_char(ctx.font, styles, '√', span)?;
+ sqrt.stretch_vertical(ctx, target);
+ let sqrt = sqrt.into_frame();
// Layout the index.
let sscript = EquationElem::set_size(MathSize::ScriptScript).wrap();
diff --git a/crates/typst-layout/src/math/shared.rs b/crates/typst-layout/src/math/shared.rs
index 600c130d..1f88d2dd 100644
--- a/crates/typst-layout/src/math/shared.rs
+++ b/crates/typst-layout/src/math/shared.rs
@@ -1,7 +1,9 @@
use ttf_parser::math::MathValue;
+use ttf_parser::Tag;
use typst_library::foundations::{Style, StyleChain};
-use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size, VAlignment};
+use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size};
use typst_library::math::{EquationElem, MathSize};
+use typst_library::text::{FontFeatures, TextElem};
use typst_utils::LazyHash;
use super::{LeftRightAlternator, MathContext, MathFragment, MathRun};
@@ -59,6 +61,16 @@ pub fn style_cramped() -> LazyHash<Style> {
EquationElem::set_cramped(true).wrap()
}
+/// Sets flac OpenType feature.
+pub fn style_flac() -> LazyHash<Style> {
+ TextElem::set_features(FontFeatures(vec![(Tag::from_bytes(b"flac"), 1)])).wrap()
+}
+
+/// Sets dtls OpenType feature.
+pub fn style_dtls() -> LazyHash<Style> {
+ TextElem::set_features(FontFeatures(vec![(Tag::from_bytes(b"dtls"), 1)])).wrap()
+}
+
/// The style for subscripts in the current style.
pub fn style_for_subscript(styles: StyleChain) -> [LazyHash<Style>; 2] {
[style_for_superscript(styles), EquationElem::set_cramped(true).wrap()]
@@ -97,15 +109,6 @@ pub fn style_for_script_scale(ctx: &MathContext) -> LazyHash<Style> {
.wrap()
}
-/// How a delimieter should be aligned when scaling.
-pub fn delimiter_alignment(delimiter: char) -> VAlignment {
- match delimiter {
- '⌜' | '⌝' => VAlignment::Top,
- '⌞' | '⌟' => VAlignment::Bottom,
- _ => VAlignment::Horizon,
- }
-}
-
/// Stack rows on top of each other.
///
/// Add a `gap` between each row and uses the baseline of the `baseline`-th
diff --git a/crates/typst-layout/src/math/stretch.rs b/crates/typst-layout/src/math/stretch.rs
index 40f76da5..d4370e7b 100644
--- a/crates/typst-layout/src/math/stretch.rs
+++ b/crates/typst-layout/src/math/stretch.rs
@@ -1,19 +1,10 @@
-use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
-use ttf_parser::LazyArray16;
use typst_library::diag::{warning, SourceResult};
use typst_library::foundations::{Packed, StyleChain};
-use typst_library::layout::{Abs, Axis, Frame, Point, Rel, Size};
+use typst_library::layout::{Abs, Axis, Rel};
use typst_library::math::StretchElem;
use typst_utils::Get;
-use super::{
- delimiter_alignment, GlyphFragment, MathContext, MathFragment, Scaled,
- VariantFragment,
-};
-use crate::modifiers::FrameModify;
-
-/// Maximum number of times extenders can be repeated.
-const MAX_REPEATS: usize = 1024;
+use super::{stretch_axes, MathContext, MathFragment};
/// Lays out a [`StretchElem`].
#[typst_macros::time(name = "math.stretch", span = elem.span())]
@@ -23,15 +14,7 @@ pub fn layout_stretch(
styles: StyleChain,
) -> SourceResult<()> {
let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
- stretch_fragment(
- ctx,
- styles,
- &mut fragment,
- None,
- None,
- elem.size(styles),
- Abs::zero(),
- );
+ stretch_fragment(ctx, &mut fragment, None, None, elem.size(styles), Abs::zero());
ctx.push(fragment);
Ok(())
}
@@ -39,266 +22,49 @@ pub fn layout_stretch(
/// Attempts to stretch the given fragment by/to the amount given in stretch.
pub fn stretch_fragment(
ctx: &mut MathContext,
- styles: StyleChain,
fragment: &mut MathFragment,
axis: Option<Axis>,
relative_to: Option<Abs>,
stretch: Rel<Abs>,
short_fall: Abs,
) {
- let glyph = match fragment {
- MathFragment::Glyph(glyph) => glyph.clone(),
- MathFragment::Variant(variant) => {
- GlyphFragment::new(ctx, styles, variant.c, variant.span)
- }
- _ => return,
- };
+ let size = fragment.size();
+
+ let MathFragment::Glyph(ref mut glyph) = fragment else { return };
// Return if we attempt to stretch along an axis which isn't stretchable,
// so that the original fragment isn't modified.
- let Some(stretch_axis) = stretch_axis(ctx, &glyph) else { return };
- let axis = axis.unwrap_or(stretch_axis);
- if axis != stretch_axis {
- return;
- }
-
- let relative_to_size = relative_to.unwrap_or_else(|| fragment.size().get(axis));
-
- let mut variant = stretch_glyph(
- ctx,
- glyph,
- stretch.relative_to(relative_to_size) - short_fall,
- axis,
- );
-
- if axis == Axis::Y {
- variant.align_on_axis(ctx, delimiter_alignment(variant.c));
- }
-
- *fragment = MathFragment::Variant(variant);
-}
-
-/// Return whether the glyph is stretchable and if it is, along which axis it
-/// can be stretched.
-fn stretch_axis(ctx: &mut MathContext, base: &GlyphFragment) -> Option<Axis> {
- let base_id = base.id;
- let vertical = ctx
- .table
- .variants
- .and_then(|variants| variants.vertical_constructions.get(base_id))
- .map(|_| Axis::Y);
- let horizontal = ctx
- .table
- .variants
- .and_then(|variants| variants.horizontal_constructions.get(base_id))
- .map(|_| Axis::X);
-
- match (vertical, horizontal) {
- (vertical, None) => vertical,
- (None, horizontal) => horizontal,
- _ => {
- // As far as we know, there aren't any glyphs that have both
- // vertical and horizontal constructions. So for the time being, we
- // will assume that a glyph cannot have both.
- ctx.engine.sink.warn(warning!(
- base.span,
- "glyph has both vertical and horizontal constructions";
- hint: "this is probably a font bug";
- hint: "please file an issue at https://github.com/typst/typst/issues"
- ));
-
- None
- }
- }
-}
-
-/// Try to stretch a glyph to a desired width or height.
-///
-/// The resulting frame may not have the exact desired width.
-pub fn stretch_glyph(
- ctx: &mut MathContext,
- mut base: GlyphFragment,
- target: Abs,
- axis: Axis,
-) -> VariantFragment {
- // If the base glyph is good enough, use it.
- let advance = match axis {
- Axis::X => base.width,
- Axis::Y => base.height(),
- };
- if target <= advance {
- return base.into_variant();
- }
-
- let mut min_overlap = Abs::zero();
- let construction = ctx
- .table
- .variants
- .and_then(|variants| {
- min_overlap = variants.min_connector_overlap.scaled(ctx, base.font_size);
- match axis {
- Axis::X => variants.horizontal_constructions,
- Axis::Y => variants.vertical_constructions,
- }
- .get(base.id)
- })
- .unwrap_or(GlyphConstruction { assembly: None, variants: LazyArray16::new(&[]) });
-
- // Search for a pre-made variant with a good advance.
- let mut best_id = base.id;
- let mut best_advance = base.width;
- for variant in construction.variants {
- best_id = variant.variant_glyph;
- best_advance = base.font.to_em(variant.advance_measurement).at(base.font_size);
- if target <= best_advance {
- break;
+ let axes = stretch_axes(&glyph.item.font, glyph.base_glyph.id);
+ let stretch_axis = if let Some(axis) = axis {
+ if !axes.get(axis) {
+ return;
}
- }
-
- // This is either good or the best we've got.
- if target <= best_advance || construction.assembly.is_none() {
- base.set_id(ctx, best_id);
- return base.into_variant();
- }
-
- // Assemble from parts.
- let assembly = construction.assembly.unwrap();
- assemble(ctx, base, assembly, min_overlap, target, axis)
-}
-
-/// Assemble a glyph from parts.
-fn assemble(
- ctx: &mut MathContext,
- base: GlyphFragment,
- assembly: GlyphAssembly,
- min_overlap: Abs,
- target: Abs,
- axis: Axis,
-) -> VariantFragment {
- // Determine the number of times the extenders need to be repeated as well
- // as a ratio specifying how much to spread the parts apart
- // (0 = maximal overlap, 1 = minimal overlap).
- let mut full;
- let mut ratio;
- let mut repeat = 0;
- loop {
- full = Abs::zero();
- ratio = 0.0;
-
- let mut parts = parts(assembly, repeat).peekable();
- let mut growable = Abs::zero();
-
- while let Some(part) = parts.next() {
- let mut advance = part.full_advance.scaled(ctx, base.font_size);
- if let Some(next) = parts.peek() {
- let max_overlap = part
- .end_connector_length
- .min(next.start_connector_length)
- .scaled(ctx, base.font_size);
- if max_overlap < min_overlap {
- // This condition happening is indicative of a bug in the
- // font.
- ctx.engine.sink.warn(warning!(
- base.span,
- "glyph has assembly parts with overlap less than minConnectorOverlap";
- hint: "its rendering may appear broken - this is probably a font bug";
- hint: "please file an issue at https://github.com/typst/typst/issues"
- ));
- }
-
- advance -= max_overlap;
- growable += max_overlap - min_overlap;
+ axis
+ } else {
+ match (axes.x, axes.y) {
+ (true, false) => Axis::X,
+ (false, true) => Axis::Y,
+ (false, false) => return,
+ (true, true) => {
+ // As far as we know, there aren't any glyphs that have both
+ // vertical and horizontal constructions. So for the time being, we
+ // will assume that a glyph cannot have both.
+ ctx.engine.sink.warn(warning!(
+ glyph.item.glyphs[0].span.0,
+ "glyph has both vertical and horizontal constructions";
+ hint: "this is probably a font bug";
+ hint: "please file an issue at https://github.com/typst/typst/issues"
+ ));
+ return;
}
-
- full += advance;
- }
-
- if full < target {
- let delta = target - full;
- ratio = (delta / growable).min(1.0);
- full += ratio * growable;
- }
-
- if target <= full || repeat >= MAX_REPEATS {
- break;
- }
-
- repeat += 1;
- }
-
- let mut selected = vec![];
- let mut parts = parts(assembly, repeat).peekable();
- while let Some(part) = parts.next() {
- let mut advance = part.full_advance.scaled(ctx, base.font_size);
- if let Some(next) = parts.peek() {
- let max_overlap = part
- .end_connector_length
- .min(next.start_connector_length)
- .scaled(ctx, base.font_size);
- advance -= max_overlap;
- advance += ratio * (max_overlap - min_overlap);
- }
-
- let mut fragment = base.clone();
- fragment.set_id(ctx, part.glyph_id);
- selected.push((fragment, advance));
- }
-
- let size;
- let baseline;
- match axis {
- Axis::X => {
- let height = base.ascent + base.descent;
- size = Size::new(full, height);
- baseline = base.ascent;
- }
- Axis::Y => {
- let axis = ctx.constants.axis_height().scaled(ctx, base.font_size);
- let width = selected.iter().map(|(f, _)| f.width).max().unwrap_or_default();
- size = Size::new(width, full);
- baseline = full / 2.0 + axis;
}
- }
+ };
- let mut frame = Frame::soft(size);
- let mut offset = Abs::zero();
- frame.set_baseline(baseline);
- frame.modify(&base.modifiers);
+ let relative_to_size = relative_to.unwrap_or_else(|| size.get(stretch_axis));
- for (fragment, advance) in selected {
- let pos = match axis {
- Axis::X => Point::new(offset, frame.baseline() - fragment.ascent),
- Axis::Y => Point::with_y(full - offset - fragment.height()),
- };
- frame.push_frame(pos, fragment.into_frame());
- offset += advance;
- }
+ glyph.stretch(ctx, stretch.relative_to(relative_to_size) - short_fall, stretch_axis);
- let accent_attach = match axis {
- Axis::X => (frame.width() / 2.0, frame.width() / 2.0),
- Axis::Y => base.accent_attach,
- };
-
- VariantFragment {
- c: base.c,
- frame,
- font_size: base.font_size,
- italics_correction: Abs::zero(),
- accent_attach,
- class: base.class,
- math_size: base.math_size,
- span: base.span,
- limits: base.limits,
- mid_stretched: None,
- extended_shape: true,
+ if stretch_axis == Axis::Y {
+ glyph.center_on_axis();
}
}
-
-/// Return an iterator over the assembly's parts with extenders repeated the
-/// specified number of times.
-fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ {
- assembly.parts.into_iter().flat_map(move |part| {
- let count = if part.part_flags.extender() { repeat } else { 1 };
- std::iter::repeat_n(part, count)
- })
-}
diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs
index e191ec17..67dc0a2c 100644
--- a/crates/typst-layout/src/math/text.rs
+++ b/crates/typst-layout/src/math/text.rs
@@ -12,7 +12,10 @@ use typst_syntax::{is_newline, Span};
use unicode_math_class::MathClass;
use unicode_segmentation::UnicodeSegmentation;
-use super::{FrameFragment, GlyphFragment, MathContext, MathFragment, MathRun};
+use super::{
+ has_dtls_feat, style_dtls, FrameFragment, GlyphFragment, MathContext, MathFragment,
+ MathRun,
+};
/// Lays out a [`TextElem`].
pub fn layout_text(
@@ -67,12 +70,7 @@ fn layout_inline_text(
let mut fragments = vec![];
for unstyled_c in text.chars() {
let c = styled_char(styles, unstyled_c, false);
- let mut glyph = GlyphFragment::new(ctx, styles, c, span);
- match EquationElem::size_in(styles) {
- MathSize::Script => glyph.make_script_size(ctx),
- MathSize::ScriptScript => glyph.make_script_script_size(ctx),
- _ => {}
- }
+ let glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?;
fragments.push(glyph.into());
}
let frame = MathRun::new(fragments).into_frame(styles);
@@ -121,54 +119,45 @@ pub fn layout_symbol(
) -> SourceResult<()> {
// Switch dotless char to normal when we have the dtls OpenType feature.
// This should happen before the main styling pass.
- let (unstyled_c, dtls) = match try_dotless(elem.text) {
- Some(c) if ctx.dtls_table.is_some() => (c, true),
- _ => (elem.text, false),
+ let dtls = style_dtls();
+ let (unstyled_c, symbol_styles) = match try_dotless(elem.text) {
+ Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)),
+ _ => (elem.text, styles),
};
let c = styled_char(styles, unstyled_c, true);
- let fragment = match GlyphFragment::try_new(ctx, styles, c, elem.span()) {
- Some(glyph) => layout_glyph(glyph, dtls, ctx, styles),
- None => {
- // Not in the math font, fallback to normal inline text layout.
- layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)?
- .into()
- }
- };
+ let fragment: MathFragment =
+ match GlyphFragment::new_char(ctx.font, symbol_styles, c, elem.span()) {
+ Ok(mut glyph) => {
+ adjust_glyph_layout(&mut glyph, ctx, styles);
+ glyph.into()
+ }
+ Err(_) => {
+ // Not in the math font, fallback to normal inline text layout.
+ // TODO: Should replace this with proper fallback in [`GlyphFragment::new`].
+ layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)?
+ .into()
+ }
+ };
ctx.push(fragment);
Ok(())
}
-/// Layout a [`GlyphFragment`].
-fn layout_glyph(
- mut glyph: GlyphFragment,
- dtls: bool,
+/// Centers large glyphs vertically on the axis, scaling them if in display
+/// style.
+fn adjust_glyph_layout(
+ glyph: &mut GlyphFragment,
ctx: &mut MathContext,
styles: StyleChain,
-) -> MathFragment {
- if dtls {
- glyph.make_dotless_form(ctx);
- }
- let math_size = EquationElem::size_in(styles);
- match math_size {
- MathSize::Script => glyph.make_script_size(ctx),
- MathSize::ScriptScript => glyph.make_script_script_size(ctx),
- _ => {}
- }
-
+) {
if glyph.class == MathClass::Large {
- let mut variant = if math_size == MathSize::Display {
+ if EquationElem::size_in(styles) == MathSize::Display {
let height = scaled!(ctx, styles, display_operator_min_height)
- .max(SQRT_2 * glyph.height());
- glyph.stretch_vertical(ctx, height)
- } else {
- glyph.into_variant()
+ .max(SQRT_2 * glyph.size.y);
+ glyph.stretch_vertical(ctx, height);
};
// TeXbook p 155. Large operators are always vertically centered on the
// axis.
- variant.center_on_axis(ctx);
- variant.into()
- } else {
- glyph.into()
+ glyph.center_on_axis();
}
}
diff --git a/crates/typst-layout/src/math/underover.rs b/crates/typst-layout/src/math/underover.rs
index a24113c8..c29d9947 100644
--- a/crates/typst-layout/src/math/underover.rs
+++ b/crates/typst-layout/src/math/underover.rs
@@ -285,14 +285,14 @@ fn layout_underoverspreader(
let body = ctx.layout_into_run(body, styles)?;
let body_class = body.class();
let body = body.into_fragment(styles);
- let glyph = GlyphFragment::new(ctx, styles, c, span);
- let stretched = glyph.stretch_horizontal(ctx, body.width());
+ let mut glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?;
+ glyph.stretch_horizontal(ctx, body.width());
let mut rows = vec![];
let baseline = match position {
Position::Under => {
rows.push(MathRun::new(vec![body]));
- rows.push(stretched.into());
+ rows.push(glyph.into());
if let Some(annotation) = annotation {
let under_style = style_for_subscript(styles);
let annotation_styles = styles.chain(&under_style);
@@ -306,7 +306,7 @@ fn layout_underoverspreader(
let annotation_styles = styles.chain(&over_style);
rows.extend(ctx.layout_into_run(annotation, annotation_styles)?.rows());
}
- rows.push(stretched.into());
+ rows.push(glyph.into());
rows.push(MathRun::new(vec![body]));
rows.len() - 1
}
diff --git a/crates/typst-library/src/text/font/mod.rs b/crates/typst-library/src/text/font/mod.rs
index afa92e77..0383bfe1 100644
--- a/crates/typst-library/src/text/font/mod.rs
+++ b/crates/typst-library/src/text/font/mod.rs
@@ -106,13 +106,26 @@ impl Font {
}
/// Look up the horizontal advance width of a glyph.
- pub fn advance(&self, glyph: u16) -> Option<Em> {
+ pub fn x_advance(&self, glyph: u16) -> Option<Em> {
self.0
.ttf
.glyph_hor_advance(GlyphId(glyph))
.map(|units| self.to_em(units))
}
+ /// Look up the vertical advance width of a glyph.
+ pub fn y_advance(&self, glyph: u16) -> Option<Em> {
+ self.0
+ .ttf
+ .glyph_ver_advance(GlyphId(glyph))
+ .map(|units| self.to_em(units))
+ }
+
+ /// Look up the width of a space.
+ pub fn space_width(&self) -> Option<Em> {
+ self.0.ttf.glyph_index(' ').and_then(|id| self.x_advance(id.0))
+ }
+
/// Lookup a name by id.
pub fn find_name(&self, id: u16) -> Option<String> {
find_name(&self.0.ttf, id)
diff --git a/crates/typst-library/src/text/item.rs b/crates/typst-library/src/text/item.rs
index ed559aec..518b082b 100644
--- a/crates/typst-library/src/text/item.rs
+++ b/crates/typst-library/src/text/item.rs
@@ -35,6 +35,11 @@ impl TextItem {
pub fn width(&self) -> Abs {
self.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size)
}
+
+ /// The height of the text run.
+ pub fn height(&self) -> Abs {
+ self.glyphs.iter().map(|g| g.y_advance).sum::<Em>().at(self.size)
+ }
}
impl Debug for TextItem {
@@ -54,6 +59,10 @@ pub struct Glyph {
pub x_advance: Em,
/// The horizontal offset of the glyph.
pub x_offset: Em,
+ /// The advance height (Y-up) of the glyph.
+ pub y_advance: Em,
+ /// The vertical offset (Y-up) of the glyph.
+ pub y_offset: Em,
/// The range of the glyph in its item's text. The range's length may
/// be more than one due to multi-byte UTF-8 encoding or ligatures.
pub range: Range<u16>,
@@ -115,4 +124,13 @@ impl<'a> TextItemView<'a> {
.sum::<Em>()
.at(self.item.size)
}
+
+ /// The total height of this text slice
+ pub fn height(&self) -> Abs {
+ self.glyphs()
+ .iter()
+ .map(|g| g.y_advance)
+ .sum::<Em>()
+ .at(self.item.size)
+ }
}
diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs
index 23edc9e9..1c0696d1 100644
--- a/crates/typst-library/src/text/mod.rs
+++ b/crates/typst-library/src/text/mod.rs
@@ -30,6 +30,7 @@ pub use self::space::*;
use std::fmt::{self, Debug, Formatter};
use std::hash::Hash;
+use std::str::FromStr;
use std::sync::LazyLock;
use ecow::{eco_format, EcoString};
@@ -1283,6 +1284,12 @@ pub fn features(styles: StyleChain) -> Vec<Feature> {
feat(b"frac", 1);
}
+ match EquationElem::size_in(styles) {
+ MathSize::Script => feat(b"ssty", 1),
+ MathSize::ScriptScript => feat(b"ssty", 2),
+ _ => {}
+ }
+
for (tag, value) in TextElem::features_in(styles).0 {
tags.push(Feature::new(tag, value, ..))
}
@@ -1290,6 +1297,17 @@ pub fn features(styles: StyleChain) -> Vec<Feature> {
tags
}
+/// Process the language and region of a style chain into a
+/// rustybuzz-compatible BCP 47 language.
+pub fn language(styles: StyleChain) -> rustybuzz::Language {
+ let mut bcp: EcoString = TextElem::lang_in(styles).as_str().into();
+ if let Some(region) = TextElem::region_in(styles) {
+ bcp.push('-');
+ bcp.push_str(region.as_str());
+ }
+ rustybuzz::Language::from_str(&bcp).unwrap()
+}
+
/// A toggle that turns on and off alternatingly if folded.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct ItalicToggle(pub bool);
diff --git a/crates/typst-pdf/src/text.rs b/crates/typst-pdf/src/text.rs
index 8d532e08..9876927d 100644
--- a/crates/typst-pdf/src/text.rs
+++ b/crates/typst-pdf/src/text.rs
@@ -120,13 +120,15 @@ impl krilla::text::Glyph for PdfGlyph {
}
#[inline(always)]
- fn y_offset(&self, _: f32) -> f32 {
- 0.0
+ fn y_offset(&self, size: f32) -> f32 {
+ // Don't use `Em::at`, because it contains an expensive check whether the result is finite.
+ self.0.y_offset.get() as f32 * size
}
#[inline(always)]
- fn y_advance(&self, _: f32) -> f32 {
- 0.0
+ fn y_advance(&self, size: f32) -> f32 {
+ // Don't use `Em::at`, because it contains an expensive check whether the result is finite.
+ self.0.y_advance.get() as f32 * size
}
fn location(&self) -> Option<Location> {
diff --git a/crates/typst-render/src/text.rs b/crates/typst-render/src/text.rs
index 16490aff..55fe04b5 100644
--- a/crates/typst-render/src/text.rs
+++ b/crates/typst-render/src/text.rs
@@ -14,18 +14,20 @@ use crate::{shape, AbsExt, State};
/// Render a text run into the canvas.
pub fn render_text(canvas: &mut sk::Pixmap, state: State, text: &TextItem) {
let mut x = Abs::zero();
+ let mut y = Abs::zero();
for glyph in &text.glyphs {
let id = GlyphId(glyph.id);
- let offset = x + glyph.x_offset.at(text.size);
+ let x_offset = x + glyph.x_offset.at(text.size);
+ let y_offset = y + glyph.y_offset.at(text.size);
if should_outline(&text.font, glyph) {
- let state = state.pre_translate(Point::with_x(offset));
+ let state = state.pre_translate(Point::new(x_offset, -y_offset));
render_outline_glyph(canvas, state, text, id);
} else {
let upem = text.font.units_per_em();
let text_scale = text.size / upem;
let state = state
- .pre_translate(Point::new(offset, -text.size))
+ .pre_translate(Point::new(x_offset, -y_offset - text.size))
.pre_scale(Axes::new(text_scale, text_scale));
let (glyph_frame, _) = glyph_frame(&text.font, glyph.id);
@@ -33,6 +35,7 @@ pub fn render_text(canvas: &mut sk::Pixmap, state: State, text: &TextItem) {
}
x += glyph.x_advance.at(text.size);
+ y += glyph.y_advance.at(text.size);
}
}
diff --git a/crates/typst-svg/src/text.rs b/crates/typst-svg/src/text.rs
index e6620a59..7099a9d7 100644
--- a/crates/typst-svg/src/text.rs
+++ b/crates/typst-svg/src/text.rs
@@ -25,25 +25,32 @@ impl SVGRenderer {
self.xml.write_attribute("transform", "scale(1, -1)");
let mut x: f64 = 0.0;
+ let mut y: f64 = 0.0;
for glyph in &text.glyphs {
let id = GlyphId(glyph.id);
- let offset = x + glyph.x_offset.at(text.size).to_pt();
+ let x_offset = x + glyph.x_offset.at(text.size).to_pt();
+ let y_offset = y + glyph.y_offset.at(text.size).to_pt();
- self.render_svg_glyph(text, id, offset, scale)
- .or_else(|| self.render_bitmap_glyph(text, id, offset))
+ self.render_svg_glyph(text, id, x_offset, y_offset, scale)
+ .or_else(|| self.render_bitmap_glyph(text, id, x_offset, y_offset))
.or_else(|| {
self.render_outline_glyph(
state
.pre_concat(Transform::scale(Ratio::one(), -Ratio::one()))
- .pre_translate(Point::new(Abs::pt(offset), Abs::zero())),
+ .pre_translate(Point::new(
+ Abs::pt(x_offset),
+ Abs::pt(y_offset),
+ )),
text,
id,
- offset,
+ x_offset,
+ y_offset,
scale,
)
});
x += glyph.x_advance.at(text.size).to_pt();
+ y += glyph.y_advance.at(text.size).to_pt();
}
self.xml.end_element();
@@ -55,6 +62,7 @@ impl SVGRenderer {
text: &TextItem,
id: GlyphId,
x_offset: f64,
+ y_offset: f64,
scale: f64,
) -> Option<()> {
let data_url = convert_svg_glyph_to_base64_url(&text.font, id)?;
@@ -73,6 +81,7 @@ impl SVGRenderer {
self.xml.start_element("use");
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
self.xml.write_attribute("x", &x_offset);
+ self.xml.write_attribute("y", &y_offset);
self.xml.end_element();
Some(())
@@ -84,6 +93,7 @@ impl SVGRenderer {
text: &TextItem,
id: GlyphId,
x_offset: f64,
+ y_offset: f64,
) -> Option<()> {
let (image, bitmap_x_offset, bitmap_y_offset) =
convert_bitmap_glyph_to_image(&text.font, id)?;
@@ -109,6 +119,7 @@ impl SVGRenderer {
// it.
let scale_factor = target_height / image.height();
self.xml.write_attribute("x", &(x_offset / scale_factor));
+ self.xml.write_attribute("y", &(y_offset / scale_factor));
self.xml.write_attribute_fmt(
"transform",
format_args!("scale({scale_factor} -{scale_factor})",),
@@ -125,6 +136,7 @@ impl SVGRenderer {
text: &TextItem,
glyph_id: GlyphId,
x_offset: f64,
+ y_offset: f64,
scale: f64,
) -> Option<()> {
let scale = Ratio::new(scale);
@@ -139,6 +151,7 @@ impl SVGRenderer {
self.xml.start_element("use");
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
self.xml.write_attribute_fmt("x", format_args!("{x_offset}"));
+ self.xml.write_attribute_fmt("y", format_args!("{y_offset}"));
self.write_fill(
&text.fill,
FillRule::default(),
diff --git a/tests/ref/math-accent-dotless-greedy.png b/tests/ref/math-accent-dotless-greedy.png
new file mode 100644
index 00000000..2c28363a
--- /dev/null
+++ b/tests/ref/math-accent-dotless-greedy.png
Binary files differ
diff --git a/tests/suite/math/accent.typ b/tests/suite/math/accent.typ
index 2239d897..0aef41e2 100644
--- a/tests/suite/math/accent.typ
+++ b/tests/suite/math/accent.typ
@@ -51,6 +51,12 @@ $hat(i), hat(i, dotless: #false), accent(j, tilde), accent(j, tilde, dotless: #f
#set math.accent(dotless: false)
$ hat(i) $
+--- math-accent-dotless-greedy ---
+// Currently the dotless style propogates to everything in the accent's base,
+// even though it shouldn't.
+$ arrow(P_(c, i dot j) P_(1, i) j) \
+ arrow(P_(c, i dot j) P_(1, i) j, dotless: #false) $
+
--- math-accent-flattened ---
// Test flattened accent glyph variants.
#show math.equation: set text(font: "STIX Two Math")