use ecow::EcoString;
use ttf_parser::GlyphId;
use typst_library::layout::{Abs, Ratio, Size, Transform};
use typst_library::text::TextItem;
use typst_library::text::color::{
GlyphFrame, GlyphFrameItem, glyph_frame, should_outline,
};
use typst_library::visualize::{FillRule, Paint, RelativeTo};
use crate::path::SvgPathBuilder;
use crate::write::{SvgElem, SvgIdRef, SvgTransform};
use crate::{DedupId, SVGRenderer, State};
#[derive(Clone)]
pub enum RenderedGlyph {
Frame(GlyphFrame),
Path(EcoString),
}
impl SVGRenderer<'_> {
pub(super) fn render_text(
&mut self,
svg: &mut SvgElem,
state: &State,
text: &TextItem,
) {
let svg = &mut svg.elem("g");
let state = state.pre_concat(Transform::scale(Ratio::one(), -Ratio::one()));
svg.attr("transform", SvgTransform(state.transform));
let mut x = Abs::pt(0.0);
let mut y = Abs::pt(0.0);
for glyph in &text.glyphs {
let id = GlyphId(glyph.id);
let x_offset = x + glyph.x_offset.at(text.size);
let y_offset = y + glyph.y_offset.at(text.size);
self.render_glyph(svg, &state, text, id, x_offset, y_offset);
x += glyph.x_advance.at(text.size);
y += glyph.y_advance.at(text.size);
}
}
fn render_glyph(
&mut self,
svg: &mut SvgElem,
state: &State,
text: &TextItem,
glyph_id: GlyphId,
x_offset: Abs,
y_offset: Abs,
) {
if should_outline(&text.font, glyph_id) {
let scale = Ratio::new(text.size.to_pt() / text.font.units_per_em());
let key = (&text.font, glyph_id, scale);
let (id, path) = self.glyphs.insert_with_val(key, || {
let mut builder = SvgPathBuilder::with_scale(scale);
text.font.ttf().outline_glyph(glyph_id, &mut builder)?;
Some(RenderedGlyph::Path(builder.finsish()))
});
if path.is_some() {
self.render_path_glyph(svg, state, text, glyph_id, x_offset, y_offset, id)
}
} else {
let key = (&text.font, glyph_id);
let (id, frame) = self.glyphs.insert_with_val(key, || {
let frame = glyph_frame(&text.font, glyph_id.0)?;
Some(RenderedGlyph::Frame(frame))
});
if frame.is_some() {
self.render_image_glyph(svg, x_offset, y_offset, text, id);
}
}
}
fn render_image_glyph(
&mut self,
svg: &mut SvgElem,
x_offset: Abs,
y_offset: Abs,
text: &TextItem,
id: DedupId,
) {
let scale = Ratio::new(text.size.to_pt() / text.font.units_per_em());
let ts = Transform::translate(x_offset, y_offset)
.pre_concat(Transform::scale(scale, -scale));
svg.elem("use")
.attr("xlink:href", SvgIdRef(id))
.attr("transform", SvgTransform(ts));
}
#[allow(clippy::too_many_arguments)]
fn render_path_glyph(
&mut self,
svg: &mut SvgElem,
state: &State,
text: &TextItem,
glyph_id: GlyphId,
x_offset: Abs,
y_offset: Abs,
id: DedupId,
) {
let state = state.pre_concat(Transform::translate(x_offset, y_offset));
let Some(glyph_size) = text.font.ttf().glyph_bounding_box(glyph_id) else {
return;
};
let aspect_ratio = Size::new(
Abs::pt(glyph_size.width() as f64),
Abs::pt(glyph_size.height() as f64),
)
.aspect_ratio();
let mut use_ = svg.elem("use");
use_.attr("xlink:href", SvgIdRef(id))
.attr("x", x_offset.to_pt())
.attr("y", y_offset.to_pt());
self.write_fill(
&mut use_,
&text.fill,
FillRule::default(),
aspect_ratio,
self.text_paint_transform(&state, &text.fill),
);
if let Some(stroke) = &text.stroke {
self.write_stroke(
&mut use_,
stroke,
aspect_ratio,
self.text_paint_transform(&state, &stroke.paint),
);
}
}
fn text_paint_transform(&self, state: &State, paint: &Paint) -> Transform {
match paint {
Paint::Solid(_) => Transform::identity(),
Paint::Gradient(gradient) => match gradient.unwrap_relative(true) {
RelativeTo::Self_ => Transform::identity(),
RelativeTo::Parent => Transform::scale(
Ratio::new(state.size.x.to_pt()),
Ratio::new(state.size.y.to_pt()),
)
.post_concat(state.transform.invert().unwrap()),
},
Paint::Tiling(tiling) => match tiling.unwrap_relative(true) {
RelativeTo::Self_ => Transform::identity(),
RelativeTo::Parent => state.transform.invert().unwrap(),
},
}
}
pub(super) fn write_glyph_defs(&mut self, svg: &mut SvgElem) {
if self.glyphs.iter().all(|(_, g)| g.is_none()) {
return;
}
let mut defs = svg.elem("defs");
let glyphs = std::mem::take(&mut self.glyphs);
for (id, glyph) in glyphs.iter() {
let Some(glyph) = glyph else { continue };
let mut symbol = defs.elem("symbol");
symbol.attr("id", id);
symbol.attr("overflow", "visible");
match glyph {
RenderedGlyph::Frame(frame) => {
let state = State::new(frame.size()).pre_translate(frame.item.pos());
match &frame.item {
GlyphFrameItem::Tofu(_, shape) => {
self.render_shape(&mut symbol, &state, shape);
}
GlyphFrameItem::Image(_, image, size) => {
self.render_image(&mut symbol, &state, image, size);
}
}
}
RenderedGlyph::Path(path) => {
symbol.elem("path").attr("d", path);
}
}
}
assert!(self.glyphs.is_empty());
}
}