315 lines
11 KiB
Rust

use std::io::Read;
use base64::Engine;
use ecow::EcoString;
use ttf_parser::GlyphId;
use typst_library::foundations::Bytes;
use typst_library::layout::{Abs, Point, Ratio, Size, Transform};
use typst_library::text::{Font, TextItem};
use typst_library::visualize::{FillRule, Image, Paint, RasterFormat, RelativeTo};
use typst_utils::hash128;
use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder};
impl SVGRenderer {
/// Render a text item. The text is rendered as a group of glyphs. We will
/// try to render the text as SVG first, then bitmap, then outline. If none
/// of them works, we will skip the text.
pub(super) fn render_text(&mut self, state: State, text: &TextItem) {
let scale: f64 = text.size.to_pt() / text.font.units_per_em();
self.xml.start_element("g");
self.xml.write_attribute("class", "typst-text");
self.xml.write_attribute("transform", "scale(1, -1)");
let mut x: f64 = 0.0;
for glyph in &text.glyphs {
let id = GlyphId(glyph.id);
let offset = x + glyph.x_offset.at(text.size).to_pt();
self.render_svg_glyph(text, id, offset, scale)
.or_else(|| self.render_bitmap_glyph(text, id, offset))
.or_else(|| {
self.render_outline_glyph(
state
.pre_concat(Transform::scale(Ratio::one(), -Ratio::one()))
.pre_translate(Point::new(Abs::pt(offset), Abs::zero())),
text,
id,
offset,
scale,
)
});
x += glyph.x_advance.at(text.size).to_pt();
}
self.xml.end_element();
}
/// Render a glyph defined by an SVG.
fn render_svg_glyph(
&mut self,
text: &TextItem,
id: GlyphId,
x_offset: f64,
scale: f64,
) -> Option<()> {
let data_url = convert_svg_glyph_to_base64_url(&text.font, id)?;
let upem = text.font.units_per_em();
let origin_ascender = text.font.metrics().ascender.at(Abs::pt(upem));
let glyph_hash = hash128(&(&text.font, id));
let id = self.glyphs.insert_with(glyph_hash, || RenderedGlyph::Image {
url: data_url,
width: upem,
height: upem,
ts: Transform::translate(Abs::zero(), -origin_ascender)
.post_concat(Transform::scale(Ratio::new(scale), Ratio::new(-scale))),
});
self.xml.start_element("use");
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
self.xml.write_attribute("x", &x_offset);
self.xml.end_element();
Some(())
}
/// Render a glyph defined by a bitmap.
fn render_bitmap_glyph(
&mut self,
text: &TextItem,
id: GlyphId,
x_offset: f64,
) -> Option<()> {
let (image, bitmap_x_offset, bitmap_y_offset) =
convert_bitmap_glyph_to_image(&text.font, id)?;
let glyph_hash = hash128(&(&text.font, id));
let id = self.glyphs.insert_with(glyph_hash, || {
let width = image.width();
let height = image.height();
let url = crate::image::convert_image_to_base64_url(&image);
let ts = Transform::translate(
Abs::pt(bitmap_x_offset),
Abs::pt(-height - bitmap_y_offset),
);
RenderedGlyph::Image { url, width, height, ts }
});
let target_height = text.size.to_pt();
self.xml.start_element("use");
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
// The image is stored with the height of `image.height()`, but we want
// to render it with a height of `target_height`. So we need to scale
// it.
let scale_factor = target_height / image.height();
self.xml.write_attribute("x", &(x_offset / scale_factor));
self.xml.write_attribute_fmt(
"transform",
format_args!("scale({scale_factor} -{scale_factor})",),
);
self.xml.end_element();
Some(())
}
/// Render a glyph defined by an outline.
fn render_outline_glyph(
&mut self,
state: State,
text: &TextItem,
glyph_id: GlyphId,
x_offset: f64,
scale: f64,
) -> Option<()> {
let scale = Ratio::new(scale);
let path = convert_outline_glyph_to_path(&text.font, glyph_id, scale)?;
let hash = hash128(&(&text.font, glyph_id, scale));
let id = self.glyphs.insert_with(hash, || RenderedGlyph::Path(path));
let glyph_size = text.font.ttf().glyph_bounding_box(glyph_id)?;
let width = glyph_size.width() as f64 * scale.get();
let height = glyph_size.height() as f64 * scale.get();
self.xml.start_element("use");
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
self.xml.write_attribute_fmt("x", format_args!("{x_offset}"));
self.write_fill(
&text.fill,
FillRule::default(),
Size::new(Abs::pt(width), Abs::pt(height)),
self.text_paint_transform(state, &text.fill),
);
if let Some(stroke) = &text.stroke {
self.write_stroke(
stroke,
Size::new(Abs::pt(width), Abs::pt(height)),
self.text_paint_transform(state, &stroke.paint),
);
}
self.xml.end_element();
Some(())
}
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(),
},
}
}
/// Build the glyph definitions.
pub(super) fn write_glyph_defs(&mut self) {
if self.glyphs.is_empty() {
return;
}
self.xml.start_element("defs");
self.xml.write_attribute("id", "glyph");
for (id, glyph) in self.glyphs.iter() {
self.xml.start_element("symbol");
self.xml.write_attribute("id", &id);
self.xml.write_attribute("overflow", "visible");
match glyph {
RenderedGlyph::Path(path) => {
self.xml.start_element("path");
self.xml.write_attribute("d", &path);
self.xml.end_element();
}
RenderedGlyph::Image { url, width, height, ts } => {
self.xml.start_element("image");
self.xml.write_attribute("xlink:href", &url);
self.xml.write_attribute("width", &width);
self.xml.write_attribute("height", &height);
if !ts.is_identity() {
self.xml.write_attribute("transform", &SvgMatrix(*ts));
}
self.xml.write_attribute("preserveAspectRatio", "none");
self.xml.end_element();
}
}
self.xml.end_element();
}
self.xml.end_element();
}
}
/// Represents a glyph to be rendered.
pub enum RenderedGlyph {
/// A path is a sequence of drawing commands.
///
/// It is in the format of `M x y L x y C x1 y1 x2 y2 x y Z`.
Path(EcoString),
/// An image is a URL to an image file, plus the size and transform.
///
/// The url is in the format of `data:image/{format};base64,`.
Image { url: EcoString, width: f64, height: f64, ts: Transform },
}
/// Convert an outline glyph to an SVG path.
#[comemo::memoize]
fn convert_outline_glyph_to_path(
font: &Font,
id: GlyphId,
scale: Ratio,
) -> Option<EcoString> {
let mut builder = SvgPathBuilder::with_scale(scale);
font.ttf().outline_glyph(id, &mut builder)?;
Some(builder.0)
}
/// Convert a bitmap glyph to an encoded image URL.
#[comemo::memoize]
fn convert_bitmap_glyph_to_image(font: &Font, id: GlyphId) -> Option<(Image, f64, f64)> {
let raster = font.ttf().glyph_raster_image(id, u16::MAX)?;
if raster.format != ttf_parser::RasterImageFormat::PNG {
return None;
}
let image =
Image::new(Bytes::new(raster.data.to_vec()), RasterFormat::Png.into(), None)
.ok()?;
Some((image, raster.x as f64, raster.y as f64))
}
/// Convert an SVG glyph to an encoded image URL.
#[comemo::memoize]
fn convert_svg_glyph_to_base64_url(font: &Font, id: GlyphId) -> Option<EcoString> {
let mut data = font.ttf().glyph_svg_image(id)?.data;
// Decompress SVGZ.
let mut decoded = vec![];
if data.starts_with(&[0x1f, 0x8b]) {
let mut decoder = flate2::read::GzDecoder::new(data);
decoder.read_to_end(&mut decoded).ok()?;
data = &decoded;
}
let upem = font.units_per_em();
let width = upem;
let height = upem;
let origin_ascender = font.metrics().ascender.at(Abs::pt(upem));
// Parse XML.
let mut svg_str = std::str::from_utf8(data).ok()?.to_owned();
let mut start_span = None;
let mut last_viewbox = None;
// Parse xml and find the viewBox of the svg element.
// <svg viewBox="0 0 1000 1000">...</svg>
// ~~~~~^~~~~~~
for n in xmlparser::Tokenizer::from(svg_str.as_str()) {
let tok = n.unwrap();
match tok {
xmlparser::Token::ElementStart { span, local, .. } => {
if local.as_str() == "svg" {
start_span = Some(span);
break;
}
}
xmlparser::Token::Attribute { span, local, value, .. } => {
if local.as_str() == "viewBox" {
last_viewbox = Some((span, value));
}
}
xmlparser::Token::ElementEnd { .. } => break,
_ => {}
}
}
if last_viewbox.is_none() {
// Correct the viewbox if it is not present. `-origin_ascender` is to
// make sure the glyph is rendered at the correct position
svg_str.insert_str(
start_span.unwrap().range().end,
format!(r#" viewBox="0 {} {width} {height}""#, -origin_ascender.to_pt())
.as_str(),
);
}
let mut url: EcoString = "data:image/svg+xml;base64,".into();
let b64_encoded =
base64::engine::general_purpose::STANDARD.encode(svg_str.as_bytes());
url.push_str(&b64_encoded);
Some(url)
}