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 fn take_warnings(&mut self) -> Vec<String> {
266 std::mem::take(&mut self.warnings)
267 }
268
269 fn mark_class(&self, kind: &str) -> String {
272 match &self.mark_geom {
273 Some(g) => format!(" class=\"gg-mark gg-{kind}\" data-geom=\"{}\"", escape(g)),
274 None => String::new(),
275 }
276 }
277
278 fn push_mark(&mut self, tag: &str, kind: &str, attrs: &str) {
286 let mut pairs: Vec<(String, String)> = Vec::new();
296 for (name, val) in [
297 ("data-x", &self.axis_key),
298 ("data-series", &self.series_key),
299 ("data-value", &self.value_key),
300 ] {
301 if let Some(v) = val {
302 pairs.push((name.to_string(), v.clone()));
303 }
304 }
305 if let Some(fx) = &self.mark_fx {
309 if self.axis_key.as_deref() != Some(fx.as_str()) {
310 pairs.push(("data-fx".to_string(), fx.clone()));
311 }
312 }
313 if let Some(fvalue) = &self.mark_fvalue {
314 pairs.push(("data-fvalue".to_string(), fvalue.clone()));
315 }
316 for (name, val) in &self.mark_attrs {
317 if !is_valid_mark_attr_name(name) {
318 continue;
319 }
320 match pairs.iter_mut().find(|(n, _)| n == name) {
321 Some(existing) => existing.1 = val.clone(),
322 None => pairs.push((name.clone(), val.clone())),
323 }
324 }
325 if let Some(part) = &self.mark_part {
330 match pairs.iter_mut().find(|(n, _)| n == "data-part") {
331 Some(existing) => existing.1 = part.clone(),
332 None => pairs.push(("data-part".to_string(), part.clone())),
333 }
334 }
335 let mut data = String::new();
336 for (name, val) in &pairs {
337 data.push_str(&format!(" {name}=\"{}\"", escape(val)));
338 }
339 if self.mark_part.is_some() {
340 data.push_str(" aria-hidden=\"true\"");
341 }
342 let effective_kind: &str = self.mark_part.as_deref().unwrap_or(kind);
347 data.push_str(&self.mark_class(effective_kind));
348 let mark = if self.mark_part.is_some() {
349 format!("<{tag} {attrs}{data}/>")
350 } else {
351 match &self.tooltip {
352 Some(t) => format!("<{tag} {attrs}{data}><title>{}</title></{tag}>", escape(t)),
353 None => format!("<{tag} {attrs}{data}/>"),
354 }
355 };
356 self.body.push_str(&mark);
357 }
358
359 fn root_open(&self, prefix: &str) -> String {
360 let p = &self.plot_area;
361 let (w, h) = (self.total_area.width as i64, self.total_area.height as i64);
362 let mut extra = String::new();
363 for (k, v) in &self.root_attrs {
364 extra.push_str(&format!(" {k}=\"{}\"", escape(v)));
365 }
366 let plot = if self.plot_attr {
367 format!(
368 " data-plot=\"{} {} {} {}\"",
369 num(p.x),
370 num(p.y),
371 num(p.width),
372 num(p.height),
373 )
374 } else {
375 String::new()
376 };
377 format!("<svg {prefix}width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\"{plot}{extra}>")
378 }
379
380 pub fn finish(self) -> String {
384 let open = self.root_open("xmlns=\"http://www.w3.org/2000/svg\" ");
385 let defs = self.pattern_defs();
386 format!("{open}{defs}{}</svg>", self.body)
387 }
388
389 pub fn finish_fragment(self, x: f64, y: f64) -> String {
394 let open = self.root_open(&format!("x=\"{}\" y=\"{}\" ", num(x), num(y)));
395 let defs = self.pattern_defs();
396 format!("{open}{defs}{}</svg>", self.body)
397 }
398
399 fn draw_line_run(&mut self, pts: &[(f64, f64)], style: &LineStyle) {
402 let dash = match style
403 .linetype
404 .pattern()
405 .iter()
406 .flat_map(|(d, g)| [*d, *g])
407 .map(|v| format!("{v}"))
408 .collect::<Vec<_>>()
409 .join(",")
410 {
411 s if s.is_empty() => String::new(),
412 s => format!(" stroke-dasharray=\"{s}\""),
413 };
414 let linecap = match style.lineend {
417 LineEnd::Butt => "",
418 LineEnd::Round => " stroke-linecap=\"round\"",
419 LineEnd::Square => " stroke-linecap=\"square\"",
420 };
421 let attrs = format!(
422 "points=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
423 points(pts),
424 rgb(style.color),
425 finite_or(style.width, 1.0),
426 opacity_attr("stroke", style.alpha),
427 dash,
428 linecap
429 );
430 self.push_mark("polyline", "line", &attrs);
431 }
432}
433
434fn num(v: f64) -> String {
437 if v.is_finite() {
438 format!("{v:.2}")
439 } else {
440 "0".to_string()
441 }
442}
443
444fn finite_or(v: f64, fallback: f64) -> f64 {
445 if v.is_finite() {
446 v
447 } else {
448 fallback
449 }
450}
451
452fn opacity_attr(prefix: &str, alpha: f64) -> String {
458 let a = finite_or(alpha, 1.0);
459 if format!("{a:.3}") == "1.000" {
460 String::new()
461 } else {
462 format!(" {prefix}-opacity=\"{a:.3}\"")
463 }
464}
465
466pub fn root_data_attrs(built: &crate::build::BuiltPlot) -> Vec<(String, String)> {
483 use crate::aes::Aesthetic;
484 let mut out = Vec::new();
485 if !built.panel_scales.is_empty() {
486 return out;
487 }
488 let axis = |aes: &Aesthetic| -> (Option<(f64, f64)>, Option<String>) {
489 match built.scales.get(aes) {
490 Some(s) if s.is_discrete() => {
491 let labels: Vec<String> = s
492 .breaks()
493 .into_iter()
494 .map(|(_, l)| json_string(&l))
495 .collect();
496 (None, Some(format!("[{}]", labels.join(","))))
497 }
498 Some(s) => (s.expanded_domain(), None),
499 None => (None, None),
500 }
501 };
502 let (xd, xl) = axis(&Aesthetic::X);
503 let (yd, yl) = axis(&Aesthetic::Y);
504 if let (Some((x0, x1)), Some((y0, y1))) = (xd, yd) {
505 out.push((
506 "data-domain".into(),
507 format!("{} {} {} {}", g(x0), g(x1), g(y0), g(y1)),
508 ));
509 }
510 if let Some((a, b)) = xd {
511 out.push(("data-xdomain".into(), format!("{} {}", g(a), g(b))));
512 }
513 if let Some((a, b)) = yd {
514 out.push(("data-ydomain".into(), format!("{} {}", g(a), g(b))));
515 }
516 if let Some(l) = xl {
517 out.push(("data-xlevels".into(), l));
518 }
519 if let Some(l) = yl {
520 out.push(("data-ylevels".into(), l));
521 }
522 if built.coord.is_flipped() {
523 out.push(("data-flip".into(), "true".into()));
524 }
525 out
526}
527
528fn g(v: f64) -> String {
531 format!("{v}")
532}
533
534fn json_string(s: &str) -> String {
537 let mut out = String::with_capacity(s.len() + 2);
538 out.push('"');
539 for c in s.chars() {
540 match c {
541 '"' => out.push_str("\\\""),
542 '\\' => out.push_str("\\\\"),
543 '\n' => out.push_str("\\n"),
544 '\r' => out.push_str("\\r"),
545 '\t' => out.push_str("\\t"),
546 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
547 c => out.push(c),
548 }
549 }
550 out.push('"');
551 out
552}
553
554fn is_allowed_root_attr_name(name: &str) -> bool {
561 matches!(name, "role" | "id" | "class" | "lang")
562 || name
563 .strip_prefix("data-")
564 .is_some_and(crate::aes::is_valid_data_attr_name)
565 || name
566 .strip_prefix("aria-")
567 .is_some_and(crate::aes::is_valid_data_attr_name)
568}
569
570pub(crate) fn validate_root_attrs(
580 requested: &[(String, String)],
581 existing: &[(String, String)],
582 label: &str,
583 warnings: &mut Vec<String>,
584) -> Vec<(String, String)> {
585 let built_in: Vec<&str> = existing.iter().map(|(k, _)| k.as_str()).collect();
586 let mut used: Vec<&str> = built_in.clone();
587 let mut out = Vec::new();
588 for (name, value) in requested {
589 if !is_allowed_root_attr_name(name) {
590 warnings.push(format!(
591 "{label}: svg_attr(\"{name}\") is not on the allow-list (data-*, aria-*, role, id, class, lang) — ignored"
592 ));
593 continue;
594 }
595 if used.contains(&name.as_str()) {
596 if built_in.contains(&name.as_str()) {
601 warnings.push(format!(
602 "{label}: svg_attr(\"{name}\") collides with an attribute ggplot-rs already writes to the root — ignored"
603 ));
604 } else {
605 warnings.push(format!(
606 "{label}: svg_attr(\"{name}\") was already set by an earlier svg_attr call — ignored"
607 ));
608 }
609 continue;
610 }
611 used.push(name.as_str());
612 out.push((name.clone(), value.clone()));
613 }
614 out
615}
616
617pub(crate) fn warnings_json(warnings: &[String]) -> String {
621 let items: Vec<String> = warnings.iter().map(|w| json_string(w)).collect();
622 format!("[{}]", items.join(","))
623}
624
625fn is_valid_mark_attr_name(name: &str) -> bool {
633 match name.strip_prefix("data-") {
634 Some(suffix) => crate::aes::is_valid_data_attr_name(suffix),
635 None => false,
636 }
637}
638
639fn rgb((r, g, b): (u8, u8, u8)) -> String {
640 format!("#{r:02X}{g:02X}{b:02X}")
641}
642
643fn escape(s: &str) -> String {
650 let mut out = String::with_capacity(s.len());
651 for c in s.chars() {
652 match c {
653 '&' => out.push_str("&"),
654 '<' => out.push_str("<"),
655 '>' => out.push_str(">"),
656 '"' => out.push_str("""),
657 '\'' => out.push_str("'"),
658 '\t' | '\n' | '\r' => out.push(c),
659 c if (c as u32) < 0x20 || c == '\u{FFFE}' || c == '\u{FFFF}' => {}
660 c => out.push(c),
661 }
662 }
663 out
664}
665
666fn points(pts: &[(f64, f64)]) -> String {
667 pts.iter()
668 .map(|(x, y)| format!("{x:.2},{y:.2}"))
669 .collect::<Vec<_>>()
670 .join(" ")
671}
672
673fn finite_pts(pts: &[(f64, f64)]) -> Vec<(f64, f64)> {
674 pts.iter()
675 .copied()
676 .filter(|(x, y)| x.is_finite() && y.is_finite())
677 .collect()
678}
679
680fn finite_runs(pts: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
689 let mut runs = Vec::new();
690 let mut cur: Vec<(f64, f64)> = Vec::new();
691 for &(x, y) in pts {
692 if x.is_finite() && y.is_finite() {
693 cur.push((x, y));
694 } else if !cur.is_empty() {
695 if cur.len() >= 2 {
696 runs.push(std::mem::take(&mut cur));
697 } else {
698 cur.clear();
699 }
700 }
701 }
702 if cur.len() >= 2 {
703 runs.push(cur);
704 }
705 runs
706}
707
708impl DrawBackend for SvgBackend {
709 fn plot_area(&self) -> Rect {
710 self.plot_area.clone()
711 }
712 fn total_area(&self) -> Rect {
713 self.total_area.clone()
714 }
715
716 fn set_tooltip(&mut self, tooltip: Option<String>) {
717 self.tooltip = tooltip;
718 }
719
720 fn set_mark_axis(&mut self, key: Option<String>) {
721 self.axis_key = key;
722 }
723
724 fn set_mark_series(&mut self, series: Option<String>) {
725 self.series_key = series;
726 }
727
728 fn set_mark_value(&mut self, value: Option<String>) {
729 self.value_key = value;
730 }
731
732 fn set_mark_geom(&mut self, geom: Option<&str>) {
733 self.mark_geom = geom.map(str::to_string);
734 }
735
736 fn warn(&mut self, message: String) {
737 self.warnings.push(message);
738 }
739
740 fn set_mark_part(&mut self, part: Option<&str>) {
741 self.mark_part = part.map(|s| s.to_string());
742 }
743
744 fn set_mark_attrs(&mut self, attrs: Vec<(String, String)>) {
745 self.mark_attrs = attrs;
746 }
747
748 fn wants_formatted_values(&self) -> bool {
749 self.formatted_values
750 }
751
752 fn set_mark_fvalue(&mut self, value: Option<String>) {
753 self.mark_fvalue = value;
754 }
755
756 fn set_mark_fx(&mut self, value: Option<String>) {
757 self.mark_fx = value;
758 }
759
760 fn draw_circle(
761 &mut self,
762 (cx, cy): (f64, f64),
763 radius: f64,
764 style: &PointStyle,
765 ) -> Result<(), RenderError> {
766 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
767 return Ok(());
768 }
769 let attrs = format!(
770 "cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"{:.2}\" fill=\"{}\"{}",
771 radius.max(0.0),
772 rgb(style.color),
773 opacity_attr("fill", style.alpha)
774 );
775 self.push_mark("circle", "point", &attrs);
776 Ok(())
777 }
778
779 fn draw_shape(
783 &mut self,
784 (cx, cy): (f64, f64),
785 radius: f64,
786 style: &PointStyle,
787 ) -> Result<(), RenderError> {
788 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
789 return Ok(());
790 }
791 let r = radius.max(0.0);
792 let color = rgb(style.color);
793 let fill_opacity = opacity_attr("fill", style.alpha);
794 let stroke_opacity = opacity_attr("stroke", style.alpha);
795 let poly = |pts: &[(f64, f64)]| {
796 if style.filled {
797 format!("points=\"{}\" fill=\"{color}\"{fill_opacity}", points(pts))
798 } else {
799 format!(
800 "points=\"{}\" fill=\"none\" stroke=\"{color}\"{stroke_opacity}",
801 points(pts)
802 )
803 }
804 };
805 match style.shape {
806 PointShape::Circle => return self.draw_circle((cx, cy), radius, style),
807 PointShape::Square => {
808 let a = poly(&[
809 (cx - r, cy - r),
810 (cx + r, cy - r),
811 (cx + r, cy + r),
812 (cx - r, cy + r),
813 ]);
814 self.push_mark("polygon", "point", &a);
815 }
816 PointShape::Triangle => {
817 let a = poly(&[(cx, cy - r), (cx + r, cy + r), (cx - r, cy + r)]);
818 self.push_mark("polygon", "point", &a);
819 }
820 PointShape::Diamond => {
821 let a = poly(&[(cx, cy - r), (cx + r, cy), (cx, cy + r), (cx - r, cy)]);
822 self.push_mark("polygon", "point", &a);
823 }
824 PointShape::Plus | PointShape::Cross => {
825 let d = if style.shape == PointShape::Plus {
826 format!(
827 "M{:.2} {cy:.2}H{:.2}M{cx:.2} {:.2}V{:.2}",
828 cx - r,
829 cx + r,
830 cy - r,
831 cy + r
832 )
833 } else {
834 format!(
835 "M{:.2} {:.2}L{:.2} {:.2}M{:.2} {:.2}L{:.2} {:.2}",
836 cx - r,
837 cy - r,
838 cx + r,
839 cy + r,
840 cx - r,
841 cy + r,
842 cx + r,
843 cy - r
844 )
845 };
846 let a = format!(
847 "d=\"{d}\" fill=\"none\" stroke=\"{color}\" stroke-width=\"{:.2}\"{stroke_opacity}",
848 (r / 2.5).clamp(1.0, 3.0)
849 );
850 self.push_mark("path", "point", &a);
851 }
852 }
853 Ok(())
854 }
855
856 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
857 for run in finite_runs(pts) {
863 self.draw_line_run(&run, style);
864 }
865 Ok(())
866 }
867
868 fn draw_quadratic(
871 &mut self,
872 p0: (f64, f64),
873 ctrl: (f64, f64),
874 p1: (f64, f64),
875 style: &LineStyle,
876 ) -> Result<(), RenderError> {
877 if !(p0.0.is_finite()
878 && p0.1.is_finite()
879 && ctrl.0.is_finite()
880 && ctrl.1.is_finite()
881 && p1.0.is_finite()
882 && p1.1.is_finite())
883 {
884 return Ok(());
885 }
886 let dash = match style
887 .linetype
888 .pattern()
889 .iter()
890 .flat_map(|(d, g)| [*d, *g])
891 .map(|v| format!("{v}"))
892 .collect::<Vec<_>>()
893 .join(",")
894 {
895 s if s.is_empty() => String::new(),
896 s => format!(" stroke-dasharray=\"{s}\""),
897 };
898 let linecap = match style.lineend {
899 LineEnd::Butt => "",
900 LineEnd::Round => " stroke-linecap=\"round\"",
901 LineEnd::Square => " stroke-linecap=\"square\"",
902 };
903 let d = format!(
904 "M{:.2} {:.2} Q{:.2} {:.2} {:.2} {:.2}",
905 p0.0, p0.1, ctrl.0, ctrl.1, p1.0, p1.1
906 );
907 let attrs = format!(
908 "d=\"{d}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
909 rgb(style.color),
910 finite_or(style.width, 1.0),
911 opacity_attr("stroke", style.alpha),
912 dash,
913 linecap
914 );
915 self.push_mark("path", "line", &attrs);
916 Ok(())
917 }
918
919 fn draw_rect(
920 &mut self,
921 top_left: (f64, f64),
922 bottom_right: (f64, f64),
923 style: &RectStyle,
924 ) -> Result<(), RenderError> {
925 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
926 self.rect_markup(top_left, bottom_right, &fill, style)
927 }
928
929 fn draw_rect_pattern(
932 &mut self,
933 top_left: (f64, f64),
934 bottom_right: (f64, f64),
935 pattern: &FillPattern,
936 style: &RectStyle,
937 ) -> Result<(), RenderError> {
938 let fill = self.pattern_url(pattern);
939 self.rect_markup(top_left, bottom_right, &fill, style)
940 }
941
942 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
943 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
944 self.polygon_markup(pts, &fill, style)
945 }
946
947 fn draw_polygon_pattern(
950 &mut self,
951 pts: &[(f64, f64)],
952 pattern: &FillPattern,
953 style: &RectStyle,
954 ) -> Result<(), RenderError> {
955 let fill = self.pattern_url(pattern);
956 self.polygon_markup(pts, &fill, style)
957 }
958
959 fn draw_polygon_rings(
965 &mut self,
966 rings: &[Vec<(f64, f64)>],
967 style: &RectStyle,
968 ) -> Result<(), RenderError> {
969 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
970 self.polygon_rings_markup(rings, &fill, style)
971 }
972
973 fn draw_polygon_rings_pattern(
977 &mut self,
978 rings: &[Vec<(f64, f64)>],
979 pattern: &FillPattern,
980 style: &RectStyle,
981 ) -> Result<(), RenderError> {
982 let fill = self.pattern_url(pattern);
983 self.polygon_rings_markup(rings, &fill, style)
984 }
985
986 fn draw_text(
987 &mut self,
988 text: &str,
989 pos: (f64, f64),
990 style: &TextStyle,
991 ) -> Result<(), RenderError> {
992 self.draw_text_with_title(text, pos, style, None)
993 }
994
995 fn draw_text_with_title(
999 &mut self,
1000 text: &str,
1001 (x, y): (f64, f64),
1002 style: &TextStyle,
1003 title: Option<&str>,
1004 ) -> Result<(), RenderError> {
1005 if !(x.is_finite() && y.is_finite()) {
1006 return Ok(());
1007 }
1008 let anchor = match style.anchor {
1009 TextAnchor::Start => "start",
1010 TextAnchor::Middle => "middle",
1011 TextAnchor::End => "end",
1012 };
1013 let family = escape(style.family.as_deref().unwrap_or("sans-serif"));
1014 let weight = if style.face == FontFace::Bold {
1015 " font-weight=\"bold\""
1016 } else {
1017 ""
1018 };
1019 let fstyle = if style.face == FontFace::Italic {
1020 " font-style=\"italic\""
1021 } else {
1022 ""
1023 };
1024 let angle = finite_or(style.angle, 0.0);
1025 let transform = if angle.abs() > 0.01 {
1026 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
1027 } else {
1028 String::new()
1029 };
1030 let class = self.mark_class("text");
1031 let title_tag = match title {
1032 Some(t) => format!("<title>{}</title>", escape(t)),
1033 None => String::new(),
1034 };
1035 self.body.push_str(&format!(
1036 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
1037 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle} fill=\"{}\"{transform}{class}>{title_tag}{}</text>",
1038 finite_or(style.size, 12.0),
1039 rgb(style.color),
1040 escape(text)
1041 ));
1042 Ok(())
1043 }
1044
1045 fn draw_text_runs(
1052 &mut self,
1053 runs: &[(&str, Option<&str>)],
1054 (x, y): (f64, f64),
1055 style: &TextStyle,
1056 ) -> Result<(), RenderError> {
1057 if !(x.is_finite() && y.is_finite()) {
1058 return Ok(());
1059 }
1060 let anchor = match style.anchor {
1061 TextAnchor::Start => "start",
1062 TextAnchor::Middle => "middle",
1063 TextAnchor::End => "end",
1064 };
1065 let family = escape(style.family.as_deref().unwrap_or("sans-serif"));
1066 let weight = if style.face == FontFace::Bold {
1067 " font-weight=\"bold\""
1068 } else {
1069 ""
1070 };
1071 let fstyle = if style.face == FontFace::Italic {
1072 " font-style=\"italic\""
1073 } else {
1074 ""
1075 };
1076 let angle = finite_or(style.angle, 0.0);
1077 let transform = if angle.abs() > 0.01 {
1078 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
1079 } else {
1080 String::new()
1081 };
1082 let class = self.mark_class("text");
1083 let mut inner = String::new();
1084 for (text, href) in runs {
1085 match href {
1086 Some(u) => inner.push_str(&format!(
1087 "<a href=\"{}\" target=\"_blank\" rel=\"noopener\"><tspan text-decoration=\"underline\">{}</tspan></a>",
1088 escape(u),
1089 escape(text)
1090 )),
1091 None => inner.push_str(&format!("<tspan>{}</tspan>", escape(text))),
1092 }
1093 }
1094 self.body.push_str(&format!(
1095 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
1096 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle} fill=\"{}\"{transform}{class}>{inner}</text>",
1097 finite_or(style.size, 12.0),
1098 rgb(style.color),
1099 ));
1100 Ok(())
1101 }
1102}
1103
1104#[cfg(test)]
1105mod tests {
1106 use super::*;
1107 use crate::render::Rect;
1108
1109 fn backend() -> SvgBackend {
1110 SvgBackend::new(
1111 100,
1112 100,
1113 Rect {
1114 x: 0.0,
1115 y: 0.0,
1116 width: 100.0,
1117 height: 100.0,
1118 },
1119 )
1120 }
1121
1122 #[test]
1123 fn draw_quadratic_emits_a_path_with_m_and_q() {
1124 let mut b = backend();
1125 b.draw_quadratic((0.0, 0.0), (10.0, 20.0), (20.0, 0.0), &LineStyle::default())
1126 .unwrap();
1127 let svg = b.finish();
1128 assert!(svg.contains("<path"));
1129 assert!(svg.contains("d=\"M0.00 0.00 Q10.00 20.00 20.00 0.00\""));
1130 assert!(!svg.contains("NaN"));
1131 assert!(!svg.contains("inf"));
1132 }
1133
1134 #[test]
1135 fn draw_quadratic_drops_non_finite_input() {
1136 let mut b = backend();
1137 b.draw_quadratic(
1138 (f64::NAN, 0.0),
1139 (10.0, 20.0),
1140 (20.0, 0.0),
1141 &LineStyle::default(),
1142 )
1143 .unwrap();
1144 b.draw_quadratic(
1145 (0.0, 0.0),
1146 (f64::INFINITY, 20.0),
1147 (20.0, 0.0),
1148 &LineStyle::default(),
1149 )
1150 .unwrap();
1151 let svg = b.finish();
1152 assert!(!svg.contains("<path"));
1153 }
1154
1155 #[test]
1156 fn draw_line_omits_linecap_by_default_but_writes_it_when_set() {
1157 let mut b = backend();
1158 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &LineStyle::default())
1159 .unwrap();
1160 let svg = b.finish();
1161 assert!(!svg.contains("stroke-linecap"));
1162
1163 let mut b2 = backend();
1164 let style = LineStyle {
1165 lineend: LineEnd::Round,
1166 ..Default::default()
1167 };
1168 b2.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
1169 let svg2 = b2.finish();
1170 assert!(svg2.contains("stroke-linecap=\"round\""));
1171 }
1172
1173 #[test]
1174 fn square_linecap_is_written_on_both_draw_line_and_draw_quadratic() {
1175 let style = LineStyle {
1176 lineend: LineEnd::Square,
1177 ..Default::default()
1178 };
1179 let mut b = backend();
1180 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
1181 assert!(b.finish().contains("stroke-linecap=\"square\""));
1182
1183 let mut b2 = backend();
1184 b2.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &style)
1185 .unwrap();
1186 assert!(b2.finish().contains("stroke-linecap=\"square\""));
1187 }
1188
1189 #[test]
1194 fn set_mark_attrs_defence_in_depth_drops_invalid_names() {
1195 let mut b = backend();
1196 b.set_mark_attrs(vec![
1197 ("not-data-prefixed".to_string(), "x".to_string()),
1198 ("data-".to_string(), "x".to_string()), ("data-Upper".to_string(), "x".to_string()), ("data-ok".to_string(), "valid".to_string()), ]);
1202 b.draw_circle((1.0, 1.0), 1.0, &PointStyle::default())
1203 .unwrap();
1204 let svg = b.finish();
1205 assert!(!svg.contains("not-data-prefixed"));
1206 assert!(!svg.contains("data-Upper"));
1207 assert!(!svg.contains("data-=\""));
1208 assert!(svg.contains("data-ok=\"valid\""));
1209 }
1210
1211 fn stripe() -> FillPattern {
1214 FillPattern::Stripe {
1215 fg: (10, 20, 30),
1216 bg: (200, 210, 220),
1217 size_px: 4.0,
1218 angle_deg: 45.0,
1219 }
1220 }
1221
1222 fn pattern_id(svg: &str) -> String {
1223 let i = svg.find("<pattern id=\"").unwrap() + "<pattern id=\"".len();
1224 svg[i..i + svg[i..].find('"').unwrap()].to_string()
1225 }
1226
1227 #[test]
1228 fn a_pattern_fill_registers_one_defs_pattern_and_references_it() {
1229 let mut b = backend();
1230 b.draw_rect_pattern((0.0, 0.0), (10.0, 10.0), &stripe(), &RectStyle::default())
1231 .unwrap();
1232 let svg = b.finish();
1233 assert_eq!(svg.matches("<pattern ").count(), 1, "{svg}");
1234 let id = pattern_id(&svg);
1235 assert!(
1236 svg.contains(&format!("fill=\"url(#{id})\"")),
1237 "rect references the registered pattern id {id}: {svg}"
1238 );
1239 }
1240
1241 #[test]
1242 fn a_rounded_rect_pattern_still_rounds_its_corners() {
1243 let mut b = backend();
1244 b.draw_rect_pattern(
1245 (0.0, 0.0),
1246 (10.0, 10.0),
1247 &stripe(),
1248 &RectStyle {
1249 corner_radius: 3.0,
1250 ..Default::default()
1251 },
1252 )
1253 .unwrap();
1254 let svg = b.finish();
1255 assert!(svg.contains("rx=\"3.00\""), "{svg}");
1256 assert!(svg.contains("fill=\"url(#"), "{svg}");
1257 }
1258
1259 #[test]
1260 fn an_equal_pattern_reuses_the_same_defs_entry() {
1261 let mut b = backend();
1262 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1263 .unwrap();
1264 b.draw_polygon_pattern(
1265 &[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)],
1266 &stripe(),
1267 &RectStyle::default(),
1268 )
1269 .unwrap();
1270 let svg = b.finish();
1271 assert_eq!(
1272 svg.matches("<pattern ").count(),
1273 1,
1274 "two equal patterns share one <defs> entry: {svg}"
1275 );
1276 }
1277
1278 #[test]
1279 fn different_patterns_get_their_own_defs_entries() {
1280 let mut b = backend();
1281 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1282 .unwrap();
1283 let other = FillPattern::Stripe {
1284 fg: (1, 1, 1),
1285 bg: (2, 2, 2),
1286 size_px: 8.0,
1287 angle_deg: 0.0,
1288 };
1289 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &other, &RectStyle::default())
1290 .unwrap();
1291 let svg = b.finish();
1292 assert_eq!(svg.matches("<pattern ").count(), 2, "{svg}");
1293 }
1294
1295 #[test]
1296 fn no_pattern_used_means_no_defs_block_at_all() {
1297 let mut b = backend();
1298 b.draw_rect((0.0, 0.0), (5.0, 5.0), &RectStyle::default())
1299 .unwrap();
1300 let svg = b.finish();
1301 assert!(!svg.contains("<defs>"), "{svg}");
1302 }
1303
1304 #[test]
1305 fn pattern_fill_cuts_out_holes_like_a_flat_fill_does() {
1306 let mut b = backend();
1307 let outer = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1308 let hole = vec![(2.0, 2.0), (8.0, 2.0), (8.0, 8.0), (2.0, 8.0)];
1309 b.draw_polygon_rings_pattern(&[outer, hole], &stripe(), &RectStyle::default())
1310 .unwrap();
1311 let svg = b.finish();
1312 assert!(svg.contains("fill-rule=\"evenodd\""), "{svg}");
1313 assert_eq!(
1314 svg.matches("<path").count(),
1315 1,
1316 "one path, both rings: {svg}"
1317 );
1318 }
1319
1320 #[test]
1321 fn two_backends_on_one_page_never_collide_on_a_pattern_id() {
1322 let mut a = backend();
1323 a.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1324 .unwrap();
1325 let mut b = backend();
1326 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1327 .unwrap();
1328 let (sa, sb) = (a.finish(), b.finish());
1329 assert_ne!(pattern_id(&sa), pattern_id(&sb), "{sa}\n{sb}");
1330 }
1331}