1use rpdfium_core::Rect;
13
14#[derive(Debug, Clone, PartialEq)]
18pub enum PathOp {
19 MoveTo { x: f32, y: f32 },
21 LineTo { x: f32, y: f32 },
23 CurveTo {
25 x1: f32,
26 y1: f32,
27 x2: f32,
28 y2: f32,
29 x3: f32,
30 y3: f32,
31 },
32 Close,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
38pub enum FillRule {
39 #[default]
41 NonZero,
42 EvenOdd,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
50#[repr(u8)]
51pub enum LineCapStyle {
52 #[default]
54 Butt = 0,
55 Round = 1,
57 Square = 2,
59}
60
61impl LineCapStyle {
62 pub fn from_i64(value: i64) -> Self {
64 match value {
65 0 => Self::Butt,
66 1 => Self::Round,
67 2 => Self::Square,
68 _ => Self::Butt,
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
77#[repr(u8)]
78pub enum LineJoinStyle {
79 #[default]
81 Miter = 0,
82 Round = 1,
84 Bevel = 2,
86}
87
88impl LineJoinStyle {
89 pub fn from_i64(value: i64) -> Self {
91 match value {
92 0 => Self::Miter,
93 1 => Self::Round,
94 2 => Self::Bevel,
95 _ => Self::Miter,
96 }
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Default)]
102pub struct DashPattern {
103 pub array: Vec<f32>,
105 pub phase: f32,
107}
108
109#[derive(Debug, Clone, PartialEq)]
113pub struct PathStyle {
114 pub fill: Option<FillRule>,
116 pub stroke: bool,
118 pub line_width: f32,
120 pub line_cap: LineCapStyle,
122 pub line_join: LineJoinStyle,
124 pub miter_limit: f32,
126 pub dash: Option<DashPattern>,
128}
129
130impl Default for PathStyle {
131 fn default() -> Self {
132 Self {
133 fill: None,
134 stroke: false,
135 line_width: 1.0,
136 line_cap: LineCapStyle::default(),
137 line_join: LineJoinStyle::default(),
138 miter_limit: 10.0,
139 dash: None,
140 }
141 }
142}
143
144pub fn bounding_box(ops: &[PathOp]) -> Option<Rect> {
153 let mut min_x = f32::INFINITY;
154 let mut min_y = f32::INFINITY;
155 let mut max_x = f32::NEG_INFINITY;
156 let mut max_y = f32::NEG_INFINITY;
157 let mut found = false;
158
159 for op in ops {
160 match op {
161 PathOp::MoveTo { x, y } | PathOp::LineTo { x, y } => {
162 min_x = min_x.min(*x);
163 min_y = min_y.min(*y);
164 max_x = max_x.max(*x);
165 max_y = max_y.max(*y);
166 found = true;
167 }
168 PathOp::CurveTo {
169 x1,
170 y1,
171 x2,
172 y2,
173 x3,
174 y3,
175 } => {
176 min_x = min_x.min(*x1).min(*x2).min(*x3);
177 min_y = min_y.min(*y1).min(*y2).min(*y3);
178 max_x = max_x.max(*x1).max(*x2).max(*x3);
179 max_y = max_y.max(*y1).max(*y2).max(*y3);
180 found = true;
181 }
182 PathOp::Close => {}
183 }
184 }
185
186 if found {
187 Some(Rect::new(
188 min_x as f64,
189 min_y as f64,
190 max_x as f64,
191 max_y as f64,
192 ))
193 } else {
194 None
195 }
196}
197
198#[inline]
202pub fn get_bounding_box(ops: &[PathOp]) -> Option<Rect> {
203 bounding_box(ops)
204}
205
206pub fn bounding_box_for_stroke_path(
220 ops: &[PathOp],
221 line_width: f32,
222 miter_limit: f32,
223) -> Option<Rect> {
224 let inner = bounding_box(ops)?;
225 let half_lw = line_width / 2.0;
226 let inflate = (miter_limit * half_lw).max(half_lw) as f64;
227 Some(Rect::new(
228 inner.left - inflate,
229 inner.bottom - inflate,
230 inner.right + inflate,
231 inner.top + inflate,
232 ))
233}
234
235#[inline]
239pub fn get_bounding_box_for_stroke_path(
240 ops: &[PathOp],
241 line_width: f32,
242 miter_limit: f32,
243) -> Option<Rect> {
244 bounding_box_for_stroke_path(ops, line_width, miter_limit)
245}
246
247pub fn is_rect(ops: &[PathOp]) -> bool {
253 rect_if_axis_aligned(ops).is_some()
254}
255
256pub fn rect_if_axis_aligned(ops: &[PathOp]) -> Option<Rect> {
264 let effective: Vec<&PathOp> = ops
266 .iter()
267 .filter(|op| !matches!(op, PathOp::Close))
268 .collect();
269 if effective.len() != 5 {
271 return None;
272 }
273 let (mx, my) = match effective[0] {
274 PathOp::MoveTo { x, y } => (*x, *y),
275 _ => return None,
276 };
277 let pts: Option<Vec<(f32, f32)>> = effective[1..]
278 .iter()
279 .map(|op| match op {
280 PathOp::LineTo { x, y } => Some((*x, *y)),
281 _ => None,
282 })
283 .collect();
284 let pts = pts?;
285 let all_x = [mx, pts[0].0, pts[1].0, pts[2].0, pts[3].0];
287 let all_y = [my, pts[0].1, pts[1].1, pts[2].1, pts[3].1];
288 let mut xs: Vec<f32> = all_x.to_vec();
289 let mut ys: Vec<f32> = all_y.to_vec();
290 xs.dedup();
291 ys.dedup();
292 xs.sort_by(f32::total_cmp);
294 ys.sort_by(f32::total_cmp);
295 xs.dedup();
296 ys.dedup();
297 if xs.len() == 2 && ys.len() == 2 {
298 Some(Rect::new(
299 xs[0] as f64,
300 ys[0] as f64,
301 xs[1] as f64,
302 ys[1] as f64,
303 ))
304 } else {
305 None
306 }
307}
308
309#[inline]
313pub fn get_rect(ops: &[PathOp]) -> Option<Rect> {
314 rect_if_axis_aligned(ops)
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_line_cap_from_i64() {
323 assert_eq!(LineCapStyle::from_i64(0), LineCapStyle::Butt);
324 assert_eq!(LineCapStyle::from_i64(1), LineCapStyle::Round);
325 assert_eq!(LineCapStyle::from_i64(2), LineCapStyle::Square);
326 assert_eq!(LineCapStyle::from_i64(99), LineCapStyle::Butt); }
328
329 #[test]
330 fn test_line_join_from_i64() {
331 assert_eq!(LineJoinStyle::from_i64(0), LineJoinStyle::Miter);
332 assert_eq!(LineJoinStyle::from_i64(1), LineJoinStyle::Round);
333 assert_eq!(LineJoinStyle::from_i64(2), LineJoinStyle::Bevel);
334 assert_eq!(LineJoinStyle::from_i64(-1), LineJoinStyle::Miter); }
336
337 #[test]
338 fn test_path_style_default() {
339 let style = PathStyle::default();
340 assert!(style.fill.is_none());
341 assert!(!style.stroke);
342 assert_eq!(style.line_width, 1.0);
343 assert_eq!(style.line_cap, LineCapStyle::Butt);
344 assert_eq!(style.line_join, LineJoinStyle::Miter);
345 assert_eq!(style.miter_limit, 10.0);
346 assert!(style.dash.is_none());
347 }
348
349 #[test]
350 fn test_dash_pattern_default() {
351 let dash = DashPattern::default();
352 assert!(dash.array.is_empty());
353 assert_eq!(dash.phase, 0.0);
354 }
355
356 #[test]
357 fn test_path_op_clone() {
358 let op = PathOp::CurveTo {
359 x1: 1.0,
360 y1: 2.0,
361 x2: 3.0,
362 y2: 4.0,
363 x3: 5.0,
364 y3: 6.0,
365 };
366 let op2 = op.clone();
367 assert_eq!(op, op2);
368 }
369
370 #[test]
371 fn test_fill_rule_default() {
372 assert_eq!(FillRule::default(), FillRule::NonZero);
373 }
374
375 #[test]
376 fn test_bounding_box_empty() {
377 assert_eq!(bounding_box(&[]), None);
378 assert_eq!(bounding_box(&[PathOp::Close]), None);
379 }
380
381 #[test]
382 fn test_bounding_box_line() {
383 let ops = vec![
384 PathOp::MoveTo { x: 10.0, y: 20.0 },
385 PathOp::LineTo { x: 50.0, y: 80.0 },
386 ];
387 let bb = bounding_box(&ops).unwrap();
388 assert_eq!(bb.left, 10.0);
389 assert_eq!(bb.bottom, 20.0);
390 assert_eq!(bb.right, 50.0);
391 assert_eq!(bb.top, 80.0);
392 }
393
394 #[test]
395 fn test_bounding_box_rect_path() {
396 let ops = vec![
397 PathOp::MoveTo { x: 0.0, y: 0.0 },
398 PathOp::LineTo { x: 100.0, y: 0.0 },
399 PathOp::LineTo { x: 100.0, y: 200.0 },
400 PathOp::LineTo { x: 0.0, y: 200.0 },
401 PathOp::Close,
402 ];
403 let bb = bounding_box(&ops).unwrap();
404 assert_eq!(bb.left, 0.0);
405 assert_eq!(bb.bottom, 0.0);
406 assert_eq!(bb.right, 100.0);
407 assert_eq!(bb.top, 200.0);
408 }
409
410 #[test]
411 fn test_bounding_box_curve_includes_control_points() {
412 let ops = vec![PathOp::CurveTo {
413 x1: -5.0,
414 y1: 30.0,
415 x2: 60.0,
416 y2: -10.0,
417 x3: 40.0,
418 y3: 50.0,
419 }];
420 let bb = bounding_box(&ops).unwrap();
421 assert_eq!(bb.left as f32, -5.0_f32);
422 assert_eq!(bb.bottom as f32, -10.0_f32);
423 assert_eq!(bb.right as f32, 60.0_f32);
424 assert_eq!(bb.top as f32, 50.0_f32);
425 }
426
427 #[test]
428 fn test_bounding_box_negative_coords() {
429 let ops = vec![
430 PathOp::MoveTo {
431 x: -100.0,
432 y: -200.0,
433 },
434 PathOp::LineTo { x: -10.0, y: -5.0 },
435 ];
436 let bb = bounding_box(&ops).unwrap();
437 assert_eq!(bb.left as f32, -100.0_f32);
438 assert_eq!(bb.bottom as f32, -200.0_f32);
439 assert_eq!(bb.right as f32, -10.0_f32);
440 assert_eq!(bb.top as f32, -5.0_f32);
441 }
442
443 #[test]
444 fn test_stroke_bounding_box_inflates_by_half_line_width() {
445 let ops = vec![
446 PathOp::MoveTo { x: 10.0, y: 10.0 },
447 PathOp::LineTo { x: 50.0, y: 10.0 },
448 ];
449 let bb = bounding_box_for_stroke_path(&ops, 4.0, 10.0).unwrap();
451 assert!((bb.left - (10.0 - 20.0)).abs() < 0.01);
452 assert!((bb.right - (50.0 + 20.0)).abs() < 0.01);
453 assert!((bb.bottom - (10.0 - 20.0)).abs() < 0.01);
454 assert!((bb.top - (10.0 + 20.0)).abs() < 0.01);
455 }
456
457 #[test]
458 fn test_stroke_bounding_box_min_inflate_is_half_lw() {
459 let ops = vec![
461 PathOp::MoveTo { x: 0.0, y: 0.0 },
462 PathOp::LineTo { x: 10.0, y: 0.0 },
463 ];
464 let bb = bounding_box_for_stroke_path(&ops, 2.0, 0.5).unwrap();
465 assert!((bb.left - (-1.0)).abs() < 0.01);
466 assert!((bb.right - 11.0).abs() < 0.01);
467 }
468
469 #[test]
470 fn test_stroke_bounding_box_empty_path() {
471 assert_eq!(bounding_box_for_stroke_path(&[], 1.0, 10.0), None);
472 }
473
474 #[test]
475 fn test_get_rect_detects_axis_aligned_rect() {
476 let ops = vec![
477 PathOp::MoveTo { x: 10.0, y: 20.0 },
478 PathOp::LineTo { x: 50.0, y: 20.0 },
479 PathOp::LineTo { x: 50.0, y: 80.0 },
480 PathOp::LineTo { x: 10.0, y: 80.0 },
481 PathOp::LineTo { x: 10.0, y: 20.0 },
482 PathOp::Close,
483 ];
484 let r = rect_if_axis_aligned(&ops).unwrap();
485 assert!((r.left - 10.0).abs() < 0.01);
486 assert!((r.bottom - 20.0).abs() < 0.01);
487 assert!((r.right - 50.0).abs() < 0.01);
488 assert!((r.top - 80.0).abs() < 0.01);
489 }
490
491 #[test]
492 fn test_get_rect_rejects_non_rect_path() {
493 let ops = vec![
495 PathOp::MoveTo { x: 0.0, y: 0.0 },
496 PathOp::LineTo { x: 10.0, y: 5.0 },
497 PathOp::LineTo { x: 20.0, y: 0.0 },
498 PathOp::LineTo { x: 10.0, y: -5.0 },
499 PathOp::LineTo { x: 0.0, y: 0.0 },
500 ];
501 assert_eq!(rect_if_axis_aligned(&ops), None);
502 }
503
504 #[test]
505 fn test_get_rect_rejects_triangle() {
506 let ops = vec![
507 PathOp::MoveTo { x: 0.0, y: 0.0 },
508 PathOp::LineTo { x: 10.0, y: 0.0 },
509 PathOp::LineTo { x: 5.0, y: 10.0 },
510 PathOp::Close,
511 ];
512 assert_eq!(rect_if_axis_aligned(&ops), None);
513 }
514
515 #[test]
519 fn test_basic_path_operations() {
520 let ops = vec![
522 PathOp::MoveTo { x: 1.0, y: 2.0 },
523 PathOp::LineTo { x: 3.0, y: 2.0 },
524 PathOp::LineTo { x: 3.0, y: 5.0 },
525 PathOp::LineTo { x: 1.0, y: 5.0 },
526 PathOp::LineTo { x: 1.0, y: 2.0 },
527 ];
528 assert!(is_rect(&ops));
529 let rect = rect_if_axis_aligned(&ops).unwrap();
530 assert_eq!(
531 (
532 rect.left as f32,
533 rect.bottom as f32,
534 rect.right as f32,
535 rect.top as f32
536 ),
537 (1.0, 2.0, 3.0, 5.0)
538 );
539 let bb = bounding_box(&ops).unwrap();
540 assert_eq!(
541 (
542 bb.left as f32,
543 bb.bottom as f32,
544 bb.right as f32,
545 bb.top as f32
546 ),
547 (1.0, 2.0, 3.0, 5.0)
548 );
549
550 let empty: Vec<PathOp> = vec![];
552 assert!(!is_rect(&empty));
553 assert_eq!(bounding_box(&empty), None);
554
555 let ops = vec![
557 PathOp::MoveTo { x: 0.0, y: 0.0 },
558 PathOp::LineTo { x: 0.0, y: 1.0 },
559 PathOp::LineTo { x: 1.0, y: 1.0 },
560 PathOp::LineTo { x: 1.0, y: 0.0 },
561 PathOp::LineTo { x: 0.0, y: 0.0 },
562 ];
563 assert!(is_rect(&ops));
564 let rect = rect_if_axis_aligned(&ops).unwrap();
565 assert_eq!(
566 (
567 rect.left as f32,
568 rect.bottom as f32,
569 rect.right as f32,
570 rect.top as f32
571 ),
572 (0.0, 0.0, 1.0, 1.0)
573 );
574
575 let ops = vec![
577 PathOp::MoveTo { x: 0.0, y: 0.0 },
578 PathOp::LineTo { x: 0.0, y: 1.0 },
579 PathOp::LineTo { x: 1.0, y: 1.0 },
580 PathOp::LineTo { x: 1.0, y: 0.0 },
581 PathOp::LineTo { x: 0.0, y: 0.0 },
582 PathOp::Close,
583 ];
584 assert!(is_rect(&ops));
585 }
586
587 #[test]
593 fn test_shear_transform_breaks_rect() {
594 let ops = vec![
596 PathOp::MoveTo { x: 1.0, y: 2.0 },
597 PathOp::LineTo { x: 3.0, y: 2.0 },
598 PathOp::LineTo { x: 3.0, y: 5.0 },
599 PathOp::LineTo { x: 1.0, y: 5.0 },
600 PathOp::LineTo { x: 1.0, y: 2.0 },
601 ];
602 assert!(is_rect(&ops));
603
604 let sheared: Vec<PathOp> = ops
606 .iter()
607 .map(|op| match op {
608 PathOp::MoveTo { x, y } => PathOp::MoveTo {
609 x: *x,
610 y: 2.0 * x + y,
611 },
612 PathOp::LineTo { x, y } => PathOp::LineTo {
613 x: *x,
614 y: 2.0 * x + y,
615 },
616 _ => op.clone(),
617 })
618 .collect();
619
620 assert!(!is_rect(&sheared));
622 assert!(rect_if_axis_aligned(&sheared).is_none());
623
624 let bb = bounding_box(&sheared).unwrap();
626 assert_eq!(
627 (
628 bb.left as f32,
629 bb.bottom as f32,
630 bb.right as f32,
631 bb.top as f32
632 ),
633 (1.0, 4.0, 3.0, 11.0)
634 );
635 }
636
637 #[test]
639 fn test_hexagon_not_rect() {
640 let ops = vec![
641 PathOp::MoveTo { x: 1.0, y: 0.0 },
642 PathOp::LineTo { x: 2.0, y: 0.0 },
643 PathOp::LineTo { x: 3.0, y: 1.0 },
644 PathOp::LineTo { x: 2.0, y: 2.0 },
645 PathOp::LineTo { x: 1.0, y: 2.0 },
646 PathOp::LineTo { x: 0.0, y: 1.0 },
647 ];
648 assert!(!is_rect(&ops));
649 assert!(rect_if_axis_aligned(&ops).is_none());
650 let bb = bounding_box(&ops).unwrap();
651 assert_eq!(
652 (
653 bb.left as f32,
654 bb.bottom as f32,
655 bb.right as f32,
656 bb.top as f32
657 ),
658 (0.0, 0.0, 3.0, 2.0)
659 );
660
661 let mut ops_closed = ops.clone();
663 ops_closed.push(PathOp::Close);
664 assert!(!is_rect(&ops_closed));
665
666 let bb2 = bounding_box(&ops_closed).unwrap();
668 assert_eq!((bb2.left as f32, bb2.bottom as f32), (0.0, 0.0));
669 assert_eq!((bb2.right as f32, bb2.top as f32), (3.0, 2.0));
670
671 let ops_loop = vec![
673 PathOp::MoveTo { x: 1.0, y: 0.0 },
674 PathOp::LineTo { x: 2.0, y: 0.0 },
675 PathOp::LineTo { x: 3.0, y: 1.0 },
676 PathOp::LineTo { x: 2.0, y: 2.0 },
677 PathOp::LineTo { x: 1.0, y: 2.0 },
678 PathOp::LineTo { x: 0.0, y: 1.0 },
679 PathOp::LineTo { x: 1.0, y: 0.0 },
680 ];
681 assert!(!is_rect(&ops_loop));
682 }
683
684 #[test]
689 fn test_path_append() {
690 let mut path = vec![PathOp::MoveTo { x: 5.0, y: 6.0 }];
691 assert_eq!(path.len(), 1);
692
693 let empty: Vec<PathOp> = vec![];
695 path.extend(empty.iter().cloned());
696 assert_eq!(path.len(), 1);
697
698 let snapshot = path.clone();
700 path.extend(snapshot);
701 assert_eq!(path.len(), 2);
702 assert_eq!(path[0], PathOp::MoveTo { x: 5.0, y: 6.0 });
703 assert_eq!(path[1], PathOp::MoveTo { x: 5.0, y: 6.0 });
704
705 let transformed: Vec<PathOp> = path
707 .iter()
708 .map(|op| match op {
709 PathOp::MoveTo { x, y } => PathOp::MoveTo {
710 x: 1.0 * x + 60.0,
711 y: 2.0 * y + 70.0,
712 },
713 PathOp::LineTo { x, y } => PathOp::LineTo {
714 x: 1.0 * x + 60.0,
715 y: 2.0 * y + 70.0,
716 },
717 other => other.clone(),
718 })
719 .collect();
720 path.extend(transformed);
721 assert_eq!(path.len(), 4);
722 assert_eq!(path[2], PathOp::MoveTo { x: 65.0, y: 82.0 });
723 assert_eq!(path[3], PathOp::MoveTo { x: 65.0, y: 82.0 });
724 }
725}