1use crate::action::Action;
11use crate::annotation::{Annotation, AnnotationType};
12use crate::destination::Destination;
13
14#[derive(Debug, Clone)]
22pub struct LinkObject {
23 pub rect: [f32; 4],
25 pub quad_points: Option<Vec<f32>>,
27 pub destination: Option<Destination>,
29 pub action: Option<Action>,
31 pub annotation_index: usize,
33}
34
35impl LinkObject {
36 fn from_annotation(annot: &Annotation, index: usize) -> Self {
38 Self {
39 rect: annot.rect,
40 quad_points: annot.subtype_data.quad_points.clone(),
41 destination: annot.destination.clone(),
42 action: annot.action.clone(),
43 annotation_index: index,
44 }
45 }
46
47 pub fn quad_point_count(&self) -> usize {
53 self.quad_points.as_ref().map_or(0, |v| v.len() / 8)
54 }
55
56 #[inline]
60 pub fn link_count_quad_points(&self) -> usize {
61 self.quad_point_count()
62 }
63
64 #[deprecated(
67 since = "0.1.0",
68 note = "use link_count_quad_points() or quad_point_count() instead"
69 )]
70 #[inline]
71 pub fn count_quad_points(&self) -> usize {
72 self.quad_point_count()
73 }
74
75 pub fn quad_points_at(&self, index: usize) -> Option<[f32; 8]> {
80 let flat = self.quad_points.as_ref()?;
81 let start = index * 8;
82 if start + 8 > flat.len() {
83 return None;
84 }
85 let s = &flat[start..start + 8];
86 Some([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]])
87 }
88
89 #[inline]
93 pub fn link_get_quad_points(&self, index: usize) -> Option<[f32; 8]> {
94 self.quad_points_at(index)
95 }
96
97 #[deprecated(
100 since = "0.1.0",
101 note = "use link_get_quad_points() or quad_points_at() instead"
102 )]
103 #[inline]
104 pub fn get_quad_points(&self, index: usize) -> Option<[f32; 8]> {
105 self.quad_points_at(index)
106 }
107
108 #[deprecated(
111 since = "0.1.0",
112 note = "use link_get_quad_points() or quad_points_at() instead"
113 )]
114 #[inline]
115 pub fn get_quad_points_at(&self, index: usize) -> Option<[f32; 8]> {
116 self.quad_points_at(index)
117 }
118
119 pub fn dest(&self) -> Option<&Destination> {
123 self.destination.as_ref()
124 }
125
126 #[inline]
130 pub fn link_get_dest(&self) -> Option<&Destination> {
131 self.dest()
132 }
133
134 #[deprecated(since = "0.1.0", note = "use link_get_dest() or dest() instead")]
137 #[inline]
138 pub fn get_dest(&self) -> Option<&Destination> {
139 self.dest()
140 }
141
142 pub fn action(&self) -> Option<&Action> {
146 self.action.as_ref()
147 }
148
149 #[inline]
153 pub fn link_get_action(&self) -> Option<&Action> {
154 self.action()
155 }
156
157 #[deprecated(since = "0.1.0", note = "use link_get_action() or action() instead")]
160 #[inline]
161 pub fn get_action(&self) -> Option<&Action> {
162 self.action()
163 }
164
165 pub fn rect(&self) -> [f32; 4] {
169 self.rect
170 }
171
172 #[inline]
176 pub fn link_get_annot_rect(&self) -> [f32; 4] {
177 self.rect()
178 }
179
180 #[deprecated(since = "0.1.0", note = "use link_get_annot_rect() or rect() instead")]
183 #[inline]
184 pub fn get_annot_rect(&self) -> [f32; 4] {
185 self.rect()
186 }
187
188 pub fn annotation_index(&self) -> usize {
197 self.annotation_index
198 }
199
200 #[inline]
204 pub fn link_get_annot(&self) -> usize {
205 self.annotation_index()
206 }
207
208 #[deprecated(
211 since = "0.1.0",
212 note = "use link_get_annot() or annotation_index() instead"
213 )]
214 #[inline]
215 pub fn get_annot(&self) -> usize {
216 self.annotation_index()
217 }
218
219 #[deprecated(
222 since = "0.1.0",
223 note = "use link_get_annot() or annotation_index() instead"
224 )]
225 #[inline]
226 pub fn get_annotation_index(&self) -> usize {
227 self.annotation_index()
228 }
229}
230
231pub fn collect_links(annotations: &[Annotation]) -> Vec<LinkObject> {
237 annotations
238 .iter()
239 .enumerate()
240 .filter(|(_, a)| a.subtype == AnnotationType::Link)
241 .map(|(idx, a)| LinkObject::from_annotation(a, idx))
242 .collect()
243}
244
245#[inline]
249pub fn link_enumerate(annotations: &[Annotation]) -> Vec<LinkObject> {
250 collect_links(annotations)
251}
252
253#[deprecated(
257 since = "0.1.0",
258 note = "use link_enumerate() or collect_links() instead"
259)]
260#[inline]
261pub fn enumerate(annotations: &[Annotation]) -> Vec<LinkObject> {
262 collect_links(annotations)
263}
264
265#[deprecated(
269 since = "0.1.0",
270 note = "use link_enumerate() or collect_links() instead"
271)]
272#[inline]
273pub fn enumerate_links(annotations: &[Annotation]) -> Vec<LinkObject> {
274 collect_links(annotations)
275}
276
277pub fn link_at_point(annotations: &[Annotation], x: f32, y: f32) -> Option<LinkObject> {
281 for (idx, annot) in annotations.iter().enumerate() {
282 if annot.subtype != AnnotationType::Link {
283 continue;
284 }
285
286 let hit = if let Some(ref qp) = annot.subtype_data.quad_points {
287 point_in_quad_points(qp, x, y)
288 } else {
289 point_in_rect(&annot.rect, x, y)
290 };
291
292 if hit {
293 return Some(LinkObject::from_annotation(annot, idx));
294 }
295 }
296 None
297}
298
299#[inline]
303pub fn link_get_link_at_point(annotations: &[Annotation], x: f32, y: f32) -> Option<LinkObject> {
304 link_at_point(annotations, x, y)
305}
306
307#[deprecated(
311 since = "0.1.0",
312 note = "use link_get_link_at_point() or link_at_point() instead"
313)]
314#[inline]
315pub fn get_link_at_point(annotations: &[Annotation], x: f32, y: f32) -> Option<LinkObject> {
316 link_at_point(annotations, x, y)
317}
318
319#[derive(Debug, Clone)]
321pub struct HitTestResult {
322 pub annotation_index: usize,
324 pub action: Option<Action>,
326 pub destination: Option<Destination>,
328 pub uri: Option<String>,
330}
331
332pub fn find_link_at_position(annotations: &[Annotation], x: f32, y: f32) -> Option<HitTestResult> {
340 for (idx, annot) in annotations.iter().enumerate() {
341 if annot.subtype != AnnotationType::Link {
342 continue;
343 }
344
345 let hit = if let Some(ref quad_points) = annot.subtype_data.quad_points {
346 point_in_quad_points(quad_points, x, y)
347 } else {
348 point_in_rect(&annot.rect, x, y)
349 };
350
351 if hit {
352 let uri = annot.action.as_ref().and_then(|a| {
353 if let Action::Uri(u) = a {
354 Some(u.clone())
355 } else {
356 None
357 }
358 });
359
360 return Some(HitTestResult {
361 annotation_index: idx,
362 action: annot.action.clone(),
363 destination: annot.destination.clone(),
364 uri,
365 });
366 }
367 }
368 None
369}
370
371fn point_in_rect(rect: &[f32; 4], x: f32, y: f32) -> bool {
373 let (x_min, x_max) = if rect[0] <= rect[2] {
374 (rect[0], rect[2])
375 } else {
376 (rect[2], rect[0])
377 };
378 let (y_min, y_max) = if rect[1] <= rect[3] {
379 (rect[1], rect[3])
380 } else {
381 (rect[3], rect[1])
382 };
383 x >= x_min && x <= x_max && y >= y_min && y <= y_max
384}
385
386fn point_in_quad_points(quad_points: &[f32], x: f32, y: f32) -> bool {
391 let mut offset = 0;
393 while offset + 7 < quad_points.len() {
394 let p0 = (quad_points[offset], quad_points[offset + 1]);
395 let p1 = (quad_points[offset + 2], quad_points[offset + 3]);
396 let p2 = (quad_points[offset + 4], quad_points[offset + 5]);
397 let p3 = (quad_points[offset + 6], quad_points[offset + 7]);
398
399 if point_in_quad(p0, p1, p2, p3, x, y) {
400 return true;
401 }
402 offset += 8;
403 }
404 false
405}
406
407fn point_in_quad(
412 p0: (f32, f32),
413 p1: (f32, f32),
414 p2: (f32, f32),
415 p3: (f32, f32),
416 x: f32,
417 y: f32,
418) -> bool {
419 let edges = [(p0, p1), (p1, p2), (p2, p3), (p3, p0)];
420
421 let mut pos = 0i32;
422 let mut neg = 0i32;
423
424 for &(a, b) in &edges {
425 let cross = (b.0 - a.0) * (y - a.1) - (b.1 - a.1) * (x - a.0);
426 if cross > 0.0 {
427 pos += 1;
428 } else if cross < 0.0 {
429 neg += 1;
430 }
431 }
432
433 pos == 0 || neg == 0
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use crate::annotation::{AnnotationFlags, AnnotationSubtypeData};
441
442 fn make_link(
443 rect: [f32; 4],
444 action: Option<Action>,
445 quad_points: Option<Vec<f32>>,
446 ) -> Annotation {
447 Annotation {
448 subtype: AnnotationType::Link,
449 rect,
450 contents: None,
451 flags: AnnotationFlags::from_bits(0),
452 name: None,
453 appearance: None,
454 color: None,
455 border: None,
456 action,
457 destination: None,
458 subtype_data: AnnotationSubtypeData {
459 quad_points,
460 ..Default::default()
461 },
462 mk: None,
463 file_spec: None,
464 parent_ref: None,
465 object_id: None,
466 open: None,
467 ap_n_bytes: None,
468 ap_r_bytes: None,
469 ap_d_bytes: None,
470 irt_ref: None,
471 field_name: None,
472 alternate_name: None,
473 field_value: None,
474 form_field_flags: None,
475 additional_actions: None,
476 form_field_type: None,
477 options: None,
478 }
479 }
480
481 fn make_text(rect: [f32; 4]) -> Annotation {
482 Annotation {
483 subtype: AnnotationType::Text,
484 rect,
485 contents: None,
486 flags: AnnotationFlags::from_bits(0),
487 name: None,
488 appearance: None,
489 color: None,
490 border: None,
491 action: None,
492 destination: None,
493 subtype_data: AnnotationSubtypeData::default(),
494 mk: None,
495 file_spec: None,
496 parent_ref: None,
497 object_id: None,
498 open: None,
499 ap_n_bytes: None,
500 ap_r_bytes: None,
501 ap_d_bytes: None,
502 irt_ref: None,
503 field_name: None,
504 alternate_name: None,
505 field_value: None,
506 form_field_flags: None,
507 additional_actions: None,
508 form_field_type: None,
509 options: None,
510 }
511 }
512
513 #[test]
514 fn test_hit_test_rect_inside() {
515 let annotations = vec![make_link(
516 [10.0, 10.0, 100.0, 30.0],
517 Some(Action::Uri("https://example.com".into())),
518 None,
519 )];
520
521 let result = find_link_at_position(&annotations, 50.0, 20.0).unwrap();
522 assert_eq!(result.annotation_index, 0);
523 assert_eq!(result.uri.as_deref(), Some("https://example.com"));
524 }
525
526 #[test]
527 fn test_hit_test_rect_outside() {
528 let annotations = vec![make_link(
529 [10.0, 10.0, 100.0, 30.0],
530 Some(Action::Uri("https://example.com".into())),
531 None,
532 )];
533
534 assert!(find_link_at_position(&annotations, 5.0, 5.0).is_none());
535 }
536
537 #[test]
538 fn test_hit_test_skips_non_link() {
539 let annotations = vec![
540 make_text([0.0, 0.0, 200.0, 200.0]),
541 make_link(
542 [10.0, 10.0, 100.0, 30.0],
543 Some(Action::Uri("https://example.com".into())),
544 None,
545 ),
546 ];
547
548 let result = find_link_at_position(&annotations, 50.0, 20.0).unwrap();
549 assert_eq!(result.annotation_index, 1);
550 }
551
552 #[test]
553 fn test_hit_test_quad_points() {
554 let quad_points = vec![0.0, 0.0, 100.0, 0.0, 100.0, 20.0, 0.0, 20.0];
556 let annotations = vec![make_link(
557 [0.0, 0.0, 100.0, 20.0],
558 Some(Action::Uri("https://example.com".into())),
559 Some(quad_points),
560 )];
561
562 assert!(find_link_at_position(&annotations, 50.0, 10.0).is_some());
563 assert!(find_link_at_position(&annotations, 150.0, 10.0).is_none());
564 }
565
566 #[test]
567 fn test_hit_test_no_links_returns_none() {
568 let annotations: Vec<Annotation> = Vec::new();
569 assert!(find_link_at_position(&annotations, 50.0, 20.0).is_none());
570 }
571
572 #[test]
573 fn test_hit_test_returns_first_match() {
574 let annotations = vec![
575 make_link(
576 [0.0, 0.0, 100.0, 100.0],
577 Some(Action::Uri("https://first.com".into())),
578 None,
579 ),
580 make_link(
581 [0.0, 0.0, 200.0, 200.0],
582 Some(Action::Uri("https://second.com".into())),
583 None,
584 ),
585 ];
586
587 let result = find_link_at_position(&annotations, 50.0, 50.0).unwrap();
588 assert_eq!(result.annotation_index, 0);
589 assert_eq!(result.uri.as_deref(), Some("https://first.com"));
590 }
591
592 #[test]
597 fn test_link_at_point_returns_link_object() {
598 let annotations = vec![make_link(
599 [10.0, 10.0, 100.0, 30.0],
600 Some(Action::Uri("https://example.com".into())),
601 None,
602 )];
603
604 let link = link_at_point(&annotations, 50.0, 20.0).unwrap();
605 assert_eq!(link.annotation_index, 0);
606 assert_eq!(link.rect(), [10.0, 10.0, 100.0, 30.0]);
607 match link.action() {
608 Some(Action::Uri(u)) => assert_eq!(u, "https://example.com"),
609 _ => panic!("expected URI action"),
610 }
611 assert!(link.dest().is_none());
612 }
613
614 #[test]
615 fn test_link_at_point_miss_returns_none() {
616 let annotations = vec![make_link(
617 [10.0, 10.0, 100.0, 30.0],
618 Some(Action::Uri("https://example.com".into())),
619 None,
620 )];
621
622 assert!(link_at_point(&annotations, 200.0, 200.0).is_none());
623 }
624
625 #[test]
626 fn test_link_at_point_with_quad_points() {
627 let qp = vec![0.0, 0.0, 100.0, 0.0, 100.0, 20.0, 0.0, 20.0];
628 let annotations = vec![make_link(
629 [0.0, 0.0, 100.0, 20.0],
630 Some(Action::Uri("https://example.com".into())),
631 Some(qp),
632 )];
633
634 let link = link_at_point(&annotations, 50.0, 10.0).unwrap();
635 assert_eq!(link.quad_point_count(), 1);
636 assert_eq!(link.link_count_quad_points(), 1);
637 let qp = link.quad_points_at(0).unwrap();
638 assert_eq!(qp[0], 0.0);
639 assert_eq!(qp[2], 100.0);
640 assert!(link.quad_points_at(1).is_none());
641 }
642
643 #[test]
644 fn test_link_object_quad_points_multiple_groups() {
645 let qp = vec![
646 0.0, 0.0, 10.0, 0.0, 10.0, 10.0, 0.0, 10.0, 20.0, 20.0, 30.0, 20.0, 30.0, 30.0, 20.0, 30.0, ];
649 let link = LinkObject {
650 rect: [0.0, 0.0, 30.0, 30.0],
651 quad_points: Some(qp),
652 destination: None,
653 action: None,
654 annotation_index: 0,
655 };
656 assert_eq!(link.quad_point_count(), 2);
657 let g0 = link.link_get_quad_points(0).unwrap();
658 assert_eq!(g0[0], 0.0);
659 let g1 = link.link_get_quad_points(1).unwrap();
660 assert_eq!(g1[0], 20.0);
661 assert!(link.link_get_quad_points(2).is_none());
662 }
663
664 #[test]
665 fn test_link_object_no_quad_points() {
666 let link = LinkObject {
667 rect: [0.0, 0.0, 100.0, 50.0],
668 quad_points: None,
669 destination: None,
670 action: None,
671 annotation_index: 0,
672 };
673 assert_eq!(link.quad_point_count(), 0);
674 assert!(link.quad_points_at(0).is_none());
675 }
676
677 #[test]
678 fn test_collect_links_filters_link_annotations() {
679 let annotations = vec![
680 make_text([0.0, 0.0, 100.0, 100.0]),
681 make_link(
682 [10.0, 10.0, 50.0, 20.0],
683 Some(Action::Uri("https://a.com".into())),
684 None,
685 ),
686 make_text([200.0, 200.0, 300.0, 300.0]),
687 make_link(
688 [60.0, 60.0, 90.0, 80.0],
689 Some(Action::Uri("https://b.com".into())),
690 None,
691 ),
692 ];
693
694 let links = collect_links(&annotations);
695 assert_eq!(links.len(), 2);
696 assert_eq!(links[0].annotation_index, 1);
697 assert_eq!(links[1].annotation_index, 3);
698 }
699
700 #[test]
701 fn test_link_get_link_at_point_alias_works() {
702 let annotations = vec![make_link(
703 [0.0, 0.0, 100.0, 100.0],
704 Some(Action::Uri("https://example.com".into())),
705 None,
706 )];
707
708 let link = link_get_link_at_point(&annotations, 50.0, 50.0).unwrap();
709 assert_eq!(link.annotation_index, 0);
710 }
711
712 #[test]
713 fn test_link_object_dest_accessor() {
714 use crate::destination::{Destination, PageFit};
715 let link = LinkObject {
716 rect: [0.0, 0.0, 100.0, 50.0],
717 quad_points: None,
718 destination: Some(Destination::Page {
719 page_index: 3,
720 page_ref: None,
721 fit: PageFit::Fit,
722 }),
723 action: None,
724 annotation_index: 0,
725 };
726 assert!(link.dest().is_some());
727 assert!(link.link_get_dest().is_some());
728 assert!(link.action().is_none());
729 assert!(link.link_get_action().is_none());
730 }
731
732 #[test]
737 fn test_annotation_index_returns_correct_index() {
738 let annotations = vec![
740 make_text([0.0, 0.0, 100.0, 100.0]),
741 make_link(
742 [10.0, 10.0, 90.0, 30.0],
743 Some(Action::Uri("https://example.com".into())),
744 None,
745 ),
746 ];
747
748 let links = collect_links(&annotations);
749 assert_eq!(links.len(), 1);
750
751 let link = &links[0];
752 assert_eq!(link.annotation_index(), 1);
754 assert_eq!(link.link_get_annot(), 1);
755 }
756
757 #[test]
758 fn test_annotation_index_matches_link_at_point() {
759 let annotations = vec![
761 make_link(
762 [0.0, 0.0, 50.0, 50.0],
763 Some(Action::Uri("https://first.com".into())),
764 None,
765 ),
766 make_link(
767 [100.0, 100.0, 200.0, 150.0],
768 Some(Action::Uri("https://second.com".into())),
769 None,
770 ),
771 ];
772
773 let link0 = link_at_point(&annotations, 25.0, 25.0).unwrap();
774 let link1 = link_at_point(&annotations, 150.0, 125.0).unwrap();
775
776 assert_eq!(link0.annotation_index(), 0);
777 assert_eq!(link0.link_get_annot(), 0);
778 assert_eq!(link1.annotation_index(), 1);
779 assert_eq!(link1.link_get_annot(), 1);
780 }
781}