João Gris234 downloadsHighlight text and add notes to PDFs, saved into the file itself so they work in any reader — with a built-in page to browse, jump to, and copy them all.
Highlight text and take notes in PDFs from inside Obsidian. Everything is written as
standard PDF annotations directly into the .pdf file, so it's portable — highlights
and notes show up in Obsidian's built-in PDF viewer (including inline
![[file.pdf#page=3]] embeds) as well as in Adobe Acrobat, Apple Preview, and any
other PDF reader. The file itself is the source of truth; nothing is stored on the
side.
| Click a highlight | Add a note | Browse everything |
|---|---|---|
![]() |
![]() |
![]() |
/Highlight annotations with a real appearance stream, so every reader renders
them. Size and colors are calibrated to match desktop marker tools./Contents — the standard field — so it appears as the
highlight's comment in Adobe/Preview too. Unicode-safe.#page=N&annotation=ID) or as
plain Markdown.<PDF name> (highlights).md beside the PDF — grouped by page, colour-swatched,
each quote a deep link back to its annotation, with your note beneath it. It's a
sync, not a dump: only the block between the %%study-pdf:begin%% /
%%study-pdf:end%% markers is rewritten, so anything you add around it survives.<PDF name> (flashcards).md — your note is the question, the highlighted text and
a deep link back are the answer. Only highlights that carry a note become cards.
The sync converges: run it again
and the file only changes if your highlights did. Edit a note in the PDF and its
card is rewritten in place; delete a highlight and its card moves to an
## Orphaned section rather than disappearing (put the highlight back and the
card returns to its page). Either way the card's review schedule travels with it
— obsidian-spaced-repetition
stores that inline, on the line after the card, so nothing you've already studied
is reset. Anything you wrote in the file yourself is left where it is.The PDF is fully re-serialized on every save (that's how pdf-lib works), so every write is verified before it touches your file: the output is re-parsed and checked (page count, annotation counts) and the write is aborted loudly if anything looks wrong. Non-highlight annotations — links, form fields, XFA — are covered by round-trip tests against real-world PDFs.
Still: this plugin modifies your PDF files in place. Keep backups of documents you care about, especially the first time you use it on a new kind of PDF.
To keep its popup UI from colliding with Obsidian's built-in annotation popup, the plugin patches one internal viewer method (restored on unload) and reads the viewer's internal PDF.js objects. An Obsidian update can break these integration points; the plugin fails loudly with a clear message rather than misbehaving silently.
npm install
npm run dev # esbuild watch mode
npm run build # typecheck + production build -> main.js
npm test # vitest — unit tests for the pure modules + PDF round-trip tests
npm run lint # eslint-plugin-obsidianmd -- same checks the community-plugin review does
To try it in a vault: build, then symlink main.js, manifest.json, and styles.css
into <vault>/.obsidian/plugins/study-pdf/, then reload Obsidian and enable the
plugin under Community Plugins.
The unit tests only cover the pure modules; anything touching the viewer, popups, or
selection has to be exercised in a running Obsidian. With the symlink in place, the
obsidian CLI makes that loop quick:
npm run build && obsidian plugin:reload id=study-pdf
then obsidian dev:errors, obsidian dev:console, obsidian dev:dom selector=...,
and obsidian dev:screenshot path=... to inspect the result.
Releases are built and published entirely by GitHub Actions
from a pushed version tag (e.g. 0.1.0, no v prefix) — never uploaded from a local
machine. Each release asset (main.js, manifest.json, styles.css) carries a
build provenance attestation,
so anyone can verify it was built from this repository's source:
gh attestation verify <(curl -sL https://github.com/gris/study-pdf/releases/download/<version>/main.js) --repo gris/study-pdf
npm run lint should exit clean. The two intentional exceptions (a version-gated
settings-tab call the linter can't statically verify as safe) are downgraded to
warnings in eslint.config.mjs, with the reasoning next to each call site in
src/settings.ts.
src/annotate.ts — all PDF mutation (add/remove/note/inspect), pure, no Obsidian
imports. Uses @cantoo/pdf-lib (pdf-lib fork with decryption support).src/geometry.ts — pure coordinate mapping: selection rects → PDF QuadPoints,
calibrated against reference marker software.src/pdf-text-extraction.ts — pure text recovery: maps a highlight's quads back
onto a page's text items to reconstruct the quoted text when a highlight has no
quote stored (highlights made in other readers, or by older plugin versions).src/pdf-highlights.ts — reads every highlight out of the viewer's live PDF.js
document (no file re-parse); shared by the list modal and the note export.src/highlight-export.ts — pure Markdown formatting for the exported note and
the modal's copy buttons, including the managed-block merge.src/flashcards.ts — pure card formatting and the converging merge (update in
place, orphan rather than delete) that keeps spaced-repetition schedules intact.src/obsidian-pdf-internals.ts — the ONLY module touching undocumented
Obsidian/PDF.js internals (viewer access, native popup suppression).src/ui/ — icon popup, note editor, reload curtain, highlights list modal.src/settings.ts, src/main.ts — settings tab and plugin wiring.