This repository has no description
JavaScript 95%
5%

README.md

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 .rm files at the block/tree level
  • bin/notable — a CLI tool for converting .rm files 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:

  • rmscene by Rick Lupton — the core .rm v6 parser
  • rmc — the CLI converter

The binary format was reverse-engineered with help from ddvk's reader.