diff --git a/crates/optimize/src/external_refs.rs b/crates/optimize/src/external_refs.rs index a9a31bca5a..4a61f37e88 100644 --- a/crates/optimize/src/external_refs.rs +++ b/crates/optimize/src/external_refs.rs @@ -373,6 +373,7 @@ fn discard_bookmarks(doc: &mut Document) -> Result { /// changed; a link counts whether it was removed or kept for its other actions. pub(crate) fn discard(doc: &mut Document, pages: &[ObjRef]) -> Result<(usize, usize), OptimizeError> { let mut links = 0; + let mut removed_links = HashSet::new(); for p in pages { let Some(pd) = doc.get(*p).as_dict().cloned() else { continue }; let Some(annots) = pd.get(b"Annots").map(|a| doc.resolve(a)).and_then(|a| a.as_array().cloned()) else { continue }; @@ -393,7 +394,11 @@ pub(crate) fn discard(doc: &mut Document, pages: &[ObjRef]) -> Result<(usize, us _ => None, }; let Some(action) = stripped else { - keep.push(x.clone()); + if !matches!(x, Object::Ref(r) if removed_links.contains(r)) { + keep.push(x.clone()); + } else { + changed = true; + } continue; }; links += 1; @@ -408,6 +413,7 @@ pub(crate) fn discard(doc: &mut Document, pages: &[ObjRef]) -> Result<(usize, us Object::Ref(r) => { doc.set(*r, Object::Dict(d)); if removed { + removed_links.insert(*r); changed = true; } else { keep.push(x.clone()); diff --git a/crates/optimize/tests/optimize.rs b/crates/optimize/tests/optimize.rs index bb9734101d..eda167b18b 100644 --- a/crates/optimize/tests/optimize.rs +++ b/crates/optimize/tests/optimize.rs @@ -2436,6 +2436,38 @@ fn links_and_bookmarks_to_other_documents_are_discarded_and_links_within_stay() assert_eq!(chain(&doc, bookmark(&doc, items[1]).get(b"A").unwrap()), ["Named"]); } +#[test] +fn repeated_indirect_remote_link_is_removed_from_one_page() { + let (mut doc, p, _) = linked_doc( + |doc, _| { + let link = doc.add(link_with(Some(action("GoToR", None)), None)); + vec![Object::Ref(link), Object::Ref(link)] + }, + &[], + ); + let report = optimize(&mut doc, &Settings { discard_external_refs: true, ..Settings::default() }).unwrap(); + assert_eq!(report.external_links, 1, "{report:?}"); + assert!(annots_of(&doc, p).is_empty(), "both references leave the page"); +} + +#[test] +fn shared_indirect_remote_link_with_empty_next_is_removed_from_every_page() { + let (mut doc, first, items) = linked_doc(|_, _| Vec::new(), &[action("Named", None), action("GoToR", None)]); + let second = page(&mut doc, &[], "BT ET"); + let link = doc.add(link_with(Some(action("GoToR", Some(Object::Array(vec![Object::Null])))), None)); + for p in [first, second] { + doc.update_dict(p, |d| d.set(b"Annots".to_vec(), Object::Array(vec![Object::Ref(link)]))).unwrap(); + } + doc.update_dict(items[0], |d| d.set(b"First".to_vec(), Object::Ref(items[1]))).unwrap(); + let report = optimize(&mut doc, &Settings { discard_external_refs: true, ..Settings::default() }).unwrap(); + assert_eq!(report.external_links, 1, "{report:?}"); + assert_eq!(report.external_bookmarks, 1, "{report:?}"); + assert!(doc.get(items[1]).as_dict().unwrap().get(b"A").is_none(), "the shared bookmark has no action"); + assert!(annots_of(&doc, first).is_empty(), "the first page loses the link"); + assert!(annots_of(&doc, second).is_empty(), "the second page loses the link"); + assert!(doc.get(link).as_dict().unwrap().get(b"A").is_none(), "the shared link has no action"); +} + #[test] fn an_action_to_another_document_hands_its_place_to_the_actions_after_it() { let (doc, p, _) = linked_doc(