1use std::collections::HashMap;
15
16use rpdfium_core::{Name, PdfSource};
17use rpdfium_parser::{Object, ObjectStore};
18
19use crate::action::{Action, parse_action};
20use crate::destination::{Destination, parse_destination};
21use crate::error::DocResult;
22
23#[derive(Debug, Clone)]
28pub struct Link {
29 pub rect: [f32; 4],
31 pub destination: Option<Destination>,
33 pub action: Option<Action>,
35}
36
37pub fn parse_link<S: PdfSource>(
41 dict: &HashMap<Name, Object>,
42 store: &ObjectStore<S>,
43) -> DocResult<Link> {
44 let rect_obj = dict
46 .get(&Name::rect())
47 .ok_or_else(|| crate::error::DocError::MissingKey("/Rect".into()))?;
48 let resolved = store
49 .deep_resolve(rect_obj)
50 .map_err(|e| crate::error::DocError::Parser(e.to_string()))?;
51 let arr = resolved
52 .as_array()
53 .ok_or(crate::error::DocError::UnexpectedType)?;
54 let get = |idx: usize| -> f32 {
55 arr.get(idx)
56 .and_then(|o| o.as_f64())
57 .map(|f| f as f32)
58 .unwrap_or(0.0)
59 };
60 let rect = [get(0), get(1), get(2), get(3)];
61
62 let destination = dict
64 .get(&Name::dest())
65 .and_then(|o| parse_destination(o, store).ok());
66
67 let action = dict
69 .get(&Name::a())
70 .and_then(|o| parse_action(o, store).ok());
71
72 Ok(Link {
73 rect,
74 destination,
75 action,
76 })
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::action::Action;
83 use crate::destination::Destination;
84 use rpdfium_core::PdfString;
85
86 fn build_store() -> ObjectStore<Vec<u8>> {
87 let pdf = build_minimal_pdf();
88 ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
89 }
90
91 fn build_minimal_pdf() -> Vec<u8> {
92 let mut pdf = Vec::new();
93 pdf.extend_from_slice(b"%PDF-1.4\n");
94 let obj1_offset = pdf.len();
95 pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
96 let obj2_offset = pdf.len();
97 pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
98 let xref_offset = pdf.len();
99 pdf.extend_from_slice(b"xref\n0 3\n");
100 pdf.extend_from_slice(b"0000000000 65535 f \r\n");
101 pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
102 pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
103 pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
104 pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
105 pdf
106 }
107
108 #[test]
109 fn test_parse_link_with_rect_only() {
110 let store = build_store();
111 let mut dict = HashMap::new();
112 dict.insert(
113 Name::rect(),
114 Object::Array(vec![
115 Object::Real(10.0),
116 Object::Real(20.0),
117 Object::Real(100.0),
118 Object::Real(50.0),
119 ]),
120 );
121 let link = parse_link(&dict, &store).unwrap();
122 assert_eq!(link.rect, [10.0, 20.0, 100.0, 50.0]);
123 assert!(link.destination.is_none());
124 assert!(link.action.is_none());
125 }
126
127 #[test]
128 fn test_parse_link_with_dest() {
129 let store = build_store();
130 let mut dict = HashMap::new();
131 dict.insert(
132 Name::rect(),
133 Object::Array(vec![
134 Object::Real(0.0),
135 Object::Real(0.0),
136 Object::Real(200.0),
137 Object::Real(20.0),
138 ]),
139 );
140 dict.insert(
141 Name::dest(),
142 Object::String(PdfString::from_bytes(b"page2".to_vec())),
143 );
144 let link = parse_link(&dict, &store).unwrap();
145 match &link.destination {
146 Some(Destination::Named(n)) => assert_eq!(n, "page2"),
147 _ => panic!("expected named destination"),
148 }
149 assert!(link.action.is_none());
150 }
151
152 #[test]
153 fn test_parse_link_with_uri_action() {
154 let store = build_store();
155 let mut action_dict = HashMap::new();
156 action_dict.insert(Name::s(), Object::Name(Name::from("URI")));
157 action_dict.insert(
158 Name::uri(),
159 Object::String(PdfString::from_bytes(b"https://example.com".to_vec())),
160 );
161
162 let mut dict = HashMap::new();
163 dict.insert(
164 Name::rect(),
165 Object::Array(vec![
166 Object::Real(0.0),
167 Object::Real(0.0),
168 Object::Real(100.0),
169 Object::Real(20.0),
170 ]),
171 );
172 dict.insert(Name::a(), Object::Dictionary(action_dict));
173
174 let link = parse_link(&dict, &store).unwrap();
175 assert!(link.destination.is_none());
176 match &link.action {
177 Some(Action::Uri(uri)) => assert_eq!(uri, "https://example.com"),
178 _ => panic!("expected URI action"),
179 }
180 }
181
182 #[test]
183 fn test_parse_link_missing_rect_returns_error() {
184 let store = build_store();
185 let dict = HashMap::new();
186 let result = parse_link(&dict, &store);
187 assert!(result.is_err());
188 }
189}