feat(pdf): append an incremental update - #845
Merged
Merged
Conversation
`IncrementalWriter` pipes the source through untouched and writes the objects it collected after it, under a cross-reference section naming only their ids and a trailer chaining back through `/Prev`. Nothing is re-serialized, so every read-side gap — an unmodelled key, a filter we pass through, an object stream we never recompressed — survives by being copied rather than rewritten. It matches the file's own cross-reference flavor, a classic table or a stream that mints an id and an entry for itself. A recovered file and an encrypted one are refused in the constructor, which is also where the two facts the rest of the writer needs stop being optional. `/ID[1]` comes from the update's own bytes rather than a clock, so the same update written twice gives the same file. Verified plumbing-first: a no-op update that re-parses identically, then a `/Rotate` rewrite. `qpdf --check` passes and ghostscript, CoreGraphics and our own renderer all turn the page. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018e3PEzyU2oAFSzsEoWsSmz
Comments restating the code, justifying a choice at paragraph length, or
naming what the implementation used to do. `{:g}` having rounded to four
significant digits is what the commit that changed it is for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018e3PEzyU2oAFSzsEoWsSmz
andiwand
force-pushed
the
feat/pdf-incremental-writer
branch
from
September 6, 2026 15:09
a605fb4 to
c7a3896
Compare
This was referenced Sep 6, 2026
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.
🤖 Generated with Claude Code
Phase 1 of the plan in
docs/design/pdf-annotation.md— the writer itself. No annotations yet: this is the machinery they will ride on. (Was stacked on #844; rebased ontomainonce that merged.)What it does
IncrementalWriterpipes the source through untouched, appends the objects it collected, and closes with a cross-reference section naming only their ids plus a trailer chaining back through/Prev(7.5.6).Nothing is re-serialized, and that is the whole point of decision 2: every read-side gap — an unmodelled key, a filter we pass through, an object stream we never recompressed — survives because it is copied, not rewritten.
xref_kind()from feat(pdf): expose the parse facts a writer needs #844: a classic table, or a cross-reference stream that mints an id and an entry for itself.start_xref_position()andxref_kind()stop beingoptionalfor everything downstream./ID[1]is derived from the update's own bytes, not from a clock. Writing the same update twice gives the same file, which is what lets a test pin the output at all.Verified plumbing-first
Sequenced so the append machinery could fail separately from any annotation semantics:
/Rotaterewrite — one integer on an existing dictionary. No new object types, no appearance stream, but it exercises the risky part: overriding an object the file already defines.Then new objects, a stream object with a computed
/Length, both refusals, and an end-to-end pass overstyle-various-1.pdfwhere page 1 rotates and page 2, the existing annotations and every content stream stay intact.Externally, on real output:
qpdf --checkclean, and ghostscript, CoreGraphics and our own renderer all honour the new rotation — odr's page box turns 8.5×11in into 11×8.5in.Known gap
A page dictionary living inside an object stream is not tested. It has to be rewritten uncompressed in the new section — legal, the newer entry wins — but no fixture we have puts one there. Recorded in the doc; owed before Phase 2 ships.
Also
docs/design/pdf-annotation.mdmarks Phase 1 done and folds in what it settled and what it did not.328 PDF/parser/writer tests pass;
-Werrorclean; clang-tidy clean on the new files.No consumer-visible change — internal API only — so no changelog entry.