The faux-redaction audit held every operator of a page's content and of each form XObject in a vector before walking it, so a stream of millions of short operators (compressed to a few kilobytes) was materialised whole within the page's decode budget. Each Do and gs also copied the whole XObject or ExtGState table to pick one entry, and each annotation listed in /Annots was deep-copied before its subtype was read. The walk now reads operators one at a time through pdfcraft_content::ops, borrows resource tables and annotation dictionaries through the resolved Arc, charges a hidden line's text bytes (padding included) against the comparison bound once per cover, and charges each finding's record against the output bound, so a shared annotation list on many pages or withheld text cannot grow the finding list for free.
pdfcraft-audit
Layer L4 (may use edit): verification tooling, the faux-redaction audit.
let findings: Vec<Finding> = audit_redactions(&doc)?; // page, kind, rect, covered_text
It reports two kinds of finding:
- faux redaction: an opaque filled rectangle painted after text it overlaps, dark enough against the text's colour (WCAG contrast below 3) to hide it, while the text is still extractable underneath; opaque filled Square annotations count as covers too;
- unapplied mark: a Redact annotation that was marked but never applied.
A properly applied redaction removes the text, so it never flags. Page backgrounds, table shading and highlights don't either: they're painted before the text, or light against it.
A rectangle covers a text line when it hides at least half of one of the line's words, at
least 40% of the area of the words the line draws, or at least 40% of the line's box together
with a positive-area intersection with one of its words. The gap an applied redaction leaves (a TJ adjustment where the text was) is in the line's box but
holds no word, so a redaction box over it alone does not count.
Limits, by design and documented rather than hidden:
- only filled rectangles count as covers, drawn with
reor as a singlem/lsubpath whose corners form an axis-aligned rectangle (not other paths, shadings or patterns), and a translucent one (/cabelow 1) is not flagged; - covers inside form XObjects are found, but only text drawn directly in the page content is checked under them;
- annotation rectangles are compared in unrotated user space, so
/Rotatepages may misalign; - a cover that hides less than half of every word it touches, less than 40% of a line's box and less than 40% of its words (a sliver of a long value) is not flagged.
It fails closed: an undecodable content stream, malformed /Annots, form XObjects nested deeper
than 32 or in a cycle, a walk past the operator bound, more than 50 million cover-against-word
comparisons on a page, or more than 64 MiB of covered text in the report is an error, never "no
findings".
A document whose permissions withhold copying (Table 22, bit 5) still gets every finding, with
an empty covered_text: the audit hands out no text that copying would refuse.
Surfaces: the doc_audit_redactions automation tool and pdfcraft-cli audit. Both report each
rectangle in points from the top-left of the page as displayed (y down, after /Rotate), like
the other tools; AuditFinding::rect itself stays in PDF user space.