Skip to content

feat(pdf): annotate a pdf from json - #848

Merged
andiwand merged 2 commits into
mainfrom
feat/pdf-annotate-api
Sep 6, 2026
Merged

feat(pdf): annotate a pdf from json#848
andiwand merged 2 commits into
mainfrom
feat/pdf-annotate-api

Conversation

@andiwand

@andiwand andiwand commented Sep 6, 2026

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Phase 4 of docs/design/pdf-annotation.md. Stacked on #847. Second of four; the browser layer and bindings follow.

void PdfFile::annotate(std::string_view annotations, std::ostream &out,
                       const Logger & = Logger::null()) const;

Takes the JSON payload the browser-side annotator will produce and writes the annotated file. Per decision 1, it hangs off PdfFile, not Document — pdf has none — and addresses annotations by page index and user-space geometry, never by element id. That is what keeps this feature independent of the deferred editing.md work.

{"version": 1, "annotations": [
  {"page": 0, "type": "highlight",
   "quads": [[72, 700, 300, 700, 72, 688, 300, 688]],
   "color": [1, 0.9, 0.2], "opacity": 1, "author": "", "contents": ""},
  {"page": 1, "type": "ink", "strokes": [[100, 500, 130, 540]],
   "width": 2, "color": [0.9, 0.1, 0.1]}
]}

Refused rather than half-understood

Fail-fast, per the repo convention — each with its own test:

  • a version that is not 1 (the drift guard against a newer frontend)
  • text that is not JSON
  • a type we do not write
  • a page that is not there
  • a quad that is not eight coordinates
  • a missing required member (color, quads, type, page)
  • an encrypted file, and one whose xref was recovered (decisions 2 and 6)

An empty payload is legal and writes a no-op update, which falls out of the writer rather than being special-cased.

Annotations landing on the same page share a single page rewrite rather than one per annotation.

Capability

FileTypeCapabilities gains annotate, set on the pdf row. The existing edit/save bits are documented against Document, which pdf has none of, so reusing either would have been a lie — and the conformance test's edit ⇒ save invariant would not hold. annotate gets the same treatment the others get: a type nothing can open cannot claim it.

Bindings for it come with the rest in the bindings PR.

Verified

Seven tests over style-various-1.pdf through the real public entry point: a highlight lands with its appearance resolved, all five types land on the right pages, author/contents/opacity reach the file (with a ( in contents surviving escaping), the empty payload, every refusal, and the encrypted file.

448 tests pass across the pdf, writer, odr, file and html suites; -Werror clean. clang-tidy on the touched file reports only two pre-existing findings (file_meta's exception-escape at an untouched line, and the known decrypt shadowing false positive).

Changelog entry added.

Base automatically changed from feat/pdf-annotation-writers to main September 6, 2026 15:37
andiwand and others added 2 commits September 6, 2026 17:38
`PdfFile::annotate` takes the payload our browser-side annotator will produce —
highlight, underline, strike-out, squiggly and ink, in pdf user space — and
writes the annotated file. It hangs off the file rather than off `Document`,
which pdf does not have, and addresses annotations by page index and geometry
rather than by element id.

A `version` that is not 1 is refused rather than half-understood, as is an
unknown type, a page that is not there, a quad that is not eight coordinates,
and a missing required member. Annotations landing on one page share a single
page rewrite.

`FileTypeCapabilities` gains `annotate`, and the conformance test holds it to
the same rule as the rest: a type nothing can open cannot claim it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018e3PEzyU2oAFSzsEoWsSmz
`PdfFile::annotate` promises `std::invalid_argument` for a malformed payload,
but nlohmann reports a member of the wrong type — `"page": "0"`, a colour of
strings, a string `version` — as a `json::type_error`, which escaped the
public entry point unconverted. One catch now covers the whole read, so the
parse error and the type error arrive the same way, and three cases pin it.

Keys the per-page bundle by page index rather than by `Page *`, so the page
rewrites run in page order rather than in allocation order, and puts the new
includes in the order the file already had.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147n68S7LNAv9KynLaGgXN4
@andiwand
andiwand force-pushed the feat/pdf-annotate-api branch from 28a9001 to 16a4509 Compare September 6, 2026 15:45
@andiwand
andiwand merged commit c4c4037 into main Sep 6, 2026
25 checks passed
@andiwand
andiwand deleted the feat/pdf-annotate-api branch September 6, 2026 15:48
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.

1 participant