Use the shaper in math
@ -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;
|
||||
|
@ -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,
|
||||
@ -1049,17 +1049,6 @@ fn nbsp_delta(font: &Font) -> Option<Em> {
|
||||
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()
|
||||
}
|
||||
|
||||
/// Returns true if all glyphs in `glyphs` have ranges within the range `range`.
|
||||
#[cfg(debug_assertions)]
|
||||
fn assert_all_glyphs_in_range(glyphs: &[ShapedGlyph], text: &str, range: Range) {
|
||||
|
@ -3,7 +3,10 @@ use typst_library::foundations::{Packed, StyleChain};
|
||||
use typst_library::layout::{Em, Frame, Point, Size};
|
||||
use typst_library::math::{Accent, 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,15 +18,12 @@ 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))?;
|
||||
// Try to replace the base glyph with its dotless variant.
|
||||
let dtls = style_dtls();
|
||||
let base_styles = if elem.dotless(styles) { styles.chain(&dtls) } else { styles };
|
||||
|
||||
// Try to replace a glyph with its dotless variant.
|
||||
if elem.dotless(styles) {
|
||||
if let MathFragment::Glyph(glyph) = &mut base {
|
||||
glyph.make_dotless_form(ctx);
|
||||
}
|
||||
}
|
||||
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();
|
||||
@ -31,21 +31,21 @@ pub fn layout_accent(
|
||||
|
||||
let width = elem.size(styles).relative_to(base.width());
|
||||
|
||||
let Accent(c) = elem.accent;
|
||||
let mut glyph = GlyphFragment::new(ctx, styles, c, elem.span());
|
||||
|
||||
// Try to replace accent glyph with flattened variant.
|
||||
// Try to replace the accent glyph with its flattened variant.
|
||||
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
|
||||
if base.ascent() > flattened_base_height {
|
||||
glyph.make_flattened_accent_form(ctx);
|
||||
}
|
||||
let flac = style_flac();
|
||||
let accent_styles =
|
||||
if base.ascent() > flattened_base_height { styles.chain(&flac) } else { styles };
|
||||
|
||||
let Accent(c) = elem.accent;
|
||||
let mut glyph = GlyphFragment::new(ctx.font, accent_styles, c, elem.span());
|
||||
|
||||
// Forcing the accent to be at least as large as the base makes it too
|
||||
// wide in many case.
|
||||
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;
|
||||
let short_fall = ACCENT_SHORT_FALL.at(glyph.text.size);
|
||||
glyph.stretch_horizontal(ctx, width, short_fall);
|
||||
let accent_attach = glyph.accent_attach;
|
||||
let accent = glyph.into_frame();
|
||||
|
||||
// Descent is negative because the accent's ink bottom is above the
|
||||
// baseline. Therefore, the default gap is the accent's negated descent
|
||||
|
@ -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
|
||||
});
|
||||
|
||||
@ -467,13 +462,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.
|
||||
|
||||
@ -498,8 +487,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
|
||||
};
|
||||
|
||||
|
@ -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(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(ctx.font, styles, ')', span);
|
||||
right.stretch_vertical(ctx, height, short_fall);
|
||||
right.center_on_axis();
|
||||
ctx.push(right);
|
||||
} else {
|
||||
frame.push(
|
||||
|
@ -1,28 +1,31 @@
|
||||
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 rustybuzz::{BufferFlags, UnicodeBuffer};
|
||||
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
|
||||
use ttf_parser::GlyphId;
|
||||
use typst_library::diag::warning;
|
||||
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 crate::inline::create_shape_plan;
|
||||
use crate::modifiers::{FrameModifiers, FrameModify};
|
||||
|
||||
use super::MathContext;
|
||||
|
||||
/// Maximum number of times extenders can be repeated.
|
||||
const MAX_REPEATS: usize = 1024;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MathFragment {
|
||||
Glyph(GlyphFragment),
|
||||
Variant(VariantFragment),
|
||||
Frame(FrameFragment),
|
||||
Spacing(Abs, bool),
|
||||
Space(Abs),
|
||||
@ -33,13 +36,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 +57,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 +65,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 +90,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 +102,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 +109,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.text.size),
|
||||
Self::Frame(fragment) => Some(fragment.font_size),
|
||||
_ => None,
|
||||
}
|
||||
@ -116,7 +118,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 +126,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 +149,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 +157,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 +165,6 @@ impl MathFragment {
|
||||
pub fn accent_attach(&self) -> 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,
|
||||
}
|
||||
@ -176,7 +173,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 +186,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 +193,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)
|
||||
// For glyph assemblies we pick either the start or end glyph
|
||||
// depending on the corner.
|
||||
let is_vertical =
|
||||
glyph.text.glyphs.iter().any(|glyph| glyph.y_advance != Em::zero());
|
||||
let glyph_index = match (is_vertical, corner) {
|
||||
(true, Corner::TopLeft | Corner::TopRight) => {
|
||||
glyph.text.glyphs.len() - 1
|
||||
}
|
||||
(false, Corner::TopRight | Corner::BottomRight) => {
|
||||
glyph.text.glyphs.len() - 1
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
kern_at_height(
|
||||
&glyph.text.font,
|
||||
GlyphId(glyph.text.glyphs[glyph_index].id),
|
||||
corner,
|
||||
Em::from_length(height, glyph.text.size),
|
||||
)
|
||||
.unwrap_or_default()
|
||||
.at(glyph.text.size)
|
||||
}
|
||||
_ => Abs::zero(),
|
||||
}
|
||||
@ -215,12 +230,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 +238,281 @@ 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 text: TextItem,
|
||||
pub base_id: GlyphId,
|
||||
// Math stuff.
|
||||
pub size: Size,
|
||||
pub baseline: Option<Abs>,
|
||||
pub italics_correction: Abs,
|
||||
pub accent_attach: 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 new(font: &Font, styles: StyleChain, c: char, span: Span) -> Self {
|
||||
Self::try_new(font, styles, c.encode_utf8(&mut [0; 4]), span).unwrap()
|
||||
}
|
||||
|
||||
pub fn try_new(
|
||||
ctx: &MathContext,
|
||||
font: &Font,
|
||||
styles: StyleChain,
|
||||
c: char,
|
||||
str: &str,
|
||||
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))
|
||||
let mut buffer = UnicodeBuffer::new();
|
||||
buffer.push_str(str);
|
||||
buffer.set_language(language(styles));
|
||||
// TODO: Bug in rustybuzz? rustybuzz::script::SCRIPT_MATH does not work.
|
||||
// i.e. ssty is not applied
|
||||
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 {
|
||||
return None;
|
||||
}
|
||||
|
||||
pub fn with_id(
|
||||
ctx: &MathContext,
|
||||
styles: StyleChain,
|
||||
c: char,
|
||||
id: GlyphId,
|
||||
span: Span,
|
||||
) -> Self {
|
||||
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 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cluster = info.cluster as usize;
|
||||
let c = str[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 text = 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: str.into(),
|
||||
glyphs: vec![Glyph {
|
||||
id: info.glyph_id as u16,
|
||||
x_advance: font.to_em(pos.x_advance),
|
||||
x_offset: Em::zero(),
|
||||
y_advance: Em::zero(),
|
||||
y_offset: Em::zero(),
|
||||
range: 0..str.len() as u16,
|
||||
span: (span, 0),
|
||||
}],
|
||||
};
|
||||
|
||||
let mut fragment = Self {
|
||||
text,
|
||||
base_id: GlyphId(info.glyph_id as u16),
|
||||
// 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(),
|
||||
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();
|
||||
Some(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.text.glyphs[0].id);
|
||||
|
||||
let mut width = advance.scaled(ctx, self.font_size);
|
||||
let accent_attach =
|
||||
accent_attach(ctx, id, self.font_size).unwrap_or((width + italics) / 2.0);
|
||||
|
||||
let extended_shape = is_extended_shape(ctx, id);
|
||||
let extended_shape = is_extended_shape(&self.text.font, id);
|
||||
let italics = italics_correction(&self.text.font, id).unwrap_or_default();
|
||||
let width = self.text.width();
|
||||
if !extended_shape {
|
||||
width += italics;
|
||||
self.text.glyphs[0].x_advance += italics;
|
||||
}
|
||||
let italics = italics.at(self.text.size);
|
||||
|
||||
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);
|
||||
let (ascent, descent) =
|
||||
ascent_descent(&self.text.font, id).unwrap_or((Em::zero(), Em::zero()));
|
||||
|
||||
let accent_attach = accent_attach(&self.text.font, id)
|
||||
.map(|x| x.at(self.text.size))
|
||||
.unwrap_or((width + italics) / 2.0);
|
||||
|
||||
self.baseline = Some(ascent.at(self.text.size));
|
||||
self.size = Size::new(
|
||||
self.text.width(),
|
||||
ascent.at(self.text.size) + descent.at(self.text.size),
|
||||
);
|
||||
self.italics_correction = italics;
|
||||
self.accent_attach = accent_attach;
|
||||
self.extended_shape = extended_shape;
|
||||
}
|
||||
|
||||
pub fn height(&self) -> Abs {
|
||||
self.ascent + self.descent
|
||||
// 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.text.glyphs = vec![Glyph {
|
||||
id: self.base_id.0,
|
||||
x_advance: self.text.font.advance(self.base_id.0).unwrap_or_default(),
|
||||
..self.text.glyphs[0].clone()
|
||||
}];
|
||||
self.update_glyph();
|
||||
}
|
||||
|
||||
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,
|
||||
pub fn baseline(&self) -> Abs {
|
||||
self.ascent()
|
||||
}
|
||||
|
||||
/// The distance from the baseline to the top of the frame.
|
||||
pub fn ascent(&self) -> Abs {
|
||||
self.baseline.unwrap_or(self.size.y)
|
||||
}
|
||||
|
||||
/// The distance from the baseline to the bottom of the frame.
|
||||
pub fn descent(&self) -> Abs {
|
||||
self.size.y - self.ascent()
|
||||
}
|
||||
|
||||
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));
|
||||
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.text),
|
||||
);
|
||||
frame.modify(&self.modifiers);
|
||||
frame
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_dotless_form(&mut self, ctx: &MathContext) {
|
||||
let alt_id =
|
||||
ctx.dtls_table.as_ref().and_then(|dtls| dtls.try_apply(self.id, None));
|
||||
if let Some(alt_id) = alt_id {
|
||||
self.set_id(ctx, alt_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_flattened_accent_form(&mut self, ctx: &MathContext) {
|
||||
let alt_id =
|
||||
ctx.flac_table.as_ref().and_then(|flac| flac.try_apply(self.id, None));
|
||||
if let Some(alt_id) = alt_id {
|
||||
self.set_id(ctx, alt_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to stretch a glyph to a desired height.
|
||||
pub fn stretch_vertical(
|
||||
self,
|
||||
&mut self,
|
||||
ctx: &mut MathContext,
|
||||
height: Abs,
|
||||
short_fall: Abs,
|
||||
) -> VariantFragment {
|
||||
stretch_glyph(ctx, self, height, short_fall, Axis::Y)
|
||||
) {
|
||||
self.stretch(ctx, height, short_fall, Axis::Y)
|
||||
}
|
||||
|
||||
/// Try to stretch a glyph to a desired width.
|
||||
pub fn stretch_horizontal(
|
||||
self,
|
||||
&mut self,
|
||||
ctx: &mut MathContext,
|
||||
width: Abs,
|
||||
short_fall: Abs,
|
||||
) -> VariantFragment {
|
||||
stretch_glyph(ctx, self, width, short_fall, Axis::X)
|
||||
) {
|
||||
self.stretch(ctx, width, short_fall, 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,
|
||||
short_fall: Abs,
|
||||
axis: Axis,
|
||||
) {
|
||||
// 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;
|
||||
}
|
||||
let short_target = target - short_fall;
|
||||
if short_target <= advance {
|
||||
return;
|
||||
}
|
||||
|
||||
let id = GlyphId(self.text.glyphs[0].id);
|
||||
let font = self.text.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.text.font.to_em(variant.advance_measurement).at(self.text.size);
|
||||
if short_target <= best_advance {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// This is either good or the best we've got.
|
||||
if short_target <= best_advance || construction.assembly.is_none() {
|
||||
self.text.glyphs[0].id = best_id.0;
|
||||
self.text.glyphs[0].x_advance =
|
||||
self.text.font.advance(best_id.0).unwrap_or_default();
|
||||
self.update_glyph();
|
||||
return;
|
||||
}
|
||||
|
||||
// Assemble from parts.
|
||||
let assembly = construction.assembly.unwrap();
|
||||
let min_overlap = min_connector_overlap(&self.text.font)
|
||||
.unwrap_or_default()
|
||||
.at(self.text.size);
|
||||
assemble(ctx, self, assembly, min_overlap, target, axis);
|
||||
}
|
||||
|
||||
/// Vertically adjust the fragment's frame so that it is centered
|
||||
/// on the axis.
|
||||
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, align: VAlignment) {
|
||||
let h = self.size.y;
|
||||
let axis = axis_height(&self.text.font).unwrap().at(self.text.size);
|
||||
self.align += self.baseline();
|
||||
self.baseline = Some(align.inv().position(h + axis * 2.0));
|
||||
self.align -= self.baseline();
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for GlyphFragment {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "GlyphFragment({:?})", self.c)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct VariantFragment {
|
||||
pub c: char,
|
||||
pub italics_correction: Abs,
|
||||
pub accent_attach: 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,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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));
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for VariantFragment {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "VariantFragment({:?})", self.c)
|
||||
write!(f, "GlyphFragment({:?})", self.text.text)
|
||||
}
|
||||
}
|
||||
|
||||
@ -559,46 +583,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
|
||||
fn italics_correction(font: &Font, id: GlyphId) -> Option<Em> {
|
||||
font.ttf()
|
||||
.tables()
|
||||
.math?
|
||||
.glyph_info?
|
||||
.italic_corrections?
|
||||
.get(id)?
|
||||
.scaled(ctx, font_size),
|
||||
)
|
||||
.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
|
||||
fn accent_attach(font: &Font, id: GlyphId) -> Option<Em> {
|
||||
font.ttf()
|
||||
.tables()
|
||||
.math?
|
||||
.glyph_info?
|
||||
.top_accent_attachments?
|
||||
.get(id)?
|
||||
.scaled(ctx, font_size),
|
||||
)
|
||||
.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,
|
||||
@ -607,11 +632,186 @@ 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: GlyphId) -> Axes<bool> {
|
||||
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,
|
||||
})
|
||||
.and_then(|constructions| 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.text.font.to_em(part.full_advance).at(base.text.size);
|
||||
if let Some(next) = parts.peek() {
|
||||
let max_overlap = base
|
||||
.text
|
||||
.font
|
||||
.to_em(part.end_connector_length.min(next.start_connector_length))
|
||||
.at(base.text.size);
|
||||
if max_overlap < min_overlap {
|
||||
// This condition happening is indicative of a bug in the
|
||||
// font.
|
||||
ctx.engine.sink.warn(warning!(
|
||||
base.text.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.text.font.to_em(part.full_advance).at(base.text.size);
|
||||
if let Some(next) = parts.peek() {
|
||||
let max_overlap = base
|
||||
.text
|
||||
.font
|
||||
.to_em(part.end_connector_length.min(next.start_connector_length))
|
||||
.at(base.text.size);
|
||||
advance -= max_overlap;
|
||||
advance += ratio * (max_overlap - min_overlap);
|
||||
}
|
||||
let (x, y) = match axis {
|
||||
Axis::X => (Em::from_length(advance, base.text.size), Em::zero()),
|
||||
Axis::Y => (Em::zero(), Em::from_length(advance, base.text.size)),
|
||||
};
|
||||
glyphs.push(Glyph {
|
||||
id: part.glyph_id.0,
|
||||
x_advance: x,
|
||||
x_offset: Em::zero(),
|
||||
y_advance: y,
|
||||
y_offset: Em::zero(),
|
||||
..base.text.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.text.font.advance(glyph.id).unwrap_or_default())
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
.at(base.text.size);
|
||||
}
|
||||
}
|
||||
|
||||
base.text.glyphs = glyphs;
|
||||
base.italics_correction = base
|
||||
.text
|
||||
.font
|
||||
.to_em(assembly.italics_correction.value)
|
||||
.at(base.text.size);
|
||||
if axis == Axis::X {
|
||||
base.accent_attach = 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.
|
||||
@ -664,56 +864,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)
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -184,7 +184,7 @@ fn layout_body(
|
||||
// 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());
|
||||
GlyphFragment::new(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 +202,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 +312,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(ctx.font, styles, left_c, span);
|
||||
left.stretch_vertical(ctx, target, short_fall);
|
||||
left.align_on_axis(delimiter_alignment(left_c));
|
||||
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(ctx.font, styles, right_c, span);
|
||||
right.stretch_vertical(ctx, target, short_fall);
|
||||
right.align_on_axis(delimiter_alignment(right_c));
|
||||
ctx.push(right);
|
||||
}
|
||||
|
||||
|
@ -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,13 +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,26 +383,12 @@ 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(' ')
|
||||
@ -423,13 +401,7 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
|
||||
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![],
|
||||
}
|
||||
|
@ -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, Abs::zero())
|
||||
.frame;
|
||||
let mut sqrt = GlyphFragment::new(ctx.font, styles, '√', span);
|
||||
sqrt.stretch_vertical(ctx, target, Abs::zero());
|
||||
let sqrt = sqrt.into_frame();
|
||||
|
||||
// Layout the index.
|
||||
let sscript = EquationElem::set_size(MathSize::ScriptScript).wrap();
|
||||
|
@ -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::math::{EquationElem, MathSize};
|
||||
use typst_library::text::{FontFeatures, TextElem};
|
||||
use typst_utils::LazyHash;
|
||||
|
||||
use super::{LeftRightAlternator, MathContext, MathFragment, MathRun};
|
||||
@ -59,6 +61,14 @@ pub fn style_cramped() -> LazyHash<Style> {
|
||||
EquationElem::set_cramped(true).wrap()
|
||||
}
|
||||
|
||||
pub fn style_flac() -> LazyHash<Style> {
|
||||
TextElem::set_features(FontFeatures(vec![(Tag::from_bytes(b"flac"), 1)])).wrap()
|
||||
}
|
||||
|
||||
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()]
|
||||
|
@ -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,269 +22,52 @@ 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 {
|
||||
let axes = stretch_axes(&glyph.text.font, glyph.base_id);
|
||||
let stretch_axis = if let Some(axis) = axis {
|
||||
if !axes.get(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,
|
||||
_ => {
|
||||
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!(
|
||||
base.span,
|
||||
glyph.text.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"
|
||||
));
|
||||
|
||||
None
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
short_fall: 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(),
|
||||
};
|
||||
let short_target = target - short_fall;
|
||||
if short_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 short_target <= best_advance {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// This is either good or the best we've got.
|
||||
if short_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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
let accent_attach = match axis {
|
||||
Axis::X => 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,
|
||||
}
|
||||
}
|
||||
glyph.reset_glyph();
|
||||
|
||||
/// 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)
|
||||
})
|
||||
let relative_to_size = relative_to.unwrap_or_else(|| size.get(stretch_axis));
|
||||
|
||||
glyph.stretch(ctx, stretch.relative_to(relative_to_size), short_fall, stretch_axis);
|
||||
|
||||
if stretch_axis == Axis::Y {
|
||||
// TODO: this should use delimiter_alignment
|
||||
glyph.center_on_axis();
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
@ -65,19 +68,9 @@ fn layout_inline_text(
|
||||
// Small optimization for numbers. Note that this lays out slightly
|
||||
// differently to normal text and is worth re-evaluating in the future.
|
||||
let mut fragments = vec![];
|
||||
let is_single = text.chars().count() == 1;
|
||||
for unstyled_c in text.chars() {
|
||||
let c = styled_char(styles, unstyled_c, false);
|
||||
let mut glyph = GlyphFragment::new(ctx, styles, c, span);
|
||||
if is_single {
|
||||
// Duplicate what `layout_glyph` does exactly even if it's
|
||||
// probably incorrect here.
|
||||
match EquationElem::size_in(styles) {
|
||||
MathSize::Script => glyph.make_script_size(ctx),
|
||||
MathSize::ScriptScript => glyph.make_script_script_size(ctx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let glyph = GlyphFragment::new(ctx.font, styles, c, span);
|
||||
fragments.push(glyph.into());
|
||||
}
|
||||
let frame = MathRun::new(fragments).into_frame(styles);
|
||||
@ -126,15 +119,25 @@ 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),
|
||||
let fragment: MathFragment = match GlyphFragment::try_new(
|
||||
ctx.font,
|
||||
symbol_styles,
|
||||
c.encode_utf8(&mut [0; 4]),
|
||||
elem.span(),
|
||||
) {
|
||||
Some(mut glyph) => {
|
||||
layout_glyph(&mut glyph, ctx, styles);
|
||||
glyph.into()
|
||||
}
|
||||
None => {
|
||||
// Not in the math font, fallback to normal inline text layout.
|
||||
// TODO: Should replace this with proper fallback in [`GlyphFragment::try_new`].
|
||||
layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)?
|
||||
.into()
|
||||
}
|
||||
@ -144,36 +147,17 @@ pub fn layout_symbol(
|
||||
}
|
||||
|
||||
/// Layout a [`GlyphFragment`].
|
||||
fn layout_glyph(
|
||||
mut glyph: GlyphFragment,
|
||||
dtls: bool,
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> MathFragment {
|
||||
if dtls {
|
||||
glyph.make_dotless_form(ctx);
|
||||
}
|
||||
fn layout_glyph(glyph: &mut GlyphFragment, ctx: &mut MathContext, styles: StyleChain) {
|
||||
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 math_size == MathSize::Display {
|
||||
let height = scaled!(ctx, styles, display_operator_min_height)
|
||||
.max(SQRT_2 * glyph.height());
|
||||
glyph.stretch_vertical(ctx, height, Abs::zero())
|
||||
} else {
|
||||
glyph.into_variant()
|
||||
.max(SQRT_2 * glyph.size.y);
|
||||
glyph.stretch_vertical(ctx, height, Abs::zero());
|
||||
};
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(), Abs::zero());
|
||||
let mut glyph = GlyphFragment::new(ctx.font, styles, c, span);
|
||||
glyph.stretch_horizontal(ctx, body.width(), Abs::zero());
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -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 of the glyph.
|
||||
pub y_advance: Em,
|
||||
/// The vertical offset 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)
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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> {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
|
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 461 B |
Before Width: | Height: | Size: 644 B After Width: | Height: | Size: 716 B |
BIN
tests/ref/math-accent-dotless-greedy.png
Normal file
After Width: | Height: | Size: 726 B |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 670 B After Width: | Height: | Size: 687 B |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 902 B After Width: | Height: | Size: 897 B |
Before Width: | Height: | Size: 648 B After Width: | Height: | Size: 640 B |
@ -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")
|
||||
|