1use super::backend::{
8 DrawBackend, FillPattern, FontFace, LineEnd, LineStyle, PointShape, PointStyle, RectStyle,
9 TextAnchor, TextStyle,
10};
11use super::{Rect, RenderError};
12
13pub struct SvgBackend {
20 plot_area: Rect,
21 total_area: Rect,
22 body: String,
23 tooltip: Option<String>,
24 axis_key: Option<String>,
25 series_key: Option<String>,
26 value_key: Option<String>,
27 mark_part: Option<String>,
28 mark_attrs: Vec<(String, String)>,
29 mark_geom: Option<String>,
31 root_attrs: Vec<(String, String)>,
32 warnings: Vec<String>,
33 plot_attr: bool,
36 formatted_values: bool,
39 mark_fvalue: Option<String>,
42 mark_fx: Option<String>,
45 patterns: Vec<(FillPattern, String)>,
50 id_prefix: String,
55}
56
57static NEXT_ID_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
60
61impl SvgBackend {
62 pub fn new(width: u32, height: u32, plot_area: Rect) -> Self {
63 SvgBackend {
64 plot_area,
65 total_area: Rect {
66 x: 0.0,
67 y: 0.0,
68 width: width as f64,
69 height: height as f64,
70 },
71 body: String::new(),
72 tooltip: None,
73 axis_key: None,
74 series_key: None,
75 value_key: None,
76 mark_part: None,
77 mark_attrs: Vec::new(),
78 mark_geom: None,
79 root_attrs: Vec::new(),
80 warnings: Vec::new(),
81 plot_attr: true,
82 formatted_values: false,
83 mark_fvalue: None,
84 mark_fx: None,
85 patterns: Vec::new(),
86 id_prefix: format!(
87 "gg{:x}",
88 NEXT_ID_PREFIX.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
89 ),
90 }
91 }
92
93 fn pattern_url(&mut self, pattern: &FillPattern) -> String {
97 if let Some((_, id)) = self.patterns.iter().find(|(p, _)| p == pattern) {
98 return format!("url(#{id})");
99 }
100 let id = format!("{}-pat-{}", self.id_prefix, self.patterns.len());
101 self.patterns.push((*pattern, id.clone()));
102 format!("url(#{id})")
103 }
104
105 fn rect_markup(
108 &mut self,
109 (x0, y0): (f64, f64),
110 (x1, y1): (f64, f64),
111 fill: &str,
112 style: &RectStyle,
113 ) -> Result<(), RenderError> {
114 if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) {
115 return Ok(());
116 }
117 let (x, y) = (x0.min(x1), y0.min(y1));
118 let (w, h) = ((x1 - x0).abs(), (y1 - y0).abs());
119 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
120 let r = style.corner_radius.max(0.0).min(w / 2.0).min(h / 2.0);
123 let radius = if r > 0.0 {
124 format!(" rx=\"{r:.2}\" ry=\"{r:.2}\"")
125 } else {
126 String::new()
127 };
128 let attrs = format!(
129 "x=\"{x:.2}\" y=\"{y:.2}\" width=\"{w:.2}\" height=\"{h:.2}\"{radius} fill=\"{fill}\"{} \
130 stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
131 opacity_attr("fill", style.alpha),
132 finite_or(style.stroke_width, 0.0)
133 );
134 self.push_mark("rect", "bar", &attrs);
135 Ok(())
136 }
137
138 fn polygon_markup(
140 &mut self,
141 pts: &[(f64, f64)],
142 fill: &str,
143 style: &RectStyle,
144 ) -> Result<(), RenderError> {
145 let pts = finite_pts(pts);
146 if pts.len() < 3 {
147 return Ok(());
148 }
149 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
150 let attrs = format!(
151 "points=\"{}\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
152 points(&pts),
153 opacity_attr("fill", style.alpha),
154 finite_or(style.stroke_width, 0.0)
155 );
156 self.push_mark("polygon", "band", &attrs);
157 Ok(())
158 }
159
160 fn polygon_rings_markup(
163 &mut self,
164 rings: &[Vec<(f64, f64)>],
165 fill: &str,
166 style: &RectStyle,
167 ) -> Result<(), RenderError> {
168 let mut d = String::new();
169 for ring in rings {
170 let pts = finite_pts(ring);
171 if pts.len() < 3 {
172 continue;
173 }
174 d.push('M');
175 d.push_str(&format!("{},{}", num(pts[0].0), num(pts[0].1)));
176 for p in &pts[1..] {
177 d.push('L');
178 d.push_str(&format!("{},{}", num(p.0), num(p.1)));
179 }
180 d.push('Z');
181 }
182 if d.is_empty() {
183 return Ok(());
184 }
185 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
186 let attrs = format!(
187 "d=\"{d}\" fill-rule=\"evenodd\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
188 opacity_attr("fill", style.alpha),
189 finite_or(style.stroke_width, 0.0)
190 );
191 self.push_mark("path", "band", &attrs);
192 Ok(())
193 }
194
195 fn pattern_defs(&self) -> String {
200 if self.patterns.is_empty() {
201 return String::new();
202 }
203 let mut defs = String::from("<defs>");
204 for (p, id) in &self.patterns {
205 let FillPattern::Stripe {
206 fg,
207 bg,
208 size_px,
209 angle_deg,
210 } = p;
211 let size = if *size_px > 0.0 { *size_px } else { 1.0 };
212 defs.push_str(&format!(
213 "<pattern id=\"{id}\" width=\"{size}\" height=\"{size}\" patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate({angle_deg})\"><rect width=\"{size}\" height=\"{size}\" fill=\"{}\"/><rect width=\"{:.3}\" height=\"{size}\" fill=\"{}\"/></pattern>",
214 rgb(*bg),
215 size * 0.3,
216 rgb(*fg),
217 ));
218 }
219 defs.push_str("</defs>");
220 defs
221 }
222
223 pub(crate) fn set_formatted_values(&mut self, on: bool) {
227 self.formatted_values = on;
228 }
229
230 pub fn set_root_attrs(&mut self, attrs: Vec<(String, String)>) {
234 self.root_attrs = attrs;
235 }
236
237 pub fn extend_root_attrs(&mut self, attrs: Vec<(String, String)>) {
245 self.root_attrs.extend(attrs);
246 }
247
248 pub(crate) fn without_plot_attr(&mut self) {
250 self.plot_attr = false;
251 }
252
253 pub(crate) fn push_raw(&mut self, markup: &str) {
256 self.body.push_str(markup);
257 }
258
259 pub(crate) fn body(&self) -> &str {
261 &self.body
262 }
263
264 pub(crate) fn wrap_body_in_group(&mut self, attrs: &[(String, String)]) {
271 if attrs.is_empty() {
272 return;
273 }
274 let mut open = String::from("<g");
275 for (k, v) in attrs {
276 open.push_str(&format!(" {k}=\"{}\"", escape(v)));
277 }
278 open.push('>');
279 self.body = format!("{open}{}</g>", self.body);
280 }
281
282 pub fn take_warnings(&mut self) -> Vec<String> {
284 std::mem::take(&mut self.warnings)
285 }
286
287 fn mark_class(&self, kind: &str) -> String {
290 match &self.mark_geom {
291 Some(g) => format!(" class=\"gg-mark gg-{kind}\" data-geom=\"{}\"", escape(g)),
292 None => String::new(),
293 }
294 }
295
296 fn push_mark(&mut self, tag: &str, kind: &str, attrs: &str) {
304 let mut pairs: Vec<(String, String)> = Vec::new();
314 for (name, val) in [
315 ("data-x", &self.axis_key),
316 ("data-series", &self.series_key),
317 ("data-value", &self.value_key),
318 ] {
319 if let Some(v) = val {
320 pairs.push((name.to_string(), v.clone()));
321 }
322 }
323 if let Some(fx) = &self.mark_fx {
327 if self.axis_key.as_deref() != Some(fx.as_str()) {
328 pairs.push(("data-fx".to_string(), fx.clone()));
329 }
330 }
331 if let Some(fvalue) = &self.mark_fvalue {
332 pairs.push(("data-fvalue".to_string(), fvalue.clone()));
333 }
334 for (name, val) in &self.mark_attrs {
335 if !is_valid_mark_attr_name(name) {
336 continue;
337 }
338 match pairs.iter_mut().find(|(n, _)| n == name) {
339 Some(existing) => existing.1 = val.clone(),
340 None => pairs.push((name.clone(), val.clone())),
341 }
342 }
343 if let Some(part) = &self.mark_part {
348 match pairs.iter_mut().find(|(n, _)| n == "data-part") {
349 Some(existing) => existing.1 = part.clone(),
350 None => pairs.push(("data-part".to_string(), part.clone())),
351 }
352 }
353 let mut data = String::new();
354 for (name, val) in &pairs {
355 data.push_str(&format!(" {name}=\"{}\"", escape(val)));
356 }
357 if self.mark_part.is_some() {
358 data.push_str(" aria-hidden=\"true\"");
359 }
360 let effective_kind: &str = self.mark_part.as_deref().unwrap_or(kind);
365 data.push_str(&self.mark_class(effective_kind));
366 let mark = if self.mark_part.is_some() {
367 format!("<{tag} {attrs}{data}/>")
368 } else {
369 match &self.tooltip {
370 Some(t) => format!("<{tag} {attrs}{data}><title>{}</title></{tag}>", escape(t)),
371 None => format!("<{tag} {attrs}{data}/>"),
372 }
373 };
374 self.body.push_str(&mark);
375 }
376
377 fn root_open(&self, prefix: &str) -> String {
378 let p = &self.plot_area;
379 let (w, h) = (self.total_area.width as i64, self.total_area.height as i64);
380 let mut extra = String::new();
381 for (k, v) in &self.root_attrs {
382 extra.push_str(&format!(" {k}=\"{}\"", escape(v)));
383 }
384 let plot = if self.plot_attr {
385 format!(
386 " data-plot=\"{} {} {} {}\"",
387 num(p.x),
388 num(p.y),
389 num(p.width),
390 num(p.height),
391 )
392 } else {
393 String::new()
394 };
395 format!("<svg {prefix}width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\"{plot}{extra}>")
396 }
397
398 pub fn finish(self) -> String {
402 let open = self.root_open("xmlns=\"http://www.w3.org/2000/svg\" ");
403 let defs = self.pattern_defs();
404 format!("{open}{defs}{}</svg>", self.body)
405 }
406
407 pub fn finish_fragment(self, x: f64, y: f64) -> String {
412 let open = self.root_open(&format!("x=\"{}\" y=\"{}\" ", num(x), num(y)));
413 let defs = self.pattern_defs();
414 format!("{open}{defs}{}</svg>", self.body)
415 }
416
417 fn draw_line_run(&mut self, pts: &[(f64, f64)], style: &LineStyle) {
420 let dash = match style
421 .linetype
422 .pattern()
423 .iter()
424 .flat_map(|(d, g)| [*d, *g])
425 .map(|v| format!("{v}"))
426 .collect::<Vec<_>>()
427 .join(",")
428 {
429 s if s.is_empty() => String::new(),
430 s => format!(" stroke-dasharray=\"{s}\""),
431 };
432 let linecap = match style.lineend {
435 LineEnd::Butt => "",
436 LineEnd::Round => " stroke-linecap=\"round\"",
437 LineEnd::Square => " stroke-linecap=\"square\"",
438 };
439 let attrs = format!(
440 "points=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
441 points(pts),
442 rgb(style.color),
443 finite_or(style.width, 1.0),
444 opacity_attr("stroke", style.alpha),
445 dash,
446 linecap
447 );
448 self.push_mark("polyline", "line", &attrs);
449 }
450}
451
452fn num(v: f64) -> String {
455 if v.is_finite() {
456 format!("{v:.2}")
457 } else {
458 "0".to_string()
459 }
460}
461
462fn compact_rings_d(rings: &[Vec<(f64, f64)>], precision: u8) -> String {
472 let mut d = String::new();
473 for ring in rings {
474 let pts = finite_pts(ring);
475 if pts.len() < 3 {
476 continue;
477 }
478 d.push('M');
479 push_num(&mut d, &fmt_compact(pts[0].0, precision));
480 push_num(&mut d, &fmt_compact(pts[0].1, precision));
481 if pts.len() > 1 {
482 d.push('l');
483 let mut prev = pts[0];
484 for p in &pts[1..] {
485 push_num(&mut d, &fmt_compact(p.0 - prev.0, precision));
486 push_num(&mut d, &fmt_compact(p.1 - prev.1, precision));
487 prev = *p;
488 }
489 }
490 d.push('z');
491 }
492 d
493}
494
495fn push_num(d: &mut String, s: &str) {
503 let needs_sep =
504 matches!(d.chars().last(), Some(c) if c.is_ascii_digit()) && !s.starts_with('-');
505 if needs_sep {
506 d.push(',');
507 }
508 d.push_str(s);
509}
510
511fn fmt_compact(v: f64, precision: u8) -> String {
517 if !v.is_finite() {
518 return "0".to_string();
519 }
520 let s = format!("{v:.*}", precision as usize);
521 let trimmed = if s.contains('.') {
522 s.trim_end_matches('0').trim_end_matches('.')
523 } else {
524 s.as_str()
525 };
526 match trimmed {
527 "" | "-" | "-0" => "0".to_string(),
528 other => other.to_string(),
529 }
530}
531
532fn finite_or(v: f64, fallback: f64) -> f64 {
533 if v.is_finite() {
534 v
535 } else {
536 fallback
537 }
538}
539
540fn opacity_attr(prefix: &str, alpha: f64) -> String {
546 let a = finite_or(alpha, 1.0);
547 if format!("{a:.3}") == "1.000" {
548 String::new()
549 } else {
550 format!(" {prefix}-opacity=\"{a:.3}\"")
551 }
552}
553
554pub fn root_data_attrs(built: &crate::build::BuiltPlot) -> Vec<(String, String)> {
571 use crate::aes::Aesthetic;
572 let mut out = Vec::new();
573 if !built.panel_scales.is_empty() {
574 return out;
575 }
576 let axis = |aes: &Aesthetic| -> (Option<(f64, f64)>, Option<String>) {
577 match built.scales.get(aes) {
578 Some(s) if s.is_discrete() => {
579 let labels: Vec<String> = s
580 .breaks()
581 .into_iter()
582 .map(|(_, l)| json_string(&l))
583 .collect();
584 (None, Some(format!("[{}]", labels.join(","))))
585 }
586 Some(s) => (s.expanded_domain(), None),
587 None => (None, None),
588 }
589 };
590 let (xd, xl) = axis(&Aesthetic::X);
591 let (yd, yl) = axis(&Aesthetic::Y);
592 if let (Some((x0, x1)), Some((y0, y1))) = (xd, yd) {
593 out.push((
594 "data-domain".into(),
595 format!("{} {} {} {}", g(x0), g(x1), g(y0), g(y1)),
596 ));
597 }
598 if let Some((a, b)) = xd {
599 out.push(("data-xdomain".into(), format!("{} {}", g(a), g(b))));
600 }
601 if let Some((a, b)) = yd {
602 out.push(("data-ydomain".into(), format!("{} {}", g(a), g(b))));
603 }
604 if let Some(l) = xl {
605 out.push(("data-xlevels".into(), l));
606 }
607 if let Some(l) = yl {
608 out.push(("data-ylevels".into(), l));
609 }
610 if built.coord.is_flipped() {
611 out.push(("data-flip".into(), "true".into()));
612 }
613 out
614}
615
616fn g(v: f64) -> String {
619 format!("{v}")
620}
621
622fn json_string(s: &str) -> String {
625 let mut out = String::with_capacity(s.len() + 2);
626 out.push('"');
627 for c in s.chars() {
628 match c {
629 '"' => out.push_str("\\\""),
630 '\\' => out.push_str("\\\\"),
631 '\n' => out.push_str("\\n"),
632 '\r' => out.push_str("\\r"),
633 '\t' => out.push_str("\\t"),
634 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
635 c => out.push(c),
636 }
637 }
638 out.push('"');
639 out
640}
641
642fn is_allowed_root_attr_name(name: &str) -> bool {
649 matches!(name, "role" | "id" | "class" | "lang")
650 || name
651 .strip_prefix("data-")
652 .is_some_and(crate::aes::is_valid_data_attr_name)
653 || name
654 .strip_prefix("aria-")
655 .is_some_and(crate::aes::is_valid_data_attr_name)
656}
657
658pub(crate) fn validate_root_attrs(
668 requested: &[(String, String)],
669 existing: &[(String, String)],
670 label: &str,
671 warnings: &mut Vec<String>,
672) -> Vec<(String, String)> {
673 let built_in: Vec<&str> = existing.iter().map(|(k, _)| k.as_str()).collect();
674 let mut used: Vec<&str> = built_in.clone();
675 let mut out = Vec::new();
676 for (name, value) in requested {
677 if !is_allowed_root_attr_name(name) {
678 warnings.push(format!(
679 "{label}: svg_attr(\"{name}\") is not on the allow-list (data-*, aria-*, role, id, class, lang) — ignored"
680 ));
681 continue;
682 }
683 if used.contains(&name.as_str()) {
684 if built_in.contains(&name.as_str()) {
689 warnings.push(format!(
690 "{label}: svg_attr(\"{name}\") collides with an attribute ggplot-rs already writes to the root — ignored"
691 ));
692 } else {
693 warnings.push(format!(
694 "{label}: svg_attr(\"{name}\") was already set by an earlier svg_attr call — ignored"
695 ));
696 }
697 continue;
698 }
699 used.push(name.as_str());
700 out.push((name.clone(), value.clone()));
701 }
702 out
703}
704
705pub(crate) fn warnings_json(warnings: &[String]) -> String {
709 let items: Vec<String> = warnings.iter().map(|w| json_string(w)).collect();
710 format!("[{}]", items.join(","))
711}
712
713fn is_valid_mark_attr_name(name: &str) -> bool {
721 match name.strip_prefix("data-") {
722 Some(suffix) => crate::aes::is_valid_data_attr_name(suffix),
723 None => false,
724 }
725}
726
727fn rgb((r, g, b): (u8, u8, u8)) -> String {
728 format!("#{r:02X}{g:02X}{b:02X}")
729}
730
731fn escape(s: &str) -> String {
738 let mut out = String::with_capacity(s.len());
739 for c in s.chars() {
740 match c {
741 '&' => out.push_str("&"),
742 '<' => out.push_str("<"),
743 '>' => out.push_str(">"),
744 '"' => out.push_str("""),
745 '\'' => out.push_str("'"),
746 '\t' | '\n' | '\r' => out.push(c),
747 c if (c as u32) < 0x20 || c == '\u{FFFE}' || c == '\u{FFFF}' => {}
748 c => out.push(c),
749 }
750 }
751 out
752}
753
754fn points(pts: &[(f64, f64)]) -> String {
755 pts.iter()
756 .map(|(x, y)| format!("{x:.2},{y:.2}"))
757 .collect::<Vec<_>>()
758 .join(" ")
759}
760
761fn finite_pts(pts: &[(f64, f64)]) -> Vec<(f64, f64)> {
762 pts.iter()
763 .copied()
764 .filter(|(x, y)| x.is_finite() && y.is_finite())
765 .collect()
766}
767
768fn finite_runs(pts: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
777 let mut runs = Vec::new();
778 let mut cur: Vec<(f64, f64)> = Vec::new();
779 for &(x, y) in pts {
780 if x.is_finite() && y.is_finite() {
781 cur.push((x, y));
782 } else if !cur.is_empty() {
783 if cur.len() >= 2 {
784 runs.push(std::mem::take(&mut cur));
785 } else {
786 cur.clear();
787 }
788 }
789 }
790 if cur.len() >= 2 {
791 runs.push(cur);
792 }
793 runs
794}
795
796impl DrawBackend for SvgBackend {
797 fn plot_area(&self) -> Rect {
798 self.plot_area.clone()
799 }
800 fn total_area(&self) -> Rect {
801 self.total_area.clone()
802 }
803
804 fn set_tooltip(&mut self, tooltip: Option<String>) {
805 self.tooltip = tooltip;
806 }
807
808 fn set_mark_axis(&mut self, key: Option<String>) {
809 self.axis_key = key;
810 }
811
812 fn set_mark_series(&mut self, series: Option<String>) {
813 self.series_key = series;
814 }
815
816 fn set_mark_value(&mut self, value: Option<String>) {
817 self.value_key = value;
818 }
819
820 fn set_mark_geom(&mut self, geom: Option<&str>) {
821 self.mark_geom = geom.map(str::to_string);
822 }
823
824 fn warn(&mut self, message: String) {
825 self.warnings.push(message);
826 }
827
828 fn set_mark_part(&mut self, part: Option<&str>) {
829 self.mark_part = part.map(|s| s.to_string());
830 }
831
832 fn set_mark_attrs(&mut self, attrs: Vec<(String, String)>) {
833 self.mark_attrs = attrs;
834 }
835
836 fn wants_formatted_values(&self) -> bool {
837 self.formatted_values
838 }
839
840 fn set_mark_fvalue(&mut self, value: Option<String>) {
841 self.mark_fvalue = value;
842 }
843
844 fn set_mark_fx(&mut self, value: Option<String>) {
845 self.mark_fx = value;
846 }
847
848 fn draw_circle(
849 &mut self,
850 (cx, cy): (f64, f64),
851 radius: f64,
852 style: &PointStyle,
853 ) -> Result<(), RenderError> {
854 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
855 return Ok(());
856 }
857 let attrs = format!(
858 "cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"{:.2}\" fill=\"{}\"{}",
859 radius.max(0.0),
860 rgb(style.color),
861 opacity_attr("fill", style.alpha)
862 );
863 self.push_mark("circle", "point", &attrs);
864 Ok(())
865 }
866
867 fn draw_shape(
871 &mut self,
872 (cx, cy): (f64, f64),
873 radius: f64,
874 style: &PointStyle,
875 ) -> Result<(), RenderError> {
876 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
877 return Ok(());
878 }
879 let r = radius.max(0.0);
880 let color = rgb(style.color);
881 let fill_opacity = opacity_attr("fill", style.alpha);
882 let stroke_opacity = opacity_attr("stroke", style.alpha);
883 let poly = |pts: &[(f64, f64)]| {
884 if style.filled {
885 format!("points=\"{}\" fill=\"{color}\"{fill_opacity}", points(pts))
886 } else {
887 format!(
888 "points=\"{}\" fill=\"none\" stroke=\"{color}\"{stroke_opacity}",
889 points(pts)
890 )
891 }
892 };
893 match style.shape {
894 PointShape::Circle => return self.draw_circle((cx, cy), radius, style),
895 PointShape::Square => {
896 let a = poly(&[
897 (cx - r, cy - r),
898 (cx + r, cy - r),
899 (cx + r, cy + r),
900 (cx - r, cy + r),
901 ]);
902 self.push_mark("polygon", "point", &a);
903 }
904 PointShape::Triangle => {
905 let a = poly(&[(cx, cy - r), (cx + r, cy + r), (cx - r, cy + r)]);
906 self.push_mark("polygon", "point", &a);
907 }
908 PointShape::Diamond => {
909 let a = poly(&[(cx, cy - r), (cx + r, cy), (cx, cy + r), (cx - r, cy)]);
910 self.push_mark("polygon", "point", &a);
911 }
912 PointShape::Plus | PointShape::Cross => {
913 let d = if style.shape == PointShape::Plus {
914 format!(
915 "M{:.2} {cy:.2}H{:.2}M{cx:.2} {:.2}V{:.2}",
916 cx - r,
917 cx + r,
918 cy - r,
919 cy + r
920 )
921 } else {
922 format!(
923 "M{:.2} {:.2}L{:.2} {:.2}M{:.2} {:.2}L{:.2} {:.2}",
924 cx - r,
925 cy - r,
926 cx + r,
927 cy + r,
928 cx - r,
929 cy + r,
930 cx + r,
931 cy - r
932 )
933 };
934 let a = format!(
935 "d=\"{d}\" fill=\"none\" stroke=\"{color}\" stroke-width=\"{:.2}\"{stroke_opacity}",
936 (r / 2.5).clamp(1.0, 3.0)
937 );
938 self.push_mark("path", "point", &a);
939 }
940 }
941 Ok(())
942 }
943
944 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
945 for run in finite_runs(pts) {
951 self.draw_line_run(&run, style);
952 }
953 Ok(())
954 }
955
956 fn draw_quadratic(
959 &mut self,
960 p0: (f64, f64),
961 ctrl: (f64, f64),
962 p1: (f64, f64),
963 style: &LineStyle,
964 ) -> Result<(), RenderError> {
965 if !(p0.0.is_finite()
966 && p0.1.is_finite()
967 && ctrl.0.is_finite()
968 && ctrl.1.is_finite()
969 && p1.0.is_finite()
970 && p1.1.is_finite())
971 {
972 return Ok(());
973 }
974 let dash = match style
975 .linetype
976 .pattern()
977 .iter()
978 .flat_map(|(d, g)| [*d, *g])
979 .map(|v| format!("{v}"))
980 .collect::<Vec<_>>()
981 .join(",")
982 {
983 s if s.is_empty() => String::new(),
984 s => format!(" stroke-dasharray=\"{s}\""),
985 };
986 let linecap = match style.lineend {
987 LineEnd::Butt => "",
988 LineEnd::Round => " stroke-linecap=\"round\"",
989 LineEnd::Square => " stroke-linecap=\"square\"",
990 };
991 let d = format!(
992 "M{:.2} {:.2} Q{:.2} {:.2} {:.2} {:.2}",
993 p0.0, p0.1, ctrl.0, ctrl.1, p1.0, p1.1
994 );
995 let attrs = format!(
996 "d=\"{d}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
997 rgb(style.color),
998 finite_or(style.width, 1.0),
999 opacity_attr("stroke", style.alpha),
1000 dash,
1001 linecap
1002 );
1003 self.push_mark("path", "line", &attrs);
1004 Ok(())
1005 }
1006
1007 fn draw_rect(
1008 &mut self,
1009 top_left: (f64, f64),
1010 bottom_right: (f64, f64),
1011 style: &RectStyle,
1012 ) -> Result<(), RenderError> {
1013 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1014 self.rect_markup(top_left, bottom_right, &fill, style)
1015 }
1016
1017 fn draw_rect_pattern(
1020 &mut self,
1021 top_left: (f64, f64),
1022 bottom_right: (f64, f64),
1023 pattern: &FillPattern,
1024 style: &RectStyle,
1025 ) -> Result<(), RenderError> {
1026 let fill = self.pattern_url(pattern);
1027 self.rect_markup(top_left, bottom_right, &fill, style)
1028 }
1029
1030 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
1031 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1032 self.polygon_markup(pts, &fill, style)
1033 }
1034
1035 fn draw_polygon_pattern(
1038 &mut self,
1039 pts: &[(f64, f64)],
1040 pattern: &FillPattern,
1041 style: &RectStyle,
1042 ) -> Result<(), RenderError> {
1043 let fill = self.pattern_url(pattern);
1044 self.polygon_markup(pts, &fill, style)
1045 }
1046
1047 fn draw_polygon_rings(
1053 &mut self,
1054 rings: &[Vec<(f64, f64)>],
1055 style: &RectStyle,
1056 ) -> Result<(), RenderError> {
1057 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1058 self.polygon_rings_markup(rings, &fill, style)
1059 }
1060
1061 fn draw_polygon_rings_pattern(
1065 &mut self,
1066 rings: &[Vec<(f64, f64)>],
1067 pattern: &FillPattern,
1068 style: &RectStyle,
1069 ) -> Result<(), RenderError> {
1070 let fill = self.pattern_url(pattern);
1071 self.polygon_rings_markup(rings, &fill, style)
1072 }
1073
1074 fn draw_polygon_rings_precise(
1080 &mut self,
1081 rings: &[Vec<(f64, f64)>],
1082 pattern: Option<&FillPattern>,
1083 precision: Option<u8>,
1084 style: &RectStyle,
1085 ) -> Result<(), RenderError> {
1086 let Some(precision) = precision else {
1087 return match pattern {
1088 Some(p) => self.draw_polygon_rings_pattern(rings, p, style),
1089 None => self.draw_polygon_rings(rings, style),
1090 };
1091 };
1092 let d = compact_rings_d(rings, precision);
1093 if d.is_empty() {
1094 return Ok(());
1095 }
1096 let fill = match pattern {
1097 Some(p) => self.pattern_url(p),
1098 None => style.fill.map(rgb).unwrap_or_else(|| "none".into()),
1099 };
1100 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
1101 let attrs = format!(
1102 "d=\"{d}\" fill-rule=\"evenodd\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
1103 opacity_attr("fill", style.alpha),
1104 finite_or(style.stroke_width, 0.0)
1105 );
1106 self.push_mark("path", "band", &attrs);
1107 Ok(())
1108 }
1109
1110 fn draw_text(
1111 &mut self,
1112 text: &str,
1113 pos: (f64, f64),
1114 style: &TextStyle,
1115 ) -> Result<(), RenderError> {
1116 self.draw_text_with_title(text, pos, style, None)
1117 }
1118
1119 fn draw_text_with_title(
1123 &mut self,
1124 text: &str,
1125 (x, y): (f64, f64),
1126 style: &TextStyle,
1127 title: Option<&str>,
1128 ) -> Result<(), RenderError> {
1129 if !(x.is_finite() && y.is_finite()) {
1130 return Ok(());
1131 }
1132 let anchor = match style.anchor {
1133 TextAnchor::Start => "start",
1134 TextAnchor::Middle => "middle",
1135 TextAnchor::End => "end",
1136 };
1137 let family = escape(style.family.as_deref().unwrap_or("sans-serif"));
1138 let weight = if style.face == FontFace::Bold {
1139 " font-weight=\"bold\""
1140 } else {
1141 ""
1142 };
1143 let fstyle = if style.face == FontFace::Italic {
1144 " font-style=\"italic\""
1145 } else {
1146 ""
1147 };
1148 let angle = finite_or(style.angle, 0.0);
1149 let transform = if angle.abs() > 0.01 {
1150 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
1151 } else {
1152 String::new()
1153 };
1154 let class = self.mark_class("text");
1155 let title_tag = match title {
1156 Some(t) => format!("<title>{}</title>", escape(t)),
1157 None => String::new(),
1158 };
1159 self.body.push_str(&format!(
1160 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
1161 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle} fill=\"{}\"{transform}{class}>{title_tag}{}</text>",
1162 finite_or(style.size, 12.0),
1163 rgb(style.color),
1164 escape(text)
1165 ));
1166 Ok(())
1167 }
1168
1169 fn draw_text_runs(
1176 &mut self,
1177 runs: &[(&str, Option<&str>)],
1178 (x, y): (f64, f64),
1179 style: &TextStyle,
1180 ) -> Result<(), RenderError> {
1181 if !(x.is_finite() && y.is_finite()) {
1182 return Ok(());
1183 }
1184 let anchor = match style.anchor {
1185 TextAnchor::Start => "start",
1186 TextAnchor::Middle => "middle",
1187 TextAnchor::End => "end",
1188 };
1189 let family = escape(style.family.as_deref().unwrap_or("sans-serif"));
1190 let weight = if style.face == FontFace::Bold {
1191 " font-weight=\"bold\""
1192 } else {
1193 ""
1194 };
1195 let fstyle = if style.face == FontFace::Italic {
1196 " font-style=\"italic\""
1197 } else {
1198 ""
1199 };
1200 let angle = finite_or(style.angle, 0.0);
1201 let transform = if angle.abs() > 0.01 {
1202 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
1203 } else {
1204 String::new()
1205 };
1206 let class = self.mark_class("text");
1207 let mut inner = String::new();
1208 for (text, href) in runs {
1209 match href {
1210 Some(u) => inner.push_str(&format!(
1211 "<a href=\"{}\" target=\"_blank\" rel=\"noopener\"><tspan text-decoration=\"underline\">{}</tspan></a>",
1212 escape(u),
1213 escape(text)
1214 )),
1215 None => inner.push_str(&format!("<tspan>{}</tspan>", escape(text))),
1216 }
1217 }
1218 self.body.push_str(&format!(
1219 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
1220 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle} fill=\"{}\"{transform}{class}>{inner}</text>",
1221 finite_or(style.size, 12.0),
1222 rgb(style.color),
1223 ));
1224 Ok(())
1225 }
1226}
1227
1228#[cfg(test)]
1229mod tests {
1230 use super::*;
1231 use crate::render::Rect;
1232
1233 fn backend() -> SvgBackend {
1234 SvgBackend::new(
1235 100,
1236 100,
1237 Rect {
1238 x: 0.0,
1239 y: 0.0,
1240 width: 100.0,
1241 height: 100.0,
1242 },
1243 )
1244 }
1245
1246 #[test]
1247 fn draw_quadratic_emits_a_path_with_m_and_q() {
1248 let mut b = backend();
1249 b.draw_quadratic((0.0, 0.0), (10.0, 20.0), (20.0, 0.0), &LineStyle::default())
1250 .unwrap();
1251 let svg = b.finish();
1252 assert!(svg.contains("<path"));
1253 assert!(svg.contains("d=\"M0.00 0.00 Q10.00 20.00 20.00 0.00\""));
1254 assert!(!svg.contains("NaN"));
1255 assert!(!svg.contains("inf"));
1256 }
1257
1258 #[test]
1259 fn draw_quadratic_drops_non_finite_input() {
1260 let mut b = backend();
1261 b.draw_quadratic(
1262 (f64::NAN, 0.0),
1263 (10.0, 20.0),
1264 (20.0, 0.0),
1265 &LineStyle::default(),
1266 )
1267 .unwrap();
1268 b.draw_quadratic(
1269 (0.0, 0.0),
1270 (f64::INFINITY, 20.0),
1271 (20.0, 0.0),
1272 &LineStyle::default(),
1273 )
1274 .unwrap();
1275 let svg = b.finish();
1276 assert!(!svg.contains("<path"));
1277 }
1278
1279 #[test]
1280 fn draw_line_omits_linecap_by_default_but_writes_it_when_set() {
1281 let mut b = backend();
1282 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &LineStyle::default())
1283 .unwrap();
1284 let svg = b.finish();
1285 assert!(!svg.contains("stroke-linecap"));
1286
1287 let mut b2 = backend();
1288 let style = LineStyle {
1289 lineend: LineEnd::Round,
1290 ..Default::default()
1291 };
1292 b2.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
1293 let svg2 = b2.finish();
1294 assert!(svg2.contains("stroke-linecap=\"round\""));
1295 }
1296
1297 #[test]
1298 fn square_linecap_is_written_on_both_draw_line_and_draw_quadratic() {
1299 let style = LineStyle {
1300 lineend: LineEnd::Square,
1301 ..Default::default()
1302 };
1303 let mut b = backend();
1304 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
1305 assert!(b.finish().contains("stroke-linecap=\"square\""));
1306
1307 let mut b2 = backend();
1308 b2.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &style)
1309 .unwrap();
1310 assert!(b2.finish().contains("stroke-linecap=\"square\""));
1311 }
1312
1313 #[test]
1318 fn set_mark_attrs_defence_in_depth_drops_invalid_names() {
1319 let mut b = backend();
1320 b.set_mark_attrs(vec![
1321 ("not-data-prefixed".to_string(), "x".to_string()),
1322 ("data-".to_string(), "x".to_string()), ("data-Upper".to_string(), "x".to_string()), ("data-ok".to_string(), "valid".to_string()), ]);
1326 b.draw_circle((1.0, 1.0), 1.0, &PointStyle::default())
1327 .unwrap();
1328 let svg = b.finish();
1329 assert!(!svg.contains("not-data-prefixed"));
1330 assert!(!svg.contains("data-Upper"));
1331 assert!(!svg.contains("data-=\""));
1332 assert!(svg.contains("data-ok=\"valid\""));
1333 }
1334
1335 fn stripe() -> FillPattern {
1338 FillPattern::Stripe {
1339 fg: (10, 20, 30),
1340 bg: (200, 210, 220),
1341 size_px: 4.0,
1342 angle_deg: 45.0,
1343 }
1344 }
1345
1346 fn pattern_id(svg: &str) -> String {
1347 let i = svg.find("<pattern id=\"").unwrap() + "<pattern id=\"".len();
1348 svg[i..i + svg[i..].find('"').unwrap()].to_string()
1349 }
1350
1351 #[test]
1352 fn a_pattern_fill_registers_one_defs_pattern_and_references_it() {
1353 let mut b = backend();
1354 b.draw_rect_pattern((0.0, 0.0), (10.0, 10.0), &stripe(), &RectStyle::default())
1355 .unwrap();
1356 let svg = b.finish();
1357 assert_eq!(svg.matches("<pattern ").count(), 1, "{svg}");
1358 let id = pattern_id(&svg);
1359 assert!(
1360 svg.contains(&format!("fill=\"url(#{id})\"")),
1361 "rect references the registered pattern id {id}: {svg}"
1362 );
1363 }
1364
1365 #[test]
1366 fn a_rounded_rect_pattern_still_rounds_its_corners() {
1367 let mut b = backend();
1368 b.draw_rect_pattern(
1369 (0.0, 0.0),
1370 (10.0, 10.0),
1371 &stripe(),
1372 &RectStyle {
1373 corner_radius: 3.0,
1374 ..Default::default()
1375 },
1376 )
1377 .unwrap();
1378 let svg = b.finish();
1379 assert!(svg.contains("rx=\"3.00\""), "{svg}");
1380 assert!(svg.contains("fill=\"url(#"), "{svg}");
1381 }
1382
1383 #[test]
1384 fn an_equal_pattern_reuses_the_same_defs_entry() {
1385 let mut b = backend();
1386 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1387 .unwrap();
1388 b.draw_polygon_pattern(
1389 &[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)],
1390 &stripe(),
1391 &RectStyle::default(),
1392 )
1393 .unwrap();
1394 let svg = b.finish();
1395 assert_eq!(
1396 svg.matches("<pattern ").count(),
1397 1,
1398 "two equal patterns share one <defs> entry: {svg}"
1399 );
1400 }
1401
1402 #[test]
1403 fn different_patterns_get_their_own_defs_entries() {
1404 let mut b = backend();
1405 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1406 .unwrap();
1407 let other = FillPattern::Stripe {
1408 fg: (1, 1, 1),
1409 bg: (2, 2, 2),
1410 size_px: 8.0,
1411 angle_deg: 0.0,
1412 };
1413 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &other, &RectStyle::default())
1414 .unwrap();
1415 let svg = b.finish();
1416 assert_eq!(svg.matches("<pattern ").count(), 2, "{svg}");
1417 }
1418
1419 #[test]
1420 fn no_pattern_used_means_no_defs_block_at_all() {
1421 let mut b = backend();
1422 b.draw_rect((0.0, 0.0), (5.0, 5.0), &RectStyle::default())
1423 .unwrap();
1424 let svg = b.finish();
1425 assert!(!svg.contains("<defs>"), "{svg}");
1426 }
1427
1428 #[test]
1429 fn pattern_fill_cuts_out_holes_like_a_flat_fill_does() {
1430 let mut b = backend();
1431 let outer = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1432 let hole = vec![(2.0, 2.0), (8.0, 2.0), (8.0, 8.0), (2.0, 8.0)];
1433 b.draw_polygon_rings_pattern(&[outer, hole], &stripe(), &RectStyle::default())
1434 .unwrap();
1435 let svg = b.finish();
1436 assert!(svg.contains("fill-rule=\"evenodd\""), "{svg}");
1437 assert_eq!(
1438 svg.matches("<path").count(),
1439 1,
1440 "one path, both rings: {svg}"
1441 );
1442 }
1443
1444 #[test]
1445 fn two_backends_on_one_page_never_collide_on_a_pattern_id() {
1446 let mut a = backend();
1447 a.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1448 .unwrap();
1449 let mut b = backend();
1450 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1451 .unwrap();
1452 let (sa, sb) = (a.finish(), b.finish());
1453 assert_ne!(pattern_id(&sa), pattern_id(&sb), "{sa}\n{sb}");
1454 }
1455
1456 fn decode_compact_d(d: &str) -> Vec<Vec<(f64, f64)>> {
1463 let b = d.as_bytes();
1464 let mut i = 0;
1465 let read_num = |b: &[u8], i: &mut usize| -> f64 {
1466 let start = *i;
1467 if *i < b.len() && b[*i] == b'-' {
1468 *i += 1;
1469 }
1470 while *i < b.len() && b[*i].is_ascii_digit() {
1471 *i += 1;
1472 }
1473 if *i < b.len() && b[*i] == b'.' {
1474 *i += 1;
1475 while *i < b.len() && b[*i].is_ascii_digit() {
1476 *i += 1;
1477 }
1478 }
1479 std::str::from_utf8(&b[start..*i]).unwrap().parse().unwrap()
1480 };
1481 let is_num_start = |c: u8| c.is_ascii_digit() || c == b'-' || c == b'.';
1482 let mut rings = Vec::new();
1483 let mut current = Vec::new();
1484 let mut cursor = (0.0, 0.0);
1485 while i < b.len() {
1486 match b[i] {
1487 b'M' => {
1488 if !current.is_empty() {
1489 rings.push(std::mem::take(&mut current));
1490 }
1491 i += 1;
1492 let x = read_num(b, &mut i);
1493 if i < b.len() && b[i] == b',' {
1494 i += 1;
1495 }
1496 let y = read_num(b, &mut i);
1497 cursor = (x, y);
1498 current.push(cursor);
1499 }
1500 b'l' => {
1501 i += 1;
1502 while i < b.len() && is_num_start(b[i]) {
1503 let dx = read_num(b, &mut i);
1504 if i < b.len() && b[i] == b',' {
1505 i += 1;
1506 }
1507 let dy = read_num(b, &mut i);
1508 if i < b.len() && b[i] == b',' {
1509 i += 1;
1510 }
1511 cursor = (cursor.0 + dx, cursor.1 + dy);
1512 current.push(cursor);
1513 }
1514 }
1515 b'z' => {
1516 i += 1;
1517 if !current.is_empty() {
1518 rings.push(std::mem::take(&mut current));
1519 }
1520 }
1521 _ => i += 1,
1522 }
1523 }
1524 if !current.is_empty() {
1525 rings.push(current);
1526 }
1527 rings
1528 }
1529
1530 fn noisy_ring(n: usize, cx: f64, cy: f64, r: f64) -> Vec<(f64, f64)> {
1533 (0..n)
1534 .map(|i| {
1535 let t = i as f64 / n as f64 * std::f64::consts::TAU;
1536 let wobble = 1.0 + 0.03 * ((i * 7) as f64).sin();
1537 (cx + r * wobble * t.cos(), cy + r * wobble * t.sin())
1538 })
1539 .collect()
1540 }
1541
1542 #[test]
1543 fn compact_encoding_is_well_under_the_absolute_encodings_size() {
1544 let ring = noisy_ring(2000, 400.0, 300.0, 280.0);
1548 let mut b = backend();
1549 b.draw_polygon_rings(std::slice::from_ref(&ring), &RectStyle::default())
1550 .unwrap();
1551 let absolute_svg = b.finish();
1552 let absolute_d = absolute_svg
1553 .split("d=\"")
1554 .nth(1)
1555 .and_then(|s| s.split('"').next())
1556 .expect("absolute path d");
1557
1558 let compact_d = compact_rings_d(std::slice::from_ref(&ring), 1);
1559
1560 assert!(
1561 compact_d.len() * 3 < absolute_d.len() * 2,
1562 "compact {} bytes should be well under 2/3 of absolute {} bytes",
1563 compact_d.len(),
1564 absolute_d.len()
1565 );
1566 }
1567
1568 #[test]
1569 fn compact_encoding_round_trips_within_precision() {
1570 let ring = noisy_ring(40, 100.0, 80.0, 60.0);
1571 let precision: u8 = 1;
1572 let d = compact_rings_d(std::slice::from_ref(&ring), precision);
1573 let decoded = decode_compact_d(&d);
1574 assert_eq!(decoded.len(), 1, "{d}");
1575 assert_eq!(decoded[0].len(), ring.len(), "{d}");
1576 let step_tol = 0.5 * 10f64.powi(-(precision as i32));
1581 for (k, (orig, got)) in ring.iter().zip(decoded[0].iter()).enumerate() {
1582 let tol = step_tol * (k as f64 + 1.0) + 1e-9;
1583 assert!(
1584 (orig.0 - got.0).abs() <= tol && (orig.1 - got.1).abs() <= tol,
1585 "point {k}: {orig:?} vs {got:?} (tol {tol}): {d}"
1586 );
1587 }
1588 }
1589
1590 #[test]
1591 fn compact_encoding_round_trips_a_hole_and_a_multipolygons_parts() {
1592 let part1 = vec![(0.0, 0.0), (20.0, 0.0), (20.0, 20.0), (0.0, 20.0)];
1596 let part2_outer = vec![(50.0, 0.0), (90.0, 0.0), (90.0, 40.0), (50.0, 40.0)];
1597 let part2_hole = vec![(60.0, 10.0), (80.0, 10.0), (80.0, 30.0), (60.0, 30.0)];
1598 let rings = vec![part1.clone(), part2_outer.clone(), part2_hole.clone()];
1599 let d = compact_rings_d(&rings, 1);
1600 let decoded = decode_compact_d(&d);
1601 assert_eq!(decoded.len(), 3, "{d}");
1602 for (orig, got) in [part1, part2_outer, part2_hole].iter().zip(decoded.iter()) {
1603 assert_eq!(orig.len(), got.len(), "{d}");
1604 for (o, g) in orig.iter().zip(got.iter()) {
1605 assert!((o.0 - g.0).abs() <= 0.1 && (o.1 - g.1).abs() <= 0.1, "{d}");
1606 }
1607 }
1608 assert!(d.contains("M0,0l20,0,0,20-20,0z"), "{d}");
1609 }
1610}