npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

remarkable-rm

v1.1.0

Published

Parse and render reMarkable tablet .rm files to SVG

Readme

remarkable-rm

MIT License npm

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 .rm files 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-rm

Or 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 only

Browser 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-serif font, 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, and fontName settings in the .content file, 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 textScale setting from .content files 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.