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    /// Document-level default appearance (/DA).
153    pub document_da: Option<String>,
154    /// Document-level quadding (/Q).
155    pub document_quadding: Option<Quadding>,
156    /// Whether /NeedAppearances is set.
157    pub need_appearances: bool,
158    /// SigFlags value.
159    pub sig_flags: u32,
160}
161
162impl FieldTree {
163    /// Create an empty field tree.
164    pub fn new() -> Self {
165        Self {
166            nodes: Vec::new(),
167            calculation_order: Vec::new(),
168            document_da: None,
169            document_quadding: None,
170            need_appearances: false,
171            sig_flags: 0,
172        }
173    }
174
175    /// Allocate a new node, returning its ID.
176    pub fn alloc(&mut self, node: FieldNode) -> FieldId {
177        let id = FieldId(self.nodes.len());
178        self.nodes.push(node);
179        id
180    }
181
182    /// Get a node by ID.
183    pub fn get(&self, id: FieldId) -> &FieldNode {
184        &self.nodes[id.0]
185    }
186
187    /// Get a mutable node by ID.
188    pub fn get_mut(&mut self, id: FieldId) -> &mut FieldNode {
189        &mut self.nodes[id.0]
190    }
191
192    /// Number of nodes.
193    pub fn len(&self) -> usize {
194        self.nodes.len()
195    }
196
197    /// Whether the tree is empty.
198    pub fn is_empty(&self) -> bool {
199        self.nodes.is_empty()
200    }
201
202    /// Return IDs of all root nodes (no parent).
203    pub fn roots(&self) -> Vec<FieldId> {
204        self.nodes
205            .iter()
206            .enumerate()
207            .filter(|(_, n)| n.parent.is_none())
208            .map(|(i, _)| FieldId(i))
209            .collect()
210    }
211
212    /// Return IDs of all terminal (leaf/widget) fields.
213    pub fn terminal_fields(&self) -> Vec<FieldId> {
214        self.nodes
215            .iter()
216            .enumerate()
217            .filter(|(_, n)| n.children.is_empty())
218            .map(|(i, _)| FieldId(i))
219            .collect()
220    }
221
222    /// Compute the fully qualified field name by walking up the parent chain.
223    pub fn fully_qualified_name(&self, id: FieldId) -> String {
224        let mut parts = Vec::new();
225        let mut cur = Some(id);
226        while let Some(cid) = cur {
227            let node = self.get(cid);
228            if !node.partial_name.is_empty() {
229                parts.push(node.partial_name.as_str());
230            }
231            cur = node.parent;
232        }
233        parts.reverse();
234        parts.join(".")
235    }
236
237    /// Walk up the tree to find the effective field type.
238    pub fn effective_field_type(&self, id: FieldId) -> Option<FieldType> {
239        let mut cur = Some(id);
240        while let Some(cid) = cur {
241            if let Some(ft) = self.get(cid).field_type {
242                return Some(ft);
243            }
244            cur = self.get(cid).parent;
245        }
246        None
247    }
248
249    /// Walk up the tree to find the effective value.
250    pub fn effective_value(&self, id: FieldId) -> Option<&FieldValue> {
251        let mut cur = Some(id);
252        while let Some(cid) = cur {
253            let node = self.get(cid);
254            if node.value.is_some() {
255                return node.value.as_ref();
256            }
257            cur = node.parent;
258        }
259        None
260    }
261
262    /// Walk up the tree to find the effective DA string.
263    pub fn effective_da(&self, id: FieldId) -> Option<&str> {
264        let mut cur = Some(id);
265        while let Some(cid) = cur {
266            if let Some(ref da) = self.get(cid).default_appearance {
267                return Some(da.as_str());
268            }
269            cur = self.get(cid).parent;
270        }
271        self.document_da.as_deref()
272    }
273
274    /// Walk up the tree to find the effective quadding.
275    pub fn effective_quadding(&self, id: FieldId) -> Quadding {
276        let mut cur = Some(id);
277        while let Some(cid) = cur {
278            if let Some(q) = self.get(cid).quadding {
279                return q;
280            }
281            cur = self.get(cid).parent;
282        }
283        self.document_quadding.unwrap_or_default()
284    }
285
286    /// Walk up the tree to get effective flags.
287    pub fn effective_flags(&self, id: FieldId) -> FieldFlags {
288        self.get(id).flags
289    }
290
291    /// Walk up the tree to find the effective MaxLen.
292    ///
293    /// `/MaxLen` is treated as inheritable (like `/FT`, `/DA`, `/Q`): if a
294    /// widget does not carry it directly, the value propagates from the nearest
295    /// ancestor that does.
296    pub fn effective_max_len(&self, id: FieldId) -> Option<u32> {
297        let mut cur = Some(id);
298        while let Some(cid) = cur {
299            if let Some(ml) = self.get(cid).max_len {
300                return Some(ml);
301            }
302            cur = self.get(cid).parent;
303        }
304        None
305    }
306
307    /// Find a terminal field by fully qualified name.
308    pub fn find_by_name(&self, name: &str) -> Option<FieldId> {
309        self.terminal_fields()
310            .into_iter()
311            .find(|&id| self.fully_qualified_name(id) == name)
312    }
313
314    /// Return all node IDs.
315    pub fn all_ids(&self) -> impl Iterator<Item = FieldId> {
316        (0..self.nodes.len()).map(FieldId)
317    }
318}
319
320impl Default for FieldTree {
321    fn default() -> Self {
322        Self::new()
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    fn make_node(name: &str) -> FieldNode {
331        FieldNode {
332            partial_name: name.into(),
333            alternate_name: None,
334            mapping_name: None,
335            field_type: None,
336            flags: FieldFlags::empty(),
337            value: None,
338            default_value: None,
339            default_appearance: None,
340            quadding: None,
341            max_len: None,
342            options: vec![],
343            top_index: None,
344            rect: None,
345            appearance_state: None,
346            on_state: None,
347            page_index: None,
348            parent: None,
349            children: vec![],
350            object_id: None,
351            has_actions: false,
352            mk: None,
353            border_style: None,
354        }
355    }
356
357    #[test]
358    fn fqn_simple() {
359        let mut tree = FieldTree::new();
360        let root = tree.alloc(make_node("form"));
361        let mut child = make_node("name");
362        child.parent = Some(root);
363        child.field_type = Some(FieldType::Text);
364        let child_id = tree.alloc(child);
365        tree.get_mut(root).children.push(child_id);
366        assert_eq!(tree.fully_qualified_name(child_id), "form.name");
367    }
368
369    #[test]
370    fn inherited_field_type() {
371        let mut tree = FieldTree::new();
372        let mut parent = make_node("group");
373        parent.field_type = Some(FieldType::Button);
374        let parent_id = tree.alloc(parent);
375        let mut child = make_node("opt1");
376        child.parent = Some(parent_id);
377        let child_id = tree.alloc(child);
378        tree.get_mut(parent_id).children.push(child_id);
379        assert_eq!(tree.effective_field_type(child_id), Some(FieldType::Button));
380    }
381
382    #[test]
383    fn inherited_da() {
384        let mut tree = FieldTree::new();
385        tree.document_da = Some("0 g /Helv 12 Tf".into());
386        let id = tree.alloc(make_node("field"));
387        assert_eq!(tree.effective_da(id), Some("0 g /Helv 12 Tf"));
388    }
389
390    #[test]
391    fn inherited_max_len() {
392        let mut tree = FieldTree::new();
393        // Parent carries MaxLen; child does not.
394        let mut parent = make_node("group");
395        parent.max_len = Some(10);
396        let parent_id = tree.alloc(parent);
397
398        let mut child = make_node("field");
399        child.parent = Some(parent_id);
400        let child_id = tree.alloc(child);
401        tree.get_mut(parent_id).children.push(child_id);
402
403        assert_eq!(tree.effective_max_len(child_id), Some(10));
404    }
405
406    #[test]
407    fn own_max_len_overrides_parent() {
408        let mut tree = FieldTree::new();
409        let mut parent = make_node("group");
410        parent.max_len = Some(10);
411        let parent_id = tree.alloc(parent);
412
413        let mut child = make_node("field");
414        child.parent = Some(parent_id);
415        child.max_len = Some(5);
416        let child_id = tree.alloc(child);
417        tree.get_mut(parent_id).children.push(child_id);
418
419        assert_eq!(tree.effective_max_len(child_id), Some(5));
420    }
421}