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