typst-svg 0.12.0

SVG exporter for Typst.
Documentation
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,
                shape.fill_rule,
                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
}