use std::collections::HashMap;
use rpdfium_core::{Name, PdfSource};
use rpdfium_parser::{Object, ObjectStore};
use crate::error::{DocError, DocResult};
use crate::form_field::{FormField, FormFieldFlags, FormFieldType};
use crate::variable_text::Alignment;
const MAX_FIELDS: usize = 10_000;
#[derive(Debug, Clone)]
pub struct InteractiveForm {
pub fields: Vec<FormField>,
pub calculation_order: Vec<String>,
pub default_appearance: Option<String>,
pub default_alignment: Alignment,
}
impl InteractiveForm {
pub fn from_catalog<S: PdfSource>(
catalog: &Object,
store: &ObjectStore<S>,
) -> DocResult<Option<Self>> {
let catalog_dict = store
.deep_resolve(catalog)
.map_err(|e| DocError::Parser(e.to_string()))?
.as_dict()
.ok_or(DocError::UnexpectedType)?;
let acroform_obj = match catalog_dict.get(&Name::acro_form()) {
Some(obj) => store
.deep_resolve(obj)
.map_err(|e| DocError::Parser(e.to_string()))?,
None => return Ok(None),
};
let acroform_dict = match acroform_obj.as_dict() {
Some(d) => d,
None => return Ok(None),
};
let calculation_order = parse_calculation_order(acroform_dict, store);
let default_appearance = acroform_dict
.get(&Name::da())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
let default_alignment = acroform_dict
.get(&Name::q())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_i64())
.map(|v| Alignment::from_value(v as i32))
.unwrap_or(Alignment::Left);
let fields_obj = match acroform_dict.get(&Name::fields()) {
Some(obj) => store
.deep_resolve(obj)
.map_err(|e| DocError::Parser(e.to_string()))?,
None => {
return Ok(Some(InteractiveForm {
fields: Vec::new(),
calculation_order,
default_appearance: default_appearance.clone(),
default_alignment,
}));
}
};
let fields_arr = match fields_obj.as_array() {
Some(a) => a,
None => {
return Ok(Some(InteractiveForm {
fields: Vec::new(),
calculation_order,
default_appearance: default_appearance.clone(),
default_alignment,
}));
}
};
let mut result_fields: Vec<FormField> = Vec::new();
let mut total_count: usize = 0;
struct StackItem {
dict: HashMap<Name, Object>,
parent_ft: Option<String>,
parent_name: String,
path: Vec<usize>,
}
let mut stack: Vec<StackItem> = Vec::new();
for field_obj in fields_arr.iter().rev() {
let resolved = match store.deep_resolve(field_obj).ok() {
Some(o) => o,
None => continue,
};
if let Some(d) = resolved.as_dict() {
stack.push(StackItem {
dict: d.clone(),
parent_ft: None,
parent_name: String::new(),
path: Vec::new(),
});
}
}
while let Some(item) = stack.pop() {
if total_count >= MAX_FIELDS {
break;
}
let ft_str = item
.dict
.get(&Name::ft())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_name().map(|n| n.as_str().into_owned()));
let effective_ft = ft_str.as_deref().or(item.parent_ft.as_deref());
let partial_name = item
.dict
.get(&Name::t())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
let current_name = match (&partial_name, item.parent_name.is_empty()) {
(Some(pn), true) => pn.clone(),
(Some(pn), false) => format!("{}.{pn}", item.parent_name),
(None, _) => item.parent_name.clone(),
};
let kids = item
.dict
.get(&Name::kids())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_array().map(|a| a.to_vec()));
if let Some(kids_arr) = kids {
let field = FormField::from_dict(
&item.dict,
store,
item.parent_ft.as_deref(),
&item.parent_name,
);
if let Some(mut f) = field {
let idx = insert_at_path(&mut result_fields, &item.path, &mut f);
total_count += 1;
let mut child_path = item.path.clone();
child_path.push(idx);
for kid_obj in kids_arr.iter().rev() {
let resolved = match store.deep_resolve(kid_obj).ok() {
Some(o) => o,
None => continue,
};
if let Some(d) = resolved.as_dict() {
stack.push(StackItem {
dict: d.clone(),
parent_ft: effective_ft.map(|s| s.to_string()),
parent_name: current_name.clone(),
path: child_path.clone(),
});
}
}
} else {
for kid_obj in kids_arr.iter().rev() {
let resolved = match store.deep_resolve(kid_obj).ok() {
Some(o) => o,
None => continue,
};
if let Some(d) = resolved.as_dict() {
stack.push(StackItem {
dict: d.clone(),
parent_ft: effective_ft.map(|s| s.to_string()),
parent_name: current_name.clone(),
path: item.path.clone(),
});
}
}
}
} else {
let field = FormField::from_dict(
&item.dict,
store,
item.parent_ft.as_deref(),
&item.parent_name,
);
if let Some(mut f) = field {
insert_at_path(&mut result_fields, &item.path, &mut f);
total_count += 1;
}
}
}
Ok(Some(InteractiveForm {
fields: result_fields,
calculation_order,
default_appearance,
default_alignment,
}))
}
pub fn calculation_order(&self) -> &[String] {
&self.calculation_order
}
pub fn all_fields(&self) -> Vec<&FormField> {
let mut result = Vec::new();
let mut stack: Vec<&FormField> = self.fields.iter().rev().collect();
while let Some(field) = stack.pop() {
result.push(field);
for child in field.children.iter().rev() {
stack.push(child);
}
}
result
}
pub fn field_by_name(&self, name: &str) -> Option<&FormField> {
let mut stack: Vec<&FormField> = self.fields.iter().rev().collect();
while let Some(field) = stack.pop() {
if field.name == name {
return Some(field);
}
for child in field.children.iter().rev() {
stack.push(child);
}
}
None
}
pub fn field_by_name_mut(&mut self, name: &str) -> Option<&mut FormField> {
let mut stack: Vec<&mut FormField> = self.fields.iter_mut().rev().collect();
while let Some(field) = stack.pop() {
if field.name == name {
return Some(field);
}
for child in field.children.iter_mut().rev() {
stack.push(child);
}
}
None
}
pub fn all_field_names(&self) -> Vec<String> {
self.all_fields().iter().map(|f| f.name.clone()).collect()
}
pub fn default_appearance(&self) -> Option<&str> {
self.default_appearance.as_deref()
}
#[inline]
pub fn get_default_appearance(&self) -> Option<&str> {
self.default_appearance()
}
pub fn default_alignment(&self) -> Alignment {
self.default_alignment
}
#[inline]
pub fn get_form_alignment(&self) -> Alignment {
self.default_alignment()
}
pub fn check_required_fields(&self) -> Vec<&str> {
let mut missing = Vec::new();
let all = self.all_fields();
for field in &all {
if field.flags.is_required() && field.value.is_none() {
missing.push(field.name.as_str());
}
}
missing
}
pub fn control_at_point(&self, x: f64, y: f64) -> Option<(&str, usize)> {
let all = self.all_fields();
for field in &all {
for (i, control) in field.controls.iter().enumerate() {
let r = &control.rect;
if x >= r.left && x <= r.right && y >= r.bottom && y <= r.top {
return Some((&field.name, i));
}
}
}
None
}
#[inline]
pub fn get_control_at_point(&self, x: f64, y: f64) -> Option<(&str, usize)> {
self.control_at_point(x, y)
}
}
fn parse_calculation_order<S: PdfSource>(
acroform_dict: &HashMap<Name, Object>,
store: &ObjectStore<S>,
) -> Vec<String> {
let co_obj = match acroform_dict.get(&Name::co()) {
Some(o) => o,
None => return Vec::new(),
};
let resolved = match store.deep_resolve(co_obj).ok() {
Some(o) => o,
None => return Vec::new(),
};
let arr = match resolved.as_array() {
Some(a) => a,
None => return Vec::new(),
};
let mut order = Vec::new();
for item in arr {
let field_obj = match store.deep_resolve(item).ok() {
Some(o) => o,
None => continue,
};
if let Some(dict) = field_obj.as_dict() {
if let Some(t_obj) = dict.get(&Name::t()) {
if let Ok(resolved_t) = store.deep_resolve(t_obj) {
if let Some(s) = resolved_t.as_string() {
order.push(s.to_string_lossy());
}
}
}
}
}
order
}
fn insert_at_path(root: &mut Vec<FormField>, path: &[usize], field: &mut FormField) -> usize {
let container = get_children_at_path(root, path);
let idx = container.len();
let owned = std::mem::replace(
field,
FormField {
name: String::new(),
field_type: FormFieldType::Text,
value: None,
default_value: None,
flags: FormFieldFlags::from_bits(0),
tooltip: None,
alternate_name: None,
mapping_name: None,
max_len: None,
options: Vec::new(),
appearance_state: None,
children: Vec::new(),
controls: Vec::new(),
dirty: false,
selected_indices: Vec::new(),
additional_actions: None,
},
);
container.push(owned);
idx
}
fn get_children_at_path<'a>(
root: &'a mut Vec<FormField>,
path: &[usize],
) -> &'a mut Vec<FormField> {
let mut current = root;
for &idx in path {
current = &mut current[idx].children;
}
current
}
#[cfg(test)]
mod tests {
use super::*;
use rpdfium_core::PdfString;
fn build_store() -> ObjectStore<Vec<u8>> {
let pdf = build_minimal_pdf();
ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
}
fn build_minimal_pdf() -> Vec<u8> {
let mut pdf = Vec::new();
pdf.extend_from_slice(b"%PDF-1.4\n");
let obj1_offset = pdf.len();
pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
let obj2_offset = pdf.len();
pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
let xref_offset = pdf.len();
pdf.extend_from_slice(b"xref\n0 3\n");
pdf.extend_from_slice(b"0000000000 65535 f \r\n");
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
pdf
}
fn str_obj(s: &str) -> Object {
Object::String(PdfString::from_bytes(s.as_bytes().to_vec()))
}
fn make_field_dict(ft: &str, name: &str) -> HashMap<Name, Object> {
let mut dict = HashMap::new();
dict.insert(Name::ft(), Object::Name(Name::from(ft)));
dict.insert(Name::t(), str_obj(name));
dict
}
#[test]
fn test_no_acroform_returns_none() {
let store = build_store();
let mut catalog = HashMap::new();
catalog.insert(Name::r#type(), Object::Name(Name::from("Catalog")));
let obj = Object::Dictionary(catalog);
let result = InteractiveForm::from_catalog(&obj, &store).unwrap();
assert!(result.is_none());
}
#[test]
fn test_empty_fields_array() {
let store = build_store();
let mut acroform = HashMap::new();
acroform.insert(Name::fields(), Object::Array(Vec::new()));
let mut catalog = HashMap::new();
catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
let obj = Object::Dictionary(catalog);
let form = InteractiveForm::from_catalog(&obj, &store)
.unwrap()
.unwrap();
assert!(form.fields.is_empty());
}
#[test]
fn test_single_text_field() {
let store = build_store();
let mut field_dict = make_field_dict("Tx", "username");
field_dict.insert(Name::v(), str_obj("alice"));
let mut acroform = HashMap::new();
acroform.insert(
Name::fields(),
Object::Array(vec![Object::Dictionary(field_dict)]),
);
let mut catalog = HashMap::new();
catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
let obj = Object::Dictionary(catalog);
let form = InteractiveForm::from_catalog(&obj, &store)
.unwrap()
.unwrap();
assert_eq!(form.fields.len(), 1);
assert_eq!(form.fields[0].name, "username");
assert_eq!(form.fields[0].value.as_deref(), Some("alice"));
}
#[test]
fn test_hierarchical_fields_with_inherited_ft() {
let store = build_store();
let child1 = {
let mut d = HashMap::new();
d.insert(Name::t(), str_obj("city"));
d.insert(Name::v(), str_obj("Tokyo"));
Object::Dictionary(d)
};
let child2 = {
let mut d = HashMap::new();
d.insert(Name::t(), str_obj("zip"));
d.insert(Name::v(), str_obj("100-0001"));
Object::Dictionary(d)
};
let mut parent = HashMap::new();
parent.insert(Name::ft(), Object::Name(Name::from("Tx")));
parent.insert(Name::t(), str_obj("address"));
parent.insert(Name::kids(), Object::Array(vec![child1, child2]));
let mut acroform = HashMap::new();
acroform.insert(
Name::fields(),
Object::Array(vec![Object::Dictionary(parent)]),
);
let mut catalog = HashMap::new();
catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
let obj = Object::Dictionary(catalog);
let form = InteractiveForm::from_catalog(&obj, &store)
.unwrap()
.unwrap();
let all = form.all_fields();
let leaf_fields: Vec<_> = all
.iter()
.filter(|f| f.field_type == FormFieldType::Text && f.value.is_some())
.collect();
assert_eq!(leaf_fields.len(), 2);
let city = form.field_by_name("address.city");
assert!(city.is_some());
assert_eq!(city.unwrap().value.as_deref(), Some("Tokyo"));
let zip = form.field_by_name("address.zip");
assert!(zip.is_some());
assert_eq!(zip.unwrap().value.as_deref(), Some("100-0001"));
}
#[test]
fn test_multiple_top_level_fields() {
let store = build_store();
let field1 = make_field_dict("Tx", "name");
let field2 = make_field_dict("Btn", "submit");
let field3 = make_field_dict("Ch", "color");
let mut acroform = HashMap::new();
acroform.insert(
Name::fields(),
Object::Array(vec![
Object::Dictionary(field1),
Object::Dictionary(field2),
Object::Dictionary(field3),
]),
);
let mut catalog = HashMap::new();
catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
let obj = Object::Dictionary(catalog);
let form = InteractiveForm::from_catalog(&obj, &store)
.unwrap()
.unwrap();
assert_eq!(form.fields.len(), 3);
assert_eq!(form.fields[0].field_type, FormFieldType::Text);
assert_eq!(form.fields[1].field_type, FormFieldType::Button);
assert_eq!(form.fields[2].field_type, FormFieldType::Choice);
}
#[test]
fn test_all_fields_flat() {
let store = build_store();
let child = {
let mut d = HashMap::new();
d.insert(Name::ft(), Object::Name(Name::from("Tx")));
d.insert(Name::t(), str_obj("child"));
Object::Dictionary(d)
};
let mut parent = make_field_dict("Tx", "parent");
parent.insert(Name::kids(), Object::Array(vec![child]));
let standalone = make_field_dict("Btn", "btn1");
let mut acroform = HashMap::new();
acroform.insert(
Name::fields(),
Object::Array(vec![
Object::Dictionary(parent),
Object::Dictionary(standalone),
]),
);
let mut catalog = HashMap::new();
catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
let obj = Object::Dictionary(catalog);
let form = InteractiveForm::from_catalog(&obj, &store)
.unwrap()
.unwrap();
let all = form.all_fields();
assert_eq!(all.len(), 3);
}
#[test]
fn test_field_by_name_not_found() {
let store = build_store();
let field = make_field_dict("Tx", "exists");
let mut acroform = HashMap::new();
acroform.insert(
Name::fields(),
Object::Array(vec![Object::Dictionary(field)]),
);
let mut catalog = HashMap::new();
catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
let obj = Object::Dictionary(catalog);
let form = InteractiveForm::from_catalog(&obj, &store)
.unwrap()
.unwrap();
assert!(form.field_by_name("nonexistent").is_none());
}
#[test]
fn test_from_catalog_with_programmatic_pdf() {
let mut pdf = Vec::new();
pdf.extend_from_slice(b"%PDF-1.4\n");
let obj1_offset = pdf.len();
pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [ << /FT /Tx /T (email) /V (test@example.com) >> ] >> >>\nendobj\n");
let obj2_offset = pdf.len();
pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
let xref_offset = pdf.len();
pdf.extend_from_slice(b"xref\n0 3\n");
pdf.extend_from_slice(b"0000000000 65535 f \r\n");
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
let store = ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap();
let root_id = store.trailer().root;
let catalog = store.resolve(root_id).unwrap();
let form = InteractiveForm::from_catalog(catalog, &store)
.unwrap()
.unwrap();
assert_eq!(form.fields.len(), 1);
assert_eq!(form.fields[0].name, "email");
assert_eq!(form.fields[0].value.as_deref(), Some("test@example.com"));
}
#[test]
fn test_calculation_order_parsed() {
let store = build_store();
let field_a = {
let mut d = make_field_dict("Tx", "total");
d.insert(Name::v(), str_obj("100"));
Object::Dictionary(d)
};
let field_b = {
let mut d = make_field_dict("Tx", "tax");
d.insert(Name::v(), str_obj("10"));
Object::Dictionary(d)
};
let co_a = {
let mut d = HashMap::new();
d.insert(Name::t(), str_obj("tax"));
Object::Dictionary(d)
};
let co_b = {
let mut d = HashMap::new();
d.insert(Name::t(), str_obj("total"));
Object::Dictionary(d)
};
let mut acroform = HashMap::new();
acroform.insert(Name::fields(), Object::Array(vec![field_a, field_b]));
acroform.insert(Name::co(), Object::Array(vec![co_a, co_b]));
let mut catalog = HashMap::new();
catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
let obj = Object::Dictionary(catalog);
let form = InteractiveForm::from_catalog(&obj, &store)
.unwrap()
.unwrap();
assert_eq!(form.calculation_order(), &["tax", "total"]);
}
#[test]
fn test_calculation_order_empty_when_absent() {
let store = build_store();
let mut acroform = HashMap::new();
acroform.insert(Name::fields(), Object::Array(Vec::new()));
let mut catalog = HashMap::new();
catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
let obj = Object::Dictionary(catalog);
let form = InteractiveForm::from_catalog(&obj, &store)
.unwrap()
.unwrap();
assert!(form.calculation_order().is_empty());
}
#[test]
fn test_default_appearance_parsed() {
let store = build_store();
let mut acroform = HashMap::new();
acroform.insert(Name::fields(), Object::Array(Vec::new()));
acroform.insert(Name::da(), str_obj("0 g /Helv 12 Tf"));
acroform.insert(Name::q(), Object::Integer(1));
let mut catalog = HashMap::new();
catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
let obj = Object::Dictionary(catalog);
let form = InteractiveForm::from_catalog(&obj, &store)
.unwrap()
.unwrap();
assert_eq!(form.default_appearance(), Some("0 g /Helv 12 Tf"));
assert_eq!(form.default_alignment(), Alignment::Center);
}
#[test]
fn test_default_alignment_defaults_to_left() {
let store = build_store();
let mut acroform = HashMap::new();
acroform.insert(Name::fields(), Object::Array(Vec::new()));
let mut catalog = HashMap::new();
catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
let obj = Object::Dictionary(catalog);
let form = InteractiveForm::from_catalog(&obj, &store)
.unwrap()
.unwrap();
assert!(form.default_appearance().is_none());
assert_eq!(form.default_alignment(), Alignment::Left);
}
#[test]
fn test_check_required_fields_empty() {
let store = build_store();
let field = make_field_dict("Tx", "name");
let mut acroform = HashMap::new();
acroform.insert(
Name::fields(),
Object::Array(vec![Object::Dictionary(field)]),
);
let mut catalog = HashMap::new();
catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
let obj = Object::Dictionary(catalog);
let form = InteractiveForm::from_catalog(&obj, &store)
.unwrap()
.unwrap();
assert!(form.check_required_fields().is_empty());
}
#[test]
fn test_check_required_fields_finds_missing() {
let store = build_store();
let mut field = make_field_dict("Tx", "required_field");
field.insert(Name::ff(), Object::Integer(2));
let mut acroform = HashMap::new();
acroform.insert(
Name::fields(),
Object::Array(vec![Object::Dictionary(field)]),
);
let mut catalog = HashMap::new();
catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
let obj = Object::Dictionary(catalog);
let form = InteractiveForm::from_catalog(&obj, &store)
.unwrap()
.unwrap();
let missing = form.check_required_fields();
assert_eq!(missing.len(), 1);
assert_eq!(missing[0], "required_field");
}
#[test]
fn test_check_required_fields_skips_filled() {
let store = build_store();
let mut field = make_field_dict("Tx", "required_field");
field.insert(Name::ff(), Object::Integer(2)); field.insert(Name::v(), str_obj("filled"));
let mut acroform = HashMap::new();
acroform.insert(
Name::fields(),
Object::Array(vec![Object::Dictionary(field)]),
);
let mut catalog = HashMap::new();
catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
let obj = Object::Dictionary(catalog);
let form = InteractiveForm::from_catalog(&obj, &store)
.unwrap()
.unwrap();
assert!(form.check_required_fields().is_empty());
}
#[test]
fn test_cpdf_interactive_form_load_fields_with_referenced_names() {
let store = build_store();
let mut field1 = HashMap::new();
field1.insert(Name::ft(), Object::Name(Name::from("Btn")));
field1.insert(Name::t(), str_obj("good_string"));
let mut field2 = HashMap::new();
field2.insert(Name::ft(), Object::Name(Name::from("Btn")));
field2.insert(Name::t(), Object::Name(Name::from("bad_name")));
let mut field3 = HashMap::new();
field3.insert(Name::ft(), Object::Name(Name::from("Btn")));
field3.insert(
Name::t(),
Object::Stream {
dict: HashMap::new(),
data: rpdfium_parser::StreamData::Decoded {
data: b"bad_stream".to_vec(),
},
},
);
let mut acroform = HashMap::new();
acroform.insert(
Name::fields(),
Object::Array(vec![
Object::Dictionary(field1),
Object::Dictionary(field2),
Object::Dictionary(field3),
]),
);
let mut catalog = HashMap::new();
catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
let obj = Object::Dictionary(catalog);
let form = InteractiveForm::from_catalog(&obj, &store)
.unwrap()
.unwrap();
let good = form.field_by_name("good_string");
assert!(
good.is_some(),
"String-based /T should produce a valid field name"
);
let all = form.all_fields();
assert_eq!(all.len(), 3);
let good_field = all.iter().find(|f| f.name == "good_string");
assert!(good_field.is_some());
let empty_name_fields: Vec<_> = all.iter().filter(|f| f.name.is_empty()).collect();
assert_eq!(
empty_name_fields.len(),
2,
"Name and Stream /T values should result in empty field names"
);
}
}