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:
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
-
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
-
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).
-
Call AddRedaction / ApplyRedactions with a red fill on a page-space rect
padded around that hit (e.g. [98, 110, 461, 138]).
-
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
Bug Description
ApplyRedactionscorrectly 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:
That pattern shows up in PDFs exported from Word, LibreOffice, and many
enterprise tools. Text is still upright because each
Tmcompensates for theflip, but the CTM remains inverted at stream end.
Glyph classification in
redact_text_streamalready maps through the CTM intopage space, so the right text is removed. Overlay emission does not:
region_overlay_ops()appends a rectangle using the caller’s page-spacecoordinates 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):
Likely files:
src/redaction/engine.rs,src/redaction/overlay.rs.Steps to Reproduce
Build a minimal US Letter page (
MediaBox [0 0 612 792]) whose contentstream starts with
1 0 0 -1 0 792 cm, then draws two lines with compensatingtext matrices, e.g.:
Open the PDF and locate
"REDACT ME"via search / extraction. On this fileSearchPage reports page-space roughly
x=100, y=112, w≈359, h=24(local y=680 → page y = 792 − 680 = 112).
Call
AddRedaction/ApplyRedactionswith a red fill on a page-space rectpadded around that hit (e.g.
[98, 110, 461, 138]).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 redoverlay appears near the visual top (around where
"KEEP THIS"is). Thepage-space y≈110 is interpreted under the still-active Y-flip CTM as a
near-top local coordinate.
PDF Information (if applicable)
[0 0 612 792]cmTm(1 0 0 -1 …) so glyphs render uprightcm/Q)small generator). Original triggering documents were Word-style exports; those
will not be attached.
Environment
github.com/yfedoseev/pdf_oxide/go)Code Sample
Error Messages / Logs
Additional Context
No response
Checklist