use roxmltree::Node;
use xfa_layout_engine::form::{
AnchorType, ContentArea, DrawContent, EventScript, FieldKind, FormNode, FormNodeId,
FormNodeMeta, FormNodeStyle, FormNodeType, FormTree, GroupKind, Occur, Presence,
ScriptLanguage,
};
use xfa_layout_engine::text::{FontFamily, FontMetrics};
use xfa_layout_engine::types::{
BoxModel, Caption, CaptionPlacement, Insets, LayoutStrategy, Measurement, TextAlign,
VerticalAlign,
};
use crate::error::{Result, XfaError};
pub fn parse_template(xml: &str, datasets_xml: Option<&str>) -> Result<(FormTree, FormNodeId)> {
let doc = roxmltree::Document::parse(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()))?
};
let mut tree = FormTree::new();
let (root_id, _trailing) = parse_node(&mut tree, template_elem, true)?;
if let Some(ds_xml) = datasets_xml {
if let Ok(ds_doc) = roxmltree::Document::parse(ds_xml) {
if let Some(data_root) = find_data_root(ds_doc.root_element()) {
bind_data(&mut tree, root_id, &data_root, &data_root);
}
}
}
Ok((tree, root_id))
}
fn parse_node(
tree: &mut FormTree,
elem: Node<'_, '_>,
is_root: bool,
) -> Result<(FormNodeId, (bool, Option<String>))> {
let tag = elem.tag_name().name();
let (node, trailing_info) = match tag {
"template" => {
let mut n = parse_root_node(tree, elem)?;
let ti = add_children(tree, &mut n, elem)?;
(n, ti)
}
"subform" | "exclGroup" | "area" => {
let mut n = parse_subform_node(tree, elem, is_root)?;
let ti = add_children(tree, &mut n, elem)?;
if tag == "exclGroup" {
for &child_id in &n.children {
let child_meta = tree.meta_mut(child_id);
if child_meta.field_kind == FieldKind::Checkbox {
child_meta.field_kind = FieldKind::Radio;
}
}
}
(n, ti)
}
"field" => (parse_field(tree, elem)?, (false, None)),
"draw" => (parse_draw(tree, elem)?, (false, None)),
"pageSet" => (parse_page_set(tree, elem)?, (false, None)),
"pageArea" => (parse_page_area(tree, elem)?, (false, None)),
_ => {
let mut n = blank_node(tag);
let ti = add_children(tree, &mut n, elem)?;
(n, ti)
}
};
let mut meta = parse_node_meta(elem);
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);
}
Ok((tree.add_node_with_meta(node, meta), trailing_info))
}
fn parse_root_node(_tree: &mut FormTree, _elem: Node<'_, '_>) -> Result<FormNode> {
Ok(FormNode {
name: "root".to_string(),
node_type: FormNodeType::Root,
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 parse_subform_node(
_tree: &mut FormTree,
elem: Node<'_, '_>,
_is_root: bool,
) -> Result<FormNode> {
let name = attr(elem, "name").unwrap_or("").to_string();
let layout = parse_layout_attr(elem);
let mut bm = parse_box_model(elem);
if layout == LayoutStrategy::TopToBottom && bm.width.is_none() {
bm.width = Some(612.0);
}
Ok(FormNode {
name,
node_type: FormNodeType::Subform,
box_model: bm,
layout,
children: Vec::new(),
occur: parse_occur(elem),
font: FontMetrics::default(),
calculate: None,
validate: None,
column_widths: Vec::new(),
col_span: 1,
})
}
fn parse_field(tree: &mut FormTree, elem: Node<'_, '_>) -> Result<FormNode> {
let name = attr(elem, "name").unwrap_or("").to_string();
let bm = parse_box_model(elem);
let value = extract_value_text(elem).unwrap_or_default();
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;
}
let node = 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),
};
let _ = tree; Ok(node)
}
fn parse_draw(tree: &mut FormTree, elem: Node<'_, '_>) -> 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) {
let node = 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,
};
let _ = tree;
return Ok(node);
}
if let Some((image_data, mime_type)) = extract_value_image(elem) {
let node = 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 _ = tree;
return Ok(node);
}
let mut font = parse_font_metrics(elem);
if let Some(html_size) = extract_exdata_font_size(elem) {
font.size = html_size;
}
let content = extract_value_text(elem).unwrap_or_default();
let node = 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,
};
let _ = tree;
Ok(node)
}
fn is_hidden(elem: Node<'_, '_>) -> bool {
matches!(
attr(elem, "presence"),
Some("hidden") | Some("invisible") | Some("inactive")
)
}
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| 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_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 generic_family_str = font_elem.and_then(|f| attr(f, "genericFamily"));
let typeface = if let Some(gf) = generic_family_str {
FontFamily::from_generic_family(gf)
} else {
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" | "justifyAll" => TextAlign::Justify,
_ => TextAlign::Left,
})
.unwrap_or_default();
FontMetrics {
size,
text_align,
typeface,
..FontMetrics::default()
}
}
fn parse_page_set(tree: &mut FormTree, elem: Node<'_, '_>) -> 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, _) = parse_node(tree, child, false)?;
node.children.push(child_id);
}
}
Ok(node)
}
fn parse_page_area(tree: &mut FormTree, elem: Node<'_, '_>) -> 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,
};
add_children(tree, &mut node, elem)?;
Ok(node)
}
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 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)
}
fn parse_node_style(elem: Node<'_, '_>) -> FormNodeStyle {
let mut style = FormNodeStyle {
check_button_mark: parse_check_button_mark(elem),
..Default::default()
};
if let Some(fill) = find_first_child_by_name(elem, "fill") {
if !is_hidden(fill) {
if let Some(color) = find_first_child_by_name(fill, "color") {
if let Some(rgb) = parse_xfa_color(color) {
style.bg_color = Some(rgb);
}
}
if style.bg_color.is_none() {
if let Some(solid) = find_first_child_by_name(fill, "solid") {
if let Some(color) = find_first_child_by_name(solid, "color") {
if let Some(rgb) = parse_xfa_color(color) {
style.bg_color = Some(rgb);
}
}
}
}
}
}
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 edges: Vec<_> = border
.children()
.filter(|c| c.is_element() && c.tag_name().name() == "edge")
.collect();
let first_visible = edges
.iter()
.find(|e| !is_hidden(**e))
.or_else(|| edges.first());
if let Some(edge) = first_visible {
if let Some(color) = find_first_child_by_name(*edge, "color") {
if let Some(rgb) = parse_xfa_color(color) {
style.border_color = Some(rgb);
}
}
let stroke = attr(*edge, "stroke").unwrap_or("solid");
if stroke != "none" {
let thickness = attr(*edge, "thickness")
.and_then(Measurement::parse)
.map(|m| m.to_points())
.unwrap_or(1.0);
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 edges.len() {
0 => [true, true, true, true],
1 => {
let v = edge_visible(&edges[0]);
[v, v, v, v]
}
2 => {
let even = edge_visible(&edges[0]);
let odd = edge_visible(&edges[1]);
[even, odd, even, odd]
}
3 => {
let top = edge_visible(&edges[0]);
let rl = edge_visible(&edges[1]);
let bot = edge_visible(&edges[2]);
[top, rl, bot, rl]
}
_ => [
edge_visible(&edges[0]),
edge_visible(&edges[1]),
edge_visible(&edges[2]),
edge_visible(&edges[3]),
],
};
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| m.to_points())
.unwrap_or(default_thickness)
};
let per_edge_widths = match edges.len() {
0 | 1 => None,
2 => Some([
edge_thickness(edges[0]),
edge_thickness(edges[1]),
edge_thickness(edges[0]),
edge_thickness(edges[1]),
]),
3 => Some([
edge_thickness(edges[0]),
edge_thickness(edges[1]),
edge_thickness(edges[2]),
edge_thickness(edges[1]),
]),
_ => Some([
edge_thickness(edges[0]),
edge_thickness(edges[1]),
edge_thickness(edges[2]),
edge_thickness(edges[3]),
]),
};
if let Some(widths) = per_edge_widths {
if !(widths[0] == widths[1] && widths[1] == widths[2] && widths[2] == widths[3]) {
style.border_widths = Some(widths);
}
}
if style.bg_color.is_none() {
if let Some(fill) = find_first_child_by_name(border, "fill") {
if !is_hidden(fill) {
if let Some(color) = find_first_child_by_name(fill, "color") {
if let Some(rgb) = parse_xfa_color(color) {
style.bg_color = Some(rgb);
}
}
}
}
}
}
if style.border_color.is_none() {
if let Some(value) = find_first_child_by_name(elem, "value") {
if let Some(rect) = find_first_child_by_name(value, "rectangle") {
let edges: Vec<_> = rect
.children()
.filter(|c| c.is_element() && c.tag_name().name() == "edge")
.collect();
if let Some(edge) = edges.first() {
if let Some(color) = find_first_child_by_name(*edge, "color") {
if let Some(rgb) = parse_xfa_color(color) {
style.border_color = Some(rgb);
}
}
if style.border_width_pt.is_none() {
let thickness = attr(*edge, "thickness")
.and_then(Measurement::parse)
.map(|m| m.to_points())
.unwrap_or(1.0);
if thickness > 0.0 {
style.border_width_pt = Some(thickness);
}
}
}
}
}
}
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) {
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(v) = attr(para, "lineHeight").and_then(Measurement::parse) {
style.line_height_pt = Some(v.to_points());
}
if let Some(v) = attr(para, "textIndent").and_then(Measurement::parse) {
style.text_indent_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(ha) = attr(para, "hAlign") {
style.h_align = Some(match ha {
"center" => TextAlign::Center,
"right" => TextAlign::Right,
_ => TextAlign::Left,
});
}
}
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());
}
}
if let Some(edge) = find_first_child_by_name(border, "edge") {
if let Some(stroke) = attr(edge, "stroke") {
if stroke != "none" {
style.border_style = Some(stroke.to_string());
}
}
}
}
if let Some(format) = find_first_child_by_name(elem, "format") {
if let Some(picture) = find_first_child_by_name(format, "picture") {
if let Some(text) = picture.text() {
let trimmed = text.trim();
if !trimmed.is_empty() {
style.format_pattern = Some(trimmed.to_string());
}
}
}
}
style
}
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_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('#') {
match hex.len() {
6 => {
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
Some((r, g, b))
}
3 => {
let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
Some((r * 17, g * 17, b * 17))
}
_ => 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 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()) {
let tag = child.tag_name().name();
match tag {
"checkButton" => {
let shape = attr(child, "shape").unwrap_or("square");
return if shape == "round" {
FieldKind::Radio
} else {
FieldKind::Checkbox
};
}
"choiceList" => return FieldKind::Dropdown,
"button" => return FieldKind::Button,
"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 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 find_data_root<'a, 'input>(root: Node<'a, 'input>) -> Option<Node<'a, 'input>> {
for child in root.children().filter(|n| n.is_element()) {
if child.tag_name().name() == "data" {
return child.children().find(|n| n.is_element()).or(Some(child));
}
}
if root.tag_name().name() == "data" {
return root.children().find(|n| n.is_element()).or(Some(root));
}
root.children().find(|n| n.is_element())
}
fn bind_data(
tree: &mut FormTree,
node_id: FormNodeId,
data_root: &Node<'_, '_>,
data_node: &Node<'_, '_>,
) {
let name = tree.get(node_id).name.clone();
let children: Vec<FormNodeId> = tree.get(node_id).children.clone();
let meta = tree.meta(node_id).clone();
let group_kind = meta.group_kind;
if group_kind == GroupKind::ExclusiveChoice && !name.is_empty() {
let data_value = lookup_bound_text(data_root, data_node, &meta, &name);
let child_item_vals: Vec<(FormNodeId, Option<String>)> = children
.iter()
.map(|&cid| (cid, tree.meta(cid).item_value.clone()))
.collect();
for (child_id, item_val) in child_item_vals {
if let FormNodeType::Field { ref mut value } = tree.get_mut(child_id).node_type {
if let Some(ref dv) = data_value {
if item_val.as_deref() == Some(dv.as_str()) {
*value = dv.clone();
} else {
*value = String::new();
}
} else {
*value = String::new();
}
}
}
return;
}
if let FormNodeType::Field { ref mut value } = tree.get_mut(node_id).node_type {
if let Some(dv) = lookup_bound_text(data_root, data_node, &meta, &name) {
*value = dv;
}
return; }
let bound_nodes = resolve_bound_nodes(data_root, data_node, &meta, &name);
let effective_data = bound_nodes.first().copied().unwrap_or(*data_node);
let occur = tree.get(node_id).occur.clone();
let max_instances = occur.max.map(|max| max as usize).unwrap_or(usize::MAX);
if max_instances > 1 && !meta.data_bind_none {
let data_instances: Vec<_> = if let Some(bind_ref) = meta.data_bind_ref.as_deref() {
resolve_bind_nodes(data_root, data_node, bind_ref)
} else if !name.is_empty() {
data_node
.children()
.filter(|child| child.is_element() && child.tag_name().name() == name)
.collect()
} else {
Vec::new()
};
if !data_instances.is_empty() {
tree.get_mut(node_id).occur.initial = 1;
}
if data_instances.len() > 1 {
bind_data_children(tree, node_id, &children, data_root, &data_instances[0]);
let parent_id = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.children.contains(&node_id))
.map(|(i, _)| FormNodeId(i));
let mut insert_pos = parent_id.and_then(|pid| {
tree.get(pid)
.children
.iter()
.position(|&c| c == node_id)
.map(|pos| (pid, pos + 1))
});
for data_inst in &data_instances[1..data_instances.len().min(max_instances)] {
let cloned_id = clone_subtree(tree, node_id);
tree.get_mut(cloned_id).occur.initial = 1;
bind_data_children(
tree,
cloned_id,
&tree.get(cloned_id).children.clone(),
data_root,
data_inst,
);
if let Some((pid, pos)) = insert_pos.as_mut() {
let parent = tree.get_mut(*pid);
parent.children.insert(*pos, cloned_id);
*pos += 1;
}
}
return;
}
}
bind_data_children(tree, node_id, &children, data_root, &effective_data);
}
fn bind_data_children(
tree: &mut FormTree,
_parent_id: FormNodeId,
children: &[FormNodeId],
data_root: &Node<'_, '_>,
data_node: &Node<'_, '_>,
) {
for &child_id in children {
bind_data(tree, child_id, data_root, data_node);
}
}
fn clone_subtree(tree: &mut FormTree, source_id: FormNodeId) -> FormNodeId {
let source = tree.get(source_id).clone();
let source_meta = tree.meta(source_id).clone();
let new_children: Vec<FormNodeId> = source
.children
.iter()
.map(|&child_id| clone_subtree(tree, child_id))
.collect();
let mut new_node = source;
new_node.children = new_children;
tree.add_node_with_meta(new_node, source_meta)
}
fn lookup_data_text(data_node: &Node<'_, '_>, name: &str) -> Option<String> {
let child = find_child_element_by_name(data_node, name)?;
child.text().map(|s| s.to_string())
}
fn lookup_bound_text(
data_root: &Node<'_, '_>,
data_node: &Node<'_, '_>,
meta: &FormNodeMeta,
fallback_name: &str,
) -> Option<String> {
if meta.data_bind_none {
return None;
}
if let Some(bind_ref) = meta.data_bind_ref.as_deref() {
return resolve_bind_nodes(data_root, data_node, bind_ref)
.into_iter()
.next()
.and_then(|node| node.text().map(|s| s.to_string()));
}
if fallback_name.is_empty() {
None
} else {
lookup_data_text(data_node, fallback_name)
}
}
fn resolve_bound_nodes<'a, 'input>(
data_root: &Node<'a, 'input>,
data_node: &Node<'a, 'input>,
meta: &FormNodeMeta,
fallback_name: &str,
) -> Vec<Node<'a, 'input>> {
if meta.data_bind_none {
return Vec::new();
}
if let Some(bind_ref) = meta.data_bind_ref.as_deref() {
return resolve_bind_nodes(data_root, data_node, bind_ref);
}
if fallback_name.is_empty() {
Vec::new()
} else {
find_child_element_by_name(data_node, fallback_name)
.into_iter()
.collect()
}
}
fn resolve_bind_nodes<'a, 'input>(
data_root: &Node<'a, 'input>,
data_node: &Node<'a, 'input>,
bind_ref: &str,
) -> Vec<Node<'a, 'input>> {
let mut path = bind_ref.trim();
if path.is_empty() {
return Vec::new();
}
let mut current = if let Some(rest) = path.strip_prefix("$record.") {
path = rest;
vec![*data_root]
} else if path == "$record" {
return vec![*data_root];
} else if let Some(rest) = path.strip_prefix("$.") {
path = rest;
vec![*data_node]
} else {
vec![*data_node]
};
let segments: Vec<&str> = path
.split('.')
.map(str::trim)
.filter(|segment| !segment.is_empty())
.collect();
if segments.is_empty() {
return current;
}
for segment in segments {
let (name, selector) = parse_bind_segment(segment);
if name.is_empty() {
continue;
}
let mut next = Vec::new();
for node in current {
let matches: Vec<Node<'a, 'input>> = node
.children()
.filter(|child| child.is_element() && child.tag_name().name() == name)
.collect();
match selector {
BindSelector::First => {
if let Some(first) = matches.into_iter().next() {
next.push(first);
}
}
BindSelector::All => next.extend(matches),
BindSelector::Index(idx) => {
if let Some(found) = matches.into_iter().nth(idx) {
next.push(found);
}
}
}
}
current = next;
if current.is_empty() {
break;
}
}
current
}
#[derive(Clone, Copy)]
enum BindSelector {
First,
All,
Index(usize),
}
fn parse_bind_segment(segment: &str) -> (&str, BindSelector) {
let Some(start) = segment.find('[') else {
return (segment, BindSelector::First);
};
let name = &segment[..start];
let index = segment[start + 1..]
.strip_suffix(']')
.unwrap_or_default()
.trim();
match index {
"*" => (name, BindSelector::All),
"" => (name, BindSelector::First),
_ => index
.parse::<usize>()
.map(|idx| (name, BindSelector::Index(idx)))
.unwrap_or((name, BindSelector::First)),
}
}
fn find_child_element_by_name<'a, 'input>(
node: &Node<'a, 'input>,
name: &str,
) -> Option<Node<'a, 'input>> {
node.children()
.filter(|n| n.is_element())
.find(|n| n.tag_name().name() == name)
}
fn add_children(
tree: &mut FormTree,
node: &mut FormNode,
elem: Node<'_, '_>,
) -> std::result::Result<(bool, Option<String>), crate::error::XfaError> {
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_id, (trailing_break, trailing_target)) = parse_node(tree, child, false)?;
if pending_break {
let meta = 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 {
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());
}
"caption" | "value" | "ui" | "font" | "border" | "margin" | "para" | "format"
| "items" | "medium" | "contentArea" | "desc" | "occur" | "event" | "bind"
| "calculate" | "validate" | "assist" | "toolTip" | "fill" | "edge" | "corner"
| "linear" | "radial" | "pattern" | "stipple" | "color" | "extras" | "traversal"
| "proto" | "overflow" => {
}
_ => {
}
}
}
Ok((pending_break, pending_break_target))
}
fn blank_node(tag: &str) -> FormNode {
FormNode {
name: tag.to_string(),
node_type: FormNodeType::Subform,
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 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| m.to_points());
}
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> = attr(occur, "max")
.map(|s| if s == "-1" { None } else { s.parse().ok() })
.unwrap_or(Some(1));
let initial: u32 = attr(occur, "initial")
.and_then(|s| s.parse().ok())
.unwrap_or(min.max(1));
Occur::repeating(min, max, initial)
} else {
Occur::once()
}
}
fn parse_col_span(elem: Node<'_, '_>) -> i32 {
attr(elem, "colSpan")
.and_then(|s| s.parse().ok())
.unwrap_or(1)
}
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_image(elem: Node<'_, '_>) -> 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();
let data = image.text().unwrap_or_default();
let decoded = base64_decode(data);
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 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 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_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 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 base64_decode(input: &str) -> Vec<u8> {
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(input.trim())
.unwrap_or_default()
}
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 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 attr<'a>(elem: Node<'a, '_>, name: &str) -> Option<&'a str> {
elem.attributes()
.find(|a| a.name() == name)
.map(|a| a.value())
}
fn attr_as_f64(elem: Node<'_, '_>, name: &str) -> Option<f64> {
attr(elem, name)?.parse().ok()
}
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)
}
#[cfg(test)]
mod tests {
use super::*;
const SIMPLE_TEMPLATE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="paginate">
<pageSet>
<pageArea name="Page1">
<contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/>
<medium stock="default" short="8.5in" long="11in"/>
</pageArea>
</pageSet>
<subform name="section" layout="tb" w="7.5in">
<field name="firstName" w="3.5in" h="0.3in">
<caption><value><text>First Name</text></value></caption>
<ui><textEdit/></ui>
<value><text/></value>
</field>
<field name="lastName" w="3.5in" h="0.3in">
<caption><value><text>Last Name</text></value></caption>
<ui><textEdit/></ui>
<value><text>Default</text></value>
</field>
</subform>
</subform>
</template>
</xdp:xdp>"#;
#[test]
fn parse_simple_form() {
let (tree, root_id) = parse_template(SIMPLE_TEMPLATE, None).unwrap();
let root = tree.get(root_id);
assert!(!root.children.is_empty(), "root has no children");
}
#[test]
fn field_with_default_value() {
let (tree, root_id) = parse_template(SIMPLE_TEMPLATE, None).unwrap();
let found = find_node_by_name(&tree, root_id, "lastName");
assert!(found.is_some(), "lastName field not found");
if let Some(n) = found {
match &n.node_type {
FormNodeType::Field { value } => assert_eq!(value, "Default"),
other => panic!("expected Field, got {other:?}"),
}
}
}
#[test]
fn unlimited_occur_expands_all_dataset_instances_in_order() {
let template = r#"<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="paginate">
<pageSet>
<pageArea name="Page1">
<contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/>
</pageArea>
</pageSet>
<subform name="items" layout="tb" w="7in">
<subform name="row" layout="tb" w="7in">
<occur min="0" max="-1"/>
<field name="value" w="2in" h="0.3in">
<ui><textEdit/></ui>
<value><text/></value>
</field>
</subform>
</subform>
</subform>
</template>"#;
let datasets = r#"<?xml version="1.0" encoding="UTF-8"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form1>
<items>
<row><value>A</value></row>
<row><value>B</value></row>
<row><value>C</value></row>
</items>
</form1>
</xfa:data>
</xfa:datasets>"#;
let (tree, root_id) = parse_template(template, Some(datasets)).unwrap();
let items_id = find_node_id_by_name(&tree, root_id, "items").unwrap();
let row_ids = tree.get(items_id).children.clone();
assert_eq!(row_ids.len(), 3);
assert!(row_ids
.iter()
.all(|&row_id| tree.get(row_id).occur.count() == 1));
let values: Vec<String> = row_ids
.iter()
.map(|&row_id| {
let field_id = tree.get(row_id).children[0];
match &tree.get(field_id).node_type {
FormNodeType::Field { value } => value.clone(),
other => panic!("expected Field, got {other:?}"),
}
})
.collect();
assert_eq!(values, vec!["A", "B", "C"]);
}
#[test]
fn explicit_dataref_bind_repeats_subform_instances() {
let template = r#"<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="paginate">
<pageSet>
<pageArea name="Page1">
<contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/>
</pageArea>
</pageSet>
<subform name="items" layout="tb" w="7in">
<subform name="entryRow" layout="tb" w="7in">
<occur min="0" max="-1"/>
<bind match="dataRef" ref="$.item[*]"/>
<field name="label" w="2in" h="0.3in">
<bind match="dataRef" ref="$.value"/>
<ui><textEdit/></ui>
<value><text/></value>
</field>
</subform>
</subform>
</subform>
</template>"#;
let datasets = r#"<?xml version="1.0" encoding="UTF-8"?>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<form1>
<items>
<item><value>One</value></item>
<item><value>Two</value></item>
<item><value>Three</value></item>
</items>
</form1>
</xfa:data>
</xfa:datasets>"#;
let (tree, root_id) = parse_template(template, Some(datasets)).unwrap();
let items_id = find_node_id_by_name(&tree, root_id, "items").unwrap();
let row_ids = tree.get(items_id).children.clone();
assert_eq!(row_ids.len(), 3);
assert!(row_ids
.iter()
.all(|&row_id| tree.get(row_id).occur.count() == 1));
let values: Vec<String> = row_ids
.iter()
.map(|&row_id| {
let field_id = tree.get(row_id).children[0];
match &tree.get(field_id).node_type {
FormNodeType::Field { value } => value.clone(),
other => panic!("expected Field, got {other:?}"),
}
})
.collect();
assert_eq!(values, vec!["One", "Two", "Three"]);
}
#[test]
fn dimension_parsing() {
assert!((parse_dim("0.5in").unwrap() - 36.0).abs() < 0.01);
assert!((parse_dim("72pt").unwrap() - 72.0).abs() < 0.01);
assert!((parse_dim("1in").unwrap() - 72.0).abs() < 0.01);
assert!((parse_dim("8.5in").unwrap() - 612.0).abs() < 0.1);
assert!((parse_dim("11in").unwrap() - 792.0).abs() < 0.1);
}
#[test]
fn layout_attr_parsing() {
assert_eq!(parse_layout_str("tb"), LayoutStrategy::TopToBottom);
assert_eq!(parse_layout_str("lr-tb"), LayoutStrategy::LeftToRightTB);
assert_eq!(parse_layout_str("paginate"), LayoutStrategy::TopToBottom);
assert_eq!(parse_layout_str("position"), LayoutStrategy::Positioned);
}
fn parse_layout_str(s: &str) -> LayoutStrategy {
let xml = format!(
r#"<?xml version="1.0"?><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform layout="{s}"/></template>"#
);
let doc = roxmltree::Document::parse(&xml).unwrap();
let root = doc.root_element();
let subform = root.children().filter(|n| n.is_element()).next().unwrap();
parse_layout_attr(subform)
}
fn find_node_by_name<'a>(
tree: &'a FormTree,
id: FormNodeId,
name: &str,
) -> Option<&'a FormNode> {
let node = tree.get(id);
if node.name == name {
return Some(node);
}
for &child_id in &node.children {
if let Some(found) = find_node_by_name(tree, child_id, name) {
return Some(found);
}
}
None
}
fn find_node_id_by_name(tree: &FormTree, id: FormNodeId, name: &str) -> Option<FormNodeId> {
if tree.get(id).name == name {
return Some(id);
}
for &child_id in &tree.get(id).children.clone() {
if let Some(found) = find_node_id_by_name(tree, child_id, name) {
return Some(found);
}
}
None
}
#[test]
fn draw_exdata_html_text_extracted() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="paginate">
<pageSet>
<pageArea name="Page1">
<contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/>
<medium stock="default" short="8.5in" long="11in"/>
</pageArea>
</pageSet>
<subform name="body" layout="tb" w="7.5in">
<draw name="instructions" w="7in" h="1in">
<value>
<exData contentType="text/html">
<body xmlns="http://www.w3.org/1999/xhtml">
<p>Do <span>not</span> file this form.</p>
</body>
</exData>
</value>
</draw>
</subform>
</subform>
</template>"#;
let (tree, root_id) = parse_template(xml, None).unwrap();
let node =
find_node_by_name(&tree, root_id, "instructions").expect("instructions draw not found");
match &node.node_type {
FormNodeType::Draw(DrawContent::Text(content)) => {
assert!(
content.contains("not") && content.contains("file"),
"expected HTML text extracted, got: {content:?}"
);
}
other => panic!("expected Draw, got {other:?}"),
}
}
#[test]
fn hidden_elements_have_empty_content() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="paginate">
<pageSet>
<pageArea name="Page1">
<contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/>
<medium stock="default" short="8.5in" long="11in"/>
</pageArea>
</pageSet>
<subform name="body" layout="tb" w="7.5in">
<draw name="visible_draw" w="7in" h="0.5in">
<value><text>Visible text</text></value>
</draw>
<draw name="hidden_draw" w="7in" h="0.5in" presence="hidden">
<value><text>DRAFT</text></value>
</draw>
<field name="hidden_field" w="3in" h="0.3in" presence="hidden">
<value><text>secret</text></value>
</field>
</subform>
</subform>
</template>"#;
let (tree, root_id) = parse_template(xml, None).unwrap();
let visible = find_node_by_name(&tree, root_id, "visible_draw").unwrap();
match &visible.node_type {
FormNodeType::Draw(DrawContent::Text(content)) => assert_eq!(content, "Visible text"),
other => panic!("expected Draw, got {other:?}"),
}
let hidden_draw = find_node_by_name(&tree, root_id, "hidden_draw").unwrap();
match &hidden_draw.node_type {
FormNodeType::Draw(DrawContent::Text(content)) => assert_eq!(content, "DRAFT"),
other => panic!("expected Draw, got {other:?}"),
}
let hidden_draw_id = find_node_id_by_name(&tree, root_id, "hidden_draw").unwrap();
assert!(tree.meta(hidden_draw_id).presence.is_not_visible());
let hidden_field = find_node_by_name(&tree, root_id, "hidden_field").unwrap();
match &hidden_field.node_type {
FormNodeType::Field { value } => assert_eq!(value, "secret"),
other => panic!("expected Field, got {other:?}"),
}
let hidden_field_id = find_node_id_by_name(&tree, root_id, "hidden_field").unwrap();
assert!(tree.meta(hidden_field_id).presence.is_not_visible());
}
#[test]
fn font_size_bare_number_is_points() {
assert_eq!(parse_font_size("10"), Some(10.0));
assert_eq!(parse_font_size("8"), Some(8.0));
assert_eq!(parse_font_size("12"), Some(12.0));
assert!((parse_font_size("10pt").unwrap() - 10.0).abs() < 0.01);
assert_eq!(parse_font_size("0"), None);
}
#[test]
fn para_halign_parsed_into_font_metrics() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="paginate">
<pageSet>
<pageArea name="Page1">
<contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/>
<medium stock="default" short="8.5in" long="11in"/>
</pageArea>
</pageSet>
<subform name="body" layout="tb" w="7.5in">
<draw name="left_draw" w="7in" h="0.5in">
<value><text>Left</text></value>
<para hAlign="left"/>
</draw>
<draw name="center_draw" w="7in" h="0.5in">
<value><text>Centered</text></value>
<para hAlign="center"/>
</draw>
<draw name="right_draw" w="7in" h="0.5in">
<value><text>Right</text></value>
<para hAlign="right"/>
</draw>
</subform>
</subform>
</template>"#;
let (tree, root_id) = parse_template(xml, None).unwrap();
let left = find_node_by_name(&tree, root_id, "left_draw").unwrap();
assert_eq!(
left.font.text_align,
TextAlign::Left,
"left_draw should be Left"
);
let center = find_node_by_name(&tree, root_id, "center_draw").unwrap();
assert_eq!(
center.font.text_align,
TextAlign::Center,
"center_draw should be Center"
);
let right = find_node_by_name(&tree, root_id, "right_draw").unwrap();
assert_eq!(
right.font.text_align,
TextAlign::Right,
"right_draw should be Right"
);
}
#[test]
fn bmp_image_converted_to_png() {
let bmp_bytes: [u8; 58] = [
0x42, 0x4D, 0x3A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x13, 0x0B, 0x00, 0x00, 0x13, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00,
0x00, ];
use base64::Engine;
let b64 = base64::engine::general_purpose::STANDARD.encode(&bmp_bytes);
let xml = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="paginate">
<pageSet>
<pageArea name="Page1">
<contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/>
<medium stock="default" short="8.5in" long="11in"/>
</pageArea>
</pageSet>
<subform name="body" layout="tb" w="7.5in">
<draw name="barcode_img" w="2in" h="0.5in">
<value>
<image contentType="image/bmp">{b64}</image>
</value>
</draw>
</subform>
</subform>
</template>"#
);
let (tree, root_id) = parse_template(&xml, None).unwrap();
let node = find_node_by_name(&tree, root_id, "barcode_img").expect("barcode_img not found");
match &node.node_type {
FormNodeType::Image { data, mime_type } => {
assert_eq!(mime_type, "image/png", "BMP should be converted to PNG");
assert!(
data.starts_with(&[0x89, 0x50, 0x4E, 0x47]),
"expected PNG magic bytes, got {:?}",
&data[..4.min(data.len())]
);
}
other => panic!("expected Image, got {other:?}"),
}
}
#[test]
fn parse_percentage_values() {
assert!((parse_percentage("96%").unwrap() - 0.96).abs() < 1e-10);
assert!((parse_percentage("110%").unwrap() - 1.10).abs() < 1e-10);
assert!((parse_percentage("100%").unwrap() - 1.0).abs() < 1e-10);
assert!((parse_percentage("50%").unwrap() - 0.50).abs() < 1e-10);
assert!(parse_percentage("notanumber%").is_none());
assert!(parse_percentage("96").is_none()); }
#[test]
fn parse_letter_spacing_values() {
let font_size = 10.0;
let v = parse_letter_spacing("-0.018em", font_size).unwrap();
assert!((v - (-0.018 * 10.0)).abs() < 1e-10);
let v = parse_letter_spacing("0.1em", font_size).unwrap();
assert!((v - 1.0).abs() < 1e-10);
assert_eq!(parse_letter_spacing("0", font_size), Some(0.0));
let v = parse_letter_spacing("0.5pt", font_size).unwrap();
assert!((v - 0.5).abs() < 0.01);
}
#[test]
fn parse_font_color_attr_hex6() {
assert_eq!(parse_font_color_attr("#000080"), Some((0, 0, 128)));
assert_eq!(parse_font_color_attr("#FF0000"), Some((255, 0, 0)));
assert_eq!(parse_font_color_attr("#00ff00"), Some((0, 255, 0)));
assert_eq!(parse_font_color_attr("#ABCDEF"), Some((0xAB, 0xCD, 0xEF)));
}
#[test]
fn parse_font_color_attr_hex3() {
assert_eq!(parse_font_color_attr("#F00"), Some((255, 0, 0)));
assert_eq!(parse_font_color_attr("#0F0"), Some((0, 255, 0)));
assert_eq!(parse_font_color_attr("#00F"), Some((0, 0, 255)));
assert_eq!(parse_font_color_attr("#ABC"), Some((0xAA, 0xBB, 0xCC)));
}
#[test]
fn parse_font_color_attr_decimal_csv() {
assert_eq!(parse_font_color_attr("0,0,128"), Some((0, 0, 128)));
assert_eq!(parse_font_color_attr("255, 128, 0"), Some((255, 128, 0)));
}
#[test]
fn parse_font_color_attr_invalid() {
assert_eq!(parse_font_color_attr(""), None);
assert_eq!(parse_font_color_attr("#GG0000"), None);
assert_eq!(parse_font_color_attr("#12345"), None);
assert_eq!(parse_font_color_attr("not_a_color"), None);
}
#[test]
fn font_color_attribute_parsed() {
let xml = r##"<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="paginate">
<pageSet>
<pageArea name="Page1">
<contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/>
<medium stock="default" short="8.5in" long="11in"/>
</pageArea>
</pageSet>
<subform name="body" layout="tb" w="7.5in">
<draw name="blue_text" w="7in" h="0.5in">
<value><text>Navy blue</text></value>
<font typeface="Arial" size="10pt" color="#000080"/>
</draw>
</subform>
</subform>
</template>"##;
let (tree, root_id) = parse_template(xml, None).unwrap();
let id = find_node_id_by_name(&tree, root_id, "blue_text").unwrap();
let style = &tree.meta(id).style;
assert_eq!(
style.text_color,
Some((0, 0, 128)),
"font color=#000080 should parse to (0, 0, 128)"
);
}
#[test]
fn font_horizontal_scale_and_letter_spacing_parsed() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="paginate">
<pageSet>
<pageArea name="Page1">
<contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/>
<medium stock="default" short="8.5in" long="11in"/>
</pageArea>
</pageSet>
<subform name="body" layout="tb" w="7.5in">
<draw name="scaled_text" w="7in" h="0.5in">
<value><text>Scaled</text></value>
<font typeface="Arial" size="10pt" fontHorizontalScale="96%" letterSpacing="-0.018em"/>
</draw>
</subform>
</subform>
</template>"#;
let (tree, root_id) = parse_template(xml, None).unwrap();
let id = find_node_id_by_name(&tree, root_id, "scaled_text").unwrap();
let style = &tree.meta(id).style;
assert!(
(style.font_horizontal_scale.unwrap() - 0.96).abs() < 1e-10,
"expected 0.96, got {:?}",
style.font_horizontal_scale
);
assert!(
(style.letter_spacing_pt.unwrap() - (-0.18)).abs() < 0.01,
"expected -0.18pt, got {:?}",
style.letter_spacing_pt
);
}
#[test]
fn choice_list_items_parsed() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="root" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="8in" h="10in"/>
</pageArea>
</pageSet>
<field name="country" w="3in" h="0.3in">
<ui><choiceList/></ui>
<value><text>US</text></value>
<items>
<text>United States</text>
<text>United Kingdom</text>
<text>Canada</text>
</items>
<items save="1">
<text>US</text>
<text>UK</text>
<text>CA</text>
</items>
</field>
<field name="single_items" w="3in" h="0.3in">
<ui><choiceList/></ui>
<value><text>Red</text></value>
<items>
<text>Red</text>
<text>Green</text>
<text>Blue</text>
</items>
</field>
</subform>
</template>"#;
let (tree, _pages) = parse_template(xml, None).unwrap();
let country = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "country")
.map(|(i, _)| FormNodeId(i))
.expect("country field not found");
let meta = tree.meta(country);
assert_eq!(meta.field_kind, FieldKind::Dropdown);
assert_eq!(
meta.display_items,
vec!["United States", "United Kingdom", "Canada"]
);
assert_eq!(meta.save_items, vec!["US", "UK", "CA"]);
let single = tree
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.name == "single_items")
.map(|(i, _)| FormNodeId(i))
.expect("single_items field not found");
let meta_s = tree.meta(single);
assert_eq!(meta_s.display_items, vec!["Red", "Green", "Blue"]);
assert!(meta_s.save_items.is_empty());
}
#[test]
fn content_area_without_xy_defaults_to_origin() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="root" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="8in" h="10in"/>
</pageArea>
</pageSet>
</subform>
</template>"#;
let (tree, root_id) = parse_template(xml, None).unwrap();
let page_area_id = find_node_id_by_name(&tree, root_id, "Page1").unwrap();
let page_area = tree.get(page_area_id);
match &page_area.node_type {
FormNodeType::PageArea { content_areas } => {
assert_eq!(content_areas.len(), 1);
assert_eq!(content_areas[0].x, 0.0);
assert_eq!(content_areas[0].y, 0.0);
assert!((content_areas[0].width - 576.0).abs() < 0.01);
assert!((content_areas[0].height - 720.0).abs() < 0.01);
}
other => panic!("expected PageArea, got {other:?}"),
}
}
#[test]
fn check_button_mark_parsed_into_style() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="root" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="8in" h="10in"/>
</pageArea>
</pageSet>
<field name="agree" w="0.3in" h="0.3in">
<ui><checkButton mark="circle"/></ui>
<value><text>1</text></value>
</field>
</subform>
</template>"#;
let (tree, root_id) = parse_template(xml, None).unwrap();
let id = find_node_id_by_name(&tree, root_id, "agree").unwrap();
let meta = tree.meta(id);
assert_eq!(meta.field_kind, FieldKind::Checkbox);
assert_eq!(meta.style.check_button_mark.as_deref(), Some("circle"));
}
#[test]
fn border_widths_parsed_from_per_edge_template_border() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="root" layout="tb">
<pageSet>
<pageArea name="Page1">
<contentArea w="8in" h="10in"/>
</pageArea>
</pageSet>
<field name="amount" w="2in" h="0.3in">
<ui><textEdit/></ui>
<value><text>42</text></value>
<border>
<edge thickness="1pt"/>
<edge thickness="2pt"/>
<edge thickness="3pt"/>
<edge thickness="4pt"/>
</border>
</field>
</subform>
</template>"#;
let (tree, root_id) = parse_template(xml, None).unwrap();
let id = find_node_id_by_name(&tree, root_id, "amount").unwrap();
let style = &tree.meta(id).style;
assert_eq!(style.border_width_pt, Some(1.0));
assert_eq!(style.border_widths, Some([1.0, 2.0, 3.0, 4.0]));
}
}