Refactor typst-svg (#4074)

This commit is contained in:
Tulio Martins 2024-05-07 06:55:59 -03:00 committed by GitHub
parent c9e91d4cf1
commit c49c0955be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 1140 additions and 1086 deletions

View File

@ -0,0 +1,40 @@
use base64::Engine;
use ecow::{eco_format, EcoString};
use typst::layout::{Abs, Axes};
use typst::visualize::{Image, ImageFormat, RasterFormat, VectorFormat};
use crate::SVGRenderer;
impl SVGRenderer {
/// Render an image element.
pub(super) fn render_image(&mut self, image: &Image, size: &Axes<Abs>) {
let url = convert_image_to_base64_url(image);
self.xml.start_element("image");
self.xml.write_attribute("xlink:href", &url);
self.xml.write_attribute("width", &size.x.to_pt());
self.xml.write_attribute("height", &size.y.to_pt());
self.xml.write_attribute("preserveAspectRatio", "none");
self.xml.end_element();
}
}
/// Encode an image into a data URL. The format of the URL is
/// `data:image/{format};base64,`.
#[comemo::memoize]
pub fn convert_image_to_base64_url(image: &Image) -> EcoString {
let format = match image.format() {
ImageFormat::Raster(f) => match f {
RasterFormat::Png => "png",
RasterFormat::Jpg => "jpeg",
RasterFormat::Gif => "gif",
},
ImageFormat::Vector(f) => match f {
VectorFormat::Svg => "svg+xml",
},
};
let mut url = eco_format!("data:image/{format};base64,");
let data = base64::engine::general_purpose::STANDARD.encode(image.data());
url.push_str(&data);
url
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,583 @@
use std::f32::consts::TAU;
use ecow::{eco_format, EcoString};
use ttf_parser::OutlineBuilder;
use typst::foundations::Repr;
use typst::layout::{Angle, Axes, Frame, Quadrant, Ratio, Size, Transform};
use typst::util::hash128;
use typst::visualize::{Color, Gradient, Paint, Pattern, RatioOrAngle};
use xmlwriter::XmlWriter;
use crate::{Id, SVGRenderer, State, SvgMatrix, SvgPathBuilder};
/// The number of segments in a conic gradient.
/// This is a heuristic value that seems to work well.
/// Smaller values could be interesting for optimization.
const CONIC_SEGMENT: usize = 360;
impl SVGRenderer {
/// Render a frame to a string.
pub(super) fn render_pattern_frame(
&mut self,
state: State,
ts: Transform,
frame: &Frame,
) -> String {
let mut xml = XmlWriter::new(xmlwriter::Options::default());
std::mem::swap(&mut self.xml, &mut xml);
self.render_frame(state, ts, frame);
std::mem::swap(&mut self.xml, &mut xml);
xml.end_document()
}
/// Write a fill attribute.
pub(super) fn write_fill(&mut self, fill: &Paint, size: Size, ts: Transform) {
match fill {
Paint::Solid(color) => self.xml.write_attribute("fill", &color.encode()),
Paint::Gradient(gradient) => {
let id = self.push_gradient(gradient, size, ts);
self.xml.write_attribute_fmt("fill", format_args!("url(#{id})"));
}
Paint::Pattern(pattern) => {
let id = self.push_pattern(pattern, size, ts);
self.xml.write_attribute_fmt("fill", format_args!("url(#{id})"));
}
}
}
/// Pushes a gradient to the list of gradients to write SVG file.
///
/// If the gradient is already present, returns the id of the existing
/// gradient. Otherwise, inserts the gradient and returns the id of the
/// inserted gradient. If the transform of the gradient is the identify
/// matrix, the returned ID will be the ID of the "source" gradient,
/// this is a file size optimization.
pub(super) fn push_gradient(
&mut self,
gradient: &Gradient,
size: Size,
ts: Transform,
) -> Id {
let gradient_id = self
.gradients
.insert_with(hash128(&(gradient, size.aspect_ratio())), || {
(gradient.clone(), size.aspect_ratio())
});
if ts.is_identity() {
return gradient_id;
}
self.gradient_refs
.insert_with(hash128(&(gradient_id, ts)), || GradientRef {
id: gradient_id,
kind: gradient.into(),
transform: ts,
})
}
pub(super) fn push_pattern(
&mut self,
pattern: &Pattern,
size: Size,
ts: Transform,
) -> Id {
let pattern_size = pattern.size() + pattern.spacing();
// Unfortunately due to a limitation of `xmlwriter`, we need to
// render the frame twice: once to allocate all of the resources
// that it needs and once to actually render it.
self.render_pattern_frame(
State::new(pattern_size, Transform::identity()),
Transform::identity(),
pattern.frame(),
);
let pattern_id = self.patterns.insert_with(hash128(pattern), || pattern.clone());
self.pattern_refs
.insert_with(hash128(&(pattern_id, ts)), || PatternRef {
id: pattern_id,
transform: ts,
ratio: Axes::new(
Ratio::new(pattern_size.x.to_pt() / size.x.to_pt()),
Ratio::new(pattern_size.y.to_pt() / size.y.to_pt()),
),
})
}
/// Write the raw gradients (without transform) to the SVG file.
pub(super) fn write_gradients(&mut self) {
if self.gradients.is_empty() {
return;
}
self.xml.start_element("defs");
self.xml.write_attribute("id", "gradients");
for (id, (gradient, ratio)) in self.gradients.iter() {
match &gradient {
Gradient::Linear(linear) => {
self.xml.start_element("linearGradient");
self.xml.write_attribute("id", &id);
self.xml.write_attribute("spreadMethod", "pad");
self.xml.write_attribute("gradientUnits", "userSpaceOnUse");
let angle = Gradient::correct_aspect_ratio(linear.angle, *ratio);
let (sin, cos) = (angle.sin(), angle.cos());
let length = sin.abs() + cos.abs();
let (x1, y1, x2, y2) = match angle.quadrant() {
Quadrant::First => (0.0, 0.0, cos * length, sin * length),
Quadrant::Second => (1.0, 0.0, cos * length + 1.0, sin * length),
Quadrant::Third => {
(1.0, 1.0, cos * length + 1.0, sin * length + 1.0)
}
Quadrant::Fourth => (0.0, 1.0, cos * length, sin * length + 1.0),
};
self.xml.write_attribute("x1", &x1);
self.xml.write_attribute("y1", &y1);
self.xml.write_attribute("x2", &x2);
self.xml.write_attribute("y2", &y2);
}
Gradient::Radial(radial) => {
self.xml.start_element("radialGradient");
self.xml.write_attribute("id", &id);
self.xml.write_attribute("spreadMethod", "pad");
self.xml.write_attribute("gradientUnits", "userSpaceOnUse");
self.xml.write_attribute("cx", &radial.center.x.get());
self.xml.write_attribute("cy", &radial.center.y.get());
self.xml.write_attribute("r", &radial.radius.get());
self.xml.write_attribute("fx", &radial.focal_center.x.get());
self.xml.write_attribute("fy", &radial.focal_center.y.get());
self.xml.write_attribute("fr", &radial.focal_radius.get());
}
Gradient::Conic(conic) => {
self.xml.start_element("pattern");
self.xml.write_attribute("id", &id);
self.xml.write_attribute("viewBox", "0 0 1 1");
self.xml.write_attribute("preserveAspectRatio", "none");
self.xml.write_attribute("patternUnits", "userSpaceOnUse");
self.xml.write_attribute("width", "2");
self.xml.write_attribute("height", "2");
self.xml.write_attribute("x", "-0.5");
self.xml.write_attribute("y", "-0.5");
// The rotation angle, negated to match rotation in PNG.
let angle: f32 =
-(Gradient::correct_aspect_ratio(conic.angle, *ratio).to_rad()
as f32)
.rem_euclid(TAU);
let center: (f32, f32) =
(conic.center.x.get() as f32, conic.center.y.get() as f32);
// We build an arg segment for each segment of a circle.
let dtheta = TAU / CONIC_SEGMENT as f32;
for i in 0..CONIC_SEGMENT {
let theta1 = dtheta * i as f32;
let theta2 = dtheta * (i + 1) as f32;
// Create the path for the segment.
let mut builder = SvgPathBuilder::default();
builder.move_to(
correct_pattern_pos(center.0),
correct_pattern_pos(center.1),
);
builder.line_to(
correct_pattern_pos(-2.0 * (theta1 + angle).cos() + center.0),
correct_pattern_pos(2.0 * (theta1 + angle).sin() + center.1),
);
builder.arc(
(2.0, 2.0),
0.0,
0,
1,
(
correct_pattern_pos(
-2.0 * (theta2 + angle).cos() + center.0,
),
correct_pattern_pos(
2.0 * (theta2 + angle).sin() + center.1,
),
),
);
builder.close();
let t1 = (i as f32) / CONIC_SEGMENT as f32;
let t2 = (i + 1) as f32 / CONIC_SEGMENT as f32;
let subgradient = SVGSubGradient {
center: conic.center,
t0: Angle::rad((theta1 + angle) as f64),
t1: Angle::rad((theta2 + angle) as f64),
c0: gradient
.sample(RatioOrAngle::Ratio(Ratio::new(t1 as f64))),
c1: gradient
.sample(RatioOrAngle::Ratio(Ratio::new(t2 as f64))),
};
let id = self
.conic_subgradients
.insert_with(hash128(&subgradient), || subgradient);
// Add the path to the pattern.
self.xml.start_element("path");
self.xml.write_attribute("d", &builder.0);
self.xml.write_attribute_fmt("fill", format_args!("url(#{id})"));
self.xml
.write_attribute_fmt("stroke", format_args!("url(#{id})"));
self.xml.write_attribute("stroke-width", "0");
self.xml.write_attribute("shape-rendering", "optimizeSpeed");
self.xml.end_element();
}
// We skip the default stop generation code.
self.xml.end_element();
continue;
}
}
for window in gradient.stops_ref().windows(2) {
let (start_c, start_t) = window[0];
let (end_c, end_t) = window[1];
self.xml.start_element("stop");
self.xml.write_attribute("offset", &start_t.repr());
self.xml.write_attribute("stop-color", &start_c.to_hex());
self.xml.end_element();
// Generate (256 / len) stops between the two stops.
// This is a workaround for a bug in many readers:
// They tend to just ignore the color space of the gradient.
// The goal is to have smooth gradients but not to balloon the file size
// too much if there are already a lot of stops as in most presets.
let len = if gradient.anti_alias() {
(256 / gradient.stops_ref().len() as u32).max(2)
} else {
2
};
for i in 1..(len - 1) {
let t0 = i as f64 / (len - 1) as f64;
let t = start_t + (end_t - start_t) * t0;
let c = gradient.sample(RatioOrAngle::Ratio(t));
self.xml.start_element("stop");
self.xml.write_attribute("offset", &t.repr());
self.xml.write_attribute("stop-color", &c.to_hex());
self.xml.end_element();
}
self.xml.start_element("stop");
self.xml.write_attribute("offset", &end_t.repr());
self.xml.write_attribute("stop-color", &end_c.to_hex());
self.xml.end_element()
}
self.xml.end_element();
}
self.xml.end_element()
}
/// Write the sub-gradients that are used for conic gradients.
pub(super) fn write_subgradients(&mut self) {
if self.conic_subgradients.is_empty() {
return;
}
self.xml.start_element("defs");
self.xml.write_attribute("id", "subgradients");
for (id, gradient) in self.conic_subgradients.iter() {
let x1 = 2.0 - gradient.t0.cos() as f32 + gradient.center.x.get() as f32;
let y1 = gradient.t0.sin() as f32 + gradient.center.y.get() as f32;
let x2 = 2.0 - gradient.t1.cos() as f32 + gradient.center.x.get() as f32;
let y2 = gradient.t1.sin() as f32 + gradient.center.y.get() as f32;
self.xml.start_element("linearGradient");
self.xml.write_attribute("id", &id);
self.xml.write_attribute("gradientUnits", "objectBoundingBox");
self.xml.write_attribute("x1", &x1);
self.xml.write_attribute("y1", &y1);
self.xml.write_attribute("x2", &x2);
self.xml.write_attribute("y2", &y2);
self.xml.start_element("stop");
self.xml.write_attribute("offset", "0%");
self.xml.write_attribute("stop-color", &gradient.c0.to_hex());
self.xml.end_element();
self.xml.start_element("stop");
self.xml.write_attribute("offset", "100%");
self.xml.write_attribute("stop-color", &gradient.c1.to_hex());
self.xml.end_element();
self.xml.end_element();
}
self.xml.end_element();
}
pub(super) fn write_gradient_refs(&mut self) {
if self.gradient_refs.is_empty() {
return;
}
self.xml.start_element("defs");
self.xml.write_attribute("id", "gradient-refs");
for (id, gradient_ref) in self.gradient_refs.iter() {
match gradient_ref.kind {
GradientKind::Linear => {
self.xml.start_element("linearGradient");
self.xml.write_attribute(
"gradientTransform",
&SvgMatrix(gradient_ref.transform),
);
}
GradientKind::Radial => {
self.xml.start_element("radialGradient");
self.xml.write_attribute(
"gradientTransform",
&SvgMatrix(gradient_ref.transform),
);
}
GradientKind::Conic => {
self.xml.start_element("pattern");
self.xml.write_attribute(
"patternTransform",
&SvgMatrix(gradient_ref.transform),
);
}
}
self.xml.write_attribute("id", &id);
// Writing the href attribute to the "reference" gradient.
self.xml
.write_attribute_fmt("href", format_args!("#{}", gradient_ref.id));
// Also writing the xlink:href attribute for compatibility.
self.xml
.write_attribute_fmt("xlink:href", format_args!("#{}", gradient_ref.id));
self.xml.end_element();
}
self.xml.end_element();
}
/// Write the raw gradients (without transform) to the SVG file.
pub(super) fn write_patterns(&mut self) {
if self.patterns.is_empty() {
return;
}
self.xml.start_element("defs");
self.xml.write_attribute("id", "patterns");
for (id, pattern) in
self.patterns.iter().map(|(i, p)| (i, p.clone())).collect::<Vec<_>>()
{
let size = pattern.size() + pattern.spacing();
self.xml.start_element("pattern");
self.xml.write_attribute("id", &id);
self.xml.write_attribute("width", &size.x.to_pt());
self.xml.write_attribute("height", &size.y.to_pt());
self.xml.write_attribute("patternUnits", "userSpaceOnUse");
self.xml.write_attribute_fmt(
"viewBox",
format_args!("0 0 {:.3} {:.3}", size.x.to_pt(), size.y.to_pt()),
);
// Render the frame.
let state = State::new(size, Transform::identity());
let ts = Transform::identity();
self.render_frame(state, ts, pattern.frame());
self.xml.end_element();
}
self.xml.end_element()
}
/// Writes the references to the deduplicated patterns for each usage site.
pub(super) fn write_pattern_refs(&mut self) {
if self.pattern_refs.is_empty() {
return;
}
self.xml.start_element("defs");
self.xml.write_attribute("id", "pattern-refs");
for (id, pattern_ref) in self.pattern_refs.iter() {
self.xml.start_element("pattern");
self.xml
.write_attribute("patternTransform", &SvgMatrix(pattern_ref.transform));
self.xml.write_attribute("id", &id);
// Writing the href attribute to the "reference" pattern.
self.xml
.write_attribute_fmt("href", format_args!("#{}", pattern_ref.id));
// Also writing the xlink:href attribute for compatibility.
self.xml
.write_attribute_fmt("xlink:href", format_args!("#{}", pattern_ref.id));
self.xml.end_element();
}
self.xml.end_element();
}
}
/// A reference to a deduplicated pattern, with a transform matrix.
///
/// Allows patterns to be reused across multiple invocations,
/// simply by changing the transform matrix.
#[derive(Hash)]
pub struct PatternRef {
/// The ID of the deduplicated gradient
id: Id,
/// The transform matrix to apply to the pattern.
transform: Transform,
/// The ratio of the size of the cell to the size of the filled area.
ratio: Axes<Ratio>,
}
/// A reference to a deduplicated gradient, with a transform matrix.
///
/// Allows gradients to be reused across multiple invocations,
/// simply by changing the transform matrix.
#[derive(Hash)]
pub struct GradientRef {
/// The ID of the deduplicated gradient
id: Id,
/// The gradient kind (used to determine the SVG element to use)
/// but without needing to clone the entire gradient.
kind: GradientKind,
/// The transform matrix to apply to the gradient.
transform: Transform,
}
/// A subgradient for conic gradients.
#[derive(Hash)]
pub struct SVGSubGradient {
/// The center point of the gradient.
center: Axes<Ratio>,
/// The start point of the subgradient.
t0: Angle,
/// The end point of the subgradient.
t1: Angle,
/// The color at the start point of the subgradient.
c0: Color,
/// The color at the end point of the subgradient.
c1: Color,
}
/// The kind of linear gradient.
#[derive(Hash, Clone, Copy, PartialEq, Eq)]
enum GradientKind {
/// A linear gradient.
Linear,
/// A radial gradient.
Radial,
/// A conic gradient.
Conic,
}
impl From<&Gradient> for GradientKind {
fn from(value: &Gradient) -> Self {
match value {
Gradient::Linear { .. } => GradientKind::Linear,
Gradient::Radial { .. } => GradientKind::Radial,
Gradient::Conic { .. } => GradientKind::Conic,
}
}
}
/// Encode the color as an SVG color.
pub trait ColorEncode {
/// Encode the color.
fn encode(&self) -> EcoString;
}
impl ColorEncode for Color {
fn encode(&self) -> EcoString {
match *self {
c @ Color::Rgb(_)
| c @ Color::Luma(_)
| c @ Color::Cmyk(_)
| c @ Color::Hsv(_) => c.to_hex(),
Color::LinearRgb(rgb) => {
if rgb.alpha != 1.0 {
eco_format!(
"color(srgb-linear {:.5} {:.5} {:.5} / {:.5})",
rgb.red,
rgb.green,
rgb.blue,
rgb.alpha
)
} else {
eco_format!(
"color(srgb-linear {:.5} {:.5} {:.5})",
rgb.red,
rgb.green,
rgb.blue,
)
}
}
Color::Oklab(oklab) => {
if oklab.alpha != 1.0 {
eco_format!(
"oklab({:.3}% {:.5} {:.5} / {:.5})",
oklab.l * 100.0,
oklab.a,
oklab.b,
oklab.alpha
)
} else {
eco_format!(
"oklab({:.3}% {:.5} {:.5})",
oklab.l * 100.0,
oklab.a,
oklab.b,
)
}
}
Color::Oklch(oklch) => {
if oklch.alpha != 1.0 {
eco_format!(
"oklch({:.3}% {:.5} {:.3}deg / {:.3})",
oklch.l * 100.0,
oklch.chroma,
oklch.hue.into_degrees(),
oklch.alpha
)
} else {
eco_format!(
"oklch({:.3}% {:.5} {:.3}deg)",
oklch.l * 100.0,
oklch.chroma,
oklch.hue.into_degrees(),
)
}
}
Color::Hsl(hsl) => {
if hsl.alpha != 1.0 {
eco_format!(
"hsla({:.3}deg {:.3}% {:.3}% / {:.5})",
hsl.hue.into_degrees(),
hsl.saturation * 100.0,
hsl.lightness * 100.0,
hsl.alpha,
)
} else {
eco_format!(
"hsl({:.3}deg {:.3}% {:.3}%)",
hsl.hue.into_degrees(),
hsl.saturation * 100.0,
hsl.lightness * 100.0,
)
}
}
}
}
}
/// Maps a coordinate in a unit size square to a coordinate in the pattern.
pub fn correct_pattern_pos(x: f32) -> f32 {
(x + 0.5) / 2.0
}

View File

@ -0,0 +1,195 @@
use ecow::EcoString;
use ttf_parser::OutlineBuilder;
use typst::layout::{Abs, Ratio, Size, Transform};
use typst::visualize::{
FixedStroke, Geometry, LineCap, LineJoin, Paint, Path, PathItem, RelativeTo, Shape,
};
use crate::paint::ColorEncode;
use crate::{SVGRenderer, State, SvgPathBuilder};
impl SVGRenderer {
/// Render a shape element.
pub(super) fn render_shape(&mut self, state: State, shape: &Shape) {
self.xml.start_element("path");
self.xml.write_attribute("class", "typst-shape");
if let Some(paint) = &shape.fill {
self.write_fill(
paint,
self.shape_fill_size(state, paint, shape),
self.shape_paint_transform(state, paint, shape),
);
} else {
self.xml.write_attribute("fill", "none");
}
if let Some(stroke) = &shape.stroke {
self.write_stroke(
stroke,
self.shape_fill_size(state, &stroke.paint, shape),
self.shape_paint_transform(state, &stroke.paint, shape),
);
}
let path = convert_geometry_to_path(&shape.geometry);
self.xml.write_attribute("d", &path);
self.xml.end_element();
}
/// Calculate the transform of the shape's fill or stroke.
fn shape_paint_transform(
&self,
state: State,
paint: &Paint,
shape: &Shape,
) -> Transform {
let mut shape_size = shape.geometry.bbox_size();
// Edge cases for strokes.
if shape_size.x.to_pt() == 0.0 {
shape_size.x = Abs::pt(1.0);
}
if shape_size.y.to_pt() == 0.0 {
shape_size.y = Abs::pt(1.0);
}
if let Paint::Gradient(gradient) = paint {
match gradient.unwrap_relative(false) {
RelativeTo::Self_ => Transform::scale(
Ratio::new(shape_size.x.to_pt()),
Ratio::new(shape_size.y.to_pt()),
),
RelativeTo::Parent => Transform::scale(
Ratio::new(state.size.x.to_pt()),
Ratio::new(state.size.y.to_pt()),
)
.post_concat(state.transform.invert().unwrap()),
}
} else if let Paint::Pattern(pattern) = paint {
match pattern.unwrap_relative(false) {
RelativeTo::Self_ => Transform::identity(),
RelativeTo::Parent => state.transform.invert().unwrap(),
}
} else {
Transform::identity()
}
}
/// Calculate the size of the shape's fill.
fn shape_fill_size(&self, state: State, paint: &Paint, shape: &Shape) -> Size {
let mut shape_size = shape.geometry.bbox_size();
// Edge cases for strokes.
if shape_size.x.to_pt() == 0.0 {
shape_size.x = Abs::pt(1.0);
}
if shape_size.y.to_pt() == 0.0 {
shape_size.y = Abs::pt(1.0);
}
if let Paint::Gradient(gradient) = paint {
match gradient.unwrap_relative(false) {
RelativeTo::Self_ => shape_size,
RelativeTo::Parent => state.size,
}
} else {
shape_size
}
}
/// Write a stroke attribute.
pub(super) fn write_stroke(
&mut self,
stroke: &FixedStroke,
size: Size,
fill_transform: Transform,
) {
match &stroke.paint {
Paint::Solid(color) => self.xml.write_attribute("stroke", &color.encode()),
Paint::Gradient(gradient) => {
let id = self.push_gradient(gradient, size, fill_transform);
self.xml.write_attribute_fmt("stroke", format_args!("url(#{id})"));
}
Paint::Pattern(pattern) => {
let id = self.push_pattern(pattern, size, fill_transform);
self.xml.write_attribute_fmt("stroke", format_args!("url(#{id})"));
}
}
self.xml.write_attribute("stroke-width", &stroke.thickness.to_pt());
self.xml.write_attribute(
"stroke-linecap",
match stroke.cap {
LineCap::Butt => "butt",
LineCap::Round => "round",
LineCap::Square => "square",
},
);
self.xml.write_attribute(
"stroke-linejoin",
match stroke.join {
LineJoin::Miter => "miter",
LineJoin::Round => "round",
LineJoin::Bevel => "bevel",
},
);
self.xml
.write_attribute("stroke-miterlimit", &stroke.miter_limit.get());
if let Some(pattern) = &stroke.dash {
self.xml.write_attribute("stroke-dashoffset", &pattern.phase.to_pt());
self.xml.write_attribute(
"stroke-dasharray",
&pattern
.array
.iter()
.map(|dash| dash.to_pt().to_string())
.collect::<Vec<_>>()
.join(" "),
);
}
}
}
/// Convert a geometry to an SVG path.
#[comemo::memoize]
fn convert_geometry_to_path(geometry: &Geometry) -> EcoString {
let mut builder = SvgPathBuilder::default();
match geometry {
Geometry::Line(t) => {
builder.move_to(0.0, 0.0);
builder.line_to(t.x.to_pt() as f32, t.y.to_pt() as f32);
}
Geometry::Rect(rect) => {
let x = rect.x.to_pt() as f32;
let y = rect.y.to_pt() as f32;
builder.rect(x, y);
}
Geometry::Path(p) => return convert_path(p),
};
builder.0
}
pub fn convert_path(path: &Path) -> EcoString {
let mut builder = SvgPathBuilder::default();
for item in &path.0 {
match item {
PathItem::MoveTo(m) => {
builder.move_to(m.x.to_pt() as f32, m.y.to_pt() as f32)
}
PathItem::LineTo(l) => {
builder.line_to(l.x.to_pt() as f32, l.y.to_pt() as f32)
}
PathItem::CubicTo(c1, c2, t) => builder.curve_to(
c1.x.to_pt() as f32,
c1.y.to_pt() as f32,
c2.x.to_pt() as f32,
c2.y.to_pt() as f32,
t.x.to_pt() as f32,
t.y.to_pt() as f32,
),
PathItem::ClosePath => builder.close(),
}
}
builder.0
}

View File

@ -0,0 +1,308 @@
use std::io::Read;
use base64::Engine;
use ecow::EcoString;
use ttf_parser::GlyphId;
use typst::layout::{Abs, Point, Ratio, Size, Transform};
use typst::text::{Font, TextItem};
use typst::util::hash128;
use typst::visualize::{Image, Paint, RasterFormat, RelativeTo};
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 = Abs::raw(text.font.units_per_em());
let origin_ascender = text.font.metrics().ascender.at(upem).to_pt();
let glyph_hash = hash128(&(&text.font, id));
let id = self.glyphs.insert_with(glyph_hash, || RenderedGlyph::Image {
url: data_url,
width: upem.to_pt(),
height: upem.to_pt(),
ts: Transform::translate(Abs::zero(), Abs::pt(-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,
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::Pattern(pattern) => match pattern.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, std::u16::MAX)?;
if raster.format != ttf_parser::RasterImageFormat::PNG {
return None;
}
let image = Image::new(raster.data.into(), 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 = Abs::raw(font.units_per_em());
let (width, height) = (upem.to_pt(), upem.to_pt());
let origin_ascender = font.metrics().ascender.at(upem).to_pt();
// 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).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)
}