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