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/notable
Or globally to use the CLI:
npm install -g @robin.berjon/notable
CLI 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 help
Available FORMAT values: rm, markdown, svg, pdf, blocks, blocks-data.
Examples #
Convert a .rm file to Markdown (printed to stdout):
notable -t markdown note.rm
Convert a .rm file to SVG, writing to a file:
notable -t svg -o note.svg note.rm
Guess the output format from the file extension:
notable note.rm -o note.pdf
Convert a Markdown file to a .rm file:
notable -t rm note.md -o note.rm
Dump the internal block structure of a .rm file:
notable -t blocks note.rm
notable -t blocks-data note.rm # omit point/byte data
Format 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 file
The 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.sceneInfo
Walking 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.rm
Typed 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 test
Tests 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.