fix(redact): removing comments keeps non-comment annotations (#1325) (#1375)

Remove Hidden Information counted and removed as a comment every
annotation that was not a link, widget, pop-up, file attachment or
redaction mark, so a Comments-only cleanup also deleted Watermark,
PrinterMark, TrapNet, Screen, Movie, RichMedia and 3D annotations.

Use pdfcraft_annot::is_comment_subtype, which the comments list already
uses, and keep excluding file attachments (removed with Attachments)
and redaction marks as before.

Adds tests::removing_comments_keeps_annotations_that_are_not_comments,
which fails before this change.
This commit is contained in:
Marco Arnulfo authored and GitHub committed 2026-10-12 06:27:04 -04:00
1 parent f97f24cfbc
commit 9593ff7419
2 files changed
+30 -1

No files matched your search

+3 -1
View File
@@ -113,8 +113,10 @@ fn outline_len(doc: &Document, first: Option<&Object>, depth: usize) -> usize {
n
}
/// A comment as the comments list sees it, less file attachments (removed with Attachments) and
/// redaction marks.
fn is_comment(subtype: &[u8]) -> bool {
!matches!(subtype, b"Link" | b"Widget" | b"Popup" | b"FileAttachment" | b"Redact")
!matches!(subtype, b"FileAttachment" | b"Redact") && pdfcraft_annot::is_comment_subtype(&String::from_utf8_lossy(subtype))
}
/// The layers that are off in the default configuration.
+27
View File
@@ -533,6 +533,33 @@ fn hidden_information_is_counted_and_removed() {
assert!(shown, "the field's value stays visible as page content");
}
/// Issue #1325: removing comments keeps the annotations that are not comments: the multimedia
/// and print-production ones (Watermark, PrinterMark, TrapNet, Screen, Movie, RichMedia, 3D).
#[test]
fn removing_comments_keeps_annotations_that_are_not_comments() {
use crate::sanitize::{Hidden, remove_hidden, scan};
let kept = ["Link", "Watermark", "PrinterMark", "TrapNet", "Screen", "Movie", "RichMedia", "3D"];
let annots: Vec<String> = (0..=kept.len()).map(|i| format!("{} 0 R", i + 4)).collect();
let mut objs = vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Resources << >> /Annots [{}] >>", annots.join(" ")).into_bytes(),
b"<< /Type /Annot /Subtype /Square /Rect [10 10 50 50] >>".to_vec(),
];
objs.extend(kept.iter().map(|s| format!("<< /Type /Annot /Subtype /{s} /Rect [60 60 90 90] >>").into_bytes()));
let mut doc = pdf(objs);
let counts: std::collections::HashMap<Hidden, usize> = scan(&doc).into_iter().collect();
assert_eq!(counts[&Hidden::Comments], 1, "only the Square");
assert_eq!(remove_hidden(&mut doc, &[Hidden::Comments]).unwrap(), [(Hidden::Comments, 1)]);
let doc = reopen(&doc);
let p = pdfcraft_model::pages(&doc).swap_remove(0);
let left: Vec<String> = annots_of(&doc, &p.dict)
.iter()
.filter_map(|a| doc.resolve(a).as_dict().and_then(|d| d.name(b"Subtype")).map(|s| String::from_utf8_lossy(s).into_owned()))
.collect();
assert_eq!(left, kept);
}
#[test]
fn overlay_text_takes_its_font_size_colour_alignment_and_repeats() {
let mut doc = one_page(b"BT /F1 12 Tf 20 250 Td (Secret salary figures) Tj ET", "", Vec::new());