1use std::collections::HashMap;
8
9use zpdf_core::{ObjectId, PdfObject, Rect};
10use zpdf_parser::PdfFile;
11
12use crate::destinations::{resolve_link_target, Destination};
13use crate::forms::{AcroForm, GeneratedAppearance};
14use crate::measure::{parse_measure, Measure};
15use crate::page::PdfPage;
16use crate::Catalog;
17
18pub const ANNOT_FLAG_HIDDEN: i64 = 1 << 1;
20pub const ANNOT_FLAG_NOVIEW: i64 = 1 << 5;
21
22#[derive(Debug, Clone)]
23pub struct Annotation {
24 pub subtype: String,
25 pub rect: Rect,
27 pub flags: i64,
29 pub appearance: Option<ObjectId>,
32 pub generated: Option<GeneratedAppearance>,
36 pub oc: Option<PdfObject>,
39 pub dest: Option<Destination>,
45 pub uri: Option<String>,
48 pub measure: Option<Measure>,
52}
53
54impl Annotation {
55 pub fn is_viewable(&self) -> bool {
58 self.flags & (ANNOT_FLAG_HIDDEN | ANNOT_FLAG_NOVIEW) == 0
59 && self.subtype != "Popup"
61 && (self.appearance.is_some() || self.generated.is_some())
62 && self.rect.width() > 0.0
63 && self.rect.height() > 0.0
64 }
65}
66
67pub fn parse_annotations(
75 file: &PdfFile,
76 page: &PdfPage,
77 catalog: &Catalog,
78 named: &HashMap<Vec<u8>, PdfObject>,
79 acro_form: Option<&AcroForm>,
80) -> Vec<Annotation> {
81 page.annots
82 .iter()
83 .filter_map(|&id| parse_annotation(file, id, catalog, named, acro_form))
84 .collect()
85}
86
87fn parse_annotation(
88 file: &PdfFile,
89 id: ObjectId,
90 catalog: &Catalog,
91 named: &HashMap<Vec<u8>, PdfObject>,
92 acro_form: Option<&AcroForm>,
93) -> Option<Annotation> {
94 let obj = file.resolve(id).ok()?;
95 let dict = obj.as_dict().ok()?;
96
97 let subtype = dict.get_name("Subtype").unwrap_or("").to_string();
98 let rect = crate::page::resolve_rect(file, dict, "Rect")?;
99 let flags = match dict.get("F") {
100 Some(PdfObject::Integer(n)) => *n,
101 Some(PdfObject::Ref(r)) => file
102 .resolve(*r)
103 .ok()
104 .and_then(|o| o.as_i64().ok())
105 .unwrap_or(0),
106 _ => 0,
107 };
108
109 let appearance = select_appearance(file, dict);
110 let oc = dict.get("OC").cloned();
111 let (dest, uri) = resolve_link_target(file, catalog, dict, Some(named));
114
115 let measure = parse_measure(file, dict);
117
118 let generated = if subtype == "Widget" {
123 acro_form
124 .and_then(|af| af.field_for_widget(id).map(|field| (af, field)))
125 .filter(|(af, _)| af.need_appearances || appearance.is_none())
126 .and_then(|(af, field)| {
127 crate::forms::generate_widget_appearance(field, rect, af.dr_fonts.as_ref())
128 })
129 } else if appearance.is_none() {
130 crate::annot_appearance::generate_annotation_appearance(file, dict, &subtype, rect)
131 } else {
132 None
133 };
134
135 Some(Annotation {
136 subtype,
137 rect,
138 flags,
139 appearance,
140 generated,
141 oc,
142 dest,
143 uri,
144 measure,
145 })
146}
147
148fn select_appearance(file: &PdfFile, annot: &zpdf_core::PdfDict) -> Option<ObjectId> {
151 let ap = match annot.get("AP")? {
152 PdfObject::Dict(d) => d.clone(),
153 PdfObject::Ref(r) => file.resolve(*r).ok()?.as_dict().ok()?.clone(),
154 _ => return None,
155 };
156 let n = ap.get("N")?;
157
158 if let PdfObject::Ref(r) = n {
160 match file.resolve(*r).ok()? {
161 PdfObject::Stream(_) => return Some(*r),
162 PdfObject::Dict(states) => return select_state(file, &states, annot),
163 _ => return None,
164 }
165 }
166 if let PdfObject::Dict(states) = n {
168 return select_state(file, states, annot);
169 }
170 None
171}
172
173fn select_state(
174 file: &PdfFile,
175 states: &zpdf_core::PdfDict,
176 annot: &zpdf_core::PdfDict,
177) -> Option<ObjectId> {
178 let state = annot.get_name("AS").ok().or_else(|| match annot.get("V") {
181 Some(PdfObject::Name(n)) => Some(n.as_str()),
182 _ => None,
183 });
184 if let Some(state) = state {
185 if let Some(PdfObject::Ref(r)) = states.get(state) {
186 return Some(*r);
187 }
188 }
189 if states.0.len() == 1 {
191 if let Some(PdfObject::Ref(r)) = states.0.values().next() {
192 return Some(*r);
193 }
194 }
195 let _ = file;
196 None
197}
198
199#[cfg(test)]
200mod tests {
201 use crate::test_util::build_pdf;
202 use crate::PdfDocument;
203
204 fn doc_with_annots(annot_refs: &str, annots: &[&str]) -> PdfDocument {
207 let mut objs: Vec<String> = vec![
208 "<< /Type /Catalog /Pages 2 0 R >>".into(),
209 "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>".into(),
210 format!(
211 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Annots [{annot_refs}] >>"
212 ),
213 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>".into(),
214 ];
215 objs.extend(annots.iter().map(|a| (*a).to_string()));
216 let refs: Vec<&str> = objs.iter().map(|s| s.as_str()).collect();
217 PdfDocument::open(build_pdf(&refs)).expect("open")
218 }
219
220 #[test]
221 fn link_explicit_dest_resolves() {
222 let doc = doc_with_annots(
223 "5 0 R",
224 &["<< /Type /Annot /Subtype /Link /Rect [10 10 100 30] /Dest [4 0 R /Fit] >>"],
225 );
226 let page = doc.page(0).unwrap();
227 let annots = doc.page_annotations(&page);
228 assert_eq!(annots.len(), 1);
229 let d = annots[0].dest.as_ref().expect("dest");
230 assert_eq!(d.page, Some(1));
231 assert!(annots[0].uri.is_none());
232 }
233
234 #[test]
235 fn link_uri_action_captured() {
236 let doc = doc_with_annots(
237 "5 0 R",
238 &["<< /Type /Annot /Subtype /Link /Rect [0 0 100 20] \
239 /A << /S /URI /URI (https://example.com) >> >>"],
240 );
241 let page = doc.page(0).unwrap();
242 let a = &doc.page_annotations(&page)[0];
243 assert_eq!(a.uri.as_deref(), Some("https://example.com"));
244 assert!(a.dest.is_none());
245 }
246
247 #[test]
248 fn link_goto_action_dest_resolves() {
249 let doc = doc_with_annots(
250 "5 0 R",
251 &["<< /Type /Annot /Subtype /Link /Rect [0 0 50 50] \
252 /A << /S /GoTo /D [3 0 R /XYZ null 700 null] >> >>"],
253 );
254 let page = doc.page(0).unwrap();
255 let d = doc.page_annotations(&page)[0].dest.clone().expect("dest");
256 assert_eq!(d.page, Some(0));
257 }
258
259 #[test]
260 fn link_gotor_remote_file_name() {
261 let doc = doc_with_annots(
262 "5 0 R",
263 &["<< /Type /Annot /Subtype /Link /Rect [0 0 50 50] \
264 /A << /S /GoToR /F (other.pdf) >> >>"],
265 );
266 let page = doc.page(0).unwrap();
267 let a = &doc.page_annotations(&page)[0];
268 assert_eq!(a.uri.as_deref(), Some("other.pdf"));
269 assert!(a.dest.is_none());
270 }
271
272 #[test]
273 fn non_link_annotation_has_no_target() {
274 let doc = doc_with_annots(
275 "5 0 R",
276 &["<< /Type /Annot /Subtype /Text /Rect [0 0 20 20] /Contents (note) >>"],
277 );
278 let page = doc.page(0).unwrap();
279 let a = &doc.page_annotations(&page)[0];
280 assert!(a.dest.is_none() && a.uri.is_none());
281 }
282
283 #[test]
284 fn link_named_dest_via_collected_map() {
285 let doc = PdfDocument::open(build_pdf(&[
288 "<< /Type /Catalog /Pages 2 0 R /Names << /Dests 6 0 R >> >>",
289 "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>",
290 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Annots [5 0 R] >>",
291 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>",
292 "<< /Type /Annot /Subtype /Link /Rect [0 0 50 50] /Dest (chap2) >>",
293 "<< /Names [ (chap2) [4 0 R /Fit] ] >>",
294 ]))
295 .expect("open");
296 let page = doc.page(0).unwrap();
297 let d = doc.page_annotations(&page)[0]
298 .dest
299 .clone()
300 .expect("named dest");
301 assert_eq!(d.page, Some(1));
302 }
303}