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 svg_title: Option<String>,
77 svg_desc: Option<String>,
82}
83
84static NEXT_ID_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
87
88impl SvgBackend {
89 pub fn new(width: u32, height: u32, plot_area: Rect) -> Self {
90 SvgBackend {
91 plot_area,
92 total_area: Rect {
93 x: 0.0,
94 y: 0.0,
95 width: width as f64,
96 height: height as f64,
97 },
98 body: String::new(),
99 tooltip: None,
100 axis_key: None,
101 series_key: None,
102 value_key: None,
103 mark_part: None,
104 mark_attrs: Vec::new(),
105 mark_geom: None,
106 root_attrs: Vec::new(),
107 warnings: Vec::new(),
108 plot_attr: true,
109 formatted_values: false,
110 mark_fvalue: None,
111 mark_fx: None,
112 patterns: Vec::new(),
113 id_prefix: format!(
114 "gg{:x}",
115 NEXT_ID_PREFIX.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
116 ),
117 mark_user_class: None,
118 mark_group: None,
119 layer_attrs: Vec::new(),
120 groups: Vec::new(),
121 group_slots: Vec::new(),
122 svg_title: None,
123 svg_desc: None,
124 }
125 }
126
127 fn pattern_url(&mut self, pattern: &FillPattern) -> String {
131 if let Some((_, id)) = self.patterns.iter().find(|(p, _)| p == pattern) {
132 return format!("url(#{id})");
133 }
134 let id = format!("{}-pat-{}", self.id_prefix, self.patterns.len());
135 self.patterns.push((*pattern, id.clone()));
136 format!("url(#{id})")
137 }
138
139 fn rect_markup(
142 &mut self,
143 (x0, y0): (f64, f64),
144 (x1, y1): (f64, f64),
145 fill: &str,
146 style: &RectStyle,
147 ) -> Result<(), RenderError> {
148 if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) {
149 return Ok(());
150 }
151 let (x, y) = (x0.min(x1), y0.min(y1));
152 let (w, h) = ((x1 - x0).abs(), (y1 - y0).abs());
153 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
154 let r = style.corner_radius.max(0.0).min(w / 2.0).min(h / 2.0);
157 let radius = if r > 0.0 {
158 format!(" rx=\"{r:.2}\" ry=\"{r:.2}\"")
159 } else {
160 String::new()
161 };
162 let attrs = format!(
163 "x=\"{x:.2}\" y=\"{y:.2}\" width=\"{w:.2}\" height=\"{h:.2}\"{radius} fill=\"{fill}\"{} \
164 stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
165 opacity_attr("fill", style.alpha),
166 finite_or(style.stroke_width, 0.0)
167 );
168 self.push_mark("rect", "bar", &attrs);
169 Ok(())
170 }
171
172 fn polygon_markup(
174 &mut self,
175 pts: &[(f64, f64)],
176 fill: &str,
177 style: &RectStyle,
178 ) -> Result<(), RenderError> {
179 let pts = finite_pts(pts);
180 if pts.len() < 3 {
181 return Ok(());
182 }
183 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
184 let attrs = format!(
185 "points=\"{}\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
186 points(&pts),
187 opacity_attr("fill", style.alpha),
188 finite_or(style.stroke_width, 0.0)
189 );
190 self.push_mark("polygon", "band", &attrs);
191 Ok(())
192 }
193
194 fn polygon_rings_markup(
197 &mut self,
198 rings: &[Vec<(f64, f64)>],
199 fill: &str,
200 style: &RectStyle,
201 ) -> Result<(), RenderError> {
202 let mut d = String::new();
203 for ring in rings {
204 let pts = finite_pts(ring);
205 if pts.len() < 3 {
206 continue;
207 }
208 d.push('M');
209 d.push_str(&format!("{},{}", num(pts[0].0), num(pts[0].1)));
210 for p in &pts[1..] {
211 d.push('L');
212 d.push_str(&format!("{},{}", num(p.0), num(p.1)));
213 }
214 d.push('Z');
215 }
216 if d.is_empty() {
217 return Ok(());
218 }
219 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
220 let attrs = format!(
221 "d=\"{d}\" fill-rule=\"evenodd\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
222 opacity_attr("fill", style.alpha),
223 finite_or(style.stroke_width, 0.0)
224 );
225 self.push_mark("path", "band", &attrs);
226 Ok(())
227 }
228
229 fn pattern_defs(&self) -> String {
234 if self.patterns.is_empty() {
235 return String::new();
236 }
237 let mut defs = String::from("<defs>");
238 for (p, id) in &self.patterns {
239 let FillPattern::Stripe {
240 fg,
241 bg,
242 size_px,
243 angle_deg,
244 } = p;
245 let size = if *size_px > 0.0 { *size_px } else { 1.0 };
246 defs.push_str(&format!(
247 "<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>",
248 rgb(*bg),
249 size * 0.3,
250 rgb(*fg),
251 ));
252 }
253 defs.push_str("</defs>");
254 defs
255 }
256
257 pub(crate) fn set_formatted_values(&mut self, on: bool) {
261 self.formatted_values = on;
262 }
263
264 pub fn set_root_attrs(&mut self, attrs: Vec<(String, String)>) {
268 self.root_attrs = attrs;
269 }
270
271 pub fn extend_root_attrs(&mut self, attrs: Vec<(String, String)>) {
279 self.root_attrs.extend(attrs);
280 }
281
282 pub(crate) fn without_plot_attr(&mut self) {
284 self.plot_attr = false;
285 }
286
287 pub(crate) fn set_svg_title(&mut self, title: Option<String>) {
292 self.svg_title = title;
293 }
294
295 pub(crate) fn set_svg_desc(&mut self, desc: Option<String>) {
300 self.svg_desc = desc;
301 }
302
303 pub(crate) fn push_raw(&mut self, markup: &str) {
306 self.body.push_str(markup);
307 }
308
309 pub(crate) fn body(&mut self) -> &str {
312 self.flush_groups();
313 &self.body
314 }
315
316 fn emit(&mut self, markup: &str) {
319 let key = match (&self.mark_group, &self.mark_geom) {
320 (Some(k), Some(_)) => k,
321 _ => {
322 self.body.push_str(markup);
323 return;
324 }
325 };
326 match self
327 .groups
328 .iter_mut()
329 .find(|(n, v, _)| n == &key.0 && v == &key.1)
330 {
331 Some((_, _, content)) => content.push_str(markup),
332 None => {
333 self.group_slots.push((self.body.len(), self.groups.len()));
334 self.groups
335 .push((key.0.clone(), key.1.clone(), markup.to_string()));
336 }
337 }
338 }
339
340 fn flush_groups(&mut self) {
346 if self.group_slots.is_empty() {
347 return;
348 }
349 let groups = std::mem::take(&mut self.groups);
350 let slots = std::mem::take(&mut self.group_slots);
351 let extra: usize = groups
352 .iter()
353 .map(|(n, v, c)| n.len() + v.len() + c.len() + 12)
354 .sum();
355 let mut out = String::with_capacity(self.body.len() + extra);
356 let mut last = 0;
357 for (off, gi) in slots {
358 out.push_str(&self.body[last..off]);
359 last = off;
360 let (name, value, content) = &groups[gi];
361 if is_valid_mark_attr_name(name) {
362 out.push_str(&format!(
363 "<g {name}=\"{}\">{content}</g>",
364 escape_attr(value)
365 ));
366 } else {
367 out.push_str(content);
368 }
369 }
370 out.push_str(&self.body[last..]);
371 self.body = out;
372 }
373
374 pub(crate) fn wrap_body_in_group(&mut self, attrs: &[(String, String)]) {
381 self.flush_groups();
382 if attrs.is_empty() {
383 return;
384 }
385 let mut open = String::from("<g");
386 for (k, v) in attrs {
387 open.push_str(&format!(" {k}=\"{}\"", escape_attr(v)));
388 }
389 open.push('>');
390 self.body = format!("{open}{}</g>", self.body);
391 }
392
393 pub fn take_warnings(&mut self) -> Vec<String> {
395 std::mem::take(&mut self.warnings)
396 }
397
398 fn mark_class(&self, kind: &str) -> String {
405 match &self.mark_geom {
406 Some(g) => {
407 let mut class = format!("gg-mark gg-{kind}");
408 let layer = self
409 .layer_attrs
410 .iter()
411 .filter(|(n, _)| n == "class")
412 .map(|(_, v)| v.as_str());
413 let mut seen: Vec<&str> = Vec::new();
414 for t in layer
415 .chain(self.mark_user_class.as_deref())
416 .flat_map(str::split_whitespace)
417 {
418 if crate::aes::is_valid_class_token(t) && !seen.contains(&t) {
419 seen.push(t);
420 class.push(' ');
421 class.push_str(t);
422 }
423 }
424 format!(" class=\"{class}\" data-geom=\"{}\"", escape_attr(g))
425 }
426 None => String::new(),
427 }
428 }
429
430 fn layer_flag_attrs(&self, aria_hidden_written: bool) -> String {
435 let mut out = String::new();
436 if self.mark_geom.is_none() {
437 return out;
438 }
439 for (name, value) in &self.layer_attrs {
440 let ok = match name.as_str() {
441 "aria-hidden" => !aria_hidden_written && matches!(value.as_str(), "true" | "false"),
442 "pointer-events" => POINTER_EVENTS_VALUES.contains(&value.as_str()),
443 _ => false,
444 };
445 if ok {
446 out.push_str(&format!(" {name}=\"{value}\""));
447 }
448 }
449 out
450 }
451
452 fn text_data_attrs(&self) -> String {
457 let mut pairs: Vec<(&str, &str)> = Vec::new();
458 if self.mark_geom.is_none() {
459 return String::new();
460 }
461 for (name, val) in self.layer_attrs.iter().chain(self.mark_attrs.iter()) {
462 if !is_valid_mark_attr_name(name) {
463 continue;
464 }
465 match pairs.iter_mut().find(|(n, _)| n == name) {
466 Some(existing) => existing.1 = val,
467 None => pairs.push((name, val)),
468 }
469 }
470 let mut out = String::new();
471 for (name, val) in pairs {
472 out.push_str(&format!(" {name}=\"{}\"", escape_attr(val)));
473 }
474 out
475 }
476
477 fn push_mark(&mut self, tag: &str, kind: &str, attrs: &str) {
485 let mut pairs: Vec<(String, String)> = Vec::new();
495 for (name, val) in [
496 ("data-x", &self.axis_key),
497 ("data-series", &self.series_key),
498 ("data-value", &self.value_key),
499 ] {
500 if let Some(v) = val {
501 pairs.push((name.to_string(), v.clone()));
502 }
503 }
504 if let Some(fx) = &self.mark_fx {
508 if self.axis_key.as_deref() != Some(fx.as_str()) {
509 pairs.push(("data-fx".to_string(), fx.clone()));
510 }
511 }
512 if let Some(fvalue) = &self.mark_fvalue {
513 pairs.push(("data-fvalue".to_string(), fvalue.clone()));
514 }
515 let layer_data = self.layer_attrs.iter().filter(|_| self.mark_geom.is_some());
518 for (name, val) in layer_data.chain(self.mark_attrs.iter()) {
519 if !is_valid_mark_attr_name(name) {
520 continue;
521 }
522 match pairs.iter_mut().find(|(n, _)| n == name) {
523 Some(existing) => existing.1 = val.clone(),
524 None => pairs.push((name.clone(), val.clone())),
525 }
526 }
527 if let Some(part) = &self.mark_part {
532 match pairs.iter_mut().find(|(n, _)| n == "data-part") {
533 Some(existing) => existing.1 = part.clone(),
534 None => pairs.push(("data-part".to_string(), part.clone())),
535 }
536 }
537 let mut data = String::new();
538 for (name, val) in &pairs {
539 data.push_str(&format!(" {name}=\"{}\"", escape_attr(val)));
540 }
541 if self.mark_part.is_some() {
542 data.push_str(" aria-hidden=\"true\"");
543 }
544 data.push_str(&self.layer_flag_attrs(self.mark_part.is_some()));
545 let effective_kind: &str = self.mark_part.as_deref().unwrap_or(kind);
550 data.push_str(&self.mark_class(effective_kind));
551 let mark = if self.mark_part.is_some() {
552 format!("<{tag} {attrs}{data}/>")
553 } else {
554 match &self.tooltip {
555 Some(t) => format!("<{tag} {attrs}{data}><title>{}</title></{tag}>", escape(t)),
556 None => format!("<{tag} {attrs}{data}/>"),
557 }
558 };
559 self.emit(&mark);
560 }
561
562 fn root_open(&self, prefix: &str) -> String {
563 let p = &self.plot_area;
564 let (w, h) = (self.total_area.width as i64, self.total_area.height as i64);
565 let mut extra = String::new();
566 for (k, v) in &self.root_attrs {
567 extra.push_str(&format!(" {k}=\"{}\"", escape_attr(v)));
568 }
569 let plot = if self.plot_attr {
570 format!(
571 " data-plot=\"{} {} {} {}\"",
572 num(p.x),
573 num(p.y),
574 num(p.width),
575 num(p.height),
576 )
577 } else {
578 String::new()
579 };
580 let mut access = String::new();
585 if self.svg_title.is_some() || self.svg_desc.is_some() {
586 access.push_str(" role=\"img\"");
587 }
588 if self.svg_title.is_some() {
589 access.push_str(&format!(" aria-labelledby=\"{}-title\"", self.id_prefix));
590 }
591 if self.svg_desc.is_some() {
592 access.push_str(&format!(" aria-describedby=\"{}-desc\"", self.id_prefix));
593 }
594 format!(
595 "<svg {prefix}width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\"{plot}{access}{extra}>"
596 )
597 }
598
599 fn access_children(&self) -> String {
604 let mut out = String::new();
605 if let Some(t) = &self.svg_title {
606 out.push_str(&format!(
607 "<title id=\"{}-title\">{}</title>",
608 self.id_prefix,
609 escape(t)
610 ));
611 }
612 if let Some(d) = &self.svg_desc {
613 out.push_str(&format!(
614 "<desc id=\"{}-desc\">{}</desc>",
615 self.id_prefix,
616 escape(d)
617 ));
618 }
619 out
620 }
621
622 pub fn finish(mut self) -> String {
626 self.flush_groups();
627 let open = self.root_open("xmlns=\"http://www.w3.org/2000/svg\" ");
628 let access = self.access_children();
629 let defs = self.pattern_defs();
630 format!("{open}{access}{defs}{}</svg>", self.body)
631 }
632
633 pub fn finish_fragment(mut self, x: f64, y: f64) -> String {
638 self.flush_groups();
639 let open = self.root_open(&format!("x=\"{}\" y=\"{}\" ", num(x), num(y)));
640 let access = self.access_children();
641 let defs = self.pattern_defs();
642 format!("{open}{access}{defs}{}</svg>", self.body)
643 }
644
645 fn draw_line_run(&mut self, pts: &[(f64, f64)], style: &LineStyle) {
648 let dash = dasharray_attr(&style.linetype);
649 let linecap = match style.lineend {
652 LineEnd::Butt => "",
653 LineEnd::Round => " stroke-linecap=\"round\"",
654 LineEnd::Square => " stroke-linecap=\"square\"",
655 };
656 let attrs = format!(
657 "points=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
658 points(pts),
659 rgb(style.color),
660 finite_or(style.width, 1.0),
661 opacity_attr("stroke", style.alpha),
662 dash,
663 linecap
664 );
665 self.push_mark("polyline", "line", &attrs);
666 }
667}
668
669fn num(v: f64) -> String {
672 if v.is_finite() {
673 format!("{v:.2}")
674 } else {
675 "0".to_string()
676 }
677}
678
679fn dasharray_attr(linetype: &Linetype) -> String {
683 let pattern = linetype.dash_pattern();
684 if pattern.is_empty() {
685 return String::new();
686 }
687 let s = pattern
688 .iter()
689 .flat_map(|(d, g)| [*d, *g])
690 .map(|v| format!("{v}"))
691 .collect::<Vec<_>>()
692 .join(",");
693 format!(" stroke-dasharray=\"{s}\"")
694}
695
696fn compact_rings_d(rings: &[Vec<(f64, f64)>], precision: u8) -> String {
706 let mut d = String::new();
707 for ring in rings {
708 let pts = finite_pts(ring);
709 if pts.len() < 3 {
710 continue;
711 }
712 d.push('M');
713 push_num(&mut d, &fmt_compact(pts[0].0, precision));
714 push_num(&mut d, &fmt_compact(pts[0].1, precision));
715 if pts.len() > 1 {
716 d.push('l');
717 let mut prev = pts[0];
718 for p in &pts[1..] {
719 push_num(&mut d, &fmt_compact(p.0 - prev.0, precision));
720 push_num(&mut d, &fmt_compact(p.1 - prev.1, precision));
721 prev = *p;
722 }
723 }
724 d.push('z');
725 }
726 d
727}
728
729fn push_num(d: &mut String, s: &str) {
737 let needs_sep =
738 matches!(d.chars().last(), Some(c) if c.is_ascii_digit()) && !s.starts_with('-');
739 if needs_sep {
740 d.push(',');
741 }
742 d.push_str(s);
743}
744
745fn fmt_compact(v: f64, precision: u8) -> String {
751 if !v.is_finite() {
752 return "0".to_string();
753 }
754 let s = format!("{v:.*}", precision as usize);
755 let trimmed = if s.contains('.') {
756 s.trim_end_matches('0').trim_end_matches('.')
757 } else {
758 s.as_str()
759 };
760 match trimmed {
761 "" | "-" | "-0" => "0".to_string(),
762 other => other.to_string(),
763 }
764}
765
766fn finite_or(v: f64, fallback: f64) -> f64 {
767 if v.is_finite() {
768 v
769 } else {
770 fallback
771 }
772}
773
774fn opacity_attr(prefix: &str, alpha: f64) -> String {
780 let a = finite_or(alpha, 1.0);
781 if format!("{a:.3}") == "1.000" {
782 String::new()
783 } else {
784 format!(" {prefix}-opacity=\"{a:.3}\"")
785 }
786}
787
788pub fn root_data_attrs(built: &crate::build::BuiltPlot) -> Vec<(String, String)> {
805 use crate::aes::Aesthetic;
806 let mut out = Vec::new();
807 if !built.panel_scales.is_empty() {
808 return out;
809 }
810 let axis = |aes: &Aesthetic| -> (Option<(f64, f64)>, Option<String>) {
811 match built.scales.get(aes) {
812 Some(s) if s.is_discrete() => {
813 let labels: Vec<String> = s
814 .breaks()
815 .into_iter()
816 .map(|(_, l)| json_string(&l))
817 .collect();
818 (None, Some(format!("[{}]", labels.join(","))))
819 }
820 Some(s) => (s.expanded_domain(), None),
821 None => (None, None),
822 }
823 };
824 let (xd, xl) = axis(&Aesthetic::X);
825 let (yd, yl) = axis(&Aesthetic::Y);
826 if let (Some((x0, x1)), Some((y0, y1))) = (xd, yd) {
827 out.push((
828 "data-domain".into(),
829 format!("{} {} {} {}", g(x0), g(x1), g(y0), g(y1)),
830 ));
831 }
832 if let Some((a, b)) = xd {
833 out.push(("data-xdomain".into(), format!("{} {}", g(a), g(b))));
834 }
835 if let Some((a, b)) = yd {
836 out.push(("data-ydomain".into(), format!("{} {}", g(a), g(b))));
837 }
838 if let Some(l) = xl {
839 out.push(("data-xlevels".into(), l));
840 }
841 if let Some(l) = yl {
842 out.push(("data-ylevels".into(), l));
843 }
844 if built.coord.is_flipped() {
845 out.push(("data-flip".into(), "true".into()));
846 }
847 out
848}
849
850fn g(v: f64) -> String {
853 format!("{v}")
854}
855
856fn json_string(s: &str) -> String {
859 let mut out = String::with_capacity(s.len() + 2);
860 out.push('"');
861 for c in s.chars() {
862 match c {
863 '"' => out.push_str("\\\""),
864 '\\' => out.push_str("\\\\"),
865 '\n' => out.push_str("\\n"),
866 '\r' => out.push_str("\\r"),
867 '\t' => out.push_str("\\t"),
868 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
869 c => out.push(c),
870 }
871 }
872 out.push('"');
873 out
874}
875
876fn is_allowed_root_attr_name(name: &str) -> bool {
883 matches!(name, "role" | "id" | "class" | "lang")
884 || name
885 .strip_prefix("data-")
886 .is_some_and(crate::aes::is_valid_data_attr_name)
887 || name
888 .strip_prefix("aria-")
889 .is_some_and(crate::aes::is_valid_data_attr_name)
890}
891
892pub(crate) fn validate_root_attrs(
902 requested: &[(String, String)],
903 existing: &[(String, String)],
904 label: &str,
905 warnings: &mut Vec<String>,
906) -> Vec<(String, String)> {
907 let built_in: Vec<&str> = existing.iter().map(|(k, _)| k.as_str()).collect();
908 let mut used: Vec<&str> = built_in.clone();
909 let mut out = Vec::new();
910 for (name, value) in requested {
911 if !is_allowed_root_attr_name(name) {
912 warnings.push(format!(
913 "{label}: svg_attr(\"{name}\") is not on the allow-list (data-*, aria-*, role, id, class, lang) — ignored"
914 ));
915 continue;
916 }
917 if used.contains(&name.as_str()) {
918 if built_in.contains(&name.as_str()) {
923 warnings.push(format!(
924 "{label}: svg_attr(\"{name}\") collides with an attribute ggplot-rs already writes to the root — ignored"
925 ));
926 } else {
927 warnings.push(format!(
928 "{label}: svg_attr(\"{name}\") was already set by an earlier svg_attr call — ignored"
929 ));
930 }
931 continue;
932 }
933 used.push(name.as_str());
934 out.push((name.clone(), value.clone()));
935 }
936 out
937}
938
939pub(crate) fn warnings_json(warnings: &[String]) -> String {
943 let items: Vec<String> = warnings.iter().map(|w| json_string(w)).collect();
944 format!("[{}]", items.join(","))
945}
946
947fn is_valid_mark_attr_name(name: &str) -> bool {
955 match name.strip_prefix("data-") {
956 Some(suffix) => crate::aes::is_valid_data_attr_name(suffix),
957 None => false,
958 }
959}
960
961const POINTER_EVENTS_VALUES: &[&str] = &[
964 "auto",
965 "none",
966 "visiblePainted",
967 "visibleFill",
968 "visibleStroke",
969 "visible",
970 "painted",
971 "fill",
972 "stroke",
973 "all",
974 "bounding-box",
975];
976
977pub(crate) fn validate_layer_svg_attr(
986 name: &str,
987 value: &str,
988 warnings: &mut Vec<String>,
989) -> Option<(String, String)> {
990 let keep = |v: &str| Some((name.to_string(), v.to_string()));
991 match name {
992 "class" => {
993 let (ok, bad) = crate::aes::split_class_tokens(value);
994 if !bad.is_empty() {
995 warnings.push(format!(
996 "layer_svg_attr(\"class\"): invalid class token(s) {bad:?} (must match [A-Za-z][A-Za-z0-9_-]*) — dropped"
997 ));
998 }
999 if ok.is_empty() {
1000 None
1001 } else {
1002 keep(&ok.join(" "))
1003 }
1004 }
1005 "aria-hidden" if matches!(value, "true" | "false") => keep(value),
1006 "pointer-events" if POINTER_EVENTS_VALUES.contains(&value) => keep(value),
1007 "aria-hidden" | "pointer-events" => {
1008 warnings.push(format!(
1009 "layer_svg_attr(\"{name}\"): value {value:?} is not allowed — ignored"
1010 ));
1011 None
1012 }
1013 _ if is_valid_mark_attr_name(name) => keep(value),
1014 _ => {
1015 warnings.push(format!(
1016 "layer_svg_attr(\"{name}\") is not on the allow-list (class, aria-hidden, pointer-events, data-*) — ignored"
1017 ));
1018 None
1019 }
1020 }
1021}
1022
1023fn rgb((r, g, b): (u8, u8, u8)) -> String {
1024 format!("#{r:02X}{g:02X}{b:02X}")
1025}
1026
1027fn escape(s: &str) -> String {
1038 escape_impl(s, false)
1039}
1040
1041fn escape_attr(s: &str) -> String {
1052 escape_impl(s, true)
1053}
1054
1055fn escape_impl(s: &str, in_attr: bool) -> String {
1056 let mut out = String::with_capacity(s.len());
1057 for c in s.chars() {
1058 match c {
1059 '&' => out.push_str("&"),
1060 '<' => out.push_str("<"),
1061 '>' => out.push_str(">"),
1062 '"' => out.push_str("""),
1063 '\'' => out.push_str("'"),
1064 '\t' if in_attr => out.push_str("	"),
1065 '\n' if in_attr => out.push_str(" "),
1066 '\r' if in_attr => out.push_str(" "),
1067 '\t' | '\n' | '\r' => out.push(c),
1068 c if (c as u32) < 0x20 || c == '\u{FFFE}' || c == '\u{FFFF}' => {}
1069 c => out.push(c),
1070 }
1071 }
1072 out
1073}
1074
1075fn points(pts: &[(f64, f64)]) -> String {
1076 pts.iter()
1077 .map(|(x, y)| format!("{x:.2},{y:.2}"))
1078 .collect::<Vec<_>>()
1079 .join(" ")
1080}
1081
1082fn finite_pts(pts: &[(f64, f64)]) -> Vec<(f64, f64)> {
1083 pts.iter()
1084 .copied()
1085 .filter(|(x, y)| x.is_finite() && y.is_finite())
1086 .collect()
1087}
1088
1089fn finite_runs(pts: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
1098 let mut runs = Vec::new();
1099 let mut cur: Vec<(f64, f64)> = Vec::new();
1100 for &(x, y) in pts {
1101 if x.is_finite() && y.is_finite() {
1102 cur.push((x, y));
1103 } else if !cur.is_empty() {
1104 if cur.len() >= 2 {
1105 runs.push(std::mem::take(&mut cur));
1106 } else {
1107 cur.clear();
1108 }
1109 }
1110 }
1111 if cur.len() >= 2 {
1112 runs.push(cur);
1113 }
1114 runs
1115}
1116
1117impl DrawBackend for SvgBackend {
1118 fn plot_area(&self) -> Rect {
1119 self.plot_area.clone()
1120 }
1121 fn total_area(&self) -> Rect {
1122 self.total_area.clone()
1123 }
1124
1125 fn set_tooltip(&mut self, tooltip: Option<String>) {
1126 self.tooltip = tooltip;
1127 }
1128
1129 fn set_mark_axis(&mut self, key: Option<String>) {
1130 self.axis_key = key;
1131 }
1132
1133 fn set_mark_series(&mut self, series: Option<String>) {
1134 self.series_key = series;
1135 }
1136
1137 fn set_mark_value(&mut self, value: Option<String>) {
1138 self.value_key = value;
1139 }
1140
1141 fn set_mark_geom(&mut self, geom: Option<&str>) {
1142 self.mark_geom = geom.map(str::to_string);
1143 }
1144
1145 fn warn(&mut self, message: String) {
1146 self.warnings.push(message);
1147 }
1148
1149 fn set_mark_part(&mut self, part: Option<&str>) {
1150 self.mark_part = part.map(|s| s.to_string());
1151 }
1152
1153 fn set_mark_attrs(&mut self, attrs: Vec<(String, String)>) {
1154 self.mark_attrs = attrs;
1155 }
1156
1157 fn wants_formatted_values(&self) -> bool {
1158 self.formatted_values
1159 }
1160
1161 fn set_mark_fvalue(&mut self, value: Option<String>) {
1162 self.mark_fvalue = value;
1163 }
1164
1165 fn set_mark_fx(&mut self, value: Option<String>) {
1166 self.mark_fx = value;
1167 }
1168
1169 fn set_mark_class(&mut self, class: Option<String>) {
1170 self.mark_user_class = class;
1171 }
1172
1173 fn set_mark_group(&mut self, group: Option<(String, String)>) {
1174 self.mark_group = group;
1175 }
1176
1177 fn set_layer_attrs(&mut self, attrs: Vec<(String, String)>) {
1178 self.layer_attrs = attrs;
1179 }
1180
1181 fn draw_circle(
1182 &mut self,
1183 (cx, cy): (f64, f64),
1184 radius: f64,
1185 style: &PointStyle,
1186 ) -> Result<(), RenderError> {
1187 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
1188 return Ok(());
1189 }
1190 let attrs = format!(
1191 "cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"{:.2}\" fill=\"{}\"{}",
1192 radius.max(0.0),
1193 rgb(style.color),
1194 opacity_attr("fill", style.alpha)
1195 );
1196 self.push_mark("circle", "point", &attrs);
1197 Ok(())
1198 }
1199
1200 fn draw_shape(
1204 &mut self,
1205 (cx, cy): (f64, f64),
1206 radius: f64,
1207 style: &PointStyle,
1208 ) -> Result<(), RenderError> {
1209 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
1210 return Ok(());
1211 }
1212 let r = radius.max(0.0);
1213 let color = rgb(style.color);
1214 let fill_opacity = opacity_attr("fill", style.alpha);
1215 let stroke_opacity = opacity_attr("stroke", style.alpha);
1216 let poly = |pts: &[(f64, f64)]| {
1217 if style.filled {
1218 format!("points=\"{}\" fill=\"{color}\"{fill_opacity}", points(pts))
1219 } else {
1220 format!(
1221 "points=\"{}\" fill=\"none\" stroke=\"{color}\"{stroke_opacity}",
1222 points(pts)
1223 )
1224 }
1225 };
1226 match style.shape {
1227 PointShape::Circle => return self.draw_circle((cx, cy), radius, style),
1228 PointShape::Square => {
1229 let a = poly(&[
1230 (cx - r, cy - r),
1231 (cx + r, cy - r),
1232 (cx + r, cy + r),
1233 (cx - r, cy + r),
1234 ]);
1235 self.push_mark("polygon", "point", &a);
1236 }
1237 PointShape::Triangle => {
1238 let a = poly(&[(cx, cy - r), (cx + r, cy + r), (cx - r, cy + r)]);
1239 self.push_mark("polygon", "point", &a);
1240 }
1241 PointShape::Diamond => {
1242 let a = poly(&[(cx, cy - r), (cx + r, cy), (cx, cy + r), (cx - r, cy)]);
1243 self.push_mark("polygon", "point", &a);
1244 }
1245 PointShape::Plus | PointShape::Cross => {
1246 let d = if style.shape == PointShape::Plus {
1247 format!(
1248 "M{:.2} {cy:.2}H{:.2}M{cx:.2} {:.2}V{:.2}",
1249 cx - r,
1250 cx + r,
1251 cy - r,
1252 cy + r
1253 )
1254 } else {
1255 format!(
1256 "M{:.2} {:.2}L{:.2} {:.2}M{:.2} {:.2}L{:.2} {:.2}",
1257 cx - r,
1258 cy - r,
1259 cx + r,
1260 cy + r,
1261 cx - r,
1262 cy + r,
1263 cx + r,
1264 cy - r
1265 )
1266 };
1267 let a = format!(
1268 "d=\"{d}\" fill=\"none\" stroke=\"{color}\" stroke-width=\"{:.2}\"{stroke_opacity}",
1269 (r / 2.5).clamp(1.0, 3.0)
1270 );
1271 self.push_mark("path", "point", &a);
1272 }
1273 }
1274 Ok(())
1275 }
1276
1277 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
1278 for run in finite_runs(pts) {
1284 self.draw_line_run(&run, style);
1285 }
1286 Ok(())
1287 }
1288
1289 fn draw_quadratic(
1292 &mut self,
1293 p0: (f64, f64),
1294 ctrl: (f64, f64),
1295 p1: (f64, f64),
1296 style: &LineStyle,
1297 ) -> Result<(), RenderError> {
1298 if !(p0.0.is_finite()
1299 && p0.1.is_finite()
1300 && ctrl.0.is_finite()
1301 && ctrl.1.is_finite()
1302 && p1.0.is_finite()
1303 && p1.1.is_finite())
1304 {
1305 return Ok(());
1306 }
1307 let dash = dasharray_attr(&style.linetype);
1308 let linecap = match style.lineend {
1309 LineEnd::Butt => "",
1310 LineEnd::Round => " stroke-linecap=\"round\"",
1311 LineEnd::Square => " stroke-linecap=\"square\"",
1312 };
1313 let d = format!(
1314 "M{:.2} {:.2} Q{:.2} {:.2} {:.2} {:.2}",
1315 p0.0, p0.1, ctrl.0, ctrl.1, p1.0, p1.1
1316 );
1317 let attrs = format!(
1318 "d=\"{d}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
1319 rgb(style.color),
1320 finite_or(style.width, 1.0),
1321 opacity_attr("stroke", style.alpha),
1322 dash,
1323 linecap
1324 );
1325 self.push_mark("path", "line", &attrs);
1326 Ok(())
1327 }
1328
1329 fn draw_rect(
1330 &mut self,
1331 top_left: (f64, f64),
1332 bottom_right: (f64, f64),
1333 style: &RectStyle,
1334 ) -> Result<(), RenderError> {
1335 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1336 self.rect_markup(top_left, bottom_right, &fill, style)
1337 }
1338
1339 fn draw_rect_pattern(
1342 &mut self,
1343 top_left: (f64, f64),
1344 bottom_right: (f64, f64),
1345 pattern: &FillPattern,
1346 style: &RectStyle,
1347 ) -> Result<(), RenderError> {
1348 let fill = self.pattern_url(pattern);
1349 self.rect_markup(top_left, bottom_right, &fill, style)
1350 }
1351
1352 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
1353 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1354 self.polygon_markup(pts, &fill, style)
1355 }
1356
1357 fn draw_polygon_pattern(
1360 &mut self,
1361 pts: &[(f64, f64)],
1362 pattern: &FillPattern,
1363 style: &RectStyle,
1364 ) -> Result<(), RenderError> {
1365 let fill = self.pattern_url(pattern);
1366 self.polygon_markup(pts, &fill, style)
1367 }
1368
1369 fn draw_polygon_rings(
1375 &mut self,
1376 rings: &[Vec<(f64, f64)>],
1377 style: &RectStyle,
1378 ) -> Result<(), RenderError> {
1379 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1380 self.polygon_rings_markup(rings, &fill, style)
1381 }
1382
1383 fn draw_polygon_rings_pattern(
1387 &mut self,
1388 rings: &[Vec<(f64, f64)>],
1389 pattern: &FillPattern,
1390 style: &RectStyle,
1391 ) -> Result<(), RenderError> {
1392 let fill = self.pattern_url(pattern);
1393 self.polygon_rings_markup(rings, &fill, style)
1394 }
1395
1396 fn draw_polygon_rings_precise(
1402 &mut self,
1403 rings: &[Vec<(f64, f64)>],
1404 pattern: Option<&FillPattern>,
1405 precision: Option<u8>,
1406 style: &RectStyle,
1407 ) -> Result<(), RenderError> {
1408 let Some(precision) = precision else {
1409 return match pattern {
1410 Some(p) => self.draw_polygon_rings_pattern(rings, p, style),
1411 None => self.draw_polygon_rings(rings, style),
1412 };
1413 };
1414 let d = compact_rings_d(rings, precision);
1415 if d.is_empty() {
1416 return Ok(());
1417 }
1418 let fill = match pattern {
1419 Some(p) => self.pattern_url(p),
1420 None => style.fill.map(rgb).unwrap_or_else(|| "none".into()),
1421 };
1422 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
1423 let attrs = format!(
1424 "d=\"{d}\" fill-rule=\"evenodd\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
1425 opacity_attr("fill", style.alpha),
1426 finite_or(style.stroke_width, 0.0)
1427 );
1428 self.push_mark("path", "band", &attrs);
1429 Ok(())
1430 }
1431
1432 fn draw_text(
1433 &mut self,
1434 text: &str,
1435 pos: (f64, f64),
1436 style: &TextStyle,
1437 ) -> Result<(), RenderError> {
1438 self.draw_text_with_title(text, pos, style, None)
1439 }
1440
1441 fn draw_text_with_title(
1445 &mut self,
1446 text: &str,
1447 (x, y): (f64, f64),
1448 style: &TextStyle,
1449 title: Option<&str>,
1450 ) -> Result<(), RenderError> {
1451 if !(x.is_finite() && y.is_finite()) {
1452 return Ok(());
1453 }
1454 let anchor = match style.anchor {
1455 TextAnchor::Start => "start",
1456 TextAnchor::Middle => "middle",
1457 TextAnchor::End => "end",
1458 };
1459 let family = escape_attr(style.family.as_deref().unwrap_or("sans-serif"));
1460 let weight = if style.face == FontFace::Bold {
1461 " font-weight=\"bold\""
1462 } else {
1463 ""
1464 };
1465 let fstyle = if style.face == FontFace::Italic {
1466 " font-style=\"italic\""
1467 } else {
1468 ""
1469 };
1470 let halo = text_halo_attrs(style);
1471 let angle = finite_or(style.angle, 0.0);
1472 let transform = if angle.abs() > 0.01 {
1473 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
1474 } else {
1475 String::new()
1476 };
1477 let class = format!(
1478 "{}{}{}",
1479 self.text_data_attrs(),
1480 self.layer_flag_attrs(false),
1481 self.mark_class("text")
1482 );
1483 let title_tag = match title {
1484 Some(t) => format!("<title>{}</title>", escape(t)),
1485 None => String::new(),
1486 };
1487 let markup = format!(
1488 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
1489 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle}{halo} fill=\"{}\"{transform}{class}>{title_tag}{}</text>",
1490 finite_or(style.size, 12.0),
1491 rgb(style.color),
1492 escape(text)
1493 );
1494 self.emit(&markup);
1495 Ok(())
1496 }
1497
1498 fn draw_text_runs(
1505 &mut self,
1506 runs: &[(&str, Option<&str>)],
1507 (x, y): (f64, f64),
1508 style: &TextStyle,
1509 ) -> Result<(), RenderError> {
1510 if !(x.is_finite() && y.is_finite()) {
1511 return Ok(());
1512 }
1513 let anchor = match style.anchor {
1514 TextAnchor::Start => "start",
1515 TextAnchor::Middle => "middle",
1516 TextAnchor::End => "end",
1517 };
1518 let family = escape_attr(style.family.as_deref().unwrap_or("sans-serif"));
1519 let weight = if style.face == FontFace::Bold {
1520 " font-weight=\"bold\""
1521 } else {
1522 ""
1523 };
1524 let fstyle = if style.face == FontFace::Italic {
1525 " font-style=\"italic\""
1526 } else {
1527 ""
1528 };
1529 let halo = text_halo_attrs(style);
1530 let angle = finite_or(style.angle, 0.0);
1531 let transform = if angle.abs() > 0.01 {
1532 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
1533 } else {
1534 String::new()
1535 };
1536 let class = format!(
1537 "{}{}{}",
1538 self.text_data_attrs(),
1539 self.layer_flag_attrs(false),
1540 self.mark_class("text")
1541 );
1542 let mut inner = String::new();
1543 for (text, href) in runs {
1544 match href {
1545 Some(u) => inner.push_str(&format!(
1546 "<a href=\"{}\" target=\"_blank\" rel=\"noopener\"><tspan text-decoration=\"underline\">{}</tspan></a>",
1547 escape_attr(u),
1548 escape(text)
1549 )),
1550 None => inner.push_str(&format!("<tspan>{}</tspan>", escape(text))),
1551 }
1552 }
1553 let markup = format!(
1554 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
1555 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle}{halo} fill=\"{}\"{transform}{class}>{inner}</text>",
1556 finite_or(style.size, 12.0),
1557 rgb(style.color),
1558 );
1559 self.emit(&markup);
1560 Ok(())
1561 }
1562}
1563
1564fn text_halo_attrs(style: &TextStyle) -> String {
1574 let Some(bg) = style.bg_colour else {
1575 return String::new();
1576 };
1577 let r = finite_or(style.bg_r, 0.0);
1578 if r <= 0.0 {
1579 return String::new();
1580 }
1581 format!(
1582 " stroke=\"{}\" stroke-width=\"{:.2}\" stroke-linejoin=\"round\" paint-order=\"stroke\"",
1583 rgb(bg),
1584 2.0 * r
1585 )
1586}
1587
1588#[cfg(test)]
1589mod tests {
1590 use super::*;
1591 use crate::render::Rect;
1592
1593 #[test]
1596 fn issue221_backend_rechecks_class_group_and_layer_attrs() {
1597 let mut b = backend();
1598 b.set_mark_geom(Some("point"));
1599 b.set_mark_class(Some("ok \"bad 1x ok".into()));
1600 b.set_layer_attrs(vec![
1601 ("aria-hidden".into(), "maybe".into()),
1602 ("pointer-events".into(), "none\" onload=\"x".into()),
1603 ("style".into(), "fill:red".into()),
1604 ("class".into(), "lay".into()),
1605 ]);
1606 b.set_mark_group(Some(("onclick".into(), "x".into())));
1607 b.draw_circle((1.0, 1.0), 1.0, &PointStyle::default())
1608 .unwrap();
1609 b.set_mark_group(Some(("data-of".into(), "n".into())));
1610 let runs = [("a", None), ("b", Some("https://e.x"))];
1611 b.draw_text_runs(&runs, (5.0, 5.0), &TextStyle::default())
1612 .unwrap();
1613 b.set_mark_group(None);
1614 b.set_mark_geom(None);
1615 let body = b.body().to_string();
1616 assert!(
1617 body.contains(r#"class="gg-mark gg-point lay ok""#),
1618 "{body}"
1619 );
1620 assert!(!body.contains("bad") && !body.contains("1x"), "{body}");
1621 assert!(
1622 !body.contains("aria-hidden") && !body.contains("onload"),
1623 "{body}"
1624 );
1625 assert!(
1626 !body.contains("style=") && !body.contains("onclick"),
1627 "{body}"
1628 );
1629 assert!(body.contains(r#"<g data-of="n"><text"#), "{body}");
1630 assert!(body.contains("gg-text lay ok"), "{body}");
1631 assert!(b.finish().contains(r#"<g data-of="n">"#));
1633 }
1634
1635 fn backend() -> SvgBackend {
1636 SvgBackend::new(
1637 100,
1638 100,
1639 Rect {
1640 x: 0.0,
1641 y: 0.0,
1642 width: 100.0,
1643 height: 100.0,
1644 },
1645 )
1646 }
1647
1648 #[test]
1649 fn draw_quadratic_emits_a_path_with_m_and_q() {
1650 let mut b = backend();
1651 b.draw_quadratic((0.0, 0.0), (10.0, 20.0), (20.0, 0.0), &LineStyle::default())
1652 .unwrap();
1653 let svg = b.finish();
1654 assert!(svg.contains("<path"));
1655 assert!(svg.contains("d=\"M0.00 0.00 Q10.00 20.00 20.00 0.00\""));
1656 assert!(!svg.contains("NaN"));
1657 assert!(!svg.contains("inf"));
1658 }
1659
1660 #[test]
1661 fn draw_quadratic_drops_non_finite_input() {
1662 let mut b = backend();
1663 b.draw_quadratic(
1664 (f64::NAN, 0.0),
1665 (10.0, 20.0),
1666 (20.0, 0.0),
1667 &LineStyle::default(),
1668 )
1669 .unwrap();
1670 b.draw_quadratic(
1671 (0.0, 0.0),
1672 (f64::INFINITY, 20.0),
1673 (20.0, 0.0),
1674 &LineStyle::default(),
1675 )
1676 .unwrap();
1677 let svg = b.finish();
1678 assert!(!svg.contains("<path"));
1679 }
1680
1681 #[test]
1682 fn draw_line_omits_linecap_by_default_but_writes_it_when_set() {
1683 let mut b = backend();
1684 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &LineStyle::default())
1685 .unwrap();
1686 let svg = b.finish();
1687 assert!(!svg.contains("stroke-linecap"));
1688
1689 let mut b2 = backend();
1690 let style = LineStyle {
1691 lineend: LineEnd::Round,
1692 ..Default::default()
1693 };
1694 b2.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
1695 let svg2 = b2.finish();
1696 assert!(svg2.contains("stroke-linecap=\"round\""));
1697 }
1698
1699 #[test]
1700 fn square_linecap_is_written_on_both_draw_line_and_draw_quadratic() {
1701 let style = LineStyle {
1702 lineend: LineEnd::Square,
1703 ..Default::default()
1704 };
1705 let mut b = backend();
1706 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
1707 assert!(b.finish().contains("stroke-linecap=\"square\""));
1708
1709 let mut b2 = backend();
1710 b2.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &style)
1711 .unwrap();
1712 assert!(b2.finish().contains("stroke-linecap=\"square\""));
1713 }
1714
1715 #[test]
1720 fn set_mark_attrs_defence_in_depth_drops_invalid_names() {
1721 let mut b = backend();
1722 b.set_mark_attrs(vec![
1723 ("not-data-prefixed".to_string(), "x".to_string()),
1724 ("data-".to_string(), "x".to_string()), ("data-Upper".to_string(), "x".to_string()), ("data-ok".to_string(), "valid".to_string()), ]);
1728 b.draw_circle((1.0, 1.0), 1.0, &PointStyle::default())
1729 .unwrap();
1730 let svg = b.finish();
1731 assert!(!svg.contains("not-data-prefixed"));
1732 assert!(!svg.contains("data-Upper"));
1733 assert!(!svg.contains("data-=\""));
1734 assert!(svg.contains("data-ok=\"valid\""));
1735 }
1736
1737 fn stripe() -> FillPattern {
1740 FillPattern::Stripe {
1741 fg: (10, 20, 30),
1742 bg: (200, 210, 220),
1743 size_px: 4.0,
1744 angle_deg: 45.0,
1745 }
1746 }
1747
1748 fn pattern_id(svg: &str) -> String {
1749 let i = svg.find("<pattern id=\"").unwrap() + "<pattern id=\"".len();
1750 svg[i..i + svg[i..].find('"').unwrap()].to_string()
1751 }
1752
1753 #[test]
1754 fn a_pattern_fill_registers_one_defs_pattern_and_references_it() {
1755 let mut b = backend();
1756 b.draw_rect_pattern((0.0, 0.0), (10.0, 10.0), &stripe(), &RectStyle::default())
1757 .unwrap();
1758 let svg = b.finish();
1759 assert_eq!(svg.matches("<pattern ").count(), 1, "{svg}");
1760 let id = pattern_id(&svg);
1761 assert!(
1762 svg.contains(&format!("fill=\"url(#{id})\"")),
1763 "rect references the registered pattern id {id}: {svg}"
1764 );
1765 }
1766
1767 #[test]
1768 fn a_rounded_rect_pattern_still_rounds_its_corners() {
1769 let mut b = backend();
1770 b.draw_rect_pattern(
1771 (0.0, 0.0),
1772 (10.0, 10.0),
1773 &stripe(),
1774 &RectStyle {
1775 corner_radius: 3.0,
1776 ..Default::default()
1777 },
1778 )
1779 .unwrap();
1780 let svg = b.finish();
1781 assert!(svg.contains("rx=\"3.00\""), "{svg}");
1782 assert!(svg.contains("fill=\"url(#"), "{svg}");
1783 }
1784
1785 #[test]
1786 fn an_equal_pattern_reuses_the_same_defs_entry() {
1787 let mut b = backend();
1788 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1789 .unwrap();
1790 b.draw_polygon_pattern(
1791 &[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)],
1792 &stripe(),
1793 &RectStyle::default(),
1794 )
1795 .unwrap();
1796 let svg = b.finish();
1797 assert_eq!(
1798 svg.matches("<pattern ").count(),
1799 1,
1800 "two equal patterns share one <defs> entry: {svg}"
1801 );
1802 }
1803
1804 #[test]
1805 fn different_patterns_get_their_own_defs_entries() {
1806 let mut b = backend();
1807 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1808 .unwrap();
1809 let other = FillPattern::Stripe {
1810 fg: (1, 1, 1),
1811 bg: (2, 2, 2),
1812 size_px: 8.0,
1813 angle_deg: 0.0,
1814 };
1815 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &other, &RectStyle::default())
1816 .unwrap();
1817 let svg = b.finish();
1818 assert_eq!(svg.matches("<pattern ").count(), 2, "{svg}");
1819 }
1820
1821 #[test]
1822 fn no_pattern_used_means_no_defs_block_at_all() {
1823 let mut b = backend();
1824 b.draw_rect((0.0, 0.0), (5.0, 5.0), &RectStyle::default())
1825 .unwrap();
1826 let svg = b.finish();
1827 assert!(!svg.contains("<defs>"), "{svg}");
1828 }
1829
1830 #[test]
1831 fn pattern_fill_cuts_out_holes_like_a_flat_fill_does() {
1832 let mut b = backend();
1833 let outer = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1834 let hole = vec![(2.0, 2.0), (8.0, 2.0), (8.0, 8.0), (2.0, 8.0)];
1835 b.draw_polygon_rings_pattern(&[outer, hole], &stripe(), &RectStyle::default())
1836 .unwrap();
1837 let svg = b.finish();
1838 assert!(svg.contains("fill-rule=\"evenodd\""), "{svg}");
1839 assert_eq!(
1840 svg.matches("<path").count(),
1841 1,
1842 "one path, both rings: {svg}"
1843 );
1844 }
1845
1846 #[test]
1847 fn two_backends_on_one_page_never_collide_on_a_pattern_id() {
1848 let mut a = backend();
1849 a.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1850 .unwrap();
1851 let mut b = backend();
1852 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1853 .unwrap();
1854 let (sa, sb) = (a.finish(), b.finish());
1855 assert_ne!(pattern_id(&sa), pattern_id(&sb), "{sa}\n{sb}");
1856 }
1857
1858 fn decode_compact_d(d: &str) -> Vec<Vec<(f64, f64)>> {
1865 let b = d.as_bytes();
1866 let mut i = 0;
1867 let read_num = |b: &[u8], i: &mut usize| -> f64 {
1868 let start = *i;
1869 if *i < b.len() && b[*i] == b'-' {
1870 *i += 1;
1871 }
1872 while *i < b.len() && b[*i].is_ascii_digit() {
1873 *i += 1;
1874 }
1875 if *i < b.len() && b[*i] == b'.' {
1876 *i += 1;
1877 while *i < b.len() && b[*i].is_ascii_digit() {
1878 *i += 1;
1879 }
1880 }
1881 std::str::from_utf8(&b[start..*i]).unwrap().parse().unwrap()
1882 };
1883 let is_num_start = |c: u8| c.is_ascii_digit() || c == b'-' || c == b'.';
1884 let mut rings = Vec::new();
1885 let mut current = Vec::new();
1886 let mut cursor = (0.0, 0.0);
1887 while i < b.len() {
1888 match b[i] {
1889 b'M' => {
1890 if !current.is_empty() {
1891 rings.push(std::mem::take(&mut current));
1892 }
1893 i += 1;
1894 let x = read_num(b, &mut i);
1895 if i < b.len() && b[i] == b',' {
1896 i += 1;
1897 }
1898 let y = read_num(b, &mut i);
1899 cursor = (x, y);
1900 current.push(cursor);
1901 }
1902 b'l' => {
1903 i += 1;
1904 while i < b.len() && is_num_start(b[i]) {
1905 let dx = read_num(b, &mut i);
1906 if i < b.len() && b[i] == b',' {
1907 i += 1;
1908 }
1909 let dy = read_num(b, &mut i);
1910 if i < b.len() && b[i] == b',' {
1911 i += 1;
1912 }
1913 cursor = (cursor.0 + dx, cursor.1 + dy);
1914 current.push(cursor);
1915 }
1916 }
1917 b'z' => {
1918 i += 1;
1919 if !current.is_empty() {
1920 rings.push(std::mem::take(&mut current));
1921 }
1922 }
1923 _ => i += 1,
1924 }
1925 }
1926 if !current.is_empty() {
1927 rings.push(current);
1928 }
1929 rings
1930 }
1931
1932 fn noisy_ring(n: usize, cx: f64, cy: f64, r: f64) -> Vec<(f64, f64)> {
1935 (0..n)
1936 .map(|i| {
1937 let t = i as f64 / n as f64 * std::f64::consts::TAU;
1938 let wobble = 1.0 + 0.03 * ((i * 7) as f64).sin();
1939 (cx + r * wobble * t.cos(), cy + r * wobble * t.sin())
1940 })
1941 .collect()
1942 }
1943
1944 #[test]
1945 fn compact_encoding_is_well_under_the_absolute_encodings_size() {
1946 let ring = noisy_ring(2000, 400.0, 300.0, 280.0);
1950 let mut b = backend();
1951 b.draw_polygon_rings(std::slice::from_ref(&ring), &RectStyle::default())
1952 .unwrap();
1953 let absolute_svg = b.finish();
1954 let absolute_d = absolute_svg
1955 .split("d=\"")
1956 .nth(1)
1957 .and_then(|s| s.split('"').next())
1958 .expect("absolute path d");
1959
1960 let compact_d = compact_rings_d(std::slice::from_ref(&ring), 1);
1961
1962 assert!(
1963 compact_d.len() * 3 < absolute_d.len() * 2,
1964 "compact {} bytes should be well under 2/3 of absolute {} bytes",
1965 compact_d.len(),
1966 absolute_d.len()
1967 );
1968 }
1969
1970 #[test]
1971 fn compact_encoding_round_trips_within_precision() {
1972 let ring = noisy_ring(40, 100.0, 80.0, 60.0);
1973 let precision: u8 = 1;
1974 let d = compact_rings_d(std::slice::from_ref(&ring), precision);
1975 let decoded = decode_compact_d(&d);
1976 assert_eq!(decoded.len(), 1, "{d}");
1977 assert_eq!(decoded[0].len(), ring.len(), "{d}");
1978 let step_tol = 0.5 * 10f64.powi(-(precision as i32));
1983 for (k, (orig, got)) in ring.iter().zip(decoded[0].iter()).enumerate() {
1984 let tol = step_tol * (k as f64 + 1.0) + 1e-9;
1985 assert!(
1986 (orig.0 - got.0).abs() <= tol && (orig.1 - got.1).abs() <= tol,
1987 "point {k}: {orig:?} vs {got:?} (tol {tol}): {d}"
1988 );
1989 }
1990 }
1991
1992 #[test]
1993 fn compact_encoding_round_trips_a_hole_and_a_multipolygons_parts() {
1994 let part1 = vec![(0.0, 0.0), (20.0, 0.0), (20.0, 20.0), (0.0, 20.0)];
1998 let part2_outer = vec![(50.0, 0.0), (90.0, 0.0), (90.0, 40.0), (50.0, 40.0)];
1999 let part2_hole = vec![(60.0, 10.0), (80.0, 10.0), (80.0, 30.0), (60.0, 30.0)];
2000 let rings = vec![part1.clone(), part2_outer.clone(), part2_hole.clone()];
2001 let d = compact_rings_d(&rings, 1);
2002 let decoded = decode_compact_d(&d);
2003 assert_eq!(decoded.len(), 3, "{d}");
2004 for (orig, got) in [part1, part2_outer, part2_hole].iter().zip(decoded.iter()) {
2005 assert_eq!(orig.len(), got.len(), "{d}");
2006 for (o, g) in orig.iter().zip(got.iter()) {
2007 assert!((o.0 - g.0).abs() <= 0.1 && (o.1 - g.1).abs() <= 0.1, "{d}");
2008 }
2009 }
2010 assert!(d.contains("M0,0l20,0,0,20-20,0z"), "{d}");
2011 }
2012}