1use super::backend::{
8 DrawBackend, FillPattern, FontFace, LineEnd, LineStyle, Linetype, PointShape, PointStyle,
9 RectStyle, 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 mark_user_class: Option<String>,
58 mark_group: Option<(String, String)>,
61 layer_attrs: Vec<(String, String)>,
64 groups: Vec<(String, String, String)>,
67 group_slots: Vec<(usize, usize)>,
72}
73
74static NEXT_ID_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
77
78impl SvgBackend {
79 pub fn new(width: u32, height: u32, plot_area: Rect) -> Self {
80 SvgBackend {
81 plot_area,
82 total_area: Rect {
83 x: 0.0,
84 y: 0.0,
85 width: width as f64,
86 height: height as f64,
87 },
88 body: String::new(),
89 tooltip: None,
90 axis_key: None,
91 series_key: None,
92 value_key: None,
93 mark_part: None,
94 mark_attrs: Vec::new(),
95 mark_geom: None,
96 root_attrs: Vec::new(),
97 warnings: Vec::new(),
98 plot_attr: true,
99 formatted_values: false,
100 mark_fvalue: None,
101 mark_fx: None,
102 patterns: Vec::new(),
103 id_prefix: format!(
104 "gg{:x}",
105 NEXT_ID_PREFIX.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
106 ),
107 mark_user_class: None,
108 mark_group: None,
109 layer_attrs: Vec::new(),
110 groups: Vec::new(),
111 group_slots: Vec::new(),
112 }
113 }
114
115 fn pattern_url(&mut self, pattern: &FillPattern) -> String {
119 if let Some((_, id)) = self.patterns.iter().find(|(p, _)| p == pattern) {
120 return format!("url(#{id})");
121 }
122 let id = format!("{}-pat-{}", self.id_prefix, self.patterns.len());
123 self.patterns.push((*pattern, id.clone()));
124 format!("url(#{id})")
125 }
126
127 fn rect_markup(
130 &mut self,
131 (x0, y0): (f64, f64),
132 (x1, y1): (f64, f64),
133 fill: &str,
134 style: &RectStyle,
135 ) -> Result<(), RenderError> {
136 if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) {
137 return Ok(());
138 }
139 let (x, y) = (x0.min(x1), y0.min(y1));
140 let (w, h) = ((x1 - x0).abs(), (y1 - y0).abs());
141 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
142 let r = style.corner_radius.max(0.0).min(w / 2.0).min(h / 2.0);
145 let radius = if r > 0.0 {
146 format!(" rx=\"{r:.2}\" ry=\"{r:.2}\"")
147 } else {
148 String::new()
149 };
150 let attrs = format!(
151 "x=\"{x:.2}\" y=\"{y:.2}\" width=\"{w:.2}\" height=\"{h:.2}\"{radius} fill=\"{fill}\"{} \
152 stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
153 opacity_attr("fill", style.alpha),
154 finite_or(style.stroke_width, 0.0)
155 );
156 self.push_mark("rect", "bar", &attrs);
157 Ok(())
158 }
159
160 fn polygon_markup(
162 &mut self,
163 pts: &[(f64, f64)],
164 fill: &str,
165 style: &RectStyle,
166 ) -> Result<(), RenderError> {
167 let pts = finite_pts(pts);
168 if pts.len() < 3 {
169 return Ok(());
170 }
171 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
172 let attrs = format!(
173 "points=\"{}\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
174 points(&pts),
175 opacity_attr("fill", style.alpha),
176 finite_or(style.stroke_width, 0.0)
177 );
178 self.push_mark("polygon", "band", &attrs);
179 Ok(())
180 }
181
182 fn polygon_rings_markup(
185 &mut self,
186 rings: &[Vec<(f64, f64)>],
187 fill: &str,
188 style: &RectStyle,
189 ) -> Result<(), RenderError> {
190 let mut d = String::new();
191 for ring in rings {
192 let pts = finite_pts(ring);
193 if pts.len() < 3 {
194 continue;
195 }
196 d.push('M');
197 d.push_str(&format!("{},{}", num(pts[0].0), num(pts[0].1)));
198 for p in &pts[1..] {
199 d.push('L');
200 d.push_str(&format!("{},{}", num(p.0), num(p.1)));
201 }
202 d.push('Z');
203 }
204 if d.is_empty() {
205 return Ok(());
206 }
207 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
208 let attrs = format!(
209 "d=\"{d}\" fill-rule=\"evenodd\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
210 opacity_attr("fill", style.alpha),
211 finite_or(style.stroke_width, 0.0)
212 );
213 self.push_mark("path", "band", &attrs);
214 Ok(())
215 }
216
217 fn pattern_defs(&self) -> String {
222 if self.patterns.is_empty() {
223 return String::new();
224 }
225 let mut defs = String::from("<defs>");
226 for (p, id) in &self.patterns {
227 let FillPattern::Stripe {
228 fg,
229 bg,
230 size_px,
231 angle_deg,
232 } = p;
233 let size = if *size_px > 0.0 { *size_px } else { 1.0 };
234 defs.push_str(&format!(
235 "<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>",
236 rgb(*bg),
237 size * 0.3,
238 rgb(*fg),
239 ));
240 }
241 defs.push_str("</defs>");
242 defs
243 }
244
245 pub(crate) fn set_formatted_values(&mut self, on: bool) {
249 self.formatted_values = on;
250 }
251
252 pub fn set_root_attrs(&mut self, attrs: Vec<(String, String)>) {
256 self.root_attrs = attrs;
257 }
258
259 pub fn extend_root_attrs(&mut self, attrs: Vec<(String, String)>) {
267 self.root_attrs.extend(attrs);
268 }
269
270 pub(crate) fn without_plot_attr(&mut self) {
272 self.plot_attr = false;
273 }
274
275 pub(crate) fn push_raw(&mut self, markup: &str) {
278 self.body.push_str(markup);
279 }
280
281 pub(crate) fn body(&mut self) -> &str {
284 self.flush_groups();
285 &self.body
286 }
287
288 fn emit(&mut self, markup: &str) {
291 let key = match (&self.mark_group, &self.mark_geom) {
292 (Some(k), Some(_)) => k,
293 _ => {
294 self.body.push_str(markup);
295 return;
296 }
297 };
298 match self
299 .groups
300 .iter_mut()
301 .find(|(n, v, _)| n == &key.0 && v == &key.1)
302 {
303 Some((_, _, content)) => content.push_str(markup),
304 None => {
305 self.group_slots.push((self.body.len(), self.groups.len()));
306 self.groups
307 .push((key.0.clone(), key.1.clone(), markup.to_string()));
308 }
309 }
310 }
311
312 fn flush_groups(&mut self) {
318 if self.group_slots.is_empty() {
319 return;
320 }
321 let groups = std::mem::take(&mut self.groups);
322 let slots = std::mem::take(&mut self.group_slots);
323 let extra: usize = groups
324 .iter()
325 .map(|(n, v, c)| n.len() + v.len() + c.len() + 12)
326 .sum();
327 let mut out = String::with_capacity(self.body.len() + extra);
328 let mut last = 0;
329 for (off, gi) in slots {
330 out.push_str(&self.body[last..off]);
331 last = off;
332 let (name, value, content) = &groups[gi];
333 if is_valid_mark_attr_name(name) {
334 out.push_str(&format!(
335 "<g {name}=\"{}\">{content}</g>",
336 escape_attr(value)
337 ));
338 } else {
339 out.push_str(content);
340 }
341 }
342 out.push_str(&self.body[last..]);
343 self.body = out;
344 }
345
346 pub(crate) fn wrap_body_in_group(&mut self, attrs: &[(String, String)]) {
353 self.flush_groups();
354 if attrs.is_empty() {
355 return;
356 }
357 let mut open = String::from("<g");
358 for (k, v) in attrs {
359 open.push_str(&format!(" {k}=\"{}\"", escape_attr(v)));
360 }
361 open.push('>');
362 self.body = format!("{open}{}</g>", self.body);
363 }
364
365 pub fn take_warnings(&mut self) -> Vec<String> {
367 std::mem::take(&mut self.warnings)
368 }
369
370 fn mark_class(&self, kind: &str) -> String {
377 match &self.mark_geom {
378 Some(g) => {
379 let mut class = format!("gg-mark gg-{kind}");
380 let layer = self
381 .layer_attrs
382 .iter()
383 .filter(|(n, _)| n == "class")
384 .map(|(_, v)| v.as_str());
385 let mut seen: Vec<&str> = Vec::new();
386 for t in layer
387 .chain(self.mark_user_class.as_deref())
388 .flat_map(str::split_whitespace)
389 {
390 if crate::aes::is_valid_class_token(t) && !seen.contains(&t) {
391 seen.push(t);
392 class.push(' ');
393 class.push_str(t);
394 }
395 }
396 format!(" class=\"{class}\" data-geom=\"{}\"", escape_attr(g))
397 }
398 None => String::new(),
399 }
400 }
401
402 fn layer_flag_attrs(&self, aria_hidden_written: bool) -> String {
407 let mut out = String::new();
408 if self.mark_geom.is_none() {
409 return out;
410 }
411 for (name, value) in &self.layer_attrs {
412 let ok = match name.as_str() {
413 "aria-hidden" => !aria_hidden_written && matches!(value.as_str(), "true" | "false"),
414 "pointer-events" => POINTER_EVENTS_VALUES.contains(&value.as_str()),
415 _ => false,
416 };
417 if ok {
418 out.push_str(&format!(" {name}=\"{value}\""));
419 }
420 }
421 out
422 }
423
424 fn text_data_attrs(&self) -> String {
429 let mut pairs: Vec<(&str, &str)> = Vec::new();
430 if self.mark_geom.is_none() {
431 return String::new();
432 }
433 for (name, val) in self.layer_attrs.iter().chain(self.mark_attrs.iter()) {
434 if !is_valid_mark_attr_name(name) {
435 continue;
436 }
437 match pairs.iter_mut().find(|(n, _)| n == name) {
438 Some(existing) => existing.1 = val,
439 None => pairs.push((name, val)),
440 }
441 }
442 let mut out = String::new();
443 for (name, val) in pairs {
444 out.push_str(&format!(" {name}=\"{}\"", escape_attr(val)));
445 }
446 out
447 }
448
449 fn push_mark(&mut self, tag: &str, kind: &str, attrs: &str) {
457 let mut pairs: Vec<(String, String)> = Vec::new();
467 for (name, val) in [
468 ("data-x", &self.axis_key),
469 ("data-series", &self.series_key),
470 ("data-value", &self.value_key),
471 ] {
472 if let Some(v) = val {
473 pairs.push((name.to_string(), v.clone()));
474 }
475 }
476 if let Some(fx) = &self.mark_fx {
480 if self.axis_key.as_deref() != Some(fx.as_str()) {
481 pairs.push(("data-fx".to_string(), fx.clone()));
482 }
483 }
484 if let Some(fvalue) = &self.mark_fvalue {
485 pairs.push(("data-fvalue".to_string(), fvalue.clone()));
486 }
487 let layer_data = self.layer_attrs.iter().filter(|_| self.mark_geom.is_some());
490 for (name, val) in layer_data.chain(self.mark_attrs.iter()) {
491 if !is_valid_mark_attr_name(name) {
492 continue;
493 }
494 match pairs.iter_mut().find(|(n, _)| n == name) {
495 Some(existing) => existing.1 = val.clone(),
496 None => pairs.push((name.clone(), val.clone())),
497 }
498 }
499 if let Some(part) = &self.mark_part {
504 match pairs.iter_mut().find(|(n, _)| n == "data-part") {
505 Some(existing) => existing.1 = part.clone(),
506 None => pairs.push(("data-part".to_string(), part.clone())),
507 }
508 }
509 let mut data = String::new();
510 for (name, val) in &pairs {
511 data.push_str(&format!(" {name}=\"{}\"", escape_attr(val)));
512 }
513 if self.mark_part.is_some() {
514 data.push_str(" aria-hidden=\"true\"");
515 }
516 data.push_str(&self.layer_flag_attrs(self.mark_part.is_some()));
517 let effective_kind: &str = self.mark_part.as_deref().unwrap_or(kind);
522 data.push_str(&self.mark_class(effective_kind));
523 let mark = if self.mark_part.is_some() {
524 format!("<{tag} {attrs}{data}/>")
525 } else {
526 match &self.tooltip {
527 Some(t) => format!("<{tag} {attrs}{data}><title>{}</title></{tag}>", escape(t)),
528 None => format!("<{tag} {attrs}{data}/>"),
529 }
530 };
531 self.emit(&mark);
532 }
533
534 fn root_open(&self, prefix: &str) -> String {
535 let p = &self.plot_area;
536 let (w, h) = (self.total_area.width as i64, self.total_area.height as i64);
537 let mut extra = String::new();
538 for (k, v) in &self.root_attrs {
539 extra.push_str(&format!(" {k}=\"{}\"", escape_attr(v)));
540 }
541 let plot = if self.plot_attr {
542 format!(
543 " data-plot=\"{} {} {} {}\"",
544 num(p.x),
545 num(p.y),
546 num(p.width),
547 num(p.height),
548 )
549 } else {
550 String::new()
551 };
552 format!("<svg {prefix}width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\"{plot}{extra}>")
553 }
554
555 pub fn finish(mut self) -> String {
559 self.flush_groups();
560 let open = self.root_open("xmlns=\"http://www.w3.org/2000/svg\" ");
561 let defs = self.pattern_defs();
562 format!("{open}{defs}{}</svg>", self.body)
563 }
564
565 pub fn finish_fragment(mut self, x: f64, y: f64) -> String {
570 self.flush_groups();
571 let open = self.root_open(&format!("x=\"{}\" y=\"{}\" ", num(x), num(y)));
572 let defs = self.pattern_defs();
573 format!("{open}{defs}{}</svg>", self.body)
574 }
575
576 fn draw_line_run(&mut self, pts: &[(f64, f64)], style: &LineStyle) {
579 let dash = dasharray_attr(&style.linetype);
580 let linecap = match style.lineend {
583 LineEnd::Butt => "",
584 LineEnd::Round => " stroke-linecap=\"round\"",
585 LineEnd::Square => " stroke-linecap=\"square\"",
586 };
587 let attrs = format!(
588 "points=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
589 points(pts),
590 rgb(style.color),
591 finite_or(style.width, 1.0),
592 opacity_attr("stroke", style.alpha),
593 dash,
594 linecap
595 );
596 self.push_mark("polyline", "line", &attrs);
597 }
598}
599
600fn num(v: f64) -> String {
603 if v.is_finite() {
604 format!("{v:.2}")
605 } else {
606 "0".to_string()
607 }
608}
609
610fn dasharray_attr(linetype: &Linetype) -> String {
614 let pattern = linetype.dash_pattern();
615 if pattern.is_empty() {
616 return String::new();
617 }
618 let s = pattern
619 .iter()
620 .flat_map(|(d, g)| [*d, *g])
621 .map(|v| format!("{v}"))
622 .collect::<Vec<_>>()
623 .join(",");
624 format!(" stroke-dasharray=\"{s}\"")
625}
626
627fn compact_rings_d(rings: &[Vec<(f64, f64)>], precision: u8) -> String {
637 let mut d = String::new();
638 for ring in rings {
639 let pts = finite_pts(ring);
640 if pts.len() < 3 {
641 continue;
642 }
643 d.push('M');
644 push_num(&mut d, &fmt_compact(pts[0].0, precision));
645 push_num(&mut d, &fmt_compact(pts[0].1, precision));
646 if pts.len() > 1 {
647 d.push('l');
648 let mut prev = pts[0];
649 for p in &pts[1..] {
650 push_num(&mut d, &fmt_compact(p.0 - prev.0, precision));
651 push_num(&mut d, &fmt_compact(p.1 - prev.1, precision));
652 prev = *p;
653 }
654 }
655 d.push('z');
656 }
657 d
658}
659
660fn push_num(d: &mut String, s: &str) {
668 let needs_sep =
669 matches!(d.chars().last(), Some(c) if c.is_ascii_digit()) && !s.starts_with('-');
670 if needs_sep {
671 d.push(',');
672 }
673 d.push_str(s);
674}
675
676fn fmt_compact(v: f64, precision: u8) -> String {
682 if !v.is_finite() {
683 return "0".to_string();
684 }
685 let s = format!("{v:.*}", precision as usize);
686 let trimmed = if s.contains('.') {
687 s.trim_end_matches('0').trim_end_matches('.')
688 } else {
689 s.as_str()
690 };
691 match trimmed {
692 "" | "-" | "-0" => "0".to_string(),
693 other => other.to_string(),
694 }
695}
696
697fn finite_or(v: f64, fallback: f64) -> f64 {
698 if v.is_finite() {
699 v
700 } else {
701 fallback
702 }
703}
704
705fn opacity_attr(prefix: &str, alpha: f64) -> String {
711 let a = finite_or(alpha, 1.0);
712 if format!("{a:.3}") == "1.000" {
713 String::new()
714 } else {
715 format!(" {prefix}-opacity=\"{a:.3}\"")
716 }
717}
718
719pub fn root_data_attrs(built: &crate::build::BuiltPlot) -> Vec<(String, String)> {
736 use crate::aes::Aesthetic;
737 let mut out = Vec::new();
738 if !built.panel_scales.is_empty() {
739 return out;
740 }
741 let axis = |aes: &Aesthetic| -> (Option<(f64, f64)>, Option<String>) {
742 match built.scales.get(aes) {
743 Some(s) if s.is_discrete() => {
744 let labels: Vec<String> = s
745 .breaks()
746 .into_iter()
747 .map(|(_, l)| json_string(&l))
748 .collect();
749 (None, Some(format!("[{}]", labels.join(","))))
750 }
751 Some(s) => (s.expanded_domain(), None),
752 None => (None, None),
753 }
754 };
755 let (xd, xl) = axis(&Aesthetic::X);
756 let (yd, yl) = axis(&Aesthetic::Y);
757 if let (Some((x0, x1)), Some((y0, y1))) = (xd, yd) {
758 out.push((
759 "data-domain".into(),
760 format!("{} {} {} {}", g(x0), g(x1), g(y0), g(y1)),
761 ));
762 }
763 if let Some((a, b)) = xd {
764 out.push(("data-xdomain".into(), format!("{} {}", g(a), g(b))));
765 }
766 if let Some((a, b)) = yd {
767 out.push(("data-ydomain".into(), format!("{} {}", g(a), g(b))));
768 }
769 if let Some(l) = xl {
770 out.push(("data-xlevels".into(), l));
771 }
772 if let Some(l) = yl {
773 out.push(("data-ylevels".into(), l));
774 }
775 if built.coord.is_flipped() {
776 out.push(("data-flip".into(), "true".into()));
777 }
778 out
779}
780
781fn g(v: f64) -> String {
784 format!("{v}")
785}
786
787fn json_string(s: &str) -> String {
790 let mut out = String::with_capacity(s.len() + 2);
791 out.push('"');
792 for c in s.chars() {
793 match c {
794 '"' => out.push_str("\\\""),
795 '\\' => out.push_str("\\\\"),
796 '\n' => out.push_str("\\n"),
797 '\r' => out.push_str("\\r"),
798 '\t' => out.push_str("\\t"),
799 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
800 c => out.push(c),
801 }
802 }
803 out.push('"');
804 out
805}
806
807fn is_allowed_root_attr_name(name: &str) -> bool {
814 matches!(name, "role" | "id" | "class" | "lang")
815 || name
816 .strip_prefix("data-")
817 .is_some_and(crate::aes::is_valid_data_attr_name)
818 || name
819 .strip_prefix("aria-")
820 .is_some_and(crate::aes::is_valid_data_attr_name)
821}
822
823pub(crate) fn validate_root_attrs(
833 requested: &[(String, String)],
834 existing: &[(String, String)],
835 label: &str,
836 warnings: &mut Vec<String>,
837) -> Vec<(String, String)> {
838 let built_in: Vec<&str> = existing.iter().map(|(k, _)| k.as_str()).collect();
839 let mut used: Vec<&str> = built_in.clone();
840 let mut out = Vec::new();
841 for (name, value) in requested {
842 if !is_allowed_root_attr_name(name) {
843 warnings.push(format!(
844 "{label}: svg_attr(\"{name}\") is not on the allow-list (data-*, aria-*, role, id, class, lang) — ignored"
845 ));
846 continue;
847 }
848 if used.contains(&name.as_str()) {
849 if built_in.contains(&name.as_str()) {
854 warnings.push(format!(
855 "{label}: svg_attr(\"{name}\") collides with an attribute ggplot-rs already writes to the root — ignored"
856 ));
857 } else {
858 warnings.push(format!(
859 "{label}: svg_attr(\"{name}\") was already set by an earlier svg_attr call — ignored"
860 ));
861 }
862 continue;
863 }
864 used.push(name.as_str());
865 out.push((name.clone(), value.clone()));
866 }
867 out
868}
869
870pub(crate) fn warnings_json(warnings: &[String]) -> String {
874 let items: Vec<String> = warnings.iter().map(|w| json_string(w)).collect();
875 format!("[{}]", items.join(","))
876}
877
878fn is_valid_mark_attr_name(name: &str) -> bool {
886 match name.strip_prefix("data-") {
887 Some(suffix) => crate::aes::is_valid_data_attr_name(suffix),
888 None => false,
889 }
890}
891
892const POINTER_EVENTS_VALUES: &[&str] = &[
895 "auto",
896 "none",
897 "visiblePainted",
898 "visibleFill",
899 "visibleStroke",
900 "visible",
901 "painted",
902 "fill",
903 "stroke",
904 "all",
905 "bounding-box",
906];
907
908pub(crate) fn validate_layer_svg_attr(
917 name: &str,
918 value: &str,
919 warnings: &mut Vec<String>,
920) -> Option<(String, String)> {
921 let keep = |v: &str| Some((name.to_string(), v.to_string()));
922 match name {
923 "class" => {
924 let (ok, bad) = crate::aes::split_class_tokens(value);
925 if !bad.is_empty() {
926 warnings.push(format!(
927 "layer_svg_attr(\"class\"): invalid class token(s) {bad:?} (must match [A-Za-z][A-Za-z0-9_-]*) — dropped"
928 ));
929 }
930 if ok.is_empty() {
931 None
932 } else {
933 keep(&ok.join(" "))
934 }
935 }
936 "aria-hidden" if matches!(value, "true" | "false") => keep(value),
937 "pointer-events" if POINTER_EVENTS_VALUES.contains(&value) => keep(value),
938 "aria-hidden" | "pointer-events" => {
939 warnings.push(format!(
940 "layer_svg_attr(\"{name}\"): value {value:?} is not allowed — ignored"
941 ));
942 None
943 }
944 _ if is_valid_mark_attr_name(name) => keep(value),
945 _ => {
946 warnings.push(format!(
947 "layer_svg_attr(\"{name}\") is not on the allow-list (class, aria-hidden, pointer-events, data-*) — ignored"
948 ));
949 None
950 }
951 }
952}
953
954fn rgb((r, g, b): (u8, u8, u8)) -> String {
955 format!("#{r:02X}{g:02X}{b:02X}")
956}
957
958fn escape(s: &str) -> String {
969 escape_impl(s, false)
970}
971
972fn escape_attr(s: &str) -> String {
983 escape_impl(s, true)
984}
985
986fn escape_impl(s: &str, in_attr: bool) -> String {
987 let mut out = String::with_capacity(s.len());
988 for c in s.chars() {
989 match c {
990 '&' => out.push_str("&"),
991 '<' => out.push_str("<"),
992 '>' => out.push_str(">"),
993 '"' => out.push_str("""),
994 '\'' => out.push_str("'"),
995 '\t' if in_attr => out.push_str("	"),
996 '\n' if in_attr => out.push_str(" "),
997 '\r' if in_attr => out.push_str(" "),
998 '\t' | '\n' | '\r' => out.push(c),
999 c if (c as u32) < 0x20 || c == '\u{FFFE}' || c == '\u{FFFF}' => {}
1000 c => out.push(c),
1001 }
1002 }
1003 out
1004}
1005
1006fn points(pts: &[(f64, f64)]) -> String {
1007 pts.iter()
1008 .map(|(x, y)| format!("{x:.2},{y:.2}"))
1009 .collect::<Vec<_>>()
1010 .join(" ")
1011}
1012
1013fn finite_pts(pts: &[(f64, f64)]) -> Vec<(f64, f64)> {
1014 pts.iter()
1015 .copied()
1016 .filter(|(x, y)| x.is_finite() && y.is_finite())
1017 .collect()
1018}
1019
1020fn finite_runs(pts: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
1029 let mut runs = Vec::new();
1030 let mut cur: Vec<(f64, f64)> = Vec::new();
1031 for &(x, y) in pts {
1032 if x.is_finite() && y.is_finite() {
1033 cur.push((x, y));
1034 } else if !cur.is_empty() {
1035 if cur.len() >= 2 {
1036 runs.push(std::mem::take(&mut cur));
1037 } else {
1038 cur.clear();
1039 }
1040 }
1041 }
1042 if cur.len() >= 2 {
1043 runs.push(cur);
1044 }
1045 runs
1046}
1047
1048impl DrawBackend for SvgBackend {
1049 fn plot_area(&self) -> Rect {
1050 self.plot_area.clone()
1051 }
1052 fn total_area(&self) -> Rect {
1053 self.total_area.clone()
1054 }
1055
1056 fn set_tooltip(&mut self, tooltip: Option<String>) {
1057 self.tooltip = tooltip;
1058 }
1059
1060 fn set_mark_axis(&mut self, key: Option<String>) {
1061 self.axis_key = key;
1062 }
1063
1064 fn set_mark_series(&mut self, series: Option<String>) {
1065 self.series_key = series;
1066 }
1067
1068 fn set_mark_value(&mut self, value: Option<String>) {
1069 self.value_key = value;
1070 }
1071
1072 fn set_mark_geom(&mut self, geom: Option<&str>) {
1073 self.mark_geom = geom.map(str::to_string);
1074 }
1075
1076 fn warn(&mut self, message: String) {
1077 self.warnings.push(message);
1078 }
1079
1080 fn set_mark_part(&mut self, part: Option<&str>) {
1081 self.mark_part = part.map(|s| s.to_string());
1082 }
1083
1084 fn set_mark_attrs(&mut self, attrs: Vec<(String, String)>) {
1085 self.mark_attrs = attrs;
1086 }
1087
1088 fn wants_formatted_values(&self) -> bool {
1089 self.formatted_values
1090 }
1091
1092 fn set_mark_fvalue(&mut self, value: Option<String>) {
1093 self.mark_fvalue = value;
1094 }
1095
1096 fn set_mark_fx(&mut self, value: Option<String>) {
1097 self.mark_fx = value;
1098 }
1099
1100 fn set_mark_class(&mut self, class: Option<String>) {
1101 self.mark_user_class = class;
1102 }
1103
1104 fn set_mark_group(&mut self, group: Option<(String, String)>) {
1105 self.mark_group = group;
1106 }
1107
1108 fn set_layer_attrs(&mut self, attrs: Vec<(String, String)>) {
1109 self.layer_attrs = attrs;
1110 }
1111
1112 fn draw_circle(
1113 &mut self,
1114 (cx, cy): (f64, f64),
1115 radius: f64,
1116 style: &PointStyle,
1117 ) -> Result<(), RenderError> {
1118 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
1119 return Ok(());
1120 }
1121 let attrs = format!(
1122 "cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"{:.2}\" fill=\"{}\"{}",
1123 radius.max(0.0),
1124 rgb(style.color),
1125 opacity_attr("fill", style.alpha)
1126 );
1127 self.push_mark("circle", "point", &attrs);
1128 Ok(())
1129 }
1130
1131 fn draw_shape(
1135 &mut self,
1136 (cx, cy): (f64, f64),
1137 radius: f64,
1138 style: &PointStyle,
1139 ) -> Result<(), RenderError> {
1140 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
1141 return Ok(());
1142 }
1143 let r = radius.max(0.0);
1144 let color = rgb(style.color);
1145 let fill_opacity = opacity_attr("fill", style.alpha);
1146 let stroke_opacity = opacity_attr("stroke", style.alpha);
1147 let poly = |pts: &[(f64, f64)]| {
1148 if style.filled {
1149 format!("points=\"{}\" fill=\"{color}\"{fill_opacity}", points(pts))
1150 } else {
1151 format!(
1152 "points=\"{}\" fill=\"none\" stroke=\"{color}\"{stroke_opacity}",
1153 points(pts)
1154 )
1155 }
1156 };
1157 match style.shape {
1158 PointShape::Circle => return self.draw_circle((cx, cy), radius, style),
1159 PointShape::Square => {
1160 let a = poly(&[
1161 (cx - r, cy - r),
1162 (cx + r, cy - r),
1163 (cx + r, cy + r),
1164 (cx - r, cy + r),
1165 ]);
1166 self.push_mark("polygon", "point", &a);
1167 }
1168 PointShape::Triangle => {
1169 let a = poly(&[(cx, cy - r), (cx + r, cy + r), (cx - r, cy + r)]);
1170 self.push_mark("polygon", "point", &a);
1171 }
1172 PointShape::Diamond => {
1173 let a = poly(&[(cx, cy - r), (cx + r, cy), (cx, cy + r), (cx - r, cy)]);
1174 self.push_mark("polygon", "point", &a);
1175 }
1176 PointShape::Plus | PointShape::Cross => {
1177 let d = if style.shape == PointShape::Plus {
1178 format!(
1179 "M{:.2} {cy:.2}H{:.2}M{cx:.2} {:.2}V{:.2}",
1180 cx - r,
1181 cx + r,
1182 cy - r,
1183 cy + r
1184 )
1185 } else {
1186 format!(
1187 "M{:.2} {:.2}L{:.2} {:.2}M{:.2} {:.2}L{:.2} {:.2}",
1188 cx - r,
1189 cy - r,
1190 cx + r,
1191 cy + r,
1192 cx - r,
1193 cy + r,
1194 cx + r,
1195 cy - r
1196 )
1197 };
1198 let a = format!(
1199 "d=\"{d}\" fill=\"none\" stroke=\"{color}\" stroke-width=\"{:.2}\"{stroke_opacity}",
1200 (r / 2.5).clamp(1.0, 3.0)
1201 );
1202 self.push_mark("path", "point", &a);
1203 }
1204 }
1205 Ok(())
1206 }
1207
1208 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
1209 for run in finite_runs(pts) {
1215 self.draw_line_run(&run, style);
1216 }
1217 Ok(())
1218 }
1219
1220 fn draw_quadratic(
1223 &mut self,
1224 p0: (f64, f64),
1225 ctrl: (f64, f64),
1226 p1: (f64, f64),
1227 style: &LineStyle,
1228 ) -> Result<(), RenderError> {
1229 if !(p0.0.is_finite()
1230 && p0.1.is_finite()
1231 && ctrl.0.is_finite()
1232 && ctrl.1.is_finite()
1233 && p1.0.is_finite()
1234 && p1.1.is_finite())
1235 {
1236 return Ok(());
1237 }
1238 let dash = dasharray_attr(&style.linetype);
1239 let linecap = match style.lineend {
1240 LineEnd::Butt => "",
1241 LineEnd::Round => " stroke-linecap=\"round\"",
1242 LineEnd::Square => " stroke-linecap=\"square\"",
1243 };
1244 let d = format!(
1245 "M{:.2} {:.2} Q{:.2} {:.2} {:.2} {:.2}",
1246 p0.0, p0.1, ctrl.0, ctrl.1, p1.0, p1.1
1247 );
1248 let attrs = format!(
1249 "d=\"{d}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
1250 rgb(style.color),
1251 finite_or(style.width, 1.0),
1252 opacity_attr("stroke", style.alpha),
1253 dash,
1254 linecap
1255 );
1256 self.push_mark("path", "line", &attrs);
1257 Ok(())
1258 }
1259
1260 fn draw_rect(
1261 &mut self,
1262 top_left: (f64, f64),
1263 bottom_right: (f64, f64),
1264 style: &RectStyle,
1265 ) -> Result<(), RenderError> {
1266 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1267 self.rect_markup(top_left, bottom_right, &fill, style)
1268 }
1269
1270 fn draw_rect_pattern(
1273 &mut self,
1274 top_left: (f64, f64),
1275 bottom_right: (f64, f64),
1276 pattern: &FillPattern,
1277 style: &RectStyle,
1278 ) -> Result<(), RenderError> {
1279 let fill = self.pattern_url(pattern);
1280 self.rect_markup(top_left, bottom_right, &fill, style)
1281 }
1282
1283 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
1284 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1285 self.polygon_markup(pts, &fill, style)
1286 }
1287
1288 fn draw_polygon_pattern(
1291 &mut self,
1292 pts: &[(f64, f64)],
1293 pattern: &FillPattern,
1294 style: &RectStyle,
1295 ) -> Result<(), RenderError> {
1296 let fill = self.pattern_url(pattern);
1297 self.polygon_markup(pts, &fill, style)
1298 }
1299
1300 fn draw_polygon_rings(
1306 &mut self,
1307 rings: &[Vec<(f64, f64)>],
1308 style: &RectStyle,
1309 ) -> Result<(), RenderError> {
1310 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1311 self.polygon_rings_markup(rings, &fill, style)
1312 }
1313
1314 fn draw_polygon_rings_pattern(
1318 &mut self,
1319 rings: &[Vec<(f64, f64)>],
1320 pattern: &FillPattern,
1321 style: &RectStyle,
1322 ) -> Result<(), RenderError> {
1323 let fill = self.pattern_url(pattern);
1324 self.polygon_rings_markup(rings, &fill, style)
1325 }
1326
1327 fn draw_polygon_rings_precise(
1333 &mut self,
1334 rings: &[Vec<(f64, f64)>],
1335 pattern: Option<&FillPattern>,
1336 precision: Option<u8>,
1337 style: &RectStyle,
1338 ) -> Result<(), RenderError> {
1339 let Some(precision) = precision else {
1340 return match pattern {
1341 Some(p) => self.draw_polygon_rings_pattern(rings, p, style),
1342 None => self.draw_polygon_rings(rings, style),
1343 };
1344 };
1345 let d = compact_rings_d(rings, precision);
1346 if d.is_empty() {
1347 return Ok(());
1348 }
1349 let fill = match pattern {
1350 Some(p) => self.pattern_url(p),
1351 None => style.fill.map(rgb).unwrap_or_else(|| "none".into()),
1352 };
1353 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
1354 let attrs = format!(
1355 "d=\"{d}\" fill-rule=\"evenodd\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
1356 opacity_attr("fill", style.alpha),
1357 finite_or(style.stroke_width, 0.0)
1358 );
1359 self.push_mark("path", "band", &attrs);
1360 Ok(())
1361 }
1362
1363 fn draw_text(
1364 &mut self,
1365 text: &str,
1366 pos: (f64, f64),
1367 style: &TextStyle,
1368 ) -> Result<(), RenderError> {
1369 self.draw_text_with_title(text, pos, style, None)
1370 }
1371
1372 fn draw_text_with_title(
1376 &mut self,
1377 text: &str,
1378 (x, y): (f64, f64),
1379 style: &TextStyle,
1380 title: Option<&str>,
1381 ) -> Result<(), RenderError> {
1382 if !(x.is_finite() && y.is_finite()) {
1383 return Ok(());
1384 }
1385 let anchor = match style.anchor {
1386 TextAnchor::Start => "start",
1387 TextAnchor::Middle => "middle",
1388 TextAnchor::End => "end",
1389 };
1390 let family = escape_attr(style.family.as_deref().unwrap_or("sans-serif"));
1391 let weight = if style.face == FontFace::Bold {
1392 " font-weight=\"bold\""
1393 } else {
1394 ""
1395 };
1396 let fstyle = if style.face == FontFace::Italic {
1397 " font-style=\"italic\""
1398 } else {
1399 ""
1400 };
1401 let halo = text_halo_attrs(style);
1402 let angle = finite_or(style.angle, 0.0);
1403 let transform = if angle.abs() > 0.01 {
1404 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
1405 } else {
1406 String::new()
1407 };
1408 let class = format!(
1409 "{}{}{}",
1410 self.text_data_attrs(),
1411 self.layer_flag_attrs(false),
1412 self.mark_class("text")
1413 );
1414 let title_tag = match title {
1415 Some(t) => format!("<title>{}</title>", escape(t)),
1416 None => String::new(),
1417 };
1418 let markup = format!(
1419 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
1420 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle}{halo} fill=\"{}\"{transform}{class}>{title_tag}{}</text>",
1421 finite_or(style.size, 12.0),
1422 rgb(style.color),
1423 escape(text)
1424 );
1425 self.emit(&markup);
1426 Ok(())
1427 }
1428
1429 fn draw_text_runs(
1436 &mut self,
1437 runs: &[(&str, Option<&str>)],
1438 (x, y): (f64, f64),
1439 style: &TextStyle,
1440 ) -> Result<(), RenderError> {
1441 if !(x.is_finite() && y.is_finite()) {
1442 return Ok(());
1443 }
1444 let anchor = match style.anchor {
1445 TextAnchor::Start => "start",
1446 TextAnchor::Middle => "middle",
1447 TextAnchor::End => "end",
1448 };
1449 let family = escape_attr(style.family.as_deref().unwrap_or("sans-serif"));
1450 let weight = if style.face == FontFace::Bold {
1451 " font-weight=\"bold\""
1452 } else {
1453 ""
1454 };
1455 let fstyle = if style.face == FontFace::Italic {
1456 " font-style=\"italic\""
1457 } else {
1458 ""
1459 };
1460 let halo = text_halo_attrs(style);
1461 let angle = finite_or(style.angle, 0.0);
1462 let transform = if angle.abs() > 0.01 {
1463 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
1464 } else {
1465 String::new()
1466 };
1467 let class = format!(
1468 "{}{}{}",
1469 self.text_data_attrs(),
1470 self.layer_flag_attrs(false),
1471 self.mark_class("text")
1472 );
1473 let mut inner = String::new();
1474 for (text, href) in runs {
1475 match href {
1476 Some(u) => inner.push_str(&format!(
1477 "<a href=\"{}\" target=\"_blank\" rel=\"noopener\"><tspan text-decoration=\"underline\">{}</tspan></a>",
1478 escape_attr(u),
1479 escape(text)
1480 )),
1481 None => inner.push_str(&format!("<tspan>{}</tspan>", escape(text))),
1482 }
1483 }
1484 let markup = format!(
1485 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
1486 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle}{halo} fill=\"{}\"{transform}{class}>{inner}</text>",
1487 finite_or(style.size, 12.0),
1488 rgb(style.color),
1489 );
1490 self.emit(&markup);
1491 Ok(())
1492 }
1493}
1494
1495fn text_halo_attrs(style: &TextStyle) -> String {
1505 let Some(bg) = style.bg_colour else {
1506 return String::new();
1507 };
1508 let r = finite_or(style.bg_r, 0.0);
1509 if r <= 0.0 {
1510 return String::new();
1511 }
1512 format!(
1513 " stroke=\"{}\" stroke-width=\"{:.2}\" stroke-linejoin=\"round\" paint-order=\"stroke\"",
1514 rgb(bg),
1515 2.0 * r
1516 )
1517}
1518
1519#[cfg(test)]
1520mod tests {
1521 use super::*;
1522 use crate::render::Rect;
1523
1524 #[test]
1527 fn issue221_backend_rechecks_class_group_and_layer_attrs() {
1528 let mut b = backend();
1529 b.set_mark_geom(Some("point"));
1530 b.set_mark_class(Some("ok \"bad 1x ok".into()));
1531 b.set_layer_attrs(vec![
1532 ("aria-hidden".into(), "maybe".into()),
1533 ("pointer-events".into(), "none\" onload=\"x".into()),
1534 ("style".into(), "fill:red".into()),
1535 ("class".into(), "lay".into()),
1536 ]);
1537 b.set_mark_group(Some(("onclick".into(), "x".into())));
1538 b.draw_circle((1.0, 1.0), 1.0, &PointStyle::default())
1539 .unwrap();
1540 b.set_mark_group(Some(("data-of".into(), "n".into())));
1541 let runs = [("a", None), ("b", Some("https://e.x"))];
1542 b.draw_text_runs(&runs, (5.0, 5.0), &TextStyle::default())
1543 .unwrap();
1544 b.set_mark_group(None);
1545 b.set_mark_geom(None);
1546 let body = b.body().to_string();
1547 assert!(
1548 body.contains(r#"class="gg-mark gg-point lay ok""#),
1549 "{body}"
1550 );
1551 assert!(!body.contains("bad") && !body.contains("1x"), "{body}");
1552 assert!(
1553 !body.contains("aria-hidden") && !body.contains("onload"),
1554 "{body}"
1555 );
1556 assert!(
1557 !body.contains("style=") && !body.contains("onclick"),
1558 "{body}"
1559 );
1560 assert!(body.contains(r#"<g data-of="n"><text"#), "{body}");
1561 assert!(body.contains("gg-text lay ok"), "{body}");
1562 assert!(b.finish().contains(r#"<g data-of="n">"#));
1564 }
1565
1566 fn backend() -> SvgBackend {
1567 SvgBackend::new(
1568 100,
1569 100,
1570 Rect {
1571 x: 0.0,
1572 y: 0.0,
1573 width: 100.0,
1574 height: 100.0,
1575 },
1576 )
1577 }
1578
1579 #[test]
1580 fn draw_quadratic_emits_a_path_with_m_and_q() {
1581 let mut b = backend();
1582 b.draw_quadratic((0.0, 0.0), (10.0, 20.0), (20.0, 0.0), &LineStyle::default())
1583 .unwrap();
1584 let svg = b.finish();
1585 assert!(svg.contains("<path"));
1586 assert!(svg.contains("d=\"M0.00 0.00 Q10.00 20.00 20.00 0.00\""));
1587 assert!(!svg.contains("NaN"));
1588 assert!(!svg.contains("inf"));
1589 }
1590
1591 #[test]
1592 fn draw_quadratic_drops_non_finite_input() {
1593 let mut b = backend();
1594 b.draw_quadratic(
1595 (f64::NAN, 0.0),
1596 (10.0, 20.0),
1597 (20.0, 0.0),
1598 &LineStyle::default(),
1599 )
1600 .unwrap();
1601 b.draw_quadratic(
1602 (0.0, 0.0),
1603 (f64::INFINITY, 20.0),
1604 (20.0, 0.0),
1605 &LineStyle::default(),
1606 )
1607 .unwrap();
1608 let svg = b.finish();
1609 assert!(!svg.contains("<path"));
1610 }
1611
1612 #[test]
1613 fn draw_line_omits_linecap_by_default_but_writes_it_when_set() {
1614 let mut b = backend();
1615 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &LineStyle::default())
1616 .unwrap();
1617 let svg = b.finish();
1618 assert!(!svg.contains("stroke-linecap"));
1619
1620 let mut b2 = backend();
1621 let style = LineStyle {
1622 lineend: LineEnd::Round,
1623 ..Default::default()
1624 };
1625 b2.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
1626 let svg2 = b2.finish();
1627 assert!(svg2.contains("stroke-linecap=\"round\""));
1628 }
1629
1630 #[test]
1631 fn square_linecap_is_written_on_both_draw_line_and_draw_quadratic() {
1632 let style = LineStyle {
1633 lineend: LineEnd::Square,
1634 ..Default::default()
1635 };
1636 let mut b = backend();
1637 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
1638 assert!(b.finish().contains("stroke-linecap=\"square\""));
1639
1640 let mut b2 = backend();
1641 b2.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &style)
1642 .unwrap();
1643 assert!(b2.finish().contains("stroke-linecap=\"square\""));
1644 }
1645
1646 #[test]
1651 fn set_mark_attrs_defence_in_depth_drops_invalid_names() {
1652 let mut b = backend();
1653 b.set_mark_attrs(vec![
1654 ("not-data-prefixed".to_string(), "x".to_string()),
1655 ("data-".to_string(), "x".to_string()), ("data-Upper".to_string(), "x".to_string()), ("data-ok".to_string(), "valid".to_string()), ]);
1659 b.draw_circle((1.0, 1.0), 1.0, &PointStyle::default())
1660 .unwrap();
1661 let svg = b.finish();
1662 assert!(!svg.contains("not-data-prefixed"));
1663 assert!(!svg.contains("data-Upper"));
1664 assert!(!svg.contains("data-=\""));
1665 assert!(svg.contains("data-ok=\"valid\""));
1666 }
1667
1668 fn stripe() -> FillPattern {
1671 FillPattern::Stripe {
1672 fg: (10, 20, 30),
1673 bg: (200, 210, 220),
1674 size_px: 4.0,
1675 angle_deg: 45.0,
1676 }
1677 }
1678
1679 fn pattern_id(svg: &str) -> String {
1680 let i = svg.find("<pattern id=\"").unwrap() + "<pattern id=\"".len();
1681 svg[i..i + svg[i..].find('"').unwrap()].to_string()
1682 }
1683
1684 #[test]
1685 fn a_pattern_fill_registers_one_defs_pattern_and_references_it() {
1686 let mut b = backend();
1687 b.draw_rect_pattern((0.0, 0.0), (10.0, 10.0), &stripe(), &RectStyle::default())
1688 .unwrap();
1689 let svg = b.finish();
1690 assert_eq!(svg.matches("<pattern ").count(), 1, "{svg}");
1691 let id = pattern_id(&svg);
1692 assert!(
1693 svg.contains(&format!("fill=\"url(#{id})\"")),
1694 "rect references the registered pattern id {id}: {svg}"
1695 );
1696 }
1697
1698 #[test]
1699 fn a_rounded_rect_pattern_still_rounds_its_corners() {
1700 let mut b = backend();
1701 b.draw_rect_pattern(
1702 (0.0, 0.0),
1703 (10.0, 10.0),
1704 &stripe(),
1705 &RectStyle {
1706 corner_radius: 3.0,
1707 ..Default::default()
1708 },
1709 )
1710 .unwrap();
1711 let svg = b.finish();
1712 assert!(svg.contains("rx=\"3.00\""), "{svg}");
1713 assert!(svg.contains("fill=\"url(#"), "{svg}");
1714 }
1715
1716 #[test]
1717 fn an_equal_pattern_reuses_the_same_defs_entry() {
1718 let mut b = backend();
1719 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1720 .unwrap();
1721 b.draw_polygon_pattern(
1722 &[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)],
1723 &stripe(),
1724 &RectStyle::default(),
1725 )
1726 .unwrap();
1727 let svg = b.finish();
1728 assert_eq!(
1729 svg.matches("<pattern ").count(),
1730 1,
1731 "two equal patterns share one <defs> entry: {svg}"
1732 );
1733 }
1734
1735 #[test]
1736 fn different_patterns_get_their_own_defs_entries() {
1737 let mut b = backend();
1738 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1739 .unwrap();
1740 let other = FillPattern::Stripe {
1741 fg: (1, 1, 1),
1742 bg: (2, 2, 2),
1743 size_px: 8.0,
1744 angle_deg: 0.0,
1745 };
1746 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &other, &RectStyle::default())
1747 .unwrap();
1748 let svg = b.finish();
1749 assert_eq!(svg.matches("<pattern ").count(), 2, "{svg}");
1750 }
1751
1752 #[test]
1753 fn no_pattern_used_means_no_defs_block_at_all() {
1754 let mut b = backend();
1755 b.draw_rect((0.0, 0.0), (5.0, 5.0), &RectStyle::default())
1756 .unwrap();
1757 let svg = b.finish();
1758 assert!(!svg.contains("<defs>"), "{svg}");
1759 }
1760
1761 #[test]
1762 fn pattern_fill_cuts_out_holes_like_a_flat_fill_does() {
1763 let mut b = backend();
1764 let outer = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1765 let hole = vec![(2.0, 2.0), (8.0, 2.0), (8.0, 8.0), (2.0, 8.0)];
1766 b.draw_polygon_rings_pattern(&[outer, hole], &stripe(), &RectStyle::default())
1767 .unwrap();
1768 let svg = b.finish();
1769 assert!(svg.contains("fill-rule=\"evenodd\""), "{svg}");
1770 assert_eq!(
1771 svg.matches("<path").count(),
1772 1,
1773 "one path, both rings: {svg}"
1774 );
1775 }
1776
1777 #[test]
1778 fn two_backends_on_one_page_never_collide_on_a_pattern_id() {
1779 let mut a = backend();
1780 a.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1781 .unwrap();
1782 let mut b = backend();
1783 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1784 .unwrap();
1785 let (sa, sb) = (a.finish(), b.finish());
1786 assert_ne!(pattern_id(&sa), pattern_id(&sb), "{sa}\n{sb}");
1787 }
1788
1789 fn decode_compact_d(d: &str) -> Vec<Vec<(f64, f64)>> {
1796 let b = d.as_bytes();
1797 let mut i = 0;
1798 let read_num = |b: &[u8], i: &mut usize| -> f64 {
1799 let start = *i;
1800 if *i < b.len() && b[*i] == b'-' {
1801 *i += 1;
1802 }
1803 while *i < b.len() && b[*i].is_ascii_digit() {
1804 *i += 1;
1805 }
1806 if *i < b.len() && b[*i] == b'.' {
1807 *i += 1;
1808 while *i < b.len() && b[*i].is_ascii_digit() {
1809 *i += 1;
1810 }
1811 }
1812 std::str::from_utf8(&b[start..*i]).unwrap().parse().unwrap()
1813 };
1814 let is_num_start = |c: u8| c.is_ascii_digit() || c == b'-' || c == b'.';
1815 let mut rings = Vec::new();
1816 let mut current = Vec::new();
1817 let mut cursor = (0.0, 0.0);
1818 while i < b.len() {
1819 match b[i] {
1820 b'M' => {
1821 if !current.is_empty() {
1822 rings.push(std::mem::take(&mut current));
1823 }
1824 i += 1;
1825 let x = read_num(b, &mut i);
1826 if i < b.len() && b[i] == b',' {
1827 i += 1;
1828 }
1829 let y = read_num(b, &mut i);
1830 cursor = (x, y);
1831 current.push(cursor);
1832 }
1833 b'l' => {
1834 i += 1;
1835 while i < b.len() && is_num_start(b[i]) {
1836 let dx = read_num(b, &mut i);
1837 if i < b.len() && b[i] == b',' {
1838 i += 1;
1839 }
1840 let dy = read_num(b, &mut i);
1841 if i < b.len() && b[i] == b',' {
1842 i += 1;
1843 }
1844 cursor = (cursor.0 + dx, cursor.1 + dy);
1845 current.push(cursor);
1846 }
1847 }
1848 b'z' => {
1849 i += 1;
1850 if !current.is_empty() {
1851 rings.push(std::mem::take(&mut current));
1852 }
1853 }
1854 _ => i += 1,
1855 }
1856 }
1857 if !current.is_empty() {
1858 rings.push(current);
1859 }
1860 rings
1861 }
1862
1863 fn noisy_ring(n: usize, cx: f64, cy: f64, r: f64) -> Vec<(f64, f64)> {
1866 (0..n)
1867 .map(|i| {
1868 let t = i as f64 / n as f64 * std::f64::consts::TAU;
1869 let wobble = 1.0 + 0.03 * ((i * 7) as f64).sin();
1870 (cx + r * wobble * t.cos(), cy + r * wobble * t.sin())
1871 })
1872 .collect()
1873 }
1874
1875 #[test]
1876 fn compact_encoding_is_well_under_the_absolute_encodings_size() {
1877 let ring = noisy_ring(2000, 400.0, 300.0, 280.0);
1881 let mut b = backend();
1882 b.draw_polygon_rings(std::slice::from_ref(&ring), &RectStyle::default())
1883 .unwrap();
1884 let absolute_svg = b.finish();
1885 let absolute_d = absolute_svg
1886 .split("d=\"")
1887 .nth(1)
1888 .and_then(|s| s.split('"').next())
1889 .expect("absolute path d");
1890
1891 let compact_d = compact_rings_d(std::slice::from_ref(&ring), 1);
1892
1893 assert!(
1894 compact_d.len() * 3 < absolute_d.len() * 2,
1895 "compact {} bytes should be well under 2/3 of absolute {} bytes",
1896 compact_d.len(),
1897 absolute_d.len()
1898 );
1899 }
1900
1901 #[test]
1902 fn compact_encoding_round_trips_within_precision() {
1903 let ring = noisy_ring(40, 100.0, 80.0, 60.0);
1904 let precision: u8 = 1;
1905 let d = compact_rings_d(std::slice::from_ref(&ring), precision);
1906 let decoded = decode_compact_d(&d);
1907 assert_eq!(decoded.len(), 1, "{d}");
1908 assert_eq!(decoded[0].len(), ring.len(), "{d}");
1909 let step_tol = 0.5 * 10f64.powi(-(precision as i32));
1914 for (k, (orig, got)) in ring.iter().zip(decoded[0].iter()).enumerate() {
1915 let tol = step_tol * (k as f64 + 1.0) + 1e-9;
1916 assert!(
1917 (orig.0 - got.0).abs() <= tol && (orig.1 - got.1).abs() <= tol,
1918 "point {k}: {orig:?} vs {got:?} (tol {tol}): {d}"
1919 );
1920 }
1921 }
1922
1923 #[test]
1924 fn compact_encoding_round_trips_a_hole_and_a_multipolygons_parts() {
1925 let part1 = vec![(0.0, 0.0), (20.0, 0.0), (20.0, 20.0), (0.0, 20.0)];
1929 let part2_outer = vec![(50.0, 0.0), (90.0, 0.0), (90.0, 40.0), (50.0, 40.0)];
1930 let part2_hole = vec![(60.0, 10.0), (80.0, 10.0), (80.0, 30.0), (60.0, 30.0)];
1931 let rings = vec![part1.clone(), part2_outer.clone(), part2_hole.clone()];
1932 let d = compact_rings_d(&rings, 1);
1933 let decoded = decode_compact_d(&d);
1934 assert_eq!(decoded.len(), 3, "{d}");
1935 for (orig, got) in [part1, part2_outer, part2_hole].iter().zip(decoded.iter()) {
1936 assert_eq!(orig.len(), got.len(), "{d}");
1937 for (o, g) in orig.iter().zip(got.iter()) {
1938 assert!((o.0 - g.0).abs() <= 0.1 && (o.1 - g.1).abs() <= 0.1, "{d}");
1939 }
1940 }
1941 assert!(d.contains("M0,0l20,0,0,20-20,0z"), "{d}");
1942 }
1943}