Skip to content

ops: destroy the pixels under a redaction - #81

Merged
tannevaled merged 1 commit into
mainfrom
feat/destroy-the-pixels
Nov 9, 2026
Merged

tannevaled merged 1 commit into
mainfrom
feat/destroy-the-pixels

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

ISO 32000-1, 12.5.6.23:

If a portion of an image is contained in a redaction region, that portion of
the image data shall be destroyed; clipping or image masks shall not be used
to hide that data.

Redact refused every region a picture was drawn across — which, for a scanned
page, is every region there is. It now overwrites the samples under the region
and writes the picture back.

What it can do that to

A picture whose samples are plain (one the reader has already unfiltered),
eight bits a component, DeviceGray or DeviceRGB, drawn upright and drawn
once on the page.

Everything else is refused by its own name, with the reason, at Redact —
where a caller can still choose another region or another tool — because one
function plans the change for both the check and the write:

refused why
a JPEG (or any /Filter) it would have to be decoded and re-encoded: a size and a quality question of its own
drawn turned or skewed the box it covers looks the same as an upright one's, so mapping the region through it destroys more than was asked for
drawn more than once replacing the picture replaces it for every Do, so blanking one placement blanks the others, elsewhere on the page
inline it is written into the content stream; there is no resource to write back
/ImageMask, /SMask, /Mask, /Decode they change what a sample means
not 8 bits a component this blanks whole bytes
/Indexed, /ICCBased, … a zero sample in a palette is the palette's first colour, which may be white

extract v0.11.0's Image.Matrix was added for the upright test; this PR bumps
to it.

Two traps, both with a test that would catch them again

⛔ The page's /Resources are rebuilt, not copied. Copying them would have
left the original picture in the file beside the blanked one — referenced by
nothing, holding every pixel the redaction promised to remove. That is defect
#74 one object along. The test walks every object of the written file and
decodes it, because samples are compressed and a raw grep cannot see them.
decorate goes through the same function, or a page both redacted and stamped
would have had its original picture put back, silently.

⛔ The sample grid counts down while the page counts up. Row zero is the
picture's top row and v runs from its bottom edge. Reading one for the other
blanks the mirror image of what was asked for, which looks like a redaction
until somebody compares it with the page — so the test gives every row its own
colour and names which rows must survive. A region ending part way into a sample
destroys all of it: a third of a secret is a secret.

Gate: ok github.com/go-pdfkit/ops 13.8s coverage: 100.0% of statements,
covblocks: 0 blocks uncovered.

Closes #80 for the plain case; the JPEG half stays open there.

🤖 Generated with Claude Code

ISO 32000-1 12.5.6.23 is explicit:

    If a portion of an image is contained in a redaction region, that portion
    of the image data shall be destroyed; clipping or image masks shall not be
    used to hide that data.

Redact refused every region a picture was drawn across, which for a scanned
page — one picture — is everything. The samples under the region are now
overwritten and the picture is written back.

What it does that to is a picture whose samples are PLAIN (one the reader has
already unfiltered), eight bits a component, DeviceGray or DeviceRGB, drawn
UPRIGHT and drawn ONCE. The rest are refused by their own names, each with its
reason: a JPEG would have to be decoded and re-encoded, which is a size and a
quality question of its own; a turned picture cannot be mapped into its samples
by the box it covers without destroying more than was asked for (extract
v0.11.0's Image.Matrix was added for exactly this); a picture drawn twice would
be blanked for both placements; an inline image has nothing to write back to;
an /ImageMask, /SMask, /Mask or /Decode changes what a sample means; an
/Indexed zero is the palette's FIRST colour, which may be white.

⛔⛔ THE PAGE'S RESOURCES ARE REBUILT, NOT COPIED. Copying them would have put
the original picture into the file beside the blanked one, referenced by
nothing and holding every pixel the redaction promised to remove -- defect #74
one object along. The test walks EVERY object of the written file and decodes
it, because a picture's samples are compressed and a raw grep cannot see them.
decorate goes through the same function, or a page both redacted and stamped
would have had its original picture put back, silently.

⛔ The sample grid counts DOWN while the page counts UP: row zero is the
picture's top row and v runs from its bottom edge. Reading one for the other
blanks the mirror image of what was asked for, which looks like a redaction
until somebody compares it with the page — so the test gives every row its own
colour and names which must survive. A region ending part way into a sample
destroys all of it: a third of a secret is a secret.

Closes #80 for the plain case; the JPEG half stays open there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 951ea58 into main Nov 9, 2026
1 check passed
@tannevaled
tannevaled deleted the feat/destroy-the-pixels branch November 9, 2026 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Redact a region a picture is drawn across: destroy the pixels

1 participant