typst/library/src/math/tex.rs

152 lines
4.3 KiB
Rust

use rex::error::{Error, LayoutError};
use rex::font::FontContext;
use rex::layout::{LayoutSettings, Style};
use rex::parser::color::RGBA;
use rex::render::{Backend, Cursor, Renderer};
use typst::font::Font;
use crate::prelude::*;
use crate::text::{families, variant, TextNode};
/// Layout a TeX formula into a frame.
pub fn layout_tex(
vt: &Vt,
tex: &str,
display: bool,
styles: StyleChain,
) -> Result<Fragment, &'static str> {
// Load the font.
let variant = variant(styles);
let world = vt.world();
let mut font = None;
for family in families(styles) {
font = world.book().select(family, variant).and_then(|id| world.font(id));
if font.as_ref().map_or(false, |font| font.math().is_some()) {
break;
}
}
// Prepare the font context.
let Some(font) = font else { return Err("failed to find suitable math font") };
let Some(ctx) = font
.math()
.map(|math| FontContext::new(font.ttf(), math))
else {
return Err("failed to create math font context");
};
// Layout the formula.
let em = styles.get(TextNode::SIZE);
let style = if display { Style::Display } else { Style::Text };
let settings = LayoutSettings::new(&ctx, em.to_pt(), style);
let renderer = Renderer::new();
let Ok(layout) = renderer
.layout(&tex, settings)
.map_err(|err| match err {
Error::Parse(err) => err.to_string(),
Error::Layout(LayoutError::Font(err)) => err.to_string(),
})
else {
return Err("failed to layout math");
};
// Determine the metrics.
let (x0, y0, x1, y1) = renderer.size(&layout);
let width = Abs::pt(x1 - x0);
let mut top = Abs::pt(y1);
let mut bottom = Abs::pt(-y0);
if style != Style::Display {
let metrics = font.metrics();
top = styles.get(TextNode::TOP_EDGE).resolve(styles, metrics);
bottom = -styles.get(TextNode::BOTTOM_EDGE).resolve(styles, metrics);
};
// Prepare a frame rendering backend.
let size = Size::new(width, top + bottom);
let mut backend = FrameBackend {
frame: {
let mut frame = Frame::new(size);
frame.set_baseline(top);
frame
},
baseline: top,
font: font.clone(),
paint: styles.get(TextNode::FILL),
lang: styles.get(TextNode::LANG),
colors: vec![],
};
// Render into the frame.
renderer.render(&layout, &mut backend);
Ok(Fragment::frame(backend.frame))
}
/// A ReX rendering backend that renders into a frame.
struct FrameBackend {
frame: Frame,
baseline: Abs,
font: Font,
paint: Paint,
lang: Lang,
colors: Vec<RGBA>,
}
impl FrameBackend {
/// The currently active paint.
fn paint(&self) -> Paint {
self.colors
.last()
.map(|&RGBA(r, g, b, a)| RgbaColor::new(r, g, b, a).into())
.unwrap_or(self.paint)
}
/// Convert a cursor to a point.
fn transform(&self, cursor: Cursor) -> Point {
Point::new(Abs::pt(cursor.x), self.baseline + Abs::pt(cursor.y))
}
}
impl Backend for FrameBackend {
fn symbol(&mut self, pos: Cursor, gid: u16, scale: f64) {
self.frame.push(
self.transform(pos),
Element::Text(Text {
font: self.font.clone(),
size: Abs::pt(scale),
fill: self.paint(),
lang: self.lang,
glyphs: vec![Glyph {
id: gid,
x_advance: Em::new(0.0),
x_offset: Em::new(0.0),
c: ' ',
}],
}),
);
}
fn rule(&mut self, pos: Cursor, width: f64, height: f64) {
if height == 0.0 {
return;
}
self.frame.push(
self.transform(pos) + Point::with_y(Abs::pt(height) / 2.0),
Element::Shape(Shape {
geometry: Geometry::Line(Point::new(Abs::pt(width), Abs::zero())),
fill: None,
stroke: Some(Stroke { paint: self.paint(), thickness: Abs::pt(height) }),
}),
);
}
fn begin_color(&mut self, color: RGBA) {
self.colors.push(color);
}
fn end_color(&mut self) {
self.colors.pop();
}
}