Skip to main content

pdfluent_forms/
tree.rs

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