mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 18:47:22 -06:00
organize: preserve widget actions and inherited field state
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user