fix(pdf): answer whether a file can be annotated, and retire the plan - #851
Merged
Conversation
The design doc becomes a record: the decisions and the alternatives they beat, which is what a reader changing this next needs, and a short account of how it landed. The phase list, the day estimates and the line counts go — git and the pull requests hold that. Two things cost more than the estimate said and neither was found by a test, so both are written down: a `mix-blend-mode` inside an svg composites against that svg's own canvas rather than the page, and `selectionchange` fires on every character a drag covers. `pdf/AGENTS.md` no longer calls the module read-only, and its module table has the two files that write. Advances the reference-output pins for #849, which put `data-odr-page` and `data-odr-space` on every pdf page and two files in `resources/`. 829 files changed; no line differs beyond those attributes, the stylesheet link and the script tag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018e3PEzyU2oAFSzsEoWsSmz
andiwand
force-pushed
the
docs/retire-pdf-annotation-plan
branch
from
September 6, 2026 16:30
913d553 to
9e4b0e0
Compare
`capabilities().annotate` was true for every pdf, so a caller keying a button off it got one that throws: `IncrementalWriter` refuses a file whose cross-reference table was rebuilt, and one declaring an `/Encrypt`. `PdfFile::is_annotatable` is the counterpart of `Document::is_editable` — `FileTypeCapabilities` answers for the format, this for the file in hand — and `DecodedFile::capabilities()` narrows `annotate` by it. Bound in all four bindings, as `is_editable` is. Neither condition is `encryption_state()`. An owner-locked pdf opens with the empty password and reports itself *unencrypted*, yet its `/Encrypt` still stops us appending, so the answer is settled during the construction-time probe, where the parser that knows both is already in hand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018e3PEzyU2oAFSzsEoWsSmz
andiwand
force-pushed
the
docs/retire-pdf-annotation-plan
branch
from
September 6, 2026 16:34
9e4b0e0 to
833ddba
Compare
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
The close-out for #843–#850. Rebased onto main; four things.
1. A capability that lied
DecodedFile::capabilities().annotatewas true for every pdf, whileannotate()throws for a fileIncrementalWriterrefuses. A mobile UI keying its annotate button off it gets a button that throws. Found by the other session sweeping the public corpus.The sharp part is that the obvious fix — narrowing by
encryption_state(), astranslate_htmldoes — would not have worked.Casio_WVA-M650-7AJF.pdfis owner-locked: the empty password opens it, so odr reports itnot_encryptedandpassword_encrypted() == false, yet the/Encryptis still there and its new objects would need a key the parser deliberately never keeps.So, as you suggested, a counterpart to
Document::is_editable:FileTypeCapabilitiesanswers for the format; this answers for the file in hand, andcapabilities().annotateis narrowed by it. Bound in all four bindings, asis_editableis.Both conditions are settled in the construction-time probe, where the
DocumentParserthat knows them is already in hand — no second parse. Verified:is_annotatablestyle-various-1.pdfCasio_WVA-M650-7AJF.pdf(owner-locked)startxref2. The reference-output pins
#849 put
data-odr-pageanddata-odr-spaceon every pdf page and two files inresources/, so main emits html the pinned references do not have.odr-public 27 files, odr-private 802, all under
output/pdf/, plus the two resources in each; nothing added or removed. I checked all 829 diffs line by line — allowing only the old bare<div class="p …">, the new one with the attributes, the stylesheet link and the script tag, zero files have any other differing line. Output commits19392df/ee3077eon theirmain; a fresh local run is now byte-identical to both.3. The design doc becomes a record
Phase list, day estimates and line counts go — git and the PRs hold that. What survives is what someone changing this next needs: the six decisions and the alternatives they beat, the wire format, the objects a highlight writes. Plus two things that cost more than the estimate said and neither was found by a test:
mix-blend-modeinside an svg composites against that svg's own canvas rather than the page, andselectionchangefires on every character a drag covers.4.
pdf/AGENTS.mdcaught upIt still opened "Goal. Faithful read-only HTML" and had no
pdf_writer/pdf_annotationrows.Interop, finished
Six engines read what we write: ghostscript, PDFium, CoreGraphics, pdf.js (added here — every subtype, quad count, colour, author and contents correct, and it renders the highlight with the text showing through), qpdf's structural check, and our own renderer. Acrobat is the one gap, and the doc says so rather than implying it was covered — it is not installed here.
Verified
1533 C++ tests (1527 pass, 6 pre-existing skips), JNI 56, python 75, wasm 42, the Apple slice compiles;
-Werrorclean.