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 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 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 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 fn shape_fill_size(&self, state: State, paint: &Paint, shape: &Shape) -> Size {
82 let mut shape_size = shape.geometry.bbox_size();
83 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 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#[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}