use std::fmt::{self, Debug, Formatter};
use comemo::Tracked;
use ecow::EcoString;
use ttf_parser::GlyphId;
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
use typst_library::World;
use typst_library::diag::warning;
use typst_library::engine::Engine;
use typst_library::foundations::StyleChain;
use typst_library::layout::{
Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
};
use typst_library::math::ir::{MathProperties, Stretch};
use typst_library::math::{EquationElem, MathSize};
use typst_library::text::{FontInstance, Glyph, TextElem, TextItem, features};
use typst_syntax::Span;
use typst_utils::{Get, default_math_class};
use unicode_math_class::MathClass;
use crate::math::shaping;
use crate::modifiers::{FrameModifiers, FrameModify};
const MAX_REPEATS: usize = 1024;
#[derive(Clone)]
pub struct GlyphFragment {
pub(super) item: TextItem,
pub(super) size: Size,
baseline: Option<Abs>,
pub(super) italics_correction: Abs,
pub(super) accent_attach: (Abs, Abs),
pub(super) math_size: MathSize,
pub class: MathClass,
pub(super) extended_shape: bool,
modifiers: FrameModifiers,
shift: Abs,
align: Abs,
}
impl GlyphFragment {
pub fn synthetic(
engine: &mut Engine,
styles: StyleChain,
c: char,
span: Span,
) -> Option<Self> {
let class = default_math_class(c).unwrap_or(MathClass::Normal);
let math_size = styles.get(EquationElem::size);
Self::base(
engine.world,
styles,
&features(styles),
c.encode_utf8(&mut [0; 4]),
class,
math_size,
)
.map(|glyph| glyph.with_span(span))
}
pub fn new(
engine: &mut Engine,
text: &str,
stretch: &Stretch,
styles: StyleChain,
props: &MathProperties,
) -> Option<GlyphFragment> {
let PlannedGlyph { mut glyph, action } = Self::planned(
engine.world,
styles,
text,
props.class(),
props.size,
*stretch,
)?
.with_span(props.span);
match action {
Action::Stretch { axis, target, short_fall } => {
glyph.stretch(engine, target, short_fall, axis);
if axis == Axis::Y {
glyph.center_on_axis();
}
}
Action::WarnBothAxes => {
engine.sink.warn(warning!(
props.span,
"glyph has both vertical and horizontal constructions";
hint: "this is probably a font bug";
hint: "please file an issue at https://github.com/typst/typst/issues";
));
}
Action::Keep | Action::Fallback => {}
}
Some(glyph)
}
#[comemo::memoize]
fn planned(
world: Tracked<dyn World + '_>,
styles: StyleChain,
text: &str,
class: MathClass,
math_size: MathSize,
stretch: Stretch,
) -> Option<PlannedGlyph> {
let features = features(styles);
let shape = |feats: &[rustybuzz::Feature]| {
Self::base(world, styles, feats, text, class, math_size)
};
let mut glyph = shape(&features)?;
let mut action = decide(&glyph, &stretch);
if matches!(action, Action::Fallback) {
shaping::feat_fallback(features, |feats| {
let Some(new) = shape(feats) else { return false };
match decide(&new, &stretch) {
Action::Fallback => false,
other => {
glyph = new;
action = other;
true
}
}
});
}
Some(PlannedGlyph { glyph, action })
}
#[comemo::memoize]
fn base(
world: Tracked<dyn World + '_>,
styles: StyleChain,
features: &[rustybuzz::Feature],
text: &str,
class: MathClass,
math_size: MathSize,
) -> Option<GlyphFragment> {
let shaped = shaping::shape(world, styles, features, text)?;
Some(Self::from_shaped(styles, text.into(), class, math_size, shaped))
}
fn from_shaped(
styles: StyleChain,
text: EcoString,
class: MathClass,
math_size: MathSize,
shaped: (FontInstance, Vec<Glyph>),
) -> GlyphFragment {
let (font, glyphs) = shaped;
let item = TextItem {
text,
font,
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),
glyphs,
};
let mut fragment = Self {
item,
math_size,
class,
extended_shape: false,
italics_correction: Abs::zero(),
accent_attach: (Abs::zero(), Abs::zero()),
size: Size::zero(),
baseline: None,
align: Abs::zero(),
shift: styles.resolve(TextElem::baseline),
modifiers: FrameModifiers::get_in(styles),
};
fragment.update_glyph();
fragment
}
fn with_span(mut self, span: Span) -> Self {
for glyph in &mut self.item.glyphs {
glyph.span = (span, 0);
}
self
}
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()));
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;
}
fn baseline(&self) -> Abs {
self.ascent()
}
pub fn ascent(&self) -> Abs {
self.baseline.unwrap_or(self.size.y)
}
pub fn descent(&self) -> Abs {
self.size.y - self.ascent()
}
pub(super) 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
}
fn stretch(&mut self, engine: &mut Engine, target: Abs, short_fall: Abs, axis: Axis) {
let advance = self.stretch_advance(axis);
let short_target = target - short_fall;
if short_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 };
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 short_target <= best_advance {
break;
}
}
if short_target <= best_advance || construction.assembly.is_none() {
self.item.glyphs = vec![Glyph {
id: best_id.0,
x_advance: self.item.font.x_advance(best_id.0).unwrap_or_default(),
x_offset: Em::zero(),
y_advance: self.item.font.y_advance(best_id.0).unwrap_or_default(),
y_offset: Em::zero(),
range: self.item.glyphs[0].range.clone(),
span: self.item.glyphs[0].span,
}];
self.update_glyph();
return;
}
let assembly = construction.assembly.unwrap();
let min_overlap = min_connector_overlap(&self.item.font)
.unwrap_or_default()
.at(self.item.size);
assemble(engine, self, assembly, min_overlap, target, axis);
}
fn stretch_advance(&self, axis: Axis) -> Abs {
let mut advance = self.size.get(axis);
if axis == Axis::X && !self.extended_shape {
advance -= self.italics_correction;
}
advance
}
pub fn center_on_axis(&mut self) {
self.align_on_axis(VAlignment::Horizon);
}
fn align_on_axis(&mut self, align: VAlignment) {
let h = self.size.y;
let axis = self.item.font.math().axis_height.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(Clone)]
struct PlannedGlyph {
glyph: GlyphFragment,
action: Action,
}
impl PlannedGlyph {
fn with_span(mut self, span: Span) -> Self {
self.glyph = self.glyph.with_span(span);
self
}
}
#[derive(Clone, Copy)]
enum Action {
Keep,
Stretch { axis: Axis, target: Abs, short_fall: Abs },
WarnBothAxes,
Fallback,
}
fn decide(glyph: &GlyphFragment, stretch: &Stretch) -> Action {
enum AxisStatus {
Sufficient,
Stretchable { target: Abs, short_fall: Abs },
Fallback,
}
let font = &glyph.item.font;
let id = glyph.item.glyphs[0].id;
let axes = stretch_axes(font, id);
let assess = |axis| {
let Some((target, short_fall)) = resolve_stretch(glyph, stretch, axis) else {
return AxisStatus::Sufficient;
};
if axes.get(axis) {
return AxisStatus::Stretchable { target, short_fall };
}
let mut advance = glyph.stretch_advance(axis);
if let Some(bbox) = font.ttf().glyph_bounding_box(GlyphId(id)) {
let extents = Axes::new(
font.to_em(bbox.x_max - bbox.x_min),
font.to_em(bbox.y_max - bbox.y_min),
);
advance.set_max(extents.get(axis).at(glyph.item.size));
}
if target - short_fall <= advance {
AxisStatus::Sufficient
} else {
AxisStatus::Fallback
}
};
match (assess(Axis::X), assess(Axis::Y)) {
(AxisStatus::Stretchable { .. }, AxisStatus::Stretchable { .. }) => {
Action::WarnBothAxes
}
(AxisStatus::Stretchable { target, short_fall }, _) => {
Action::Stretch { axis: Axis::X, target, short_fall }
}
(_, AxisStatus::Stretchable { target, short_fall }) => {
Action::Stretch { axis: Axis::Y, target, short_fall }
}
(AxisStatus::Sufficient, AxisStatus::Sufficient) => Action::Keep,
_ => Action::Fallback,
}
}
fn resolve_stretch(
glyph: &GlyphFragment,
stretch: &Stretch,
axis: Axis,
) -> Option<(Abs, Abs)> {
let stretch = stretch.resolve(axis)?;
let relative_to_size = stretch.relative_to.unwrap_or_else(|| {
if axis == Axis::Y
&& glyph.class == MathClass::Large
&& glyph.math_size == MathSize::Display
{
glyph.item.font.math().display_operator_min_height.at(glyph.item.size)
} else {
glyph.size.get(axis)
}
});
let target = stretch.target.relative_to(relative_to_size);
let short_fall = stretch.short_fall.at(stretch.font_size.unwrap_or(glyph.item.size));
Some((target, short_fall))
}
fn ascent_descent(font: &FontInstance, 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)))
}
fn italics_correction(font: &FontInstance, id: GlyphId) -> Option<Em> {
font.ttf()
.tables()
.math?
.glyph_info?
.italic_corrections?
.get(id)
.map(|value| font.to_em(value.value))
}
fn accent_attach(font: &FontInstance, id: GlyphId) -> Option<Em> {
font.ttf()
.tables()
.math?
.glyph_info?
.top_accent_attachments?
.get(id)
.map(|value| font.to_em(value.value))
}
fn is_extended_shape(font: &FontInstance, 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()
}
pub(super) fn kern_at_height(
font: &FontInstance,
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 stretch_axes(font: &FontInstance, 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: &FontInstance) -> Option<Em> {
font.ttf()
.tables()
.math?
.variants
.map(|variants| font.to_em(variants.min_connector_overlap))
}
fn glyph_construction(
font: &FontInstance,
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)
}
fn assemble(
engine: &mut Engine,
base: &mut GlyphFragment,
assembly: GlyphAssembly,
min_overlap: Abs,
target: Abs,
axis: Axis,
) {
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 {
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).max(Abs::zero());
}
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_advance, y_advance, y_offset) = match axis {
Axis::X => (Em::from_abs(advance, base.item.size), Em::zero(), Em::zero()),
Axis::Y => (
Em::zero(),
Em::from_abs(advance, base.item.size),
ascent_descent(&base.item.font, part.glyph_id)
.map(|x| x.1)
.unwrap_or_default(),
),
};
glyphs.push(Glyph {
id: part.glyph_id.0,
x_advance,
x_offset: Em::zero(),
y_advance,
y_offset,
range: base.item.glyphs[0].range.clone(),
span: base.item.glyphs[0].span,
});
}
match axis {
Axis::X => {
base.size.x = full;
let (ascent, descent) = glyphs
.iter()
.filter_map(|glyph| ascent_descent(&base.item.font, GlyphId(glyph.id)))
.reduce(|(ma, md), (a, d)| (ma.max(a), md.max(d)))
.unwrap_or((Em::zero(), Em::zero()));
base.baseline = Some(ascent.at(base.item.size));
base.size.y = (ascent + descent).at(base.item.size);
}
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.extended_shape = true;
}
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)
})
}