#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergeMode {
ConsumeData,
MatchTemplate,
}
pub fn detect_merge_mode(template_xml: &str) -> MergeMode {
if template_xml.contains("matchTemplate") {
return MergeMode::MatchTemplate;
}
MergeMode::ConsumeData
}
use crate::error::{Result, XfaError};
use roxmltree::Node;
use xfa_dom_resolver::data_dom::{DataDom, DataNodeId};
use xfa_dom_resolver::som::resolve_data_path;
use xfa_layout_engine::form::{
AnchorType, ContentArea, DrawContent, EventScript, FieldKind, FormNode, FormNodeId,
FormNodeMeta, FormNodeStyle, FormNodeType, FormTree, GroupKind, Occur, Presence, RichTextSpan,
ScriptLanguage,
};
use xfa_layout_engine::text::{FontFamily, FontMetrics};
use xfa_layout_engine::types::{
BoxModel, Caption, CaptionPlacement, Insets, LayoutStrategy, Measurement, TextAlign,
VerticalAlign,
};
pub struct FormMerger<'a> {
data_dom: &'a DataDom,
form_tree: FormTree,
image_files: std::collections::HashMap<String, Vec<u8>>,
invisible_bindings_ignored: usize,
}
fn area_layout(elem: Node<'_, '_>) -> LayoutStrategy {
if elem.tag_name().name() == "area" && attr(elem, "layout").is_none() {
let has_positioned_child = elem.children().filter(|n| n.is_element()).any(|child| {
attr(child, "x")
.and_then(parse_dim)
.is_some_and(|v| v > 0.0)
|| attr(child, "y")
.and_then(parse_dim)
.is_some_and(|v| v > 0.0)
});
if has_positioned_child {
LayoutStrategy::Positioned
} else {
LayoutStrategy::TopToBottom
}
} else {
parse_layout_attr(elem)
}
}
impl<'a> FormMerger<'a> {
pub fn new(data_dom: &'a DataDom) -> Self {
Self {
data_dom,
form_tree: FormTree::new(),
image_files: std::collections::HashMap::new(),
invisible_bindings_ignored: 0,
}
}
pub fn with_image_files(mut self, files: std::collections::HashMap<String, Vec<u8>>) -> Self {
self.image_files = files;
self
}
pub fn merge(mut self, template_xml: &str) -> Result<(FormTree, FormNodeId)> {
let mode = detect_merge_mode(template_xml);
log::debug!(
"XFA merge: mode={:?}, {} data nodes",
mode,
self.data_dom.len()
);
let doc = roxmltree::Document::parse(template_xml)
.map_err(|e| XfaError::ParseFailed(format!("template XML parse error: {e}")))?;
let root_elem = doc.root_element();
let template_elem = if root_elem.tag_name().name() == "template" {
root_elem
} else {
find_first_child_by_name(root_elem, "template").ok_or_else(|| {
XfaError::PacketNotFound("no <template> element found".to_string())
})?
};
if mode == MergeMode::MatchTemplate {
self.apply_match_template_bindings(template_elem);
}
self.collect_variables_scripts(template_elem);
let (root_id, _trailing) = self.parse_node(template_elem, None, true)?;
crate::adobe_compat::emit_invisible_binding_summary(self.invisible_bindings_ignored);
Ok((self.form_tree, root_id))
}
fn collect_variables_scripts(&mut self, template_elem: Node<'_, '_>) {
let Some(root_subform) = find_first_child_by_name(template_elem, "subform") else {
return;
};
self.collect_variables_scripts_recursive(root_subform, None);
}
fn collect_variables_scripts_recursive(
&mut self,
subform: Node<'_, '_>,
subform_scope: Option<String>,
) {
for child in subform.children().filter(|n| n.is_element()) {
match child.tag_name().name() {
"variables" => {
for var_child in child.children().filter(|n| n.is_element()) {
if var_child.tag_name().name() != "script" {
continue;
}
let Some(name) = attr(var_child, "name") else {
continue;
};
let body: String = var_child
.children()
.filter(|n| n.is_text())
.filter_map(|n| n.text())
.collect::<String>();
if body.trim().is_empty() {
continue;
}
self.form_tree.variables_scripts.push((
subform_scope.clone(),
name.to_string(),
body,
));
}
}
"subform" | "area" | "exclGroup" => {
let child_scope = attr(child, "name")
.filter(|n| !n.is_empty())
.map(|n| n.to_string());
self.collect_variables_scripts_recursive(child, child_scope);
}
_ => {}
}
}
}
fn apply_match_template_bindings(&mut self, _template_elem: roxmltree::Node<'_, '_>) {
}
fn parse_node(
&mut self,
elem: Node<'_, '_>,
data_context: Option<DataNodeId>,
is_root: bool,
) -> Result<(FormNodeId, (bool, Option<String>))> {
let tag = elem.tag_name().name();
let (node, trailing_info) = match tag {
"template" => {
let mut n = self.blank_node("root", FormNodeType::Root);
n.layout = LayoutStrategy::TopToBottom;
let ti = self.add_children(&mut n, elem, data_context)?;
(n, ti)
}
"subform" | "exclGroup" | "area" => {
let name = attr(elem, "name").unwrap_or("").to_string();
let layout = area_layout(elem);
let bm = parse_box_model(elem);
let occur = parse_occur(elem);
self.build_subform_instance(elem, data_context, is_root, occur, name, layout, bm)?
}
"field" => (self.parse_field(elem, data_context)?, (false, None)),
"draw" => (self.parse_draw(elem, data_context)?, (false, None)),
"pageSet" => (self.parse_page_set(elem, data_context)?, (false, None)),
"pageArea" => (self.parse_page_area(elem, data_context)?, (false, None)),
_ => {
let mut n = self.blank_node(tag, FormNodeType::Subform);
let ti = self.add_children(&mut n, elem, data_context)?;
(n, ti)
}
};
if tag == "exclGroup" {
self.apply_exclusive_choice_value(elem, data_context, &node.children);
}
let mut meta = parse_node_meta(elem);
if let Some(bound_presence) = self.lookup_value_by_name("presence", data_context) {
meta.presence = parse_presence_str(&bound_presence);
}
let is_draw_or_field = tag == "draw" || tag == "field";
if is_draw_or_field {
if tag == "field" {
meta.style.inset_top_pt = Some(node.box_model.margins.top);
meta.style.inset_bottom_pt = Some(node.box_model.margins.bottom);
meta.style.inset_left_pt = Some(node.box_model.margins.left);
meta.style.inset_right_pt = Some(node.box_model.margins.right);
}
if meta.style.font_weight.is_none() {
if let Some(weight) = extract_exdata_font_weight(elem) {
meta.style.font_weight = Some(weight);
}
}
if meta.style.font_style.is_none() {
if let Some(style) = extract_exdata_font_style(elem) {
meta.style.font_style = Some(style);
}
}
if meta.style.font_family.is_none() {
if let Some(family) = extract_exdata_font_family(elem) {
meta.style.font_family = Some(family);
}
}
if meta.style.text_color.is_none() {
if let Some(color) = extract_exdata_color(elem) {
meta.style.text_color = Some(color);
}
}
if meta.style.space_above_pt.is_none() || meta.style.space_below_pt.is_none() {
if let Some((above, below)) = extract_exdata_margins(elem) {
if meta.style.space_above_pt.is_none() {
meta.style.space_above_pt = Some(above);
}
if meta.style.space_below_pt.is_none() {
meta.style.space_below_pt = Some(below);
}
}
}
if meta.style.rich_text_spans.is_none() {
meta.style.rich_text_spans = parse_exdata_rich_text_spans(elem);
}
}
meta.bound_data_node = data_context.map(|c| c.as_raw());
let id = self.form_tree.add_node_with_meta(node, meta);
Ok((id, trailing_info))
}
#[allow(clippy::too_many_arguments)]
fn build_subform_instance(
&mut self,
elem: Node<'_, '_>,
data_context: Option<DataNodeId>,
is_root: bool,
occur: Occur,
name: String,
layout: LayoutStrategy,
mut bm: BoxModel,
) -> Result<(FormNode, (bool, Option<String>))> {
if layout == LayoutStrategy::TopToBottom && bm.width.is_none() && is_root {
bm.width = Some(612.0);
}
if name.is_empty() {
let mut n = FormNode {
name,
node_type: FormNodeType::Subform,
box_model: bm,
layout,
children: Vec::new(),
occur,
font: FontMetrics::default(),
calculate: None,
validate: None,
column_widths: Vec::new(),
col_span: 1,
};
let ti = self.add_children(&mut n, elem, data_context)?;
return Ok((n, ti));
}
let already_bound_instance = data_context
.and_then(|ctx| self.data_dom.get(ctx))
.is_some_and(|n| n.name() == name);
let mut child_context = data_context;
if !already_bound_instance {
if let Some(ctx) = data_context {
let matches = self.data_dom.children_by_name(ctx, &name);
if let Some(&first) = matches.first() {
child_context = Some(first);
} else {
child_context = self.resolve_with_scope_group(ctx, &name).or(data_context);
}
} else if let Some(root) = self.data_dom.root() {
if self.data_dom.get(root).is_some_and(|n| n.name() == name) {
child_context = Some(root);
} else {
let matches = self.data_dom.children_by_name(root, &name);
if let Some(&first) = matches.first() {
child_context = Some(first);
} else {
child_context = Some(root);
}
}
}
}
let mut n = FormNode {
name,
node_type: FormNodeType::Subform,
box_model: bm,
layout,
children: Vec::new(),
occur,
font: FontMetrics::default(),
calculate: None,
validate: None,
column_widths: Vec::new(),
col_span: 1,
};
let ti = self.add_children(&mut n, elem, child_context)?;
Ok((n, ti))
}
#[allow(clippy::type_complexity)]
fn expand_repeating_subform_instances(
&mut self,
element: Node<'_, '_>,
data_context: Option<DataNodeId>,
is_root: bool,
) -> Result<Vec<(FormNodeId, (bool, Option<String>))>> {
let name = attr(element, "name").unwrap_or("").to_string();
let occur = parse_occur(element);
let (_bind_ref_unused, bind_none) = parse_bind(element);
if bind_none {
return Ok(vec![self.parse_node(element, data_context, is_root)?]);
}
let bind_ref = find_first_child_by_name(element, "bind")
.and_then(|b| attr(b, "ref"))
.map(|s| s.to_string());
let data_instances = if let Some(ref raw_ref) = bind_ref {
let normalized = if let Some(rest) = raw_ref.strip_prefix("$record") {
format!("${}", rest)
} else {
raw_ref.clone()
};
let with_wildcard = if normalized.ends_with("[*]") {
normalized
} else {
format!("{}[*]", normalized)
};
resolve_data_path(self.data_dom, &with_wildcard, data_context)
.or_else(|_| resolve_data_path(self.data_dom, &with_wildcard, self.data_dom.root()))
.unwrap_or_default()
} else {
if let Some(ctx) = data_context {
self.data_dom.children_by_name(ctx, &name)
} else if let Some(root) = self.data_dom.root() {
self.data_dom.children_by_name(root, &name)
} else {
Vec::new()
}
};
let data_count = data_instances.len() as u32;
let (count, _outcome) =
crate::adobe_compat::repeating_subform_instance_count_clamped_to_occur_range(
&name, data_count, occur.min, occur.max,
);
let layout = area_layout(element);
let bm = parse_box_model(element);
if count == 0 && occur.max.unwrap_or(u32::MAX) > 0 {
let (inst_node, trailing) = self.build_subform_instance(
element,
None,
is_root,
Occur::once(),
name.clone(),
layout,
bm.clone(),
)?;
let mut meta = parse_node_meta(element);
meta.is_zero_instance_prototype = true;
meta.presence = Presence::Hidden;
let inst_id = self.form_tree.add_node_with_meta(inst_node, meta);
return Ok(vec![(inst_id, trailing)]);
}
let mut instances = Vec::with_capacity(count as usize);
for i in 0..count {
let instance_data_ctx = data_instances.get(i as usize).copied();
let (inst_node, trailing) = self.build_subform_instance(
element,
instance_data_ctx,
is_root,
Occur::once(),
name.clone(),
layout,
bm.clone(),
)?;
if element.tag_name().name() == "exclGroup" {
self.apply_exclusive_choice_value(element, instance_data_ctx, &inst_node.children);
}
let mut meta = parse_node_meta(element);
meta.bound_data_node = instance_data_ctx.map(|c| c.as_raw());
let inst_id = self.form_tree.add_node_with_meta(inst_node, meta);
instances.push((inst_id, trailing));
}
Ok(instances)
}
fn find_value_in_descendants(&self, node: DataNodeId, name: &str) -> Option<String> {
for &child in self.data_dom.children(node) {
if let Some(cn) = self.data_dom.get(child) {
if cn.name() == name && cn.is_value() {
return self.data_dom.value(child).ok().map(|s| s.to_string());
}
}
}
for &child in self.data_dom.children(node) {
if self.data_dom.get(child).is_some_and(|n| n.is_group()) {
if let Some(val) = self.find_value_in_descendants(child, name) {
return Some(val);
}
}
}
None
}
fn resolve_with_scope_value(&self, data_context: DataNodeId, name: &str) -> Option<String> {
let direct = self.data_dom.children_by_name(data_context, name);
if let Some(&val_id) = direct.first() {
if self.data_dom.get(val_id).is_some_and(|n| n.is_value()) {
return self.data_dom.value(val_id).ok().map(|s| s.to_string());
}
}
if let Some(parent) = self.data_dom.get(data_context).and_then(|n| n.parent()) {
let sibling_matches = self.data_dom.children_by_name(parent, name);
if let Some(&val_id) = sibling_matches.first() {
if self.data_dom.get(val_id).is_some_and(|n| n.is_value()) {
return self.data_dom.value(val_id).ok().map(|s| s.to_string());
}
}
let mut ancestor = self.data_dom.get(parent).and_then(|n| n.parent());
while let Some(anc_id) = ancestor {
let anc_matches = self.data_dom.children_by_name(anc_id, name);
if let Some(&val_id) = anc_matches.first() {
if self.data_dom.get(val_id).is_some_and(|n| n.is_value()) {
return self.data_dom.value(val_id).ok().map(|s| s.to_string());
}
}
ancestor = self.data_dom.get(anc_id).and_then(|n| n.parent());
}
}
self.data_dom
.root()
.and_then(|root| self.find_value_in_descendants(root, name))
}
fn resolve_with_scope_group(&self, data_context: DataNodeId, name: &str) -> Option<DataNodeId> {
let mut cursor = self.data_dom.get(data_context).and_then(|n| n.parent());
while let Some(anc_id) = cursor {
let matches = self.data_dom.children_by_name(anc_id, name);
if let Some(&grp_id) = matches.first() {
if self.data_dom.get(grp_id).is_some_and(|n| n.is_group()) {
return Some(grp_id);
}
}
cursor = self.data_dom.get(anc_id).and_then(|n| n.parent());
}
None
}
fn lookup_value_by_name(&self, name: &str, data_context: Option<DataNodeId>) -> Option<String> {
if name.is_empty() {
return None;
}
if let Some(ctx) = data_context {
return self.resolve_with_scope_value(ctx, name);
}
self.data_dom
.root()
.and_then(|root| self.find_value_in_descendants(root, name))
}
fn apply_exclusive_choice_value(
&mut self,
elem: Node<'_, '_>,
data_context: Option<DataNodeId>,
child_ids: &[FormNodeId],
) {
let Some(group_name) = attr(elem, "name") else {
return;
};
let Some(group_value) = self.lookup_value_by_name(group_name, data_context) else {
return;
};
for &child_id in child_ids {
let item_value = self.form_tree.meta(child_id).item_value.clone();
if let FormNodeType::Field { value } = &mut self.form_tree.get_mut(child_id).node_type {
*value = if item_value.as_deref() == Some(group_value.as_str()) {
group_value.clone()
} else {
String::new()
};
}
}
}
fn parse_field(
&mut self,
elem: Node<'_, '_>,
data_context: Option<DataNodeId>,
) -> Result<FormNode> {
let name = attr(elem, "name").unwrap_or("").to_string();
let bm = parse_box_model(elem);
let mut value = extract_value_text(elem).unwrap_or_default();
if let Some(bound_value) = self.lookup_value_by_name(&name, data_context) {
value = bound_value;
let presence = attr(elem, "presence").unwrap_or("");
match crate::adobe_compat::ignore_invisible_server_metadata_bindings(presence) {
crate::adobe_compat::InvisibleBindingDecision::AcceptedVisible => {
self.form_tree.any_data_bound = true;
}
crate::adobe_compat::InvisibleBindingDecision::IgnoredInvisible => {
self.invisible_bindings_ignored += 1;
}
}
}
let mut bm_with_caption = bm.clone();
if !is_hidden(elem) {
if let Some(cap) = parse_caption(elem) {
bm_with_caption.caption = Some(cap);
}
}
let mut font = parse_font_metrics(elem);
if let Some(html_size) = extract_exdata_font_size(elem) {
font.size = html_size;
}
if let Some(css_align) = extract_exdata_text_align(elem) {
font.text_align = css_align;
}
Ok(FormNode {
name,
node_type: FormNodeType::Field { value },
box_model: bm_with_caption,
layout: LayoutStrategy::Positioned,
children: Vec::new(),
occur: Occur::once(),
font,
calculate: None,
validate: None,
column_widths: Vec::new(),
col_span: parse_col_span(elem),
})
}
fn parse_draw(
&mut self,
elem: Node<'_, '_>,
_data_context: Option<DataNodeId>,
) -> Result<FormNode> {
let name = attr(elem, "name").unwrap_or("").to_string();
let bm = parse_box_model(elem);
if let Some(draw_content) = extract_draw_content(elem) {
return Ok(FormNode {
name,
node_type: FormNodeType::Draw(draw_content),
box_model: bm,
layout: LayoutStrategy::Positioned,
children: Vec::new(),
occur: Occur::once(),
font: FontMetrics::default(),
calculate: None,
validate: None,
column_widths: Vec::new(),
col_span: 1,
});
}
if let Some((image_data, mime_type)) = extract_value_image(elem, &self.image_files) {
return Ok(FormNode {
name,
node_type: FormNodeType::Image {
data: image_data,
mime_type,
},
box_model: bm,
layout: LayoutStrategy::Positioned,
children: Vec::new(),
occur: Occur::once(),
font: FontMetrics::default(),
calculate: None,
validate: None,
column_widths: Vec::new(),
col_span: 1,
});
}
let mut font = parse_font_metrics(elem);
if let Some(html_size) = extract_exdata_font_size(elem) {
font.size = html_size;
}
if let Some(css_align) = extract_exdata_text_align(elem) {
font.text_align = css_align;
}
let content = extract_value_text(elem).unwrap_or_default();
Ok(FormNode {
name,
node_type: FormNodeType::Draw(DrawContent::Text(content)),
box_model: bm,
layout: LayoutStrategy::Positioned,
children: Vec::new(),
occur: Occur::once(),
font,
calculate: None,
validate: None,
column_widths: Vec::new(),
col_span: 1,
})
}
fn parse_page_set(
&mut self,
elem: Node<'_, '_>,
data_context: Option<DataNodeId>,
) -> Result<FormNode> {
let name = attr(elem, "name").unwrap_or("pageSet").to_string();
let mut node = FormNode {
name,
node_type: FormNodeType::PageSet,
box_model: BoxModel {
max_width: f64::MAX,
max_height: f64::MAX,
..Default::default()
},
layout: LayoutStrategy::TopToBottom,
children: Vec::new(),
occur: Occur::once(),
font: FontMetrics::default(),
calculate: None,
validate: None,
column_widths: Vec::new(),
col_span: 1,
};
for child in elem.children().filter(|n| n.is_element()) {
if child.tag_name().name() == "pageArea" {
let (child_id, _) = self.parse_node(child, data_context, false)?;
node.children.push(child_id);
}
}
Ok(node)
}
fn parse_page_area(
&mut self,
elem: Node<'_, '_>,
data_context: Option<DataNodeId>,
) -> Result<FormNode> {
let name = attr(elem, "name").unwrap_or("").to_string();
let (page_w, page_h) = read_medium(elem);
let content_areas = read_content_areas(elem, page_w, page_h);
let bm = BoxModel {
width: Some(page_w),
height: Some(page_h),
..Default::default()
};
let mut node = FormNode {
name,
node_type: FormNodeType::PageArea { content_areas },
box_model: bm,
layout: LayoutStrategy::Positioned,
children: Vec::new(),
occur: Occur::once(),
font: FontMetrics::default(),
calculate: None,
validate: None,
column_widths: Vec::new(),
col_span: 1,
};
self.add_children(&mut node, elem, data_context)?;
Ok(node)
}
fn add_children(
&mut self,
node: &mut FormNode,
elem: Node<'_, '_>,
data_context: Option<DataNodeId>,
) -> Result<(bool, Option<String>)> {
let mut pending_break = false;
let mut pending_break_target = None;
let mut pending_ca_break = false;
for child in elem.children().filter(|n| n.is_element()) {
let tag = child.tag_name().name();
match tag {
"subform" | "field" | "draw" | "pageSet" | "pageArea" | "exclGroup" | "area" => {
let child_entries = if matches!(tag, "subform" | "exclGroup" | "area") {
let name = attr(child, "name").unwrap_or("");
let occur = parse_occur(child);
let (_, bind_none) = parse_bind(child);
let gate = crate::adobe_compat::bind_none_subform_does_not_auto_expand(
name,
occur.is_repeating(),
name.is_empty(),
bind_none,
);
let bind_none_allows =
matches!(gate, crate::adobe_compat::BindNoneExpansionGate::Allowed);
if occur.is_repeating() && !name.is_empty() && bind_none_allows {
self.expand_repeating_subform_instances(child, data_context, false)?
} else {
vec![self.parse_node(child, data_context, false)?]
}
} else {
vec![self.parse_node(child, data_context, false)?]
};
for (child_id, (trailing_break, trailing_target)) in child_entries {
if pending_break {
let meta = self.form_tree.meta_mut(child_id);
meta.page_break_before = true;
if meta.break_target.is_none() {
meta.break_target = pending_break_target.take();
}
pending_break = false;
}
if pending_ca_break {
self.form_tree.meta_mut(child_id).content_area_break = true;
pending_ca_break = false;
}
node.children.push(child_id);
if trailing_break {
pending_break = true;
pending_break_target = trailing_target;
}
}
}
"breakBefore" => {
let target_type = attr(child, "targetType");
if target_type == Some("pageArea") {
pending_break = true;
pending_break_target = attr(child, "target").map(|s| s.to_string());
} else if target_type == Some("contentArea") {
pending_ca_break = true;
}
}
"break"
if attr(child, "before") == Some("pageArea")
&& attr(child, "targetType") == Some("pageArea") =>
{
pending_break = true;
pending_break_target = attr(child, "target").map(|s| s.to_string());
}
_ => {}
}
}
Ok((pending_break, pending_break_target))
}
fn blank_node(&self, name: &str, node_type: FormNodeType) -> FormNode {
FormNode {
name: name.to_string(),
node_type,
box_model: BoxModel {
max_width: f64::MAX,
max_height: f64::MAX,
..Default::default()
},
layout: LayoutStrategy::TopToBottom,
children: Vec::new(),
occur: Occur::once(),
font: FontMetrics::default(),
calculate: None,
validate: None,
column_widths: Vec::new(),
col_span: 1,
}
}
}
fn attr<'a>(elem: Node<'a, '_>, name: &str) -> Option<&'a str> {
elem.attributes()
.find(|a| a.name() == name)
.map(|a| a.value())
}
fn find_first_child_by_name<'a, 'input>(
elem: Node<'a, 'input>,
name: &str,
) -> Option<Node<'a, 'input>> {
elem.children()
.filter(|n| n.is_element())
.find(|n| n.tag_name().name() == name)
}
fn parse_layout_attr(elem: Node<'_, '_>) -> LayoutStrategy {
match attr(elem, "layout").unwrap_or("") {
"tb" => LayoutStrategy::TopToBottom,
"lr-tb" => LayoutStrategy::LeftToRightTB,
"rl-tb" => LayoutStrategy::RightToLeftTB,
"table" => LayoutStrategy::Table,
"row" => LayoutStrategy::Row,
"paginate" => LayoutStrategy::TopToBottom,
"position" => LayoutStrategy::Positioned,
_ => LayoutStrategy::Positioned,
}
}
fn parse_anchor_type(elem: Node<'_, '_>) -> AnchorType {
match attr(elem, "anchorType").unwrap_or("") {
"topCenter" => AnchorType::TopCenter,
"topRight" => AnchorType::TopRight,
"middleLeft" => AnchorType::MiddleLeft,
"middleCenter" => AnchorType::MiddleCenter,
"middleRight" => AnchorType::MiddleRight,
"bottomLeft" => AnchorType::BottomLeft,
"bottomCenter" => AnchorType::BottomCenter,
"bottomRight" => AnchorType::BottomRight,
_ => AnchorType::TopLeft,
}
}
fn parse_box_model(elem: Node<'_, '_>) -> BoxModel {
let w = attr(elem, "w").and_then(parse_dim);
let h = attr(elem, "h").and_then(parse_dim);
let x = attr(elem, "x").and_then(parse_dim).unwrap_or(0.0);
let y = attr(elem, "y").and_then(parse_dim).unwrap_or(0.0);
let min_h = attr(elem, "minH").and_then(parse_dim).unwrap_or(0.0);
let min_w = attr(elem, "minW").and_then(parse_dim).unwrap_or(0.0);
let max_h = attr(elem, "maxH").and_then(parse_dim).unwrap_or(f64::MAX);
let max_w = attr(elem, "maxW").and_then(parse_dim).unwrap_or(f64::MAX);
let margins = parse_margin(elem);
BoxModel {
width: w,
height: h,
x,
y,
margins,
min_width: min_w,
max_width: max_w,
min_height: min_h,
max_height: max_h,
..Default::default()
}
}
fn parse_margin(elem: Node<'_, '_>) -> Insets {
if let Some(margin) = find_first_child_by_name(elem, "margin") {
Insets {
top: attr(margin, "topInset").and_then(parse_dim).unwrap_or(0.0),
bottom: attr(margin, "bottomInset")
.and_then(parse_dim)
.unwrap_or(0.0),
left: attr(margin, "leftInset").and_then(parse_dim).unwrap_or(0.0),
right: attr(margin, "rightInset")
.and_then(parse_dim)
.unwrap_or(0.0),
}
} else {
Insets::default()
}
}
fn parse_dim(s: &str) -> Option<f64> {
if s.trim().parse::<f64>().is_ok() {
return Measurement::parse(&format!("{s}in")).map(|m: Measurement| m.to_points());
}
Measurement::parse(s).map(|m: Measurement| m.to_points())
}
fn parse_percentage(s: &str) -> Option<f64> {
let s = s.trim();
let num_str = s.strip_suffix('%')?;
let v: f64 = num_str.trim().parse().ok()?;
Some(v / 100.0)
}
fn parse_letter_spacing(s: &str, font_size_pt: f64) -> Option<f64> {
let s = s.trim();
if s == "0" {
return Some(0.0);
}
if let Some(num_str) = s.strip_suffix("em") {
let v: f64 = num_str.trim().parse().ok()?;
return Some(v * font_size_pt);
}
Measurement::parse(s).map(|m| m.to_points())
}
fn parse_occur(elem: Node<'_, '_>) -> Occur {
if let Some(occur) = find_first_child_by_name(elem, "occur") {
let min: u32 = attr(occur, "min").and_then(|s| s.parse().ok()).unwrap_or(1);
let max: Option<u32> = match attr(occur, "max") {
Some("-1") => None, Some(s) => Some(s.parse::<u32>().unwrap_or(min)),
None => Some(min),
};
let initial: u32 = attr(occur, "initial")
.and_then(|s| s.parse().ok())
.unwrap_or(min);
Occur::repeating(min, max, initial)
} else {
Occur::once()
}
}
fn parse_font_size(s: &str) -> Option<f64> {
if let Ok(v) = s.trim().parse::<f64>() {
return if v > 0.0 { Some(v) } else { None };
}
Measurement::parse(s).map(|m: Measurement| m.to_points())
}
fn parse_font_metrics(elem: Node<'_, '_>) -> FontMetrics {
let font_elem = find_first_child_by_name(elem, "font");
let size = font_elem
.and_then(|f| attr(f, "size"))
.and_then(parse_font_size)
.unwrap_or(FontMetrics::default().size);
let typeface = font_elem
.and_then(|f| attr(f, "typeface"))
.map(FontFamily::from_typeface)
.unwrap_or_default();
let text_align = find_first_child_by_name(elem, "para")
.and_then(|p| attr(p, "hAlign"))
.map(|a| match a {
"center" => TextAlign::Center,
"right" => TextAlign::Right,
"justify" => TextAlign::Justify,
_ => TextAlign::Left,
})
.unwrap_or_default();
FontMetrics {
size,
text_align,
typeface,
..FontMetrics::default()
}
}
fn read_medium(page_area: Node<'_, '_>) -> (f64, f64) {
if let Some(m) = find_first_child_by_name(page_area, "medium") {
let short = attr(m, "short").and_then(parse_dim).unwrap_or(612.0);
let long_ = attr(m, "long").and_then(parse_dim).unwrap_or(792.0);
(short, long_)
} else {
(612.0, 792.0)
}
}
fn read_content_areas(
page_area: Node<'_, '_>,
page_width: f64,
page_height: f64,
) -> Vec<ContentArea> {
let mut areas = Vec::new();
for child in page_area.children().filter(|n| n.is_element()) {
if child.tag_name().name() == "contentArea" {
let x = attr(child, "x").and_then(parse_dim).unwrap_or(0.0);
let y = attr(child, "y").and_then(parse_dim).unwrap_or(0.0);
let w = attr(child, "w").and_then(parse_dim).unwrap_or(page_width);
let h = attr(child, "h").and_then(parse_dim).unwrap_or(page_height);
areas.push(ContentArea {
name: attr(child, "name").unwrap_or("").to_string(),
x,
y,
width: w,
height: h,
leader: None,
trailer: None,
});
}
}
if areas.is_empty() {
areas.push(ContentArea {
name: String::new(),
x: 0.0,
y: 0.0,
width: page_width,
height: page_height,
leader: None,
trailer: None,
});
}
areas
}
fn extract_value_text(elem: Node<'_, '_>) -> Option<String> {
let value = find_first_child_by_name(elem, "value")?;
for tag in &["text", "float", "integer", "date", "dateTime", "decimal"] {
if let Some(child) = find_first_child_by_name(value, tag) {
let text = child.text().unwrap_or("");
let trimmed = text.trim_start_matches(|c: char| c.is_whitespace() && c != '\n');
let trimmed = trimmed.trim_end_matches(|c: char| c.is_whitespace() && c != '\n');
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
}
if let Some(ex) = find_first_child_by_name(value, "exData") {
let text = extract_text_from_descendants(ex);
if !text.is_empty() {
return Some(text);
}
}
None
}
fn extract_text_from_descendants(node: Node<'_, '_>) -> String {
let block_tags = [
"p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "li", "tr", "br",
];
let mut result = String::new();
let mut last_was_block = false;
for desc in node.descendants() {
let is_block = desc.is_element() && block_tags.contains(&desc.tag_name().name());
if is_block && !result.is_empty() {
result.push('\n');
last_was_block = true;
}
if desc.is_text() {
if let Some(t) = desc.text() {
let t = t.trim();
if !t.is_empty() {
if last_was_block || result.is_empty() {
result.push_str(t);
} else {
result.push(' ');
result.push_str(t);
}
last_was_block = false;
}
}
}
}
result.trim().to_string()
}
fn extract_exdata_font_size(elem: Node<'_, '_>) -> Option<f64> {
let value = find_first_child_by_name(elem, "value")?;
let ex = find_first_child_by_name(value, "exData")?;
for desc in ex.descendants() {
if !desc.is_element() {
continue;
}
let style = desc
.attribute("style")
.or_else(|| desc.attribute("Style"))?;
for part in style.split(';') {
let part = part.trim();
if let Some(val) = part
.strip_prefix("font-size:")
.or_else(|| part.strip_prefix("font-size :"))
{
let val = val.trim();
if let Some(pt) = val.strip_suffix("pt") {
if let Ok(size) = pt.trim().parse::<f64>() {
if size > 0.0 {
return Some(size);
}
}
}
}
}
}
None
}
fn extract_exdata_font_weight(elem: Node<'_, '_>) -> Option<String> {
let value = find_first_child_by_name(elem, "value")?;
let ex = find_first_child_by_name(value, "exData")?;
for desc in ex.descendants() {
if !desc.is_element() {
continue;
}
let style = desc.attribute("style")?;
for part in style.split(';') {
let part = part.trim();
if let Some(val) = part
.strip_prefix("font-weight:")
.or_else(|| part.strip_prefix("font-weight :"))
{
let val = val.trim();
if val == "bold" || val == "700" || val == "800" || val == "900" {
return Some("bold".to_string());
}
if val == "normal" || val == "400" || val == "500" || val == "600" {
return Some("normal".to_string());
}
}
}
}
None
}
fn extract_exdata_font_style(elem: Node<'_, '_>) -> Option<String> {
let value = find_first_child_by_name(elem, "value")?;
let ex = find_first_child_by_name(value, "exData")?;
for desc in ex.descendants() {
if !desc.is_element() {
continue;
}
let style = desc.attribute("style")?;
for part in style.split(';') {
let part = part.trim();
if let Some(val) = part
.strip_prefix("font-style:")
.or_else(|| part.strip_prefix("font-style :"))
{
let val = val.trim();
if val == "italic" || val == "oblique" {
return Some("italic".to_string());
}
if val == "normal" {
return Some("normal".to_string());
}
}
}
}
None
}
fn extract_exdata_color(elem: Node<'_, '_>) -> Option<(u8, u8, u8)> {
let value = find_first_child_by_name(elem, "value")?;
let ex = find_first_child_by_name(value, "exData")?;
for desc in ex.descendants() {
if !desc.is_element() {
continue;
}
let style = desc.attribute("style")?;
for part in style.split(';') {
let part = part.trim();
if let Some(val) = part
.strip_prefix("color:")
.or_else(|| part.strip_prefix("color :"))
{
let val = val.trim();
if let Some(rgb) = parse_css_color(val) {
return Some(rgb);
}
}
}
}
None
}
fn parse_ascii_hex_color(hex: &str) -> Option<(u8, u8, u8)> {
fn hex_nibble(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
match hex.as_bytes() {
[r, g, b] => Some((
hex_nibble(*r)? * 17,
hex_nibble(*g)? * 17,
hex_nibble(*b)? * 17,
)),
[r1, r2, g1, g2, b1, b2] => Some((
(hex_nibble(*r1)? << 4) | hex_nibble(*r2)?,
(hex_nibble(*g1)? << 4) | hex_nibble(*g2)?,
(hex_nibble(*b1)? << 4) | hex_nibble(*b2)?,
)),
_ => None,
}
}
fn parse_css_color(s: &str) -> Option<(u8, u8, u8)> {
let s = s.trim();
if let Some(hex) = s.strip_prefix('#') {
parse_ascii_hex_color(hex)
} else if let Some(rgb_part) = s.strip_prefix("rgb(") {
if let Some(inner) = rgb_part.strip_suffix(')') {
let parts: Vec<&str> = inner.split(',').collect();
if parts.len() >= 3 {
let r: f64 = parts[0].trim().parse().ok()?;
let g: f64 = parts[1].trim().parse().ok()?;
let b: f64 = parts[2].trim().parse().ok()?;
return Some((r as u8, g as u8, b as u8));
}
}
None
} else {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() >= 3 {
let r = parts[0].trim().parse::<u8>().ok()?;
let g = parts[1].trim().parse::<u8>().ok()?;
let b = parts[2].trim().parse::<u8>().ok()?;
Some((r, g, b))
} else {
None
}
}
}
fn extract_exdata_font_family(elem: Node<'_, '_>) -> Option<String> {
let value = find_first_child_by_name(elem, "value")?;
let ex = find_first_child_by_name(value, "exData")?;
for desc in ex.descendants() {
if !desc.is_element() {
continue;
}
let style = desc.attribute("style")?;
for part in style.split(';') {
let part = part.trim();
if let Some(val) = part
.strip_prefix("font-family:")
.or_else(|| part.strip_prefix("font-family :"))
{
let val = val.trim();
let first_family = val
.split(',')
.next()
.map(|s| s.trim().trim_matches(['"', '\'']))
.filter(|s| !s.is_empty())?;
return Some(first_family.to_string());
}
}
}
None
}
fn extract_exdata_text_align(elem: Node<'_, '_>) -> Option<TextAlign> {
let value = find_first_child_by_name(elem, "value")?;
let ex = find_first_child_by_name(value, "exData")?;
for desc in ex.descendants() {
if !desc.is_element() {
continue;
}
let style = desc.attribute("style")?;
for part in style.split(';') {
let part = part.trim();
if let Some(val) = part
.strip_prefix("text-align:")
.or_else(|| part.strip_prefix("text-align :"))
{
let val = val.trim();
return Some(match val {
"center" => TextAlign::Center,
"right" => TextAlign::Right,
"justify" => TextAlign::Justify,
_ => TextAlign::Left,
});
}
}
}
None
}
fn extract_exdata_margins(elem: Node<'_, '_>) -> Option<(f64, f64)> {
let value = find_first_child_by_name(elem, "value")?;
let ex = find_first_child_by_name(value, "exData")?;
let mut space_above: Option<f64> = None;
let mut space_below: Option<f64> = None;
for desc in ex.descendants() {
if !desc.is_element() {
continue;
}
let style = match desc.attribute("style") {
Some(s) => s,
None => continue,
};
for part in style.split(';') {
let part = part.trim();
if space_above.is_none() {
if let Some(val) = part
.strip_prefix("margin-top:")
.or_else(|| part.strip_prefix("margin-top :"))
{
let val = val.trim();
if let Some(m) = Measurement::parse(val) {
let pt = m.to_points();
if pt >= 0.0 {
space_above = Some(pt);
}
}
}
}
if space_below.is_none() {
if let Some(val) = part
.strip_prefix("margin-bottom:")
.or_else(|| part.strip_prefix("margin-bottom :"))
{
let val = val.trim();
if let Some(m) = Measurement::parse(val) {
let pt = m.to_points();
if pt >= 0.0 {
space_below = Some(pt);
}
}
}
}
}
if space_above.is_some() && space_below.is_some() {
break;
}
}
match (space_above, space_below) {
(Some(above), Some(below)) => Some((above, below)),
(Some(above), None) => Some((above, 0.0)),
(None, Some(below)) => Some((0.0, below)),
(None, None) => None,
}
}
#[derive(Debug, Clone, Default)]
struct InheritedStyle {
font_size: Option<f64>,
font_family: Option<String>,
font_weight: Option<String>,
font_style: Option<String>,
text_color: Option<(u8, u8, u8)>,
underline: bool,
line_through: bool,
}
impl InheritedStyle {
fn merge_with_css(&self, css: &str) -> Self {
let mut child = self.clone();
for part in css.split(';') {
let part = part.trim();
if let Some(val) = strip_css_prop(part, "font-size") {
if let Some(pt) = val.strip_suffix("pt") {
if let Ok(size) = pt.trim().parse::<f64>() {
if size > 0.0 {
child.font_size = Some(size);
}
}
}
} else if let Some(val) = strip_css_prop(part, "font-family") {
let first = val
.split(',')
.next()
.map(|s| s.trim().trim_matches(['"', '\'']))
.filter(|s| !s.is_empty());
if let Some(fam) = first {
child.font_family = Some(fam.to_string());
}
} else if let Some(val) = strip_css_prop(part, "font-weight") {
if val == "bold" || val == "700" || val == "800" || val == "900" {
child.font_weight = Some("bold".to_string());
} else if val == "normal" || val == "400" || val == "500" {
child.font_weight = Some("normal".to_string());
}
} else if let Some(val) = strip_css_prop(part, "font-style") {
if val == "italic" || val == "oblique" {
child.font_style = Some("italic".to_string());
} else if val == "normal" {
child.font_style = Some("normal".to_string());
}
} else if let Some(val) = strip_css_prop(part, "text-decoration") {
if val.contains("underline") {
child.underline = true;
} else if val.contains("line-through") {
child.line_through = true;
} else if val == "none" {
child.underline = false;
child.line_through = false;
}
} else if let Some(val) = strip_css_prop(part, "color") {
if let Some(rgb) = parse_css_color(val) {
child.text_color = Some(rgb);
}
}
}
child
}
fn to_span(&self, text: String) -> RichTextSpan {
RichTextSpan {
text,
font_size: self.font_size,
font_family: self.font_family.clone(),
font_weight: self.font_weight.clone(),
font_style: self.font_style.clone(),
text_color: self.text_color,
underline: self.underline,
line_through: self.line_through,
}
}
}
fn strip_css_prop<'a>(decl: &'a str, prop: &str) -> Option<&'a str> {
decl.strip_prefix(prop)
.and_then(|rest| rest.trim_start().strip_prefix(':'))
.map(|v| v.trim())
}
fn parse_exdata_rich_text_spans(elem: Node<'_, '_>) -> Option<Vec<RichTextSpan>> {
let value = find_first_child_by_name(elem, "value")?;
let ex = find_first_child_by_name(value, "exData")?;
let ct = ex.attribute("contentType").unwrap_or("");
if ct != "text/html" && ct != "text/xml" {
return None;
}
let body = ex
.descendants()
.find(|d| d.is_element() && d.tag_name().name() == "body")?;
let mut spans: Vec<RichTextSpan> = Vec::new();
let base_style = InheritedStyle::default();
let mut first_para = true;
for child in body.children() {
if child.is_element() && child.tag_name().name() == "p" {
if !first_para && !spans.is_empty() {
spans.push(RichTextSpan {
text: "\n".to_string(),
font_size: None,
font_family: None,
font_weight: None,
font_style: None,
text_color: None,
underline: false,
line_through: false,
});
}
first_para = false;
let p_style = match child.attribute("style") {
Some(css) => base_style.merge_with_css(css),
None => base_style.clone(),
};
collect_inline_spans(child, &p_style, &mut spans);
} else if child.is_text() {
if let Some(t) = child.text() {
let t = t.trim();
if !t.is_empty() {
spans.push(base_style.to_span(t.to_string()));
}
}
}
}
if spans.is_empty() {
return None;
}
let all_default = spans.iter().all(|s| {
s.font_size.is_none()
&& s.font_family.is_none()
&& s.font_weight.is_none()
&& s.font_style.is_none()
&& s.text_color.is_none()
&& !s.underline
});
if all_default {
return None;
}
Some(spans)
}
fn collect_inline_spans(
node: Node<'_, '_>,
inherited: &InheritedStyle,
spans: &mut Vec<RichTextSpan>,
) {
for child in node.children() {
if child.is_text() {
if let Some(t) = child.text() {
let t = t.trim();
if !t.is_empty() {
spans.push(inherited.to_span(t.to_string()));
}
}
} else if child.is_element() {
let tag = child.tag_name().name();
match tag {
"br" => {
spans.push(RichTextSpan {
text: "\n".to_string(),
font_size: None,
font_family: None,
font_weight: None,
font_style: None,
text_color: None,
underline: false,
line_through: false,
});
}
"span" => {
let child_style = match child.attribute("style") {
Some(css) => {
if css.contains("xfa-spacerun:yes") {
if let Some(t) = child.text() {
if !t.is_empty() {
spans.push(inherited.to_span(t.to_string()));
}
}
collect_inline_spans(child, inherited, spans);
continue;
}
inherited.merge_with_css(css)
}
None => inherited.clone(),
};
collect_inline_spans(child, &child_style, spans);
}
"b" | "strong" => {
let mut s = inherited.clone();
s.font_weight = Some("bold".to_string());
if let Some(css) = child.attribute("style") {
s = s.merge_with_css(css);
}
collect_inline_spans(child, &s, spans);
}
"i" | "em" => {
let mut s = inherited.clone();
s.font_style = Some("italic".to_string());
if let Some(css) = child.attribute("style") {
s = s.merge_with_css(css);
}
collect_inline_spans(child, &s, spans);
}
"u" => {
let mut s = inherited.clone();
s.underline = true;
if let Some(css) = child.attribute("style") {
s = s.merge_with_css(css);
}
collect_inline_spans(child, &s, spans);
}
_ => {
let child_style = match child.attribute("style") {
Some(css) => inherited.merge_with_css(css),
None => inherited.clone(),
};
collect_inline_spans(child, &child_style, spans);
}
}
}
}
}
fn parse_caption(elem: Node<'_, '_>) -> Option<Caption> {
let cap_elem = find_first_child_by_name(elem, "caption")?;
if is_hidden(cap_elem) {
return None;
}
let text = extract_value_text(cap_elem)?;
if text.is_empty() {
return None;
}
let placement = match attr(cap_elem, "placement") {
Some("right") => CaptionPlacement::Right,
Some("top") => CaptionPlacement::Top,
Some("bottom") => CaptionPlacement::Bottom,
Some("inline") => CaptionPlacement::Inline,
_ => CaptionPlacement::Left,
};
let reserve = attr(cap_elem, "reserve")
.and_then(Measurement::parse)
.map(|m| m.to_points());
Some(Caption {
placement,
reserve,
text,
})
}
fn is_hidden(elem: Node<'_, '_>) -> bool {
matches!(
attr(elem, "presence"),
Some("hidden") | Some("invisible") | Some("inactive")
)
}
fn parse_presence_str(s: &str) -> Presence {
match s.trim() {
"hidden" => Presence::Hidden,
"invisible" => Presence::Invisible,
"inactive" => Presence::Inactive,
_ => Presence::Visible,
}
}
fn extract_value_image(
elem: Node<'_, '_>,
image_files: &std::collections::HashMap<String, Vec<u8>>,
) -> Option<(Vec<u8>, String)> {
let value = find_first_child_by_name(elem, "value")?;
let image = find_first_child_by_name(value, "image")?;
let content_type = attr(image, "contentType")
.unwrap_or("image/png")
.to_string();
if let Some(href) = attr(image, "href") {
if let Some(data) = image_files.get(href) {
let decoded = data.clone();
if decoded.starts_with(b"BM") || content_type == "image/bmp" {
if let Some(png_data) = bmp_to_png(&decoded) {
return Some((png_data, "image/png".to_string()));
}
return None;
}
return Some((decoded, content_type));
}
let normalized = href.trim_start_matches(".\\").trim_start_matches("./");
for (k, data) in image_files {
let k_norm = k.trim_start_matches(".\\").trim_start_matches("./");
if k_norm == normalized {
let decoded = data.clone();
if decoded.starts_with(b"BM") || content_type == "image/bmp" {
if let Some(png_data) = bmp_to_png(&decoded) {
return Some((png_data, "image/png".to_string()));
}
return None;
}
return Some((decoded, content_type));
}
}
}
let data = image.text().unwrap_or_default();
let decoded = base64_decode(data);
if decoded.is_empty() {
return None;
}
if decoded.starts_with(b"BM") || content_type == "image/bmp" {
if let Some(png_data) = bmp_to_png(&decoded) {
return Some((png_data, "image/png".to_string()));
}
log::warn!("BMP to PNG conversion failed; skipping image");
return None;
}
Some((decoded, content_type))
}
fn extract_draw_content(elem: Node<'_, '_>) -> Option<DrawContent> {
let value = find_first_child_by_name(elem, "value")?;
if let Some(line) = find_first_child_by_name(value, "line") {
let x1 = attr_as_f64(line, "x1").unwrap_or(0.0);
let y1 = attr_as_f64(line, "y1").unwrap_or(0.0);
let x2 = attr_as_f64(line, "x2").unwrap_or(0.0);
let y2 = attr_as_f64(line, "y2").unwrap_or(0.0);
return Some(DrawContent::Line { x1, y1, x2, y2 });
}
if let Some(rect) = find_first_child_by_name(value, "rectangle") {
let x = attr_as_f64(rect, "x").unwrap_or(0.0);
let y = attr_as_f64(rect, "y").unwrap_or(0.0);
let w = attr_as_f64(rect, "w").unwrap_or(attr_as_f64(rect, "width").unwrap_or(0.0));
let h = attr_as_f64(rect, "h").unwrap_or(attr_as_f64(rect, "height").unwrap_or(0.0));
let radius =
attr_as_f64(rect, "r").unwrap_or(attr_as_f64(rect, "cornerRadius").unwrap_or(0.0));
return Some(DrawContent::Rectangle { x, y, w, h, radius });
}
if let Some(arc) = find_first_child_by_name(value, "arc") {
let x = attr_as_f64(arc, "x").unwrap_or(0.0);
let y = attr_as_f64(arc, "y").unwrap_or(0.0);
let w = attr_as_f64(arc, "w").unwrap_or(attr_as_f64(arc, "width").unwrap_or(0.0));
let h = attr_as_f64(arc, "h").unwrap_or(attr_as_f64(arc, "height").unwrap_or(0.0));
let start_angle = attr_as_f64(arc, "startAngle").unwrap_or(0.0);
let sweep_angle = attr_as_f64(arc, "sweepAngle").unwrap_or(0.0);
return Some(DrawContent::Arc {
x,
y,
w,
h,
start_angle,
sweep_angle,
});
}
None
}
fn base64_decode(input: &str) -> Vec<u8> {
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(input.trim())
.unwrap_or_default()
}
fn bmp_to_png(bmp_data: &[u8]) -> Option<Vec<u8>> {
let img = image::load_from_memory_with_format(bmp_data, image::ImageFormat::Bmp).ok()?;
let mut buf = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
.ok()?;
Some(buf)
}
fn attr_as_f64(elem: Node<'_, '_>, name: &str) -> Option<f64> {
attr(elem, name)?.parse().ok()
}
fn parse_col_span(elem: Node<'_, '_>) -> i32 {
attr(elem, "colSpan")
.and_then(|s| s.parse().ok())
.unwrap_or(1)
}
fn parse_node_meta(elem: Node<'_, '_>) -> FormNodeMeta {
let tag = elem.tag_name().name();
let presence = match attr(elem, "presence") {
Some("hidden") => Presence::Hidden,
Some("invisible") => Presence::Invisible,
Some("inactive") => Presence::Inactive,
_ => Presence::Visible,
};
let (page_break_before, break_before_target) = detect_page_break_before(elem);
let (page_break_after, break_after_target) = detect_page_break_after(elem);
let content_area_break = detect_content_area_break(elem);
let break_target = break_before_target.or(break_after_target);
let event_scripts = collect_event_scripts(elem);
let (keep_next_content_area, keep_previous_content_area, keep_intact_content_area) =
parse_keep(elem);
let (overflow_leader, overflow_trailer) = parse_overflow(elem);
let group_kind = if tag == "exclGroup" {
GroupKind::ExclusiveChoice
} else {
GroupKind::None
};
let item_value = if tag == "field" {
parse_item_value(elem)
} else {
None
};
let (display_items, save_items) = if tag == "field" {
parse_items_lists(elem)
} else {
(Vec::new(), Vec::new())
};
let xfa_id = attr(elem, "id").map(|s| s.to_string());
let field_kind = detect_field_kind(elem);
let style = parse_node_style(elem);
let (data_bind_ref, data_bind_none) = parse_bind(elem);
let anchor_type = parse_anchor_type(elem);
FormNodeMeta {
xfa_id,
presence,
page_break_before,
page_break_after,
break_target,
content_area_break,
overflow_leader,
overflow_trailer,
keep_next_content_area,
keep_previous_content_area,
keep_intact_content_area,
event_scripts,
data_bind_ref,
data_bind_none,
group_kind,
item_value,
field_kind,
style,
display_items,
save_items,
anchor_type,
..Default::default()
}
}
fn detect_page_break_before(elem: Node<'_, '_>) -> (bool, Option<String>) {
for child in elem.children().filter(|n| n.is_element()) {
let tag = child.tag_name().name();
if matches!(tag, "subform" | "field" | "draw" | "exclGroup") {
break;
}
if tag == "breakBefore" && attr(child, "targetType") == Some("pageArea") {
return (true, attr(child, "target").map(|s| s.to_string()));
}
if tag == "break" && attr(child, "before") == Some("pageArea") {
return (true, attr(child, "target").map(|s| s.to_string()));
}
}
(false, None)
}
fn detect_page_break_after(elem: Node<'_, '_>) -> (bool, Option<String>) {
let mut last_content_idx = 0;
let children: Vec<_> = elem.children().filter(|n| n.is_element()).collect();
for (i, child) in children.iter().enumerate() {
let tag = child.tag_name().name();
if matches!(tag, "subform" | "field" | "draw" | "exclGroup") {
last_content_idx = i;
}
}
for child in children.iter().skip(last_content_idx) {
let tag = child.tag_name().name();
if tag == "breakAfter" && attr(*child, "targetType") == Some("pageArea") {
return (true, attr(*child, "target").map(|s| s.to_string()));
}
if tag == "break" && attr(*child, "after") == Some("pageArea") {
return (true, attr(*child, "target").map(|s| s.to_string()));
}
}
(false, None)
}
fn detect_content_area_break(elem: Node<'_, '_>) -> bool {
for child in elem.children().filter(|n| n.is_element()) {
let tag = child.tag_name().name();
if tag == "breakBefore" && attr(child, "targetType") == Some("contentArea") {
return true;
}
}
false
}
fn collect_event_scripts(elem: Node<'_, '_>) -> Vec<EventScript> {
let mut scripts = Vec::new();
for child in elem.children().filter(|n| n.is_element()) {
let child_tag = child.tag_name().name();
if child_tag == "event" {
let activity = attr(child, "activity");
let event_ref = attr(child, "ref");
if activity == Some("ready") && event_ref == Some("$layout") {
continue;
}
if let Some(script_elem) = find_first_child_by_name(child, "script") {
if let Some(script) =
build_event_script(script_elem, activity, event_ref, attr(script_elem, "runAt"))
{
scripts.push(script);
}
}
} else if child_tag == "calculate" {
if let Some(script_elem) = find_first_child_by_name(child, "script") {
if let Some(script) = build_event_script(
script_elem,
Some("calculate"),
None,
attr(script_elem, "runAt"),
) {
scripts.push(script);
}
}
}
}
scripts
}
fn build_event_script(
script_elem: Node<'_, '_>,
activity: Option<&str>,
event_ref: Option<&str>,
run_at: Option<&str>,
) -> Option<EventScript> {
let text = script_elem.text()?.trim();
if text.is_empty() {
return None;
}
Some(EventScript::new(
text.to_string(),
detect_script_language(attr(script_elem, "contentType")),
activity.map(str::to_string),
event_ref.map(str::to_string),
run_at.map(str::to_string),
))
}
fn detect_script_language(content_type: Option<&str>) -> ScriptLanguage {
match content_type.map(|value| value.trim().to_ascii_lowercase()) {
None => ScriptLanguage::FormCalc,
Some(value) if value == "application/x-formcalc" || value.ends_with("/x-formcalc") => {
ScriptLanguage::FormCalc
}
Some(value)
if value == "application/x-javascript"
|| value == "application/javascript"
|| value == "text/javascript"
|| value.ends_with("/x-javascript") =>
{
ScriptLanguage::JavaScript
}
Some(_) => ScriptLanguage::Other,
}
}
fn parse_keep(elem: Node<'_, '_>) -> (bool, bool, bool) {
if let Some(keep) = find_first_child_by_name(elem, "keep") {
let next = attr(keep, "next") == Some("contentArea");
let prev = attr(keep, "previous") == Some("contentArea");
let intact = attr(keep, "intact") == Some("contentArea");
(next, prev, intact)
} else {
(false, false, false)
}
}
fn parse_overflow(elem: Node<'_, '_>) -> (Option<String>, Option<String>) {
if let Some(overflow) = find_first_child_by_name(elem, "overflow") {
let leader = attr(overflow, "leader").map(|s| s.to_string());
let trailer = attr(overflow, "trailer").map(|s| s.to_string());
(leader, trailer)
} else {
(None, None)
}
}
fn parse_item_value(elem: Node<'_, '_>) -> Option<String> {
let items = find_first_child_by_name(elem, "items")?;
let text_elem = find_first_child_by_name(items, "text")?;
let text = text_elem.text()?.trim();
if text.is_empty() {
None
} else {
Some(text.to_string())
}
}
fn collect_items_texts(items_elem: Node<'_, '_>) -> Vec<String> {
items_elem
.children()
.filter(|n| n.is_element())
.filter_map(|child| {
let txt = child.text().unwrap_or("").trim().to_string();
if txt.is_empty() {
None
} else {
Some(txt)
}
})
.collect()
}
fn parse_items_lists(elem: Node<'_, '_>) -> (Vec<String>, Vec<String>) {
let items_elems: Vec<_> = elem
.children()
.filter(|n| n.is_element() && n.tag_name().name() == "items")
.collect();
match items_elems.len() {
0 => (Vec::new(), Vec::new()),
1 => {
let vals = collect_items_texts(items_elems[0]);
(vals, Vec::new())
}
_ => {
let first = items_elems[0];
let second = items_elems[1];
let first_is_save = attr(first, "save") == Some("1");
if first_is_save {
(collect_items_texts(second), collect_items_texts(first))
} else {
(collect_items_texts(first), collect_items_texts(second))
}
}
}
}
fn detect_field_kind(elem: Node<'_, '_>) -> FieldKind {
let Some(ui) = find_first_child_by_name(elem, "ui") else {
return FieldKind::Text;
};
for child in ui.children().filter(|n| n.is_element()) {
match child.tag_name().name() {
"button" => return FieldKind::Button,
"checkButton" => {
let shape = attr(child, "shape").unwrap_or("square");
return if shape == "round" {
FieldKind::Radio
} else {
FieldKind::Checkbox
};
}
"choiceList" => return FieldKind::Dropdown,
"dateTimeEdit" => return FieldKind::DateTimePicker,
"numericEdit" => return FieldKind::NumericEdit,
"passwordEdit" => return FieldKind::PasswordEdit,
"imageEdit" => return FieldKind::ImageEdit,
"signature" => return FieldKind::Signature,
"barcode" => return FieldKind::Barcode,
_ => {}
}
}
FieldKind::Text
}
fn parse_node_style(elem: Node<'_, '_>) -> FormNodeStyle {
let (check_on_value, check_off_value, check_neutral_value) = parse_check_button_values(elem);
let mut style = FormNodeStyle {
check_button_mark: parse_check_button_mark(elem),
check_button_on_value: check_on_value,
check_button_off_value: check_off_value,
check_button_neutral_value: check_neutral_value,
..Default::default()
};
if let Some(fill) = find_first_child_by_name(elem, "fill") {
if !is_hidden(fill) {
style.bg_color = parse_fill_color(fill);
}
}
let border = find_first_child_by_name(elem, "border").or_else(|| {
let ui = find_first_child_by_name(elem, "ui")?;
ui.children()
.filter(|c| c.is_element() && c.tag_name().name() != "border")
.find_map(|widget| find_first_child_by_name(widget, "border"))
});
if let Some(border) = border {
let edge_elems: Vec<_> = border
.children()
.filter(|n| n.is_element() && n.tag_name().name() == "edge")
.collect();
if !edge_elems.is_empty() {
let first_visible = edge_elems
.iter()
.find(|e| !is_hidden(**e) && attr(**e, "stroke").unwrap_or("solid") != "none")
.or_else(|| edge_elems.first())
.copied();
let first = first_visible.unwrap_or(edge_elems[0]);
if let Some(color) = find_first_child_by_name(first, "color") {
if let Some(rgb) = parse_xfa_color(color) {
style.border_color = Some(rgb);
}
}
let stroke = attr(first, "stroke").unwrap_or("solid");
if stroke != "none" {
let thickness = attr(first, "thickness")
.and_then(Measurement::parse)
.map(|m: Measurement| m.to_points())
.unwrap_or(0.5);
if thickness > 0.0 {
style.border_width_pt = Some(thickness);
}
}
let edge_visible = |e: &roxmltree::Node<'_, '_>| -> bool {
!is_hidden(*e) && attr(*e, "stroke").unwrap_or("solid") != "none"
};
style.border_edges = match edge_elems.len() {
0 => [true, true, true, true],
1 => {
let v = edge_visible(&edge_elems[0]);
[v, v, v, v]
}
2 => {
let even = edge_visible(&edge_elems[0]);
let odd = edge_visible(&edge_elems[1]);
[even, odd, even, odd]
}
3 => {
let top = edge_visible(&edge_elems[0]);
let rl = edge_visible(&edge_elems[1]);
let bot = edge_visible(&edge_elems[2]);
[top, rl, bot, rl]
}
_ => [
edge_visible(&edge_elems[0]),
edge_visible(&edge_elems[1]),
edge_visible(&edge_elems[2]),
edge_visible(&edge_elems[3]),
],
};
if edge_elems.len() > 1 {
let default_rgb = style.border_color.unwrap_or((0, 0, 0));
let edge_color = |edge: roxmltree::Node<'_, '_>| -> (u8, u8, u8) {
find_first_child_by_name(edge, "color")
.and_then(parse_xfa_color)
.unwrap_or(default_rgb)
};
let per_edge_colors = match edge_elems.len() {
2 => Some([
edge_color(edge_elems[0]),
edge_color(edge_elems[1]),
edge_color(edge_elems[0]),
edge_color(edge_elems[1]),
]),
3 => Some([
edge_color(edge_elems[0]),
edge_color(edge_elems[1]),
edge_color(edge_elems[2]),
edge_color(edge_elems[1]),
]),
_ => Some([
edge_color(edge_elems[0]),
edge_color(edge_elems[1]),
edge_color(edge_elems[2]),
edge_color(edge_elems[3]),
]),
};
if let Some([top, right, bottom, left]) = per_edge_colors {
if !(top == bottom && bottom == left && left == right) {
style.border_colors = Some([top, right, bottom, left]);
}
}
let default_thickness = style.border_width_pt.unwrap_or(0.5);
let edge_thickness = |edge: roxmltree::Node<'_, '_>| -> f64 {
attr(edge, "thickness")
.and_then(Measurement::parse)
.map(|m: Measurement| m.to_points())
.unwrap_or(default_thickness)
};
let per_edge_widths = match edge_elems.len() {
2 => Some([
edge_thickness(edge_elems[0]),
edge_thickness(edge_elems[1]),
edge_thickness(edge_elems[0]),
edge_thickness(edge_elems[1]),
]),
3 => Some([
edge_thickness(edge_elems[0]),
edge_thickness(edge_elems[1]),
edge_thickness(edge_elems[2]),
edge_thickness(edge_elems[1]),
]),
_ => Some([
edge_thickness(edge_elems[0]),
edge_thickness(edge_elems[1]),
edge_thickness(edge_elems[2]),
edge_thickness(edge_elems[3]),
]),
};
if let Some([top_t, right_t, bottom_t, left_t]) = per_edge_widths {
if !(top_t == bottom_t && bottom_t == left_t && left_t == right_t) {
style.border_widths = Some([top_t, right_t, bottom_t, left_t]);
}
}
}
}
if style.bg_color.is_none() {
if let Some(fill) = find_first_child_by_name(border, "fill") {
if !is_hidden(fill) {
style.bg_color = parse_fill_color(fill);
}
}
}
}
if let Some(font) = find_first_child_by_name(elem, "font") {
if let Some(typeface) = attr(font, "typeface") {
style.font_family = Some(typeface.to_string());
}
if let Some(gf) = attr(font, "genericFamily") {
style.generic_family = Some(gf.to_string());
}
if let Some(size_str) = attr(font, "size") {
if let Some(m) = Measurement::parse(size_str) {
let m: Measurement = m;
style.font_size = Some(m.to_points());
}
}
if let Some(weight) = attr(font, "weight") {
style.font_weight = Some(weight.to_string());
}
if let Some(posture) = attr(font, "posture") {
style.font_style = Some(posture.to_string());
}
if let Some(fill) = find_first_child_by_name(font, "fill") {
if let Some(color) = find_first_child_by_name(fill, "color") {
if let Some(rgb) = parse_xfa_color(color) {
style.text_color = Some(rgb);
}
}
}
if style.text_color.is_none() {
if let Some(color_str) = attr(font, "color") {
if let Some(rgb) = parse_font_color_attr(color_str) {
style.text_color = Some(rgb);
}
}
}
if let Some(scale_str) = attr(font, "fontHorizontalScale") {
if let Some(v) = parse_percentage(scale_str) {
style.font_horizontal_scale = Some(v);
}
}
if let Some(ls_str) = attr(font, "letterSpacing") {
if let Some(v) = parse_letter_spacing(ls_str, style.font_size.unwrap_or(10.0)) {
style.letter_spacing_pt = Some(v);
}
}
if let Some(underline_str) = attr(font, "underline") {
style.underline = underline_str == "1" || underline_str == "2";
}
if let Some(line_through_str) = attr(font, "lineThrough") {
style.line_through = line_through_str == "1";
}
}
if let Some(para) = find_first_child_by_name(elem, "para") {
if let Some(v) = attr(para, "spaceAbove").and_then(Measurement::parse) {
style.space_above_pt = Some(v.to_points());
}
if let Some(v) = attr(para, "spaceBelow").and_then(Measurement::parse) {
style.space_below_pt = Some(v.to_points());
}
if let Some(v) = attr(para, "marginLeft").and_then(Measurement::parse) {
style.margin_left_pt = Some(v.to_points());
}
if let Some(v) = attr(para, "marginRight").and_then(Measurement::parse) {
style.margin_right_pt = Some(v.to_points());
}
if let Some(va) = attr(para, "vAlign") {
style.v_align = Some(match va {
"middle" => VerticalAlign::Middle,
"bottom" => VerticalAlign::Bottom,
_ => VerticalAlign::Top,
});
}
}
if let Some(border) = border {
if let Some(corner) = find_first_child_by_name(border, "corner") {
if let Some(v) = attr(corner, "radius").and_then(Measurement::parse) {
style.border_radius_pt = Some(v.to_points());
}
}
let edge_elems2: Vec<_> = border
.children()
.filter(|n| n.is_element() && n.tag_name().name() == "edge")
.collect();
if !edge_elems2.is_empty() {
if let Some(stroke) = attr(edge_elems2[0], "stroke") {
if stroke != "none" {
style.border_style = Some(stroke.to_string());
}
}
}
}
if let Some(cap) = parse_caption(elem) {
style.caption_text = Some(cap.text);
style.caption_placement = Some(
match cap.placement {
CaptionPlacement::Left => "left",
CaptionPlacement::Right => "right",
CaptionPlacement::Top => "top",
CaptionPlacement::Bottom => "bottom",
CaptionPlacement::Inline => "inline",
}
.to_string(),
);
style.caption_reserve = cap.reserve;
}
style
}
fn parse_fill_color(fill_node: Node<'_, '_>) -> Option<(u8, u8, u8)> {
find_first_child_by_name(fill_node, "color")
.and_then(parse_xfa_color)
.or_else(|| {
find_first_child_by_name(fill_node, "solid")
.and_then(|solid| find_first_child_by_name(solid, "color"))
.and_then(parse_xfa_color)
})
}
fn parse_check_button_mark(elem: Node<'_, '_>) -> Option<String> {
let ui = find_first_child_by_name(elem, "ui")?;
let check_button = ui
.children()
.find(|n| n.is_element() && n.tag_name().name() == "checkButton")?;
let mark = attr(check_button, "mark")?.to_ascii_lowercase();
match mark.as_str() {
"check" | "circle" | "cross" | "diamond" | "square" | "star" => Some(mark),
_ => None,
}
}
fn parse_check_button_values(
elem: Node<'_, '_>,
) -> (Option<String>, Option<String>, Option<String>) {
let ui = match find_first_child_by_name(elem, "ui") {
Some(ui) => ui,
None => return (None, None, None),
};
let check_button = match find_first_child_by_name(ui, "checkButton") {
Some(check_button) => check_button,
None => return (None, None, None),
};
let items = find_first_child_by_name(elem, "items")
.or_else(|| find_first_child_by_name(check_button, "items"));
let Some(items) = items else {
return (None, None, None);
};
let values: Vec<String> = items
.children()
.filter(|n| n.is_element())
.map(|child| child.text().unwrap_or("").trim().to_string())
.collect();
(
values.first().cloned(),
values.get(1).cloned(),
values.get(2).cloned(),
)
}
fn parse_xfa_color(color_node: Node<'_, '_>) -> Option<(u8, u8, u8)> {
let value = attr(color_node, "value")?;
let parts: Vec<&str> = value.split(',').collect();
if parts.len() >= 3 {
let r = parts[0].trim().parse::<u8>().ok()?;
let g = parts[1].trim().parse::<u8>().ok()?;
let b = parts[2].trim().parse::<u8>().ok()?;
Some((r, g, b))
} else {
None
}
}
fn parse_font_color_attr(s: &str) -> Option<(u8, u8, u8)> {
let s = s.trim();
if let Some(hex) = s.strip_prefix('#') {
parse_ascii_hex_color(hex)
} else {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() >= 3 {
let r = parts[0].trim().parse::<u8>().ok()?;
let g = parts[1].trim().parse::<u8>().ok()?;
let b = parts[2].trim().parse::<u8>().ok()?;
Some((r, g, b))
} else {
None
}
}
}
#[allow(dead_code)]
fn parse_bind_data_name(elem: Node<'_, '_>) -> Option<String> {
let bind = find_first_child_by_name(elem, "bind")?;
let ref_val = attr(bind, "ref")?;
let segment = ref_val.rsplit('.').next().unwrap_or(ref_val);
let name = segment.split('[').next().unwrap_or(segment).trim();
if name.is_empty() || name == "$" {
None
} else {
Some(name.to_string())
}
}
fn parse_bind(elem: Node<'_, '_>) -> (Option<String>, bool) {
let Some(bind) = find_first_child_by_name(elem, "bind") else {
return (None, false);
};
let bind_none = attr(bind, "match") == Some("none");
let bind_ref = if bind_none {
None
} else {
attr(bind, "ref").map(|s| s.trim().to_string())
};
(bind_ref, bind_none)
}
#[cfg(test)]
mod tests {
use super::*;
use xfa_dom_resolver::data_dom::DataDom;
#[test]
fn parse_css_color_rejects_non_ascii_hex_without_panicking() {
assert_eq!(parse_css_color("#€"), None);
assert_eq!(parse_css_color("#€abc"), None);
assert_eq!(parse_css_color("#0f8"), Some((0x00, 0xff, 0x88)));
assert_eq!(parse_css_color("#00ff88"), Some((0x00, 0xff, 0x88)));
}
#[test]
fn parse_font_color_attr_rejects_non_ascii_hex_without_panicking() {
assert_eq!(parse_font_color_attr("#€"), None);
assert_eq!(parse_font_color_attr("#€abc"), None);
assert_eq!(parse_font_color_attr("#0f8"), Some((0x00, 0xff, 0x88)));
assert_eq!(parse_font_color_attr("#00ff88"), Some((0x00, 0xff, 0x88)));
}
#[test]
fn repeating_subform_expands_from_data() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<subform name="Orders" layout="tb" w="500pt">
<subform name="Order" layout="position" w="500pt" h="60pt">
<occur min="0" max="10" initial="1"/>
<field name="Item" w="200pt" h="20pt" x="0pt" y="0pt"/>
<field name="Qty" w="100pt" h="20pt" x="200pt" y="0pt"/>
</subform>
</subform>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form1>
<Order><Item>Widget A</Item><Qty>5</Qty></Order>
<Order><Item>Widget B</Item><Qty>3</Qty></Order>
<Order><Item>Widget C</Item><Qty>7</Qty></Order>
</form1>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let orders_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "Orders")
.map(|(i, _)| FormNodeId(i))
.unwrap();
let orders = tree.get(orders_id);
assert_eq!(orders.children.len(), 3);
assert!(orders
.children
.iter()
.all(|&id| tree.get(id).name == "Order" && tree.get(id).occur.count() == 1));
}
#[test]
fn repeating_subform_double_wrapped_data() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<pageSet>
<pageArea name="Page">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<subform name="Orders" layout="tb" w="559pt">
<subform name="CoreOrders" layout="tb" w="361pt">
<subform name="Order" layout="position" w="360pt" h="162pt">
<occur min="0" max="3" initial="1"/>
<field name="Item" w="200pt" h="25pt" x="9pt" y="9pt"/>
</subform>
</subform>
</subform>
</subform>
</template>"#;
let data_xml = concat!(
r#"<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">"#,
r#"<xfa:data><xfa:data xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">"#,
r#"<form1>"#,
r#"<Order><Item>A</Item></Order>"#,
r#"<Order><Item>B</Item></Order>"#,
r#"<Order><Item>C</Item></Order>"#,
r#"</form1>"#,
r#"</xfa:data></xfa:data></xfa:datasets>"#,
);
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let core_orders_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "CoreOrders")
.map(|(i, _)| FormNodeId(i))
.unwrap();
let core_orders = tree.get(core_orders_id);
assert_eq!(
core_orders.children.len(),
3,
"Expected 3 Order instances from double-wrapped data"
);
}
#[test]
fn repeating_subform_bind_ref_resolves_data_name() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<subform name="ListItems" layout="tb" w="500pt">
<subform name="ItemGroup" layout="tb" w="500pt" h="50pt">
<occur min="0" max="-1"/>
<bind match="dataRef" ref="$.itemGroup[*]"/>
<field name="title" w="200pt" h="20pt" x="0pt" y="0pt"/>
</subform>
</subform>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form>
<itemGroup><title>Group A</title></itemGroup>
<itemGroup><title>Group B</title></itemGroup>
</form>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let list_items = tree
.nodes
.iter()
.find(|n| n.name == "ListItems")
.expect("ListItems must exist");
assert_eq!(
list_items.children.len(),
2,
"Expected 2 instances from bind ref $.itemGroup[*]"
);
}
#[test]
fn repeating_table_rows_expand_as_sibling_rows() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<subform name="Table1" layout="table" columnWidths="100pt 100pt 100pt">
<subform name="HeaderRow" layout="row">
<draw name="H1" w="100pt" h="20pt"/>
<draw name="H2" w="100pt" h="20pt"/>
<draw name="H3" w="100pt" h="20pt"/>
</subform>
<subform name="Row1" layout="row">
<occur min="0" max="-1"/>
<field name="Cell1" w="100pt" h="20pt"/>
<field name="Cell2" w="100pt" h="20pt"/>
<field name="Cell3" w="100pt" h="20pt"/>
</subform>
</subform>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form1>
<Table1>
<Row1><Cell1>A1</Cell1><Cell2>B1</Cell2><Cell3>C1</Cell3></Row1>
<Row1><Cell1>A2</Cell1><Cell2>B2</Cell2><Cell3>C2</Cell3></Row1>
<Row1><Cell1>A3</Cell1><Cell2>B3</Cell2><Cell3>C3</Cell3></Row1>
<Row1><Cell1>A4</Cell1><Cell2>B4</Cell2><Cell3>C4</Cell3></Row1>
</Table1>
</form1>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let table_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "Table1")
.map(|(i, _)| FormNodeId(i))
.unwrap();
let table = tree.get(table_id);
assert_eq!(table.children.len(), 5, "header + 4 repeated data rows");
assert_eq!(tree.get(table.children[0]).name, "HeaderRow");
assert!(table.children[1..]
.iter()
.all(|&id| tree.get(id).name == "Row1" && tree.get(id).children.len() == 3));
}
#[test]
fn repeating_subform_clamps_to_occur_max() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<subform name="Orders" layout="tb">
<subform name="Order" layout="position" w="200pt" h="20pt">
<occur min="0" max="2"/>
<field name="Item" w="100pt" h="20pt"/>
</subform>
</subform>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form1>
<Order><Item>A</Item></Order>
<Order><Item>B</Item></Order>
<Order><Item>C</Item></Order>
<Order><Item>D</Item></Order>
<Order><Item>E</Item></Order>
</form1>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let orders_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "Orders")
.map(|(i, _)| FormNodeId(i))
.unwrap();
assert_eq!(tree.get(orders_id).children.len(), 2);
}
#[test]
fn repeating_subform_unbounded_uses_data_count() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<subform name="Orders" layout="tb">
<subform name="Order" layout="position" w="200pt" h="20pt">
<occur min="0" max="-1"/>
<field name="Item" w="100pt" h="20pt"/>
</subform>
</subform>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form1>
<Order><Item>A</Item></Order>
<Order><Item>B</Item></Order>
<Order><Item>C</Item></Order>
</form1>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let orders_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "Orders")
.map(|(i, _)| FormNodeId(i))
.unwrap();
assert_eq!(tree.get(orders_id).children.len(), 3);
}
#[test]
fn repeating_subform_respects_occur_min() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<subform name="Orders" layout="tb">
<subform name="Order" layout="position" w="200pt" h="20pt">
<occur min="2" max="-1"/>
<field name="Item" w="100pt" h="20pt"/>
</subform>
</subform>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form1/>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let orders_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "Orders")
.map(|(i, _)| FormNodeId(i))
.unwrap();
let orders = tree.get(orders_id);
assert_eq!(orders.children.len(), 2);
assert!(orders
.children
.iter()
.all(|&id| tree.get(id).name == "Order"));
}
#[test]
fn root_subform_binds_to_matching_data_root() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<field name="title" w="200pt" h="20pt" x="0pt" y="0pt"/>
<field name="code" w="200pt" h="20pt" x="0pt" y="20pt"/>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form>
<title>Hello World</title>
<code>42</code>
</form>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let title_node = tree
.nodes
.iter()
.find(|n| n.name == "title")
.expect("title field must exist");
match &title_node.node_type {
FormNodeType::Field { value } => {
assert_eq!(value, "Hello World", "title should bind to data root");
}
_ => panic!("title should be a field"),
}
}
#[test]
fn check_button_mark_parsed_into_style() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<field name="agree" w="20pt" h="20pt">
<ui><checkButton mark="circle"/></ui>
<value><text>1</text></value>
<items><text>1</text><text>0</text><text>2</text></items>
</field>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form>
<agree>1</agree>
</form>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let agree_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "agree")
.map(|(i, _)| FormNodeId(i))
.expect("agree field must exist");
let meta = tree.meta(agree_id);
assert_eq!(meta.field_kind, FieldKind::Checkbox);
assert_eq!(meta.style.check_button_mark.as_deref(), Some("circle"));
assert_eq!(meta.style.check_button_on_value.as_deref(), Some("1"));
assert_eq!(meta.style.check_button_off_value.as_deref(), Some("0"));
assert_eq!(meta.style.check_button_neutral_value.as_deref(), Some("2"));
}
#[test]
fn nested_check_button_items_are_used_for_checkbox_values() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<field name="agree" w="20pt" h="20pt">
<ui>
<checkButton>
<items><text>Yes</text><text>No</text></items>
</checkButton>
</ui>
<value><text>Yes</text></value>
</field>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form>
<agree>Yes</agree>
</form>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let agree_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "agree")
.map(|(i, _)| FormNodeId(i))
.expect("agree field must exist");
let meta = tree.meta(agree_id);
assert_eq!(meta.field_kind, FieldKind::Checkbox);
assert_eq!(meta.style.check_button_on_value.as_deref(), Some("Yes"));
assert_eq!(meta.style.check_button_off_value.as_deref(), Some("No"));
match &tree.get(agree_id).node_type {
FormNodeType::Field { value } => assert_eq!(value, "Yes"),
_ => panic!("agree should be a field"),
}
}
#[test]
fn border_fill_solid_color_populates_background_color() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<field name="name" w="200pt" h="20pt">
<ui><textEdit/></ui>
<border>
<fill>
<solid>
<color value="240,240,240"/>
</solid>
</fill>
</border>
</field>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form>
<name>Jane Doe</name>
</form>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let name_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "name")
.map(|(i, _)| FormNodeId(i))
.expect("name field must exist");
assert_eq!(tree.meta(name_id).style.bg_color, Some((240, 240, 240)));
}
#[test]
fn border_fill_direct_color_populates_button_bg_color() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<field name="Btn" w="25.4mm" h="9.525mm">
<ui><button highlight="inverted"/></ui>
<caption><value><text>E-Mail</text></value></caption>
<border hand="right">
<edge stroke="raised"/>
<fill><color value="255,255,153"/></fill>
</border>
</field>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data><form/></xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let btn_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "Btn")
.map(|(i, _)| FormNodeId(i))
.expect("button field must exist");
assert_eq!(tree.meta(btn_id).style.bg_color, Some((255, 255, 153)));
}
#[test]
fn rich_text_exdata_captions_are_collapsed_to_plain_text() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<field name="patient_name" w="200pt" h="20pt">
<ui><textEdit/></ui>
<caption placement="top" reserve="0.230972in">
<value>
<exData contentType="text/html" maxLength="0">
<body xmlns="http://www.w3.org/1999/xhtml"
xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"
xfa:APIVersion="1.4.4136.0">
<p>NAME<span style="xfa-spacerun:yes"> </span><span style="font-style:italic">(Last, First, Middle Initial)</span></p>
</body>
</exData>
</value>
</caption>
</field>
<field name="telephone" w="200pt" h="20pt">
<ui><textEdit/></ui>
<caption placement="top" reserve="0.230972in">
<value>
<exData contentType="text/html" maxLength="0">
<body xmlns="http://www.w3.org/1999/xhtml"
xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"
xfa:APIVersion="1.4.4136.0">
<p>TELEPHONE<span style="font-style:italic"> (Include area code)</span></p>
</body>
</exData>
</value>
</caption>
</field>
</subform>
</template>"#;
let data_dom = DataDom::new();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let patient_name_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "patient_name")
.map(|(i, _)| FormNodeId(i))
.expect("patient_name field must exist");
let patient_name = tree.meta(patient_name_id);
assert_eq!(
patient_name.style.caption_text.as_deref(),
Some("NAME (Last, First, Middle Initial)")
);
assert_eq!(patient_name.style.caption_placement.as_deref(), Some("top"));
assert_eq!(
tree.get(patient_name_id)
.box_model
.caption
.as_ref()
.map(|caption| caption.text.as_str()),
Some("NAME (Last, First, Middle Initial)")
);
let telephone_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "telephone")
.map(|(i, _)| FormNodeId(i))
.expect("telephone field must exist");
let telephone = tree.meta(telephone_id);
assert_eq!(
telephone.style.caption_text.as_deref(),
Some("TELEPHONE (Include area code)")
);
assert_eq!(telephone.style.caption_placement.as_deref(), Some("top"));
assert_eq!(
tree.get(telephone_id)
.box_model
.caption
.as_ref()
.map(|caption| caption.text.as_str()),
Some("TELEPHONE (Include area code)")
);
}
#[test]
fn border_edge_order_keeps_bottom_only_underline() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<field name="underlined" w="200pt" h="20pt">
<ui><textEdit/></ui>
<border>
<edge stroke="none" thickness="1pt"/>
<edge stroke="none" thickness="2pt"/>
<edge stroke="solid" thickness="3pt"/>
<edge stroke="none" thickness="4pt"/>
</border>
</field>
</subform>
</template>"#;
let data_dom = DataDom::new();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let underlined_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "underlined")
.map(|(i, _)| FormNodeId(i))
.expect("underlined field must exist");
let style = &tree.meta(underlined_id).style;
assert_eq!(style.border_edges, [false, false, true, false]);
assert_eq!(style.border_widths, Some([1.0, 2.0, 3.0, 4.0]));
assert_eq!(style.border_width_pt, Some(3.0));
}
#[test]
fn round_check_button_parsed_as_radio() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<field name="choice" w="20pt" h="20pt">
<ui><checkButton shape="round"/></ui>
<items><text>Y</text><text>N</text></items>
</field>
</subform>
</template>"#;
let data_dom = DataDom::new();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let choice_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "choice")
.map(|(i, _)| FormNodeId(i))
.expect("choice field must exist");
assert_eq!(tree.meta(choice_id).field_kind, FieldKind::Radio);
}
#[test]
fn excl_group_value_selects_matching_child() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<exclGroup name="choice">
<field name="yes" w="20pt" h="20pt">
<ui><checkButton shape="round"/></ui>
<items><text>Y</text><text>N</text></items>
</field>
<field name="no" w="20pt" h="20pt">
<ui><checkButton shape="round"/></ui>
<items><text>N</text><text>Y</text></items>
</field>
</exclGroup>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form>
<choice>Y</choice>
</form>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let yes_node = tree
.nodes
.iter()
.find(|n| n.name == "yes")
.expect("yes field must exist");
let no_node = tree
.nodes
.iter()
.find(|n| n.name == "no")
.expect("no field must exist");
match (&yes_node.node_type, &no_node.node_type) {
(FormNodeType::Field { value: yes }, FormNodeType::Field { value: no }) => {
assert_eq!(yes, "Y");
assert!(
no.is_empty(),
"non-selected exclGroup child should be cleared"
);
}
_ => panic!("exclGroup children must be fields"),
}
}
#[test]
fn draw_with_image_creates_image_node() {
let image_b64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
let template = format!(
r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<draw name="logo" x="0pt" y="0pt" w="100pt" h="100pt">
<value>
<image contentType="image/png">{}</image>
</value>
</draw>
</subform>
</template>"#,
image_b64
);
let data_dom = DataDom::new();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(&template).unwrap();
let logo_node = tree
.nodes
.iter()
.find(|n| n.name == "logo")
.expect("logo draw must exist");
match &logo_node.node_type {
FormNodeType::Image { data, mime_type } => {
assert_eq!(mime_type, "image/png");
assert!(!data.is_empty(), "image data should not be empty");
assert!(
data.starts_with(&[0x89, 0x50, 0x4E, 0x47]),
"decoded data should be PNG"
);
}
_ => panic!(
"draw with image should create Image node, got {:?}",
logo_node.node_type
),
}
}
#[test]
fn draw_with_rectangle_creates_draw_node() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<draw name="box" x="0pt" y="0pt" w="100pt" h="100pt">
<value>
<rectangle/>
</value>
</draw>
</subform>
</template>"#;
let data_dom = DataDom::new();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let box_node = tree
.nodes
.iter()
.find(|n| n.name == "box")
.expect("box draw must exist");
match &box_node.node_type {
FormNodeType::Draw(DrawContent::Rectangle { .. }) => {}
other => panic!(
"draw with rectangle should create Rectangle draw, got {:?}",
other
),
}
}
#[test]
fn field_anchor_type_is_preserved_in_merged_meta() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<field name="centeredBox" layout="position" anchorType="middleCenter" w="20pt" h="10pt" x="100pt" y="50pt"/>
</subform>
</template>"#;
let data_dom = DataDom::new();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let centered_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "centeredBox")
.map(|(i, _)| FormNodeId(i))
.expect("centeredBox field must exist");
assert_eq!(tree.meta(centered_id).anchor_type, AnchorType::MiddleCenter);
}
#[test]
fn area_without_layout_uses_positioned_when_children_have_coordinates() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<area name="Nagl">
<draw name="headerLine" x="10pt" y="12pt" w="100pt" h="1pt">
<value><rectangle/></value>
</draw>
</area>
</subform>
</template>"#;
let data_dom = DataDom::new();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let nagl = tree
.nodes
.iter()
.find(|n| n.name == "Nagl")
.expect("Nagl area must exist");
assert_eq!(nagl.layout, LayoutStrategy::Positioned);
}
#[test]
fn merge_mode_defaults_to_consume_data() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb"/>
</template>"#;
assert_eq!(detect_merge_mode(template), MergeMode::ConsumeData);
}
#[test]
fn merge_mode_detects_match_template() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<!-- config: matchTemplate -->
<subform name="form1" layout="tb"/>
</template>"#;
assert_eq!(detect_merge_mode(template), MergeMode::MatchTemplate);
}
#[test]
fn match_template_binds_flat_data_to_named_fields() {
let template = r#"<?xml version="1.0"?>
<!-- matchTemplate -->
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<subform name="section" layout="tb">
<field name="firstName" w="200pt" h="20pt" x="0pt" y="0pt"/>
<field name="lastName" w="200pt" h="20pt" x="0pt" y="20pt"/>
</subform>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form1>
<firstName>John</firstName>
<lastName>Doe</lastName>
</form1>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let first = tree
.nodes
.iter()
.find(|n| n.name == "firstName")
.expect("firstName must exist");
let last = tree
.nodes
.iter()
.find(|n| n.name == "lastName")
.expect("lastName must exist");
match &first.node_type {
FormNodeType::Field { value } => assert_eq!(value, "John"),
_ => panic!("firstName must be a field"),
}
match &last.node_type {
FormNodeType::Field { value } => assert_eq!(value, "Doe"),
_ => panic!("lastName must be a field"),
}
}
#[test]
fn field_binds_via_sibling_scope() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<subform name="address" layout="tb">
<subform name="details" layout="tb">
<field name="city" w="200pt" h="20pt" x="0pt" y="0pt"/>
</subform>
</subform>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form1>
<address>
<city>Rotterdam</city>
<details/>
</address>
</form1>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let city = tree
.nodes
.iter()
.find(|n| n.name == "city")
.expect("city field must exist");
match &city.node_type {
FormNodeType::Field { value } => assert_eq!(
value, "Rotterdam",
"city should be found via sibling scope resolution"
),
_ => panic!("city should be a field"),
}
}
#[test]
fn transparent_subform_passes_data_context_through() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<subform layout="tb">
<field name="country" w="200pt" h="20pt" x="0pt" y="0pt"/>
</subform>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form1>
<country>Netherlands</country>
</form1>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let country = tree
.nodes
.iter()
.find(|n| n.name == "country")
.expect("country field must exist");
match &country.node_type {
FormNodeType::Field { value } => assert_eq!(
value, "Netherlands",
"transparent subform should pass context through"
),
_ => panic!("country should be a field"),
}
}
#[test]
fn nested_transparent_subforms_bind_correctly() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<subform layout="tb">
<subform layout="tb">
<field name="postalCode" w="100pt" h="20pt" x="0pt" y="0pt"/>
</subform>
</subform>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form1>
<postalCode>1234AB</postalCode>
</form1>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let postal = tree
.nodes
.iter()
.find(|n| n.name == "postalCode")
.expect("postalCode field must exist");
match &postal.node_type {
FormNodeType::Field { value } => assert_eq!(value, "1234AB"),
_ => panic!("postalCode should be a field"),
}
}
#[test]
fn repeating_subform_three_items_creates_three_instances() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<subform name="row" layout="position" w="500pt" h="30pt">
<occur min="0" max="-1" initial="0"/>
<field name="label" w="200pt" h="20pt" x="0pt" y="0pt"/>
</subform>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form1>
<row><label>Alpha</label></row>
<row><label>Beta</label></row>
<row><label>Gamma</label></row>
</form1>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let rows: Vec<_> = tree.nodes.iter().filter(|n| n.name == "row").collect();
assert_eq!(rows.len(), 3, "must have exactly 3 row instances");
}
#[test]
fn occur_min1_max1_limits_to_single_instance() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<subform name="item" layout="position" w="500pt" h="30pt">
<occur min="1" max="1"/>
<field name="val" w="100pt" h="20pt" x="0pt" y="0pt"/>
</subform>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form1>
<item><val>X</val></item>
<item><val>Y</val></item>
<item><val>Z</val></item>
</form1>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let items: Vec<_> = tree.nodes.iter().filter(|n| n.name == "item").collect();
assert_eq!(items.len(), 1, "occur max=1 must limit to one instance");
}
#[test]
fn field_with_presence_hidden_stays_hidden() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<field name="secretField" presence="hidden" w="100pt" h="20pt" x="0pt" y="0pt"/>
</subform>
</template>"#;
let data_dom = DataDom::new();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let secret_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "secretField")
.map(|(i, _)| FormNodeId(i))
.expect("secretField must exist");
assert_eq!(
tree.meta(secret_id).presence,
Presence::Hidden,
"presence=hidden in template must be preserved"
);
}
#[test]
fn field_without_presence_data_defaults_to_visible() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<field name="normalField" w="100pt" h="20pt" x="0pt" y="0pt"/>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form1>
<normalField>hello</normalField>
</form1>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let field_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "normalField")
.map(|(i, _)| FormNodeId(i))
.expect("normalField must exist");
assert_eq!(
tree.meta(field_id).presence,
Presence::Visible,
"field with no presence data must default to Visible"
);
}
#[test]
fn field_binds_via_ancestor_scope_walk() {
let template = r#"<?xml version="1.0"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="595pt" h="842pt"/>
<medium short="595pt" long="842pt"/>
</pageArea>
</pageSet>
<subform name="details" layout="tb">
<field name="city" w="200pt" h="20pt" x="0pt" y="0pt"/>
</subform>
</subform>
</template>"#;
let data_xml = r#"<?xml version="1.0"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form1>
<city>Amsterdam</city>
<details/>
</form1>
</xfa:data>
</xfa:datasets>"#;
let data_dom = DataDom::from_xml(data_xml).unwrap();
let merger = FormMerger::new(&data_dom);
let (tree, _root_id) = merger.merge(template).unwrap();
let city = tree
.nodes
.iter()
.find(|n| n.name == "city")
.expect("city field must exist");
match &city.node_type {
FormNodeType::Field { value } => {
assert_eq!(
value, "Amsterdam",
"city should be found via ancestor scope walk"
);
}
_ => panic!("city should be a field"),
}
}
}