layout/backends/
svg.rs

1//! SVG rendering backend that accepts draw calls and saves the output to a file.
2
3use 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("&amp;");
32            }
33            '<' => {
34                res.push_str("&lt;");
35            }
36            '>' => {
37                res.push_str("&gt;");
38            }
39            '"' => {
40                res.push_str("&quot;");
41            }
42            '\'' => {
43                res.push_str("&apos;");
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    // Maps font sizes to their class name and class impl.
59    font_style_map: HashMap<usize, (String, String)>,
60    // A list of clip regions to generate.
61    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
82// This trivial implementation of `drop` adds a print to a file.
83impl Drop for SVGWriter {
84    fn drop(&mut self) {}
85}
86
87impl SVGWriter {
88    // Grow the viewable svg window to include the point \p point plus some
89    // offset \p size.
90    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    // Gets or creates a font 'class' for the parameters. Returns the class
96    // name.
97    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        // This is a list of vectors. The first vector is the "exit" vector
242        // from the first point, and the rest of the vectors are "entry" vectors
243        // into the following points.
244        path: &[(Point, Point)],
245        dashed: bool,
246        head: (bool, bool),
247        look: &StyleAttr,
248        properties: Option<String>,
249        text: &str,
250    ) {
251        // Control points as defined in here:
252        // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#curve_commands
253        // Structured as [(M,C) S S S ...]
254        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        // Handle the "exit vector" from the first point.
278        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        // Handle the "entry vector" from the rest of the points.
291        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}