Skip to main content

pdfluent/
xfa.rs

1//! XFA form field model — public DTOs for the Phase-1 XFA fill API.
2//!
3//! XFA forms (XML Forms Architecture, used by LiveCycle/AEM government and
4//! enterprise forms) store their field tree and values in XML packets, not
5//! in AcroForm dictionaries. [`PdfDocument::xfa_form_model`] enumerates the
6//! currently layouted fields of such a document and
7//! [`PdfDocument::set_xfa_field_value`] fills them, persisting values into
8//! the `datasets` packet so Adobe Acrobat/Reader reopens the form with them.
9//!
10//! [`PdfDocument::xfa_form_model`]: crate::PdfDocument::xfa_form_model
11//! [`PdfDocument::set_xfa_field_value`]: crate::PdfDocument::set_xfa_field_value
12
13// Copyright (c) 2026 Innovation Trigger B.V.
14//
15// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
16// the PDFluent Commercial Licence. See the LICENSE file in this repository --
17// that file travels with the copy you received, which a URL does not.
18
19use crate::error::Error;
20
21/// The type of an XFA form field.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[non_exhaustive]
24pub enum XfaFieldType {
25    /// Single-line or multiline text edit.
26    Text,
27    /// Standalone checkbox.
28    Checkbox,
29    /// Exclusive radio group (one logical field per `exclGroup`).
30    RadioGroup,
31    /// Push button (not fillable).
32    Button,
33    /// Dropdown / choice list.
34    Dropdown,
35    /// Signature field (not fillable in this phase).
36    Signature,
37    /// Date/time picker (accepts text values).
38    DateTime,
39    /// Numeric edit (accepts text values).
40    Numeric,
41    /// Password edit.
42    Password,
43    /// Image edit (not fillable in this phase).
44    Image,
45    /// Barcode (not fillable).
46    Barcode,
47}
48
49/// Rectangle in page space: points, top-left origin (XFA convention — `y`
50/// grows downward from the top of the page).
51#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct XfaRect {
53    /// Left edge.
54    pub x: f64,
55    /// Top edge.
56    pub y: f64,
57    /// Width.
58    pub width: f64,
59    /// Height.
60    pub height: f64,
61}
62
63/// One layouted widget occurrence of a field.
64#[derive(Debug, Clone)]
65pub struct XfaWidget {
66    /// 0-based page index in the XFA layout.
67    pub page: usize,
68    /// Widget rectangle.
69    pub rect: XfaRect,
70    /// For radio groups: the on-value this member widget asserts.
71    pub on_value: Option<String>,
72}
73
74/// A selectable option of a dropdown / choice field.
75#[derive(Debug, Clone)]
76pub struct XfaFieldOption {
77    /// User-visible text.
78    pub display: String,
79    /// Persisted save value.
80    pub save: String,
81}
82
83/// One logical XFA form field.
84#[derive(Debug, Clone)]
85pub struct XfaField {
86    /// Fully-qualified dotted name (Adobe display-SOM style, `[n]` omitted
87    /// when 0), e.g. `form1.applicant.name`. Accepted by
88    /// [`set_xfa_field_value`](crate::PdfDocument::set_xfa_field_value).
89    pub name: String,
90    /// Fully-qualified SOM path with explicit indices,
91    /// e.g. `form1[0].applicant[0].name[0]`. Also accepted as a field name.
92    pub som_path: String,
93    /// Field type.
94    pub field_type: XfaFieldType,
95    /// Current value (radio groups: the selected member's on-value).
96    pub value: String,
97    /// Effective read-only state (the field or an ancestor container sets
98    /// `access="readOnly" | "protected" | "nonInteractive"`, in the template
99    /// or in the saved form state).
100    pub read_only: bool,
101    /// Mandatory field (`<validate nullTest="error">`).
102    pub required: bool,
103    /// Multiline text edit.
104    pub multiline: bool,
105    /// Hidden / invisible / inactive presence.
106    pub hidden: bool,
107    /// Dropdown options; for radio groups one option per member.
108    pub options: Vec<XfaFieldOption>,
109    /// Checkbox on-value, when known.
110    pub on_value: Option<String>,
111    /// Checkbox off-value, when known.
112    pub off_value: Option<String>,
113    /// First page the field appears on (0-based in the XFA layout); `None`
114    /// when the field is not part of the current layout (e.g. hidden, or on
115    /// a master page).
116    pub page: Option<usize>,
117    /// Rectangle of the first widget occurrence.
118    pub rect: Option<XfaRect>,
119    /// All widget occurrences (radio groups: one per member).
120    pub widgets: Vec<XfaWidget>,
121    /// Whether a datasets node currently backs this field. Unbound fields
122    /// (except `bind="none"`) get their data node created on first write.
123    pub bound_to_data: bool,
124    /// `<bind match="none">` — values of such fields are not persisted in
125    /// the datasets packet (Adobe keeps them in the saved form state only).
126    pub bind_none: bool,
127}
128
129/// The XFA form model of a document: layout page count plus the enumerated
130/// fields, in document order.
131#[derive(Debug, Clone)]
132pub struct XfaFormModel {
133    /// Number of pages in the XFA layout. May differ from
134    /// [`page_count`](crate::PdfDocument::page_count) of the PDF shell —
135    /// dynamic XFA PDFs often ship a 1-page "please update your reader"
136    /// shell while the real form lays out to N pages.
137    pub page_count: usize,
138    /// The fields.
139    pub fields: Vec<XfaField>,
140}
141
142/// A value to write into an XFA field.
143#[derive(Debug, Clone, Copy)]
144pub enum XfaFieldValue<'a> {
145    /// Text-like fields (text, multiline, numeric, date/time, password,
146    /// dropdown — display or save values both resolve).
147    Text(&'a str),
148    /// Checkbox state.
149    Checkbox(bool),
150    /// Radio group selection: the on-value of the member to select.
151    Radio(&'a str),
152}
153
154/// Outcome of a successful [`set_xfa_field_value`] call.
155///
156/// [`set_xfa_field_value`]: crate::PdfDocument::set_xfa_field_value
157#[derive(Debug, Clone)]
158pub struct XfaSetOutcome {
159    /// The normalized value written (e.g. a dropdown display value resolved
160    /// to its save value, a checkbox `true` to its on-value).
161    pub raw_value: String,
162    /// The value was written through to the datasets packet and persists
163    /// across save/reopen. `false` only for `bind="none"` fields (or
164    /// unnamed/unanchorable nodes) — those values update the in-memory
165    /// model and the saved form state but not the datasets.
166    pub persisted_to_datasets: bool,
167}
168
169pub(crate) fn field_type_from_engine(t: pdf_engine::xfa::XfaFieldType) -> XfaFieldType {
170    use pdf_engine::xfa::XfaFieldType as E;
171    match t {
172        E::Text => XfaFieldType::Text,
173        E::Checkbox => XfaFieldType::Checkbox,
174        E::RadioGroup => XfaFieldType::RadioGroup,
175        E::Button => XfaFieldType::Button,
176        E::Dropdown => XfaFieldType::Dropdown,
177        E::Signature => XfaFieldType::Signature,
178        E::DateTime => XfaFieldType::DateTime,
179        E::Numeric => XfaFieldType::Numeric,
180        E::Password => XfaFieldType::Password,
181        E::Image => XfaFieldType::Image,
182        E::Barcode => XfaFieldType::Barcode,
183    }
184}
185
186pub(crate) fn field_from_engine(f: &pdf_engine::xfa::XfaFieldModel) -> XfaField {
187    let rect = |r: &pdf_engine::xfa::XfaRect| XfaRect {
188        x: r.x,
189        y: r.y,
190        width: r.width,
191        height: r.height,
192    };
193    XfaField {
194        name: f.name.clone(),
195        som_path: f.som_path.clone(),
196        field_type: field_type_from_engine(f.field_type),
197        value: f.value.clone(),
198        read_only: f.read_only,
199        required: f.required,
200        multiline: f.multiline,
201        hidden: f.hidden,
202        options: f
203            .options
204            .iter()
205            .map(|o| XfaFieldOption {
206                display: o.display.clone(),
207                save: o.save.clone(),
208            })
209            .collect(),
210        on_value: f.on_value.clone(),
211        off_value: f.off_value.clone(),
212        page: f.page,
213        rect: f.rect.as_ref().map(rect),
214        widgets: f
215            .widgets
216            .iter()
217            .map(|w| XfaWidget {
218                page: w.page,
219                rect: rect(&w.rect),
220                on_value: w.on_value.clone(),
221            })
222            .collect(),
223        bound_to_data: f.bound_to_data,
224        bind_none: f.bind_none,
225    }
226}
227
228/// Map session errors onto the public error surface: caller-addressable
229/// conditions become [`Error::Unsupported`] with a descriptive message,
230/// engine faults become [`Error::Internal`].
231pub(crate) fn map_xfa_err(e: pdf_engine::xfa::XfaError) -> Error {
232    use pdf_engine::xfa::XfaError as X;
233    match e {
234        X::PacketNotFound(_) => {
235            Error::Unsupported("document has no XFA form (template packet missing)".to_string())
236        }
237        X::FieldNotFound(name) => Error::Unsupported(format!("XFA field not found: {name}")),
238        X::FieldReadOnly(name) => Error::Unsupported(format!("XFA field is read-only: {name}")),
239        X::InvalidFieldValue { name, reason } => {
240            Error::Unsupported(format!("invalid value for XFA field {name}: {reason}"))
241        }
242        other => crate::error::internal_error(other.to_string()),
243    }
244}