svgplot/
lib.rs

1use std::io::Write;
2
3use crate::escape::escape_xml;
4pub use circle::*;
5pub use color::*;
6use common_attributes::*;
7pub use element::*;
8pub use group::*;
9pub use id::*;
10pub use path::*;
11pub use rect::*;
12pub use script::*;
13pub use stroke::*;
14pub use style::*;
15pub use svg_use::*;
16pub use transform::*;
17pub use view_box::*;
18
19pub mod circle;
20pub mod color;
21pub mod common_attributes;
22pub mod element;
23pub(crate) mod escape;
24pub mod group;
25pub mod id;
26pub mod path;
27pub mod rect;
28pub mod script;
29pub mod stroke;
30pub mod style;
31pub mod svg_use;
32pub mod transform;
33pub mod view_box;
34
35pub type Coordinate = f64;
36
37pub type SvgInteger = i64;
38
39enum OptionalSvgId {
40    None,
41    Some(SvgId),
42    Def(SvgId),
43}
44
45pub struct SvgImage {
46    id_sequence: u32,
47    dimensions: Option<(SvgInteger, SvgInteger)>,
48    view_box: Option<ViewBox>,
49    elements: Vec<(OptionalSvgId, SvgElement)>,
50    data_attributes: Vec<(String, String)>,
51    common_attributes: CommonAttributes,
52}
53
54implement_common_attributes!(SvgImage);
55
56impl SvgImage {
57    pub const fn new() -> Self {
58        Self {
59            id_sequence: 0,
60            dimensions: None,
61            view_box: None,
62            elements: Vec::new(),
63            data_attributes: Vec::new(),
64            common_attributes: CommonAttributes::new(),
65        }
66    }
67
68    pub const fn dimensions(mut self, width: SvgInteger, height: SvgInteger) -> Self {
69        self.dimensions = Some((width, height));
70        self
71    }
72
73    pub fn data_attribute(mut self, name: String, value: String) -> Self {
74        self.data_attributes.push((name, value));
75        self
76    }
77
78    pub fn view_box<V: Into<ViewBox>>(mut self, view_box: V) -> Self {
79        self.view_box = Some(view_box.into());
80        self
81    }
82
83    pub fn add<E: Into<SvgElement>>(&mut self, element: E) -> &mut Self {
84        self.elements.push((OptionalSvgId::None, element.into()));
85        self
86    }
87
88    pub fn add_with_id<E: Into<SvgElement>>(&mut self, element: E) -> SvgId {
89        let new_id = SvgId {
90            value: self.id_sequence,
91        };
92        self.id_sequence += 1;
93        self.elements
94            .push((OptionalSvgId::Some(new_id), element.into()));
95        new_id
96    }
97
98    pub fn define<E: Into<SvgElement>>(&mut self, element: E) -> SvgId {
99        let new_id = SvgId {
100            value: self.id_sequence,
101        };
102        self.id_sequence += 1;
103        self.elements
104            .push((OptionalSvgId::Def(new_id), element.into()));
105        new_id
106    }
107
108    pub fn to_svg_string(&self) -> String {
109        #![allow(clippy::unwrap_used)]
110        let mut buffer = Vec::new();
111        buffer
112            .write_all(b"<svg xmlns=\"http://www.w3.org/2000/svg\"")
113            .unwrap();
114        if let Some((x, y)) = &self.dimensions {
115            buffer
116                .write_all(format!(" x=\"{x}\" y=\"{y}\"").as_bytes())
117                .unwrap();
118        }
119        if let Some(view_box) = &self.view_box {
120            let s = format!(
121                " viewBox=\"{} {} {} {}\" preserveAspectRatio=\"xMidYMid\"",
122                view_box.min_x, view_box.min_y, view_box.width, view_box.height
123            );
124            buffer.write_all(s.as_bytes()).unwrap();
125        }
126        for (name, value) in &self.data_attributes {
127            buffer
128                .write_all(
129                    format!("data-{}=\"{}\"", escape_xml(name), escape_xml(value)).as_bytes(),
130                )
131                .unwrap();
132        }
133        self.common_attributes.write(&mut buffer);
134        buffer.write_all(b">\n").unwrap();
135
136        let mut first = true;
137        for (id, element) in &self.elements {
138            if let OptionalSvgId::Def(id) = id {
139                if first {
140                    first = false;
141                    buffer.write_all(b"<defs>").unwrap();
142                }
143                element.write(Some(*id), &mut buffer);
144            }
145        }
146        if !first {
147            buffer.write_all(b"</defs>").unwrap();
148        }
149
150        for (id, element) in &self.elements {
151            match id {
152                OptionalSvgId::None => {
153                    element.write(None, &mut buffer);
154                }
155                OptionalSvgId::Some(id) => {
156                    element.write(Some(*id), &mut buffer);
157                }
158                OptionalSvgId::Def(_) => {}
159            }
160        }
161
162        buffer.write_all(b"</svg>").unwrap();
163        String::from_utf8(buffer).unwrap()
164    }
165}
166
167#[test]
168fn test() {
169    let mut image = SvgImage::new()
170        .dimensions(200, 00)
171        .view_box((-100, -100, 200, 200))
172        .style("--step: 0");
173    for (offset_x, offset_y, color) in [
174        (-100, -100, (0xFF, 0, 0)),
175        (0, -100, (0, 0xFF, 0)),
176        (-100, 0, (0, 0, 0xFF)),
177        (0, 0, (0xFF, 0xFF, 0xFF)),
178    ] {
179        let id = image.add_with_id(
180            SvgGroup::with_elements(vec![SvgRect::default()
181                .x(offset_x)
182                .y(offset_y)
183                .width(100)
184                .height(100)
185                .fill(SvgColor::Rgb(color.0, color.1, color.2))])
186            .style("opacity: 0"),
187        );
188        if color.0 == 0xff {
189            image.add(SvgScript::new(format!(
190                "setTimeout(() => {{ document.getElementById('{id}').remove(); }}, 1000);"
191            )));
192        }
193    }
194    image.add(SvgPath {
195        stroke: Some(SvgColor::Rgb(0xFF, 0xFF, 0)),
196        shape: SvgShape::at(10.6, 10.).close(),
197        ..Default::default()
198    });
199}