remarkable-rm
v1.1.0
Published
Parse and render reMarkable tablet .rm files to SVG
Maintainers
Readme
remarkable-rm
A pure TypeScript library for parsing and rendering reMarkable tablet .rm files to SVG. Works in Node.js and browsers with zero runtime dependencies.
Need pixel-perfect output? If the reMarkable tablet is USB-connected, you can download rendered PDFs directly from its built-in web API for exact fidelity. This library is for offline rendering, browser-side display, and structured data extraction from
.rmfiles without needing the tablet.
Features
- Parses reMarkable v5 and v6 binary formats
- Renders to SVG with realistic pen stroke simulation (pressure, tilt, speed)
- Supports all pen types: ballpoint, fineliner, pencil, marker, highlighter, calligraphy, shader, brush, mechanical pencil, eraser
- Decodes per-stroke RGBA colours for highlights and shaders (green, cyan, pink, custom colours); a first among open-source reMarkable parsers
- Text rendering with paragraph styles: plain, heading, bold, bullet, checkbox, numbered list
- Inline text formatting: bold spans, strikethrough on checked items
- Highlight and glyph range support
- CRDT sequence reconstruction for v6 format
- Zero dependencies at runtime: uses only
Uint8Array,DataView,TextDecoder - Tree-shakeable ESM, CJS, and IIFE builds
Install
npm install remarkable-rmOr use directly from a CDN:
<script src="https://cdn.jsdelivr.net/npm/remarkable-rm/dist/index.global.js"></script>
<script>
const { parse, renderToSvg } = RemarkableRM;
</script>Usage
SVG output (default)
import { parse, renderToSvg } from 'remarkable-rm';
import { readFileSync } from 'node:fs';
const data = new Uint8Array(readFileSync('page.rm'));
// SVG with system font
const svg = renderToSvg(data);
// SVG with embedded Noto Sans (self-contained, +400KB)
const svgWithFont = renderToSvg(data, { embedFont: true });PNG and JPEG output (with bundled Noto Sans)
import { renderToPng, renderToJpeg } from 'remarkable-rm';
import { readFileSync, writeFileSync } from 'node:fs';
const data = new Uint8Array(readFileSync('page.rm'));
// PNG: lossless, larger files
const png = await renderToPng(data, { width: 1404 });
writeFileSync('page.png', png);
// JPEG: lossy, ~30% smaller (quality 0-100, default 85)
const jpeg = await renderToJpeg(data, { width: 1404, quality: 85 });
writeFileSync('page.jpg', jpeg);Rasterised output uses the bundled Noto Sans font from the reMarkable tablet for accurate text. Install the required peer dependencies:
npm install @resvg/resvg-js # required for PNG and JPEG
npm install sharp # required for JPEG onlyBrowser usage
const response = await fetch('/path/to/page.rm');
const data = new Uint8Array(await response.arrayBuffer());
const svg = renderToSvg(data);
document.getElementById('container').innerHTML = svg;Parse only (no rendering)
import { parse } from 'remarkable-rm';
const doc = parse(data);
// Access stroke data for analysis
for (const page of doc.pages) {
for (const layer of page.layers) {
for (const item of layer.items) {
if (item.type === 'stroke') {
console.log(item.pen, item.penColour, item.points.length);
}
}
}
}API
parse(data: Uint8Array): RmDocument
Parses a .rm file and returns a structured document. Automatically detects v5 or v6 format.
renderToSvg(input: Uint8Array | RmDocument, options?: RenderOptions): string
Renders to an SVG string. Pass { embedFont: true } to embed the bundled Noto Sans font as base64, making the SVG self-contained.
renderToPng(input: Uint8Array | RmDocument, options?: RenderOptions): Promise<Uint8Array>
Renders to PNG using the bundled Noto Sans font for accurate text. Requires @resvg/resvg-js. Options: width (default 1404), background (default 'white').
renderToJpeg(input: Uint8Array | RmDocument, options?: RenderOptions & { quality?: number }): Promise<Uint8Array>
Renders to JPEG. ~30% smaller than PNG. Requires @resvg/resvg-js and sharp. Options: quality (0-100, default 85), width, background.
Types
interface RmDocument {
version: 5 | 6;
pages: RmPage[];
}
interface RmStroke {
type: 'stroke';
pen: PenType; // 'ballpoint' | 'fineliner' | 'pencil' | ...
colour: RmColour; // { r, g, b, a? } resolved RGB with optional alpha
penColour: PenColour; // 'black' | 'blue' | 'red' | ...
thicknessScale: number;
points: RmPoint[];
}
interface RmPoint {
x: number;
y: number;
pressure: number;
speed: number;
direction: number;
width: number;
}See src/types.ts for the complete type definitions.
Format Support
| Format | Status | Notes | |--------|--------|-------| | v6 (firmware 3.x+) | Supported | Tagged-block protocol with CRDT semantics | | v5 (legacy) | Supported | Simpler binary format from earlier firmware | | v3/v4 | Not supported | Extremely rare |
See docs/rm-v6-format.md for a comprehensive binary format specification including original discoveries from this project.
Rendering Comparison
A sample page rendered three ways. Click thumbnails for full size.
The .rm file is included at examples/sample-page.rm for testing.
RGBA Colour Discovery
Prior to this library, all open-source reMarkable parsers (including rmscene) rendered every highlight stroke as yellow. The v6 binary format stores the base colour as an enum value (colour ID 9 = HIGHLIGHT for all highlight tools), with the actual RGB colour in optional tagged fields appended after the standard block content.
We reverse-engineered these fields by examining unread bytes flagged by the Python parser's "Some data has not been read" warning:
Stroke colour override (SceneLineItemBlock)
After the standard line fields (tool, colour, thickness, points, timestamp, moveId), an optional field at tag index 8, type Byte4 contains the RGBA colour:
Tag: varuint((8 << 4) | 0x4) = 0x84 0x01 (2-byte varuint for index 8, Byte4)
Value: 4 bytes, B, G, R, A (packed as little-endian uint32, BGRA byte order)Examples from a test page:
| Stroke | Bytes (BGRA) | Colour | Description |
|--------|-------|--------|-------------|
| Highlighter | 75 ed ff ff | rgb(255, 237, 117) | Yellow |
| Highlighter | fe ea be ff | rgb(190, 234, 254) | Blue |
| Highlighter | ff 9e f2 ff | rgb(242, 158, 255) | Pink |
| Highlighter | 8c c3 ff ff | rgb(255, 195, 140) | Orange |
| Highlighter | 85 ff ac ff | rgb(172, 255, 133) | Green |
| Shader | 1c 1e 21 40 | rgb(33, 30, 28) a=64 | Dark grey at 25% opacity |
The alpha byte serves dual purpose:
- Alpha = 255: colour is fully defined; the pen's default opacity (0.3 for highlighter) still applies to the SVG stroke rendering
- Alpha < 255: overrides the pen's opacity (e.g. shader at alpha=64 renders at 25%)
Typed-text highlight colour (SceneGlyphItemBlock)
GlyphRange blocks (typed-text highlights) have additional optional fields after the standard content (start, length, colour, text, rectangles):
Tag index 7: type ID - anchor reference (CrdtId)
Tag index 8: type ID - anchor reference (CrdtId)
Tag index 9: type Byte1 - unknown flag
Tag index 10: type Byte4 - RGBA colour (same format as stroke override)Backward compatibility
These fields are appended as new tagged entries after the existing block content. Older parsers that don't read them simply skip the extra bytes during block position checking, so no format version change is needed. The readIntOptional / readIdOptional methods peek at the stream and return null if the expected tag isn't present; older files without colour overrides continue to work with the palette-based colour.
Colour palette fallback
When no RGBA override is present, the base PenColor enum is used:
| ID | Name | RGB | |----|------|-----| | 0 | black | (0, 0, 0) | | 1 | grey | (144, 144, 144) | | 2 | white | (255, 255, 255) | | 3 | yellow | (251, 247, 25) | | 4 | green | (0, 255, 0) | | 5 | pink | (255, 192, 203) | | 6 | blue | (78, 105, 201) | | 7 | red | (179, 62, 57) | | 8 | grey-overlap | (125, 125, 125) | | 9 | highlight | (255, 235, 59) | | 10 | green-2 | (161, 216, 125) | | 11 | cyan | (139, 208, 229) | | 12 | magenta | (183, 130, 205) | | 13 | yellow-2 | (247, 232, 81) |
Known Limitations
- Typed text font: The tablet uses Noto Sans with specific metrics. SVG output uses the system's
sans-seriffont, which will have different character widths and kerning. This means typed text won't be pixel-identical, and text-relative elements (glyph range highlights) may not align perfectly with the rendered characters. - Typed text positioning: Font sizes (9 SVG units) and line heights (78 screen units for content, 70 for empty anchor paragraphs) are calibrated from the tablet's PDF export and visual comparison. The tablet's actual line heights may vary with
textScale,lineHeight, andfontNamesettings in the.contentfile, which are not yet applied. - Typed text highlight rectangles: GlyphRange highlight rectangles are stored with the tablet's absolute coordinates, which don't match our text layout (different font metrics, approximate line heights). We snap the Y position to the matched paragraph, but the X position and width are based on the tablet's font metrics and may not align precisely with the SVG text.
- Text scale: The
textScalesetting from.contentfiles is not yet applied. Documents with non-default text scale may render text at the wrong size. - v3/v4 format: Not supported. These extremely old formats are rarely encountered.
Pixel-Perfect Alternative: USB Web API
If you have the reMarkable tablet connected via USB, you can download pixel-perfect PDF exports directly from the tablet's built-in web API. See docs/remarkable-usb-api.md for full endpoint documentation.
# Download a rendered PDF directly from the tablet
curl -o page.pdf "http://10.11.99.1/download/{document-id}/placeholder"This gives you the tablet's own rendering with correct fonts, colours, and layout. Ideal when you need exact fidelity and have the tablet connected.
Improvements over rmscene
This library builds on the work of rmscene but produces higher-fidelity output:
- RGBA colour decoding: reverse-engineered the optional RGBA colour fields in highlight and shader strokes (tag index 8 for lines, tag index 10 for glyph ranges). Green, cyan, pink, and custom highlight colours render correctly instead of all appearing yellow. See RGBA Colour Discovery for technical details. No known community parser decodes these fields.
- Numbered list support: paragraph style code 10, not present in rmscene (which falls back to plain)
- Correct paragraph style mapping: bullet, checkbox, checkbox-checked, heading, and bold styles resolved from CRDT paragraph start IDs
- Tablet-calibrated layout: font sizes, line heights, and text indentation derived from the tablet's own PDF export via the USB web API
- Bullet and checkbox markers: renders bullet, checked, and unchecked prefixes for styled paragraphs
- Anchor-based group positioning: handwritten stroke groups translated to correct Y positions relative to typed text
- Highlight rectangle alignment: typed-text highlights snapped to rendered paragraph positions
- Checkbox strikethrough: checked checkbox lines render with line-through text decoration (tablet rendering convention, not in rmscene)
- Coloured ballpoint and brush: pen colour blending uses the base colour instead of greyscale (rmscene renders all ballpoint/brush strokes as black)
- 48-bit varuint support: correctly reads large CRDT IDs (top/bottom-of-page anchors) that overflow 32-bit bitwise operations
Acknowledgements
The binary format knowledge and pen simulation algorithms are derived from Rick Lupton's rmscene library (MIT).
Layout constants were derived from the tablet's own PDF export via the USB web API.
Licence
MIT (c) Stewart McSporran (Scratchydisk)
Bundled fonts
This package includes Google Noto Sans font files sourced from the reMarkable tablet. Noto Sans is licensed under the SIL Open Font License, Version 1.1, which permits use, modification, and redistribution. The fonts are used for accurate text rendering matching the tablet's display.
