Skip to content

[Bug]: ApplyRedactions opaque overlay is drawn in content-stream CTM space, not page space #1015

Description

@gngj

Bug Description

ApplyRedactions correctly removes the glyphs that intersect a redaction region,
but paints the opaque overlay rectangle at the wrong visual position when
the page content stream ends with a non-identity CTM still active.

The common case is a page-level Y-flip at the start of the stream:

1 0 0 -1 0 792 cm

That pattern shows up in PDFs exported from Word, LibreOffice, and many
enterprise tools. Text is still upright because each Tm compensates for the
flip, but the CTM remains inverted at stream end.

Glyph classification in redact_text_stream already maps through the CTM into
page space, so the right text is removed. Overlay emission does not:
region_overlay_ops() appends a rectangle using the caller’s page-space
coordinates at the end of the rewritten stream, where those numbers are then
interpreted in the stream’s local (flipped) space.

So you get a silent-wrong redaction: sensitive text is gone from the content
stream, but the black/colored box lands somewhere else on the page (often near
the opposite edge on a Y-flip).

Affected path (as of 0.3.77):

redact_content_stream()
  → redact_text_stream()   ← CTM-aware glyph / region intersection (correct)
  → serialize operators      ← preserves cm / q / Q (correct)
  → region_overlay_ops()   ← page-space rect, no inverse CTM (bug)

Likely files: src/redaction/engine.rs, src/redaction/overlay.rs.

Steps to Reproduce

  1. Build a minimal US Letter page (MediaBox [0 0 612 792]) whose content
    stream starts with 1 0 0 -1 0 792 cm, then draws two lines with compensating
    text matrices, e.g.:

    1 0 0 -1 0 792 cm
    BT
    /F1 24 Tf
    1 0 0 -1 100 100 Tm
    (KEEP THIS - near visual top) Tj
    1 0 0 -1 100 680 Tm
    (REDACT ME - near visual bottom) Tj
    ET
    
  2. Open the PDF and locate "REDACT ME" via search / extraction. On this file
    SearchPage reports page-space roughly x=100, y=112, w≈359, h=24
    (local y=680 → page y = 792 − 680 = 112).

  3. Call AddRedaction / ApplyRedactions with a red fill on a page-space rect
    padded around that hit (e.g. [98, 110, 461, 138]).

  4. Save and render the page. Compare to the original render.

Attaching flipped_ctm.pdf and redacted.pdf (synthetic page before/after
ApplyRedactions). No third-party PDF involved.

Expected Behavior

The opaque overlay covers "REDACT ME" at the visual bottom of the page
(page-space y ≈ 110–138). Glyphs for that string are removed, and the box sits
on top of where they were.

Actual Behavior

Glyphs for "REDACT ME" are removed , but the red
overlay appears near the visual top (around where "KEEP THIS" is). The
page-space y≈110 is interpreted under the still-active Y-flip CTM as a
near-top local coordinate.

PDF Information (if applicable)

  • PDF version: 1.4 (synthetic)
  • Page count: 1
  • Encryption: None
  • MediaBox: [0 0 612 792]
  • Relevant structure:
    • Content stream begins with page-level Y-flip cm
    • Text uses compensating Tm (1 0 0 -1 …) so glyphs render upright
    • Helvetica Type1, no ToUnicode / no Tagged PDF needed
    • CTM at stream end ≠ identity (no balancing inverse cm / Q)
  • Minimal reproducer available? Yes — hand-built synthetic PDF (bytes in a
    small generator). Original triggering documents were Word-style exports; those
    will not be attached.

Environment

  • OS: macOS (darwin-arm64)
  • Rust version: n/a for the FFI repro (using the Go binding)
  • pdf_oxide version: 0.3.77
  • Language binding: Go FFI (github.com/yfedoseev/pdf_oxide/go)

Code Sample

doc, _ := pdfoxide.Open("flipped_ctm.pdf")
results, _ := doc.SearchPage(0, "REDACT ME", false)
// results[0] ≈ x=100 y=112 w=359 h=24
doc.Close()

editor, _ := pdfoxide.OpenEditor("flipped_ctm.pdf")
r := results[0]
rect := [4]float64{float64(r.X) - 2, float64(r.Y) - 2,
    float64(r.X+r.Width) + 2, float64(r.Y+r.Height) + 2}
red := [3]float64{1, 0, 0}
editor.AddRedaction(0, rect, &red)
glyphs, _ := editor.ApplyRedactions(false) // glyphs removed, overlay misplaced
editor.Save("redacted.pdf")

Error Messages / Logs

No error. ApplyRedactions returns success and a positive glyph-removal count.
Failure mode is silent-wrong overlay placement (visible only by rendering or by
inspecting the appended overlay operators vs the active CTM).

Additional Context

No response

Checklist

  • I have searched existing issues to make sure this is not a duplicate
  • I can reproduce this reliably and have given the exact steps
  • I have not attached any copyrighted or third-party PDF
  • This report is written in my own words (not AI-generated)
  • I am using the latest version of pdf_oxide

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions