@robin.berjon/notable
v1.0.1
Published
Parse and convert reMarkable .rm files
Downloads
247
Readme
notable
Parse and convert reMarkable tablet .rm files (version 6, software ≥ 3).
notable ships as two things:
lib/— a Node.js library for reading and writing.rmfiles at the block/tree levelbin/notable— a CLI tool for converting.rmfiles to Markdown, SVG, and PDF
Installation
npm install @robin.berjon/notableOr globally to use the CLI:
npm install -g @robin.berjon/notableCLI Usage
notable [options] [input...]
Options:
-f, --from <FORMAT> Format to convert from (default: guess from filename)
-t, --to <FORMAT> Format to convert to (default: guess from filename)
-o, --output <FILE> Output filename (default: stdout)
-v, --verbose Increase verbosity
-V, --version Show version
-h, --help Show helpAvailable FORMAT values: rm, markdown, svg, pdf, blocks, blocks-data.
Examples
Convert a .rm file to Markdown (printed to stdout):
notable -t markdown note.rmConvert a .rm file to SVG, writing to a file:
notable -t svg -o note.svg note.rmGuess the output format from the file extension:
notable note.rm -o note.pdfConvert a Markdown file to a .rm file:
notable -t rm note.md -o note.rmDump the internal block structure of a .rm file:
notable -t blocks note.rm
notable -t blocks-data note.rm # omit point/byte dataFormat notes
| Format | Read | Write | Notes |
|--------|------|-------|-------|
| rm | ✓ | ✓ | reMarkable binary file |
| markdown | ✓ | ✓ | Text + highlighted passages |
| svg | — | ✓ | Vector rendering of strokes |
| pdf | — | ✓ | Pure JS, no external tools; multiple inputs become pages |
| blocks | — | — | Debug dump (JSON) |
| blocks-data | — | — | Debug dump without point data |
Library API
import {
readBlocks, writeBlocks, readTree,
simpleTextDocument,
rmToMarkdown, rmToSvg, rmToPdf, rmsToPdf,
} from '@robin.berjon/notable';Converters
The CLI's converters are part of the library:
import { readFileSync, writeFileSync } from 'node:fs';
import { rmToMarkdown, rmToSvg, rmsToPdf } from '@robin.berjon/notable';
const page = readFileSync('page.rm');
const markdown = rmToMarkdown(page); // string
const svg = rmToSvg(page); // string
// one PDF page per .rm buffer, all pure JS
const pdf = rmsToPdf([page1, page2, page3]); // Buffer
writeFileSync('notebook.pdf', pdf);rmToPdf(data) is the single-page shorthand; treesToPdf(trees) and
treeToSvg(tree) accept already-parsed scene trees if you need to read the
data once and export it several ways.
Reading blocks
import { readFileSync } from 'node:fs';
import { readBlocks } from '@robin.berjon/notable';
const data = readFileSync('note.rm');
for (const block of readBlocks(data)) {
console.log(block.constructor.name);
}readBlocks(buffer) accepts a Buffer and returns a generator of Block objects.
Writing blocks
import { writeBlocks } from '@robin.berjon/notable';
const buf = writeBlocks(blocks, { version: '3.2.2' });
// buf is a Buffer ready to write to a .rm fileThe options object supports a version string to control which format features are emitted.
Reading the scene tree
import { readTree } from '@robin.berjon/notable';
const tree = readTree(data);
// tree.root — root Group containing layers
// tree.rootText — Text block (keyboard text), or null
// tree.sceneInfoWalking strokes
import { Line } from '@robin.berjon/notable';
for (const item of tree.walk()) {
if (item instanceof Line) {
console.log(`Stroke with ${item.points.length} points`);
}
}Creating a text document
import { simpleTextDocument, writeBlocks } from '@robin.berjon/notable';
const buf = writeBlocks(simpleTextDocument('Hello reMarkable!'));Block types
| Class | Block type | Description |
|-------|-----------|-------------|
| AuthorIdsBlock | 0x09 | Maps author IDs to UUIDs |
| MigrationInfoBlock | 0x00 | Migration metadata |
| PageInfoBlock | 0x0A | Page statistics |
| SceneTreeBlock | 0x01 | Tree structure entry |
| TreeNodeBlock | 0x02 | Layer / group metadata |
| SceneGroupItemBlock | 0x04 | Group child reference |
| SceneLineItemBlock | 0x05 | Stroke (drawn line) |
| SceneGlyphItemBlock | 0x03 | Highlighted text range |
| SceneTextItemBlock | 0x06 | Text item in layer |
| SceneTombstoneItemBlock | 0x08 | Deleted item marker |
| RootTextBlock | 0x07 | Keyboard-entered text |
| SceneInfo | 0x0D | Canvas / paper info |
| UnreadableBlock | — | Unrecognised or corrupt block |
Scene items
import { PenColor, Pen, ParagraphStyle } from '@robin.berjon/notable';
// PenColor values: BLACK(0), GRAY(1), WHITE(2), YELLOW(3), GREEN(4),
// PINK(5), BLUE(6), RED(7), GRAY_OVERLAP(8), HIGHLIGHT(9), ...
// Pen values: BALLPOINT_1(2), BALLPOINT_2(15), FINELINER_1(4), FINELINER_2(17),
// HIGHLIGHTER_1(5), HIGHLIGHTER_2(18), PENCIL_1(1), PENCIL_2(14), ...
// ParagraphStyle values: PLAIN(1), HEADING(2), BOLD(3), BULLET(4), BULLET2(5), ...Text extraction
import { readTree, TextDocument } from '@robin.berjon/notable';
const tree = readTree(data);
if (tree.rootText) {
const doc = TextDocument.fromSceneItem(tree.rootText);
for (const paragraph of doc.contents) {
console.log(paragraph.style.value, paragraph.toString());
}
}CRDT sequence
CrdtSequence is an ordered sequence backed by a topological sort of CRDT items:
import { CrdtSequence, CrdtSequenceItem, CrdtId } from '@robin.berjon/notable';
const seq = new CrdtSequence([
new CrdtSequenceItem(new CrdtId(1, 1), new CrdtId(0, 0), new CrdtId(0, 0), 0, 'A'),
new CrdtSequenceItem(new CrdtId(1, 2), new CrdtId(1, 1), new CrdtId(0, 0), 0, 'B'),
]);
seq.values(); // ['A', 'B']SVG / PDF conversion notes
SVG conversion renders strokes with pressure, speed, and direction data but text rendering is basic. If text boxes are present:
- Multi-line text may render on a single line
- Stroke positions relative to text may be approximate
PDF conversion is pure JS with no external dependencies. Passing several .rm
files produces a single PDF with one page per input:
notable -t pdf -o notebook.pdf page1.rm page2.rm page3.rmTyped text uses the standard base-14 PDF fonts (Helvetica/Times, WinAnsi
encoding) when possible. When the text contains characters outside WinAnsi —
Cyrillic, Greek, extended Latin — the bundled Noto Sans/Noto Serif fonts (the
same families the reMarkable itself renders typed text with, see fonts/) are
embedded automatically, with a ToUnicode CMap so text extraction and search
keep working. CJK is not covered (the Noto CJK fonts are ~20 MB per face).
Running tests
npm testTests cover binary round-trips of the test .rm files and the full test
notebook, block serialisation, CRDT sequence ordering, text extraction, inline
formatting, pen models, and SVG/PDF export (including embedded-font PDFs).
Acknowledgements
This is a JavaScript port of:
The binary format was reverse-engineered with help from ddvk's reader.
