Skip to main content

pdfluent_forms/
tree.rs

1//! Arena-based field tree for AcroForm fields (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;
10
11/// Identifier for a node in the field tree.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct FieldId(pub(crate) usize);
14
15/// The type of a form field.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum FieldType {
18    /// Text input field (/Tx).
19    Text,
20    /// Button field (/Btn) — checkbox, radio, or push button.
21    Button,
22    /// Choice field (/Ch) — combo box or list box.
23    Choice,
24    /// Digital signature field (/Sig).
25    Signature,
26}
27
28/// Text alignment (quadding).
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub enum Quadding {
31    /// Left-justified (0).
32    #[default]
33    Left,
34    /// Centered (1).
35    Center,
36    /// Right-justified (2).
37    Right,
38}
39
40/// A field value.
41#[derive(Debug, Clone, PartialEq)]
42pub enum FieldValue {
43    /// Text or name value (string).
44    Text(String),
45    /// Array of selected values (choice fields).
46    StringArray(Vec<String>),
47}
48
49/// An option in a choice field (/Opt entry).
50#[derive(Debug, Clone, PartialEq)]
51pub struct ChoiceOption {
52    /// Export value (sent when form is submitted).
53    pub export: String,
54    /// Display value (shown in the field).
55    pub display: String,
56}
57
58/// Widget appearance characteristics (/MK dictionary).
59#[derive(Debug, Clone, Default)]
60pub struct MkDict {
61    /// Border color (/BC).
62    pub border_color: Option<Vec<f32>>,
63    /// Background color (/BG).
64    pub background_color: Option<Vec<f32>>,
65    /// Normal caption (/CA).
66    pub caption: Option<String>,
67    /// Rollover caption (/RC).
68    pub rollover_caption: Option<String>,
69    /// Alternate caption (/AC).
70    pub alternate_caption: Option<String>,
71    /// Icon/caption layout (/TP).
72    pub text_position: Option<u32>,
73    /// Rotation (/R).
74    pub rotation: Option<u32>,
75}
76
77/// Border style (/BS dictionary).
78#[derive(Debug, Clone)]
79pub struct BorderStyle {
80    /// Border width (/W).
81    pub width: f32,
82    /// Border style (/S): S=solid, D=dashed, B=beveled, I=inset, U=underline.
83    pub style: u8,
84}
85
86impl Default for BorderStyle {
87    fn default() -> Self {
88        Self {
89            width: 1.0,
90            style: b'S',
91        }
92    }
93}
94
95/// A single node in the AcroForm field tree.
96#[derive(Debug, Clone)]
97pub struct FieldNode {
98    /// Partial field name (/T).
99    pub partial_name: String,
100    /// Alternate field name (/TU).
101    pub alternate_name: Option<String>,
102    /// Mapping name (/TM).
103    pub mapping_name: Option<String>,
104    /// Field type (/FT): may be inherited from parent.
105    pub field_type: Option<FieldType>,
106    /// Field flags (/Ff).
107    pub flags: FieldFlags,
108    /// Current value (/V).
109    pub value: Option<FieldValue>,
110    /// Default value (/DV).
111    pub default_value: Option<FieldValue>,
112    /// Default appearance string (/DA).
113    pub default_appearance: Option<String>,
114    /// Quadding / text alignment (/Q).
115    pub quadding: Option<Quadding>,
116    /// Maximum length (/MaxLen) for text fields.
117    pub max_len: Option<u32>,
118    /// Options (/Opt) for choice fields.
119    pub options: Vec<ChoiceOption>,
120    /// Top index (/TI) for list box scroll position.
121    pub top_index: Option<u32>,
122    /// Widget rectangle (/Rect).
123    pub rect: Option<[f32; 4]>,
124    /// Current appearance state (/AS).
125    pub appearance_state: Option<String>,
126    /// On-state name for button widgets: the first non-`Off` key of this
127    /// widget's `/AP /N` sub-dictionary. `None` for non-button widgets or
128    /// when no appearance dictionary is present.
129    pub on_state: Option<String>,
130    /// Page index (0-based) this widget appears on.
131    pub page_index: Option<usize>,
132    /// Parent node in the field tree.
133    pub parent: Option<FieldId>,
134    /// Child nodes.
135    pub children: Vec<FieldId>,
136    /// PDF object identifier (obj_number, gen_number).
137    pub object_id: Option<(i32, i32)>,
138    /// Whether this field has /AA (additional actions).
139    pub has_actions: bool,
140    /// Widget appearance characteristics (/MK).
141    pub mk: Option<MkDict>,
142    /// Border style (/BS).
143    pub border_style: Option<BorderStyle>,
144}
145
146/// The complete AcroForm field tree.
147#[derive(Debug)]
148pub struct FieldTree {
149    nodes: Vec<FieldNode>,
150    /// Calculation order (/CO) — field IDs in evaluation order.
151    pub calculation_order: Vec<FieldId>,
152    /// JavaScript source of each field's calculate action (`/AA /C /JS`).
153    pub calculate_scripts: std::collections::HashMap<FieldId, String>,
154    /// Document-level default appearance (/DA).
155    pub document_da: Option<String>,
156    /// Document-level quadding (/Q).
157    pub document_quadding: Option<Quadding>,
158    /// Whether /NeedAppearances is set.
159    pub need_appearances: bool,
160    /// SigFlags value.
161    pub sig_flags: u32,
162}
163
164impl FieldTree {
165    /// Create an empty field tree.
166    pub fn new() -> Self {
167        Self {
168            nodes: Vec::new(),
169            calculation_order: Vec::new(),
170            calculate_scripts: std::collections::HashMap::new(),
171            document_da: None,
172            document_quadding: None,
173            need_appearances: false,
174            sig_flags: 0,
175        }
176    }
177
178    /// Allocate a new node, returning its ID.
179    pub fn alloc(&mut self, node: FieldNode) -> FieldId {
180        let id = FieldId(self.nodes.len());
181        self.nodes.push(node);
182        id
183    }
184
185    /// Get a node by ID.
186    pub fn get(&self, id: FieldId) -> &FieldNode {
187        &self.nodes[id.0]
188    }
189
190    /// Get a mutable node by ID.
191    pub fn get_mut(&mut self, id: FieldId) -> &mut FieldNode {
192        &mut self.nodes[id.0]
193    }
194
195    /// Number of nodes.
196    pub fn len(&self) -> usize {
197        self.nodes.len()
198    }
199
200    /// Whether the tree is empty.
201    pub fn is_empty(&self) -> bool {
202        self.nodes.is_empty()
203    }
204
205    /// Return IDs of all root nodes (no parent).
206    pub fn roots(&self) -> Vec<FieldId> {
207        self.nodes
208            .iter()
209            .enumerate()
210            .filter(|(_, n)| n.parent.is_none())
211            .map(|(i, _)| FieldId(i))
212            .collect()
213    }
214
215    /// Return IDs of all terminal (leaf/widget) fields.
216    pub fn terminal_fields(&self) -> Vec<FieldId> {
217        self.nodes
218            .iter()
219            .enumerate()
220            .filter(|(_, n)| n.children.is_empty())
221            .map(|(i, _)| FieldId(i))
222            .collect()
223    }
224
225    /// Compute the fully qualified field name by walking up the parent chain.
226    pub fn fully_qualified_name(&self, id: FieldId) -> String {
227        let mut parts = Vec::new();
228        let mut cur = Some(id);
229        while let Some(cid) = cur {
230            let node = self.get(cid);
231            if !node.partial_name.is_empty() {
232                parts.push(node.partial_name.as_str());
233            }
234            cur = node.parent;
235        }
236        parts.reverse();
237        parts.join(".")
238    }
239
240    /// Walk up the tree to find the effective field type.
241    pub fn effective_field_type(&self, id: FieldId) -> Option<FieldType> {
242        let mut cur = Some(id);
243        while let Some(cid) = cur {
244            if let Some(ft) = self.get(cid).field_type {
245                return Some(ft);
246            }
247            cur = self.get(cid).parent;
248        }
249        None
250    }
251
252    /// Walk up the tree to find the effective value.
253    pub fn effective_value(&self, id: FieldId) -> Option<&FieldValue> {
254        let mut cur = Some(id);
255        while let Some(cid) = cur {
256            let node = self.get(cid);
257            if node.value.is_some() {
258                return node.value.as_ref();
259            }
260            cur = node.parent;
261        }
262        None
263    }
264
265    /// Walk up the tree to find the effective DA string.
266    pub fn effective_da(&self, id: FieldId) -> Option<&str> {
267        let mut cur = Some(id);
268        while let Some(cid) = cur {
269            if let Some(ref da) = self.get(cid).default_appearance {
270                return Some(da.as_str());
271            }
272            cur = self.get(cid).parent;
273        }
274        self.document_da.as_deref()
275    }
276
277    /// Walk up the tree to find the effective quadding.
278    pub fn effective_quadding(&self, id: FieldId) -> Quadding {
279        let mut cur = Some(id);
280        while let Some(cid) = cur {
281            if let Some(q) = self.get(cid).quadding {
282                return q;
283            }
284            cur = self.get(cid).parent;
285        }
286        self.document_quadding.unwrap_or_default()
287    }
288
289    /// Walk up the tree to get effective flags.
290    pub fn effective_flags(&self, id: FieldId) -> FieldFlags {
291        self.get(id).flags
292    }
293
294    /// Walk up the tree to find the effective MaxLen.
295    ///
296    /// `/MaxLen` is treated as inheritable (like `/FT`, `/DA`, `/Q`): if a
297    /// widget does not carry it directly, the value propagates from the nearest
298    /// ancestor that does.
299    pub fn effective_max_len(&self, id: FieldId) -> Option<u32> {
300        let mut cur = Some(id);
301        while let Some(cid) = cur {
302            if let Some(ml) = self.get(cid).max_len {
303                return Some(ml);
304            }
305            cur = self.get(cid).parent;
306        }
307        None
308    }
309
310    /// Find a terminal field by fully qualified name.
311    pub fn find_by_name(&self, name: &str) -> Option<FieldId> {
312        self.terminal_fields()
313            .into_iter()
314            .find(|&id| self.fully_qualified_name(id) == name)
315    }
316
317    /// Return all node IDs.
318    pub fn all_ids(&self) -> impl Iterator<Item = FieldId> {
319        (0..self.nodes.len()).map(FieldId)
320    }
321}
322
323impl Default for FieldTree {
324    fn default() -> Self {
325        Self::new()
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    fn make_node(name: &str) -> FieldNode {
334        FieldNode {
335            partial_name: name.into(),
336            alternate_name: None,
337            mapping_name: None,
338            field_type: None,
339            flags: FieldFlags::empty(),
340            value: None,
341            default_value: None,
342            default_appearance: None,
343            quadding: None,
344            max_len: None,
345            options: vec![],
346            top_index: None,
347            rect: None,
348            appearance_state: None,
349            on_state: None,
350            page_index: None,
351            parent: None,
352            children: vec![],
353            object_id: None,
354            has_actions: false,
355            mk: None,
356            border_style: None,
357        }
358    }
359
360    #[test]
361    fn fqn_simple() {
362        let mut tree = FieldTree::new();
363        let root = tree.alloc(make_node("form"));
364        let mut child = make_node("name");
365        child.parent = Some(root);
366        child.field_type = Some(FieldType::Text);
367        let child_id = tree.alloc(child);
368        tree.get_mut(root).children.push(child_id);
369        assert_eq!(tree.fully_qualified_name(child_id), "form.name");
370    }
371
372    #[test]
373    fn inherited_field_type() {
374        let mut tree = FieldTree::new();
375        let mut parent = make_node("group");
376        parent.field_type = Some(FieldType::Button);
377        let parent_id = tree.alloc(parent);
378        let mut child = make_node("opt1");
379        child.parent = Some(parent_id);
380        let child_id = tree.alloc(child);
381        tree.get_mut(parent_id).children.push(child_id);
382        assert_eq!(tree.effective_field_type(child_id), Some(FieldType::Button));
383    }
384
385    #[test]
386    fn inherited_da() {
387        let mut tree = FieldTree::new();
388        tree.document_da = Some("0 g /Helv 12 Tf".into());
389        let id = tree.alloc(make_node("field"));
390        assert_eq!(tree.effective_da(id), Some("0 g /Helv 12 Tf"));
391    }
392
393    #[test]
394    fn inherited_max_len() {
395        let mut tree = FieldTree::new();
396        // Parent carries MaxLen; child does not.
397        let mut parent = make_node("group");
398        parent.max_len = Some(10);
399        let parent_id = tree.alloc(parent);
400
401        let mut child = make_node("field");
402        child.parent = Some(parent_id);
403        let child_id = tree.alloc(child);
404        tree.get_mut(parent_id).children.push(child_id);
405
406        assert_eq!(tree.effective_max_len(child_id), Some(10));
407    }
408
409    #[test]
410    fn own_max_len_overrides_parent() {
411        let mut tree = FieldTree::new();
412        let mut parent = make_node("group");
413        parent.max_len = Some(10);
414        let parent_id = tree.alloc(parent);
415
416        let mut child = make_node("field");
417        child.parent = Some(parent_id);
418        child.max_len = Some(5);
419        let child_id = tree.alloc(child);
420        tree.get_mut(parent_id).children.push(child_id);
421
422        assert_eq!(tree.effective_max_len(child_id), Some(5));
423    }
424}