6.9 KiB
PDF annotations and visual excerpts (agy gap 1, roadmap 32)
Research (2026-10-21; agy claims checked)
Heptabase documents PDF annotation; its deep links include PDF pages and highlights. Obsidian 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/<sha256-of-vault-relative-path>.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
- Multi-line highlight, comment and rectangle survive reopen; original PDF SHA-256 unchanged; source replacement visibly stale.
- Text copy on supported backend matches fixture text; raster-only backend reports unavailable, never invents text.
- 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.
- Read-only/invalid geometry/stale revision/corrupt JSON reject without any writes; missing renderer produces recoverable error.
- Engine tests cover rename, Unicode path, traversal/symlink rejection, duplicate IDs, malformed fragments, caps and timeout/cancel; existing page links unchanged.
- 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 -bboxplus 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/annotationextension and stale-anchor behavior? No claim of Obsidian rect interoperability.
Feature ideas (researched, ranked)
- PDF highlight cards on canvas, informed by Heptabase PDF annotation.
- Cross-device highlight deep links, informed by Heptabase deep links.
- Page-only fallback for other apps, preserving Obsidian PDF embed syntax.