typst_svg/
shape.rs

1use ecow::EcoString;
2use ttf_parser::OutlineBuilder;
3use typst_library::layout::{Abs, Ratio, Size, Transform};
4use typst_library::visualize::{
5    Curve, CurveItem, FixedStroke, Geometry, LineCap, LineJoin, Paint, RelativeTo, Shape,
6};
7
8use crate::paint::ColorEncode;
9use crate::{SVGRenderer, State, SvgPathBuilder};
10
11impl SVGRenderer {
12    /// Render a shape element.
13    pub(super) fn render_shape(&mut self, state: State, shape: &Shape) {
14        self.xml.start_element("path");
15        self.xml.write_attribute("class", "typst-shape");
16
17        if let Some(paint) = &shape.fill {
18            self.write_fill(
19                paint,
20                shape.fill_rule,
21                self.shape_fill_size(state, paint, shape),
22                self.shape_paint_transform(state, paint, shape),
23            );
24        } else {
25            self.xml.write_attribute("fill", "none");
26        }
27
28        if let Some(stroke) = &shape.stroke {
29            self.write_stroke(
30                stroke,
31                self.shape_fill_size(state, &stroke.paint, shape),
32                self.shape_paint_transform(state, &stroke.paint, shape),
33            );
34        }
35
36        let path = convert_geometry_to_path(&shape.geometry);
37        self.xml.write_attribute("d", &path);
38        self.xml.end_element();
39    }
40
41    /// Calculate the transform of the shape's fill or stroke.
42    fn shape_paint_transform(
43        &self,
44        state: State,
45        paint: &Paint,
46        shape: &Shape,
47    ) -> Transform {
48        let mut shape_size = shape.geometry.bbox_size();
49        // Edge cases for strokes.
50        if shape_size.x.to_pt() == 0.0 {
51            shape_size.x = Abs::pt(1.0);
52        }
53
54        if shape_size.y.to_pt() == 0.0 {
55            shape_size.y = Abs::pt(1.0);
56        }
57
58        if let Paint::Gradient(gradient) = paint {
59            match gradient.unwrap_relative(false) {
60                RelativeTo::Self_ => Transform::scale(
61                    Ratio::new(shape_size.x.to_pt()),
62                    Ratio::new(shape_size.y.to_pt()),
63                ),
64                RelativeTo::Parent => Transform::scale(
65                    Ratio::new(state.size.x.to_pt()),
66                    Ratio::new(state.size.y.to_pt()),
67                )
68                .post_concat(state.transform.invert().unwrap()),
69            }
70        } else if let Paint::Tiling(tiling) = paint {
71            match tiling.unwrap_relative(false) {
72                RelativeTo::Self_ => Transform::identity(),
73                RelativeTo::Parent => state.transform.invert().unwrap(),
74            }
75        } else {
76            Transform::identity()
77        }
78    }
79
80    /// Calculate the size of the shape's fill.
81    fn shape_fill_size(&self, state: State, paint: &Paint, shape: &Shape) -> Size {
82        let mut shape_size = shape.geometry.bbox_size();
83        // Edge cases for strokes.
84        if shape_size.x.to_pt() == 0.0 {
85            shape_size.x = Abs::pt(1.0);
86        }
87
88        if shape_size.y.to_pt() == 0.0 {
89            shape_size.y = Abs::pt(1.0);
90        }
91
92        if let Paint::Gradient(gradient) = paint {
93            match gradient.unwrap_relative(false) {
94                RelativeTo::Self_ => shape_size,
95                RelativeTo::Parent => state.size,
96            }
97        } else {
98            shape_size
99        }
100    }
101
102    /// Write a stroke attribute.
103    pub(super) fn write_stroke(
104        &mut self,
105        stroke: &FixedStroke,
106        size: Size,
107        fill_transform: Transform,
108    ) {
109        match &stroke.paint {
110            Paint::Solid(color) => self.xml.write_attribute("stroke", &color.encode()),
111            Paint::Gradient(gradient) => {
112                let id = self.push_gradient(gradient, size, fill_transform);
113                self.xml.write_attribute_fmt("stroke", format_args!("url(#{id})"));
114            }
115            Paint::Tiling(tiling) => {
116                let id = self.push_tiling(tiling, size, fill_transform);
117                self.xml.write_attribute_fmt("stroke", format_args!("url(#{id})"));
118            }
119        }
120
121        self.xml.write_attribute("stroke-width", &stroke.thickness.to_pt());
122        self.xml.write_attribute(
123            "stroke-linecap",
124            match stroke.cap {
125                LineCap::Butt => "butt",
126                LineCap::Round => "round",
127                LineCap::Square => "square",
128            },
129        );
130        self.xml.write_attribute(
131            "stroke-linejoin",
132            match stroke.join {
133                LineJoin::Miter => "miter",
134                LineJoin::Round => "round",
135                LineJoin::Bevel => "bevel",
136            },
137        );
138        self.xml
139            .write_attribute("stroke-miterlimit", &stroke.miter_limit.get());
140        if let Some(dash) = &stroke.dash {
141            self.xml.write_attribute("stroke-dashoffset", &dash.phase.to_pt());
142            self.xml.write_attribute(
143                "stroke-dasharray",
144                &dash
145                    .array
146                    .iter()
147                    .map(|dash| dash.to_pt().to_string())
148                    .collect::<Vec<_>>()
149                    .join(" "),
150            );
151        }
152    }
153}
154
155/// Convert a geometry to an SVG path.
156#[comemo::memoize]
157fn convert_geometry_to_path(geometry: &Geometry) -> EcoString {
158    let mut builder = SvgPathBuilder::default();
159    match geometry {
160        Geometry::Line(t) => {
161            builder.move_to(0.0, 0.0);
162            builder.line_to(t.x.to_pt() as f32, t.y.to_pt() as f32);
163        }
164        Geometry::Rect(rect) => {
165            let x = rect.x.to_pt() as f32;
166            let y = rect.y.to_pt() as f32;
167            builder.rect(x, y);
168        }
169        Geometry::Curve(p) => return convert_curve(p),
170    };
171    builder.0
172}
173
174pub fn convert_curve(curve: &Curve) -> EcoString {
175    let mut builder = SvgPathBuilder::default();
176    for item in &curve.0 {
177        match item {
178            CurveItem::Move(m) => builder.move_to(m.x.to_pt() as f32, m.y.to_pt() as f32),
179            CurveItem::Line(l) => builder.line_to(l.x.to_pt() as f32, l.y.to_pt() as f32),
180            CurveItem::Cubic(c1, c2, t) => builder.curve_to(
181                c1.x.to_pt() as f32,
182                c1.y.to_pt() as f32,
183                c2.x.to_pt() as f32,
184                c2.y.to_pt() as f32,
185                t.x.to_pt() as f32,
186                t.y.to_pt() as f32,
187            ),
188            CurveItem::Close => builder.close(),
189        }
190    }
191    builder.0
192}