Skip to main content

pdf_xfa/
session.rs

1//! `XfaSession` — parse-once interactive fill session over an XFA PDF.
2//!
3//! Phase 1 SDK foundation: enumerate the currently visible/layouted fields,
4//! set values (with write-through to the bound data nodes), and save a PDF
5//! whose datasets packet carries the filled values so Adobe Acrobat/Reader
6//! reopens the form with them.
7//!
8//! Explicit non-goals at this phase (see the UEA gap analysis): no
9//! change/click event execution, no dynamic re-layout/reflow after a value
10//! write, no instanceManager add/remove, no form-packet *structural* writes.
11//!
12//! ```no_run
13//! use pdf_xfa::session::{XfaSession, XfaWriteValue};
14//!
15//! let bytes = std::fs::read("form.pdf").unwrap();
16//! let mut session = XfaSession::open(&bytes).unwrap();
17//! println!("pages: {}", session.page_count());
18//! for f in session.fields() {
19//!     println!("{} = {:?}", f.name, f.value);
20//! }
21//! session
22//!     .set_value("form1.applicant.name", XfaWriteValue::Text("Alice"))
23//!     .unwrap();
24//! let filled = session.save_to_bytes().unwrap();
25//! std::fs::write("filled.pdf", filled).unwrap();
26//! ```
27
28// Copyright (c) 2026 Innovation Trigger B.V.
29//
30// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
31// the PDFluent Commercial Licence. See the LICENSE file in this repository --
32// that file travels with the copy you received, which a URL does not.
33
34use std::collections::HashMap;
35
36use lopdf::Document;
37use xfa_dom_resolver::data_dom::{DataDom, DataNode, DataNodeId};
38use xfa_layout_engine::form::{
39    Access, FieldKind, FormNodeId, FormNodeType, FormTree, GroupKind, Presence,
40};
41use xfa_layout_engine::layout::{LayoutContent, LayoutDom, LayoutEngine, LayoutNode};
42
43use crate::datasets_writeback::{self, DatasetsEdit, FormPacketEdit};
44use crate::dynamic::apply_dynamic_scripts;
45use crate::error::{Result, XfaError};
46use crate::extract::{extract_xfa_from_bytes, XfaPackets};
47use crate::flatten::{
48    apply_form_dom_presence, extract_embedded_images, inject_resolved_metrics,
49    resolve_template_fonts, XfaRenderingPolicy,
50};
51use crate::merger::FormMerger;
52
53/// A value to write into an XFA field.
54#[derive(Debug, Clone, Copy)]
55pub enum XfaWriteValue<'a> {
56    /// Text-like fields: text, multiline text, numeric, date/time, password,
57    /// and choice lists (pass the save value or the display value).
58    Text(&'a str),
59    /// Checkbox: `true` selects the on-value, `false` the off-value.
60    Checkbox(bool),
61    /// Radio group (exclGroup): the on-value of the member to select.
62    Radio(&'a str),
63}
64
65/// Public field type, mirroring [`FieldKind`] without leaking the
66/// layout-engine type into the SDK surface.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum XfaFieldType {
69    /// Single-line or multiline text edit.
70    Text,
71    /// Standalone checkbox.
72    Checkbox,
73    /// Exclusive radio group (exclGroup with checkButton members).
74    RadioGroup,
75    /// Push button (not fillable).
76    Button,
77    /// Dropdown / choice list.
78    Dropdown,
79    /// Signature field (not fillable in Phase 1).
80    Signature,
81    /// Date/time picker (accepts text in Phase 1).
82    DateTime,
83    /// Numeric edit (accepts text in Phase 1).
84    Numeric,
85    /// Password edit.
86    Password,
87    /// Image edit (not fillable in Phase 1).
88    Image,
89    /// Barcode (not fillable).
90    Barcode,
91}
92
93impl XfaFieldType {
94    fn from_kind(kind: FieldKind) -> Self {
95        match kind {
96            FieldKind::Text => XfaFieldType::Text,
97            FieldKind::Checkbox => XfaFieldType::Checkbox,
98            FieldKind::Radio => XfaFieldType::RadioGroup,
99            FieldKind::Button => XfaFieldType::Button,
100            FieldKind::Dropdown => XfaFieldType::Dropdown,
101            FieldKind::Signature => XfaFieldType::Signature,
102            FieldKind::DateTimePicker => XfaFieldType::DateTime,
103            FieldKind::NumericEdit => XfaFieldType::Numeric,
104            FieldKind::PasswordEdit => XfaFieldType::Password,
105            FieldKind::ImageEdit => XfaFieldType::Image,
106            FieldKind::Barcode => XfaFieldType::Barcode,
107        }
108    }
109
110    /// Whether [`XfaSession::set_value`] accepts writes for this type.
111    pub fn is_fillable(self) -> bool {
112        !matches!(
113            self,
114            XfaFieldType::Button
115                | XfaFieldType::Signature
116                | XfaFieldType::Image
117                | XfaFieldType::Barcode
118        )
119    }
120}
121
122/// Axis-aligned rectangle in page space, points, top-left origin (XFA
123/// coordinate convention: y grows downward from the top of the page).
124#[derive(Debug, Clone, Copy, PartialEq)]
125pub struct XfaRect {
126    /// Left edge.
127    pub x: f64,
128    /// Top edge.
129    pub y: f64,
130    /// Width.
131    pub width: f64,
132    /// Height.
133    pub height: f64,
134}
135
136/// One layouted widget occurrence of a field.
137#[derive(Debug, Clone)]
138pub struct XfaWidget {
139    /// 0-based page index in the layout result.
140    pub page: usize,
141    /// Widget rectangle (page space, top-left origin, points).
142    pub rect: XfaRect,
143    /// For radio groups: the on-value asserted by this member widget.
144    pub on_value: Option<String>,
145}
146
147/// A selectable option of a dropdown / list field.
148#[derive(Debug, Clone)]
149pub struct XfaFieldOption {
150    /// User-visible text.
151    pub display: String,
152    /// Persisted save value (equals `display` when the template defines no
153    /// separate save items).
154    pub save: String,
155}
156
157/// SDK-level model of one fillable XFA form field, derived from the merged
158/// form tree and the current layout.
159#[derive(Debug, Clone)]
160pub struct XfaFieldModel {
161    /// Fully-qualified dotted name with `[n]` indices omitted when 0
162    /// (Adobe display-SOM style), e.g. `form1.applicant.name` or
163    /// `form1.rows.row[2].amount`.
164    pub name: String,
165    /// Fully-qualified SOM path with explicit `[n]` on every segment,
166    /// e.g. `form1[0].rows[0].row[2].amount[0]`.
167    pub som_path: String,
168    /// Field type.
169    pub field_type: XfaFieldType,
170    /// Current value (the form-tree raw value; radio groups report the
171    /// selected member's on-value, empty when none selected).
172    pub value: String,
173    /// Effective read-only state: the field or an ancestor container sets
174    /// `access="readOnly" | "protected" | "nonInteractive"`.
175    pub read_only: bool,
176    /// Mandatory field (`<validate nullTest="error">`).
177    pub required: bool,
178    /// Multiline text edit (`<ui><textEdit multiLine="1">`).
179    pub multiline: bool,
180    /// Hidden/invisible/inactive via XFA presence (template, saved form
181    /// state, or script-derived).
182    pub hidden: bool,
183    /// Dropdown/list options. For radio groups: one option per member
184    /// (display = member on-value).
185    pub options: Vec<XfaFieldOption>,
186    /// Checkbox on-value (`<items>` / checkButton binding), when known.
187    pub on_value: Option<String>,
188    /// Checkbox off-value, when known.
189    pub off_value: Option<String>,
190    /// First page this field appears on (0-based), `None` when the field is
191    /// not part of the current layout (e.g. hidden).
192    pub page: Option<usize>,
193    /// Rectangle of the first widget occurrence.
194    pub rect: Option<XfaRect>,
195    /// All layouted widget occurrences (radio groups: one per member).
196    pub widgets: Vec<XfaWidget>,
197    /// Whether the field has a bound data node in the datasets DOM. Values
198    /// written to unbound fields are persisted by creating the data node on
199    /// demand (default binding), except for `bind match="none"` fields.
200    pub bound_to_data: bool,
201    /// `<bind match="none">` — the field deliberately does not participate
202    /// in data binding; Adobe persists such values only in the form packet.
203    pub bind_none: bool,
204}
205
206/// Outcome of a successful [`XfaSession::set_value`] call.
207#[derive(Debug, Clone)]
208pub struct XfaSetOutcome {
209    /// The normalized value written to the form tree (e.g. the resolved
210    /// save value of a dropdown, or a checkbox on/off value).
211    pub raw_value: String,
212    /// The value was written through to a datasets node (existing or
213    /// created on demand) and will be persisted by [`XfaSession::save_to_bytes`].
214    pub persisted_to_datasets: bool,
215}
216
217/// Report from a save/writeback.
218#[derive(Debug, Clone, Default)]
219pub struct XfaWritebackReport {
220    /// Datasets packet rewritten by surgical splice (original XML preserved
221    /// except for the changed values). `false` = lossless splice failed and
222    /// the data section was regenerated from the data DOM instead.
223    pub datasets_spliced: bool,
224    /// Number of `<value>` updates applied to the saved form packet (kept in
225    /// sync so Adobe's `restoreState` does not resurrect stale values).
226    pub form_packet_values_updated: usize,
227}
228
229struct FieldEntry {
230    /// The field node (for radio groups: the exclGroup node).
231    node: FormNodeId,
232    /// Member fields of a radio group, with their on-values.
233    members: Vec<(FormNodeId, String)>,
234    /// Effective access at enumeration time.
235    read_only: bool,
236    /// Path of named segments from the root, with same-name sibling indices.
237    segments: Vec<(String, usize)>,
238}
239
240/// Parse-once session over a single XFA PDF.
241///
242/// Holds the extracted packets, the parsed datasets DOM, the merged form
243/// tree, and the layout result. Field enumeration and value writes operate
244/// on the cached state; no re-parse or re-layout happens per call (Phase 1:
245/// value writes do not reflow the layout).
246pub struct XfaSession {
247    pdf_bytes: Vec<u8>,
248    packets: XfaPackets,
249    data_dom: DataDom,
250    tree: FormTree,
251    layout: LayoutDom,
252    fields: Vec<FieldEntry>,
253    models: Vec<XfaFieldModel>,
254    by_name: HashMap<String, usize>,
255    /// Data nodes whose values changed (existing nodes).
256    dirty_data: Vec<DataNodeId>,
257    /// Data nodes created on demand: (id, parent path at creation time).
258    created_data: Vec<DataNodeId>,
259    /// Form-packet value syncs to apply at save.
260    form_packet_sync: Vec<FormPacketEdit>,
261}
262
263impl XfaSession {
264    /// Open a session over the given PDF bytes.
265    ///
266    /// Mirrors the flatten pipeline's Extract → Bind → (static scripts) →
267    /// saved-form-state → Layout stages, then caches everything.
268    ///
269    /// Returns [`XfaError::PacketNotFound`] when the PDF has no XFA template
270    /// packet (i.e. it is not an XFA form).
271    pub fn open(pdf_bytes: &[u8]) -> Result<XfaSession> {
272        let packets = extract_xfa_from_bytes(pdf_bytes.to_vec())?;
273        let template_xml = packets
274            .template()
275            .ok_or_else(|| XfaError::PacketNotFound("template".to_string()))?
276            .to_string();
277
278        // A malformed or empty datasets packet must not block the session —
279        // the form is still fillable; the writeback regenerates a canonical
280        // packet from scratch (Adobe tolerates both).
281        let data_dom = match packets.datasets() {
282            Some(ds) => DataDom::from_xml(ds).unwrap_or_else(|e| {
283                log::warn!("XfaSession: datasets packet unparseable ({e}); starting empty");
284                DataDom::new()
285            }),
286            None => DataDom::new(),
287        };
288
289        let image_files = match Document::load_mem(pdf_bytes) {
290            Ok(doc) => extract_embedded_images(&doc),
291            Err(_) => HashMap::new(),
292        };
293
294        let merger = FormMerger::new(&data_dom).with_image_files(image_files);
295        let (mut tree, root_id) = merger
296            .merge(&template_xml)
297            .map_err(|e| XfaError::ParseFailed(format!("template merge: {e}")))?;
298
299        // Static-analysis script pass only (BestEffortStatic): presence
300        // toggles and FormCalc the engine can resolve without a JS runtime.
301        // Phase 1 deliberately runs no change/click events.
302        let _ = apply_dynamic_scripts(&mut tree, root_id)?;
303
304        // Saved form state (Adobe-Reader-saved PDFs): presence overrides and
305        // saved field values, exactly as the flatten pipeline applies them.
306        if let Some(form_xml) = packets.get_packet("form") {
307            let _ = apply_form_dom_presence(
308                &mut tree,
309                root_id,
310                form_xml,
311                XfaRenderingPolicy::SavedStateFaithful,
312                true,
313            );
314            apply_form_dom_access(&mut tree, root_id, form_xml);
315        }
316
317        // Resolved font metrics make the layout geometry match the flatten
318        // output (widths drive wrapping and therefore field positions).
319        let resolved = resolve_template_fonts(&template_xml, pdf_bytes);
320        inject_resolved_metrics(&mut tree, &resolved);
321
322        let layout = LayoutEngine::new(&tree)
323            .layout(root_id)
324            .map_err(|e| XfaError::LayoutFailed(e.to_string()))?;
325
326        let mut session = XfaSession {
327            pdf_bytes: pdf_bytes.to_vec(),
328            packets,
329            data_dom,
330            tree,
331            layout,
332            fields: Vec::new(),
333            models: Vec::new(),
334            by_name: HashMap::new(),
335            dirty_data: Vec::new(),
336            created_data: Vec::new(),
337            form_packet_sync: Vec::new(),
338        };
339        session.enumerate_fields(root_id);
340        Ok(session)
341    }
342
343    /// Number of pages in the cached layout result.
344    pub fn page_count(&self) -> usize {
345        self.layout.pages.len()
346    }
347
348    /// Page dimensions `(width, height)` in points for a 0-based page index.
349    pub fn page_size(&self, page: usize) -> Option<(f64, f64)> {
350        self.layout.pages.get(page).map(|p| (p.width, p.height))
351    }
352
353    /// The enumerated field models, in document order.
354    pub fn fields(&self) -> &[XfaFieldModel] {
355        &self.models
356    }
357
358    /// Look up one field model by fully-qualified name. Accepts both the
359    /// display name (`a.b[2].c`) and the explicit SOM path (`a[0].b[2].c[0]`).
360    pub fn field(&self, name: &str) -> Option<&XfaFieldModel> {
361        self.by_name.get(name).map(|&i| &self.models[i])
362    }
363
364    /// Set a field's value.
365    ///
366    /// Updates the form-tree raw value, writes through to the bound
367    /// datasets node (creating it on demand for default-bound fields), and
368    /// records the change for [`save_to_bytes`](Self::save_to_bytes).
369    ///
370    /// Rejects writes to read-only fields ([`XfaError::FieldReadOnly`]) and
371    /// to non-fillable kinds (buttons, signatures, …). No change/click
372    /// scripts run and the layout is not recomputed (Phase 1).
373    pub fn set_value(&mut self, name: &str, value: XfaWriteValue<'_>) -> Result<XfaSetOutcome> {
374        let idx = *self
375            .by_name
376            .get(name)
377            .ok_or_else(|| XfaError::FieldNotFound(name.to_string()))?;
378
379        if self.fields[idx].read_only {
380            return Err(XfaError::FieldReadOnly(name.to_string()));
381        }
382        let field_type = self.models[idx].field_type;
383        if !field_type.is_fillable() {
384            return Err(XfaError::InvalidFieldValue {
385                name: name.to_string(),
386                reason: format!("{field_type:?} fields are not fillable"),
387            });
388        }
389
390        let raw = self.normalize_value(idx, value, name)?;
391
392        // 1. Form-tree raw value.
393        self.write_tree_value(idx, &raw);
394
395        // 2. Datasets write-through.
396        let persisted = self.write_data_value(idx, &raw)?;
397
398        // 3. Saved-form-packet value sync (recorded; spliced at save time).
399        if self.packets.get_packet("form").is_some() {
400            let segments = self.fields[idx].segments.clone();
401            // Drop earlier edits for this field (group prefix covers the
402            // radio-member edits too).
403            self.form_packet_sync
404                .retain(|e| !e.segments.starts_with(&segments));
405            if self.fields[idx].members.is_empty() {
406                self.form_packet_sync.push(FormPacketEdit {
407                    segments,
408                    value: raw.clone(),
409                    insert_if_missing: true,
410                });
411            } else {
412                // Radio group: the saved form DOM stores the selection on
413                // the member fields — the selected member carries the
414                // on-value, deselected members are cleared. Also refresh a
415                // group-level <value> when the producer wrote one.
416                for (member, on) in self.fields[idx].members.clone() {
417                    let mut member_segments = segments.clone();
418                    member_segments.push((self.tree.get(member).name.clone(), 0));
419                    let selected = on == raw;
420                    self.form_packet_sync.push(FormPacketEdit {
421                        segments: member_segments,
422                        value: if selected { raw.clone() } else { String::new() },
423                        insert_if_missing: selected,
424                    });
425                }
426                self.form_packet_sync.push(FormPacketEdit {
427                    segments,
428                    value: raw.clone(),
429                    insert_if_missing: false,
430                });
431            }
432        }
433
434        // 4. Refresh the public model.
435        self.models[idx].value = raw.clone();
436
437        Ok(XfaSetOutcome {
438            raw_value: raw,
439            persisted_to_datasets: persisted,
440        })
441    }
442
443    /// Serialize the updated datasets (and form-packet value syncs) into a
444    /// copy of the original PDF and return the new bytes.
445    pub fn save_to_bytes(&self) -> Result<Vec<u8>> {
446        let mut doc = Document::load_mem(&self.pdf_bytes)
447            .map_err(|e| XfaError::LoadFailed(format!("reload for writeback: {e}")))?;
448        self.write_into_document(&mut doc)?;
449        let mut out = Vec::new();
450        doc.save_to(&mut out)
451            .map_err(|e| XfaError::WritebackFailed(format!("PDF save: {e}")))?;
452        Ok(out)
453    }
454
455    /// Apply the datasets/form-packet writeback to an already-loaded
456    /// `lopdf::Document` (the SDK facade path: the caller owns the document
457    /// and serializes it itself).
458    pub fn write_into_document(&self, doc: &mut Document) -> Result<XfaWritebackReport> {
459        let mut report = XfaWritebackReport::default();
460
461        // Build the new datasets packet: surgical splice of the original
462        // packet text, falling back to data-section regeneration.
463        let edits = self.collect_datasets_edits();
464        let original_datasets = self.packets.datasets();
465        let new_datasets = match original_datasets {
466            Some(orig) => match datasets_writeback::splice_datasets(orig, &edits) {
467                Some(spliced) => {
468                    report.datasets_spliced = true;
469                    spliced
470                }
471                None => datasets_writeback::regenerate_datasets(orig, &self.data_dom),
472            },
473            None => datasets_writeback::regenerate_datasets("", &self.data_dom),
474        };
475
476        // Saved form packet: keep recorded field values in sync.
477        let new_form = self.packets.get_packet("form").and_then(|orig| {
478            if self.form_packet_sync.is_empty() {
479                return None;
480            }
481            let (xml, updated) =
482                datasets_writeback::splice_form_packet(orig, &self.form_packet_sync);
483            report.form_packet_values_updated = updated;
484            if updated > 0 {
485                Some(xml)
486            } else {
487                None
488            }
489        });
490
491        datasets_writeback::write_packets_into_pdf(doc, &new_datasets, new_form.as_deref())?;
492        Ok(report)
493    }
494
495    /// True when any value was changed since the session opened.
496    pub fn is_dirty(&self) -> bool {
497        !self.dirty_data.is_empty()
498            || !self.created_data.is_empty()
499            || !self.form_packet_sync.is_empty()
500    }
501
502    // ── internals ──────────────────────────────────────────────────────
503
504    fn normalize_value(&self, idx: usize, value: XfaWriteValue<'_>, name: &str) -> Result<String> {
505        let model = &self.models[idx];
506        match (model.field_type, value) {
507            (XfaFieldType::Checkbox, XfaWriteValue::Checkbox(on)) => Ok(if on {
508                model.on_value.clone().unwrap_or_else(|| "1".to_string())
509            } else {
510                model.off_value.clone().unwrap_or_else(|| "0".to_string())
511            }),
512            (XfaFieldType::Checkbox, XfaWriteValue::Text(s)) => {
513                let on_v = model.on_value.clone().unwrap_or_else(|| "1".to_string());
514                let off_v = model.off_value.clone().unwrap_or_else(|| "0".to_string());
515                if s == on_v || s.eq_ignore_ascii_case("true") || s == "1" {
516                    Ok(on_v)
517                } else if s == off_v || s.eq_ignore_ascii_case("false") || s == "0" || s.is_empty()
518                {
519                    Ok(off_v)
520                } else {
521                    Err(XfaError::InvalidFieldValue {
522                        name: name.to_string(),
523                        reason: format!("checkbox accepts {on_v:?} or {off_v:?}, got {s:?}"),
524                    })
525                }
526            }
527            (XfaFieldType::RadioGroup, XfaWriteValue::Radio(s))
528            | (XfaFieldType::RadioGroup, XfaWriteValue::Text(s)) => {
529                let known = self.fields[idx].members.iter().any(|(_, on)| on == s);
530                if known {
531                    Ok(s.to_string())
532                } else {
533                    let opts: Vec<&str> = self.fields[idx]
534                        .members
535                        .iter()
536                        .map(|(_, on)| on.as_str())
537                        .collect();
538                    Err(XfaError::InvalidFieldValue {
539                        name: name.to_string(),
540                        reason: format!("radio accepts one of {opts:?}, got {s:?}"),
541                    })
542                }
543            }
544            (XfaFieldType::Dropdown, XfaWriteValue::Text(s)) => {
545                if model.options.is_empty() {
546                    return Ok(s.to_string());
547                }
548                if let Some(opt) = model.options.iter().find(|o| o.save == s) {
549                    return Ok(opt.save.clone());
550                }
551                if let Some(opt) = model.options.iter().find(|o| o.display == s) {
552                    return Ok(opt.save.clone());
553                }
554                // XFA choice lists may allow free entry; the template parser
555                // does not currently surface `open="..."`, so accept unknown
556                // values rather than over-rejecting.
557                Ok(s.to_string())
558            }
559            (_, XfaWriteValue::Text(s)) => Ok(s.to_string()),
560            (ft, v) => Err(XfaError::InvalidFieldValue {
561                name: name.to_string(),
562                reason: format!("{v:?} is not assignable to a {ft:?} field"),
563            }),
564        }
565    }
566
567    fn write_tree_value(&mut self, idx: usize, raw: &str) {
568        let entry = &self.fields[idx];
569        if entry.members.is_empty() {
570            let id = entry.node;
571            if let FormNodeType::Field { .. } = self.tree.get(id).node_type {
572                self.tree.get_mut(id).node_type = FormNodeType::Field {
573                    value: raw.to_string(),
574                };
575            }
576        } else {
577            // exclGroup: the member whose on-value matches gets the value,
578            // every other member is cleared (mirrors the merger's
579            // apply_exclusive_choice_value semantics).
580            let members = entry.members.clone();
581            for (member, on_value) in members {
582                let new_value = if on_value == raw {
583                    raw.to_string()
584                } else {
585                    String::new()
586                };
587                if let FormNodeType::Field { .. } = self.tree.get(member).node_type {
588                    self.tree.get_mut(member).node_type = FormNodeType::Field { value: new_value };
589                }
590            }
591        }
592    }
593
594    /// Write through to the datasets DOM. Returns `true` when the value is
595    /// now held by a data node (existing or created).
596    fn write_data_value(&mut self, idx: usize, raw: &str) -> Result<bool> {
597        let entry_node = self.fields[idx].node;
598        if self.tree.meta(entry_node).data_bind_none {
599            // `bind match="none"` — by design not persisted in datasets.
600            return Ok(false);
601        }
602
603        if let Some(raw_id) = self.tree.meta(entry_node).bound_data_node {
604            let id = DataNodeId::from_raw(raw_id);
605            match self.data_dom.get(id) {
606                Some(DataNode::DataValue { .. }) => {
607                    self.data_dom
608                        .set_value(id, raw.to_string())
609                        .map_err(|e| XfaError::WritebackFailed(e.to_string()))?;
610                    if !self.dirty_data.contains(&id) {
611                        self.dirty_data.push(id);
612                    }
613                    return Ok(true);
614                }
615                Some(DataNode::DataGroup { .. }) => {
616                    // Container-bound (e.g. an exclGroup bound to a group):
617                    // fall through to create/find a value child below.
618                }
619                None => return Ok(false),
620            }
621        }
622
623        // No directly bound value node: create one on demand under the
624        // nearest bound ancestor (XFA default binding by name).
625        let created = self.create_data_node_for(idx, raw)?;
626        Ok(created)
627    }
628
629    fn create_data_node_for(&mut self, idx: usize, raw: &str) -> Result<bool> {
630        let field_node = self.fields[idx].node;
631        let data_name = self.tree.get(field_node).name.clone();
632        if data_name.is_empty() {
633            return Ok(false);
634        }
635
636        // Find the nearest ancestor (or the node itself when group-bound)
637        // with a bound data *group*.
638        let mut parent_group: Option<DataNodeId> = None;
639        // The node itself may be bound to a group (exclGroup case).
640        if let Some(raw_id) = self.tree.meta(field_node).bound_data_node {
641            let id = DataNodeId::from_raw(raw_id);
642            if matches!(self.data_dom.get(id), Some(DataNode::DataGroup { .. })) {
643                // The group IS the field's data home: a value child named
644                // like the field is the XFA §4.4.5 group-value convention —
645                // but Designer datasets normally store the exclGroup value
646                // directly on the group-named *value* node. Prefer an
647                // existing value child named like the field, else write a
648                // value child with the field's name.
649                parent_group = Some(id);
650            }
651        }
652        if parent_group.is_none() {
653            let mut cursor = self.parent_of(field_node);
654            while let Some(p) = cursor {
655                if let Some(raw_id) = self.tree.meta(p).bound_data_node {
656                    let id = DataNodeId::from_raw(raw_id);
657                    if matches!(self.data_dom.get(id), Some(DataNode::DataGroup { .. })) {
658                        parent_group = Some(id);
659                        break;
660                    }
661                }
662                cursor = self.parent_of(p);
663            }
664        }
665        if parent_group.is_none() {
666            // No bound ancestor at all (empty or absent datasets): synthesize
667            // the group chain from the data root along the field's named
668            // path — the same tree Adobe Reader creates on first save.
669            // Explicit `<bind ref>` fields are skipped: their data home is
670            // not the name chain.
671            if self.tree.meta(field_node).data_bind_ref.is_none() {
672                parent_group = self.synthesize_group_chain(idx)?;
673            }
674        }
675        let Some(group) = parent_group else {
676            return Ok(false);
677        };
678
679        // Reuse an existing same-name child value node when present.
680        let existing = self
681            .data_dom
682            .children_by_name(group, &data_name)
683            .into_iter()
684            .find(|&c| matches!(self.data_dom.get(c), Some(DataNode::DataValue { .. })));
685        let id = match existing {
686            Some(c) => {
687                self.data_dom
688                    .set_value(c, raw.to_string())
689                    .map_err(|e| XfaError::WritebackFailed(e.to_string()))?;
690                if !self.dirty_data.contains(&c) {
691                    self.dirty_data.push(c);
692                }
693                self.tree.meta_mut(self.fields[idx].node).bound_data_node = Some(c.as_raw());
694                return Ok(true);
695            }
696            None => self
697                .data_dom
698                .create_value(group, &data_name, raw)
699                .map_err(|e| XfaError::WritebackFailed(e.to_string()))?,
700        };
701        self.created_data.push(id);
702        self.tree.meta_mut(self.fields[idx].node).bound_data_node = Some(id.as_raw());
703        Ok(true)
704    }
705
706    /// Create (or find) the data-group chain `root → seg[0] → … → seg[n-2]`
707    /// for the field at `idx`, creating a `<data>` root when the DataDom is
708    /// empty. Returns the deepest group, or `None` when an indexed segment
709    /// (`name[i>0]`) is missing — repeated-instance synthesis is out of
710    /// Phase-1 scope.
711    fn synthesize_group_chain(&mut self, idx: usize) -> Result<Option<DataNodeId>> {
712        let segments = self.fields[idx].segments.clone();
713        if segments.len() < 2 {
714            // A root-level field still needs a root group to live under.
715            if segments.is_empty() {
716                return Ok(None);
717            }
718        }
719
720        let root = match self.data_dom.root() {
721            Some(r)
722                if self
723                    .data_dom
724                    .get(r)
725                    .map(DataNode::is_group)
726                    .unwrap_or(false) =>
727            {
728                r
729            }
730            _ => {
731                let r = self.data_dom.alloc(DataNode::DataGroup {
732                    name: "data".to_string(),
733                    namespace: None,
734                    children: Vec::new(),
735                    is_record: false,
736                    parent: None,
737                });
738                self.data_dom.set_root(r);
739                r
740            }
741        };
742
743        let mut cursor = root;
744        for (name, index) in &segments[..segments.len() - 1] {
745            let groups: Vec<DataNodeId> = self
746                .data_dom
747                .children_by_name(cursor, name)
748                .into_iter()
749                .filter(|&c| {
750                    self.data_dom
751                        .get(c)
752                        .map(DataNode::is_group)
753                        .unwrap_or(false)
754                })
755                .collect();
756            cursor = match groups.get(*index) {
757                Some(&g) => g,
758                None if *index == 0 => self
759                    .data_dom
760                    .create_group(cursor, name)
761                    .map_err(|e| XfaError::WritebackFailed(e.to_string()))?,
762                None => return Ok(None),
763            };
764        }
765        Ok(Some(cursor))
766    }
767
768    fn parent_of(&self, node: FormNodeId) -> Option<FormNodeId> {
769        // FormTree stores no parent links; derive lazily from the arena.
770        for (i, n) in self.tree.nodes.iter().enumerate() {
771            if n.children.contains(&node) {
772                return Some(FormNodeId(i));
773            }
774        }
775        None
776    }
777
778    fn collect_datasets_edits(&self) -> Vec<DatasetsEdit> {
779        let mut edits = Vec::new();
780        for &id in &self.dirty_data {
781            if let Some(path) = self.data_path_of(id) {
782                if let Ok(value) = self.data_dom.value(id) {
783                    edits.push(DatasetsEdit::SetValue {
784                        path,
785                        value: value.to_string(),
786                    });
787                }
788            }
789        }
790        for &id in &self.created_data {
791            let Some(node) = self.data_dom.get(id) else {
792                continue;
793            };
794            let name = node.name().to_string();
795            let Some(parent) = node.parent() else {
796                continue;
797            };
798            let Some(parent_path) = self.data_path_of(parent) else {
799                continue;
800            };
801            if let Ok(value) = self.data_dom.value(id) {
802                edits.push(DatasetsEdit::CreateValue {
803                    parent_path,
804                    name,
805                    value: value.to_string(),
806                });
807            }
808        }
809        edits
810    }
811
812    /// Path of `(name, index-among-same-name-element-siblings)` segments
813    /// from the data root down to `id`. The root itself is segment 0; the
814    /// upward walk stops there (the parse may keep wrapper parents like
815    /// `<xfa:datasets>` linked above the effective root).
816    fn data_path_of(&self, id: DataNodeId) -> Option<Vec<(String, usize)>> {
817        let root = self.data_dom.root()?;
818        let mut rev = Vec::new();
819        let mut cursor = Some(id);
820        while let Some(c) = cursor {
821            let node = self.data_dom.get(c)?;
822            let name = node.name().to_string();
823            let at_root = c == root;
824            let parent = if at_root { None } else { node.parent() };
825            let index = match parent {
826                Some(p) => {
827                    // Index among same-name siblings, skipping nodes the
828                    // splicer cannot see as XML elements (attribute-derived
829                    // metadata values).
830                    let mut k = 0usize;
831                    let mut found = None;
832                    for &sib in self.data_dom.children(p) {
833                        let Some(sn) = self.data_dom.get(sib) else {
834                            continue;
835                        };
836                        if sn.name() != name {
837                            continue;
838                        }
839                        if is_metadata_value(sn) {
840                            continue;
841                        }
842                        if sib == c {
843                            found = Some(k);
844                            break;
845                        }
846                        k += 1;
847                    }
848                    found?
849                }
850                None => 0,
851            };
852            rev.push((name, index));
853            if at_root {
854                cursor = None;
855            } else {
856                // A node that never reaches the effective root (e.g. it
857                // lives under a wrapper sibling) cannot be spliced by a
858                // root-relative path.
859                cursor = Some(node.parent()?);
860            }
861        }
862        rev.reverse();
863        Some(rev)
864    }
865
866    // ── enumeration ─────────────────────────────────────────────────────
867
868    fn enumerate_fields(&mut self, root: FormNodeId) {
869        // Geometry first: form node → widget occurrences.
870        let mut geometry: HashMap<usize, Vec<(usize, XfaRect)>> = HashMap::new();
871        for (page_idx, page) in self.layout.pages.iter().enumerate() {
872            for node in &page.nodes {
873                collect_geometry(node, 0.0, 0.0, page_idx, &mut geometry);
874            }
875        }
876
877        let mut entries = Vec::new();
878        let mut segments: Vec<(String, usize)> = Vec::new();
879        let mut sibling_counters: Vec<HashMap<String, usize>> = vec![HashMap::new()];
880        // The merged tree's root is a synthetic container (named "root");
881        // Adobe SOM names start at the template's root subform, so walk the
882        // root's children instead of the root itself.
883        let root_access = self.tree.meta(root).access.unwrap_or(Access::Open);
884        for &child in &self.tree.get(root).children.clone() {
885            self.walk_enumerate(
886                child,
887                root_access,
888                &mut segments,
889                &mut sibling_counters,
890                &mut entries,
891            );
892        }
893
894        let mut models = Vec::new();
895        let mut by_name: HashMap<String, usize> = HashMap::new();
896        for entry in entries {
897            let model = self.build_model(&entry, &geometry);
898            let idx = models.len();
899            // First registration wins; later duplicates stay addressable via
900            // their explicit SOM path.
901            by_name.entry(model.name.clone()).or_insert(idx);
902            by_name.entry(model.som_path.clone()).or_insert(idx);
903            models.push(model);
904            self.fields.push(entry);
905        }
906        self.models = models;
907        self.by_name = by_name;
908    }
909
910    #[allow(clippy::too_many_arguments)]
911    fn walk_enumerate(
912        &self,
913        node_id: FormNodeId,
914        inherited_access: Access,
915        segments: &mut Vec<(String, usize)>,
916        sibling_counters: &mut Vec<HashMap<String, usize>>,
917        out: &mut Vec<FieldEntry>,
918    ) {
919        let node = self.tree.get(node_id);
920        let meta = self.tree.meta(node_id);
921        let access = meta.access.unwrap_or(inherited_access);
922
923        let named = !node.name.is_empty();
924        if named {
925            let counter = sibling_counters
926                .last_mut()
927                .expect("sibling counter scope present");
928            let idx = *counter
929                .entry(node.name.clone())
930                .and_modify(|c| *c += 1)
931                .or_insert(0);
932            segments.push((node.name.clone(), idx));
933        }
934
935        let is_radio_group = matches!(node.node_type, FormNodeType::ExclGroup)
936            || meta.group_kind == GroupKind::ExclusiveChoice;
937        let is_field = matches!(node.node_type, FormNodeType::Field { .. });
938
939        if is_radio_group {
940            let mut members = Vec::new();
941            for &child in &node.children {
942                if let FormNodeType::Field { .. } = self.tree.get(child).node_type {
943                    let cmeta = self.tree.meta(child);
944                    let on = cmeta
945                        .item_value
946                        .clone()
947                        .or_else(|| cmeta.style.check_button_on_value.clone())
948                        .unwrap_or_else(|| self.tree.get(child).name.clone());
949                    members.push((child, on));
950                }
951            }
952            if !members.is_empty() {
953                out.push(FieldEntry {
954                    node: node_id,
955                    members,
956                    read_only: access.denies_writes(),
957                    segments: segments.clone(),
958                });
959            }
960        } else if is_field {
961            out.push(FieldEntry {
962                node: node_id,
963                members: Vec::new(),
964                read_only: access.denies_writes(),
965                segments: segments.clone(),
966            });
967        }
968
969        // Recurse into containers (not into exclGroup members or field
970        // internals — members are folded into the group entry above).
971        if !is_field && !is_radio_group {
972            sibling_counters.push(HashMap::new());
973            for &child in &node.children {
974                self.walk_enumerate(child, access, segments, sibling_counters, out);
975            }
976            sibling_counters.pop();
977        }
978
979        if named {
980            segments.pop();
981        }
982    }
983
984    fn build_model(
985        &self,
986        entry: &FieldEntry,
987        geometry: &HashMap<usize, Vec<(usize, XfaRect)>>,
988    ) -> XfaFieldModel {
989        let node = self.tree.get(entry.node);
990        let meta = self.tree.meta(entry.node);
991
992        let name = segments_to_display_name(&entry.segments);
993        let som_path = segments_to_som_path(&entry.segments);
994
995        let field_type = if entry.members.is_empty() {
996            XfaFieldType::from_kind(meta.field_kind)
997        } else {
998            XfaFieldType::RadioGroup
999        };
1000
1001        // Value: field raw value, or selected member's on-value for groups.
1002        let value = if entry.members.is_empty() {
1003            match &node.node_type {
1004                FormNodeType::Field { value } => value.clone(),
1005                _ => String::new(),
1006            }
1007        } else {
1008            entry
1009                .members
1010                .iter()
1011                .find_map(|(m, on)| match &self.tree.get(*m).node_type {
1012                    FormNodeType::Field { value } if !value.is_empty() && value == on => {
1013                        Some(on.clone())
1014                    }
1015                    _ => None,
1016                })
1017                .unwrap_or_default()
1018        };
1019
1020        // Options.
1021        let options = if !entry.members.is_empty() {
1022            entry
1023                .members
1024                .iter()
1025                .map(|(_, on)| XfaFieldOption {
1026                    display: on.clone(),
1027                    save: on.clone(),
1028                })
1029                .collect()
1030        } else {
1031            let displays = &meta.display_items;
1032            let saves = &meta.save_items;
1033            displays
1034                .iter()
1035                .enumerate()
1036                .map(|(i, d)| XfaFieldOption {
1037                    display: d.clone(),
1038                    save: saves.get(i).cloned().unwrap_or_else(|| d.clone()),
1039                })
1040                .collect()
1041        };
1042
1043        let on_value = meta
1044            .style
1045            .check_button_on_value
1046            .clone()
1047            .or_else(|| meta.item_value.clone());
1048        let off_value = meta.style.check_button_off_value.clone();
1049
1050        // Geometry: the group node's own rect plus member rects.
1051        let mut widgets: Vec<XfaWidget> = Vec::new();
1052        if let Some(occ) = geometry.get(&entry.node.0) {
1053            for (page, rect) in occ {
1054                widgets.push(XfaWidget {
1055                    page: *page,
1056                    rect: *rect,
1057                    on_value: None,
1058                });
1059            }
1060        }
1061        for (member, on) in &entry.members {
1062            if let Some(occ) = geometry.get(&member.0) {
1063                for (page, rect) in occ {
1064                    widgets.push(XfaWidget {
1065                        page: *page,
1066                        rect: *rect,
1067                        on_value: Some(on.clone()),
1068                    });
1069                }
1070            }
1071        }
1072        widgets.sort_by(|a, b| {
1073            a.page.cmp(&b.page).then(
1074                a.rect
1075                    .y
1076                    .partial_cmp(&b.rect.y)
1077                    .unwrap_or(std::cmp::Ordering::Equal),
1078            )
1079        });
1080        let page = widgets.first().map(|w| w.page);
1081        let rect = widgets.first().map(|w| w.rect);
1082
1083        let bound = meta
1084            .bound_data_node
1085            .map(|raw| self.data_dom.get(DataNodeId::from_raw(raw)).is_some())
1086            .unwrap_or(false);
1087
1088        XfaFieldModel {
1089            name,
1090            som_path,
1091            field_type,
1092            value,
1093            read_only: entry.read_only,
1094            required: meta.required,
1095            multiline: meta.multiline,
1096            hidden: meta.presence != Presence::Visible,
1097            options,
1098            on_value,
1099            off_value,
1100            page,
1101            rect,
1102            widgets,
1103            bound_to_data: bound,
1104            bind_none: meta.data_bind_none,
1105        }
1106    }
1107}
1108
1109fn is_metadata_value(node: &DataNode) -> bool {
1110    match node {
1111        DataNode::DataValue { contains, .. } => {
1112            *contains == xfa_dom_resolver::data_dom::DataContains::MetaData
1113        }
1114        DataNode::DataGroup { .. } => false,
1115    }
1116}
1117
1118fn segments_to_display_name(segments: &[(String, usize)]) -> String {
1119    segments
1120        .iter()
1121        .map(|(n, i)| {
1122            if *i == 0 {
1123                n.clone()
1124            } else {
1125                format!("{n}[{i}]")
1126            }
1127        })
1128        .collect::<Vec<_>>()
1129        .join(".")
1130}
1131
1132fn segments_to_som_path(segments: &[(String, usize)]) -> String {
1133    segments
1134        .iter()
1135        .map(|(n, i)| format!("{n}[{i}]"))
1136        .collect::<Vec<_>>()
1137        .join(".")
1138}
1139
1140/// Accumulate absolute widget rectangles per form node.
1141///
1142/// Layout coordinates are parent-relative (the render bridge accumulates
1143/// them the same way); rectangles are reported in XFA page space (top-left
1144/// origin, points).
1145fn collect_geometry(
1146    node: &LayoutNode,
1147    parent_x: f64,
1148    parent_y: f64,
1149    page_idx: usize,
1150    out: &mut HashMap<usize, Vec<(usize, XfaRect)>>,
1151) {
1152    let abs_x = node.rect.x + parent_x;
1153    let abs_y = node.rect.y + parent_y;
1154
1155    let record = matches!(node.content, LayoutContent::Field { .. })
1156        || matches!(
1157            node.content,
1158            LayoutContent::WrappedText {
1159                from_field: true,
1160                ..
1161            }
1162        );
1163    if record {
1164        out.entry(node.form_node.0).or_default().push((
1165            page_idx,
1166            XfaRect {
1167                x: abs_x,
1168                y: abs_y,
1169                width: node.rect.width,
1170                height: node.rect.height,
1171            },
1172        ));
1173    }
1174
1175    for child in &node.children {
1176        collect_geometry(child, abs_x, abs_y, page_idx, out);
1177    }
1178}
1179
1180/// Apply `access="…"` attributes from the saved form packet onto matching
1181/// form-tree nodes (session-only pass; the flatten pipeline ignores access).
1182///
1183/// Adobe Reader records interactive locks (e.g. fields disabled after
1184/// submission) as `access="readOnly"` in the form packet. Without this pass
1185/// the session would let callers write fields Acrobat refuses to edit.
1186fn apply_form_dom_access(tree: &mut FormTree, root_id: FormNodeId, form_xml: &str) {
1187    let Ok(doc) = roxmltree::Document::parse(form_xml) else {
1188        return;
1189    };
1190
1191    fn matches_node(tree: &FormTree, fid: FormNodeId, tag: &str, name: &str) -> bool {
1192        let node = tree.get(fid);
1193        match (tag, &node.node_type) {
1194            ("subform", FormNodeType::Subform | FormNodeType::Area) => node.name == name,
1195            ("exclGroup", FormNodeType::ExclGroup) => node.name == name,
1196            // Some producers serialize exclGroups as <field> in the form DOM.
1197            ("field", FormNodeType::Field { .. } | FormNodeType::ExclGroup) => node.name == name,
1198            _ => false,
1199        }
1200    }
1201
1202    fn walk(tree: &mut FormTree, fid: FormNodeId, xml: roxmltree::Node<'_, '_>) {
1203        if let Some(access) = xml.attribute("access") {
1204            tree.meta_mut(fid).access = Some(Access::parse(access));
1205        }
1206
1207        let xml_children: Vec<roxmltree::Node<'_, '_>> = xml
1208            .children()
1209            .filter(|c| {
1210                c.is_element() && matches!(c.tag_name().name(), "subform" | "field" | "exclGroup")
1211            })
1212            .collect();
1213        if xml_children.is_empty() {
1214            return;
1215        }
1216
1217        let tree_children = tree.get(fid).children.clone();
1218        let mut used = vec![false; tree_children.len()];
1219        for xc in xml_children {
1220            let tag = xc.tag_name().name();
1221            let name = xc.attribute("name").unwrap_or("");
1222            if name.is_empty() {
1223                continue;
1224            }
1225            for (i, &cid) in tree_children.iter().enumerate() {
1226                if used[i] || !matches_node(tree, cid, tag, name) {
1227                    continue;
1228                }
1229                used[i] = true;
1230                walk(tree, cid, xc);
1231                break;
1232            }
1233        }
1234    }
1235
1236    // The form packet root is <form> (sometimes namespaced); its element
1237    // children mirror the template root subform's children. The template
1238    // FormTree root is the <template> wrapper whose first child is the root
1239    // subform — align by descending one level when the names don't match.
1240    let Some(form_root) = doc.root().children().find(|c| c.is_element()) else {
1241        return;
1242    };
1243    // Try matching the root subform: the form packet's first subform child
1244    // corresponds to the tree root's subform child of the same name.
1245    let root_children = tree.get(root_id).children.clone();
1246    let packet_subforms: Vec<roxmltree::Node<'_, '_>> = form_root
1247        .children()
1248        .filter(|c| c.is_element() && c.tag_name().name() == "subform")
1249        .collect();
1250    for xc in packet_subforms {
1251        let name = xc.attribute("name").unwrap_or("");
1252        for &cid in &root_children {
1253            if matches_node(tree, cid, "subform", name) {
1254                walk(tree, cid, xc);
1255                break;
1256            }
1257        }
1258    }
1259}