1use crate::edges::Edge;
9use crate::geometry::BBox;
10use crate::shapes::{Line, Rect};
11use crate::table::{Cell, Intersection, Table};
12use crate::text::Char;
13
14#[derive(Debug, Clone)]
16pub struct DrawStyle {
17 pub fill: Option<String>,
19 pub stroke: Option<String>,
21 pub stroke_width: f64,
23 pub opacity: f64,
25}
26
27impl Default for DrawStyle {
28 fn default() -> Self {
29 Self {
30 fill: None,
31 stroke: Some("black".to_string()),
32 stroke_width: 0.5,
33 opacity: 1.0,
34 }
35 }
36}
37
38impl DrawStyle {
39 pub fn chars_default() -> Self {
41 Self {
42 fill: None,
43 stroke: Some("blue".to_string()),
44 stroke_width: 0.3,
45 opacity: 0.7,
46 }
47 }
48
49 pub fn lines_default() -> Self {
51 Self {
52 fill: None,
53 stroke: Some("red".to_string()),
54 stroke_width: 1.0,
55 opacity: 0.8,
56 }
57 }
58
59 pub fn rects_default() -> Self {
61 Self {
62 fill: None,
63 stroke: Some("green".to_string()),
64 stroke_width: 0.5,
65 opacity: 0.8,
66 }
67 }
68
69 pub fn edges_default() -> Self {
71 Self {
72 fill: None,
73 stroke: Some("orange".to_string()),
74 stroke_width: 0.5,
75 opacity: 0.8,
76 }
77 }
78
79 pub fn tables_default() -> Self {
81 Self {
82 fill: Some("lightblue".to_string()),
83 stroke: Some("steelblue".to_string()),
84 stroke_width: 0.5,
85 opacity: 0.3,
86 }
87 }
88
89 pub fn intersections_default() -> Self {
91 Self {
92 fill: Some("red".to_string()),
93 stroke: Some("darkred".to_string()),
94 stroke_width: 0.5,
95 opacity: 0.9,
96 }
97 }
98
99 pub fn cells_default() -> Self {
101 Self {
102 fill: None,
103 stroke: Some("magenta".to_string()),
104 stroke_width: 0.5,
105 opacity: 0.6,
106 }
107 }
108
109 fn to_svg_style(&self) -> String {
111 let mut parts = Vec::new();
112 match &self.fill {
113 Some(color) => parts.push(format!("fill:{color}")),
114 None => parts.push("fill:none".to_string()),
115 }
116 if let Some(color) = &self.stroke {
117 parts.push(format!("stroke:{color}"));
118 parts.push(format!("stroke-width:{}", self.stroke_width));
119 } else {
120 parts.push("stroke:none".to_string());
121 }
122 if (self.opacity - 1.0).abs() > f64::EPSILON {
123 parts.push(format!("opacity:{}", self.opacity));
124 }
125 parts.join(";")
126 }
127}
128
129#[derive(Debug, Clone)]
131pub struct SvgOptions {
132 pub width: Option<f64>,
134 pub height: Option<f64>,
136 pub scale: f64,
138}
139
140impl Default for SvgOptions {
141 fn default() -> Self {
142 Self {
143 width: None,
144 height: None,
145 scale: 1.0,
146 }
147 }
148}
149
150#[derive(Debug, Clone)]
155pub struct SvgDebugOptions {
156 pub show_edges: bool,
158 pub show_intersections: bool,
160 pub show_cells: bool,
162 pub show_tables: bool,
164}
165
166impl Default for SvgDebugOptions {
167 fn default() -> Self {
168 Self {
169 show_edges: true,
170 show_intersections: true,
171 show_cells: true,
172 show_tables: true,
173 }
174 }
175}
176
177pub struct SvgRenderer {
185 page_width: f64,
187 page_height: f64,
189 page_bbox: BBox,
191 elements: Vec<String>,
193}
194
195impl SvgRenderer {
196 pub fn new(page_width: f64, page_height: f64) -> Self {
198 let page_bbox = BBox::new(0.0, 0.0, page_width, page_height);
199 Self {
200 page_width,
201 page_height,
202 page_bbox,
203 elements: Vec::new(),
204 }
205 }
206
207 pub fn draw_chars(&mut self, chars: &[Char], style: &DrawStyle) {
209 let style_attr = style.to_svg_style();
210 for ch in chars {
211 self.elements.push(format!(
212 " <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" style=\"{style_attr}\"/>\n",
213 ch.bbox.x0,
214 ch.bbox.top,
215 ch.bbox.width(),
216 ch.bbox.height(),
217 ));
218 }
219 }
220
221 pub fn draw_rects(&mut self, rects: &[Rect], style: &DrawStyle) {
223 let style_attr = style.to_svg_style();
224 for r in rects {
225 self.elements.push(format!(
226 " <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" style=\"{style_attr}\"/>\n",
227 r.x0,
228 r.top,
229 r.x1 - r.x0,
230 r.bottom - r.top,
231 ));
232 }
233 }
234
235 pub fn draw_lines(&mut self, lines: &[Line], style: &DrawStyle) {
237 let style_attr = style.to_svg_style();
238 for l in lines {
239 self.elements.push(format!(
240 " <line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" style=\"{style_attr}\"/>\n",
241 l.x0, l.top, l.x1, l.bottom,
242 ));
243 }
244 }
245
246 pub fn draw_edges(&mut self, edges: &[Edge], style: &DrawStyle) {
248 let style_attr = style.to_svg_style();
249 for e in edges {
250 self.elements.push(format!(
251 " <line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" style=\"{style_attr}\"/>\n",
252 e.x0, e.top, e.x1, e.bottom,
253 ));
254 }
255 }
256
257 pub fn draw_intersections(&mut self, intersections: &[Intersection], style: &DrawStyle) {
259 let style_attr = style.to_svg_style();
260 let radius = 3.0;
261 for pt in intersections {
262 self.elements.push(format!(
263 " <circle cx=\"{}\" cy=\"{}\" r=\"{radius}\" style=\"{style_attr}\"/>\n",
264 pt.x, pt.y,
265 ));
266 }
267 }
268
269 pub fn draw_cells(&mut self, cells: &[Cell], style: &DrawStyle) {
271 let style_attr = style.to_svg_style();
272 for cell in cells {
273 self.elements.push(format!(
274 " <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" style=\"{style_attr}\" stroke-dasharray=\"4,2\"/>\n",
275 cell.bbox.x0,
276 cell.bbox.top,
277 cell.bbox.width(),
278 cell.bbox.height(),
279 ));
280 }
281 }
282
283 pub fn draw_tables(&mut self, tables: &[Table], style: &DrawStyle) {
285 let style_attr = style.to_svg_style();
286 for table in tables {
287 for cell in &table.cells {
289 self.elements.push(format!(
290 " <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" style=\"{style_attr}\"/>\n",
291 cell.bbox.x0,
292 cell.bbox.top,
293 cell.bbox.width(),
294 cell.bbox.height(),
295 ));
296 }
297 }
298 }
299
300 pub fn to_svg(&self, options: &SvgOptions) -> String {
308 let view_width = self.page_width;
309 let view_height = self.page_height;
310
311 let svg_width = options.width.unwrap_or(self.page_width * options.scale);
312 let svg_height = options.height.unwrap_or(self.page_height * options.scale);
313
314 let mut svg = String::new();
315
316 svg.push_str(&format!(
318 "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" \
319 width=\"{svg_width}\" height=\"{svg_height}\" \
320 viewBox=\"0 0 {view_width} {view_height}\">\n"
321 ));
322
323 svg.push_str(&format!(
325 " <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" \
326 fill=\"white\" stroke=\"black\" stroke-width=\"0.5\"/>\n",
327 self.page_bbox.x0,
328 self.page_bbox.top,
329 self.page_bbox.width(),
330 self.page_bbox.height(),
331 ));
332
333 for element in &self.elements {
335 svg.push_str(element);
336 }
337
338 svg.push_str("</svg>\n");
340
341 svg
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348 use crate::edges::EdgeSource;
349 use crate::geometry::Orientation;
350 use crate::painting::Color;
351 use crate::table::Cell;
352 use crate::text::TextDirection;
353
354 #[test]
357 fn test_svg_default_options() {
358 let opts = SvgOptions::default();
359 assert!(opts.width.is_none());
360 assert!(opts.height.is_none());
361 assert!((opts.scale - 1.0).abs() < f64::EPSILON);
362 }
363
364 #[test]
365 fn test_svg_generation_simple_page() {
366 let renderer = SvgRenderer::new(612.0, 792.0); let svg = renderer.to_svg(&SvgOptions::default());
368
369 assert!(svg.contains("xmlns=\"http://www.w3.org/2000/svg\""));
371 assert!(svg.contains("version=\"1.1\""));
372 assert!(svg.starts_with("<svg"));
374 assert!(svg.trim_end().ends_with("</svg>"));
375 }
376
377 #[test]
378 fn test_svg_has_correct_viewbox() {
379 let renderer = SvgRenderer::new(612.0, 792.0);
380 let svg = renderer.to_svg(&SvgOptions::default());
381
382 assert!(svg.contains("viewBox=\"0 0 612 792\""));
383 }
384
385 #[test]
386 fn test_svg_has_correct_dimensions_default() {
387 let renderer = SvgRenderer::new(612.0, 792.0);
388 let svg = renderer.to_svg(&SvgOptions::default());
389
390 assert!(svg.contains("width=\"612\""));
392 assert!(svg.contains("height=\"792\""));
393 }
394
395 #[test]
396 fn test_svg_has_correct_dimensions_with_scale() {
397 let renderer = SvgRenderer::new(612.0, 792.0);
398 let svg = renderer.to_svg(&SvgOptions {
399 scale: 2.0,
400 ..Default::default()
401 });
402
403 assert!(svg.contains("width=\"1224\""));
405 assert!(svg.contains("height=\"1584\""));
406 assert!(svg.contains("viewBox=\"0 0 612 792\""));
408 }
409
410 #[test]
411 fn test_svg_has_correct_dimensions_with_explicit_size() {
412 let renderer = SvgRenderer::new(612.0, 792.0);
413 let svg = renderer.to_svg(&SvgOptions {
414 width: Some(800.0),
415 height: Some(600.0),
416 scale: 1.0,
417 });
418
419 assert!(svg.contains("width=\"800\""));
420 assert!(svg.contains("height=\"600\""));
421 assert!(svg.contains("viewBox=\"0 0 612 792\""));
423 }
424
425 #[test]
426 fn test_svg_has_page_boundary_rect() {
427 let renderer = SvgRenderer::new(612.0, 792.0);
428 let svg = renderer.to_svg(&SvgOptions::default());
429
430 assert!(svg.contains("<rect"));
432 assert!(svg.contains("width=\"612\""));
433 assert!(svg.contains("height=\"792\""));
434 assert!(svg.contains("fill=\"white\""));
435 assert!(svg.contains("stroke=\"black\""));
436 }
437
438 #[test]
439 fn test_svg_valid_markup() {
440 let renderer = SvgRenderer::new(100.0, 200.0);
441 let svg = renderer.to_svg(&SvgOptions::default());
442
443 let open_tags = svg.matches("<svg").count();
445 let close_tags = svg.matches("</svg>").count();
446 assert_eq!(open_tags, 1, "Should have exactly one <svg> opening tag");
447 assert_eq!(close_tags, 1, "Should have exactly one </svg> closing tag");
448
449 assert!(svg.contains("/>"), "Rect should be self-closing");
451 }
452
453 #[test]
454 fn test_svg_coordinate_system_top_left_origin() {
455 let renderer = SvgRenderer::new(400.0, 300.0);
456 let svg = renderer.to_svg(&SvgOptions::default());
457
458 assert!(svg.contains("viewBox=\"0 0 400 300\""));
460 assert!(svg.contains("x=\"0\""));
462 assert!(svg.contains("y=\"0\""));
463 }
464
465 #[test]
466 fn test_svg_small_page() {
467 let renderer = SvgRenderer::new(50.0, 50.0);
468 let svg = renderer.to_svg(&SvgOptions::default());
469
470 assert!(svg.contains("viewBox=\"0 0 50 50\""));
471 assert!(svg.contains("width=\"50\""));
472 assert!(svg.contains("height=\"50\""));
473 }
474
475 #[test]
478 fn test_draw_style_default() {
479 let style = DrawStyle::default();
480 assert!(style.fill.is_none());
481 assert_eq!(style.stroke.as_deref(), Some("black"));
482 assert!((style.stroke_width - 0.5).abs() < f64::EPSILON);
483 assert!((style.opacity - 1.0).abs() < f64::EPSILON);
484 }
485
486 #[test]
487 fn test_draw_style_chars_default() {
488 let style = DrawStyle::chars_default();
489 assert!(style.fill.is_none());
490 assert_eq!(style.stroke.as_deref(), Some("blue"));
491 }
492
493 #[test]
494 fn test_draw_style_lines_default() {
495 let style = DrawStyle::lines_default();
496 assert_eq!(style.stroke.as_deref(), Some("red"));
497 }
498
499 #[test]
500 fn test_draw_style_rects_default() {
501 let style = DrawStyle::rects_default();
502 assert_eq!(style.stroke.as_deref(), Some("green"));
503 }
504
505 #[test]
506 fn test_draw_style_tables_default() {
507 let style = DrawStyle::tables_default();
508 assert_eq!(style.fill.as_deref(), Some("lightblue"));
509 }
510
511 #[test]
512 fn test_draw_style_to_svg_style_full() {
513 let style = DrawStyle {
514 fill: Some("red".to_string()),
515 stroke: Some("blue".to_string()),
516 stroke_width: 2.0,
517 opacity: 0.5,
518 };
519 let s = style.to_svg_style();
520 assert!(s.contains("fill:red"));
521 assert!(s.contains("stroke:blue"));
522 assert!(s.contains("stroke-width:2"));
523 assert!(s.contains("opacity:0.5"));
524 }
525
526 #[test]
527 fn test_draw_style_to_svg_style_no_fill() {
528 let style = DrawStyle {
529 fill: None,
530 stroke: Some("black".to_string()),
531 stroke_width: 1.0,
532 opacity: 1.0,
533 };
534 let s = style.to_svg_style();
535 assert!(s.contains("fill:none"));
536 assert!(s.contains("stroke:black"));
537 assert!(!s.contains("opacity"));
539 }
540
541 fn make_char(text: &str, x0: f64, top: f64, x1: f64, bottom: f64) -> Char {
544 Char {
545 text: text.to_string(),
546 bbox: BBox::new(x0, top, x1, bottom),
547 fontname: "Helvetica".to_string(),
548 size: 12.0,
549 doctop: top,
550 upright: true,
551 direction: TextDirection::Ltr,
552 stroking_color: None,
553 non_stroking_color: None,
554 ctm: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
555 char_code: 0,
556 mcid: None,
557 tag: None,
558 }
559 }
560
561 #[test]
562 fn test_draw_chars_adds_rect_elements() {
563 let mut renderer = SvgRenderer::new(200.0, 200.0);
564 let chars = vec![
565 make_char("A", 10.0, 20.0, 18.0, 32.0),
566 make_char("B", 20.0, 20.0, 28.0, 32.0),
567 ];
568 renderer.draw_chars(&chars, &DrawStyle::chars_default());
569 let svg = renderer.to_svg(&SvgOptions::default());
570
571 let rect_count = svg.matches("<rect").count();
573 assert_eq!(rect_count, 3, "1 page boundary + 2 char bboxes");
574 assert!(svg.contains("stroke:blue"));
575 }
576
577 #[test]
578 fn test_draw_chars_correct_coordinates() {
579 let mut renderer = SvgRenderer::new(200.0, 200.0);
580 let chars = vec![make_char("X", 10.0, 20.0, 25.0, 35.0)];
581 renderer.draw_chars(&chars, &DrawStyle::chars_default());
582 let svg = renderer.to_svg(&SvgOptions::default());
583
584 assert!(svg.contains("x=\"10\""));
586 assert!(svg.contains("y=\"20\""));
587 assert!(svg.contains("width=\"15\"")); assert!(svg.contains("height=\"15\"")); }
590
591 fn make_rect(x0: f64, top: f64, x1: f64, bottom: f64) -> Rect {
594 Rect {
595 x0,
596 top,
597 x1,
598 bottom,
599 line_width: 1.0,
600 stroke: true,
601 fill: false,
602 stroke_color: Color::Gray(0.0),
603 fill_color: Color::Gray(1.0),
604 }
605 }
606
607 #[test]
608 fn test_draw_rects_adds_rect_elements() {
609 let mut renderer = SvgRenderer::new(200.0, 200.0);
610 let rects = vec![make_rect(50.0, 50.0, 150.0, 100.0)];
611 renderer.draw_rects(&rects, &DrawStyle::rects_default());
612 let svg = renderer.to_svg(&SvgOptions::default());
613
614 let rect_count = svg.matches("<rect").count();
615 assert_eq!(rect_count, 2, "1 page boundary + 1 rect overlay");
616 assert!(svg.contains("stroke:green"));
617 }
618
619 fn make_line(x0: f64, top: f64, x1: f64, bottom: f64) -> Line {
622 Line {
623 x0,
624 top,
625 x1,
626 bottom,
627 line_width: 1.0,
628 stroke_color: Color::Gray(0.0),
629 orientation: Orientation::Horizontal,
630 }
631 }
632
633 #[test]
634 fn test_draw_lines_adds_line_elements() {
635 let mut renderer = SvgRenderer::new(200.0, 200.0);
636 let lines = vec![
637 make_line(10.0, 50.0, 190.0, 50.0),
638 make_line(100.0, 10.0, 100.0, 190.0),
639 ];
640 renderer.draw_lines(&lines, &DrawStyle::lines_default());
641 let svg = renderer.to_svg(&SvgOptions::default());
642
643 let line_count = svg.matches("<line").count();
644 assert_eq!(line_count, 2);
645 assert!(svg.contains("stroke:red"));
646 }
647
648 #[test]
649 fn test_draw_lines_correct_coordinates() {
650 let mut renderer = SvgRenderer::new(200.0, 200.0);
651 let lines = vec![make_line(10.0, 50.0, 190.0, 50.0)];
652 renderer.draw_lines(&lines, &DrawStyle::lines_default());
653 let svg = renderer.to_svg(&SvgOptions::default());
654
655 assert!(svg.contains("x1=\"10\""));
656 assert!(svg.contains("y1=\"50\""));
657 assert!(svg.contains("x2=\"190\""));
658 assert!(svg.contains("y2=\"50\""));
659 }
660
661 fn make_edge(x0: f64, top: f64, x1: f64, bottom: f64) -> Edge {
664 Edge {
665 x0,
666 top,
667 x1,
668 bottom,
669 orientation: Orientation::Horizontal,
670 source: EdgeSource::Line,
671 }
672 }
673
674 #[test]
675 fn test_draw_edges_adds_line_elements() {
676 let mut renderer = SvgRenderer::new(200.0, 200.0);
677 let edges = vec![make_edge(0.0, 100.0, 200.0, 100.0)];
678 renderer.draw_edges(&edges, &DrawStyle::edges_default());
679 let svg = renderer.to_svg(&SvgOptions::default());
680
681 let line_count = svg.matches("<line").count();
682 assert_eq!(line_count, 1);
683 assert!(svg.contains("stroke:orange"));
684 }
685
686 fn make_table() -> Table {
689 Table {
690 bbox: BBox::new(10.0, 10.0, 200.0, 100.0),
691 cells: vec![
692 Cell {
693 bbox: BBox::new(10.0, 10.0, 100.0, 50.0),
694 text: Some("A".to_string()),
695 },
696 Cell {
697 bbox: BBox::new(100.0, 10.0, 200.0, 50.0),
698 text: Some("B".to_string()),
699 },
700 Cell {
701 bbox: BBox::new(10.0, 50.0, 100.0, 100.0),
702 text: Some("C".to_string()),
703 },
704 Cell {
705 bbox: BBox::new(100.0, 50.0, 200.0, 100.0),
706 text: Some("D".to_string()),
707 },
708 ],
709 rows: vec![],
710 columns: vec![],
711 }
712 }
713
714 #[test]
715 fn test_draw_tables_adds_cell_rects() {
716 let mut renderer = SvgRenderer::new(300.0, 200.0);
717 let tables = vec![make_table()];
718 renderer.draw_tables(&tables, &DrawStyle::tables_default());
719 let svg = renderer.to_svg(&SvgOptions::default());
720
721 let rect_count = svg.matches("<rect").count();
723 assert_eq!(rect_count, 5);
724 assert!(svg.contains("fill:lightblue"));
725 }
726
727 #[test]
730 fn test_svg_with_mixed_objects() {
731 let mut renderer = SvgRenderer::new(400.0, 400.0);
732
733 let chars = vec![make_char("H", 10.0, 10.0, 20.0, 22.0)];
734 let lines = vec![make_line(0.0, 100.0, 400.0, 100.0)];
735 let rects = vec![make_rect(50.0, 50.0, 150.0, 80.0)];
736
737 renderer.draw_chars(&chars, &DrawStyle::chars_default());
738 renderer.draw_lines(&lines, &DrawStyle::lines_default());
739 renderer.draw_rects(&rects, &DrawStyle::rects_default());
740
741 let svg = renderer.to_svg(&SvgOptions::default());
742
743 assert!(svg.contains("stroke:blue"), "chars overlay");
745 assert!(svg.contains("stroke:red"), "lines overlay");
746 assert!(svg.contains("stroke:green"), "rects overlay");
747 let rect_count = svg.matches("<rect").count();
749 assert_eq!(rect_count, 3);
750 let line_count = svg.matches("<line").count();
751 assert_eq!(line_count, 1);
752 }
753
754 #[test]
755 fn test_style_customization() {
756 let mut renderer = SvgRenderer::new(100.0, 100.0);
757 let chars = vec![make_char("Z", 5.0, 5.0, 15.0, 17.0)];
758 let custom_style = DrawStyle {
759 fill: Some("yellow".to_string()),
760 stroke: Some("purple".to_string()),
761 stroke_width: 3.0,
762 opacity: 0.5,
763 };
764 renderer.draw_chars(&chars, &custom_style);
765 let svg = renderer.to_svg(&SvgOptions::default());
766
767 assert!(svg.contains("fill:yellow"));
768 assert!(svg.contains("stroke:purple"));
769 assert!(svg.contains("stroke-width:3"));
770 assert!(svg.contains("opacity:0.5"));
771 }
772
773 #[test]
774 fn test_empty_draw_no_overlays() {
775 let renderer = SvgRenderer::new(100.0, 100.0);
776 let svg = renderer.to_svg(&SvgOptions::default());
777
778 let rect_count = svg.matches("<rect").count();
780 assert_eq!(rect_count, 1);
781 let line_count = svg.matches("<line").count();
782 assert_eq!(line_count, 0);
783 }
784
785 #[test]
786 fn test_draw_chars_empty_slice() {
787 let mut renderer = SvgRenderer::new(100.0, 100.0);
788 renderer.draw_chars(&[], &DrawStyle::chars_default());
789 let svg = renderer.to_svg(&SvgOptions::default());
790
791 let rect_count = svg.matches("<rect").count();
793 assert_eq!(rect_count, 1);
794 }
795
796 #[test]
799 fn test_svg_debug_options_default() {
800 let opts = SvgDebugOptions::default();
801 assert!(opts.show_edges);
802 assert!(opts.show_intersections);
803 assert!(opts.show_cells);
804 assert!(opts.show_tables);
805 }
806
807 #[test]
808 fn test_svg_debug_options_selective() {
809 let opts = SvgDebugOptions {
810 show_edges: true,
811 show_intersections: false,
812 show_cells: false,
813 show_tables: true,
814 };
815 assert!(opts.show_edges);
816 assert!(!opts.show_intersections);
817 assert!(!opts.show_cells);
818 assert!(opts.show_tables);
819 }
820
821 #[test]
824 fn test_draw_style_intersections_default() {
825 let style = DrawStyle::intersections_default();
826 assert!(style.fill.is_some());
828 assert!(style.stroke.is_some());
829 }
830
831 #[test]
832 fn test_draw_style_cells_default() {
833 let style = DrawStyle::cells_default();
834 assert!(style.stroke.is_some());
836 }
837
838 #[test]
841 fn test_draw_intersections_adds_circle_elements() {
842 let mut renderer = SvgRenderer::new(200.0, 200.0);
843 let intersections = vec![
844 Intersection { x: 50.0, y: 50.0 },
845 Intersection { x: 100.0, y: 50.0 },
846 Intersection { x: 50.0, y: 100.0 },
847 ];
848 renderer.draw_intersections(&intersections, &DrawStyle::intersections_default());
849 let svg = renderer.to_svg(&SvgOptions::default());
850
851 let circle_count = svg.matches("<circle").count();
852 assert_eq!(circle_count, 3, "Should have 3 intersection circles");
853 }
854
855 #[test]
856 fn test_draw_intersections_correct_coordinates() {
857 let mut renderer = SvgRenderer::new(200.0, 200.0);
858 let intersections = vec![Intersection { x: 75.0, y: 125.0 }];
859 renderer.draw_intersections(&intersections, &DrawStyle::intersections_default());
860 let svg = renderer.to_svg(&SvgOptions::default());
861
862 assert!(svg.contains("cx=\"75\""));
863 assert!(svg.contains("cy=\"125\""));
864 }
865
866 #[test]
867 fn test_draw_intersections_empty_slice() {
868 let mut renderer = SvgRenderer::new(100.0, 100.0);
869 renderer.draw_intersections(&[], &DrawStyle::intersections_default());
870 let svg = renderer.to_svg(&SvgOptions::default());
871
872 let circle_count = svg.matches("<circle").count();
873 assert_eq!(circle_count, 0);
874 }
875
876 #[test]
879 fn test_draw_cells_adds_rect_elements_with_dashed_style() {
880 let mut renderer = SvgRenderer::new(200.0, 200.0);
881 let cells = vec![
882 Cell {
883 bbox: BBox::new(10.0, 10.0, 100.0, 50.0),
884 text: None,
885 },
886 Cell {
887 bbox: BBox::new(100.0, 10.0, 200.0, 50.0),
888 text: None,
889 },
890 ];
891 renderer.draw_cells(&cells, &DrawStyle::cells_default());
892 let svg = renderer.to_svg(&SvgOptions::default());
893
894 let rect_count = svg.matches("<rect").count();
896 assert_eq!(rect_count, 3);
897 assert!(svg.contains("stroke-dasharray"));
899 }
900
901 #[test]
902 fn test_draw_cells_correct_coordinates() {
903 let mut renderer = SvgRenderer::new(200.0, 200.0);
904 let cells = vec![Cell {
905 bbox: BBox::new(20.0, 30.0, 80.0, 90.0),
906 text: None,
907 }];
908 renderer.draw_cells(&cells, &DrawStyle::cells_default());
909 let svg = renderer.to_svg(&SvgOptions::default());
910
911 assert!(svg.contains("x=\"20\""));
912 assert!(svg.contains("y=\"30\""));
913 assert!(svg.contains("width=\"60\"")); assert!(svg.contains("height=\"60\"")); }
916
917 #[test]
920 fn test_debug_tablefinder_svg_with_table() {
921 let mut renderer = SvgRenderer::new(300.0, 200.0);
922
923 let edges = vec![
925 make_edge(10.0, 10.0, 200.0, 10.0),
926 make_edge(10.0, 50.0, 200.0, 50.0),
927 make_edge(10.0, 100.0, 200.0, 100.0),
928 ];
929 let intersections = vec![
930 Intersection { x: 10.0, y: 10.0 },
931 Intersection { x: 200.0, y: 10.0 },
932 Intersection { x: 10.0, y: 50.0 },
933 Intersection { x: 200.0, y: 50.0 },
934 ];
935 let cells = vec![Cell {
936 bbox: BBox::new(10.0, 10.0, 200.0, 50.0),
937 text: None,
938 }];
939 let tables = vec![Table {
940 bbox: BBox::new(10.0, 10.0, 200.0, 100.0),
941 cells: cells.clone(),
942 rows: vec![],
943 columns: vec![],
944 }];
945
946 let debug_opts = SvgDebugOptions::default();
947
948 if debug_opts.show_edges {
949 renderer.draw_edges(&edges, &DrawStyle::edges_default());
950 }
951 if debug_opts.show_intersections {
952 renderer.draw_intersections(&intersections, &DrawStyle::intersections_default());
953 }
954 if debug_opts.show_cells {
955 renderer.draw_cells(&cells, &DrawStyle::cells_default());
956 }
957 if debug_opts.show_tables {
958 renderer.draw_tables(&tables, &DrawStyle::tables_default());
959 }
960
961 let svg = renderer.to_svg(&SvgOptions::default());
962
963 assert!(svg.contains("<line"), "Should contain edge lines");
965 assert!(
967 svg.contains("<circle"),
968 "Should contain intersection circles"
969 );
970 assert!(
972 svg.contains("stroke-dasharray"),
973 "Should contain dashed cell boundaries"
974 );
975 assert!(svg.contains("fill:lightblue"), "Should contain table fill");
977 }
978
979 #[test]
980 fn test_debug_tablefinder_svg_no_tables() {
981 let renderer = SvgRenderer::new(300.0, 200.0);
982 let svg = renderer.to_svg(&SvgOptions::default());
983
984 assert!(svg.contains("<svg"));
986 assert!(svg.contains("</svg>"));
987 let rect_count = svg.matches("<rect").count();
988 assert_eq!(rect_count, 1, "Only page boundary rect");
989 }
990
991 #[test]
992 fn test_debug_tablefinder_svg_selective_show() {
993 let mut renderer = SvgRenderer::new(200.0, 200.0);
994
995 let edges = vec![make_edge(10.0, 10.0, 200.0, 10.0)];
996 let intersections = vec![Intersection { x: 10.0, y: 10.0 }];
997
998 let debug_opts = SvgDebugOptions {
999 show_edges: true,
1000 show_intersections: false,
1001 show_cells: false,
1002 show_tables: false,
1003 };
1004
1005 if debug_opts.show_edges {
1006 renderer.draw_edges(&edges, &DrawStyle::edges_default());
1007 }
1008 if debug_opts.show_intersections {
1009 renderer.draw_intersections(&intersections, &DrawStyle::intersections_default());
1010 }
1011
1012 let svg = renderer.to_svg(&SvgOptions::default());
1013
1014 assert!(svg.contains("<line"), "Edges should be shown");
1016 assert!(!svg.contains("<circle"), "Intersections should be hidden");
1018 }
1019}