pdf_annot/builder/page_attach.rs
1//! Helper to attach an annotation reference to a page's /Annots array.
2
3#[cfg(feature = "write")]
4use lopdf::{Document, Object, ObjectId};
5
6#[cfg(feature = "write")]
7use crate::error::AnnotBuildError;
8
9#[cfg(feature = "write")]
10enum AnnotsAction {
11 SetArray(Vec<Object>),
12 AppendIndirect(ObjectId),
13}
14
15/// Add an annotation reference to a page's /Annots array.
16///
17/// Handles both inline arrays and indirect (referenced) arrays. When the
18/// indirect Annots array cannot be resolved (e.g. it lives in a compressed
19/// ObjStm that lopdf failed to expand), falls back to replacing the /Annots
20/// entry with a new inline array so the annotation is never silently dropped.
21#[cfg(feature = "write")]
22pub fn add_annotation_to_page(
23 doc: &mut Document,
24 page_num: u32,
25 annot_id: ObjectId,
26) -> Result<(), AnnotBuildError> {
27 let pages = doc.get_pages();
28 let page_count = pages.len();
29 let page_id = *pages
30 .get(&page_num)
31 .ok_or(AnnotBuildError::PageOutOfRange(page_num, page_count))?;
32
33 // Read the current /Annots entry using get_dictionary (follows indirect
34 // refs, handles compressed-stream pages).
35 let annots_action = {
36 match doc.get_dictionary(page_id) {
37 Ok(page_dict) => match page_dict.get(b"Annots").ok() {
38 Some(Object::Array(arr)) => {
39 let mut new_arr = arr.clone();
40 new_arr.push(Object::Reference(annot_id));
41 AnnotsAction::SetArray(new_arr)
42 }
43 Some(Object::Reference(r)) => AnnotsAction::AppendIndirect(*r),
44 _ => AnnotsAction::SetArray(vec![Object::Reference(annot_id)]),
45 },
46 // Page dict not accessible: still attempt to set an inline Annots.
47 Err(_) => AnnotsAction::SetArray(vec![Object::Reference(annot_id)]),
48 }
49 };
50
51 match annots_action {
52 AnnotsAction::SetArray(arr) => {
53 // Propagate failure: if the page dict cannot be mutated (e.g. the
54 // page was extracted from a fully-compressed ObjStm and lopdf has
55 // trouble writing it back), return an explicit error instead of
56 // silently dropping the annotation. Fixes #470.
57 if let Ok(page_dict) = doc.get_dictionary_mut(page_id) {
58 page_dict.set("Annots", Object::Array(arr));
59 } else {
60 return Err(AnnotBuildError::PageMutationFailed);
61 }
62 }
63 AnnotsAction::AppendIndirect(annots_ref) => {
64 // Attempt to mutate the indirect array in place.
65 let appended = {
66 if let Ok(Object::Array(ref mut arr)) = doc.get_object_mut(annots_ref) {
67 arr.push(Object::Reference(annot_id));
68 true
69 } else {
70 false
71 }
72 };
73
74 if !appended {
75 // Fallback: indirect array not accessible (e.g. lives in a
76 // compressed ObjStm). Try a read-only access first — lopdf can
77 // often decompress ObjStm objects for reading even when it
78 // cannot hand out a mutable reference. This preserves existing
79 // annotations instead of silently dropping them. Fixes #466 bug 7.
80 let existing: Vec<Object> = match doc.get_object(annots_ref) {
81 Ok(Object::Array(ref arr)) => arr.clone(),
82 _ => Vec::new(),
83 };
84 let mut new_annots = existing;
85 new_annots.push(Object::Reference(annot_id));
86 if let Ok(page_dict) = doc.get_dictionary_mut(page_id) {
87 page_dict.set("Annots", Object::Array(new_annots));
88 } else {
89 return Err(AnnotBuildError::PageMutationFailed);
90 }
91 }
92 }
93 }
94
95 Ok(())
96}