From 1390ea41112ff7000e332a57e2aec788c7a56e7c Mon Sep 17 00:00:00 2001 From: midasmatterhorn <248507632+midasmatterhorn@users.noreply.github.com> Date: Fri, 16 Oct 2026 21:59:59 +0300 Subject: [PATCH] Audit: a Square the renderer never draws is not a cover Hidden (/F bit 2) and NoView (/F bit 32) annotations are not drawn on screen, so the text under them is in plain view; the audit now skips them the way the renderer does. A Print-only square still covers. --- crates/audit/src/lib.rs | 4 ++++ crates/audit/src/tests.rs | 46 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/crates/audit/src/lib.rs b/crates/audit/src/lib.rs index 894a780003..f7da4668be 100644 --- a/crates/audit/src/lib.rs +++ b/crates/audit/src/lib.rs @@ -223,6 +223,10 @@ fn annots(doc: &Document, page: &Dict) -> Result, AuditErro match dict.name(b"Subtype") { Some(b"Redact") => out.push(AnnotationCover::Redact(rect)), Some(b"Square") => { + let flags = dict.get(b"F").and_then(|f| doc.resolve(f).as_int()).unwrap_or(0); + if flags & 2 != 0 || flags & 32 != 0 { + continue; + } let alpha = dict.get(b"CA").map(|o| doc.resolve(o)).and_then(|o| o.as_f64()).unwrap_or(1.0); if alpha >= 1.0 && let Some(rgb) = annotation_color(doc, &dict, b"IC") diff --git a/crates/audit/src/tests.rs b/crates/audit/src/tests.rs index dcc479404b..13f347006c 100644 --- a/crates/audit/src/tests.rs +++ b/crates/audit/src/tests.rs @@ -192,6 +192,52 @@ fn opaque_filled_square_annotation_over_text_is_found() { assert_eq!(findings[0].covered_text, "SECRET DATA 123"); } +/// A Hidden Square (`/F 2`) is never drawn, so the text under it is not a finding. +#[test] +fn hidden_square_annotation_is_not_a_finding() { + let content = b"BT /F1 24 Tf 100 700 Td (SECRET DATA 123) Tj ET\n"; + let mut b = Builder::new(); + b.add("<< /Type /Catalog /Pages 2 0 R >>"); + b.add("<< /Type /Pages /Kids [3 0 R] /Count 1 >>"); + b.add("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> /Annots [6 0 R] >>"); + b.add_stream(content); + b.add(FONT); + b.add("<< /Type /Annot /Subtype /Square /Rect [95 685 325 725] /C [0 0 0] /IC [0 0 0] /F 2 >>"); + let findings = audit_redactions(&open(b.build())).expect("audit must run"); + assert!(findings.is_empty(), "{findings:?}"); +} + +/// A NoView Square (`/F 32`) is never drawn on screen, so the text under it is not a finding. +#[test] +fn no_view_square_annotation_is_not_a_finding() { + let content = b"BT /F1 24 Tf 100 700 Td (SECRET DATA 123) Tj ET\n"; + let mut b = Builder::new(); + b.add("<< /Type /Catalog /Pages 2 0 R >>"); + b.add("<< /Type /Pages /Kids [3 0 R] /Count 1 >>"); + b.add("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> /Annots [6 0 R] >>"); + b.add_stream(content); + b.add(FONT); + b.add("<< /Type /Annot /Subtype /Square /Rect [95 685 325 725] /C [0 0 0] /IC [0 0 0] /F 32 >>"); + let findings = audit_redactions(&open(b.build())).expect("audit must run"); + assert!(findings.is_empty(), "{findings:?}"); +} + +/// A Print-only Square (`/F 4`) is still drawn on screen, so the text under it is a finding. +#[test] +fn print_flag_square_annotation_is_still_a_finding() { + let content = b"BT /F1 24 Tf 100 700 Td (SECRET DATA 123) Tj ET\n"; + let mut b = Builder::new(); + b.add("<< /Type /Catalog /Pages 2 0 R >>"); + b.add("<< /Type /Pages /Kids [3 0 R] /Count 1 >>"); + b.add("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> /Annots [6 0 R] >>"); + b.add_stream(content); + b.add(FONT); + b.add("<< /Type /Annot /Subtype /Square /Rect [95 685 325 725] /C [0 0 0] /IC [0 0 0] /F 4 >>"); + let findings = audit_redactions(&open(b.build())).expect("audit must run"); + assert_eq!(findings.len(), 1, "{findings:?}"); + assert_eq!(findings[0].kind, FindingKind::CoveredText); +} + #[test] fn background_before_text_is_not_flagged() { // An opaque dark rectangle painted *before* the text is a background, not a cover.