use std::collections::BTreeMap;
use stet_graphics::document_structure::{
AnnotationRecord, AnnotationSubtype, ChoiceOption, FieldType, FieldValue, FormRecord,
WidgetAnnotation,
};
use crate::annotations::widget_annotation_dict;
use crate::pdf_objects::PdfObj;
use crate::pdf_writer::PdfWriter;
struct WidgetPlacement {
obj_ref: u32,
parent_ref: Option<u32>,
is_radio_kid: bool,
}
pub struct AcroFormOutput {
pub acroform_ref: u32,
pub per_page_widget_refs: Vec<Vec<u32>>,
}
pub fn write_form(
writer: &mut PdfWriter,
widgets: &[(usize, AnnotationRecord)],
form_record: Option<&FormRecord>,
page_count: usize,
) -> Option<AcroFormOutput> {
if widgets.is_empty() && form_record.is_none() {
return None;
}
let mut per_page_widget_refs: Vec<Vec<u32>> = vec![Vec::new(); page_count];
if widgets.is_empty() {
let acroform_ref = writer.alloc_obj();
writer.set_object(
acroform_ref,
&PdfObj::Dict(build_acroform_entries(form_record, &[])),
);
return Some(AcroFormOutput {
acroform_ref,
per_page_widget_refs,
});
}
let mut by_name: BTreeMap<String, Vec<usize>> = BTreeMap::new();
for (slot, (_orig_idx, record)) in widgets.iter().enumerate() {
if let AnnotationSubtype::Widget(w) = &record.subtype {
by_name.entry(w.field_name.clone()).or_default().push(slot);
}
}
let widget_obj_refs: Vec<u32> = (0..widgets.len()).map(|_| writer.alloc_obj()).collect();
let mut node_refs: BTreeMap<String, u32> = BTreeMap::new();
let mut placements: Vec<Option<WidgetPlacement>> = (0..widgets.len()).map(|_| None).collect();
for (full_name, slots) in &by_name {
let segments: Vec<&str> = full_name.split('.').filter(|s| !s.is_empty()).collect();
if segments.is_empty() {
continue;
}
for depth in 1..segments.len() {
let prefix = segments[..depth].join(".");
node_refs
.entry(prefix)
.or_insert_with(|| writer.alloc_obj());
}
let parent_path = if segments.len() > 1 {
Some(segments[..segments.len() - 1].join("."))
} else {
None
};
if slots.len() == 1 {
let slot = slots[0];
placements[slot] = Some(WidgetPlacement {
obj_ref: widget_obj_refs[slot],
parent_ref: parent_path
.as_deref()
.and_then(|p| node_refs.get(p).copied()),
is_radio_kid: false,
});
} else {
let parent_ref = *node_refs
.entry(full_name.clone())
.or_insert_with(|| writer.alloc_obj());
for &slot in slots {
placements[slot] = Some(WidgetPlacement {
obj_ref: widget_obj_refs[slot],
parent_ref: Some(parent_ref),
is_radio_kid: true,
});
}
}
}
for (slot, placement_opt) in placements.iter().enumerate() {
let Some(placement) = placement_opt else {
continue;
};
let (_orig_idx, record) = &widgets[slot];
let widget_data = match &record.subtype {
AnnotationSubtype::Widget(w) => w,
_ => continue,
};
let leaf_segment = leaf_segment_of(&widget_data.field_name);
let merge_field_keys = !placement.is_radio_kid;
let dict = widget_annotation_dict(
record,
widget_data,
leaf_segment,
placement.parent_ref,
merge_field_keys,
);
writer.set_object(placement.obj_ref, &PdfObj::Dict(dict));
if record.page > 0 && (record.page as usize) <= page_count {
per_page_widget_refs[record.page as usize - 1].push(placement.obj_ref);
}
}
for (full_name, slots_for_name) in &by_name {
let segments: Vec<&str> = full_name.split('.').filter(|s| !s.is_empty()).collect();
if segments.is_empty() {
continue;
}
for depth in 1..segments.len() {
let prefix = segments[..depth].join(".");
let Some(&parent_ref) = node_refs.get(&prefix) else {
continue;
};
if writer.is_object_set(parent_ref) {
continue;
}
let parent_segment = segments[depth - 1];
let grandparent_path = if depth > 1 {
Some(segments[..depth - 1].join("."))
} else {
None
};
let grandparent_ref = grandparent_path.and_then(|p| node_refs.get(&p).copied());
let kids = collect_kids_for_prefix(&prefix, &node_refs, &by_name, &widget_obj_refs);
let entries = container_field_dict(parent_segment, grandparent_ref, &kids);
writer.set_object(parent_ref, &PdfObj::Dict(entries));
}
if slots_for_name.len() > 1 {
let Some(&parent_ref) = node_refs.get(full_name) else {
continue;
};
if writer.is_object_set(parent_ref) {
continue;
}
let parent_segment = segments[segments.len() - 1];
let grandparent_path = if segments.len() > 1 {
Some(segments[..segments.len() - 1].join("."))
} else {
None
};
let grandparent_ref = grandparent_path.and_then(|p| node_refs.get(&p).copied());
let mut sorted_slots = slots_for_name.clone();
sorted_slots.sort_by_key(|&s| widgets[s].0);
let kid_refs: Vec<u32> = sorted_slots.iter().map(|&s| widget_obj_refs[s]).collect();
let template_widget = match &widgets[sorted_slots[0]].1.subtype {
AnnotationSubtype::Widget(w) => w,
_ => continue,
};
let entries =
radio_parent_dict(parent_segment, grandparent_ref, &kid_refs, template_widget);
writer.set_object(parent_ref, &PdfObj::Dict(entries));
}
}
let mut root_refs: Vec<u32> = Vec::new();
let mut seen_roots: BTreeMap<String, ()> = BTreeMap::new();
for (full_name, slots_for_name) in &by_name {
let root_segment = match full_name.split('.').find(|s| !s.is_empty()) {
Some(s) => s.to_string(),
None => continue,
};
if seen_roots.contains_key(&root_segment) {
continue;
}
seen_roots.insert(root_segment.clone(), ());
if let Some(&r) = node_refs.get(&root_segment) {
root_refs.push(r);
} else if slots_for_name.len() == 1 && full_name == &root_segment {
root_refs.push(widget_obj_refs[slots_for_name[0]]);
}
}
let acroform_ref = writer.alloc_obj();
writer.set_object(
acroform_ref,
&PdfObj::Dict(build_acroform_entries(form_record, &root_refs)),
);
Some(AcroFormOutput {
acroform_ref,
per_page_widget_refs,
})
}
fn build_acroform_entries(
form_record: Option<&FormRecord>,
root_refs: &[u32],
) -> Vec<(Vec<u8>, PdfObj)> {
let mut entries: Vec<(Vec<u8>, PdfObj)> = vec![(
b"Fields".to_vec(),
PdfObj::Array(root_refs.iter().map(|&r| PdfObj::Ref(r)).collect()),
)];
let need_appearances = form_record.and_then(|f| f.need_appearances).unwrap_or(true);
entries.push((b"NeedAppearances".to_vec(), PdfObj::Bool(need_appearances)));
if let Some(form) = form_record {
if let Some(flags) = form.sig_flags {
entries.push((b"SigFlags".to_vec(), PdfObj::Int(flags as i64)));
}
if let Some(co) = &form.calc_order {
entries.push((
b"CO".to_vec(),
PdfObj::Array(
co.iter()
.map(|n| PdfObj::LitString(n.clone().into_bytes()))
.collect(),
),
));
}
if let Some(da) = &form.default_appearance {
entries.push((b"DA".to_vec(), PdfObj::LitString(da.clone().into_bytes())));
}
if let Some(q) = form.quadding {
entries.push((b"Q".to_vec(), PdfObj::Int(q as i64)));
}
}
entries
}
fn container_field_dict(
segment: &str,
parent_ref: Option<u32>,
kids: &[u32],
) -> Vec<(Vec<u8>, PdfObj)> {
let mut entries: Vec<(Vec<u8>, PdfObj)> = vec![(
b"T".to_vec(),
PdfObj::LitString(segment.as_bytes().to_vec()),
)];
if let Some(p) = parent_ref {
entries.push((b"Parent".to_vec(), PdfObj::Ref(p)));
}
entries.push((
b"Kids".to_vec(),
PdfObj::Array(kids.iter().map(|&r| PdfObj::Ref(r)).collect()),
));
entries
}
fn radio_parent_dict(
segment: &str,
parent_ref: Option<u32>,
kid_refs: &[u32],
template: &WidgetAnnotation,
) -> Vec<(Vec<u8>, PdfObj)> {
let mut entries: Vec<(Vec<u8>, PdfObj)> = vec![(
b"T".to_vec(),
PdfObj::LitString(segment.as_bytes().to_vec()),
)];
if let Some(p) = parent_ref {
entries.push((b"Parent".to_vec(), PdfObj::Ref(p)));
}
push_field_level_keys(&mut entries, template);
entries.push((
b"Kids".to_vec(),
PdfObj::Array(kid_refs.iter().map(|&r| PdfObj::Ref(r)).collect()),
));
entries
}
pub(crate) fn push_field_level_keys(
entries: &mut Vec<(Vec<u8>, PdfObj)>,
widget: &WidgetAnnotation,
) {
if let Some(ft) = widget.field_type {
entries.push((b"FT".to_vec(), PdfObj::name(field_type_name(ft))));
}
if let Some(flags) = widget.flags {
entries.push((b"Ff".to_vec(), PdfObj::Int(flags as i64)));
}
if let Some(value) = &widget.value {
entries.push((b"V".to_vec(), encode_field_value(value)));
}
if let Some(default) = &widget.default_value {
entries.push((b"DV".to_vec(), encode_field_value(default)));
}
if let Some(max_len) = widget.max_len {
entries.push((b"MaxLen".to_vec(), PdfObj::Int(max_len as i64)));
}
if let Some(opts) = &widget.options {
entries.push((b"Opt".to_vec(), encode_options(opts)));
}
if let Some(q) = widget.quadding {
entries.push((b"Q".to_vec(), PdfObj::Int(q as i64)));
}
if let Some(da) = &widget.default_appearance {
entries.push((b"DA".to_vec(), PdfObj::LitString(da.clone().into_bytes())));
}
}
fn field_type_name(ft: FieldType) -> &'static str {
match ft {
FieldType::Btn => "Btn",
FieldType::Tx => "Tx",
FieldType::Ch => "Ch",
FieldType::Sig => "Sig",
_ => "Tx",
}
}
fn encode_field_value(value: &FieldValue) -> PdfObj {
match value {
FieldValue::Text(s) => PdfObj::LitString(s.clone().into_bytes()),
FieldValue::Name(n) => PdfObj::name(n),
FieldValue::TextArray(vs) => PdfObj::Array(
vs.iter()
.map(|v| PdfObj::LitString(v.clone().into_bytes()))
.collect(),
),
_ => PdfObj::Null,
}
}
fn encode_options(opts: &[ChoiceOption]) -> PdfObj {
PdfObj::Array(
opts.iter()
.map(|o| {
if o.export == o.display {
PdfObj::LitString(o.export.clone().into_bytes())
} else {
PdfObj::Array(vec![
PdfObj::LitString(o.export.clone().into_bytes()),
PdfObj::LitString(o.display.clone().into_bytes()),
])
}
})
.collect(),
)
}
fn leaf_segment_of(field_name: &str) -> &str {
field_name.rsplit('.').find(|s| !s.is_empty()).unwrap_or("")
}
fn collect_kids_for_prefix(
prefix: &str,
node_refs: &BTreeMap<String, u32>,
by_name: &BTreeMap<String, Vec<usize>>,
widget_refs_by_slot: &[u32],
) -> Vec<u32> {
let prefix_depth = prefix.split('.').filter(|s| !s.is_empty()).count();
let mut seen: BTreeMap<String, u32> = BTreeMap::new();
for (full_name, slots) in by_name {
let segments: Vec<&str> = full_name.split('.').filter(|s| !s.is_empty()).collect();
if segments.len() <= prefix_depth {
continue;
}
let candidate_prefix = segments[..prefix_depth].join(".");
if candidate_prefix != prefix {
continue;
}
let child_path = segments[..prefix_depth + 1].join(".");
if seen.contains_key(&child_path) {
continue;
}
if let Some(&r) = node_refs.get(&child_path) {
seen.insert(child_path, r);
continue;
}
if &child_path == full_name && slots.len() == 1 {
let slot = slots[0];
if let Some(&r) = widget_refs_by_slot.get(slot) {
seen.insert(child_path, r);
}
}
}
seen.into_values().collect()
}
#[cfg(test)]
mod tests {
use super::*;
use stet_graphics::document_structure::{AnnotationRecord, AnnotationSubtype, FieldType};
fn make_widget(name: &str, ft: FieldType) -> AnnotationRecord {
AnnotationRecord {
page: 1,
rect: [0.0, 0.0, 100.0, 20.0],
color: None,
border: None,
title: None,
contents: None,
subtype: AnnotationSubtype::Widget(WidgetAnnotation {
field_name: name.to_string(),
field_type: Some(ft),
..WidgetAnnotation::default()
}),
}
}
#[test]
fn leaf_segment_simple() {
assert_eq!(leaf_segment_of("firstname"), "firstname");
}
#[test]
fn leaf_segment_dotted() {
assert_eq!(leaf_segment_of("order.shipping.street"), "street");
}
#[test]
fn leaf_segment_trailing_dot() {
assert_eq!(leaf_segment_of("a.b."), "b");
}
#[test]
fn write_form_emits_acroform_and_widget_per_page_refs() {
let mut writer = PdfWriter::new();
let widgets = vec![
(0, make_widget("firstname", FieldType::Tx)),
(1, make_widget("lastname", FieldType::Tx)),
];
let out = write_form(&mut writer, &widgets, None, 1).expect("acroform emitted");
assert_eq!(out.per_page_widget_refs.len(), 1);
assert_eq!(out.per_page_widget_refs[0].len(), 2);
assert!(writer.is_object_set(out.acroform_ref));
}
#[test]
fn write_form_dotted_name_creates_container_parents() {
let mut writer = PdfWriter::new();
let widgets = vec![(0, make_widget("order.shipping.street", FieldType::Tx))];
let out = write_form(&mut writer, &widgets, None, 1).expect("acroform emitted");
assert!(writer.is_object_set(out.acroform_ref));
assert_eq!(out.per_page_widget_refs[0].len(), 1);
}
#[test]
fn write_form_radio_group_collects_three_kids() {
let mut writer = PdfWriter::new();
let widgets = vec![
(0, make_widget("answer", FieldType::Btn)),
(1, make_widget("answer", FieldType::Btn)),
(2, make_widget("answer", FieldType::Btn)),
];
let out = write_form(&mut writer, &widgets, None, 1).expect("acroform emitted");
assert_eq!(out.per_page_widget_refs[0].len(), 3);
}
#[test]
fn write_form_no_widgets_no_form_returns_none() {
let mut writer = PdfWriter::new();
assert!(write_form(&mut writer, &[], None, 1).is_none());
}
#[test]
fn write_form_form_only_emits_minimal_acroform() {
let mut writer = PdfWriter::new();
let form = FormRecord {
sig_flags: Some(3),
..FormRecord::default()
};
let out = write_form(&mut writer, &[], Some(&form), 1).expect("acroform emitted");
assert!(writer.is_object_set(out.acroform_ref));
assert!(out.per_page_widget_refs[0].is_empty());
}
}