Repository navigation
feat(run): say which bytes of the stream drew a run - #31
Merged
Merged
Conversation
Run.Op is the byte range of the operation that drew the run, and Run.InForm says whether that range is the page's own content or a Form XObject's. Closes #30. It is what a caller needs to CHANGE a page rather than only read it: a redaction has to cut the glyphs inside a region out of the content stream, and which glyphs are inside is what this package already works out — the text matrix, Td/TD/Tm/T*, Tc/Tw/Tz, the font metrics. The only thing missing was where to cut. reader.Operation already carries Start and End for exactly this reason ("They are here so that a content stream can be edited IN PLACE"), so this is a pass-through: the walk records the span and the depth of every operation it dispatches, and show() puts them on the run. The alternative was for each caller to pair runs with operators by counting them. That does not survive a form, invisible text, a run split across operators, or an operator that draws nothing — and it fails where no test notices, because both sides of the comparison move together. Two things the API has to say out loud, and does: - several runs can share one span. A TJ array shows several strings with one operator, so being rid of one piece means REWRITING the span, not deleting it. - InForm means the span indexes the form's stream. Cutting the page there would cut somewhere else, and an XObject is shared, so editing it to change one page changes every page that draws it. The last test is the one that matters: it cuts a span out of a real stream, re-extracts, and checks that the remaining run says the same thing AT THE SAME PLACE and the removed one is gone — a span nobody has cut with is a number, not a feature. All four fail with the walk not recording (empty spans, and a form's text claiming to be the page's). 63 functions at 100.0%, vet clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #30.
What
Run.Opis the byte range of the operation that drew the run, andRun.InFormsays whether that range is in the page's own content stream or a Form
XObject's.
Why
To change a page rather than only read it. A redaction has to cut the glyphs
inside a region out of the content stream — ISO 32000-1 §12.5.6.23 requires a
conforming reader to "remove all traces of the specified content", not to draw
a box over it — and deciding which glyphs are inside a region is precisely
what this package already does: the text matrix,
Td/TD/Tm/T*,Tc/Tw/Tz, the font metrics. The only thing missing was where to cut.reader.Operationalready carriesStartandEnd, and says why:So this is a pass-through, not a new mechanism: the walk records the span and
the depth of each operation it dispatches, and
showputs them on the run.The alternative, and why it is worse
A caller could pair runs with operators by counting them — the nth run from
the nth text-showing operator. That does not survive a Form XObject,
invisible text, a run split across operators, or an operator that draws
nothing. Worse, it fails in the way no test catches: both sides of the
comparison move together, so an off-by-one puts the cut in the wrong place and
everything still passes. The thing that computes the placement is the only
thing that knows both halves.
The other alternative — a second text state machine in the caller — is a second
implementation of
Runs, and the first time the two disagreed the documentwould be cut somewhere other than where the caller was told the text was.
Two things the API says out loud
TJarray shows several stringswith a single operator, so being rid of one piece means rewriting the
span, not deleting it — deleting it would take the other pieces away too.
TestTheRunsOfOneTJShareItsSpanpins this, and checks the two pieces reallyare drawn apart so the fixture is not vacuous.
InFormmeans the span is not the page's. Its offsets are in theform's stream; cutting the page there would cut somewhere else entirely, and
an XObject is shared, so editing it to change one page changes every page that
draws it. A caller editing a page must refuse such a run — the shape
ops.Doc.RecolourTextalready has, whereUnreachedTextcounts what it couldnot reach. The test reads the form's span back out of the form's own stream
to show which stream it indexes.
The test that matters
TestASpanIsEnoughToRemoveARuncuts a span out of a real stream, re-extracts,and checks that
width — removing one operator must not shift the text that stays, which is
the difference between a redaction and a re-typesetting, and the reason the
span covers the operands too.
A span nobody has cut with is a number, not a feature.
The control
With the walk not recording (
e.op, e.depth = Span{}, 0) all four tests fail,each saying what went wrong:
Checks
gofmt,go vetand the full suite clean; 63 functions at 100.0% bygo tool cover -func, which is the instrument the gate reads.Next in the chain: ops#67
(redaction that removes rather than covers).
🤖 Generated with Claude Code