1use 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
16const MAX_FIELD_DEPTH: usize = 100;
19
20pub 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 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 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(script) = parse_action_script(dict, b"C") {
131 tree.calculate_scripts.insert(id, script);
132 }
133 if let Some(pid) = parent {
134 tree.get_mut(pid).children.push(id);
135 }
136 if let Some(kids_arr) = dict.get::<Array<'_>>(keys::KIDS) {
137 for kid_dict in kids_arr.iter::<Dict<'_>>() {
138 parse_field_recursive(&kid_dict, tree, Some(id), depth + 1, visited);
139 }
140 }
141}
142
143fn parse_on_state(dict: &Dict<'_>) -> Option<String> {
147 let ap: Dict<'_> = dict.get(keys::AP)?;
148 let n: Dict<'_> = ap.get(keys::N)?;
149 let mut found = None;
150 for key in n.keys() {
151 let bytes: &[u8] = key.as_ref();
152 if bytes != b"Off" {
153 found = Some(crate::encoding::decode_name_bytes(bytes));
154 break;
155 }
156 }
157 found
158}
159
160fn parse_action_script(dict: &Dict<'_>, trigger: &[u8]) -> Option<String> {
163 let aa: Dict<'_> = dict.get(keys::AA)?;
164 let action: Dict<'_> = aa.get(trigger)?;
165 let subtype: Name = action.get(keys::S)?;
166 if subtype.as_ref() != b"JavaScript" {
167 return None;
168 }
169 match action.get::<Object<'_>>(keys::JS)? {
170 Object::String(s) => Some(crate::encoding::decode_pdf_text_bytes(s.as_bytes())),
171 Object::Stream(st) => st
172 .decoded()
173 .ok()
174 .map(|bytes| crate::encoding::decode_pdf_text_bytes(&bytes)),
175 _ => None,
176 }
177}
178
179fn parse_field_value(dict: &Dict<'_>, key: &[u8]) -> Option<FieldValue> {
180 let obj: Object<'_> = dict.get(key)?;
181 match obj {
182 Object::String(s) => Some(FieldValue::Text(crate::encoding::decode_pdf_text_bytes(
183 s.as_bytes(),
184 ))),
185 Object::Name(n) => Some(FieldValue::Text(crate::encoding::decode_name_bytes(
186 n.as_ref(),
187 ))),
188 Object::Array(arr) => {
189 let vals: Vec<String> = arr
190 .iter::<Object<'_>>()
191 .filter_map(|o| match o {
192 Object::String(s) => Some(crate::encoding::decode_pdf_text_bytes(s.as_bytes())),
193 Object::Name(n) => Some(crate::encoding::decode_name_bytes(n.as_ref())),
194 _ => None,
195 })
196 .collect();
197 Some(FieldValue::StringArray(vals))
198 }
199 _ => None,
200 }
201}
202
203fn parse_options(dict: &Dict<'_>) -> Vec<ChoiceOption> {
204 let Some(arr) = dict.get::<Array<'_>>(keys::OPT) else {
205 return vec![];
206 };
207 arr.iter::<Object<'_>>()
208 .filter_map(|obj| match obj {
209 Object::String(s) => {
210 let text = crate::encoding::decode_pdf_text_bytes(s.as_bytes());
211 Some(ChoiceOption {
212 export: text.clone(),
213 display: text,
214 })
215 }
216 Object::Array(pair) => {
217 let items: Vec<Object<'_>> = pair.iter::<Object<'_>>().collect();
218 if items.len() >= 2 {
219 Some(ChoiceOption {
220 export: obj_to_string(&items[0]).unwrap_or_default(),
221 display: obj_to_string(&items[1]).unwrap_or_default(),
222 })
223 } else {
224 None
225 }
226 }
227 _ => None,
228 })
229 .collect()
230}
231
232fn parse_mk(dict: &Dict<'_>) -> Option<MkDict> {
233 let mk_dict: Dict<'_> = dict.get(keys::MK)?;
234 Some(MkDict {
235 border_color: parse_color_array(&mk_dict, keys::BC),
236 background_color: parse_color_array(&mk_dict, keys::BG),
237 caption: get_string_value(&mk_dict, keys::CA),
238 rollover_caption: get_string_value(&mk_dict, &b"RC"[..]),
239 alternate_caption: get_string_value(&mk_dict, keys::AC),
240 text_position: mk_dict.get::<u32>(&b"TP"[..]),
241 rotation: mk_dict.get::<u32>(&b"R"[..]),
242 })
243}
244
245fn parse_color_array(dict: &Dict<'_>, key: &[u8]) -> Option<Vec<f32>> {
246 let arr: Array<'_> = dict.get(key)?;
247 let vals: Vec<f32> = arr.iter::<f32>().collect();
248 if vals.is_empty() {
249 None
250 } else {
251 Some(vals)
252 }
253}
254
255fn parse_border_style(dict: &Dict<'_>) -> Option<BorderStyle> {
256 let bs_dict: Dict<'_> = dict.get(keys::BS)?;
257 Some(BorderStyle {
258 width: bs_dict.get::<f32>(&b"W"[..]).unwrap_or(1.0),
259 style: bs_dict
260 .get::<Name>(&b"S"[..])
261 .and_then(|n| n.as_ref().first().copied())
262 .unwrap_or(b'S'),
263 })
264}
265
266fn parse_quadding(q: u32) -> Quadding {
267 match q {
268 1 => Quadding::Center,
269 2 => Quadding::Right,
270 _ => Quadding::Left,
271 }
272}
273
274fn get_string_value(dict: &Dict<'_>, key: &[u8]) -> Option<String> {
275 obj_to_string(&dict.get::<Object<'_>>(key)?)
276}
277
278fn obj_to_string(obj: &Object<'_>) -> Option<String> {
279 match obj {
280 Object::String(s) => Some(crate::encoding::decode_pdf_text_bytes(s.as_bytes())),
281 Object::Name(n) => Some(crate::encoding::decode_name_bytes(n.as_ref())),
282 _ => None,
283 }
284}
285
286fn find_by_object_id(tree: &FieldTree, target: (i32, i32)) -> Option<FieldId> {
287 tree.all_ids()
288 .find(|&id| tree.get(id).object_id == Some(target))
289}
290
291fn assign_page_indices(pdf: &Pdf, tree: &mut FieldTree) {
292 let pages = pdf.pages();
293 for (page_idx, page) in pages.iter().enumerate() {
294 let raw = page.raw();
295 let Some(annots_arr) = raw.get::<Array<'_>>(keys::ANNOTS) else {
296 continue;
297 };
298 for annot_obj in annots_arr.iter::<Object<'_>>() {
299 if let Object::Dict(annot_dict) = annot_obj {
300 if let Some(annot_oid) = annot_dict.obj_id() {
301 let target = (annot_oid.obj_number, annot_oid.gen_number);
302 if let Some(fid) = find_by_object_id(tree, target) {
303 tree.get_mut(fid).page_index = Some(page_idx);
304 }
305 }
306 }
307 }
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 #[test]
315 fn quadding_values() {
316 assert_eq!(parse_quadding(0), Quadding::Left);
317 assert_eq!(parse_quadding(1), Quadding::Center);
318 assert_eq!(parse_quadding(2), Quadding::Right);
319 }
320
321 #[test]
322 fn cyclic_field_tree_terminates_and_is_bounded() {
323 fn cyclic_form_pdf() -> Vec<u8> {
326 let objs: [&[u8]; 6] = [
327 b"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>",
328 b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
329 b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] >>",
330 b"<< /Fields [5 0 R] >>",
331 b"<< /T (A) /Kids [6 0 R] >>",
332 b"<< /T (B) /Kids [5 0 R] >>", ];
334 let mut buf = Vec::new();
335 let mut offsets = [0usize; 7];
336 buf.extend_from_slice(b"%PDF-1.7\n");
337 for (i, body) in objs.iter().enumerate() {
338 offsets[i + 1] = buf.len();
339 buf.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
340 buf.extend_from_slice(body);
341 buf.extend_from_slice(b"\nendobj\n");
342 }
343 let xref_off = buf.len();
344 buf.extend_from_slice(b"xref\n0 7\n0000000000 65535 f \n");
345 for o in &offsets[1..7] {
346 buf.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
347 }
348 buf.extend_from_slice(
349 format!("trailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n{xref_off}\n%%EOF")
350 .as_bytes(),
351 );
352 buf
353 }
354
355 let pdf = Pdf::new(cyclic_form_pdf()).expect("load cyclic-form PDF");
356 let tree = parse_acroform(&pdf).expect("acroform parses");
357 assert!(
358 tree.len() <= 2,
359 "cyclic /Kids must not inflate the field tree; got {}",
360 tree.len()
361 );
362 }
363}