1use std::path::Path;
2use std::{fmt, io};
3
4use plotive::geom::{self, Transform};
5use plotive::render::{self, Surface};
6use plotive::{Prepare, Rgba8, Style, des, drawing};
7use svg::Node;
8use svg::node::element;
9
10#[derive(Debug)]
11pub enum Error {
12 Io(io::Error),
13 Drawing(drawing::Error),
14}
15
16impl From<io::Error> for Error {
17 fn from(err: io::Error) -> Self {
18 Error::Io(err)
19 }
20}
21
22impl From<drawing::Error> for Error {
23 fn from(err: drawing::Error) -> Self {
24 Error::Drawing(err)
25 }
26}
27
28impl fmt::Display for Error {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Error::Io(err) => write!(f, "IO error: {}", err),
32 Error::Drawing(err) => write!(f, "Drawing error: {}", err),
33 }
34 }
35}
36
37impl std::error::Error for Error {}
38
39#[derive(Debug, Clone)]
41pub struct Params<'a> {
42 pub style: Style,
43 pub scale: f32,
44 pub fontdb: Option<&'a plotive::fontdb::Database>,
49 pub id_prefix: Option<String>,
52}
53
54impl Default for Params<'_> {
55 fn default() -> Self {
56 Self {
57 style: Style::default(),
58 scale: 1.0,
59 fontdb: None,
60 id_prefix: None,
61 }
62 }
63}
64
65pub trait SaveSvg {
67 fn save_svg<P, D>(&self, path: P, data_src: &D, params: Params) -> Result<(), Error>
92 where
93 P: AsRef<Path>,
94 D: plotive::data::Source + ?Sized;
95}
96
97impl SaveSvg for des::Figure {
98 fn save_svg<P, D>(&self, path: P, data_src: &D, params: Params) -> Result<(), Error>
99 where
100 P: AsRef<Path>,
101 D: plotive::data::Source + ?Sized,
102 {
103 let prepared = self.prepare(data_src, params.fontdb)?;
104 prepared.save_svg(path, data_src, params)
105 }
106}
107
108impl SaveSvg for drawing::PreparedFigure {
109 fn save_svg<P, D>(&self, path: P, _data_src: &D, params: Params) -> Result<(), Error>
110 where
111 P: AsRef<Path>,
112 D: plotive::data::Source + ?Sized,
113 {
114 let size = self.size();
115 let witdth = (size.width() * params.scale) as u32;
116 let height = (size.height() * params.scale) as u32;
117
118 let mut surface = SvgSurface::new(witdth, height);
119 if let Some(id_prefix) = params.id_prefix.as_ref() {
120 surface = surface.with_id_prefix(id_prefix);
121 }
122
123 self.draw(&mut surface, ¶ms.style);
124 surface.save_svg(path)?;
125 Ok(())
126 }
127}
128
129pub struct SvgSurface {
130 doc: svg::Document,
131 defs: Option<element::Definitions>,
132 doc_children: Vec<Box<dyn Node>>,
133 id_num: u32,
134 id_prefix: Option<String>,
135 group_stack: Vec<element::Group>,
136}
137
138impl SvgSurface {
139 pub fn new(width: u32, height: u32) -> Self {
140 let doc = svg::Document::new()
141 .set("width", width)
142 .set("height", height);
143 SvgSurface {
144 doc,
145 defs: None,
146 doc_children: Vec::new(),
147 id_prefix: None,
148 id_num: 0,
149 group_stack: vec![],
150 }
151 }
152
153 pub fn with_id_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
156 self.id_prefix = Some(prefix.into());
157 self
158 }
159
160 pub fn save_svg<P: AsRef<std::path::Path>>(&self, path: P) -> io::Result<()> {
161 if !self.group_stack.is_empty() {
162 panic!("Unbalanced clip stack");
163 }
164 svg::save(path, &self.doc)
165 }
166
167 pub fn write<W>(&self, dest: &mut W) -> io::Result<()>
168 where
169 W: io::Write,
170 {
171 if !self.group_stack.is_empty() {
172 panic!("Unbalanced clip stack");
173 }
174 svg::write(dest, &self.doc)
175 }
176}
177
178impl Surface for SvgSurface {
179 fn caps(&self) -> render::SurfaceCaps {
180 render::SurfaceCaps {
181 max_gradient_stops: usize::MAX,
182 }
183 }
184
185 fn prepare(&mut self, size: geom::Size, fill: Option<render::Paint>) {
187 self.doc
188 .assign("viewBox", (0, 0, size.width(), size.height()));
189 if let Some(fill) = fill {
190 let mut node = element::Rectangle::new()
191 .set("width", "100%")
192 .set("height", "100%");
193 match fill {
194 render::Paint::Solid(color) => node.assign("fill", color.html()),
195 render::Paint::LinearGradient {
196 start_pos,
197 end_pos,
198 stops,
199 } => {
200 let grad_id = self.add_linear_gradient(start_pos, end_pos, stops);
201 node.assign("fill", format!("url(#{})", grad_id));
202 }
203 }
204 self.append_node(node);
205 }
206 }
207
208 fn draw_rect(&mut self, rect: &render::Rect) {
210 let mut node = rectangle_node(&rect.rect);
211 self.assign_fill(&mut node, rect.fill.as_ref());
212 self.assign_stroke(&mut node, rect.stroke.as_ref());
213 self.assign_transform(&mut node, rect.transform);
214 self.append_node(node);
215 }
216
217 fn draw_path(&mut self, path: &render::Path) {
218 let mut node = element::Path::new();
219 self.assign_fill(&mut node, path.fill.as_ref());
220 self.assign_stroke(&mut node, path.stroke.as_ref());
221 self.assign_transform(&mut node, path.transform);
222 node.assign("d", path_data(path.path));
223 self.append_node(node);
224 }
225
226 fn push_clip(&mut self, clip: &render::Clip) {
227 let clip_id = self.bump_id();
228 let clip_id_url = format!("url(#{})", clip_id);
229 let mut rect_node = rectangle_node(&clip.rect);
230 self.assign_transform(&mut rect_node, clip.transform);
231 let node = element::ClipPath::new()
232 .set("id", clip_id.clone())
233 .add(rect_node);
234 let defs = self.defs.get_or_insert_with(element::Definitions::new);
235 defs.append(node);
236 self.group_stack
237 .push(element::Group::new().set("clip-path", clip_id_url));
238 }
239
240 fn pop_clip(&mut self) {
241 let g = self.group_stack.pop();
242 if g.is_none() {
243 panic!("Unbalanced clip stack");
244 }
245 self.append_node(g.unwrap());
246 }
247
248 fn finalize(&mut self) {
249 if !self.group_stack.is_empty() {
250 panic!("Unbalanced clip stack");
251 }
252
253 if let Some(defs) = self.defs.take() {
254 self.doc.append(defs);
255 }
256 for child in self.doc_children.drain(..) {
257 self.doc.append(child);
258 }
259 }
260}
261
262impl SvgSurface {
263 fn append_node<T>(&mut self, node: T)
264 where
265 T: Node,
266 {
267 if self.group_stack.is_empty() {
268 self.doc_children.push(Box::new(node));
269 } else {
270 self.group_stack.last_mut().unwrap().append(node);
271 }
272 }
273
274 fn bump_id(&mut self) -> String {
275 self.id_num += 1;
276 let prefix = self.id_prefix.as_deref().unwrap_or("plotive");
277 format!("{}{}", prefix, self.id_num)
278 }
279
280 fn add_linear_gradient(
281 &mut self,
282 start_pos: geom::Point,
283 end_pos: geom::Point,
284 stops: &[(f32, Rgba8)],
285 ) -> String {
286 let id = self.bump_id();
287 let mut grad = element::LinearGradient::new()
288 .set("id", id.clone())
289 .set("gradientUnits", "userSpaceOnUse")
290 .set("x1", start_pos.x)
291 .set("y1", start_pos.y)
292 .set("x2", end_pos.x)
293 .set("y2", end_pos.y);
294 for (offset, color) in stops {
295 grad.append(
296 element::Stop::new()
297 .set("offset", format!("{}%", offset * 100.0))
298 .set("stop-color", color.html()),
299 );
300 }
301 let defs = self.defs.get_or_insert_with(element::Definitions::new);
302 defs.append(grad);
303 id
304 }
305
306 fn assign_transform<N>(&self, node: &mut N, transform: Option<&geom::Transform>)
307 where
308 N: Node,
309 {
310 if let Some(Transform {
311 sx,
312 kx,
313 ky,
314 sy,
315 tx,
316 ty,
317 }) = transform
318 {
319 node.assign(
320 "transform",
321 format!("matrix({sx} {ky} {kx} {sy} {tx} {ty})"),
322 );
323 }
324 }
325
326 fn assign_fill<N>(&mut self, node: &mut N, fill: Option<&render::Paint>)
327 where
328 N: Node,
329 {
330 match fill {
331 Some(render::Paint::Solid(color)) => {
332 let (rgb, opacity) = color.split_rgb_opacity();
333 node.assign("fill", rgb.html());
334 if let Some(opacity) = opacity {
335 node.assign("fill-opacity", opacity);
336 }
337 }
338 Some(render::Paint::LinearGradient {
339 start_pos,
340 end_pos,
341 stops,
342 }) => {
343 let grad_id = self.add_linear_gradient(*start_pos, *end_pos, stops);
344 node.assign("fill", format!("url(#{})", grad_id));
345 }
346 None => {
347 node.assign("fill", "none");
348 }
349 }
350 }
351 fn assign_stroke<N>(&self, node: &mut N, stroke: Option<&render::Stroke>)
352 where
353 N: Node,
354 {
355 if let Some(stroke) = stroke {
356 let (rgb, opacity) = stroke.color.split_rgb_opacity();
357 node.assign("stroke", rgb.html());
358 if let Some(opacity) = opacity {
359 node.assign("stroke-opacity", opacity);
360 }
361 let w = stroke.width;
362 node.assign("stroke-width", w);
363 match stroke.pattern {
364 render::LinePattern::Solid => (),
365 render::LinePattern::Dash(dash) => {
366 let array: Vec<f32> = dash.iter().map(|d| d * w).collect();
367 node.assign("stroke-dasharray", array)
368 }
369 }
370 match stroke.join {
371 render::LineJoin::Miter => node.assign("stroke-linejoin", "miter"),
372 render::LineJoin::Round => node.assign("stroke-linejoin", "round"),
373 render::LineJoin::Bevel => node.assign("stroke-linejoin", "bevel"),
374 }
375 match stroke.cap {
376 render::LineCap::Butt => node.assign("stroke-linecap", "butt"),
377 render::LineCap::Round => node.assign("stroke-linecap", "round"),
378 render::LineCap::Square => node.assign("stroke-linecap", "square"),
379 }
380 } else {
381 node.assign("stroke", "none");
382 }
383 }
384}
385
386fn path_data(path: &geom::Path) -> element::path::Data {
387 let mut data = element::path::Data::new();
388 for segment in path.segments() {
389 match segment {
390 geom::PathSegment::MoveTo(p) => {
391 data = data.move_to((p.x, p.y));
392 }
393 geom::PathSegment::LineTo(p) => {
394 data = data.line_to((p.x, p.y));
395 }
396 geom::PathSegment::QuadTo(p1, p2) => {
397 data = data.quadratic_curve_to((p1.x, p1.y, p2.x, p2.y));
398 }
399 geom::PathSegment::CubicTo(p1, p2, p3) => {
400 data = data.cubic_curve_to((p1.x, p1.y, p2.x, p2.y, p3.x, p3.y));
401 }
402 geom::PathSegment::Close => {
403 data = data.close();
404 }
405 }
406 }
407 data
408}
409
410fn rectangle_node(rect: &geom::Rect) -> element::Rectangle {
411 element::Rectangle::new()
412 .set("x", rect.x())
413 .set("y", rect.y())
414 .set("width", rect.width())
415 .set("height", rect.height())
416}