diff --git a/crates/organize/src/import.rs b/crates/organize/src/import.rs index ab8c15076..405dc8a5f 100644 --- a/crates/organize/src/import.rs +++ b/crates/organize/src/import.rs @@ -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() { diff --git a/crates/organize/src/tests.rs b/crates/organize/src/tests.rs index 00eebaedc..ad4688e25 100644 --- a/crates/organize/src/tests.rs +++ b/crates/organize/src/tests.rs @@ -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.