diff --git a/crates/organize/src/import.rs b/crates/organize/src/import.rs index 9b68dc2ed..4d6c5957a 100644 --- a/crates/organize/src/import.rs +++ b/crates/organize/src/import.rs @@ -237,8 +237,16 @@ fn import_pages_mapped(dst: &mut Document, src: &Document, src_pages: &[usize], let mut new_pages = Vec::new(); let mut done: HashMap = HashMap::new(); for (page, new, inherited) in targets { - // A page listed twice: make a second, independent page object sharing resources. - let new = if done.contains_key(&page) { dst.add(Object::Null) } else { new }; + // A page listed twice: make a second, independent page object sharing resources. Its + // annotations (with their popups and form fields) are copied again, so each page owns its + // copies and their `/P` names it, as Duplicate Pages does (#818). + let new = if done.contains_key(&page) { + let copies: std::collections::HashSet = copier.annots.iter().chain(&copier.fields).copied().collect(); + copier.map.retain(|_, &mut d| !copies.contains(&d)); + dst.add(Object::Null) + } else { + new + }; let src_dict = src.get(page).as_dict().cloned().unwrap_or_default(); let mut d = Dict::new(); for (k, v) in src_dict.iter() { diff --git a/crates/organize/src/tests.rs b/crates/organize/src/tests.rs index 6f139d825..55815bd14 100644 --- a/crates/organize/src/tests.rs +++ b/crates/organize/src/tests.rs @@ -443,6 +443,35 @@ fn links_are_rewired_to_copied_pages_or_dropped() { assert!(!link.contains(b"Dest") && !link.contains(b"A")); } +#[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. + let out = full_roundtrip(&extract_pages(&doc_a(), &[0, 0, 1, 1, 2, 2]).unwrap()); + let ps = pages(&out).unwrap(); + let refs: Vec> = (0..6) + .map(|i| page_dict(&out, i).get(b"Annots").map(|a| out.resolve(a).as_array().cloned().unwrap_or_default()).unwrap_or_default()) + .map(|list| list.iter().filter_map(|a| a.as_ref()).collect()) + .collect(); + for i in (0..6).step_by(2) { + assert!(!refs[i].is_empty()); + assert_eq!(refs[i].len(), refs[i + 1].len()); + for (a, b) in refs[i].iter().zip(&refs[i + 1]) { + assert_ne!(a, b, "each copy of the page has its own annotation objects"); + } + } + for (i, list) in refs.iter().enumerate() { + for a in list { + assert_eq!(out.get(*a).as_dict().unwrap().reference(b"P"), Some(ps[i].obj), "/P names the page holding the copy"); + } + } + // Each note keeps its own popup, pointing back at that note. + let popups: Vec = [4, 5].iter().map(|&i| out.get(refs[i][0]).as_dict().unwrap().reference(b"Popup").unwrap()).collect(); + assert_ne!(popups[0], popups[1]); + for (k, p) in popups.iter().enumerate() { + assert_eq!(out.get(*p).as_dict().unwrap().reference(b"Parent"), Some(refs[4 + k][0])); + } +} + #[test] fn form_fields_come_along_and_are_registered() { let out = full_roundtrip(&extract_pages(&doc_a(), &[1]).unwrap());