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