use super::NativeDocument;
use crate::types::{BoundingBox, FormFieldType, PdfFormField, ProcessingWarning};
pub(crate) fn extract_form_fields(doc: &mut NativeDocument) -> (Vec<PdfFormField>, Vec<ProcessingWarning>) {
let mut fields = Vec::new();
let mut warnings = Vec::new();
match xberg_native_pdf::extractors::forms::FormExtractor::extract_fields(&doc.doc) {
Ok(native_fields) => {
fields.extend(native_fields.into_iter().map(map_form_field));
tracing::debug!("extracted {} AcroForm fields", fields.len());
}
Err(e) => {
tracing::debug!("AcroForm extraction failed: {e}");
warnings.push(ProcessingWarning {
source: std::borrow::Cow::Borrowed("pdf_forms"),
message: std::borrow::Cow::Owned(format!("AcroForm field extraction failed: {e}")),
});
}
}
match extract_xfa_fields(doc) {
Ok(Some(xfa_fields)) => {
let known: std::collections::HashSet<String> = fields.iter().map(|f| f.name.clone()).collect();
let appended: Vec<PdfFormField> = xfa_fields.into_iter().filter(|f| !known.contains(&f.name)).collect();
if !appended.is_empty() {
tracing::debug!("appended {} XFA-only fields", appended.len());
fields.extend(appended);
}
}
Ok(None) => {}
Err(warning) => warnings.push(warning),
}
(fields, warnings)
}
const XFA_ABSENT_MESSAGES: [&str; 2] = ["No AcroForm in document", "No XFA entry in AcroForm"];
fn extract_xfa_fields(doc: &mut NativeDocument) -> Result<Option<Vec<PdfFormField>>, ProcessingWarning> {
let xfa_data = match xberg_native_pdf::xfa::XfaExtractor::extract_xfa(&mut doc.doc) {
Ok(data) => data,
Err(e) => {
let message = e.to_string();
if XFA_ABSENT_MESSAGES.iter().any(|absent| message.contains(absent)) {
tracing::debug!("XFA not present: {e}");
return Ok(None);
}
tracing::debug!("XFA extraction failed: {e}");
return Err(ProcessingWarning {
source: std::borrow::Cow::Borrowed("pdf_forms"),
message: std::borrow::Cow::Owned(format!(
"XFA form data was present but could not be extracted: {e}; XFA fields were skipped"
)),
});
}
};
let mut parser = xberg_native_pdf::xfa::XfaParser::new();
let xfa_form = match parser.parse(&xfa_data) {
Ok(form) => form,
Err(e) => {
tracing::debug!("XFA parsing failed: {e}");
return Err(ProcessingWarning {
source: std::borrow::Cow::Borrowed("pdf_forms"),
message: std::borrow::Cow::Owned(format!(
"XFA form data was present but could not be parsed: {e}; XFA fields were skipped"
)),
});
}
};
let mut fields = Vec::new();
for xfa_field in &xfa_form.fields {
let field = map_xfa_field_direct(xfa_field);
fields.push(field);
}
Ok(Some(fields))
}
fn map_form_field(native_field: xberg_native_pdf::extractors::forms::FormField) -> PdfFormField {
let field_type = map_field_type(&native_field.field_type, native_field.flags);
let value = map_field_value(&native_field.value);
let default_value = native_field.default_value.as_ref().and_then(map_field_value);
let bbox = native_field.bounds.map(|bounds| BoundingBox {
x0: bounds[0],
y0: bounds[1],
x1: bounds[2],
y1: bounds[3],
});
PdfFormField {
name: native_field.name,
full_name: native_field.full_name,
field_type,
value,
default_value,
flags: native_field.flags.unwrap_or(0),
page: None,
bbox,
max_length: native_field.max_length,
tooltip: native_field.tooltip,
}
}
fn map_field_type(native_type: &xberg_native_pdf::extractors::forms::FieldType, flags: Option<u32>) -> FormFieldType {
use xberg_native_pdf::extractors::forms::{FieldType, field_flags};
match native_type {
FieldType::Button => {
if let Some(f) = flags {
if f & field_flags::PUSH_BUTTON != 0 {
FormFieldType::Button
} else if f & field_flags::RADIO != 0 {
FormFieldType::Radio
} else {
FormFieldType::Checkbox
}
} else {
FormFieldType::Checkbox
}
}
FieldType::Text => FormFieldType::Text,
FieldType::Choice => FormFieldType::Choice,
FieldType::Signature => FormFieldType::Signature,
FieldType::Unknown(_) => FormFieldType::Unknown,
}
}
fn map_field_value(native_value: &xberg_native_pdf::extractors::forms::FieldValue) -> Option<String> {
use xberg_native_pdf::extractors::forms::FieldValue;
match native_value {
FieldValue::Text(s) => Some(s.clone()),
FieldValue::Boolean(b) => Some(b.to_string()),
FieldValue::Name(n) => Some(n.clone()),
FieldValue::Array(arr) => {
if arr.is_empty() {
None
} else {
Some(arr.join(", "))
}
}
FieldValue::None => None,
}
}
fn map_xfa_field_type(xfa_type: &xberg_native_pdf::xfa::XfaFieldType) -> FormFieldType {
use xberg_native_pdf::xfa::XfaFieldType;
match xfa_type {
XfaFieldType::Text | XfaFieldType::Numeric | XfaFieldType::DateTime => FormFieldType::Text,
XfaFieldType::Checkbox => FormFieldType::Checkbox,
XfaFieldType::RadioGroup => FormFieldType::Radio,
XfaFieldType::DropDown | XfaFieldType::ListBox => FormFieldType::Choice,
XfaFieldType::Signature => FormFieldType::Signature,
XfaFieldType::Button => FormFieldType::Button,
_ => FormFieldType::Unknown,
}
}
fn map_xfa_field_direct(xfa_field: &xberg_native_pdf::xfa::XfaField) -> PdfFormField {
let field_type = map_xfa_field_type(&xfa_field.field_type);
let value = xfa_field.value.clone().or_else(|| xfa_field.default_value.clone());
let bbox =
if let (Some(x), Some(y), Some(w), Some(h)) = (xfa_field.x, xfa_field.y, xfa_field.width, xfa_field.height) {
Some(BoundingBox {
x0: x as f64,
y0: y as f64,
x1: (x + w) as f64,
y1: (y + h) as f64,
})
} else {
None
};
PdfFormField {
name: xfa_field.name.clone(),
full_name: xfa_field.binding.clone(),
field_type,
value,
default_value: xfa_field.default_value.clone(),
flags: 0,
page: None,
bbox,
max_length: xfa_field.max_length,
tooltip: xfa_field.tooltip.clone().or_else(|| xfa_field.caption.clone()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use xberg_native_pdf::extractors::forms::{FieldType, FieldValue};
#[test]
fn test_map_field_type_button_push_button() {
use xberg_native_pdf::extractors::forms::field_flags;
let flags = field_flags::PUSH_BUTTON;
assert_eq!(map_field_type(&FieldType::Button, Some(flags)), FormFieldType::Button);
}
#[test]
fn test_map_field_type_button_radio() {
use xberg_native_pdf::extractors::forms::field_flags;
let flags = field_flags::RADIO;
assert_eq!(map_field_type(&FieldType::Button, Some(flags)), FormFieldType::Radio);
}
#[test]
fn test_map_field_type_button_checkbox_no_flags() {
assert_eq!(map_field_type(&FieldType::Button, None), FormFieldType::Checkbox);
}
#[test]
fn test_map_field_type_button_checkbox_default() {
assert_eq!(map_field_type(&FieldType::Button, Some(0)), FormFieldType::Checkbox);
}
#[test]
fn test_map_field_type_text() {
assert_eq!(map_field_type(&FieldType::Text, None), FormFieldType::Text);
}
#[test]
fn test_map_field_type_choice() {
assert_eq!(map_field_type(&FieldType::Choice, None), FormFieldType::Choice);
}
#[test]
fn test_map_field_type_signature() {
assert_eq!(map_field_type(&FieldType::Signature, None), FormFieldType::Signature);
}
#[test]
fn test_map_field_type_unknown() {
assert_eq!(
map_field_type(&FieldType::Unknown("custom".to_string()), None),
FormFieldType::Unknown
);
}
#[test]
fn test_map_field_value_text() {
let value = FieldValue::Text("hello".to_string());
assert_eq!(map_field_value(&value), Some("hello".to_string()));
}
#[test]
fn test_map_field_value_boolean_true() {
let value = FieldValue::Boolean(true);
assert_eq!(map_field_value(&value), Some("true".to_string()));
}
#[test]
fn test_map_field_value_boolean_false() {
let value = FieldValue::Boolean(false);
assert_eq!(map_field_value(&value), Some("false".to_string()));
}
#[test]
fn test_map_field_value_name() {
let value = FieldValue::Name("Yes".to_string());
assert_eq!(map_field_value(&value), Some("Yes".to_string()));
}
#[test]
fn test_map_field_value_array() {
let value = FieldValue::Array(vec!["option1".to_string(), "option2".to_string()]);
assert_eq!(map_field_value(&value), Some("option1, option2".to_string()));
}
#[test]
fn test_map_field_value_empty_array() {
let value = FieldValue::Array(vec![]);
assert_eq!(map_field_value(&value), None);
}
#[test]
fn test_map_field_value_none() {
let value = FieldValue::None;
assert_eq!(map_field_value(&value), None);
}
#[test]
fn test_checkbox_boolean_true_maps_to_true_string() {
let value = FieldValue::Boolean(true);
let mapped = map_field_value(&value).expect("Some");
assert_eq!(mapped, "true", "checkbox Boolean(true) should map to 'true'");
}
#[test]
fn test_radio_name_value_maps_as_is() {
let value = FieldValue::Name("Opt".to_string());
let mapped = map_field_value(&value).expect("Some");
assert_eq!(mapped, "Opt", "radio Name('Opt') should map to 'Opt'");
}
#[test]
fn test_checkbox_field_classification_and_value() {
let field_type = map_field_type(&FieldType::Button, Some(0));
let value = map_field_value(&FieldValue::Boolean(true));
assert_eq!(field_type, FormFieldType::Checkbox);
assert_eq!(value, Some("true".to_string()));
}
#[test]
fn test_radio_field_classification_and_value() {
use xberg_native_pdf::extractors::forms::field_flags;
let field_type = map_field_type(&FieldType::Button, Some(field_flags::RADIO));
let value = map_field_value(&FieldValue::Name("Choice".to_string()));
assert_eq!(field_type, FormFieldType::Radio);
assert_eq!(value, Some("Choice".to_string()));
}
#[test]
fn test_push_button_field_classification() {
use xberg_native_pdf::extractors::forms::field_flags;
let field_type = map_field_type(&FieldType::Button, Some(field_flags::PUSH_BUTTON));
assert_eq!(field_type, FormFieldType::Button);
}
#[test]
fn test_map_xfa_field_type_text() {
let xfa_type = xberg_native_pdf::xfa::XfaFieldType::Text;
assert_eq!(map_xfa_field_type(&xfa_type), FormFieldType::Text);
}
#[test]
fn test_map_xfa_field_type_numeric() {
let xfa_type = xberg_native_pdf::xfa::XfaFieldType::Numeric;
assert_eq!(map_xfa_field_type(&xfa_type), FormFieldType::Text);
}
#[test]
fn test_map_xfa_field_type_datetime() {
let xfa_type = xberg_native_pdf::xfa::XfaFieldType::DateTime;
assert_eq!(map_xfa_field_type(&xfa_type), FormFieldType::Text);
}
#[test]
fn test_map_xfa_field_type_checkbox() {
let xfa_type = xberg_native_pdf::xfa::XfaFieldType::Checkbox;
assert_eq!(map_xfa_field_type(&xfa_type), FormFieldType::Checkbox);
}
#[test]
fn test_map_xfa_field_type_radio_group() {
let xfa_type = xberg_native_pdf::xfa::XfaFieldType::RadioGroup;
assert_eq!(map_xfa_field_type(&xfa_type), FormFieldType::Radio);
}
#[test]
fn test_map_xfa_field_type_dropdown() {
let xfa_type = xberg_native_pdf::xfa::XfaFieldType::DropDown;
assert_eq!(map_xfa_field_type(&xfa_type), FormFieldType::Choice);
}
#[test]
fn test_map_xfa_field_type_listbox() {
let xfa_type = xberg_native_pdf::xfa::XfaFieldType::ListBox;
assert_eq!(map_xfa_field_type(&xfa_type), FormFieldType::Choice);
}
#[test]
fn test_map_xfa_field_type_signature() {
let xfa_type = xberg_native_pdf::xfa::XfaFieldType::Signature;
assert_eq!(map_xfa_field_type(&xfa_type), FormFieldType::Signature);
}
#[test]
fn test_map_xfa_field_type_button() {
let xfa_type = xberg_native_pdf::xfa::XfaFieldType::Button;
assert_eq!(map_xfa_field_type(&xfa_type), FormFieldType::Button);
}
#[test]
fn test_map_xfa_field_type_image() {
let xfa_type = xberg_native_pdf::xfa::XfaFieldType::Image;
assert_eq!(map_xfa_field_type(&xfa_type), FormFieldType::Unknown);
}
#[test]
fn test_map_xfa_field_type_barcode() {
let xfa_type = xberg_native_pdf::xfa::XfaFieldType::Barcode;
assert_eq!(map_xfa_field_type(&xfa_type), FormFieldType::Unknown);
}
#[test]
fn test_map_xfa_field_type_unknown() {
let xfa_type = xberg_native_pdf::xfa::XfaFieldType::Unknown("CustomType".to_string());
assert_eq!(map_xfa_field_type(&xfa_type), FormFieldType::Unknown);
}
#[test]
fn test_map_xfa_field_direct_text_with_value() {
let xfa_field = xberg_native_pdf::xfa::XfaField::new("username", "form.username[0]");
let mut xfa_field = xfa_field;
xfa_field.field_type = xberg_native_pdf::xfa::XfaFieldType::Text;
xfa_field.value = Some("john_doe".to_string());
let mapped = map_xfa_field_direct(&xfa_field);
assert_eq!(mapped.name, "username");
assert_eq!(mapped.full_name, "form.username[0]");
assert_eq!(mapped.field_type, FormFieldType::Text);
assert_eq!(mapped.value, Some("john_doe".to_string()));
}
#[test]
fn test_map_xfa_field_direct_checkbox_checked() {
let xfa_field = xberg_native_pdf::xfa::XfaField::new("agree", "form.agree[0]");
let mut xfa_field = xfa_field;
xfa_field.field_type = xberg_native_pdf::xfa::XfaFieldType::Checkbox;
xfa_field.value = Some("Yes".to_string());
let mapped = map_xfa_field_direct(&xfa_field);
assert_eq!(mapped.name, "agree");
assert_eq!(mapped.field_type, FormFieldType::Checkbox);
assert_eq!(mapped.value, Some("Yes".to_string()));
}
#[test]
fn test_map_xfa_field_direct_with_bbox() {
let xfa_field = xberg_native_pdf::xfa::XfaField::new("name_field", "form.name[0]");
let mut xfa_field = xfa_field;
xfa_field.field_type = xberg_native_pdf::xfa::XfaFieldType::Text;
xfa_field.x = Some(72.0);
xfa_field.y = Some(700.0);
xfa_field.width = Some(200.0);
xfa_field.height = Some(20.0);
let mapped = map_xfa_field_direct(&xfa_field);
let bbox = mapped.bbox.expect("bbox should be present");
assert_eq!(bbox.x0, 72.0);
assert_eq!(bbox.y0, 700.0);
assert_eq!(bbox.x1, 272.0);
assert_eq!(bbox.y1, 720.0);
}
#[test]
fn test_map_xfa_field_direct_with_tooltip() {
let xfa_field = xberg_native_pdf::xfa::XfaField::new("email", "form.email[0]");
let mut xfa_field = xfa_field;
xfa_field.field_type = xberg_native_pdf::xfa::XfaFieldType::Text;
xfa_field.tooltip = Some("Enter your email address".to_string());
let mapped = map_xfa_field_direct(&xfa_field);
assert_eq!(mapped.tooltip, Some("Enter your email address".to_string()));
}
#[test]
fn test_map_xfa_field_direct_tooltip_fallback_to_caption() {
let xfa_field = xberg_native_pdf::xfa::XfaField::new("phone", "form.phone[0]");
let mut xfa_field = xfa_field;
xfa_field.field_type = xberg_native_pdf::xfa::XfaFieldType::Text;
xfa_field.caption = Some("Phone Number".to_string());
let mapped = map_xfa_field_direct(&xfa_field);
assert_eq!(mapped.tooltip, Some("Phone Number".to_string()));
}
#[test]
fn test_map_xfa_field_direct_max_length() {
let xfa_field = xberg_native_pdf::xfa::XfaField::new("zip", "form.zip[0]");
let mut xfa_field = xfa_field;
xfa_field.field_type = xberg_native_pdf::xfa::XfaFieldType::Numeric;
xfa_field.max_length = Some(5);
let mapped = map_xfa_field_direct(&xfa_field);
assert_eq!(mapped.max_length, Some(5));
}
#[test]
fn test_map_xfa_field_direct_value_prefers_current() {
let xfa_field = xberg_native_pdf::xfa::XfaField::new("status", "form.status[0]");
let mut xfa_field = xfa_field;
xfa_field.field_type = xberg_native_pdf::xfa::XfaFieldType::Text;
xfa_field.value = Some("Active".to_string());
xfa_field.default_value = Some("Inactive".to_string());
let mapped = map_xfa_field_direct(&xfa_field);
assert_eq!(mapped.value, Some("Active".to_string()));
assert_eq!(mapped.default_value, Some("Inactive".to_string()));
}
#[test]
fn test_map_xfa_field_direct_value_fallback_to_default() {
let xfa_field = xberg_native_pdf::xfa::XfaField::new("priority", "form.priority[0]");
let mut xfa_field = xfa_field;
xfa_field.field_type = xberg_native_pdf::xfa::XfaFieldType::Text;
xfa_field.default_value = Some("Normal".to_string());
let mapped = map_xfa_field_direct(&xfa_field);
assert_eq!(mapped.value, Some("Normal".to_string()));
}
}