README file from
GithubE-Ink Sync
Local-first bridge between your reMarkable tablet and Obsidian. Extract PDF highlights as linked markdown -- no cloud, no subscription, no data leaving your network.
What it does
- Syncs your reMarkable's document files to your computer via Syncthing (local network only)
- Extracts text highlights from PDFs into Obsidian markdown notes with backlinks
- Renders pen stroke annotations as PNG images embedded in your notes
- Manages your tablet library from an Obsidian sidebar -- browse, search, archive, delete
Works with reMarkable 1 (512MB RAM) and reMarkable 2. Supports firmware 3.0+ (v6 .rm format) and legacy firmware (v3/v5 format).
Why this plugin needs Python
Most Obsidian plugins are pure JavaScript. This one isn't — and the answer to "why?" is short: the reMarkable's v6 .rm binary format has exactly one mature parser, rmscene, and it's Python. There's no JavaScript port. Similarly, PyMuPDF is the only library that reliably extracts PDF text together with its bounding-box coordinates — which is what makes correlating a highlight rectangle back to the underlying text actually work. pdf.js exposes text but not coordinates cleanly.
The plugin spawns Python only when you trigger sync or extraction; if Python isn't installed, the plugin still loads and surfaces a clear error in the setup wizard rather than silently breaking.
Prerequisites
| Requirement | Version | Why |
|---|---|---|
| Obsidian | 1.5.0+ | Plugin host |
| Python | 3.8+ | Highlight extraction and page rendering (see above) |
| rmscene | latest | Parses v6 .rm annotation files |
| PyMuPDF | latest | Extracts text from PDF pages, renders page images |
| Syncthing | any | Syncs files between tablet and computer (local network) |
Install Python dependencies
pip install rmscene PyMuPDF
Windows users: disable Python Store aliases
Windows ships with python.exe and python3.exe aliases that open the Microsoft Store instead of running Python. The plugin will silently fail to extract highlights if these are active.
Fix: Settings > Apps > Advanced app settings > App execution aliases -- turn off python.exe and python3.exe.
Installation
From source (development)
git clone <repo-url>
cd remarkable-obsidian
npm install
npm run build
Then install the plugin into your vault using the install script:
# Interactive (prompts for vault path)
npm run install-plugin
# With vault path argument
npm run install-plugin -- /path/to/your/vault
# Or via environment variable
OBSIDIAN_VAULT=/path/to/your/vault npm run install-plugin
The script copies main.js, manifest.json, styles.css, extraction/, and templates/ into <vault>/.obsidian/plugins/eink-sync/. Re-run it after each npm run build to update.
Alternative: symlink for live development
If you prefer changes to appear immediately without re-running the install script:
# Mac/Linux
ln -s "$(pwd)" "<vault>/.obsidian/plugins/eink-sync"
# Windows (PowerShell, run as Admin)
New-Item -ItemType Junction -Path "<vault>\.obsidian\plugins\eink-sync" -Target "$(Get-Location)"
First-time setup
- Enable the plugin in Obsidian settings
- A setup wizard opens automatically -- follow the steps to configure:
- SSH connection to your tablet (USB or WiFi)
- Syncthing sync folder
- Output folder for highlight notes
- The plugin creates three folders in your vault:
| Folder | Default | Purpose |
|---|---|---|
| Sync | reMarkable/Sync |
Raw xochitl files synced from the tablet |
| Highlights | reMarkable/Highlights |
Extracted markdown notes + drawing PNGs |
| Archive | reMarkable/Archive |
Documents archived off the tablet |
Usage
Extracting highlights
Click the reMarkable icon in the sidebar to open the library view, then click the refresh button (top-right). This:
- Asks Syncthing to check for new files from the tablet
- Waits for the sync to settle
- Runs the Python extraction pipeline
- Updates your highlight notes
You can also use the command palette: E-Ink Sync: Extract highlights.
What gets extracted
| Annotation type | Extracted as |
|---|---|
| Text-selection highlights (long-press to select text) | Blockquoted text with PDF page link |
| Pen strokes on PDFs (drawing/writing over pages) | PNG image of the annotated page |
| Notebook pages (Quick sheets, etc.) | PNG image of each page with strokes |
Note: Text-selection highlights produce the best results -- you get the actual text as searchable markdown. Pen strokes (including the pen-style highlighter) are rendered as images only. The extractor does not currently OCR pen-stroke annotations to recover text.
Output format
Each document produces a markdown file like:
---
title: "Paper Title"
source_pdf: "[[uuid.pdf]]"
source_type: pdf
highlight_count: 3
remarkable_uuid: abc-123
---
<!-- eink-sync:start -->
### Page 5
> The highlighted text passage
> -- [[uuid.pdf#page=5|Page 5]]
![[Paper Title_p5.png|500]]
<!-- eink-sync:end -->
Everything between the <!-- eink-sync:start/end --> markers is managed by the plugin. Content outside the markers (your own notes) is preserved across re-extractions. Notes written by the pre-rename plugin (which used <!-- remarkable-bridge:start/end -->) are still recognised and migrated to the new markers on the next extraction.
How sync works
reMarkable tablet
|
| Syncthing (local WiFi / USB)
v
Vault/reMarkable/Sync/ <-- raw xochitl files (UUIDs)
|
| Python extraction pipeline
v
Vault/reMarkable/Highlights/ <-- readable markdown + PNGs
All data stays on your local network. The plugin never contacts reMarkable Cloud or any external server.
Incremental extraction
By default, the plugin only processes documents modified since the last extraction (based on the lastModified timestamp in each document's .metadata file). To force a full re-extraction of all documents, use the Sync button in the library view or the command palette.
Important: folder configuration
Changing folder paths
When you change any folder path in settings, a migration dialog appears:
- Shows how many files exist at the old path
- Offers three options: Move to new location, Keep in old location, or Cancel
- If you choose Move, all files (including subdirectories like drawings/) are relocated
- If the sync folder changes, the extraction timestamp is automatically reset so all documents in the new folder are re-processed
The file watcher and Syncthing configuration are updated automatically.
Archive folder location
The archive folder must not be inside the sync folder. If it is, Syncthing will sync archived documents back to the tablet, defeating the purpose of archiving.
Multiple tablets / sync sources
The plugin supports multiple sync sources -- each with its own sync folder, Syncthing folder ID, and independent extraction timestamp.
To add a second tablet:
- Open plugin settings and scroll to Sync Sources
- Click Add source and enter a label (e.g., "Partner's rM2"), sync folder path, and Syncthing folder ID
- Each source extracts independently -- extracting one does not affect the other
- Highlight notes include a
sourcefield in frontmatter so you can tell which tablet produced them - The library view has a source filter dropdown (hidden when only one source exists)
Existing single-source setups are migrated automatically to a "Default" source on upgrade -- no reconfiguration needed.
Each source can optionally specify its own highlights subfolder to keep notes organized per tablet.
Multiple computers (same vault via Syncthing)
If you sync your Obsidian vault between computers via Syncthing, the plugin is designed to avoid conflicts:
- Extraction timestamps are device-scoped -- each computer writes its own timestamp keyed by hostname, so two machines extracting at different times don't conflict on
data.json - Highlight note output is deterministic --
date_highlighteduses the document's modification date from the tablet (same on both machines), not the extraction date - No plugin files in synced folders -- claim files and internal state are stored in
.obsidian/plugins/(not synced by default), not in the sync/highlights folders
If you already have sync-conflict files from before this fix, you can safely delete them:
find /path/to/vault -name "*sync-conflict*" -delete
Subvaults and vault isolation
All folder paths are resolved relative to the vault root. The plugin detects when multiple vault instances use overlapping folders:
- On load, the plugin checks for folder collisions with other vault instances
- If another vault is using the same sync or highlights folder, a warning banner appears in settings
- Warnings are dismissible and persisted -- they never block actions
- If a configured folder resolves to a path outside the vault root, a warning is shown
When you disable and re-enable the plugin (e.g., in a subvault), it re-reads settings from that vault's data.json, re-creates folders if needed, and restarts watchers. Each vault's state is fully independent.
Troubleshooting
"No new highlights found" but I added highlights
- Check Syncthing -- open
http://127.0.0.1:8384and verify the folder is "Up to Date" - Check Python -- run
python --version(orpython3 --version) in a terminal. If it opens the Microsoft Store, disable the aliases (see Prerequisites above) - Check annotation type -- text-selection highlights (long-press to select) produce text. Pen/highlighter strokes produce only images. If you only drew with a pen, the extraction may report 0 highlights but should still generate page drawings.
- Force full extraction -- incremental mode may skip documents if their
lastModifiedtimestamp hasn't changed. Use the Sync button in the library or the command palette to force a full run.
Documents not appearing in library
- Documents must have a
.metadataand.contentfile in the sync folder - Documents uploaded to the tablet but never opened will have
pageCount: 0and empty page lists -- they appear in the library but cannot produce highlights until opened on the tablet - Check that the sync folder path in settings matches where Syncthing actually writes files
Old highlights still showing after deletion
When you delete highlights on the tablet and re-sync, the extraction finds 0 highlights for that document. If the document also has no pen stroke drawings, the pipeline skips updating the file (it treats "nothing to show" as "nothing to do"). The old highlight note remains unchanged.
Workaround: manually delete the stale highlight note, or enable overwriteExisting in settings to force a fresh write every time.
Development
npm install # install dependencies
npm run dev # watch mode (rebuilds on file change)
npm run build # production build
npm test # run test suite
npm run test:watch # run tests in watch mode
npm run lint # lint TypeScript source
Project structure
src/
plugin/ # Obsidian plugin lifecycle, settings, library view
pipeline/ # Extraction pipeline, document discovery, markdown rendering
ssh/ # SSH client for tablet communication
device/ # Firmware detection, device management
sync/ # Syncthing configuration and sync orchestration
utils/ # Logger, shared utilities
extraction/ # Python scripts (called via child_process)
extract.py # Main entry point for highlight extraction
metadata_parser.py # xochitl .metadata/.content file parsing
highlight_extractor.py # GlyphRange and legacy highlight parsing
render_pages.py # PNG rendering of annotated pages
legacy_rm_parser.py # v3/v5 .rm file format parser
templates/ # Handlebars template for highlight notes
Architecture
The plugin has three layers:
- Device layer (
ssh/,device/) -- SSH connection, firmware detection, Syncthing installation on the tablet - Sync layer (
sync/) -- Syncthing configuration, file sync orchestration - Pipeline layer (
pipeline/,extraction/) -- Document discovery, Python-based highlight extraction, markdown rendering
The pipeline delegates to Python for two tasks: (1) parsing .rm files via rmscene and extracting highlight text via PyMuPDF, and (2) rendering annotated pages as PNGs. Communication is one-directional: TypeScript spawns Python with CLI arguments, Python returns JSON on stdout.
Permissions & data access
This plugin requests broader access than a typical Obsidian plugin because it bridges your tablet (an external device on your local network) to your vault. Here's exactly what it does and why:
| Capability | Why it's needed |
|---|---|
| Filesystem access outside the vault | Reads .rm/.metadata/.content files from the Syncthing sync folder (which lives outside your vault, since Syncthing manages it). Writes rendered PNGs and extracted markdown into your vault. |
Shell execution (child_process) |
Spawns Python for highlight extraction (rmscene + PyMuPDF) and SSH for one-time tablet setup. Both are essential -- there's no JavaScript equivalent for parsing reMarkable's v6 .rm binary format. |
Hostname read (os.hostname) |
Scopes the "last extracted at" timestamp per machine, so if you sync your vault between two computers via Syncthing they don't fight over data.json and produce sync-conflict files. |
| Vault file enumeration | Used by the "Send document to reMarkable" command to find PDFs/EPUBs in your vault. |
The plugin never makes external network requests. All sync happens over your local network via Syncthing or SSH to your tablet. No data goes to reMarkable Cloud, no telemetry, no analytics, no third-party servers.
Known limitations
- Pen-stroke text extraction: Highlights drawn with a pen or the pen-style highlighter tool are captured as images, not text. Only text-selection highlights (GlyphRange) produce searchable text.
- Stale notes on highlight deletion: Documents with 0 highlights and 0 drawings are skipped, leaving old notes unchanged (see Troubleshooting above).
- Incremental extraction uses document timestamp: The
lastModifiedfield in.metadatamay not update when annotations change, causing incremental mode to miss updates. Use full re-extraction if highlights seem stuck. - Same-name documents across sources: If two tablets have a document with the same visible name, their highlight notes will overwrite each other (unless per-source subfolders are configured).
License
MIT