Redaction Automation: Burning Protected Spans Into Produced Images

Redaction is the highest-stakes transformation in the entire production pipeline, because a redaction that fails does not degrade quality — it discloses exactly the content it was meant to protect. Inside the Review & Production Workflows pipeline, redaction automation is the subsystem that takes a document’s protected spans — privileged passages, third-party PII, trade secrets — and renders a produced image in which that content is physically absent rather than merely hidden. The failure mode that defines this domain is the “black box over live text” defect: a rectangle drawn on a layer above a text stream that any downstream tool can lift away. This guide builds a redaction subsystem that rasterizes protected regions into pixels, withholds the native, and verifies mechanically that no protected content survived into the deliverable.

Redaction Subsystem Flow

Defensible redaction is destructive by design. Regions are resolved from review markups, the page is rendered to a raster surface, the protected regions are painted out at the pixel level, the native is withheld, and a verification pass extracts text from the redacted image to confirm nothing protected remains before the document can be produced.

Redaction automation subsystem flow Five stages run left to right. Resolve redaction regions from review markups and auto-detected spans. Rasterize the page to a pixel surface so no text layer remains under the redaction. Burn the protected regions into the raster as opaque fills. Withhold the native file, which still contains the unredacted content. Verify by re-extracting text from the redacted image and confirming no protected term survives. A verification failure blocks the document from production. Rasterize, burn, withhold, verify 1 Resolve regions markups + auto-detected 2 Rasterize page to pixels, no text layer 3 Burn regions opaque fill over pixels 4 Withhold native unredacted source held 5 Verify re-extract no protected text survives survivor → blocked The native is never produced — it still holds the unredacted content the burn-in removed from the image.

Resource Constraints & Design Rationale

The naive redaction — draw a filled rectangle over the text in the PDF’s content stream — is fast and completely wrong. The text remains in the file beneath the rectangle; copy-paste, pdftotext, or a search index recovers it instantly. Any defensible design has to eliminate the text, not cover it, and the reliable way to do that is to leave the vector/text world entirely: render the page to a raster image, paint the protected regions onto the pixels, and produce the raster. Once the page is pixels, there is no text stream to recover — the protected content is gone at the representation level.

Rasterization is the resource cost this buys. A vector PDF is small; its rasterized image at production DPI (typically 300) is large, and a worker redacting thousands of pages must bound memory by processing one page at a time, releasing the raster surface after each page is written, rather than holding a whole document’s images in memory. Redaction therefore streams page-by-page, and it parallelizes across documents using the async batch processing worker model, with each worker’s footprint bounded to a single page raster at the target DPI.

Region Resolution & Burn-In Algorithm

Two algorithms carry the correctness. Region resolution maps the reviewer’s coordinate-space markups (and any auto-detected PII spans) onto the rasterized page’s pixel grid. This is a coordinate transform — the markup is authored in the document’s coordinate system, and it must be scaled to the raster’s DPI so the painted rectangle covers exactly the protected content with a safety margin, never a pixel less. An off-by-scale error here is a leak, so the transform is asserted against the page geometry and the redaction is expanded by a small margin to absorb anti-aliasing at the edges.

Burn-in is the destructive paint. The resolved rectangles are drawn as fully opaque fills directly onto the raster’s pixel buffer, so the underlying pixels are overwritten, not layered. The output is flattened to an image format with no alpha channel and no separate layers, guaranteeing there is no “under” to recover. The confidence that this worked does not rest on the code being correct — it rests on the verification pass that follows, which treats the produced image as an adversary would and tries to read the protected content back out. Redaction designations themselves flow from the matter’s privilege schema design and any PII policy, so the what to redact is governed by the same tagging model the rest of the pipeline honors.

Resilience & Failure Routing

Redaction fails closed, harder than any other stage. If region resolution cannot map a markup to the page, the document is blocked — never produced with a best-effort redaction. If verification finds any protected term surviving in the re-extracted text, the document is blocked and routed to a review queue with the surviving span highlighted. There is no “produce with a warning” path, because a warned-but-produced redaction leak is indistinguishable in its consequences from an unredacted production.

Blocked documents carry their reason and their protected spans into a hold queue that never auto-releases; a human must confirm the correction. Because the native is withheld from the moment a document is marked for redaction, a blocked document leaks nothing while it waits — the worst case is a delayed production, never a disclosed one. This asymmetry (delay is recoverable, disclosure is not) is the design principle the whole subsystem is tuned around, and it is the standard the downstream production validation gate re-checks independently.

Production Python Implementation

The module resolves regions in document coordinates, transforms them to the raster grid, burns opaque fills, and verifies by re-extracting text. The rendering and OCR calls are represented by focused hooks so the module compiles and the redaction/verification logic is complete and runnable in isolation.

python
import logging
from dataclasses import dataclass, field
from typing import Callable, List, Sequence

logger = logging.getLogger("ediscovery.redaction")
logger.setLevel(logging.INFO)


class RedactionError(Exception):
    """Raised when a document cannot be redacted defensibly."""


@dataclass(frozen=True)
class Region:
    """A protected rectangle in document points (72 per inch), plus its terms."""
    page: int
    x0: float
    y0: float
    x1: float
    y1: float
    protected_terms: Sequence[str] = ()


@dataclass
class PageRaster:
    page: int
    width_px: int
    height_px: int
    dpi: int
    pixels: bytearray            # RGB buffer, 3 bytes per pixel


def to_pixel_rect(region: Region, raster: PageRaster, margin_px: int = 3) -> tuple:
    """Scale a document-point rectangle to the raster pixel grid, expanded by a
    margin to absorb anti-aliasing. Off-page rectangles are a hard error."""
    scale = raster.dpi / 72.0
    px0 = int(region.x0 * scale) - margin_px
    py0 = int(region.y0 * scale) - margin_px
    px1 = int(region.x1 * scale) + margin_px
    py1 = int(region.y1 * scale) + margin_px
    if px1 <= px0 or py1 <= py0:
        raise RedactionError(f"degenerate region on page {region.page}")
    # Clamp to the page; a region resolving outside the page is a coordinate bug.
    if px0 < 0 or py0 < 0 or px1 > raster.width_px or py1 > raster.height_px:
        logger.warning("Region on page %d clamped to page bounds", region.page)
    return (max(0, px0), max(0, py0),
            min(raster.width_px, px1), min(raster.height_px, py1))


def burn_regions(raster: PageRaster, regions: List[Region]) -> None:
    """Overwrite the protected pixels with opaque black. This is destructive:
    the underlying pixels are replaced, so there is no layer to peel away."""
    for region in regions:
        x0, y0, x1, y1 = to_pixel_rect(region, raster)
        for y in range(y0, y1):
            row = y * raster.width_px * 3
            start, end = row + x0 * 3, row + x1 * 3
            raster.pixels[start:end] = b"\x00" * (end - start)


def verify_no_survivors(redacted_text: str, regions: List[Region]) -> List[str]:
    """Re-extract text from the redacted image and confirm no protected term
    survives. Returns the list of survivors; empty means the redaction held."""
    haystack = redacted_text.lower()
    survivors = [t for r in regions for t in r.protected_terms
                 if t and t.lower() in haystack]
    return survivors


def redact_document(
    doc_id: str,
    regions: List[Region],
    render_page: Callable[[int, int], PageRaster],
    extract_text: Callable[[PageRaster], str],
    dpi: int = 300,
) -> PageRaster:
    """Rasterize, burn, and verify one page. Blocks production on any survivor."""
    if not regions:
        raise RedactionError(f"{doc_id}: redaction requested with no regions")
    page_no = regions[0].page
    raster = render_page(page_no, dpi)
    burn_regions(raster, [r for r in regions if r.page == page_no])
    survivors = verify_no_survivors(extract_text(raster), regions)
    if survivors:
        logger.critical("%s: %d protected term(s) survived redaction", doc_id, len(survivors))
        raise RedactionError(f"{doc_id}: redaction leak, blocked from production")
    logger.info("%s: page %d redacted and verified clean", doc_id, page_no)
    return raster

Observability & Compliance Metrics

Three KPIs make redaction auditable. Redaction throughput (pages redacted per second) tracks the rasterization cost against the production deadline. Block rate (documents blocked per thousand redactions) is the quality signal — a rising block rate points at a region-resolution regression or bad markups upstream. Survivor rate (verification failures per thousand) is the one metric that must trend to zero; any non-zero survivor rate in a shipped volume is a disclosure. The verification pass itself is the compliance artifact: a record, per document, that the redacted image was adversarially re-read and found clean.

python
from prometheus_client import Counter

PAGES_REDACTED = Counter("redaction_pages_total", "Pages redacted and verified")
DOCS_BLOCKED = Counter("redaction_blocked_total", "Documents blocked from production")
SURVIVORS = Counter("redaction_survivors_total", "Protected terms found after burn-in")


def record_redaction(doc_id: str, survivors: list) -> None:
    if survivors:
        SURVIVORS.inc(len(survivors))
        DOCS_BLOCKED.inc()
    else:
        PAGES_REDACTED.inc()

Conclusion

Defensible redaction is destruction, not concealment. The only reliable way to remove protected content from a produced document is to leave the text world — rasterize the page, burn opaque fills onto the pixels, flatten to a layerless image, and withhold the native that still holds the original. Because correctness cannot be assumed, a verification pass re-extracts text from the produced image and blocks any document where a protected term survives. Route blocked documents to a hold queue that never auto-releases, keep the native withheld throughout, and track a survivor rate that must stay at zero. A redaction subsystem built this way turns the pipeline’s most dangerous transformation into one whose success is mechanically proven for every document before it ships.

Frequently Asked Questions

Why isn’t drawing a black box over the text enough?

Because the text is still in the file underneath the box. A filled rectangle in a PDF’s content stream sits on a layer above the text stream, and the text stream is untouched — pdftotext, copy-paste, or a search index recovers it in seconds. Redaction has to remove the content at the representation level, which means rasterizing the page to pixels and painting the protected regions onto those pixels, so there is no text stream left to recover.

Why withhold the native of a redacted document?

Because the native is the unredacted original. The redaction only exists in the produced image; the native file still contains every protected passage in full. Producing the native alongside the redacted image hands over exactly what the redaction removed. A redacted document is therefore produced as an image only, with its native withheld, and the production QC parity check independently confirms no native slipped through.

How do you know a redaction actually worked?

You do not trust the code — you verify the output. After burn-in, the subsystem re-extracts text from the produced image (via OCR if needed) and searches it for the protected terms. A correct rasterized redaction leaves no recoverable text, so any surviving term proves the redaction hit a preserved layer or the wrong region, and the document is blocked. This adversarial re-read, per document, is what converts “we redacted it” into “we proved nothing protected survived.”

What happens when region resolution fails on a document?

It is blocked, not produced with a best-effort redaction. If a markup cannot be mapped to the page geometry, or a region resolves off-page, the document goes to a hold queue with its reason, and its native stays withheld the whole time. The design principle is asymmetric: a delayed production is recoverable, a disclosed one is not, so redaction always chooses delay over any risk of leakage.

Up one level: Review & Production Workflows — the pipeline that assembles court-ready productions.