use std::collections::HashMap;
use lopdf::Document;
use xfa_dom_resolver::data_dom::{DataDom, DataNode, DataNodeId};
use xfa_layout_engine::form::{
Access, FieldKind, FormNodeId, FormNodeType, FormTree, GroupKind, Presence,
};
use xfa_layout_engine::layout::{LayoutContent, LayoutDom, LayoutEngine, LayoutNode};
use crate::datasets_writeback::{self, DatasetsEdit, FormPacketEdit};
use crate::dynamic::apply_dynamic_scripts;
use crate::error::{Result, XfaError};
use crate::extract::{extract_xfa_from_bytes, XfaPackets};
use crate::flatten::{
apply_form_dom_presence, extract_embedded_images, inject_resolved_metrics,
resolve_template_fonts, XfaRenderingPolicy,
};
use crate::merger::FormMerger;
#[derive(Debug, Clone, Copy)]
pub enum XfaWriteValue<'a> {
Text(&'a str),
Checkbox(bool),
Radio(&'a str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum XfaFieldType {
Text,
Checkbox,
RadioGroup,
Button,
Dropdown,
Signature,
DateTime,
Numeric,
Password,
Image,
Barcode,
}
impl XfaFieldType {
fn from_kind(kind: FieldKind) -> Self {
match kind {
FieldKind::Text => XfaFieldType::Text,
FieldKind::Checkbox => XfaFieldType::Checkbox,
FieldKind::Radio => XfaFieldType::RadioGroup,
FieldKind::Button => XfaFieldType::Button,
FieldKind::Dropdown => XfaFieldType::Dropdown,
FieldKind::Signature => XfaFieldType::Signature,
FieldKind::DateTimePicker => XfaFieldType::DateTime,
FieldKind::NumericEdit => XfaFieldType::Numeric,
FieldKind::PasswordEdit => XfaFieldType::Password,
FieldKind::ImageEdit => XfaFieldType::Image,
FieldKind::Barcode => XfaFieldType::Barcode,
}
}
pub fn is_fillable(self) -> bool {
!matches!(
self,
XfaFieldType::Button
| XfaFieldType::Signature
| XfaFieldType::Image
| XfaFieldType::Barcode
)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct XfaRect {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
#[derive(Debug, Clone)]
pub struct XfaWidget {
pub page: usize,
pub rect: XfaRect,
pub on_value: Option<String>,
}
#[derive(Debug, Clone)]
pub struct XfaFieldOption {
pub display: String,
pub save: String,
}
#[derive(Debug, Clone)]
pub struct XfaFieldModel {
pub name: String,
pub som_path: String,
pub field_type: XfaFieldType,
pub value: String,
pub read_only: bool,
pub required: bool,
pub multiline: bool,
pub hidden: bool,
pub options: Vec<XfaFieldOption>,
pub on_value: Option<String>,
pub off_value: Option<String>,
pub page: Option<usize>,
pub rect: Option<XfaRect>,
pub widgets: Vec<XfaWidget>,
pub bound_to_data: bool,
pub bind_none: bool,
}
#[derive(Debug, Clone)]
pub struct XfaSetOutcome {
pub raw_value: String,
pub persisted_to_datasets: bool,
}
#[derive(Debug, Clone, Default)]
pub struct XfaWritebackReport {
pub datasets_spliced: bool,
pub form_packet_values_updated: usize,
}
struct FieldEntry {
node: FormNodeId,
members: Vec<(FormNodeId, String)>,
read_only: bool,
segments: Vec<(String, usize)>,
}
pub struct XfaSession {
pdf_bytes: Vec<u8>,
packets: XfaPackets,
data_dom: DataDom,
tree: FormTree,
layout: LayoutDom,
fields: Vec<FieldEntry>,
models: Vec<XfaFieldModel>,
by_name: HashMap<String, usize>,
dirty_data: Vec<DataNodeId>,
created_data: Vec<DataNodeId>,
form_packet_sync: Vec<FormPacketEdit>,
}
impl XfaSession {
pub fn open(pdf_bytes: &[u8]) -> Result<XfaSession> {
let packets = extract_xfa_from_bytes(pdf_bytes.to_vec())?;
let template_xml = packets
.template()
.ok_or_else(|| XfaError::PacketNotFound("template".to_string()))?
.to_string();
let data_dom = match packets.datasets() {
Some(ds) => DataDom::from_xml(ds).unwrap_or_else(|e| {
log::warn!("XfaSession: datasets packet unparseable ({e}); starting empty");
DataDom::new()
}),
None => DataDom::new(),
};
let image_files = match Document::load_mem(pdf_bytes) {
Ok(doc) => extract_embedded_images(&doc),
Err(_) => HashMap::new(),
};
let merger = FormMerger::new(&data_dom).with_image_files(image_files);
let (mut tree, root_id) = merger
.merge(&template_xml)
.map_err(|e| XfaError::ParseFailed(format!("template merge: {e}")))?;
let _ = apply_dynamic_scripts(&mut tree, root_id)?;
if let Some(form_xml) = packets.get_packet("form") {
let _ = apply_form_dom_presence(
&mut tree,
root_id,
form_xml,
XfaRenderingPolicy::SavedStateFaithful,
true,
);
apply_form_dom_access(&mut tree, root_id, form_xml);
}
let resolved = resolve_template_fonts(&template_xml, pdf_bytes);
inject_resolved_metrics(&mut tree, &resolved);
let layout = LayoutEngine::new(&tree)
.layout(root_id)
.map_err(|e| XfaError::LayoutFailed(e.to_string()))?;
let mut session = XfaSession {
pdf_bytes: pdf_bytes.to_vec(),
packets,
data_dom,
tree,
layout,
fields: Vec::new(),
models: Vec::new(),
by_name: HashMap::new(),
dirty_data: Vec::new(),
created_data: Vec::new(),
form_packet_sync: Vec::new(),
};
session.enumerate_fields(root_id);
Ok(session)
}
pub fn page_count(&self) -> usize {
self.layout.pages.len()
}
pub fn page_size(&self, page: usize) -> Option<(f64, f64)> {
self.layout.pages.get(page).map(|p| (p.width, p.height))
}
pub fn fields(&self) -> &[XfaFieldModel] {
&self.models
}
pub fn field(&self, name: &str) -> Option<&XfaFieldModel> {
self.by_name.get(name).map(|&i| &self.models[i])
}
pub fn set_value(&mut self, name: &str, value: XfaWriteValue<'_>) -> Result<XfaSetOutcome> {
let idx = *self
.by_name
.get(name)
.ok_or_else(|| XfaError::FieldNotFound(name.to_string()))?;
if self.fields[idx].read_only {
return Err(XfaError::FieldReadOnly(name.to_string()));
}
let field_type = self.models[idx].field_type;
if !field_type.is_fillable() {
return Err(XfaError::InvalidFieldValue {
name: name.to_string(),
reason: format!("{field_type:?} fields are not fillable"),
});
}
let raw = self.normalize_value(idx, value, name)?;
self.write_tree_value(idx, &raw);
let persisted = self.write_data_value(idx, &raw)?;
if self.packets.get_packet("form").is_some() {
let segments = self.fields[idx].segments.clone();
self.form_packet_sync
.retain(|e| !e.segments.starts_with(&segments));
if self.fields[idx].members.is_empty() {
self.form_packet_sync.push(FormPacketEdit {
segments,
value: raw.clone(),
insert_if_missing: true,
});
} else {
for (member, on) in self.fields[idx].members.clone() {
let mut member_segments = segments.clone();
member_segments.push((self.tree.get(member).name.clone(), 0));
let selected = on == raw;
self.form_packet_sync.push(FormPacketEdit {
segments: member_segments,
value: if selected { raw.clone() } else { String::new() },
insert_if_missing: selected,
});
}
self.form_packet_sync.push(FormPacketEdit {
segments,
value: raw.clone(),
insert_if_missing: false,
});
}
}
self.models[idx].value = raw.clone();
Ok(XfaSetOutcome {
raw_value: raw,
persisted_to_datasets: persisted,
})
}
pub fn save_to_bytes(&self) -> Result<Vec<u8>> {
let mut doc = Document::load_mem(&self.pdf_bytes)
.map_err(|e| XfaError::LoadFailed(format!("reload for writeback: {e}")))?;
self.write_into_document(&mut doc)?;
let mut out = Vec::new();
doc.save_to(&mut out)
.map_err(|e| XfaError::WritebackFailed(format!("PDF save: {e}")))?;
Ok(out)
}
pub fn write_into_document(&self, doc: &mut Document) -> Result<XfaWritebackReport> {
let mut report = XfaWritebackReport::default();
let edits = self.collect_datasets_edits();
let original_datasets = self.packets.datasets();
let new_datasets = match original_datasets {
Some(orig) => match datasets_writeback::splice_datasets(orig, &edits) {
Some(spliced) => {
report.datasets_spliced = true;
spliced
}
None => datasets_writeback::regenerate_datasets(orig, &self.data_dom),
},
None => datasets_writeback::regenerate_datasets("", &self.data_dom),
};
let new_form = self.packets.get_packet("form").and_then(|orig| {
if self.form_packet_sync.is_empty() {
return None;
}
let (xml, updated) =
datasets_writeback::splice_form_packet(orig, &self.form_packet_sync);
report.form_packet_values_updated = updated;
if updated > 0 {
Some(xml)
} else {
None
}
});
datasets_writeback::write_packets_into_pdf(doc, &new_datasets, new_form.as_deref())?;
Ok(report)
}
pub fn is_dirty(&self) -> bool {
!self.dirty_data.is_empty()
|| !self.created_data.is_empty()
|| !self.form_packet_sync.is_empty()
}
fn normalize_value(&self, idx: usize, value: XfaWriteValue<'_>, name: &str) -> Result<String> {
let model = &self.models[idx];
match (model.field_type, value) {
(XfaFieldType::Checkbox, XfaWriteValue::Checkbox(on)) => Ok(if on {
model.on_value.clone().unwrap_or_else(|| "1".to_string())
} else {
model.off_value.clone().unwrap_or_else(|| "0".to_string())
}),
(XfaFieldType::Checkbox, XfaWriteValue::Text(s)) => {
let on_v = model.on_value.clone().unwrap_or_else(|| "1".to_string());
let off_v = model.off_value.clone().unwrap_or_else(|| "0".to_string());
if s == on_v || s.eq_ignore_ascii_case("true") || s == "1" {
Ok(on_v)
} else if s == off_v || s.eq_ignore_ascii_case("false") || s == "0" || s.is_empty()
{
Ok(off_v)
} else {
Err(XfaError::InvalidFieldValue {
name: name.to_string(),
reason: format!("checkbox accepts {on_v:?} or {off_v:?}, got {s:?}"),
})
}
}
(XfaFieldType::RadioGroup, XfaWriteValue::Radio(s))
| (XfaFieldType::RadioGroup, XfaWriteValue::Text(s)) => {
let known = self.fields[idx].members.iter().any(|(_, on)| on == s);
if known {
Ok(s.to_string())
} else {
let opts: Vec<&str> = self.fields[idx]
.members
.iter()
.map(|(_, on)| on.as_str())
.collect();
Err(XfaError::InvalidFieldValue {
name: name.to_string(),
reason: format!("radio accepts one of {opts:?}, got {s:?}"),
})
}
}
(XfaFieldType::Dropdown, XfaWriteValue::Text(s)) => {
if model.options.is_empty() {
return Ok(s.to_string());
}
if let Some(opt) = model.options.iter().find(|o| o.save == s) {
return Ok(opt.save.clone());
}
if let Some(opt) = model.options.iter().find(|o| o.display == s) {
return Ok(opt.save.clone());
}
Ok(s.to_string())
}
(_, XfaWriteValue::Text(s)) => Ok(s.to_string()),
(ft, v) => Err(XfaError::InvalidFieldValue {
name: name.to_string(),
reason: format!("{v:?} is not assignable to a {ft:?} field"),
}),
}
}
fn write_tree_value(&mut self, idx: usize, raw: &str) {
let entry = &self.fields[idx];
if entry.members.is_empty() {
let id = entry.node;
if let FormNodeType::Field { .. } = self.tree.get(id).node_type {
self.tree.get_mut(id).node_type = FormNodeType::Field {
value: raw.to_string(),
};
}
} else {
let members = entry.members.clone();
for (member, on_value) in members {
let new_value = if on_value == raw {
raw.to_string()
} else {
String::new()
};
if let FormNodeType::Field { .. } = self.tree.get(member).node_type {
self.tree.get_mut(member).node_type = FormNodeType::Field { value: new_value };
}
}
}
}
fn write_data_value(&mut self, idx: usize, raw: &str) -> Result<bool> {
let entry_node = self.fields[idx].node;
if self.tree.meta(entry_node).data_bind_none {
return Ok(false);
}
if let Some(raw_id) = self.tree.meta(entry_node).bound_data_node {
let id = DataNodeId::from_raw(raw_id);
match self.data_dom.get(id) {
Some(DataNode::DataValue { .. }) => {
self.data_dom
.set_value(id, raw.to_string())
.map_err(|e| XfaError::WritebackFailed(e.to_string()))?;
if !self.dirty_data.contains(&id) {
self.dirty_data.push(id);
}
return Ok(true);
}
Some(DataNode::DataGroup { .. }) => {
}
None => return Ok(false),
}
}
let created = self.create_data_node_for(idx, raw)?;
Ok(created)
}
fn create_data_node_for(&mut self, idx: usize, raw: &str) -> Result<bool> {
let field_node = self.fields[idx].node;
let data_name = self.tree.get(field_node).name.clone();
if data_name.is_empty() {
return Ok(false);
}
let mut parent_group: Option<DataNodeId> = None;
if let Some(raw_id) = self.tree.meta(field_node).bound_data_node {
let id = DataNodeId::from_raw(raw_id);
if matches!(self.data_dom.get(id), Some(DataNode::DataGroup { .. })) {
parent_group = Some(id);
}
}
if parent_group.is_none() {
let mut cursor = self.parent_of(field_node);
while let Some(p) = cursor {
if let Some(raw_id) = self.tree.meta(p).bound_data_node {
let id = DataNodeId::from_raw(raw_id);
if matches!(self.data_dom.get(id), Some(DataNode::DataGroup { .. })) {
parent_group = Some(id);
break;
}
}
cursor = self.parent_of(p);
}
}
if parent_group.is_none() {
if self.tree.meta(field_node).data_bind_ref.is_none() {
parent_group = self.synthesize_group_chain(idx)?;
}
}
let Some(group) = parent_group else {
return Ok(false);
};
let existing = self
.data_dom
.children_by_name(group, &data_name)
.into_iter()
.find(|&c| matches!(self.data_dom.get(c), Some(DataNode::DataValue { .. })));
let id = match existing {
Some(c) => {
self.data_dom
.set_value(c, raw.to_string())
.map_err(|e| XfaError::WritebackFailed(e.to_string()))?;
if !self.dirty_data.contains(&c) {
self.dirty_data.push(c);
}
self.tree.meta_mut(self.fields[idx].node).bound_data_node = Some(c.as_raw());
return Ok(true);
}
None => self
.data_dom
.create_value(group, &data_name, raw)
.map_err(|e| XfaError::WritebackFailed(e.to_string()))?,
};
self.created_data.push(id);
self.tree.meta_mut(self.fields[idx].node).bound_data_node = Some(id.as_raw());
Ok(true)
}
fn synthesize_group_chain(&mut self, idx: usize) -> Result<Option<DataNodeId>> {
let segments = self.fields[idx].segments.clone();
if segments.len() < 2 {
if segments.is_empty() {
return Ok(None);
}
}
let root = match self.data_dom.root() {
Some(r)
if self
.data_dom
.get(r)
.map(DataNode::is_group)
.unwrap_or(false) =>
{
r
}
_ => {
let r = self.data_dom.alloc(DataNode::DataGroup {
name: "data".to_string(),
namespace: None,
children: Vec::new(),
is_record: false,
parent: None,
});
self.data_dom.set_root(r);
r
}
};
let mut cursor = root;
for (name, index) in &segments[..segments.len() - 1] {
let groups: Vec<DataNodeId> = self
.data_dom
.children_by_name(cursor, name)
.into_iter()
.filter(|&c| {
self.data_dom
.get(c)
.map(DataNode::is_group)
.unwrap_or(false)
})
.collect();
cursor = match groups.get(*index) {
Some(&g) => g,
None if *index == 0 => self
.data_dom
.create_group(cursor, name)
.map_err(|e| XfaError::WritebackFailed(e.to_string()))?,
None => return Ok(None),
};
}
Ok(Some(cursor))
}
fn parent_of(&self, node: FormNodeId) -> Option<FormNodeId> {
for (i, n) in self.tree.nodes.iter().enumerate() {
if n.children.contains(&node) {
return Some(FormNodeId(i));
}
}
None
}
fn collect_datasets_edits(&self) -> Vec<DatasetsEdit> {
let mut edits = Vec::new();
for &id in &self.dirty_data {
if let Some(path) = self.data_path_of(id) {
if let Ok(value) = self.data_dom.value(id) {
edits.push(DatasetsEdit::SetValue {
path,
value: value.to_string(),
});
}
}
}
for &id in &self.created_data {
let Some(node) = self.data_dom.get(id) else {
continue;
};
let name = node.name().to_string();
let Some(parent) = node.parent() else {
continue;
};
let Some(parent_path) = self.data_path_of(parent) else {
continue;
};
if let Ok(value) = self.data_dom.value(id) {
edits.push(DatasetsEdit::CreateValue {
parent_path,
name,
value: value.to_string(),
});
}
}
edits
}
fn data_path_of(&self, id: DataNodeId) -> Option<Vec<(String, usize)>> {
let root = self.data_dom.root()?;
let mut rev = Vec::new();
let mut cursor = Some(id);
while let Some(c) = cursor {
let node = self.data_dom.get(c)?;
let name = node.name().to_string();
let at_root = c == root;
let parent = if at_root { None } else { node.parent() };
let index = match parent {
Some(p) => {
let mut k = 0usize;
let mut found = None;
for &sib in self.data_dom.children(p) {
let Some(sn) = self.data_dom.get(sib) else {
continue;
};
if sn.name() != name {
continue;
}
if is_metadata_value(sn) {
continue;
}
if sib == c {
found = Some(k);
break;
}
k += 1;
}
found?
}
None => 0,
};
rev.push((name, index));
if at_root {
cursor = None;
} else {
cursor = Some(node.parent()?);
}
}
rev.reverse();
Some(rev)
}
fn enumerate_fields(&mut self, root: FormNodeId) {
let mut geometry: HashMap<usize, Vec<(usize, XfaRect)>> = HashMap::new();
for (page_idx, page) in self.layout.pages.iter().enumerate() {
for node in &page.nodes {
collect_geometry(node, 0.0, 0.0, page_idx, &mut geometry);
}
}
let mut entries = Vec::new();
let mut segments: Vec<(String, usize)> = Vec::new();
let mut sibling_counters: Vec<HashMap<String, usize>> = vec![HashMap::new()];
let root_access = self.tree.meta(root).access.unwrap_or(Access::Open);
for &child in &self.tree.get(root).children.clone() {
self.walk_enumerate(
child,
root_access,
&mut segments,
&mut sibling_counters,
&mut entries,
);
}
let mut models = Vec::new();
let mut by_name: HashMap<String, usize> = HashMap::new();
for entry in entries {
let model = self.build_model(&entry, &geometry);
let idx = models.len();
by_name.entry(model.name.clone()).or_insert(idx);
by_name.entry(model.som_path.clone()).or_insert(idx);
models.push(model);
self.fields.push(entry);
}
self.models = models;
self.by_name = by_name;
}
#[allow(clippy::too_many_arguments)]
fn walk_enumerate(
&self,
node_id: FormNodeId,
inherited_access: Access,
segments: &mut Vec<(String, usize)>,
sibling_counters: &mut Vec<HashMap<String, usize>>,
out: &mut Vec<FieldEntry>,
) {
let node = self.tree.get(node_id);
let meta = self.tree.meta(node_id);
let access = meta.access.unwrap_or(inherited_access);
let named = !node.name.is_empty();
if named {
let counter = sibling_counters
.last_mut()
.expect("sibling counter scope present");
let idx = *counter
.entry(node.name.clone())
.and_modify(|c| *c += 1)
.or_insert(0);
segments.push((node.name.clone(), idx));
}
let is_radio_group = matches!(node.node_type, FormNodeType::ExclGroup)
|| meta.group_kind == GroupKind::ExclusiveChoice;
let is_field = matches!(node.node_type, FormNodeType::Field { .. });
if is_radio_group {
let mut members = Vec::new();
for &child in &node.children {
if let FormNodeType::Field { .. } = self.tree.get(child).node_type {
let cmeta = self.tree.meta(child);
let on = cmeta
.item_value
.clone()
.or_else(|| cmeta.style.check_button_on_value.clone())
.unwrap_or_else(|| self.tree.get(child).name.clone());
members.push((child, on));
}
}
if !members.is_empty() {
out.push(FieldEntry {
node: node_id,
members,
read_only: access.denies_writes(),
segments: segments.clone(),
});
}
} else if is_field {
out.push(FieldEntry {
node: node_id,
members: Vec::new(),
read_only: access.denies_writes(),
segments: segments.clone(),
});
}
if !is_field && !is_radio_group {
sibling_counters.push(HashMap::new());
for &child in &node.children {
self.walk_enumerate(child, access, segments, sibling_counters, out);
}
sibling_counters.pop();
}
if named {
segments.pop();
}
}
fn build_model(
&self,
entry: &FieldEntry,
geometry: &HashMap<usize, Vec<(usize, XfaRect)>>,
) -> XfaFieldModel {
let node = self.tree.get(entry.node);
let meta = self.tree.meta(entry.node);
let name = segments_to_display_name(&entry.segments);
let som_path = segments_to_som_path(&entry.segments);
let field_type = if entry.members.is_empty() {
XfaFieldType::from_kind(meta.field_kind)
} else {
XfaFieldType::RadioGroup
};
let value = if entry.members.is_empty() {
match &node.node_type {
FormNodeType::Field { value } => value.clone(),
_ => String::new(),
}
} else {
entry
.members
.iter()
.find_map(|(m, on)| match &self.tree.get(*m).node_type {
FormNodeType::Field { value } if !value.is_empty() && value == on => {
Some(on.clone())
}
_ => None,
})
.unwrap_or_default()
};
let options = if !entry.members.is_empty() {
entry
.members
.iter()
.map(|(_, on)| XfaFieldOption {
display: on.clone(),
save: on.clone(),
})
.collect()
} else {
let displays = &meta.display_items;
let saves = &meta.save_items;
displays
.iter()
.enumerate()
.map(|(i, d)| XfaFieldOption {
display: d.clone(),
save: saves.get(i).cloned().unwrap_or_else(|| d.clone()),
})
.collect()
};
let on_value = meta
.style
.check_button_on_value
.clone()
.or_else(|| meta.item_value.clone());
let off_value = meta.style.check_button_off_value.clone();
let mut widgets: Vec<XfaWidget> = Vec::new();
if let Some(occ) = geometry.get(&entry.node.0) {
for (page, rect) in occ {
widgets.push(XfaWidget {
page: *page,
rect: *rect,
on_value: None,
});
}
}
for (member, on) in &entry.members {
if let Some(occ) = geometry.get(&member.0) {
for (page, rect) in occ {
widgets.push(XfaWidget {
page: *page,
rect: *rect,
on_value: Some(on.clone()),
});
}
}
}
widgets.sort_by(|a, b| {
a.page.cmp(&b.page).then(
a.rect
.y
.partial_cmp(&b.rect.y)
.unwrap_or(std::cmp::Ordering::Equal),
)
});
let page = widgets.first().map(|w| w.page);
let rect = widgets.first().map(|w| w.rect);
let bound = meta
.bound_data_node
.map(|raw| self.data_dom.get(DataNodeId::from_raw(raw)).is_some())
.unwrap_or(false);
XfaFieldModel {
name,
som_path,
field_type,
value,
read_only: entry.read_only,
required: meta.required,
multiline: meta.multiline,
hidden: meta.presence != Presence::Visible,
options,
on_value,
off_value,
page,
rect,
widgets,
bound_to_data: bound,
bind_none: meta.data_bind_none,
}
}
}
fn is_metadata_value(node: &DataNode) -> bool {
match node {
DataNode::DataValue { contains, .. } => {
*contains == xfa_dom_resolver::data_dom::DataContains::MetaData
}
DataNode::DataGroup { .. } => false,
}
}
fn segments_to_display_name(segments: &[(String, usize)]) -> String {
segments
.iter()
.map(|(n, i)| {
if *i == 0 {
n.clone()
} else {
format!("{n}[{i}]")
}
})
.collect::<Vec<_>>()
.join(".")
}
fn segments_to_som_path(segments: &[(String, usize)]) -> String {
segments
.iter()
.map(|(n, i)| format!("{n}[{i}]"))
.collect::<Vec<_>>()
.join(".")
}
fn collect_geometry(
node: &LayoutNode,
parent_x: f64,
parent_y: f64,
page_idx: usize,
out: &mut HashMap<usize, Vec<(usize, XfaRect)>>,
) {
let abs_x = node.rect.x + parent_x;
let abs_y = node.rect.y + parent_y;
let record = matches!(node.content, LayoutContent::Field { .. })
|| matches!(
node.content,
LayoutContent::WrappedText {
from_field: true,
..
}
);
if record {
out.entry(node.form_node.0).or_default().push((
page_idx,
XfaRect {
x: abs_x,
y: abs_y,
width: node.rect.width,
height: node.rect.height,
},
));
}
for child in &node.children {
collect_geometry(child, abs_x, abs_y, page_idx, out);
}
}
fn apply_form_dom_access(tree: &mut FormTree, root_id: FormNodeId, form_xml: &str) {
let Ok(doc) = roxmltree::Document::parse(form_xml) else {
return;
};
fn matches_node(tree: &FormTree, fid: FormNodeId, tag: &str, name: &str) -> bool {
let node = tree.get(fid);
match (tag, &node.node_type) {
("subform", FormNodeType::Subform | FormNodeType::Area) => node.name == name,
("exclGroup", FormNodeType::ExclGroup) => node.name == name,
("field", FormNodeType::Field { .. } | FormNodeType::ExclGroup) => node.name == name,
_ => false,
}
}
fn walk(tree: &mut FormTree, fid: FormNodeId, xml: roxmltree::Node<'_, '_>) {
if let Some(access) = xml.attribute("access") {
tree.meta_mut(fid).access = Some(Access::parse(access));
}
let xml_children: Vec<roxmltree::Node<'_, '_>> = xml
.children()
.filter(|c| {
c.is_element() && matches!(c.tag_name().name(), "subform" | "field" | "exclGroup")
})
.collect();
if xml_children.is_empty() {
return;
}
let tree_children = tree.get(fid).children.clone();
let mut used = vec![false; tree_children.len()];
for xc in xml_children {
let tag = xc.tag_name().name();
let name = xc.attribute("name").unwrap_or("");
if name.is_empty() {
continue;
}
for (i, &cid) in tree_children.iter().enumerate() {
if used[i] || !matches_node(tree, cid, tag, name) {
continue;
}
used[i] = true;
walk(tree, cid, xc);
break;
}
}
}
let Some(form_root) = doc.root().children().find(|c| c.is_element()) else {
return;
};
let root_children = tree.get(root_id).children.clone();
let packet_subforms: Vec<roxmltree::Node<'_, '_>> = form_root
.children()
.filter(|c| c.is_element() && c.tag_name().name() == "subform")
.collect();
for xc in packet_subforms {
let name = xc.attribute("name").unwrap_or("");
for &cid in &root_children {
if matches_node(tree, cid, "subform", name) {
walk(tree, cid, xc);
break;
}
}
}
}