1use crate::geometry::{Orientation, Point};
7use crate::painting::{Color, PaintedPath};
8use crate::path::PathSegment;
9
10pub type LineOrientation = Orientation;
12
13#[derive(Debug, Clone, PartialEq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub struct Line {
19 pub x0: f64,
21 pub top: f64,
23 pub x1: f64,
25 pub bottom: f64,
27 pub line_width: f64,
29 pub stroke_color: Color,
31 pub orientation: Orientation,
33}
34
35#[derive(Debug, Clone, PartialEq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub struct Curve {
41 pub x0: f64,
43 pub top: f64,
45 pub x1: f64,
47 pub bottom: f64,
49 pub pts: Vec<(f64, f64)>,
51 pub line_width: f64,
53 pub stroke: bool,
55 pub fill: bool,
57 pub stroke_color: Color,
59 pub fill_color: Color,
61}
62
63#[derive(Debug, Clone, PartialEq)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
68pub struct Rect {
69 pub x0: f64,
71 pub top: f64,
73 pub x1: f64,
75 pub bottom: f64,
77 pub line_width: f64,
79 pub stroke: bool,
81 pub fill: bool,
83 pub stroke_color: Color,
85 pub fill_color: Color,
87}
88
89impl Rect {
90 pub fn width(&self) -> f64 {
92 self.x1 - self.x0
93 }
94
95 pub fn height(&self) -> f64 {
97 self.bottom - self.top
98 }
99}
100
101const AXIS_TOLERANCE: f64 = 1e-6;
103
104fn classify_orientation(x0: f64, y0: f64, x1: f64, y1: f64) -> Orientation {
106 let dx = (x1 - x0).abs();
107 let dy = (y1 - y0).abs();
108 if dy < AXIS_TOLERANCE {
109 Orientation::Horizontal
110 } else if dx < AXIS_TOLERANCE {
111 Orientation::Vertical
112 } else {
113 Orientation::Diagonal
114 }
115}
116
117fn flip_y(y: f64, page_height: f64) -> f64 {
119 page_height - y
120}
121
122fn try_detect_rect(vertices: &[Point], page_height: f64) -> Option<(f64, f64, f64, f64)> {
127 if vertices.len() != 4 {
129 return None;
130 }
131
132 for i in 0..4 {
134 let a = &vertices[i];
135 let b = &vertices[(i + 1) % 4];
136 let dx = (b.x - a.x).abs();
137 let dy = (b.y - a.y).abs();
138 if dx > AXIS_TOLERANCE && dy > AXIS_TOLERANCE {
140 return None;
141 }
142 }
143
144 let xs: Vec<f64> = vertices.iter().map(|p| p.x).collect();
146 let ys: Vec<f64> = vertices.iter().map(|p| flip_y(p.y, page_height)).collect();
147
148 let x0 = xs.iter().cloned().fold(f64::INFINITY, f64::min);
149 let x1 = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
150 let top = ys.iter().cloned().fold(f64::INFINITY, f64::min);
151 let bottom = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
152
153 Some((x0, top, x1, bottom))
154}
155
156fn extract_subpaths(segments: &[PathSegment]) -> Vec<&[PathSegment]> {
161 let mut subpaths = Vec::new();
162 let mut start = 0;
163
164 for (i, seg) in segments.iter().enumerate() {
165 if i > 0 && matches!(seg, PathSegment::MoveTo(_)) {
166 if start < i {
167 subpaths.push(&segments[start..i]);
168 }
169 start = i;
170 }
171 }
172 if start < segments.len() {
173 subpaths.push(&segments[start..]);
174 }
175
176 subpaths
177}
178
179fn collect_vertices(subpath: &[PathSegment]) -> Vec<Point> {
184 let mut vertices = Vec::new();
185 let mut has_curves = false;
186
187 for seg in subpath {
188 match seg {
189 PathSegment::MoveTo(p) => {
190 vertices.push(*p);
191 }
192 PathSegment::LineTo(p) => {
193 vertices.push(*p);
194 }
195 PathSegment::CurveTo { .. } => {
196 has_curves = true;
197 }
198 PathSegment::ClosePath => {
199 }
202 }
203 }
204
205 if has_curves {
207 return Vec::new();
208 }
209
210 vertices
211}
212
213fn is_closed(subpath: &[PathSegment], vertices: &[Point]) -> bool {
215 if subpath.iter().any(|s| matches!(s, PathSegment::ClosePath)) {
216 return true;
217 }
218 if vertices.len() >= 2 {
220 let first = vertices[0];
221 let last = vertices[vertices.len() - 1];
222 return (first.x - last.x).abs() < AXIS_TOLERANCE
223 && (first.y - last.y).abs() < AXIS_TOLERANCE;
224 }
225 false
226}
227
228fn has_curves(subpath: &[PathSegment]) -> bool {
230 subpath
231 .iter()
232 .any(|s| matches!(s, PathSegment::CurveTo { .. }))
233}
234
235pub fn extract_shapes(
252 painted: &PaintedPath,
253 page_height: f64,
254) -> (Vec<Line>, Vec<Rect>, Vec<Curve>) {
255 let mut lines = Vec::new();
256 let mut rects = Vec::new();
257 let mut curves = Vec::new();
258
259 let subpaths = extract_subpaths(&painted.path.segments);
260
261 for subpath in subpaths {
262 if has_curves(subpath) {
264 extract_curves_from_subpath(subpath, painted, page_height, &mut curves, &mut lines);
265 continue;
266 }
267
268 let vertices = collect_vertices(subpath);
269 if vertices.is_empty() {
270 continue;
271 }
272
273 let closed = is_closed(subpath, &vertices);
274
275 if closed && vertices.len() == 4 {
277 if let Some((x0, top, x1, bottom)) = try_detect_rect(&vertices, page_height) {
278 rects.push(Rect {
279 x0,
280 top,
281 x1,
282 bottom,
283 line_width: painted.line_width,
284 stroke: painted.stroke,
285 fill: painted.fill,
286 stroke_color: painted.stroke_color.clone(),
287 fill_color: painted.fill_color.clone(),
288 });
289 continue;
290 }
291 }
292
293 if closed && vertices.len() == 5 {
295 let first = vertices[0];
296 let last = vertices[4];
297 if (first.x - last.x).abs() < AXIS_TOLERANCE
298 && (first.y - last.y).abs() < AXIS_TOLERANCE
299 {
300 if let Some((x0, top, x1, bottom)) = try_detect_rect(&vertices[..4], page_height) {
301 rects.push(Rect {
302 x0,
303 top,
304 x1,
305 bottom,
306 line_width: painted.line_width,
307 stroke: painted.stroke,
308 fill: painted.fill,
309 stroke_color: painted.stroke_color.clone(),
310 fill_color: painted.fill_color.clone(),
311 });
312 continue;
313 }
314 }
315 }
316
317 if !painted.stroke {
319 continue;
320 }
321
322 extract_lines_from_subpath(subpath, &vertices, painted, page_height, &mut lines);
323 }
324
325 (lines, rects, curves)
326}
327
328fn extract_lines_from_subpath(
330 subpath: &[PathSegment],
331 vertices: &[Point],
332 painted: &PaintedPath,
333 page_height: f64,
334 lines: &mut Vec<Line>,
335) {
336 let mut prev_point: Option<Point> = None;
337 for seg in subpath {
338 match seg {
339 PathSegment::MoveTo(p) => {
340 prev_point = Some(*p);
341 }
342 PathSegment::LineTo(p) => {
343 if let Some(start) = prev_point {
344 push_line(start, *p, painted, page_height, lines);
345 }
346 prev_point = Some(*p);
347 }
348 PathSegment::ClosePath => {
349 if let (Some(current), Some(start_pt)) = (prev_point, vertices.first().copied()) {
350 if (current.x - start_pt.x).abs() > AXIS_TOLERANCE
351 || (current.y - start_pt.y).abs() > AXIS_TOLERANCE
352 {
353 push_line(current, start_pt, painted, page_height, lines);
354 }
355 }
356 prev_point = vertices.first().copied();
357 }
358 PathSegment::CurveTo { .. } => {}
359 }
360 }
361}
362
363fn push_line(
365 start: Point,
366 end: Point,
367 painted: &PaintedPath,
368 page_height: f64,
369 lines: &mut Vec<Line>,
370) {
371 let fy0 = flip_y(start.y, page_height);
372 let fy1 = flip_y(end.y, page_height);
373
374 let x0 = start.x.min(end.x);
375 let x1 = start.x.max(end.x);
376 let top = fy0.min(fy1);
377 let bottom = fy0.max(fy1);
378 let orientation = classify_orientation(start.x, fy0, end.x, fy1);
379
380 lines.push(Line {
381 x0,
382 top,
383 x1,
384 bottom,
385 line_width: painted.line_width,
386 stroke_color: painted.stroke_color.clone(),
387 orientation,
388 });
389}
390
391fn extract_curves_from_subpath(
393 subpath: &[PathSegment],
394 painted: &PaintedPath,
395 page_height: f64,
396 curves: &mut Vec<Curve>,
397 lines: &mut Vec<Line>,
398) {
399 let mut prev_point: Option<Point> = None;
400 let mut subpath_start: Option<Point> = None;
401
402 for seg in subpath {
403 match seg {
404 PathSegment::MoveTo(p) => {
405 prev_point = Some(*p);
406 subpath_start = Some(*p);
407 }
408 PathSegment::LineTo(p) => {
409 if painted.stroke {
410 if let Some(start) = prev_point {
411 push_line(start, *p, painted, page_height, lines);
412 }
413 }
414 prev_point = Some(*p);
415 }
416 PathSegment::CurveTo { cp1, cp2, end } => {
417 if let Some(start) = prev_point {
418 let all_x = [start.x, cp1.x, cp2.x, end.x];
420 let all_y = [
421 flip_y(start.y, page_height),
422 flip_y(cp1.y, page_height),
423 flip_y(cp2.y, page_height),
424 flip_y(end.y, page_height),
425 ];
426
427 let x0 = all_x.iter().cloned().fold(f64::INFINITY, f64::min);
428 let x1 = all_x.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
429 let top = all_y.iter().cloned().fold(f64::INFINITY, f64::min);
430 let bottom = all_y.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
431
432 curves.push(Curve {
433 x0,
434 top,
435 x1,
436 bottom,
437 pts: vec![
438 (start.x, flip_y(start.y, page_height)),
439 (cp1.x, flip_y(cp1.y, page_height)),
440 (cp2.x, flip_y(cp2.y, page_height)),
441 (end.x, flip_y(end.y, page_height)),
442 ],
443 line_width: painted.line_width,
444 stroke: painted.stroke,
445 fill: painted.fill,
446 stroke_color: painted.stroke_color.clone(),
447 fill_color: painted.fill_color.clone(),
448 });
449 }
450 prev_point = Some(*end);
451 }
452 PathSegment::ClosePath => {
453 if painted.stroke {
455 if let (Some(current), Some(start_pt)) = (prev_point, subpath_start) {
456 if (current.x - start_pt.x).abs() > AXIS_TOLERANCE
457 || (current.y - start_pt.y).abs() > AXIS_TOLERANCE
458 {
459 push_line(current, start_pt, painted, page_height, lines);
460 }
461 }
462 }
463 prev_point = subpath_start;
464 }
465 }
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use crate::geometry::Ctm;
473 use crate::painting::{DashPattern, FillRule, GraphicsState};
474 use crate::path::PathBuilder;
475
476 const PAGE_HEIGHT: f64 = 792.0;
477
478 #[test]
481 fn test_line_construction_and_field_access() {
482 let line = Line {
483 x0: 10.0,
484 top: 20.0,
485 x1: 100.0,
486 bottom: 20.0,
487 line_width: 1.5,
488 stroke_color: Color::Rgb(1.0, 0.0, 0.0),
489 orientation: Orientation::Horizontal,
490 };
491 assert_eq!(line.x0, 10.0);
492 assert_eq!(line.top, 20.0);
493 assert_eq!(line.x1, 100.0);
494 assert_eq!(line.bottom, 20.0);
495 assert_eq!(line.line_width, 1.5);
496 assert_eq!(line.stroke_color, Color::Rgb(1.0, 0.0, 0.0));
497 assert_eq!(line.orientation, Orientation::Horizontal);
498 }
499
500 #[test]
501 fn test_rect_construction_and_field_access() {
502 let rect = Rect {
503 x0: 50.0,
504 top: 100.0,
505 x1: 200.0,
506 bottom: 300.0,
507 line_width: 2.0,
508 stroke: true,
509 fill: true,
510 stroke_color: Color::Gray(0.0),
511 fill_color: Color::Cmyk(0.0, 1.0, 1.0, 0.0),
512 };
513 assert_eq!(rect.x0, 50.0);
514 assert_eq!(rect.top, 100.0);
515 assert_eq!(rect.x1, 200.0);
516 assert_eq!(rect.bottom, 300.0);
517 assert_eq!(rect.line_width, 2.0);
518 assert!(rect.stroke);
519 assert!(rect.fill);
520 assert_eq!(rect.stroke_color, Color::Gray(0.0));
521 assert_eq!(rect.fill_color, Color::Cmyk(0.0, 1.0, 1.0, 0.0));
522 assert_eq!(rect.width(), 150.0);
523 assert_eq!(rect.height(), 200.0);
524 }
525
526 #[test]
527 fn test_curve_construction_and_field_access() {
528 let curve = Curve {
529 x0: 0.0,
530 top: 50.0,
531 x1: 100.0,
532 bottom: 100.0,
533 pts: vec![(0.0, 100.0), (30.0, 50.0), (70.0, 50.0), (100.0, 100.0)],
534 line_width: 1.0,
535 stroke: true,
536 fill: false,
537 stroke_color: Color::black(),
538 fill_color: Color::black(),
539 };
540 assert_eq!(curve.x0, 0.0);
541 assert_eq!(curve.top, 50.0);
542 assert_eq!(curve.x1, 100.0);
543 assert_eq!(curve.bottom, 100.0);
544 assert_eq!(curve.pts.len(), 4);
545 assert_eq!(curve.pts[0], (0.0, 100.0));
546 assert_eq!(curve.pts[3], (100.0, 100.0));
547 assert_eq!(curve.line_width, 1.0);
548 assert!(curve.stroke);
549 assert!(!curve.fill);
550 }
551
552 fn default_gs() -> GraphicsState {
553 GraphicsState::default()
554 }
555
556 fn custom_gs() -> GraphicsState {
557 GraphicsState {
558 line_width: 2.5,
559 stroke_color: Color::Rgb(1.0, 0.0, 0.0),
560 fill_color: Color::Rgb(0.0, 0.0, 1.0),
561 ..GraphicsState::default()
562 }
563 }
564
565 fn assert_approx(a: f64, b: f64) {
566 assert!(
567 (a - b).abs() < 1e-6,
568 "expected {b}, got {a}, diff={}",
569 (a - b).abs()
570 );
571 }
572
573 #[test]
576 fn test_horizontal_line_extraction() {
577 let mut builder = PathBuilder::new(Ctm::identity());
578 builder.move_to(100.0, 500.0);
579 builder.line_to(300.0, 500.0);
580 let painted = builder.stroke(&default_gs());
581
582 let (lines, rects, _) = extract_shapes(&painted, PAGE_HEIGHT);
583 assert_eq!(lines.len(), 1);
584 assert!(rects.is_empty());
585
586 let line = &lines[0];
587 assert_approx(line.x0, 100.0);
588 assert_approx(line.x1, 300.0);
589 assert_approx(line.top, 292.0);
591 assert_approx(line.bottom, 292.0);
592 assert_eq!(line.orientation, Orientation::Horizontal);
593 assert_approx(line.line_width, 1.0);
594 }
595
596 #[test]
599 fn test_vertical_line_extraction() {
600 let mut builder = PathBuilder::new(Ctm::identity());
601 builder.move_to(200.0, 100.0);
602 builder.line_to(200.0, 400.0);
603 let painted = builder.stroke(&default_gs());
604
605 let (lines, rects, _) = extract_shapes(&painted, PAGE_HEIGHT);
606 assert_eq!(lines.len(), 1);
607 assert!(rects.is_empty());
608
609 let line = &lines[0];
610 assert_approx(line.x0, 200.0);
611 assert_approx(line.x1, 200.0);
612 assert_approx(line.top, 392.0);
614 assert_approx(line.bottom, 692.0);
615 assert_eq!(line.orientation, Orientation::Vertical);
616 }
617
618 #[test]
621 fn test_diagonal_line_extraction() {
622 let mut builder = PathBuilder::new(Ctm::identity());
623 builder.move_to(100.0, 100.0);
624 builder.line_to(300.0, 400.0);
625 let painted = builder.stroke(&default_gs());
626
627 let (lines, rects, _) = extract_shapes(&painted, PAGE_HEIGHT);
628 assert_eq!(lines.len(), 1);
629 assert!(rects.is_empty());
630
631 let line = &lines[0];
632 assert_approx(line.x0, 100.0);
633 assert_approx(line.x1, 300.0);
634 assert_approx(line.top, 392.0);
636 assert_approx(line.bottom, 692.0);
637 assert_eq!(line.orientation, Orientation::Diagonal);
638 }
639
640 #[test]
643 fn test_line_with_custom_width_and_color() {
644 let mut builder = PathBuilder::new(Ctm::identity());
645 builder.move_to(0.0, 0.0);
646 builder.line_to(100.0, 0.0);
647 let painted = builder.stroke(&custom_gs());
648
649 let (lines, _, _) = extract_shapes(&painted, PAGE_HEIGHT);
650 assert_eq!(lines.len(), 1);
651
652 let line = &lines[0];
653 assert_approx(line.line_width, 2.5);
654 assert_eq!(line.stroke_color, Color::Rgb(1.0, 0.0, 0.0));
655 }
656
657 #[test]
660 fn test_rect_from_re_operator() {
661 let mut builder = PathBuilder::new(Ctm::identity());
662 builder.rectangle(100.0, 200.0, 200.0, 100.0);
664 let painted = builder.stroke(&default_gs());
665
666 let (lines, rects, _) = extract_shapes(&painted, PAGE_HEIGHT);
667 assert!(lines.is_empty());
668 assert_eq!(rects.len(), 1);
669
670 let rect = &rects[0];
671 assert_approx(rect.x0, 100.0);
672 assert_approx(rect.x1, 300.0);
673 assert_approx(rect.top, 492.0);
675 assert_approx(rect.bottom, 592.0);
677 assert!(rect.stroke);
678 assert!(!rect.fill);
679 }
680
681 #[test]
684 fn test_rect_from_four_line_closed_path() {
685 let mut builder = PathBuilder::new(Ctm::identity());
686 builder.move_to(50.0, 100.0);
688 builder.line_to(250.0, 100.0);
689 builder.line_to(250.0, 300.0);
690 builder.line_to(50.0, 300.0);
691 builder.close_path();
692 let painted = builder.fill(&default_gs());
693
694 let (lines, rects, _) = extract_shapes(&painted, PAGE_HEIGHT);
695 assert!(lines.is_empty());
696 assert_eq!(rects.len(), 1);
697
698 let rect = &rects[0];
699 assert_approx(rect.x0, 50.0);
700 assert_approx(rect.x1, 250.0);
701 assert_approx(rect.top, 492.0);
703 assert_approx(rect.bottom, 692.0);
704 assert!(!rect.stroke);
705 assert!(rect.fill);
706 }
707
708 #[test]
711 fn test_rect_fill_and_stroke() {
712 let mut builder = PathBuilder::new(Ctm::identity());
713 builder.rectangle(10.0, 20.0, 100.0, 50.0);
714 let painted = builder.fill_and_stroke(&custom_gs());
715
716 let (lines, rects, _) = extract_shapes(&painted, PAGE_HEIGHT);
717 assert!(lines.is_empty());
718 assert_eq!(rects.len(), 1);
719
720 let rect = &rects[0];
721 assert!(rect.stroke);
722 assert!(rect.fill);
723 assert_approx(rect.line_width, 2.5);
724 assert_eq!(rect.stroke_color, Color::Rgb(1.0, 0.0, 0.0));
725 assert_eq!(rect.fill_color, Color::Rgb(0.0, 0.0, 1.0));
726 }
727
728 #[test]
731 fn test_rect_width_and_height() {
732 let mut builder = PathBuilder::new(Ctm::identity());
733 builder.rectangle(100.0, 200.0, 150.0, 80.0);
734 let painted = builder.stroke(&default_gs());
735
736 let (_, rects, _) = extract_shapes(&painted, PAGE_HEIGHT);
737 assert_eq!(rects.len(), 1);
738
739 let rect = &rects[0];
740 assert_approx(rect.width(), 150.0);
741 assert_approx(rect.height(), 80.0);
742 }
743
744 #[test]
747 fn test_non_rect_closed_path_produces_lines() {
748 let mut builder = PathBuilder::new(Ctm::identity());
750 builder.move_to(100.0, 100.0);
751 builder.line_to(200.0, 100.0);
752 builder.line_to(150.0, 200.0);
753 builder.close_path(); let painted = builder.stroke(&default_gs());
755
756 let (lines, rects, _) = extract_shapes(&painted, PAGE_HEIGHT);
757 assert!(rects.is_empty());
758 assert_eq!(lines.len(), 3);
760
761 assert_eq!(lines[0].orientation, Orientation::Horizontal);
763 assert_eq!(lines[1].orientation, Orientation::Diagonal);
765 assert_eq!(lines[2].orientation, Orientation::Diagonal);
766 }
767
768 #[test]
771 fn test_non_axis_aligned_quadrilateral_produces_lines() {
772 let mut builder = PathBuilder::new(Ctm::identity());
774 builder.move_to(150.0, 100.0);
775 builder.line_to(200.0, 200.0);
776 builder.line_to(150.0, 300.0);
777 builder.line_to(100.0, 200.0);
778 builder.close_path();
779 let painted = builder.stroke(&default_gs());
780
781 let (lines, rects, _) = extract_shapes(&painted, PAGE_HEIGHT);
782 assert!(rects.is_empty());
783 assert_eq!(lines.len(), 4); }
785
786 #[test]
789 fn test_fill_only_does_not_produce_lines() {
790 let mut builder = PathBuilder::new(Ctm::identity());
792 builder.move_to(100.0, 100.0);
793 builder.line_to(200.0, 100.0);
794 builder.line_to(150.0, 200.0);
795 builder.close_path();
796 let painted = builder.fill(&default_gs());
797
798 let (lines, rects, _) = extract_shapes(&painted, PAGE_HEIGHT);
799 assert!(lines.is_empty()); assert!(rects.is_empty()); }
802
803 #[test]
806 fn test_multiple_subpaths_lines() {
807 let mut builder = PathBuilder::new(Ctm::identity());
808 builder.move_to(0.0, 100.0);
810 builder.line_to(200.0, 100.0);
811 builder.move_to(100.0, 0.0);
813 builder.line_to(100.0, 200.0);
814 let painted = builder.stroke(&default_gs());
815
816 let (lines, rects, _) = extract_shapes(&painted, PAGE_HEIGHT);
817 assert_eq!(lines.len(), 2);
818 assert!(rects.is_empty());
819 assert_eq!(lines[0].orientation, Orientation::Horizontal);
820 assert_eq!(lines[1].orientation, Orientation::Vertical);
821 }
822
823 #[test]
826 fn test_multiple_subpaths_rect_and_line() {
827 let mut builder = PathBuilder::new(Ctm::identity());
828 builder.rectangle(10.0, 10.0, 100.0, 50.0);
830 builder.move_to(0.0, 100.0);
832 builder.line_to(200.0, 100.0);
833 let painted = builder.stroke(&default_gs());
834
835 let (lines, rects, _) = extract_shapes(&painted, PAGE_HEIGHT);
836 assert_eq!(rects.len(), 1);
837 assert_eq!(lines.len(), 1);
838 }
839
840 #[test]
843 fn test_end_path_produces_nothing() {
844 let mut builder = PathBuilder::new(Ctm::identity());
845 builder.rectangle(10.0, 10.0, 100.0, 50.0);
846 let result = builder.end_path();
847 assert!(result.is_none());
848 }
850
851 #[test]
854 fn test_classify_orientation_horizontal() {
855 assert_eq!(
856 classify_orientation(0.0, 100.0, 200.0, 100.0),
857 Orientation::Horizontal
858 );
859 }
860
861 #[test]
862 fn test_classify_orientation_vertical() {
863 assert_eq!(
864 classify_orientation(100.0, 0.0, 100.0, 200.0),
865 Orientation::Vertical
866 );
867 }
868
869 #[test]
870 fn test_classify_orientation_diagonal() {
871 assert_eq!(
872 classify_orientation(0.0, 0.0, 100.0, 200.0),
873 Orientation::Diagonal
874 );
875 }
876
877 #[test]
880 fn test_y_flip() {
881 assert_approx(flip_y(0.0, 792.0), 792.0);
882 assert_approx(flip_y(792.0, 792.0), 0.0);
883 assert_approx(flip_y(396.0, 792.0), 396.0);
884 assert_approx(flip_y(100.0, 792.0), 692.0);
885 }
886
887 #[test]
890 fn test_empty_path_produces_nothing() {
891 let painted = PaintedPath {
892 path: crate::path::Path {
893 segments: Vec::new(),
894 },
895 stroke: true,
896 fill: false,
897 fill_rule: FillRule::NonZeroWinding,
898 line_width: 1.0,
899 stroke_color: Color::black(),
900 fill_color: Color::black(),
901 dash_pattern: DashPattern::solid(),
902 stroke_alpha: 1.0,
903 fill_alpha: 1.0,
904 };
905
906 let (lines, rects, _) = extract_shapes(&painted, PAGE_HEIGHT);
907 assert!(lines.is_empty());
908 assert!(rects.is_empty());
909 }
910
911 #[test]
914 fn test_single_moveto_produces_nothing() {
915 let mut builder = PathBuilder::new(Ctm::identity());
916 builder.move_to(100.0, 100.0);
917 let painted = builder.stroke(&default_gs());
918
919 let (lines, rects, _) = extract_shapes(&painted, PAGE_HEIGHT);
920 assert!(lines.is_empty());
921 assert!(rects.is_empty());
922 }
923
924 #[test]
927 fn test_path_with_curves_no_rect_detection() {
928 let mut builder = PathBuilder::new(Ctm::identity());
929 builder.move_to(0.0, 0.0);
930 builder.curve_to(10.0, 50.0, 90.0, 50.0, 100.0, 0.0);
931 builder.close_path();
932 let painted = builder.stroke(&default_gs());
933
934 let (lines, rects, curves) = extract_shapes(&painted, PAGE_HEIGHT);
935 assert!(rects.is_empty());
936 assert_eq!(lines.len(), 1);
938 assert_eq!(curves.len(), 1);
939 }
940
941 #[test]
944 fn test_rect_with_ctm_scale() {
945 let ctm = Ctm::new(2.0, 0.0, 0.0, 2.0, 0.0, 0.0);
947 let mut builder = PathBuilder::new(ctm);
948 builder.rectangle(50.0, 100.0, 100.0, 50.0);
949 let painted = builder.stroke(&default_gs());
950
951 let (lines, rects, _) = extract_shapes(&painted, PAGE_HEIGHT);
952 assert!(lines.is_empty());
953 assert_eq!(rects.len(), 1);
954
955 let rect = &rects[0];
956 assert_approx(rect.x0, 100.0);
958 assert_approx(rect.x1, 300.0);
959 assert_approx(rect.top, 492.0);
961 assert_approx(rect.bottom, 592.0);
962 }
963
964 #[test]
967 fn test_curve_extraction_simple() {
968 let mut builder = PathBuilder::new(Ctm::identity());
970 builder.move_to(0.0, 0.0);
971 builder.curve_to(10.0, 50.0, 90.0, 50.0, 100.0, 0.0);
972 let painted = builder.stroke(&default_gs());
973
974 let (_, _, curves) = extract_shapes(&painted, PAGE_HEIGHT);
975 assert_eq!(curves.len(), 1);
976
977 let curve = &curves[0];
978 assert_eq!(curve.pts.len(), 4);
980 assert_approx(curve.pts[0].0, 0.0);
982 assert_approx(curve.pts[0].1, 792.0);
983 assert_approx(curve.pts[1].0, 10.0);
985 assert_approx(curve.pts[1].1, 742.0);
986 assert_approx(curve.pts[2].0, 90.0);
988 assert_approx(curve.pts[2].1, 742.0);
989 assert_approx(curve.pts[3].0, 100.0);
991 assert_approx(curve.pts[3].1, 792.0);
992 }
993
994 #[test]
995 fn test_curve_bbox() {
996 let mut builder = PathBuilder::new(Ctm::identity());
997 builder.move_to(0.0, 0.0);
998 builder.curve_to(10.0, 50.0, 90.0, 50.0, 100.0, 0.0);
999 let painted = builder.stroke(&default_gs());
1000
1001 let (_, _, curves) = extract_shapes(&painted, PAGE_HEIGHT);
1002 let curve = &curves[0];
1003
1004 assert_approx(curve.x0, 0.0);
1006 assert_approx(curve.x1, 100.0);
1007 assert_approx(curve.top, 742.0);
1009 assert_approx(curve.bottom, 792.0);
1010 }
1011
1012 #[test]
1013 fn test_curve_captures_graphics_state() {
1014 let mut builder = PathBuilder::new(Ctm::identity());
1015 builder.move_to(0.0, 0.0);
1016 builder.curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 0.0);
1017 let painted = builder.stroke(&custom_gs());
1018
1019 let (_, _, curves) = extract_shapes(&painted, PAGE_HEIGHT);
1020 assert_eq!(curves.len(), 1);
1021
1022 let curve = &curves[0];
1023 assert_approx(curve.line_width, 2.5);
1024 assert!(curve.stroke);
1025 assert!(!curve.fill);
1026 assert_eq!(curve.stroke_color, Color::Rgb(1.0, 0.0, 0.0));
1027 }
1028
1029 #[test]
1030 fn test_curve_fill_only() {
1031 let mut builder = PathBuilder::new(Ctm::identity());
1032 builder.move_to(0.0, 0.0);
1033 builder.curve_to(10.0, 50.0, 90.0, 50.0, 100.0, 0.0);
1034 builder.close_path();
1035 let painted = builder.fill(&default_gs());
1036
1037 let (lines, _, curves) = extract_shapes(&painted, PAGE_HEIGHT);
1038 assert_eq!(curves.len(), 1);
1039 assert!(curves[0].fill);
1040 assert!(!curves[0].stroke);
1041 assert!(lines.is_empty());
1043 }
1044
1045 #[test]
1046 fn test_multiple_curves_in_subpath() {
1047 let mut builder = PathBuilder::new(Ctm::identity());
1049 builder.move_to(0.0, 0.0);
1050 builder.curve_to(10.0, 50.0, 40.0, 50.0, 50.0, 0.0);
1051 builder.curve_to(60.0, 50.0, 90.0, 50.0, 100.0, 0.0);
1052 let painted = builder.stroke(&default_gs());
1053
1054 let (_, _, curves) = extract_shapes(&painted, PAGE_HEIGHT);
1055 assert_eq!(curves.len(), 2);
1056
1057 assert_approx(curves[0].pts[0].0, 0.0);
1059 assert_approx(curves[0].pts[3].0, 50.0);
1060 assert_approx(curves[1].pts[0].0, 50.0);
1062 assert_approx(curves[1].pts[3].0, 100.0);
1063 }
1064
1065 #[test]
1066 fn test_mixed_line_and_curve_subpath() {
1067 let mut builder = PathBuilder::new(Ctm::identity());
1069 builder.move_to(0.0, 0.0);
1070 builder.line_to(50.0, 0.0);
1071 builder.curve_to(60.0, 0.0, 70.0, 10.0, 70.0, 20.0);
1072 builder.line_to(70.0, 50.0);
1073 let painted = builder.stroke(&default_gs());
1074
1075 let (lines, _, curves) = extract_shapes(&painted, PAGE_HEIGHT);
1076 assert_eq!(curves.len(), 1);
1077 assert_eq!(lines.len(), 2); }
1079
1080 #[test]
1081 fn test_curve_with_ctm_transform() {
1082 let ctm = Ctm::new(2.0, 0.0, 0.0, 2.0, 0.0, 0.0);
1084 let mut builder = PathBuilder::new(ctm);
1085 builder.move_to(0.0, 0.0);
1086 builder.curve_to(10.0, 25.0, 40.0, 25.0, 50.0, 0.0);
1087 let painted = builder.stroke(&default_gs());
1088
1089 let (_, _, curves) = extract_shapes(&painted, PAGE_HEIGHT);
1090 assert_eq!(curves.len(), 1);
1091
1092 let curve = &curves[0];
1093 assert_approx(curve.pts[0].0, 0.0);
1095 assert_approx(curve.pts[1].0, 20.0);
1096 assert_approx(curve.pts[2].0, 80.0);
1097 assert_approx(curve.pts[3].0, 100.0);
1098 }
1099}