Skip to main content

pdfluent_forms/
parse.rs

1//! AcroForm dictionary parser (B.1).
2
3// Copyright (c) 2026 Innovation Trigger B.V.
4//
5// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
6// the PDFluent Commercial Licence. See the LICENSE file in this repository --
7// that file travels with the copy you received, which a URL does not.
8
9use crate::flags::FieldFlags;
10use crate::tree::*;
11use pdf_syntax::object::dict::keys;
12use pdf_syntax::object::{Array, Dict, Name, Object, ObjectIdentifier, Rect};
13use pdf_syntax::Pdf;
14use std::collections::BTreeSet;
15
16/// Maximum AcroForm field-tree depth. Bounds `/Kids` recursion on adversarial
17/// or malformed forms; real field hierarchies are shallow.
18const MAX_FIELD_DEPTH: usize = 100;
19
20/// Parse the AcroForm dictionary from a PDF document and build a field tree.
21pub fn parse_acroform(pdf: &Pdf) -> Option<FieldTree> {
22    let xref = pdf.xref();
23    let catalog: Dict<'_> = xref.get(xref.root_id())?;
24    let acroform: Dict<'_> = catalog.get(keys::ACRO_FORM)?;
25    let mut tree = FieldTree::new();
26
27    if let Some(da) = get_string_value(&acroform, keys::DA) {
28        tree.document_da = Some(da);
29    }
30    if let Some(q) = acroform.get::<u32>(keys::Q) {
31        tree.document_quadding = Some(parse_quadding(q));
32    }
33    if let Some(na) = acroform.get::<bool>(keys::NEED_APPEARANCES) {
34        tree.need_appearances = na;
35    }
36    if let Some(sf) = acroform.get::<u32>(keys::SIG_FLAGS) {
37        tree.sig_flags = sf;
38    }
39
40    if let Some(fields_arr) = acroform.get::<Array<'_>>(keys::FIELDS) {
41        // `visited` breaks cyclic /Kids references and shared-node blowups;
42        // `depth` bounds deep nesting.
43        let mut visited = BTreeSet::new();
44        for field_dict in fields_arr.iter::<Dict<'_>>() {
45            parse_field_recursive(&field_dict, &mut tree, None, 0, &mut visited);
46        }
47    }
48
49    if let Some(co_arr) = acroform.get::<Array<'_>>(keys::CO) {
50        for co_obj in co_arr.iter::<Object<'_>>() {
51            if let Object::Dict(co_dict) = co_obj {
52                if let Some(obj_id) = co_dict.obj_id() {
53                    let target = (obj_id.obj_number, obj_id.gen_number);
54                    if let Some(id) = find_by_object_id(&tree, target) {
55                        tree.calculation_order.push(id);
56                    }
57                }
58            }
59        }
60    }
61
62    assign_page_indices(pdf, &mut tree);
63    Some(tree)
64}
65
66fn parse_field_recursive(
67    dict: &Dict<'_>,
68    tree: &mut FieldTree,
69    parent: Option<FieldId>,
70    depth: usize,
71    visited: &mut BTreeSet<ObjectIdentifier>,
72) {
73    // Bound deep nesting and break cyclic/shared /Kids references so a malformed
74    // form cannot overflow the stack or blow up memory.
75    if depth >= MAX_FIELD_DEPTH {
76        return;
77    }
78    if let Some(id) = dict.obj_id() {
79        if !visited.insert(id) {
80            return;
81        }
82    }
83
84    let partial_name = get_string_value(dict, keys::T).unwrap_or_default();
85    let field_type = dict.get::<Name>(keys::FT).and_then(|n| match n.as_ref() {
86        b"Tx" => Some(FieldType::Text),
87        b"Btn" => Some(FieldType::Button),
88        b"Ch" => Some(FieldType::Choice),
89        b"Sig" => Some(FieldType::Signature),
90        _ => None,
91    });
92    let flags = dict
93        .get::<u32>(keys::FF)
94        .map(FieldFlags::from_bits)
95        .unwrap_or_default();
96    let rect = dict
97        .get::<Rect>(keys::RECT)
98        .map(|r| [r.x0 as f32, r.y0 as f32, r.x1 as f32, r.y1 as f32]);
99    let appearance_state = dict
100        .get::<Name>(keys::AS)
101        .map(|n| crate::encoding::decode_name_bytes(n.as_ref()));
102    let object_id = dict.obj_id().map(|oid| (oid.obj_number, oid.gen_number));
103    let on_state = parse_on_state(dict);
104
105    let node = FieldNode {
106        partial_name,
107        alternate_name: get_string_value(dict, keys::TU),
108        mapping_name: get_string_value(dict, keys::TM),
109        field_type,
110        flags,
111        value: parse_field_value(dict, keys::V),
112        default_value: parse_field_value(dict, keys::DV),
113        default_appearance: get_string_value(dict, keys::DA),
114        quadding: dict.get::<u32>(keys::Q).map(parse_quadding),
115        max_len: dict.get::<u32>(keys::MAX_LEN),
116        options: parse_options(dict),
117        top_index: dict.get::<u32>(keys::TI),
118        rect,
119        appearance_state,
120        on_state,
121        page_index: None,
122        parent,
123        children: vec![],
124        object_id,
125        has_actions: dict.contains_key(keys::AA),
126        mk: parse_mk(dict),
127        border_style: parse_border_style(dict),
128    };
129    let id = tree.alloc(node);
130    if let Some(pid) = parent {
131        tree.get_mut(pid).children.push(id);
132    }
133    if let Some(kids_arr) = dict.get::<Array<'_>>(keys::KIDS) {
134        for kid_dict in kids_arr.iter::<Dict<'_>>() {
135            parse_field_recursive(&kid_dict, tree, Some(id), depth + 1, visited);
136        }
137    }
138}
139
140/// Extract the on-state for a button widget: the first non-`Off` key of the
141/// widget's `/AP /N` sub-dictionary (the same resolution rule pdfium's
142/// `GetOnStateName` and mupdf's `pdf_button_field_on_state` use).
143fn parse_on_state(dict: &Dict<'_>) -> Option<String> {
144    let ap: Dict<'_> = dict.get(keys::AP)?;
145    let n: Dict<'_> = ap.get(keys::N)?;
146    let mut found = None;
147    for key in n.keys() {
148        let bytes: &[u8] = key.as_ref();
149        if bytes != b"Off" {
150            found = Some(crate::encoding::decode_name_bytes(bytes));
151            break;
152        }
153    }
154    found
155}
156
157fn parse_field_value(dict: &Dict<'_>, key: &[u8]) -> Option<FieldValue> {
158    let obj: Object<'_> = dict.get(key)?;
159    match obj {
160        Object::String(s) => Some(FieldValue::Text(crate::encoding::decode_pdf_text_bytes(
161            s.as_bytes(),
162        ))),
163        Object::Name(n) => Some(FieldValue::Text(crate::encoding::decode_name_bytes(
164            n.as_ref(),
165        ))),
166        Object::Array(arr) => {
167            let vals: Vec<String> = arr
168                .iter::<Object<'_>>()
169                .filter_map(|o| match o {
170                    Object::String(s) => Some(crate::encoding::decode_pdf_text_bytes(s.as_bytes())),
171                    Object::Name(n) => Some(crate::encoding::decode_name_bytes(n.as_ref())),
172                    _ => None,
173                })
174                .collect();
175            Some(FieldValue::StringArray(vals))
176        }
177        _ => None,
178    }
179}
180
181fn parse_options(dict: &Dict<'_>) -> Vec<ChoiceOption> {
182    let Some(arr) = dict.get::<Array<'_>>(keys::OPT) else {
183        return vec![];
184    };
185    arr.iter::<Object<'_>>()
186        .filter_map(|obj| match obj {
187            Object::String(s) => {
188                let text = crate::encoding::decode_pdf_text_bytes(s.as_bytes());
189                Some(ChoiceOption {
190                    export: text.clone(),
191                    display: text,
192                })
193            }
194            Object::Array(pair) => {
195                let items: Vec<Object<'_>> = pair.iter::<Object<'_>>().collect();
196                if items.len() >= 2 {
197                    Some(ChoiceOption {
198                        export: obj_to_string(&items[0]).unwrap_or_default(),
199                        display: obj_to_string(&items[1]).unwrap_or_default(),
200                    })
201                } else {
202                    None
203                }
204            }
205            _ => None,
206        })
207        .collect()
208}
209
210fn parse_mk(dict: &Dict<'_>) -> Option<MkDict> {
211    let mk_dict: Dict<'_> = dict.get(keys::MK)?;
212    Some(MkDict {
213        border_color: parse_color_array(&mk_dict, keys::BC),
214        background_color: parse_color_array(&mk_dict, keys::BG),
215        caption: get_string_value(&mk_dict, keys::CA),
216        rollover_caption: get_string_value(&mk_dict, &b"RC"[..]),
217        alternate_caption: get_string_value(&mk_dict, keys::AC),
218        text_position: mk_dict.get::<u32>(&b"TP"[..]),
219        rotation: mk_dict.get::<u32>(&b"R"[..]),
220    })
221}
222
223fn parse_color_array(dict: &Dict<'_>, key: &[u8]) -> Option<Vec<f32>> {
224    let arr: Array<'_> = dict.get(key)?;
225    let vals: Vec<f32> = arr.iter::<f32>().collect();
226    if vals.is_empty() {
227        None
228    } else {
229        Some(vals)
230    }
231}
232
233fn parse_border_style(dict: &Dict<'_>) -> Option<BorderStyle> {
234    let bs_dict: Dict<'_> = dict.get(keys::BS)?;
235    Some(BorderStyle {
236        width: bs_dict.get::<f32>(&b"W"[..]).unwrap_or(1.0),
237        style: bs_dict
238            .get::<Name>(&b"S"[..])
239            .and_then(|n| n.as_ref().first().copied())
240            .unwrap_or(b'S'),
241    })
242}
243
244fn parse_quadding(q: u32) -> Quadding {
245    match q {
246        1 => Quadding::Center,
247        2 => Quadding::Right,
248        _ => Quadding::Left,
249    }
250}
251
252fn get_string_value(dict: &Dict<'_>, key: &[u8]) -> Option<String> {
253    obj_to_string(&dict.get::<Object<'_>>(key)?)
254}
255
256fn obj_to_string(obj: &Object<'_>) -> Option<String> {
257    match obj {
258        Object::String(s) => Some(crate::encoding::decode_pdf_text_bytes(s.as_bytes())),
259        Object::Name(n) => Some(crate::encoding::decode_name_bytes(n.as_ref())),
260        _ => None,
261    }
262}
263
264fn find_by_object_id(tree: &FieldTree, target: (i32, i32)) -> Option<FieldId> {
265    tree.all_ids()
266        .find(|&id| tree.get(id).object_id == Some(target))
267}
268
269fn assign_page_indices(pdf: &Pdf, tree: &mut FieldTree) {
270    let pages = pdf.pages();
271    for (page_idx, page) in pages.iter().enumerate() {
272        let raw = page.raw();
273        let Some(annots_arr) = raw.get::<Array<'_>>(keys::ANNOTS) else {
274            continue;
275        };
276        for annot_obj in annots_arr.iter::<Object<'_>>() {
277            if let Object::Dict(annot_dict) = annot_obj {
278                if let Some(annot_oid) = annot_dict.obj_id() {
279                    let target = (annot_oid.obj_number, annot_oid.gen_number);
280                    if let Some(fid) = find_by_object_id(tree, target) {
281                        tree.get_mut(fid).page_index = Some(page_idx);
282                    }
283                }
284            }
285        }
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    #[test]
293    fn quadding_values() {
294        assert_eq!(parse_quadding(0), Quadding::Left);
295        assert_eq!(parse_quadding(1), Quadding::Center);
296        assert_eq!(parse_quadding(2), Quadding::Right);
297    }
298
299    #[test]
300    fn cyclic_field_tree_terminates_and_is_bounded() {
301        // Two fields whose /Kids reference each other form a cycle; without the
302        // visited/depth guard parsing recurses until stack overflow.
303        fn cyclic_form_pdf() -> Vec<u8> {
304            let objs: [&[u8]; 6] = [
305                b"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>",
306                b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
307                b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] >>",
308                b"<< /Fields [5 0 R] >>",
309                b"<< /T (A) /Kids [6 0 R] >>",
310                b"<< /T (B) /Kids [5 0 R] >>", // /Kids back to A -> cycle
311            ];
312            let mut buf = Vec::new();
313            let mut offsets = [0usize; 7];
314            buf.extend_from_slice(b"%PDF-1.7\n");
315            for (i, body) in objs.iter().enumerate() {
316                offsets[i + 1] = buf.len();
317                buf.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
318                buf.extend_from_slice(body);
319                buf.extend_from_slice(b"\nendobj\n");
320            }
321            let xref_off = buf.len();
322            buf.extend_from_slice(b"xref\n0 7\n0000000000 65535 f \n");
323            for o in &offsets[1..7] {
324                buf.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
325            }
326            buf.extend_from_slice(
327                format!("trailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n{xref_off}\n%%EOF")
328                    .as_bytes(),
329            );
330            buf
331        }
332
333        let pdf = Pdf::new(cyclic_form_pdf()).expect("load cyclic-form PDF");
334        let tree = parse_acroform(&pdf).expect("acroform parses");
335        assert!(
336            tree.len() <= 2,
337            "cyclic /Kids must not inflate the field tree; got {}",
338            tree.len()
339        );
340    }
341}