diff --git a/crates/annot/src/info.rs b/crates/annot/src/info.rs index c4b385935e..6a90541437 100644 --- a/crates/annot/src/info.rs +++ b/crates/annot/src/info.rs @@ -60,7 +60,10 @@ pub struct PopupInfo { pub(crate) fn own_popup(doc: &Document, own: Option, annot: &Dict) -> Option { let pr = annot.reference(b"Popup")?; let is_own = doc.get(pr).as_dict().is_some_and(|pd| { - pd.name(b"Subtype") == Some(b"Popup") && pd.get(b"Parent").is_none_or(|parent| parent.as_ref().is_some_and(|parent| Some(parent) == own)) + pd.name(b"Subtype") == Some(b"Popup") + && pd + .get(b"Parent") + .is_none_or(|parent| parent.as_ref().is_some_and(|parent| own.is_some_and(|own| final_ref(doc, parent) == final_ref(doc, own)))) }); is_own.then_some(pr) } @@ -183,7 +186,7 @@ pub fn summaries(doc: &Document) -> Vec { fill_sign: is_fill_sign(doc, d), no_rotate: drawn_upright(doc, d), points, - popup: popup_info(doc, entry.as_ref(), d), + popup: popup_info(doc, entry.as_ref().map(|r| final_ref(doc, r)), d), }); } } diff --git a/crates/annot/src/tests.rs b/crates/annot/src/tests.rs index c8e71c68d6..e20f001a65 100644 --- a/crates/annot/src/tests.rs +++ b/crates/annot/src/tests.rs @@ -2255,6 +2255,15 @@ fn pop_ups_open_close_and_move_and_are_reported() { assert!(matches!(set_popup(&mut doc, 0, note, Some([0.0, 0.0, 0.5, 0.5]), None, &meta("n1")), Err(AnnotError::Invalid(_)))); assert!(matches!(set_popup(&mut doc, 0, note, Some([f64::NAN, 0.0, 10.0, 10.0]), None, &meta("n1")), Err(AnnotError::Invalid(_)))); assert!(matches!(set_popup(&mut doc, 0, 99, None, Some(true), &meta("n1")), Err(AnnotError::NoSuchAnnotation { .. }))); + + // #2137 review: /Annots may list the note through an object that is only a reference to it. + // Its pop-up's /Parent names the note itself, so the pop-up is still the note's. + let p = page_ref(&doc, 0).unwrap(); + let via = doc.add(Object::Ref(r)); + let list: Vec = annots(&doc, p).into_iter().map(|o| if o.as_ref() == Some(r) { Object::Ref(via) } else { o }).collect(); + set_annots(&mut doc, p, list).unwrap(); + let listed = summaries(&doc).into_iter().find(|s| s.index == note).unwrap(); + assert_eq!(listed.popup, Some(PopupInfo { rect: [150.0, 100.0, 290.0, 200.0], open: false }), "listed through {via:?}"); } /// #2137 review: a pop-up that is not the mark's own is neither reported as its pop-up nor diff --git a/crates/render/src/inspect/annotations.rs b/crates/render/src/inspect/annotations.rs index e16680119a..71e95024ab 100644 --- a/crates/render/src/inspect/annotations.rs +++ b/crates/render/src/inspect/annotations.rs @@ -160,10 +160,11 @@ impl<'a> Inspector<'a> { }; // Only the comment's own pop-up: a pop-up annotation whose /Parent, if any, is a // reference to this comment (keep in step with `pdfcraft_annot::summaries`). - let own = a.as_reference().ok(); + let own = self.resolved_id(a); let is_own = |p: &&Dictionary| { self.name(p, b"Subtype").as_deref() == Some("Popup") - && p.get(b"Parent").map_or(true, |parent| parent.as_reference().ok().is_some_and(|id| Some(id) == own)) + && p.get(b"Parent") + .map_or(true, |parent| parent.as_reference().is_ok() && self.resolved_id(parent).is_some_and(|id| Some(id) == own)) }; let popup = d.get(b"Popup").ok().and_then(|o| self.dict(o)).filter(is_own).map(|p| { let [x0, y0, x1, y1] = rect4(self.resolve(p.get(b"Rect").unwrap_or(&Object::Null))); diff --git a/crates/render/src/inspect/tests.rs b/crates/render/src/inspect/tests.rs index 5258c4f682..94794b698b 100644 --- a/crates/render/src/inspect/tests.rs +++ b/crates/render/src/inspect/tests.rs @@ -1875,6 +1875,13 @@ fn a_comments_pop_up_is_read_with_its_place_and_state() { let note = info.annotations.iter().find(|a| a.subtype == "Text").expect("the note"); assert_eq!(note.popup.is_some(), own, "/Popup {target} {extra}"); } + + // #2137 review: /Annots may list the note through an object that is only a reference to + // it; its pop-up's /Parent names the note itself, and is still its own. + let text = String::from_utf8_lossy(bytes).replace("/Annots [4 0 R", "/Annots [8 0 R").replace("trailer", "8 0 obj 4 0 R endobj\ntrailer"); + let info = inspect(Arc::new(text.into_bytes()), None).expect("opens"); + let note = info.annotations.iter().find(|a| a.subtype == "Text").expect("the note"); + assert_eq!(note.popup, Some(Popup { rect: [50.0, 160.0, 190.0, 280.0], open: true }), "listed through 8 0 R"); } #[test]