diff --git a/crates/redact/src/lib.rs b/crates/redact/src/lib.rs index 2bf9819c46..c623eb23f0 100644 --- a/crates/redact/src/lib.rs +++ b/crates/redact/src/lib.rs @@ -458,12 +458,18 @@ fn apply_within(doc: &mut Document, pages: Option<&[usize]>, images: pdfcraft_co let pages = pdfcraft_model::pages(doc); let hanging = hanging_annotations(doc, pages.iter().flat_map(|p| annots_of(doc, &p.dict))); let mut todo: Vec = removed_everywhere.iter().copied().collect(); + // (a reply is counted once, by whichever link reaches it: one with both `/IRT` and `/Parent` + // is a reply however the walk orders them) + let (mut reached, mut counted) = (HashSet::new(), HashSet::new()); while let Some(parent) = todo.pop() { for &(r, reply) in hanging.get(&parent).into_iter().flatten() { if removed_everywhere.insert(r) { - report.annotations += usize::from(reply); + reached.insert(r); todo.push(r); } + if reply && reached.contains(&r) && counted.insert(r) { + report.annotations += 1; + } } } for p in &pages { diff --git a/crates/redact/src/tests.rs b/crates/redact/src/tests.rs index 52e276bbcf..b9225ad94b 100644 --- a/crates/redact/src/tests.rs +++ b/crates/redact/src/tests.rs @@ -3872,6 +3872,27 @@ fn a_reply_on_another_page_to_a_removed_popup_goes_too() { } } +#[test] +fn a_reply_with_a_parent_link_is_counted_whatever_order_the_walk_takes() { + // 7 hangs off 6 (/Parent) and off 8 (/IRT): the walk can reach it by its /Parent link first + let objs: Vec> = vec![ + b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), + b"<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>".to_vec(), + b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 5 0 R /Annots [6 0 R 8 0 R] >>".to_vec(), + b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 5 0 R /Annots [7 0 R] >>".to_vec(), + stream("", b"BT ET"), + b"<< /Type /Annot /Subtype /Text /Rect [20 20 40 40] /Contents (PARENT-NOTE) /Popup 8 0 R >>".to_vec(), + b"<< /Type /Annot /Subtype /Text /Rect [250 250 270 270] /Parent 6 0 R /IRT 8 0 R /Contents (BOTH-LINKS) >>".to_vec(), + b"<< /Type /Annot /Subtype /Popup /Rect [200 150 290 190] /Parent 6 0 R >>".to_vec(), + ]; + let mut doc = pdf(objs); + mark(&mut doc, 0, &[[10.0, 10.0, 70.0, 50.0]], ""); + let report = apply(&mut doc, None).unwrap(); + let second = pdfcraft_model::pages(&doc).swap_remove(1); + assert!(annots_of(&doc, &second.dict).is_empty()); + assert_eq!(report.annotations, 2, "the mark's own comment and the one reply: {report:?}"); +} + #[test] fn a_rewritten_form_xobject_drops_the_source_text_of_added_content() { // A form drawing invisible text whose dictionary repeats it in /PCAdded.