summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorMax <me@mkor.je>2024-08-26 17:04:02 +0000
committerGitHub <noreply@github.com>2024-08-26 17:04:02 +0000
commit373163dba4b4a4d9186b96a671af64d435d0eda3 (patch)
tree824e2f5d4b9bf0988a4c0f9978332ea1aed4c554 /crates
parentc38d01e4c5558d839ec91895807f5942c8944348 (diff)
Implement math kerning and fix various `math.attach` bugs (#4762)
Diffstat (limited to 'crates')
-rw-r--r--crates/typst-utils/src/lib.rs21
-rw-r--r--crates/typst/src/layout/corners.rs10
-rw-r--r--crates/typst/src/math/attach.rs366
-rw-r--r--crates/typst/src/math/fragment.rs16
4 files changed, 294 insertions, 119 deletions
diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs
index 4d5f8e0c..831b2374 100644
--- a/crates/typst-utils/src/lib.rs
+++ b/crates/typst-utils/src/lib.rs
@@ -83,6 +83,27 @@ impl<T: Clone> ArcExt<T> for Arc<T> {
}
}
+/// Extra methods for [`Option`].
+pub trait OptionExt<T> {
+ /// Maps an `Option<T>` to `U` by applying a function to a contained value
+ /// (if `Some`) or returns a default (if `None`).
+ fn map_or_default<U: Default, F>(self, f: F) -> U
+ where
+ F: FnOnce(T) -> U;
+}
+
+impl<T> OptionExt<T> for Option<T> {
+ fn map_or_default<U: Default, F>(self, f: F) -> U
+ where
+ F: FnOnce(T) -> U,
+ {
+ match self {
+ Some(x) => f(x),
+ None => U::default(),
+ }
+ }
+}
+
/// Extra methods for [`[T]`](slice).
pub trait SliceExt<T> {
/// Split a slice into consecutive runs with the same key and yield for
diff --git a/crates/typst/src/layout/corners.rs b/crates/typst/src/layout/corners.rs
index a3b6ba93..cfd6b7ac 100644
--- a/crates/typst/src/layout/corners.rs
+++ b/crates/typst/src/layout/corners.rs
@@ -262,6 +262,16 @@ pub enum Corner {
}
impl Corner {
+ /// The opposite corner.
+ pub fn inv(self) -> Self {
+ match self {
+ Self::TopLeft => Self::BottomRight,
+ Self::TopRight => Self::BottomLeft,
+ Self::BottomRight => Self::TopLeft,
+ Self::BottomLeft => Self::TopRight,
+ }
+ }
+
/// The next corner, clockwise.
pub fn next_cw(self) -> Self {
match self {
diff --git a/crates/typst/src/math/attach.rs b/crates/typst/src/math/attach.rs
index 70bf2e5e..2d491dfd 100644
--- a/crates/typst/src/math/attach.rs
+++ b/crates/typst/src/math/attach.rs
@@ -2,12 +2,13 @@ use unicode_math_class::MathClass;
use crate::diag::SourceResult;
use crate::foundations::{elem, Content, Packed, StyleChain};
-use crate::layout::{Abs, Frame, Point, Size};
+use crate::layout::{Abs, Corner, Frame, Point, Size};
use crate::math::{
style_for_subscript, style_for_superscript, EquationElem, FrameFragment, LayoutMath,
MathContext, MathFragment, MathSize, Scaled,
};
use crate::text::TextElem;
+use crate::utils::OptionExt;
/// A base with optional attachments.
///
@@ -240,7 +241,7 @@ impl Limits {
}
}
- /// Whether limits should be displayed in this context
+ /// Whether limits should be displayed in this context.
pub fn active(&self, styles: StyleChain) -> bool {
match self {
Self::Always => true,
@@ -300,145 +301,231 @@ fn layout_attachments(
base: MathFragment,
[tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
) -> SourceResult<()> {
- let (shift_up, shift_down) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
- (Abs::zero(), Abs::zero())
- } else {
- compute_shifts_up_and_down(ctx, styles, &base, [&tl, &tr, &bl, &br])
- };
-
- let sup_delta = Abs::zero();
- let sub_delta = -base.italics_correction();
- let (base_width, base_ascent, base_descent) =
- (base.width(), base.ascent(), base.descent());
let base_class = base.class();
- let mut ascent = base_ascent
- .max(shift_up + measure!(tr, ascent))
- .max(shift_up + measure!(tl, ascent))
- .max(shift_up + measure!(t, height));
-
- let mut descent = base_descent
- .max(shift_down + measure!(br, descent))
- .max(shift_down + measure!(bl, descent))
- .max(shift_down + measure!(b, height));
-
- let pre_sup_width = measure!(tl, width);
- let pre_sub_width = measure!(bl, width);
- let pre_width_dif = pre_sup_width - pre_sub_width; // Could be negative.
- let pre_width_max = pre_sup_width.max(pre_sub_width);
- let post_width_max =
- (sup_delta + measure!(tr, width)).max(sub_delta + measure!(br, width));
-
- let (center_frame, base_offset) = if t.is_none() && b.is_none() {
- (base.into_frame(), Abs::zero())
+ // Calculate the distance from the base's baseline to the superscripts' and
+ // subscripts' baseline.
+ let (tx_shift, bx_shift) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
+ (Abs::zero(), Abs::zero())
} else {
- attach_top_and_bottom(ctx, styles, base, t, b)
+ compute_script_shifts(ctx, styles, &base, [&tl, &tr, &bl, &br])
};
- if [&tl, &bl, &tr, &br].iter().all(|&e| e.is_none()) {
- ctx.push(FrameFragment::new(ctx, styles, center_frame).with_class(base_class));
- return Ok(());
- }
- ascent.set_max(center_frame.ascent());
- descent.set_max(center_frame.descent());
-
- let mut frame = Frame::soft(Size::new(
- pre_width_max
- + base_width
- + post_width_max
- + scaled!(ctx, styles, space_after_script),
- ascent + descent,
- ));
- frame.set_baseline(ascent);
- frame.push_frame(
- Point::new(sup_delta + pre_width_max, frame.ascent() - base_ascent - base_offset),
- center_frame,
+ // Calculate the distance from the base's baseline to the top attachment's
+ // and bottom attachment's baseline.
+ let (t_shift, b_shift) =
+ compute_limit_shifts(ctx, styles, &base, [t.as_ref(), b.as_ref()]);
+
+ // Calculate the final frame height.
+ let ascent = base
+ .ascent()
+ .max(tx_shift + measure!(tr, ascent))
+ .max(tx_shift + measure!(tl, ascent))
+ .max(t_shift + measure!(t, ascent));
+ let descent = base
+ .descent()
+ .max(bx_shift + measure!(br, descent))
+ .max(bx_shift + measure!(bl, descent))
+ .max(b_shift + measure!(b, descent));
+ let height = ascent + descent;
+
+ // Calculate the vertical position of each element in the final frame.
+ let base_y = ascent - base.ascent();
+ let tx_y = |tx: &MathFragment| ascent - tx_shift - tx.ascent();
+ let bx_y = |bx: &MathFragment| ascent + bx_shift - bx.ascent();
+ let t_y = |t: &MathFragment| ascent - t_shift - t.ascent();
+ let b_y = |b: &MathFragment| ascent + b_shift - b.ascent();
+
+ // Calculate the distance each limit extends to the left and right of the
+ // base's width.
+ let ((t_pre_width, t_post_width), (b_pre_width, b_post_width)) =
+ compute_limit_widths(&base, [t.as_ref(), b.as_ref()]);
+
+ // `space_after_script` is extra spacing that is at the start before each
+ // pre-script, and at the end after each post-script (see the MathConstants
+ // table in the OpenType MATH spec).
+ let space_after_script = scaled!(ctx, styles, space_after_script);
+
+ // 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),
+ space_after_script,
);
- if let Some(tl) = tl {
- let pos =
- Point::new(-pre_width_dif.min(Abs::zero()), ascent - shift_up - tl.ascent());
- frame.push_frame(pos, tl.into_frame());
- }
+ // Calculate the distance each post-script extends to the right of the
+ // 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),
+ space_after_script,
+ );
- if let Some(bl) = bl {
- let pos =
- Point::new(pre_width_dif.max(Abs::zero()), ascent + shift_down - bl.ascent());
- frame.push_frame(pos, bl.into_frame());
- }
+ // Calculate the final frame width.
+ let pre_width = t_pre_width.max(b_pre_width).max(tl_pre_width).max(bl_pre_width);
+ let base_width = base.width();
+ let post_width = t_post_width.max(b_post_width).max(tr_post_width).max(br_post_width);
+ let width = pre_width + base_width + post_width;
+
+ // Calculate the horizontal position of each element in the final frame.
+ let base_x = pre_width;
+ let tl_x = pre_width - tl_pre_width + space_after_script;
+ let bl_x = pre_width - bl_pre_width + space_after_script;
+ let tr_x = pre_width + base_width + tr_kern;
+ let br_x = pre_width + base_width + br_kern;
+ let t_x = pre_width - t_pre_width;
+ let b_x = pre_width - b_pre_width;
+
+ // Create the final frame.
+ let mut frame = Frame::soft(Size::new(width, height));
+ frame.set_baseline(ascent);
+ frame.push_frame(Point::new(base_x, base_y), base.into_frame());
- if let Some(tr) = tr {
- let pos = Point::new(
- sup_delta + pre_width_max + base_width,
- ascent - shift_up - tr.ascent(),
- );
- frame.push_frame(pos, tr.into_frame());
+ macro_rules! layout {
+ ($e: ident, $x: ident, $y: ident) => {
+ if let Some($e) = $e {
+ frame.push_frame(Point::new($x, $y(&$e)), $e.into_frame());
+ }
+ };
}
- if let Some(br) = br {
- let pos = Point::new(
- sub_delta + pre_width_max + base_width,
- ascent + shift_down - br.ascent(),
- );
- frame.push_frame(pos, br.into_frame());
- }
+ layout!(tl, tl_x, tx_y); // pre-superscript
+ layout!(bl, bl_x, bx_y); // pre-subscript
+ layout!(tr, tr_x, tx_y); // post-superscript
+ layout!(br, br_x, bx_y); // post-subscript
+ layout!(t, t_x, t_y); // upper-limit
+ layout!(b, b_x, b_y); // lower-limit
+ // Done! Note that we retain the class of the base.
ctx.push(FrameFragment::new(ctx, styles, frame).with_class(base_class));
Ok(())
}
-fn attach_top_and_bottom(
+/// Calculate the distance each post-script extends to the right of the base's
+/// width, as well as its kerning value. Requires the distance from the base's
+/// baseline to each post-script's baseline to obtain the correct kerning value.
+/// Returns 2 tuples of two lengths, each first containing the distance the
+/// post-script extends left of the base's width and second containing the
+/// 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,
- styles: StyleChain,
- base: MathFragment,
- t: Option<MathFragment>,
- b: Option<MathFragment>,
-) -> (Frame, Abs) {
- let upper_gap_min = scaled!(ctx, styles, upper_limit_gap_min);
- let upper_rise_min = scaled!(ctx, styles, upper_limit_baseline_rise_min);
- let lower_gap_min = scaled!(ctx, styles, lower_limit_gap_min);
- let lower_drop_min = scaled!(ctx, styles, lower_limit_baseline_drop_min);
-
- let mut base_offset = Abs::zero();
- let mut width = base.width();
- let mut height = base.height();
-
- if let Some(t) = &t {
- let top_gap = upper_gap_min.max(upper_rise_min - t.descent());
- width.set_max(t.width());
- height += t.height() + top_gap;
- base_offset = top_gap + t.height();
- }
+ 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);
+ (space_after_post_script + tr.width() + kern, kern)
+ });
+
+ // The base's bounding box already accounts for its italic correction, so we
+ // 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)
+ - base.italics_correction();
+ (space_after_post_script + br.width() + kern, kern)
+ });
+
+ (tr_values, br_values)
+}
- if let Some(b) = &b {
- let bottom_gap = lower_gap_min.max(lower_drop_min - b.ascent());
- width.set_max(b.width());
- height += b.height() + bottom_gap;
- }
+/// Calculate the distance each pre-script extends to the left of the base's
+/// width. Requires the distance from the base's baseline to each pre-script's
+/// baseline to obtain the correct kerning value.
+/// Returns two lengths, the first being the distance the pre-superscript
+/// 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);
+ 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);
+ space_before_pre_script + bl.width() + kern
+ });
+
+ (tl_pre_width, bl_pre_width)
+}
- let base_pos = Point::new((width - base.width()) / 2.0, base_offset);
+/// Calculate the distance each limit extends beyond the base's width, in each
+/// direction. Can be a negative value if the limit does not extend beyond the
+/// base's width, indicating how far into the base's width the limit extends.
+/// Returns 2 tuples of two lengths, each first containing the distance the
+/// limit extends leftward beyond the base's width and second containing the
+/// distance the limit extends rightward beyond the base's width. The first
+/// tuple is for the upper-limit, and the second is for the lower-limit.
+fn compute_limit_widths(
+ base: &MathFragment,
+ [t, b]: [Option<&MathFragment>; 2],
+) -> ((Abs, Abs), (Abs, Abs)) {
+ // The upper- (lower-) limit is shifted to the right (left) of the base's
+ // center by half the base's italic correction.
let delta = base.italics_correction() / 2.0;
- let mut frame = Frame::soft(Size::new(width, height));
- frame.set_baseline(base_pos.y + base.ascent());
- frame.push_frame(base_pos, base.into_frame());
+ let t_widths = t.map_or_default(|t| {
+ let half = (t.width() - base.width()) / 2.0;
+ (half - delta, half + delta)
+ });
- if let Some(t) = t {
- let top_pos = Point::with_x((width - t.width()) / 2.0 + delta);
- frame.push_frame(top_pos, t.into_frame());
- }
+ let b_widths = b.map_or_default(|b| {
+ let half = (b.width() - base.width()) / 2.0;
+ (half + delta, half - delta)
+ });
- if let Some(b) = b {
- let bottom_pos =
- Point::new((width - b.width()) / 2.0 - delta, height - b.height());
- frame.push_frame(bottom_pos, b.into_frame());
- }
+ (t_widths, b_widths)
+}
- (frame, base_offset)
+/// Calculate the distance from the base's baseline to each limit's baseline.
+/// Returns two lengths, the first being the distance to the upper-limit's
+/// baseline and the second being the distance to the lower-limit's baseline.
+fn compute_limit_shifts(
+ ctx: &MathContext,
+ styles: StyleChain,
+ base: &MathFragment,
+ [t, b]: [Option<&MathFragment>; 2],
+) -> (Abs, Abs) {
+ // `upper_gap_min` and `lower_gap_min` give gaps to the descender and
+ // ascender of the limits respectively, whereas `upper_rise_min` and
+ // `lower_drop_min` give gaps to each limit's baseline (see the
+ // MathConstants table in the OpenType MATH spec).
+
+ let t_shift = t.map_or_default(|t| {
+ let upper_gap_min = scaled!(ctx, styles, upper_limit_gap_min);
+ let upper_rise_min = scaled!(ctx, styles, upper_limit_baseline_rise_min);
+ base.ascent() + upper_rise_min.max(upper_gap_min + t.descent())
+ });
+
+ let b_shift = b.map_or_default(|b| {
+ let lower_gap_min = scaled!(ctx, styles, lower_limit_gap_min);
+ let lower_drop_min = scaled!(ctx, styles, lower_limit_baseline_drop_min);
+ base.descent() + lower_drop_min.max(lower_gap_min + b.ascent())
+ });
+
+ (t_shift, b_shift)
}
-fn compute_shifts_up_and_down(
+/// Calculate the distance from the base's baseline to each script's baseline.
+/// Returns two lengths, the first being the distance to the superscripts'
+/// baseline and the second being the distance to the subscripts' baseline.
+fn compute_script_shifts(
ctx: &MathContext,
styles: StyleChain,
base: &MathFragment,
@@ -504,7 +591,56 @@ fn compute_shifts_up_and_down(
(shift_up, shift_down)
}
-/// Determines if the character is one of a variety of integral signs
+/// Calculate the kerning value for a script with respect to the base. A
+/// positive value means shifting the script further away from the base, whereas
+/// 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 {
+ // This process is described under the MathKernInfo table in the OpenType
+ // MATH spec.
+
+ let (corr_height_top, corr_height_bot) = match pos {
+ // Calculate two correction heights for superscripts:
+ // - The distance from the superscript's baseline to the top of the
+ // base's bounding box.
+ // - The distance from the base's baseline to the bottom of the
+ // superscript's bounding box.
+ Corner::TopLeft | Corner::TopRight => {
+ (base.ascent() - shift, shift - script.descent())
+ }
+ // Calculate two correction heights for subscripts:
+ // - The distance from the base's baseline to the top of the
+ // subscript's bounding box.
+ // - The distance from the subscript's baseline to the bottom of the
+ // base's bounding box.
+ Corner::BottomLeft | Corner::BottomRight => {
+ (script.ascent() - shift, shift - base.descent())
+ }
+ };
+
+ // 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);
+ base_kern + attach_kern
+ };
+
+ // Take the smaller kerning amount (and so the larger value). Note that
+ // there is a bug in the spec (as of 2024-08-15): it says to take the
+ // minimum of the two sums, but as the kerning value is usually negative it
+ // really means the smaller kern. The current wording of the spec could
+ // result in glyphs colliding.
+ summed_kern(corr_height_top).max(summed_kern(corr_height_bot))
+}
+
+/// Determines if the character is one of a variety of integral signs.
fn is_integral_char(c: char) -> bool {
('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c)
}
diff --git a/crates/typst/src/math/fragment.rs b/crates/typst/src/math/fragment.rs
index c6de2486..2adf2d9d 100644
--- a/crates/typst/src/math/fragment.rs
+++ b/crates/typst/src/math/fragment.rs
@@ -184,6 +184,18 @@ impl MathFragment {
_ => Limits::Never,
}
}
+
+ /// 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 {
+ match self {
+ Self::Glyph(glyph) => {
+ kern_at_height(ctx, glyph.font_size, glyph.id, corner, height)
+ .unwrap_or_default()
+ }
+ _ => Abs::zero(),
+ }
+ }
}
impl From<GlyphFragment> for MathFragment {
@@ -552,10 +564,6 @@ fn is_extended_shape(ctx: &MathContext, id: GlyphId) -> bool {
}
/// Look up a kerning value at a specific corner and height.
-///
-/// This can be integrated once we've found a font that actually provides this
-/// data.
-#[allow(unused)]
fn kern_at_height(
ctx: &MathContext,
font_size: Abs,