Skip to main content

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}