organize: preserve widget actions and inherited field state

This commit is contained in:
midasmatterhorn
2026-10-23 21:31:13 +03:00
parent 81e054231c
commit 1f41c0ccaa
2 changed files with 78 additions and 9 deletions
+35 -9
View File
@@ -58,6 +58,7 @@ const WIDGET_KEYS: &[&[u8]] = &[
b"StructParents",
b"OC",
b"Tabs",
b"A",
];
struct Copier<'a> {
@@ -435,7 +436,8 @@ fn import_pages_mapped(
{
let r = dst.add(Object::Null);
let c = copier.copy_value(dst, &resolved, Some(r))?;
let Object::Dict(mut d) = c else { continue };
let Object::Dict(d) = c else { continue };
let (_, mut d) = field_and_widget(dst, &d);
d.set(b"Parent".to_vec(), Object::Ref(parent));
// The new widget is not a field root of its own.
copier.fields.retain(|f| *f != r);
@@ -864,14 +866,7 @@ fn add_kid(dst: &mut Document, field: ObjRef, kid: ObjRef) -> Result<(), Organiz
/// field stays one.
fn split_own_field(dst: &mut Document, fields: &mut [ObjRef], widget: ObjRef) {
let Some(d) = dst.get(widget).as_dict().cloned() else { return };
let (mut field, mut child) = (Dict::new(), Dict::new());
for (k, v) in d.iter() {
if WIDGET_KEYS.contains(&k.as_slice()) {
child.set(k.clone(), v.clone());
} else {
field.set(k.clone(), v.clone());
}
}
let (mut field, mut child) = field_and_widget(dst, &d);
let parent = dst.add(Object::Null);
field.set(b"Kids".to_vec(), Object::Array(vec![Object::Ref(widget)]));
child.set(b"Parent".to_vec(), Object::Ref(parent));
@@ -882,6 +877,37 @@ fn split_own_field(dst: &mut Document, fields: &mut [ObjRef], widget: ObjRef) {
}
}
/// A combined field/widget has two additional-action namespaces (ISO 32000-1 tables 194/196).
/// Partition them as well as the ordinary entries, including when copying a later widget.
fn field_and_widget(doc: &Document, d: &Dict) -> (Dict, Dict) {
let (mut field, mut child) = (Dict::new(), Dict::new());
for (k, v) in d.iter() {
if k.as_slice() == b"AA"
&& let Some(actions) = doc.resolve(v).as_dict().cloned()
{
let (mut field_actions, mut widget_actions) = (Dict::new(), Dict::new());
for (trigger, action) in actions.iter() {
if [b"E".as_slice(), b"X", b"D", b"U", b"Fo", b"Bl", b"PO", b"PC", b"PV", b"PI"].contains(&trigger.as_slice()) {
widget_actions.set(trigger.clone(), action.clone());
} else {
field_actions.set(trigger.clone(), action.clone());
}
}
if field_actions.iter().next().is_some() {
field.set(k.clone(), Object::Dict(field_actions));
}
if widget_actions.iter().next().is_some() {
child.set(k.clone(), Object::Dict(widget_actions));
}
} else if WIDGET_KEYS.contains(&k.as_slice()) {
child.set(k.clone(), v.clone());
} else {
field.set(k.clone(), v.clone());
}
}
(field, child)
}
/// Add top-level fields to the destination's `/AcroForm /Fields` (creating the form if needed).
fn register_fields(dst: &mut Document, fields: &[ObjRef]) -> Result<(), OrganizeError> {
if fields.is_empty() {
+43
View File
@@ -690,6 +690,9 @@ fn repeated_page_splits_a_combined_field_widget() {
assert_ne!(w1, w2, "each copy of the page has its own widget");
for (i, w) in [(0, w1), (1, w2)] {
let d = out.get(w).as_dict().unwrap().clone();
for key in [b"T".as_slice(), b"V", b"FT"] {
assert!(!d.contains(key), "a widget inherits field entries: {key:?}");
}
assert_eq!(d.reference(b"P"), Some(ps[i].obj), "/P names the page holding the widget");
let parent = d.reference(b"Parent").expect("the widget has a parent field");
let f = out.get(parent).as_dict().unwrap().clone();
@@ -712,6 +715,46 @@ fn repeated_page_splits_a_combined_field_widget() {
}
}
#[test]
fn splitting_a_combined_widget_keeps_annotation_actions_and_field_triggers() {
let mut src = doc_a();
let page = pages(&src).unwrap()[1].obj;
let list = page_dict(&src, 1).get(b"Annots").and_then(|a| a.as_array()).cloned().unwrap();
let widget =
list.iter().filter_map(Object::as_ref).find(|r| src.get(*r).as_dict().is_some_and(|d| d.name(b"Subtype") == Some(b"Widget"))).unwrap();
let mut action = Dict::new();
action.set(b"S".to_vec(), Object::name("GoTo"));
action.set(b"D".to_vec(), Object::Array(vec![Object::Ref(page), Object::name("Fit")]));
let mut named = Dict::new();
named.set(b"S".to_vec(), Object::name("Named"));
named.set(b"N".to_vec(), Object::name("NextPage"));
let mut aa = Dict::new();
aa.set(b"E".to_vec(), Object::Dict(named.clone()));
aa.set(b"K".to_vec(), Object::Dict(named));
let aa = src.add(Object::Dict(aa));
src.update_dict(widget, |d| {
d.set(b"A".to_vec(), Object::Dict(action));
d.set(b"AA".to_vec(), Object::Ref(aa));
})
.unwrap();
let out = full_roundtrip(&extract_pages(&src, &[1, 1]).unwrap());
let ps = pages(&out).unwrap();
for i in 0..2 {
let w = annots(&out, i).into_iter().find(|d| d.name(b"Subtype") == Some(b"Widget")).unwrap();
let action = out.resolve(w.get(b"A").expect("the widget keeps its action"));
let dest = action.as_dict().unwrap().get(b"D").and_then(|a| a.as_array()).unwrap()[0].as_ref().unwrap();
assert!(ps.iter().any(|p| p.obj == dest), "the action targets a copied page");
let waa = out.resolve(w.get(b"AA").expect("widget triggers"));
assert!(waa.as_dict().unwrap().contains(b"E"));
assert!(!waa.as_dict().unwrap().contains(b"K"));
let parent = out.get(w.reference(b"Parent").unwrap()).as_dict().unwrap().clone();
assert!(!parent.contains(b"A"));
let faa = out.resolve(parent.get(b"AA").expect("field triggers"));
assert!(faa.as_dict().unwrap().contains(b"K"));
assert!(!faa.as_dict().unwrap().contains(b"E"));
}
}
#[test]
fn repeated_page_gets_independent_annotations() {
// A1 has a link, A2 a link and a form field, A3 a note with a popup; each is listed twice.