From 6fd053b91844c141a2a24947cb12ed10482d2aec Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:31:51 +0200 Subject: [PATCH] organize: deleting the opening page drops the catalog OpenAction (#821) (#855) --- crates/organize/src/lib.rs | 14 ++++++++++++- crates/organize/src/tests.rs | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/crates/organize/src/lib.rs b/crates/organize/src/lib.rs index 1459d67c6..4d6cacb17 100644 --- a/crates/organize/src/lib.rs +++ b/crates/organize/src/lib.rs @@ -195,7 +195,7 @@ pub fn delete_pages(doc: &mut Document, indices: &[usize]) -> Result<(), Organiz } /// Bookmarks, and links on the `keep` pages, that go to one of the `gone` pages lose that -/// destination (/Dest, or a GoTo /A), rather than pointing at nothing. +/// destination (/Dest, or a GoTo /A), rather than pointing at nothing; so does the catalog's /OpenAction. fn drop_destinations_to(doc: &mut Document, gone: &[ObjRef], keep: &[(ObjRef, Dict)]) -> Result<(), OrganizeError> { let mut holders = outline::items(doc); for (p, _) in keep { @@ -222,6 +222,18 @@ fn drop_destinations_to(doc: &mut Document, gone: &[ObjRef], keep: &[(ObjRef, Di dead.push((h, b"A")); } } + // The catalog's /OpenAction is either a destination array or a GoTo action; one that opens a + // deleted page is dropped, as a bookmark's would be. + if let Some(root) = doc.root() { + let open = doc.get(root).as_dict().and_then(|c| c.get(b"OpenAction").map(|o| doc.resolve(o))); + let opens_gone = match open.as_deref() { + Some(Object::Dict(a)) => a.name(b"S") == Some(b"GoTo") && to_gone(a.get(b"D")), + other => to_gone(other), + }; + if opens_gone { + dead.push((root, b"OpenAction")); + } + } for (h, key) in dead { doc.update_dict(h, |d| { d.remove(key); diff --git a/crates/organize/src/tests.rs b/crates/organize/src/tests.rs index 5dddf0296..a6d4d978a 100644 --- a/crates/organize/src/tests.rs +++ b/crates/organize/src/tests.rs @@ -170,6 +170,45 @@ fn deleted_pages_are_not_kept_by_what_points_at_them() { assert_eq!(form.as_dict().and_then(|f| f.get(b"Fields")).and_then(Object::as_array).map(Vec::len), Some(0), "the field went with its page"); } +#[test] +fn deleting_the_opening_page_drops_the_open_action() { + let open_action = |doc: &Document| doc.get(doc.root().unwrap()).as_dict().unwrap().get(b"OpenAction").cloned(); + let set_open = |doc: &mut Document, value: Object| { + let root = doc.root().unwrap(); + doc.update_dict(root, |c| c.set(b"OpenAction".to_vec(), value)).unwrap(); + }; + let page = |doc: &Document, i: usize| pages(doc).unwrap()[i].obj; + for as_action in [false, true] { + let target = |doc: &Document, i: usize| { + let dest = Object::Array(vec![Object::Ref(page(doc, i)), Object::Name(b"Fit".to_vec())]); + if as_action { + let mut a = Dict::new(); + a.set(b"S".to_vec(), Object::Name(b"GoTo".to_vec())); + a.set(b"D".to_vec(), dest); + Object::Dict(a) + } else { + dest + } + }; + // The opening page goes: no OpenAction is left to point at nothing. + let mut doc = doc_a(); + let open = target(&doc, 0); + set_open(&mut doc, open); + delete_pages(&mut doc, &[0]).unwrap(); + let out = full_roundtrip(&doc); + assert_eq!(labels(&out), ["A2", "A3"]); + assert_eq!(open_action(&out), None); + // Another page goes: the opening page keeps its OpenAction. + let mut doc = doc_a(); + let open = target(&doc, 0); + set_open(&mut doc, open); + delete_pages(&mut doc, &[2]).unwrap(); + let out = full_roundtrip(&doc); + assert_eq!(labels(&out), ["A1", "A2"]); + assert!(open_action(&out).is_some()); + } +} + #[test] fn cannot_delete_every_page_or_missing_pages() { let mut doc = open(fixture());