use crate::action::Action;
use crate::annotation::{Annotation, AnnotationType};
use crate::destination::Destination;
#[derive(Debug, Clone)]
pub struct LinkObject {
pub rect: [f32; 4],
pub quad_points: Option<Vec<f32>>,
pub destination: Option<Destination>,
pub action: Option<Action>,
pub annotation_index: usize,
}
impl LinkObject {
fn from_annotation(annot: &Annotation, index: usize) -> Self {
Self {
rect: annot.rect,
quad_points: annot.subtype_data.quad_points.clone(),
destination: annot.destination.clone(),
action: annot.action.clone(),
annotation_index: index,
}
}
pub fn quad_point_count(&self) -> usize {
self.quad_points.as_ref().map_or(0, |v| v.len() / 8)
}
#[inline]
pub fn link_count_quad_points(&self) -> usize {
self.quad_point_count()
}
#[deprecated(
since = "0.1.0",
note = "use link_count_quad_points() or quad_point_count() instead"
)]
#[inline]
pub fn count_quad_points(&self) -> usize {
self.quad_point_count()
}
pub fn quad_points_at(&self, index: usize) -> Option<[f32; 8]> {
let flat = self.quad_points.as_ref()?;
let start = index * 8;
if start + 8 > flat.len() {
return None;
}
let s = &flat[start..start + 8];
Some([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]])
}
#[inline]
pub fn link_get_quad_points(&self, index: usize) -> Option<[f32; 8]> {
self.quad_points_at(index)
}
#[deprecated(
since = "0.1.0",
note = "use link_get_quad_points() or quad_points_at() instead"
)]
#[inline]
pub fn get_quad_points(&self, index: usize) -> Option<[f32; 8]> {
self.quad_points_at(index)
}
#[deprecated(
since = "0.1.0",
note = "use link_get_quad_points() or quad_points_at() instead"
)]
#[inline]
pub fn get_quad_points_at(&self, index: usize) -> Option<[f32; 8]> {
self.quad_points_at(index)
}
pub fn dest(&self) -> Option<&Destination> {
self.destination.as_ref()
}
#[inline]
pub fn link_get_dest(&self) -> Option<&Destination> {
self.dest()
}
#[deprecated(since = "0.1.0", note = "use link_get_dest() or dest() instead")]
#[inline]
pub fn get_dest(&self) -> Option<&Destination> {
self.dest()
}
pub fn action(&self) -> Option<&Action> {
self.action.as_ref()
}
#[inline]
pub fn link_get_action(&self) -> Option<&Action> {
self.action()
}
#[deprecated(since = "0.1.0", note = "use link_get_action() or action() instead")]
#[inline]
pub fn get_action(&self) -> Option<&Action> {
self.action()
}
pub fn rect(&self) -> [f32; 4] {
self.rect
}
#[inline]
pub fn link_get_annot_rect(&self) -> [f32; 4] {
self.rect()
}
#[deprecated(since = "0.1.0", note = "use link_get_annot_rect() or rect() instead")]
#[inline]
pub fn get_annot_rect(&self) -> [f32; 4] {
self.rect()
}
pub fn annotation_index(&self) -> usize {
self.annotation_index
}
#[inline]
pub fn link_get_annot(&self) -> usize {
self.annotation_index()
}
#[deprecated(
since = "0.1.0",
note = "use link_get_annot() or annotation_index() instead"
)]
#[inline]
pub fn get_annot(&self) -> usize {
self.annotation_index()
}
#[deprecated(
since = "0.1.0",
note = "use link_get_annot() or annotation_index() instead"
)]
#[inline]
pub fn get_annotation_index(&self) -> usize {
self.annotation_index()
}
}
pub fn collect_links(annotations: &[Annotation]) -> Vec<LinkObject> {
annotations
.iter()
.enumerate()
.filter(|(_, a)| a.subtype == AnnotationType::Link)
.map(|(idx, a)| LinkObject::from_annotation(a, idx))
.collect()
}
#[inline]
pub fn link_enumerate(annotations: &[Annotation]) -> Vec<LinkObject> {
collect_links(annotations)
}
#[deprecated(
since = "0.1.0",
note = "use link_enumerate() or collect_links() instead"
)]
#[inline]
pub fn enumerate(annotations: &[Annotation]) -> Vec<LinkObject> {
collect_links(annotations)
}
#[deprecated(
since = "0.1.0",
note = "use link_enumerate() or collect_links() instead"
)]
#[inline]
pub fn enumerate_links(annotations: &[Annotation]) -> Vec<LinkObject> {
collect_links(annotations)
}
pub fn link_at_point(annotations: &[Annotation], x: f32, y: f32) -> Option<LinkObject> {
for (idx, annot) in annotations.iter().enumerate() {
if annot.subtype != AnnotationType::Link {
continue;
}
let hit = if let Some(ref qp) = annot.subtype_data.quad_points {
point_in_quad_points(qp, x, y)
} else {
point_in_rect(&annot.rect, x, y)
};
if hit {
return Some(LinkObject::from_annotation(annot, idx));
}
}
None
}
#[inline]
pub fn link_get_link_at_point(annotations: &[Annotation], x: f32, y: f32) -> Option<LinkObject> {
link_at_point(annotations, x, y)
}
#[deprecated(
since = "0.1.0",
note = "use link_get_link_at_point() or link_at_point() instead"
)]
#[inline]
pub fn get_link_at_point(annotations: &[Annotation], x: f32, y: f32) -> Option<LinkObject> {
link_at_point(annotations, x, y)
}
#[derive(Debug, Clone)]
pub struct HitTestResult {
pub annotation_index: usize,
pub action: Option<Action>,
pub destination: Option<Destination>,
pub uri: Option<String>,
}
pub fn find_link_at_position(annotations: &[Annotation], x: f32, y: f32) -> Option<HitTestResult> {
for (idx, annot) in annotations.iter().enumerate() {
if annot.subtype != AnnotationType::Link {
continue;
}
let hit = if let Some(ref quad_points) = annot.subtype_data.quad_points {
point_in_quad_points(quad_points, x, y)
} else {
point_in_rect(&annot.rect, x, y)
};
if hit {
let uri = annot.action.as_ref().and_then(|a| {
if let Action::Uri(u) = a {
Some(u.clone())
} else {
None
}
});
return Some(HitTestResult {
annotation_index: idx,
action: annot.action.clone(),
destination: annot.destination.clone(),
uri,
});
}
}
None
}
fn point_in_rect(rect: &[f32; 4], x: f32, y: f32) -> bool {
let (x_min, x_max) = if rect[0] <= rect[2] {
(rect[0], rect[2])
} else {
(rect[2], rect[0])
};
let (y_min, y_max) = if rect[1] <= rect[3] {
(rect[1], rect[3])
} else {
(rect[3], rect[1])
};
x >= x_min && x <= x_max && y >= y_min && y <= y_max
}
fn point_in_quad_points(quad_points: &[f32], x: f32, y: f32) -> bool {
let mut offset = 0;
while offset + 7 < quad_points.len() {
let p0 = (quad_points[offset], quad_points[offset + 1]);
let p1 = (quad_points[offset + 2], quad_points[offset + 3]);
let p2 = (quad_points[offset + 4], quad_points[offset + 5]);
let p3 = (quad_points[offset + 6], quad_points[offset + 7]);
if point_in_quad(p0, p1, p2, p3, x, y) {
return true;
}
offset += 8;
}
false
}
fn point_in_quad(
p0: (f32, f32),
p1: (f32, f32),
p2: (f32, f32),
p3: (f32, f32),
x: f32,
y: f32,
) -> bool {
let edges = [(p0, p1), (p1, p2), (p2, p3), (p3, p0)];
let mut pos = 0i32;
let mut neg = 0i32;
for &(a, b) in &edges {
let cross = (b.0 - a.0) * (y - a.1) - (b.1 - a.1) * (x - a.0);
if cross > 0.0 {
pos += 1;
} else if cross < 0.0 {
neg += 1;
}
}
pos == 0 || neg == 0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::annotation::{AnnotationFlags, AnnotationSubtypeData};
fn make_link(
rect: [f32; 4],
action: Option<Action>,
quad_points: Option<Vec<f32>>,
) -> Annotation {
Annotation {
subtype: AnnotationType::Link,
rect,
contents: None,
flags: AnnotationFlags::from_bits(0),
name: None,
appearance: None,
color: None,
border: None,
action,
destination: None,
subtype_data: AnnotationSubtypeData {
quad_points,
..Default::default()
},
mk: None,
file_spec: None,
parent_ref: None,
object_id: None,
open: None,
ap_n_bytes: None,
ap_r_bytes: None,
ap_d_bytes: None,
irt_ref: None,
field_name: None,
alternate_name: None,
field_value: None,
form_field_flags: None,
additional_actions: None,
form_field_type: None,
options: None,
}
}
fn make_text(rect: [f32; 4]) -> Annotation {
Annotation {
subtype: AnnotationType::Text,
rect,
contents: None,
flags: AnnotationFlags::from_bits(0),
name: None,
appearance: None,
color: None,
border: None,
action: None,
destination: None,
subtype_data: AnnotationSubtypeData::default(),
mk: None,
file_spec: None,
parent_ref: None,
object_id: None,
open: None,
ap_n_bytes: None,
ap_r_bytes: None,
ap_d_bytes: None,
irt_ref: None,
field_name: None,
alternate_name: None,
field_value: None,
form_field_flags: None,
additional_actions: None,
form_field_type: None,
options: None,
}
}
#[test]
fn test_hit_test_rect_inside() {
let annotations = vec![make_link(
[10.0, 10.0, 100.0, 30.0],
Some(Action::Uri("https://example.com".into())),
None,
)];
let result = find_link_at_position(&annotations, 50.0, 20.0).unwrap();
assert_eq!(result.annotation_index, 0);
assert_eq!(result.uri.as_deref(), Some("https://example.com"));
}
#[test]
fn test_hit_test_rect_outside() {
let annotations = vec![make_link(
[10.0, 10.0, 100.0, 30.0],
Some(Action::Uri("https://example.com".into())),
None,
)];
assert!(find_link_at_position(&annotations, 5.0, 5.0).is_none());
}
#[test]
fn test_hit_test_skips_non_link() {
let annotations = vec![
make_text([0.0, 0.0, 200.0, 200.0]),
make_link(
[10.0, 10.0, 100.0, 30.0],
Some(Action::Uri("https://example.com".into())),
None,
),
];
let result = find_link_at_position(&annotations, 50.0, 20.0).unwrap();
assert_eq!(result.annotation_index, 1);
}
#[test]
fn test_hit_test_quad_points() {
let quad_points = vec![0.0, 0.0, 100.0, 0.0, 100.0, 20.0, 0.0, 20.0];
let annotations = vec![make_link(
[0.0, 0.0, 100.0, 20.0],
Some(Action::Uri("https://example.com".into())),
Some(quad_points),
)];
assert!(find_link_at_position(&annotations, 50.0, 10.0).is_some());
assert!(find_link_at_position(&annotations, 150.0, 10.0).is_none());
}
#[test]
fn test_hit_test_no_links_returns_none() {
let annotations: Vec<Annotation> = Vec::new();
assert!(find_link_at_position(&annotations, 50.0, 20.0).is_none());
}
#[test]
fn test_hit_test_returns_first_match() {
let annotations = vec![
make_link(
[0.0, 0.0, 100.0, 100.0],
Some(Action::Uri("https://first.com".into())),
None,
),
make_link(
[0.0, 0.0, 200.0, 200.0],
Some(Action::Uri("https://second.com".into())),
None,
),
];
let result = find_link_at_position(&annotations, 50.0, 50.0).unwrap();
assert_eq!(result.annotation_index, 0);
assert_eq!(result.uri.as_deref(), Some("https://first.com"));
}
#[test]
fn test_link_at_point_returns_link_object() {
let annotations = vec![make_link(
[10.0, 10.0, 100.0, 30.0],
Some(Action::Uri("https://example.com".into())),
None,
)];
let link = link_at_point(&annotations, 50.0, 20.0).unwrap();
assert_eq!(link.annotation_index, 0);
assert_eq!(link.rect(), [10.0, 10.0, 100.0, 30.0]);
match link.action() {
Some(Action::Uri(u)) => assert_eq!(u, "https://example.com"),
_ => panic!("expected URI action"),
}
assert!(link.dest().is_none());
}
#[test]
fn test_link_at_point_miss_returns_none() {
let annotations = vec![make_link(
[10.0, 10.0, 100.0, 30.0],
Some(Action::Uri("https://example.com".into())),
None,
)];
assert!(link_at_point(&annotations, 200.0, 200.0).is_none());
}
#[test]
fn test_link_at_point_with_quad_points() {
let qp = vec![0.0, 0.0, 100.0, 0.0, 100.0, 20.0, 0.0, 20.0];
let annotations = vec![make_link(
[0.0, 0.0, 100.0, 20.0],
Some(Action::Uri("https://example.com".into())),
Some(qp),
)];
let link = link_at_point(&annotations, 50.0, 10.0).unwrap();
assert_eq!(link.quad_point_count(), 1);
assert_eq!(link.link_count_quad_points(), 1);
let qp = link.quad_points_at(0).unwrap();
assert_eq!(qp[0], 0.0);
assert_eq!(qp[2], 100.0);
assert!(link.quad_points_at(1).is_none());
}
#[test]
fn test_link_object_quad_points_multiple_groups() {
let qp = vec![
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, ];
let link = LinkObject {
rect: [0.0, 0.0, 30.0, 30.0],
quad_points: Some(qp),
destination: None,
action: None,
annotation_index: 0,
};
assert_eq!(link.quad_point_count(), 2);
let g0 = link.link_get_quad_points(0).unwrap();
assert_eq!(g0[0], 0.0);
let g1 = link.link_get_quad_points(1).unwrap();
assert_eq!(g1[0], 20.0);
assert!(link.link_get_quad_points(2).is_none());
}
#[test]
fn test_link_object_no_quad_points() {
let link = LinkObject {
rect: [0.0, 0.0, 100.0, 50.0],
quad_points: None,
destination: None,
action: None,
annotation_index: 0,
};
assert_eq!(link.quad_point_count(), 0);
assert!(link.quad_points_at(0).is_none());
}
#[test]
fn test_collect_links_filters_link_annotations() {
let annotations = vec![
make_text([0.0, 0.0, 100.0, 100.0]),
make_link(
[10.0, 10.0, 50.0, 20.0],
Some(Action::Uri("https://a.com".into())),
None,
),
make_text([200.0, 200.0, 300.0, 300.0]),
make_link(
[60.0, 60.0, 90.0, 80.0],
Some(Action::Uri("https://b.com".into())),
None,
),
];
let links = collect_links(&annotations);
assert_eq!(links.len(), 2);
assert_eq!(links[0].annotation_index, 1);
assert_eq!(links[1].annotation_index, 3);
}
#[test]
fn test_link_get_link_at_point_alias_works() {
let annotations = vec![make_link(
[0.0, 0.0, 100.0, 100.0],
Some(Action::Uri("https://example.com".into())),
None,
)];
let link = link_get_link_at_point(&annotations, 50.0, 50.0).unwrap();
assert_eq!(link.annotation_index, 0);
}
#[test]
fn test_link_object_dest_accessor() {
use crate::destination::{Destination, PageFit};
let link = LinkObject {
rect: [0.0, 0.0, 100.0, 50.0],
quad_points: None,
destination: Some(Destination::Page {
page_index: 3,
page_ref: None,
fit: PageFit::Fit,
}),
action: None,
annotation_index: 0,
};
assert!(link.dest().is_some());
assert!(link.link_get_dest().is_some());
assert!(link.action().is_none());
assert!(link.link_get_action().is_none());
}
#[test]
fn test_annotation_index_returns_correct_index() {
let annotations = vec![
make_text([0.0, 0.0, 100.0, 100.0]),
make_link(
[10.0, 10.0, 90.0, 30.0],
Some(Action::Uri("https://example.com".into())),
None,
),
];
let links = collect_links(&annotations);
assert_eq!(links.len(), 1);
let link = &links[0];
assert_eq!(link.annotation_index(), 1);
assert_eq!(link.link_get_annot(), 1);
}
#[test]
fn test_annotation_index_matches_link_at_point() {
let annotations = vec![
make_link(
[0.0, 0.0, 50.0, 50.0],
Some(Action::Uri("https://first.com".into())),
None,
),
make_link(
[100.0, 100.0, 200.0, 150.0],
Some(Action::Uri("https://second.com".into())),
None,
),
];
let link0 = link_at_point(&annotations, 25.0, 25.0).unwrap();
let link1 = link_at_point(&annotations, 150.0, 125.0).unwrap();
assert_eq!(link0.annotation_index(), 0);
assert_eq!(link0.link_get_annot(), 0);
assert_eq!(link1.annotation_index(), 1);
assert_eq!(link1.link_get_annot(), 1);
}
}