mirror of
https://github.com/typst/typst
synced 2025-07-27 22:37:54 +08:00
966 lines
30 KiB
Rust
966 lines
30 KiB
Rust
use std::fmt::{self, Debug, Formatter};
|
|
|
|
use az::SaturatingAs;
|
|
use rustybuzz::{BufferFlags, UnicodeBuffer};
|
|
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
|
|
use ttf_parser::GlyphId;
|
|
use typst_library::diag::{bail, warning, SourceResult};
|
|
use typst_library::foundations::StyleChain;
|
|
use typst_library::introspection::Tag;
|
|
use typst_library::layout::{
|
|
Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
|
|
};
|
|
use typst_library::math::{EquationElem, MathSize};
|
|
use typst_library::text::{
|
|
families, features, language, variant, Font, Glyph, TextElem, TextItem,
|
|
};
|
|
use typst_library::visualize::Paint;
|
|
use typst_library::World;
|
|
use typst_syntax::Span;
|
|
use typst_utils::{default_math_class, Get};
|
|
use unicode_math_class::MathClass;
|
|
|
|
use super::{find_math_font, MathContext};
|
|
use crate::inline::create_shape_plan;
|
|
use crate::modifiers::{FrameModifiers, FrameModify};
|
|
|
|
/// Maximum number of times extenders can be repeated.
|
|
const MAX_REPEATS: usize = 1024;
|
|
|
|
#[allow(clippy::large_enum_variant)]
|
|
#[derive(Debug, Clone)]
|
|
pub enum MathFragment {
|
|
Glyph(GlyphFragment),
|
|
Frame(FrameFragment),
|
|
Spacing(Abs, bool),
|
|
Space(Abs),
|
|
Linebreak,
|
|
Align,
|
|
Tag(Tag),
|
|
}
|
|
|
|
impl MathFragment {
|
|
pub fn size(&self) -> Size {
|
|
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.size.x,
|
|
Self::Frame(fragment) => fragment.frame.width(),
|
|
Self::Spacing(amount, _) => *amount,
|
|
Self::Space(amount) => *amount,
|
|
_ => Abs::zero(),
|
|
}
|
|
}
|
|
|
|
pub fn height(&self) -> Abs {
|
|
match self {
|
|
Self::Glyph(glyph) => glyph.size.y,
|
|
Self::Frame(fragment) => fragment.frame.height(),
|
|
_ => Abs::zero(),
|
|
}
|
|
}
|
|
|
|
pub fn ascent(&self) -> Abs {
|
|
match self {
|
|
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::Frame(fragment) => fragment.frame.descent(),
|
|
_ => Abs::zero(),
|
|
}
|
|
}
|
|
|
|
pub fn is_ignorant(&self) -> bool {
|
|
match self {
|
|
Self::Frame(fragment) => fragment.ignorant,
|
|
Self::Tag(_) => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
pub fn class(&self) -> MathClass {
|
|
match self {
|
|
Self::Glyph(glyph) => glyph.class,
|
|
Self::Frame(fragment) => fragment.class,
|
|
Self::Spacing(_, _) => MathClass::Space,
|
|
Self::Space(_) => MathClass::Space,
|
|
Self::Linebreak => MathClass::Space,
|
|
Self::Align => MathClass::Special,
|
|
Self::Tag(_) => MathClass::Special,
|
|
}
|
|
}
|
|
|
|
pub fn math_size(&self) -> Option<MathSize> {
|
|
match self {
|
|
Self::Glyph(glyph) => Some(glyph.math_size),
|
|
Self::Frame(fragment) => Some(fragment.math_size),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn font(
|
|
&self,
|
|
ctx: &MathContext,
|
|
styles: StyleChain,
|
|
span: Span,
|
|
) -> SourceResult<(Font, Abs)> {
|
|
Ok((
|
|
match self {
|
|
Self::Glyph(glyph) => glyph.item.font.clone(),
|
|
_ => find_math_font(ctx.engine.world, styles, span)?,
|
|
},
|
|
self.font_size().unwrap_or_else(|| styles.resolve(TextElem::size)),
|
|
))
|
|
}
|
|
|
|
pub fn font_size(&self) -> Option<Abs> {
|
|
match self {
|
|
Self::Glyph(glyph) => Some(glyph.item.size),
|
|
Self::Frame(fragment) => Some(fragment.font_size),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn set_class(&mut self, class: MathClass) {
|
|
match self {
|
|
Self::Glyph(glyph) => glyph.class = class,
|
|
Self::Frame(fragment) => fragment.class = class,
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
pub fn set_limits(&mut self, limits: Limits) {
|
|
match self {
|
|
Self::Glyph(glyph) => glyph.limits = limits,
|
|
Self::Frame(fragment) => fragment.limits = limits,
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
pub fn is_spaced(&self) -> bool {
|
|
if self.class() == MathClass::Fence {
|
|
return true;
|
|
}
|
|
|
|
matches!(
|
|
self,
|
|
MathFragment::Frame(FrameFragment {
|
|
spaced: true,
|
|
class: MathClass::Normal | MathClass::Alphabetic,
|
|
..
|
|
})
|
|
)
|
|
}
|
|
|
|
pub fn is_text_like(&self) -> bool {
|
|
match self {
|
|
Self::Glyph(glyph) => !glyph.extended_shape,
|
|
MathFragment::Frame(frame) => frame.text_like,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
pub fn italics_correction(&self) -> Abs {
|
|
match self {
|
|
Self::Glyph(glyph) => glyph.italics_correction,
|
|
Self::Frame(fragment) => fragment.italics_correction,
|
|
_ => Abs::zero(),
|
|
}
|
|
}
|
|
|
|
pub fn accent_attach(&self) -> (Abs, Abs) {
|
|
match self {
|
|
Self::Glyph(glyph) => glyph.accent_attach,
|
|
Self::Frame(fragment) => fragment.accent_attach,
|
|
_ => (self.width() / 2.0, self.width() / 2.0),
|
|
}
|
|
}
|
|
|
|
pub fn into_frame(self) -> Frame {
|
|
match self {
|
|
Self::Glyph(glyph) => glyph.into_frame(),
|
|
Self::Frame(fragment) => fragment.frame,
|
|
Self::Tag(tag) => {
|
|
let mut frame = Frame::soft(Size::zero());
|
|
frame.push(Point::zero(), FrameItem::Tag(tag));
|
|
frame
|
|
}
|
|
_ => Frame::soft(self.size()),
|
|
}
|
|
}
|
|
|
|
pub fn limits(&self) -> Limits {
|
|
match self {
|
|
MathFragment::Glyph(glyph) => glyph.limits,
|
|
MathFragment::Frame(fragment) => fragment.limits,
|
|
_ => Limits::Never,
|
|
}
|
|
}
|
|
|
|
pub fn fill(&self) -> Option<Paint> {
|
|
match self {
|
|
Self::Glyph(glyph) => Some(glyph.item.fill.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn stretch_vertical(&mut self, ctx: &mut MathContext, height: Abs) {
|
|
if let Self::Glyph(glyph) = self {
|
|
glyph.stretch_vertical(ctx, height)
|
|
}
|
|
}
|
|
|
|
pub fn stretch_horizontal(&mut self, ctx: &mut MathContext, width: Abs) {
|
|
if let Self::Glyph(glyph) = self {
|
|
glyph.stretch_horizontal(ctx, width)
|
|
}
|
|
}
|
|
|
|
pub fn center_on_axis(&mut self) {
|
|
if let Self::Glyph(glyph) = self {
|
|
glyph.center_on_axis()
|
|
}
|
|
}
|
|
|
|
/// If no kern table is provided for a corner, a kerning amount of zero is
|
|
/// assumed.
|
|
pub fn kern_at_height(&self, corner: Corner, height: Abs) -> Abs {
|
|
match self {
|
|
Self::Glyph(glyph) => {
|
|
// For glyph assemblies we pick either the start or end glyph
|
|
// depending on the corner.
|
|
let is_vertical =
|
|
glyph.item.glyphs.iter().all(|glyph| glyph.y_advance != Em::zero());
|
|
let glyph_index = match (is_vertical, corner) {
|
|
(true, Corner::TopLeft | Corner::TopRight) => {
|
|
glyph.item.glyphs.len() - 1
|
|
}
|
|
(false, Corner::TopRight | Corner::BottomRight) => {
|
|
glyph.item.glyphs.len() - 1
|
|
}
|
|
_ => 0,
|
|
};
|
|
|
|
kern_at_height(
|
|
&glyph.item.font,
|
|
GlyphId(glyph.item.glyphs[glyph_index].id),
|
|
corner,
|
|
Em::from_abs(height, glyph.item.size),
|
|
)
|
|
.unwrap_or_default()
|
|
.at(glyph.item.size)
|
|
}
|
|
_ => Abs::zero(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<GlyphFragment> for MathFragment {
|
|
fn from(glyph: GlyphFragment) -> Self {
|
|
Self::Glyph(glyph)
|
|
}
|
|
}
|
|
|
|
impl From<FrameFragment> for MathFragment {
|
|
fn from(fragment: FrameFragment) -> Self {
|
|
Self::Frame(fragment)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct GlyphFragment {
|
|
// Text stuff.
|
|
pub item: TextItem,
|
|
pub base_glyph: Glyph,
|
|
// Math stuff.
|
|
pub size: Size,
|
|
pub baseline: Option<Abs>,
|
|
pub italics_correction: Abs,
|
|
pub accent_attach: (Abs, Abs),
|
|
pub math_size: MathSize,
|
|
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 {
|
|
/// Calls `new` with the given character.
|
|
pub fn new_char(
|
|
ctx: &MathContext,
|
|
styles: StyleChain,
|
|
c: char,
|
|
span: Span,
|
|
) -> SourceResult<Option<Self>> {
|
|
Self::new(ctx, styles, c.encode_utf8(&mut [0; 4]), span)
|
|
}
|
|
|
|
/// Selects a font to use and then shapes text.
|
|
pub fn new(
|
|
ctx: &MathContext,
|
|
styles: StyleChain,
|
|
text: &str,
|
|
span: Span,
|
|
) -> SourceResult<Option<Self>> {
|
|
let families = families(styles);
|
|
let variant = variant(styles);
|
|
let fallback = styles.get(TextElem::fallback);
|
|
let end = text.char_indices().nth(1).map(|(i, _)| i).unwrap_or(text.len());
|
|
|
|
// Find the next available family.
|
|
let world = ctx.engine.world;
|
|
let book = world.book();
|
|
let mut selection = None;
|
|
for family in families {
|
|
selection = book
|
|
.select(family.as_str(), variant)
|
|
.and_then(|id| world.font(id))
|
|
.filter(|font| {
|
|
font.ttf().tables().math.and_then(|math| math.constants).is_some()
|
|
})
|
|
.filter(|_| family.covers().is_none_or(|cov| cov.is_match(&text[..end])));
|
|
if selection.is_some() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Do font fallback if the families are exhausted and fallback is enabled.
|
|
if selection.is_none() && fallback {
|
|
selection = book
|
|
.select_fallback(None, variant, text)
|
|
.and_then(|id| world.font(id))
|
|
.filter(|font| {
|
|
font.ttf().tables().math.and_then(|math| math.constants).is_some()
|
|
});
|
|
}
|
|
|
|
// Error out if no math font could be found at all.
|
|
let Some(font) = selection else {
|
|
bail!(span, "current font does not support math");
|
|
};
|
|
|
|
Self::shape(&font, styles, text, span)
|
|
}
|
|
|
|
/// Try to create a new glyph out of the given string. Will bail if the
|
|
/// result from shaping the string is more than a single glyph.
|
|
#[comemo::memoize]
|
|
pub fn shape(
|
|
font: &Font,
|
|
styles: StyleChain,
|
|
text: &str,
|
|
span: Span,
|
|
) -> SourceResult<Option<GlyphFragment>> {
|
|
let mut buffer = UnicodeBuffer::new();
|
|
buffer.push_str(text);
|
|
buffer.set_language(language(styles));
|
|
// TODO: Use `rustybuzz::script::MATH` once
|
|
// https://github.com/harfbuzz/rustybuzz/pull/165 is released.
|
|
buffer.set_script(
|
|
rustybuzz::Script::from_iso15924_tag(ttf_parser::Tag::from_bytes(b"math"))
|
|
.unwrap(),
|
|
);
|
|
buffer.set_direction(rustybuzz::Direction::LeftToRight);
|
|
buffer.set_flags(BufferFlags::REMOVE_DEFAULT_IGNORABLES);
|
|
|
|
let features = features(styles);
|
|
let plan = create_shape_plan(
|
|
font,
|
|
buffer.direction(),
|
|
buffer.script(),
|
|
buffer.language().as_ref(),
|
|
&features,
|
|
);
|
|
|
|
let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
|
|
match buffer.len() {
|
|
0 => return Ok(None),
|
|
1 => {}
|
|
_ => bail!(span, "did not get a single glyph after shaping {}", text),
|
|
}
|
|
|
|
let info = buffer.glyph_infos()[0];
|
|
let pos = buffer.glyph_positions()[0];
|
|
|
|
let cluster = info.cluster as usize;
|
|
let c = text[cluster..].chars().next().unwrap();
|
|
let limits = Limits::for_char(c);
|
|
let class = styles
|
|
.get(EquationElem::class)
|
|
.or_else(|| default_math_class(c))
|
|
.unwrap_or(MathClass::Normal);
|
|
|
|
let glyph = Glyph {
|
|
id: info.glyph_id as u16,
|
|
x_advance: font.to_em(pos.x_advance),
|
|
x_offset: font.to_em(pos.x_offset),
|
|
y_advance: font.to_em(pos.y_advance),
|
|
y_offset: font.to_em(pos.y_offset),
|
|
range: 0..text.len().saturating_as(),
|
|
span: (span, 0),
|
|
};
|
|
|
|
let item = TextItem {
|
|
font: font.clone(),
|
|
size: styles.resolve(TextElem::size),
|
|
fill: styles.get_ref(TextElem::fill).as_decoration(),
|
|
stroke: styles.resolve(TextElem::stroke).map(|s| s.unwrap_or_default()),
|
|
lang: styles.get(TextElem::lang),
|
|
region: styles.get(TextElem::region),
|
|
text: text.into(),
|
|
glyphs: vec![glyph.clone()],
|
|
};
|
|
|
|
let mut fragment = Self {
|
|
item,
|
|
base_glyph: glyph,
|
|
// Math
|
|
math_size: styles.get(EquationElem::size),
|
|
class,
|
|
limits,
|
|
mid_stretched: None,
|
|
// Math in need of updating.
|
|
extended_shape: false,
|
|
italics_correction: Abs::zero(),
|
|
accent_attach: (Abs::zero(), Abs::zero()),
|
|
size: Size::zero(),
|
|
baseline: None,
|
|
// Misc
|
|
align: Abs::zero(),
|
|
shift: styles.resolve(TextElem::baseline),
|
|
modifiers: FrameModifiers::get_in(styles),
|
|
};
|
|
fragment.update_glyph();
|
|
Ok(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 update_glyph(&mut self) {
|
|
let id = GlyphId(self.item.glyphs[0].id);
|
|
|
|
let extended_shape = is_extended_shape(&self.item.font, id);
|
|
let italics = italics_correction(&self.item.font, id).unwrap_or_default();
|
|
let width = self.item.width();
|
|
if !extended_shape {
|
|
self.item.glyphs[0].x_advance += italics;
|
|
}
|
|
let italics = italics.at(self.item.size);
|
|
|
|
let (ascent, descent) =
|
|
ascent_descent(&self.item.font, id).unwrap_or((Em::zero(), Em::zero()));
|
|
|
|
// The fallback for accents is half the width plus or minus the italics
|
|
// correction. This is similar to how top and bottom attachments are
|
|
// shifted. For bottom accents we do not use the accent attach of the
|
|
// base as it is meant for top acccents.
|
|
let top_accent_attach = accent_attach(&self.item.font, id)
|
|
.map(|x| x.at(self.item.size))
|
|
.unwrap_or((width + italics) / 2.0);
|
|
let bottom_accent_attach = (width - italics) / 2.0;
|
|
|
|
self.baseline = Some(ascent.at(self.item.size));
|
|
self.size = Size::new(
|
|
self.item.width(),
|
|
ascent.at(self.item.size) + descent.at(self.item.size),
|
|
);
|
|
self.italics_correction = italics;
|
|
self.accent_attach = (top_accent_attach, bottom_accent_attach);
|
|
self.extended_shape = extended_shape;
|
|
}
|
|
|
|
// Reset a GlyphFragment's text field and math properties back to its
|
|
// base_id's. This is used to return a glyph to its unstretched state.
|
|
pub fn reset_glyph(&mut self) {
|
|
self.align = Abs::zero();
|
|
self.item.glyphs = vec![self.base_glyph.clone()];
|
|
self.update_glyph();
|
|
}
|
|
|
|
pub fn 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 mut frame = Frame::soft(self.size);
|
|
frame.set_baseline(self.baseline());
|
|
frame.push(
|
|
Point::with_y(self.ascent() + self.shift + self.align),
|
|
FrameItem::Text(self.item),
|
|
);
|
|
frame.modify(&self.modifiers);
|
|
frame
|
|
}
|
|
|
|
/// Try to stretch a glyph to a desired height.
|
|
pub fn stretch_vertical(&mut self, ctx: &mut MathContext, height: Abs) {
|
|
self.stretch(ctx, height, Axis::Y)
|
|
}
|
|
|
|
/// Try to stretch a glyph to a desired width.
|
|
pub fn stretch_horizontal(&mut self, ctx: &mut MathContext, width: Abs) {
|
|
self.stretch(ctx, width, Axis::X)
|
|
}
|
|
|
|
/// Try to stretch a glyph to a desired width or height.
|
|
///
|
|
/// The resulting frame may not have the exact desired width or height.
|
|
pub fn stretch(&mut self, ctx: &mut MathContext, target: Abs, axis: Axis) {
|
|
self.reset_glyph();
|
|
|
|
// If the base glyph is good enough, use it.
|
|
let mut advance = self.size.get(axis);
|
|
if axis == Axis::X && !self.extended_shape {
|
|
// For consistency, we subtract the italics correction from the
|
|
// glyph's width if it was added in `update_glyph`.
|
|
advance -= self.italics_correction;
|
|
}
|
|
if target <= advance {
|
|
return;
|
|
}
|
|
|
|
let id = GlyphId(self.item.glyphs[0].id);
|
|
let font = self.item.font.clone();
|
|
let Some(construction) = glyph_construction(&font, id, axis) else { return };
|
|
|
|
// Search for a pre-made variant with a good advance.
|
|
let mut best_id = id;
|
|
let mut best_advance = advance;
|
|
for variant in construction.variants {
|
|
best_id = variant.variant_glyph;
|
|
best_advance =
|
|
self.item.font.to_em(variant.advance_measurement).at(self.item.size);
|
|
if target <= best_advance {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// This is either good or the best we've got.
|
|
if target <= best_advance || construction.assembly.is_none() {
|
|
self.item.glyphs[0].id = best_id.0;
|
|
self.item.glyphs[0].x_advance =
|
|
self.item.font.x_advance(best_id.0).unwrap_or_default();
|
|
self.item.glyphs[0].x_offset = Em::zero();
|
|
self.item.glyphs[0].y_advance =
|
|
self.item.font.y_advance(best_id.0).unwrap_or_default();
|
|
self.item.glyphs[0].y_offset = Em::zero();
|
|
self.update_glyph();
|
|
return;
|
|
}
|
|
|
|
// Assemble from parts.
|
|
let assembly = construction.assembly.unwrap();
|
|
let min_overlap = min_connector_overlap(&self.item.font)
|
|
.unwrap_or_default()
|
|
.at(self.item.size);
|
|
assemble(ctx, self, assembly, min_overlap, target, axis);
|
|
}
|
|
|
|
/// 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.item.font).unwrap().at(self.item.size);
|
|
self.align += self.baseline();
|
|
self.baseline = Some(align.inv().position(h + axis * 2.0));
|
|
self.align -= self.baseline();
|
|
}
|
|
}
|
|
|
|
impl Debug for GlyphFragment {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
write!(f, "GlyphFragment({:?})", self.item.text)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct FrameFragment {
|
|
pub frame: Frame,
|
|
pub font_size: Abs,
|
|
pub class: MathClass,
|
|
pub math_size: MathSize,
|
|
pub limits: Limits,
|
|
pub spaced: bool,
|
|
pub base_ascent: Abs,
|
|
pub base_descent: Abs,
|
|
pub italics_correction: Abs,
|
|
pub accent_attach: (Abs, Abs),
|
|
pub text_like: bool,
|
|
pub ignorant: bool,
|
|
}
|
|
|
|
impl FrameFragment {
|
|
pub fn new(styles: StyleChain, frame: Frame) -> Self {
|
|
let base_ascent = frame.ascent();
|
|
let base_descent = frame.descent();
|
|
let accent_attach = frame.width() / 2.0;
|
|
Self {
|
|
frame: frame.modified(&FrameModifiers::get_in(styles)),
|
|
font_size: styles.resolve(TextElem::size),
|
|
class: styles.get(EquationElem::class).unwrap_or(MathClass::Normal),
|
|
math_size: styles.get(EquationElem::size),
|
|
limits: Limits::Never,
|
|
spaced: false,
|
|
base_ascent,
|
|
base_descent,
|
|
italics_correction: Abs::zero(),
|
|
accent_attach: (accent_attach, accent_attach),
|
|
text_like: false,
|
|
ignorant: false,
|
|
}
|
|
}
|
|
|
|
pub fn with_class(self, class: MathClass) -> Self {
|
|
Self { class, ..self }
|
|
}
|
|
|
|
pub fn with_limits(self, limits: Limits) -> Self {
|
|
Self { limits, ..self }
|
|
}
|
|
|
|
pub fn with_spaced(self, spaced: bool) -> Self {
|
|
Self { spaced, ..self }
|
|
}
|
|
|
|
pub fn with_base_ascent(self, base_ascent: Abs) -> Self {
|
|
Self { base_ascent, ..self }
|
|
}
|
|
|
|
pub fn with_base_descent(self, base_descent: Abs) -> Self {
|
|
Self { base_descent, ..self }
|
|
}
|
|
|
|
pub fn with_italics_correction(self, italics_correction: Abs) -> Self {
|
|
Self { italics_correction, ..self }
|
|
}
|
|
|
|
pub fn with_accent_attach(self, accent_attach: (Abs, Abs)) -> Self {
|
|
Self { accent_attach, ..self }
|
|
}
|
|
|
|
pub fn with_text_like(self, text_like: bool) -> Self {
|
|
Self { text_like, ..self }
|
|
}
|
|
|
|
pub fn with_ignorant(self, ignorant: bool) -> Self {
|
|
Self { ignorant, ..self }
|
|
}
|
|
}
|
|
|
|
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(font: &Font, id: GlyphId) -> Option<Em> {
|
|
font.ttf()
|
|
.tables()
|
|
.math?
|
|
.glyph_info?
|
|
.italic_corrections?
|
|
.get(id)
|
|
.map(|value| font.to_em(value.value))
|
|
}
|
|
|
|
/// Loop up the top accent attachment position for a glyph.
|
|
fn accent_attach(font: &Font, id: GlyphId) -> Option<Em> {
|
|
font.ttf()
|
|
.tables()
|
|
.math?
|
|
.glyph_info?
|
|
.top_accent_attachments?
|
|
.get(id)
|
|
.map(|value| font.to_em(value.value))
|
|
}
|
|
|
|
/// Look up whether a glyph is an extended shape.
|
|
fn is_extended_shape(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(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,
|
|
Corner::BottomRight => kerns.bottom_right,
|
|
Corner::BottomLeft => kerns.bottom_left,
|
|
}?;
|
|
|
|
let mut i = 0;
|
|
while i < kern.count() && height > font.to_em(kern.height(i)?.value) {
|
|
i += 1;
|
|
}
|
|
|
|
Some(font.to_em(kern.kern(i)?.value))
|
|
}
|
|
|
|
fn axis_height(font: &Font) -> Option<Em> {
|
|
Some(font.to_em(font.ttf().tables().math?.constants?.axis_height().value))
|
|
}
|
|
|
|
pub fn stretch_axes(font: &Font, id: u16) -> Axes<bool> {
|
|
let id = GlyphId(id);
|
|
let horizontal = font
|
|
.ttf()
|
|
.tables()
|
|
.math
|
|
.and_then(|math| math.variants)
|
|
.and_then(|variants| variants.horizontal_constructions.get(id))
|
|
.is_some();
|
|
let vertical = font
|
|
.ttf()
|
|
.tables()
|
|
.math
|
|
.and_then(|math| math.variants)
|
|
.and_then(|variants| variants.vertical_constructions.get(id))
|
|
.is_some();
|
|
|
|
Axes::new(horizontal, vertical)
|
|
}
|
|
|
|
fn min_connector_overlap(font: &Font) -> Option<Em> {
|
|
font.ttf()
|
|
.tables()
|
|
.math?
|
|
.variants
|
|
.map(|variants| font.to_em(variants.min_connector_overlap))
|
|
}
|
|
|
|
fn glyph_construction(font: &Font, id: GlyphId, axis: Axis) -> Option<GlyphConstruction> {
|
|
font.ttf()
|
|
.tables()
|
|
.math?
|
|
.variants
|
|
.map(|variants| match axis {
|
|
Axis::X => variants.horizontal_constructions,
|
|
Axis::Y => variants.vertical_constructions,
|
|
})?
|
|
.get(id)
|
|
}
|
|
|
|
/// Assemble a glyph from parts.
|
|
fn assemble(
|
|
ctx: &mut MathContext,
|
|
base: &mut GlyphFragment,
|
|
assembly: GlyphAssembly,
|
|
min_overlap: Abs,
|
|
target: Abs,
|
|
axis: Axis,
|
|
) {
|
|
// Determine the number of times the extenders need to be repeated as well
|
|
// as a ratio specifying how much to spread the parts apart
|
|
// (0 = maximal overlap, 1 = minimal overlap).
|
|
let mut full;
|
|
let mut ratio;
|
|
let mut repeat = 0;
|
|
loop {
|
|
full = Abs::zero();
|
|
ratio = 0.0;
|
|
|
|
let mut parts = parts(assembly, repeat).peekable();
|
|
let mut growable = Abs::zero();
|
|
|
|
while let Some(part) = parts.next() {
|
|
let mut advance = base.item.font.to_em(part.full_advance).at(base.item.size);
|
|
if let Some(next) = parts.peek() {
|
|
let max_overlap = base
|
|
.item
|
|
.font
|
|
.to_em(part.end_connector_length.min(next.start_connector_length))
|
|
.at(base.item.size);
|
|
if max_overlap < min_overlap {
|
|
// This condition happening is indicative of a bug in the
|
|
// font.
|
|
ctx.engine.sink.warn(warning!(
|
|
base.item.glyphs[0].span.0,
|
|
"glyph has assembly parts with overlap less than minConnectorOverlap";
|
|
hint: "its rendering may appear broken - this is probably a font bug";
|
|
hint: "please file an issue at https://github.com/typst/typst/issues"
|
|
));
|
|
}
|
|
|
|
advance -= max_overlap;
|
|
growable += max_overlap - min_overlap;
|
|
}
|
|
|
|
full += advance;
|
|
}
|
|
|
|
if full < target {
|
|
let delta = target - full;
|
|
ratio = (delta / growable).min(1.0);
|
|
full += ratio * growable;
|
|
}
|
|
|
|
if target <= full || repeat >= MAX_REPEATS {
|
|
break;
|
|
}
|
|
|
|
repeat += 1;
|
|
}
|
|
|
|
let mut glyphs = vec![];
|
|
let mut parts = parts(assembly, repeat).peekable();
|
|
while let Some(part) = parts.next() {
|
|
let mut advance = base.item.font.to_em(part.full_advance).at(base.item.size);
|
|
if let Some(next) = parts.peek() {
|
|
let max_overlap = base
|
|
.item
|
|
.font
|
|
.to_em(part.end_connector_length.min(next.start_connector_length))
|
|
.at(base.item.size);
|
|
advance -= max_overlap;
|
|
advance += ratio * (max_overlap - min_overlap);
|
|
}
|
|
let (x, y) = match axis {
|
|
Axis::X => (Em::from_abs(advance, base.item.size), Em::zero()),
|
|
Axis::Y => (Em::zero(), Em::from_abs(advance, base.item.size)),
|
|
};
|
|
glyphs.push(Glyph {
|
|
id: part.glyph_id.0,
|
|
x_advance: x,
|
|
x_offset: Em::zero(),
|
|
y_advance: y,
|
|
y_offset: Em::zero(),
|
|
..base.item.glyphs[0].clone()
|
|
});
|
|
}
|
|
|
|
match axis {
|
|
Axis::X => base.size.x = full,
|
|
Axis::Y => {
|
|
base.baseline = None;
|
|
base.size.y = full;
|
|
base.size.x = glyphs
|
|
.iter()
|
|
.map(|glyph| base.item.font.x_advance(glyph.id).unwrap_or_default())
|
|
.max()
|
|
.unwrap_or_default()
|
|
.at(base.item.size);
|
|
}
|
|
}
|
|
|
|
base.item.glyphs = glyphs;
|
|
base.italics_correction = base
|
|
.item
|
|
.font
|
|
.to_em(assembly.italics_correction.value)
|
|
.at(base.item.size);
|
|
if axis == Axis::X {
|
|
base.accent_attach = (full / 2.0, full / 2.0);
|
|
}
|
|
base.mid_stretched = None;
|
|
base.extended_shape = true;
|
|
}
|
|
|
|
/// Return an iterator over the assembly's parts with extenders repeated the
|
|
/// specified number of times.
|
|
fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ {
|
|
assembly.parts.into_iter().flat_map(move |part| {
|
|
let count = if part.part_flags.extender() { repeat } else { 1 };
|
|
std::iter::repeat_n(part, count)
|
|
})
|
|
}
|
|
|
|
pub fn has_dtls_feat(font: &Font) -> bool {
|
|
font.ttf()
|
|
.tables()
|
|
.gsub
|
|
.and_then(|gsub| gsub.features.index(ttf_parser::Tag::from_bytes(b"dtls")))
|
|
.is_some()
|
|
}
|
|
|
|
/// Describes in which situation a frame should use limits for attachments.
|
|
#[derive(Debug, Copy, Clone)]
|
|
pub enum Limits {
|
|
/// Always scripts.
|
|
Never,
|
|
/// Display limits only in `display` math.
|
|
Display,
|
|
/// Always limits.
|
|
Always,
|
|
}
|
|
|
|
impl Limits {
|
|
/// The default limit configuration if the given character is the base.
|
|
pub fn for_char(c: char) -> Self {
|
|
match default_math_class(c) {
|
|
Some(MathClass::Large) => {
|
|
if is_integral_char(c) {
|
|
Limits::Never
|
|
} else {
|
|
Limits::Display
|
|
}
|
|
}
|
|
Some(MathClass::Relation) => Limits::Always,
|
|
_ => Limits::Never,
|
|
}
|
|
}
|
|
|
|
/// The default limit configuration for a math class.
|
|
pub fn for_class(class: MathClass) -> Self {
|
|
match class {
|
|
MathClass::Large => Self::Display,
|
|
MathClass::Relation => Self::Always,
|
|
_ => Self::Never,
|
|
}
|
|
}
|
|
|
|
/// Whether limits should be displayed in this context.
|
|
pub fn active(&self, styles: StyleChain) -> bool {
|
|
match self {
|
|
Self::Always => true,
|
|
Self::Display => styles.get(EquationElem::size) == MathSize::Display,
|
|
Self::Never => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Determines if the character is one of a variety of integral signs.
|
|
fn is_integral_char(c: char) -> bool {
|
|
('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c)
|
|
}
|