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}