Skip to main content

pdfluent_forms/
facade.rs

1//! Unified form access facade for language bindings.
2//!
3//! Provides [`FormAccess`] and [`DocumentOps`] traits that abstract over
4//! AcroForm and XFA form technologies.  Language bindings (C, Python, WASM,
5//! Node.js) wrap these traits instead of individual crate APIs.
6
7// Copyright (c) 2026 Innovation Trigger B.V.
8//
9// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
10// the PDFluent Commercial Licence. See the LICENSE file in this repository --
11// that file travels with the copy you received, which a URL does not.
12
13use crate::tree::{FieldTree, FieldType, FieldValue};
14
15/// The kind of forms in a PDF document.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum FormKind {
18    /// AcroForm interactive forms (ISO 32000 §12.7).
19    AcroForm,
20    /// XFA forms (XML Forms Architecture).
21    Xfa,
22    /// No forms present.
23    None,
24}
25
26/// Error type for form operations.
27#[derive(Debug, thiserror::Error)]
28pub enum FormError {
29    /// The requested field was not found.
30    #[error("field not found: {0}")]
31    FieldNotFound(String),
32    /// The field is read-only and cannot be modified.
33    #[error("read-only field: {0}")]
34    ReadOnly(String),
35    /// The provided value is invalid for the field type.
36    #[error("invalid value for field type")]
37    InvalidValue,
38}
39
40/// Unified form field access — works for AcroForm or XFA.
41///
42/// This trait provides a common interface for reading and writing form field
43/// values regardless of the underlying form technology.
44pub trait FormAccess {
45    /// Returns the kind of form (AcroForm, XFA, or None).
46    fn form_type(&self) -> FormKind;
47
48    /// Returns all fully-qualified field names in the form.
49    fn field_names(&self) -> Vec<String>;
50
51    /// Gets the current value of a field by its fully-qualified name.
52    fn get_value(&self, path: &str) -> Option<String>;
53
54    /// Sets the value of a field by its fully-qualified name.
55    fn set_value(&mut self, path: &str, value: &str) -> Result<(), FormError>;
56}
57
58/// Unified document operations.
59///
60/// Provides access to form data, page count, and other document-level
61/// operations through a single interface.
62pub trait DocumentOps {
63    /// Returns the number of pages in the document.
64    fn page_count(&self) -> usize;
65
66    /// Returns read-only access to the form engine, if any.
67    fn form(&self) -> Option<&dyn FormAccess>;
68
69    /// Returns mutable access to the form engine, if any.
70    fn form_mut(&mut self) -> Option<&mut dyn FormAccess>;
71}
72
73impl FormAccess for FieldTree {
74    fn form_type(&self) -> FormKind {
75        FormKind::AcroForm
76    }
77
78    fn field_names(&self) -> Vec<String> {
79        self.terminal_fields()
80            .into_iter()
81            .map(|id| self.fully_qualified_name(id))
82            .collect()
83    }
84
85    fn get_value(&self, path: &str) -> Option<String> {
86        let id = self.find_by_name(path)?;
87        let value = self.effective_value(id)?;
88        match value {
89            FieldValue::Text(s) => Some(s.clone()),
90            FieldValue::StringArray(arr) => Some(arr.join(", ")),
91        }
92    }
93
94    fn set_value(&mut self, path: &str, value: &str) -> Result<(), FormError> {
95        let id = self
96            .find_by_name(path)
97            .ok_or_else(|| FormError::FieldNotFound(path.to_string()))?;
98
99        let ft = self
100            .effective_field_type(id)
101            .ok_or(FormError::InvalidValue)?;
102
103        // Reject writes to read-only fields (Ff bit 1) and signatures.
104        if ft == FieldType::Signature || self.effective_flags(id).read_only() {
105            return Err(FormError::ReadOnly(path.to_string()));
106        }
107
108        match ft {
109            FieldType::Text | FieldType::Button => {
110                self.get_mut(id).value = Some(FieldValue::Text(value.to_string()));
111            }
112            FieldType::Choice => {
113                self.get_mut(id).value = Some(FieldValue::StringArray(vec![value.to_string()]));
114            }
115            FieldType::Signature => unreachable!(),
116        }
117        Ok(())
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::flags::FieldFlags;
125    use crate::tree::{FieldNode, FieldTree, FieldType, FieldValue};
126
127    fn make_node(name: &str) -> FieldNode {
128        FieldNode {
129            partial_name: name.into(),
130            alternate_name: None,
131            mapping_name: None,
132            field_type: None,
133            flags: FieldFlags::empty(),
134            value: None,
135            default_value: None,
136            default_appearance: None,
137            quadding: None,
138            max_len: None,
139            options: vec![],
140            top_index: None,
141            rect: None,
142            appearance_state: None,
143            on_state: None,
144            page_index: None,
145            parent: None,
146            children: vec![],
147            object_id: None,
148            has_actions: false,
149            mk: None,
150            border_style: None,
151        }
152    }
153
154    fn sample_tree() -> FieldTree {
155        let mut tree = FieldTree::new();
156
157        // Root "form" node
158        let form_id = tree.alloc(make_node("form"));
159
160        // Text field: form.name = "Alice"
161        let mut name_node = make_node("name");
162        name_node.field_type = Some(FieldType::Text);
163        name_node.value = Some(FieldValue::Text("Alice".to_string()));
164        name_node.parent = Some(form_id);
165        let name_id = tree.alloc(name_node);
166
167        // Button field: form.agree = "true"
168        let mut agree_node = make_node("agree");
169        agree_node.field_type = Some(FieldType::Button);
170        agree_node.value = Some(FieldValue::Text("true".to_string()));
171        agree_node.parent = Some(form_id);
172        let agree_id = tree.alloc(agree_node);
173
174        // Choice field: form.country = ["NL"]
175        let mut country_node = make_node("country");
176        country_node.field_type = Some(FieldType::Choice);
177        country_node.value = Some(FieldValue::StringArray(vec!["NL".to_string()]));
178        country_node.parent = Some(form_id);
179        let country_id = tree.alloc(country_node);
180
181        // Signature field: form.sig (no value)
182        let mut sig_node = make_node("sig");
183        sig_node.field_type = Some(FieldType::Signature);
184        sig_node.parent = Some(form_id);
185        let sig_id = tree.alloc(sig_node);
186
187        // Empty text field: form.empty
188        let mut empty_node = make_node("empty");
189        empty_node.field_type = Some(FieldType::Text);
190        empty_node.parent = Some(form_id);
191        let empty_id = tree.alloc(empty_node);
192
193        // Read-only text field: form.locked
194        let mut locked_node = make_node("locked");
195        locked_node.field_type = Some(FieldType::Text);
196        locked_node.flags = FieldFlags::from_bits(1); // Bit 1 = ReadOnly
197        locked_node.value = Some(FieldValue::Text("frozen".to_string()));
198        locked_node.parent = Some(form_id);
199        let locked_id = tree.alloc(locked_node);
200
201        // Wire children
202        let form = tree.get_mut(form_id);
203        form.children = vec![name_id, agree_id, country_id, sig_id, empty_id, locked_id];
204
205        tree
206    }
207
208    #[test]
209    fn field_names_returns_all() {
210        let tree = sample_tree();
211        let names = tree.field_names();
212        assert_eq!(names.len(), 6);
213        assert!(names.contains(&"form.name".to_string()));
214        assert!(names.contains(&"form.agree".to_string()));
215    }
216
217    #[test]
218    fn get_value_existing_text() {
219        let tree = sample_tree();
220        assert_eq!(tree.get_value("form.name"), Some("Alice".to_string()));
221    }
222
223    #[test]
224    fn get_value_returns_none_for_unknown() {
225        let tree = sample_tree();
226        assert_eq!(tree.get_value("nonexistent"), None);
227    }
228
229    #[test]
230    fn get_value_returns_none_for_empty() {
231        let tree = sample_tree();
232        assert_eq!(tree.get_value("form.empty"), None);
233    }
234
235    #[test]
236    fn set_value_updates_text() {
237        let mut tree = sample_tree();
238        tree.set_value("form.name", "Bob").unwrap();
239        assert_eq!(tree.get_value("form.name"), Some("Bob".to_string()));
240    }
241
242    #[test]
243    fn set_value_unknown_field_errors() {
244        let mut tree = sample_tree();
245        let err = tree.set_value("nonexistent", "x").unwrap_err();
246        assert!(matches!(err, FormError::FieldNotFound(_)));
247    }
248
249    #[test]
250    fn set_value_signature_errors() {
251        let mut tree = sample_tree();
252        let err = tree.set_value("form.sig", "x").unwrap_err();
253        assert!(matches!(err, FormError::ReadOnly(_)));
254    }
255
256    #[test]
257    fn set_value_readonly_field_errors() {
258        let mut tree = sample_tree();
259        let err = tree.set_value("form.locked", "new").unwrap_err();
260        assert!(matches!(err, FormError::ReadOnly(_)));
261        // Value should remain unchanged.
262        assert_eq!(tree.get_value("form.locked"), Some("frozen".to_string()));
263    }
264
265    #[test]
266    fn form_type_is_acroform() {
267        let tree = sample_tree();
268        assert_eq!(tree.form_type(), FormKind::AcroForm);
269    }
270
271    #[test]
272    fn object_safe() {
273        let tree = sample_tree();
274        let _dyn_ref: &dyn FormAccess = &tree;
275        assert_eq!(_dyn_ref.form_type(), FormKind::AcroForm);
276    }
277}