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 typst_library::foundations::StyleChain; use typst_library::introspection::Tag; use typst_library::layout::{ Abs, 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::Paint; use typst_syntax::Span; use typst_utils::default_math_class; use unicode_math_class::MathClass; use super::{stretch_glyph, MathContext, Scaled}; use crate::modifiers::{FrameModifiers, FrameModify}; #[derive(Debug, Clone)] pub enum MathFragment { Glyph(GlyphFragment), Variant(VariantFragment), Frame(FrameFragment), Spacing(Abs, bool), Space(Abs), Linebreak, Align, Tag(Tag), } impl MathFragment { pub fn size(&self) -> Size { Size::new(self.width(), self.height()) } pub fn width(&self) -> Abs { match self { Self::Glyph(glyph) => glyph.width, Self::Variant(variant) => variant.frame.width(), 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.height(), Self::Variant(variant) => variant.frame.height(), Self::Frame(fragment) => fragment.frame.height(), _ => Abs::zero(), } } pub fn ascent(&self) -> Abs { match self { Self::Glyph(glyph) => glyph.ascent, Self::Variant(variant) => variant.frame.ascent(), Self::Frame(fragment) => fragment.frame.baseline(), _ => Abs::zero(), } } pub fn descent(&self) -> Abs { match self { Self::Glyph(glyph) => glyph.descent, Self::Variant(variant) => variant.frame.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::Variant(variant) => variant.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 { match self { Self::Glyph(glyph) => Some(glyph.math_size), Self::Variant(variant) => Some(variant.math_size), Self::Frame(fragment) => Some(fragment.math_size), _ => None, } } pub fn font_size(&self) -> Option { match self { Self::Glyph(glyph) => Some(glyph.font_size), Self::Variant(variant) => Some(variant.font_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::Variant(variant) => variant.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::Variant(variant) => variant.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, Self::Variant(variant) => !variant.extended_shape, MathFragment::Frame(frame) => frame.text_like, _ => false, } } 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(), } } pub fn accent_attach(&self) -> (Abs, Abs) { match self { Self::Glyph(glyph) => glyph.accent_attach, Self::Variant(variant) => variant.accent_attach, Self::Frame(fragment) => fragment.accent_attach, _ => (self.width() / 2.0, self.width() / 2.0), } } 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()); 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::Variant(variant) => variant.limits, MathFragment::Frame(fragment) => fragment.limits, _ => Limits::Never, } } /// If no kern table is provided for a corner, a kerning amount of zero is /// assumed. pub fn kern_at_height(&self, ctx: &MathContext, corner: Corner, height: Abs) -> Abs { match self { Self::Glyph(glyph) => { kern_at_height(ctx, glyph.font_size, glyph.id, corner, height) .unwrap_or_default() } _ => Abs::zero(), } } } impl From for MathFragment { fn from(glyph: GlyphFragment) -> Self { Self::Glyph(glyph) } } impl From for MathFragment { fn from(variant: VariantFragment) -> Self { Self::Variant(variant) } } impl From for MathFragment { fn from(fragment: FrameFragment) -> Self { Self::Frame(fragment) } } #[derive(Clone)] pub struct GlyphFragment { pub id: GlyphId, pub c: char, pub font: Font, pub lang: Lang, pub region: Option, pub fill: Paint, pub shift: Abs, pub width: Abs, pub ascent: Abs, pub descent: Abs, pub italics_correction: Abs, pub accent_attach: (Abs, Abs), pub font_size: Abs, pub class: MathClass, pub math_size: MathSize, pub span: Span, pub modifiers: FrameModifiers, pub limits: Limits, pub extended_shape: bool, } impl GlyphFragment { pub fn new(ctx: &MathContext, styles: StyleChain, c: char, span: Span) -> Self { let id = ctx.ttf.glyph_index(c).unwrap_or_default(); let id = Self::adjust_glyph_index(ctx, id); Self::with_id(ctx, styles, c, id, span) } pub fn try_new( ctx: &MathContext, styles: StyleChain, c: char, span: Span, ) -> Option { let id = ctx.ttf.glyph_index(c)?; let id = Self::adjust_glyph_index(ctx, id); Some(Self::with_id(ctx, styles, c, id, span)) } pub fn with_id( ctx: &MathContext, styles: StyleChain, c: char, id: GlyphId, span: Span, ) -> Self { 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), fill: TextElem::fill_in(styles).as_decoration(), shift: TextElem::baseline_in(styles), font_size: TextElem::size_in(styles), math_size: EquationElem::size_in(styles), width: Abs::zero(), ascent: Abs::zero(), descent: Abs::zero(), limits: Limits::for_char(c), italics_correction: Abs::zero(), accent_attach: (Abs::zero(), Abs::zero()), class, span, 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 } } /// 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, }); let mut width = advance.scaled(ctx, self.font_size); // The fallback for accents is half the width plus or minus the italics // correction. This is similar to how top and bottom attachments are // shifted. For bottom accents we do not use the accent attach of the // base as it is meant for top acccents. let top_accent_attach = accent_attach(ctx, id, self.font_size).unwrap_or((width + italics) / 2.0); let bottom_accent_attach = (width - italics) / 2.0; let extended_shape = is_extended_shape(ctx, id); if !extended_shape { width += italics; } self.id = id; self.width = width; self.ascent = bbox.y_max.scaled(ctx, self.font_size); self.descent = -bbox.y_min.scaled(ctx, self.font_size); self.italics_correction = italics; self.accent_attach = (top_accent_attach, bottom_accent_attach); self.extended_shape = extended_shape; } pub fn height(&self) -> Abs { self.ascent + self.descent } pub fn into_variant(self) -> VariantFragment { VariantFragment { c: self.c, font_size: self.font_size, italics_correction: self.italics_correction, accent_attach: self.accent_attach, class: self.class, math_size: self.math_size, span: self.span, limits: self.limits, extended_shape: self.extended_shape, frame: self.into_frame(), mid_stretched: None, } } pub fn into_frame(self) -> Frame { let item = TextItem { font: self.font.clone(), size: self.font_size, fill: self.fill, lang: self.lang, region: self.region, text: self.c.into(), stroke: None, glyphs: vec![Glyph { id: self.id.0, x_advance: Em::from_length(self.width, self.font_size), x_offset: Em::zero(), range: 0..self.c.len_utf8() as u16, span: (self.span, 0), }], }; let size = Size::new(self.width, self.ascent + self.descent); let mut frame = Frame::soft(size); frame.set_baseline(self.ascent); frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item)); frame.modify(&self.modifiers); frame } pub fn 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, ctx: &mut MathContext, height: Abs, short_fall: Abs, ) -> VariantFragment { stretch_glyph(ctx, self, height, short_fall, Axis::Y) } /// Try to stretch a glyph to a desired width. pub fn stretch_horizontal( self, ctx: &mut MathContext, width: Abs, short_fall: Abs, ) -> VariantFragment { stretch_glyph(ctx, self, width, short_fall, Axis::X) } } 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, 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, 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) } } #[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: TextElem::size_in(styles), class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal), math_size: EquationElem::size_in(styles), 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 } } } /// Look up the italics correction for a glyph. fn italics_correction(ctx: &MathContext, id: GlyphId, font_size: Abs) -> Option { Some( ctx.table .glyph_info? .italic_corrections? .get(id)? .scaled(ctx, font_size), ) } /// Loop up the top accent attachment position for a glyph. fn accent_attach(ctx: &MathContext, id: GlyphId, font_size: Abs) -> Option { Some( ctx.table .glyph_info? .top_accent_attachments? .get(id)? .scaled(ctx, font_size), ) } /// 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)) .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 { let kerns = ctx.table.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 > kern.height(i)?.scaled(ctx, font_size) { i += 1; } Some(kern.kern(i)?.scaled(ctx, font_size)) } /// 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 => EquationElem::size_in(styles) == 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) } /// 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>, feature: Feature) -> Option { 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::(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, ) -> Option { 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) } }