# PDF annotations and visual excerpts (agy gap 1, roadmap 32) ## Research (2026-10-21; agy claims checked) [Heptabase](https://heptabase.com/) documents PDF annotation; its [deep links](https://support.heptabase.com/en/articles/11176386-use-deep-links) include PDF pages and highlights. [Obsidian embeds](https://help.obsidian.md/embeds) document page links. These verify demand, not the agy `rect=` syntax: that syntax below is a NotesCraft extension. Logseq/SiYuan feature equivalence was not established and is dropped. The suggested new parser crate is not justified: `crates/pdf/src/lib.rs` is an exporter, not an arbitrary-PDF reader. `crates/engine/src/vault/pdf_preview.rs` already rasterizes via PDFKit/JXA on macOS and `pdftoppm` elsewhere; UI `crates/ui-egui/src/views/media_view/pdf.rs` has paging/zoom but no text geometry or page count. Its initial comment understates current macOS support; inspect implementation. ## Problem and goal Annotate an existing PDF without rewriting it; select a text passage or page rectangle, attach a comment, copy a linked quote or visual excerpt into a note, and navigate back to exact page/region. Reuse existing PDF crate for pure geometry/sidecar types, engine for IO, existing PDF tab for interaction. No new crate. ## Design decisions ### D1 Portable sidecar and anchors Version 1 sidecar at `.notescraft/pdf-annotations/.json`, via `Vault::write_private` only; original PDF byte-identical. Shape `{version:1,path,source_sha256,annotations:[{id,page,kind,rects,quote,comment,colour}]}`. `page` is 1-based; rectangles `[x,y,width,height]` normalized to displayed crop box, top-left origin after page rotation. `kind` is `highlight` (one or more line rectangles) or `excerpt` (one rectangle). Colour is a theme-independent named token, never arbitrary UI colour. IDs are opaque stable local IDs, unique within document; edits retain IDs. No fresh randomness dependency required for sequential IDs under serialized engine writes. Caps: 4 MiB sidecar, 10,000 annotations, 256 rects per annotation, quote/comment each 16 KiB; finite coordinates, positive area wholly in unit square; page 1..10,000. Unknown version, duplicate ID, invalid kind/geometry return Err before mutation. Corrupt sidecar remains byte-identical. Store source SHA-256 and mark anchors stale on content replacement; never silently remap. Renames migrate path-keyed sidecar atomically with existing rename semantics; failure preserves original association and reports error. ### D2 Link and excerpt contract `[[paper.pdf#page=4&annotation=pa_1]]` is stable annotation navigation. `![[paper.pdf#page=4&rect=0.1,0.2,0.3,0.1]]` is derived raster crop, not a mutation to PDF. Plain `#page=4` remains supported. Parse only these recognized keys, reject duplicate/malformed/out-of-range values; do not reinterpret as heading. Stable-ID navigation requires sidecar; standalone rect navigation remains usable without it. Copy quote emits Markdown blockquote plus stable-ID link; excerpt command returns link and bounded PNG via attachment helper only when explicitly saving excerpt. Cache crop by content hash/page/geometry/render version, no source mutation. Missing/stale anchor shows diagnostic and opens page without claiming exact match. ### D3 Engine and backend capabilities Commands `pdf.annotations.list {path}`, `pdf.annotations.upsert {path,annotation,expected_revision}`, `pdf.annotations.delete {path,id,expected_revision}`, `pdf.excerpt {path,page,rect,save?}`, `pdf.page_info {path,page}`. Return revision/hash and explicit stale/capability status. Mutation rejects read-only; optimistic revision avoids lost update. `pdf.excerpt` defaults `save=false`; classify entire command WRITE in assistant gate because `save=true` writes an attachment (including when default is used); read-only permits only `save=false`. `list`/`page_info` READ; upsert/delete WRITE. Register in existing viewer group, AI read/write gate, CORE_IDS tests. `pdf.page_info` returns rotated crop geometry and text runs with normalized rectangles only when backend supports them. Existing PNG renderer cannot supply text runs: add bounded native-tool extraction adapter only after Q1 approval; unsupported backend disables text selection with explanation, retains rectangle tools. Never claim raster OCR is original PDF text. Worker performs rendering/extraction; timeout 15 seconds, 200 MiB source cap, 16 MiB PNG cap match existing preview; text <= 2 MiB/page, <= 100,000 runs, cancellation on tab close/change. Original text highlight capability must pass platform fixture gate before feature marked complete. ### D4 UI and integration PDF toolbar: Select text / Rectangle / Annotate; keyboard access, selected-region highlight across zoom and scroll, comment editor, delete confirmation, copy quote/excerpt. Engine mutations queued after frame; no IO during paint. Embed renderer draws clipped page texture and click opens page/annotation through shared link dispatch; no arbitrary path access. i18n tables/English keys use existing media-view pattern. No PDF editing/export annotation import, freehand drawing, or OCR in this feature. ## Acceptance criteria 1. Multi-line highlight, comment and rectangle survive reopen; original PDF SHA-256 unchanged; source replacement visibly stale. 2. Text copy on supported backend matches fixture text; raster-only backend reports unavailable, never invents text. 3. Stable ID opens page and overlays exact region after zoom/scroll; crop round trip works with 90/180/270-degree pages and nonzero crop-box origin. 4. Read-only/invalid geometry/stale revision/corrupt JSON reject without any writes; missing renderer produces recoverable error. 5. Engine tests cover rename, Unicode path, traversal/symlink rejection, duplicate IDs, malformed fragments, caps and timeout/cancel; existing page links unchanged. 6. egui tests and read screenshots cover annotation, crop embed, keyboard navigation, Mocha/Latte and scale 2; no per-frame worker restart. ## Open owner questions - Q1: Approve native PDFKit text extraction and installed Poppler tools (`pdftotext -bbox` plus geometry inspection) as platform capability contract? Or fund/approve a bundled PDF parser dependency and license review? No dependency added without answer; arbitrary-PDF text highlighting blocked until adapter proven. - Q2: Sync annotation sidecars as user data? Proposal yes, derived raster caches no; revision conflicts remain visible, no last-write overwrite. - Q3: Accept NotesCraft normalized `rect`/`annotation` extension and stale-anchor behavior? No claim of Obsidian rect interoperability. ## Feature ideas (researched, ranked) 1. PDF highlight cards on canvas, informed by [Heptabase PDF annotation](https://heptabase.com/). 2. Cross-device highlight deep links, informed by [Heptabase deep links](https://support.heptabase.com/en/articles/11176386-use-deep-links). 3. Page-only fallback for other apps, preserving [Obsidian PDF embed syntax](https://help.obsidian.md/embeds).