1use crate::core::color::Color;
4use crate::core::format::{ClipHandle, RenderBackend};
5use crate::core::geometry::Point;
6use crate::core::style::StyleAttr;
7use std::collections::HashMap;
8
9static SVG_HEADER: &str =
10 r#"<?xml version="1.0" encoding="UTF-8" standalone="no"?>"#;
11
12static SVG_DEFS: &str = r#"<defs>
13<marker id="startarrow" markerWidth="10" markerHeight="7"
14refX="0" refY="3.5" orient="auto">
15<polygon points="10 0, 10 7, 0 3.5" fill="context-stroke" />
16</marker>
17<marker id="endarrow" markerWidth="10" markerHeight="7"
18refX="10" refY="3.5" orient="auto">
19<polygon points="0 0, 10 3.5, 0 7" fill="context-stroke" />
20</marker>
21
22</defs>"#;
23
24static SVG_FOOTER: &str = "</svg>";
25
26fn escape_string(x: &str) -> String {
27 let mut res = String::new();
28 for c in x.chars() {
29 match c {
30 '&' => {
31 res.push_str("&");
32 }
33 '<' => {
34 res.push_str("<");
35 }
36 '>' => {
37 res.push_str(">");
38 }
39 '"' => {
40 res.push_str(""");
41 }
42 '\'' => {
43 res.push_str("'");
44 }
45 _ => {
46 res.push(c);
47 }
48 }
49 }
50 res
51}
52
53#[derive(Debug)]
54pub struct SVGWriter {
55 content: String,
56 view_size: Point,
57 counter: usize,
58 font_style_map: HashMap<usize, (String, String)>,
60 clip_regions: Vec<String>,
62}
63
64impl SVGWriter {
65 pub fn new() -> SVGWriter {
66 SVGWriter {
67 content: String::new(),
68 view_size: Point::zero(),
69 counter: 0,
70 font_style_map: HashMap::new(),
71 clip_regions: Vec::new(),
72 }
73 }
74}
75
76impl Default for SVGWriter {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82impl Drop for SVGWriter {
84 fn drop(&mut self) {}
85}
86
87impl SVGWriter {
88 fn grow_window(&mut self, point: Point, size: Point) {
91 self.view_size.x = self.view_size.x.max(point.x + size.x + 5.);
92 self.view_size.y = self.view_size.y.max(point.y + size.y + 5.);
93 }
94
95 fn get_or_create_font_style(&mut self, font_size: usize) -> String {
98 if let Option::Some(x) = self.font_style_map.get(&font_size) {
99 return x.0.clone();
100 }
101 let class_name = format!("a{}", font_size);
102 let class_impl = format!(
103 ".a{} {{ font-size: {}px; font-family: Times, serif; }}",
104 font_size, font_size
105 );
106 let impl_ = (class_name.clone(), class_impl);
107 self.font_style_map.insert(font_size, impl_);
108 class_name
109 }
110
111 fn emit_svg_font_styles(&self) -> String {
112 let mut content = String::new();
113 content.push_str("<style>\n");
114 for p in self.font_style_map.iter() {
115 content.push_str(&p.1 .1);
116 content.push('\n');
117 }
118 content.push_str("</style>\n");
119 for p in self.clip_regions.iter() {
120 content.push_str(p);
121 content.push('\n');
122 }
123 content
124 }
125
126 pub fn finalize(&self) -> String {
127 let mut result = String::new();
128 result.push_str(SVG_HEADER);
129
130 let svg_line = format!(
131 "<svg width=\"{}\" height=\"{}\" viewBox=\"0 0 {} {}\
132 \" xmlns=\"http://www.w3.org/2000/svg\">\n",
133 self.view_size.x,
134 self.view_size.y,
135 self.view_size.x,
136 self.view_size.y
137 );
138 result.push_str(&svg_line);
139 result.push_str(SVG_DEFS);
140 result.push_str(&self.emit_svg_font_styles());
141 result.push_str(&self.content);
142 result.push_str(SVG_FOOTER);
143 result
144 }
145}
146impl RenderBackend for SVGWriter {
147 fn draw_rect(
148 &mut self,
149 xy: Point,
150 size: Point,
151 look: &StyleAttr,
152 properties: Option<String>,
153 clip: Option<ClipHandle>,
154 ) {
155 self.grow_window(xy, size);
156
157 let mut clip_option = String::new();
158 if let Option::Some(clip_id) = clip {
159 clip_option = format!("clip-path=\"url(#C{})\"", clip_id);
160 }
161 let props = properties.unwrap_or_default();
162 let fill_color = look.fill_color.unwrap_or_else(Color::transparent);
163 let stroke_width = look.line_width;
164 let stroke_color = look.line_color;
165 let rounded_px = look.rounded;
166 let line1 = format!(
167 "<g {props}>\n
168 <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"{}\"
169 stroke-width=\"{}\" stroke=\"{}\" rx=\"{}\" {} />\n
170 </g>\n",
171 xy.x,
172 xy.y,
173 size.x,
174 size.y,
175 fill_color.to_web_color(),
176 stroke_width,
177 stroke_color.to_web_color(),
178 rounded_px,
179 clip_option
180 );
181 self.content.push_str(&line1);
182 }
183
184 fn draw_circle(
185 &mut self,
186 xy: Point,
187 size: Point,
188 look: &StyleAttr,
189 properties: Option<String>,
190 ) {
191 self.grow_window(xy, size);
192 let fill_color = look.fill_color.unwrap_or_else(Color::transparent);
193 let stroke_width = look.line_width;
194 let stroke_color = look.line_color;
195 let props = properties.unwrap_or_default();
196 let line1 = format!(
197 "<g {props}>\n
198 <ellipse cx=\"{}\" cy=\"{}\" rx=\"{}\" ry=\"{}\" fill=\"{}\"
199 stroke-width=\"{}\" stroke=\"{}\"/>\n
200 </g>\n",
201 xy.x,
202 xy.y,
203 size.x / 2.,
204 size.y / 2.,
205 fill_color.to_web_color(),
206 stroke_width,
207 stroke_color.to_web_color()
208 );
209 self.content.push_str(&line1);
210 }
211
212 fn draw_text(&mut self, xy: Point, text: &str, look: &StyleAttr) {
213 let len = text.len();
214
215 let font_class = self.get_or_create_font_style(look.font_size);
216
217 let mut content = String::new();
218 let cnt = 1 + text.lines().count();
219 let size_y = (cnt * look.font_size) as f64;
220 for line in text.lines() {
221 content.push_str(&format!("<tspan x = \"{}\" dy=\"1.0em\">", xy.x));
222 content.push_str(&escape_string(line));
223 content.push_str("</tspan>");
224 }
225
226 self.grow_window(xy, Point::new(10., len as f64 * 10.));
227 let line = format!(
228 "<text dominant-baseline=\"middle\" text-anchor=\"middle\"
229 x=\"{}\" y=\"{}\" class=\"{}\">{}</text>",
230 xy.x,
231 xy.y - size_y / 2.,
232 font_class,
233 &content
234 );
235
236 self.content.push_str(&line);
237 }
238
239 fn draw_arrow(
240 &mut self,
241 path: &[(Point, Point)],
245 dashed: bool,
246 head: (bool, bool),
247 look: &StyleAttr,
248 properties: Option<String>,
249 text: &str,
250 ) {
251 for point in path {
255 self.grow_window(point.0, Point::zero());
256 self.grow_window(point.1, Point::zero());
257 }
258
259 let dash = if dashed {
260 &"stroke-dasharray=\"5,5\""
261 } else {
262 &""
263 };
264 let start = if head.0 {
265 "marker-start=\"url(#startarrow)\""
266 } else {
267 ""
268 };
269 let end = if head.1 {
270 "marker-end=\"url(#endarrow)\""
271 } else {
272 ""
273 };
274
275 let mut path_builder = String::new();
276
277 path_builder.push_str(&format!(
279 "M {} {} C {} {}, {} {}, {} {} ",
280 path[0].0.x,
281 path[0].0.y,
282 path[0].1.x,
283 path[0].1.y,
284 path[1].0.x,
285 path[1].0.y,
286 path[1].1.x,
287 path[1].1.y
288 ));
289
290 for point in path.iter().skip(2) {
292 path_builder.push_str(&format!(
293 "S {} {}, {} {} ",
294 point.0.x, point.0.y, point.1.x, point.1.y
295 ));
296 }
297
298 let stroke_width = look.line_width;
299 let stroke_color = look.line_color;
300 let props = properties.unwrap_or_default();
301 let line = format!(
302 "<g {props}>\n
303 <path id=\"arrow{}\" d=\"{}\" \
304 stroke=\"{}\" stroke-width=\"{}\" {} {} {}
305 fill=\"transparent\" />\n
306 </g>\n",
307 self.counter,
308 path_builder.as_str(),
309 stroke_color.to_web_color(),
310 stroke_width,
311 dash,
312 start,
313 end
314 );
315 self.content.push_str(&line);
316
317 let font_class = self.get_or_create_font_style(look.font_size);
318 let line = format!(
319 "<text><textPath href=\"#arrow{}\" startOffset=\"50%\" \
320 text-anchor=\"middle\" class=\"{}\">{}</textPath></text>",
321 self.counter,
322 font_class,
323 escape_string(text)
324 );
325 self.content.push_str(&line);
326 self.counter += 1;
327 }
328
329 fn draw_line(
330 &mut self,
331 start: Point,
332 stop: Point,
333 look: &StyleAttr,
334 properties: Option<String>,
335 ) {
336 let stroke_width = look.line_width;
337 let stroke_color = look.line_color;
338 let props = properties.unwrap_or_default();
339 let line1 = format!(
340 "<g {props}>\n
341 <line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" stroke-width=\"{}\"
342 stroke=\"{}\" />\n
343 </g>\n",
344 start.x,
345 start.y,
346 stop.x,
347 stop.y,
348 stroke_width,
349 stroke_color.to_web_color()
350 );
351 self.content.push_str(&line1);
352 }
353
354 fn create_clip(
355 &mut self,
356 xy: Point,
357 size: Point,
358 rounded_px: usize,
359 ) -> ClipHandle {
360 let handle = self.clip_regions.len();
361
362 let clip_code = format!(
363 "<clipPath id=\"C{}\"><rect x=\"{}\" y=\"{}\" \
364 width=\"{}\" height=\"{}\" rx=\"{}\" /> \
365 </clipPath>",
366 handle, xy.x, xy.y, size.x, size.y, rounded_px
367 );
368
369 self.clip_regions.push(clip_code);
370
371 handle
372 }
373}