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.
This commit is contained in:
midasmatterhorn
2026-10-16 21:59:59 +03:00
parent 1cb113f110
commit 1390ea4111
2 changed files with 50 additions and 0 deletions
+4
View File
@@ -223,6 +223,10 @@ fn annots(doc: &Document, page: &Dict) -> Result<Vec<AnnotationCover>, 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")
+46
View File
@@ -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.