rmscene-ts
v0.1.1
Published
Read, write, and render reMarkable .rm version 6 files in TypeScript
Maintainers
Readme
rmscene-ts
rmscene-ts reads, writes, and renders reMarkable .rm version 6 scene files in TypeScript.
It is a full rewrite in TypeScript of rmscene 0.8.0
by Rick Lupton, pinned to commit
cf86cf0374ca43a53477dd27c65fe2e70e6b4750.
That Python project worked out the format and remains the behavioral reference: every reader golden in
this repository is generated by running it. What changed in the rewrite is listed under
What this port changes.
The package has no runtime dependencies, accepts and returns Uint8Array, works in Node 20+ and
ES2022 browsers, and ships ESM, CommonJS, and TypeScript declarations. It performs no filesystem,
network, credential, tablet, or cloud operations.
Install
npm install rmscene-tsRead a scene
import { readBlocks, readText, readTree, type ReadWarning } from "rmscene-ts";
const data = new Uint8Array(await file.arrayBuffer());
const warnings: ReadWarning[] = [];
const options = { onWarning: (warning: ReadWarning) => warnings.push(warning) };
const blocks = readBlocks(data, options);
const tree = readTree(data, options);
const document = readText(tree, options);
console.log(tree.sceneInfo?.paperSize, document);
for (const item of tree.walk()) {
if (item.kind === "line") console.log(item.tool, item.color, item.colorRgba, item.points);
}Warnings are delivered only through onWarning; the library does not write to the console. Set
strict: true to throw on malformed known blocks. Forward-compatible trailing bytes and unknown
numeric values remain retained in both modes.
Write .rm bytes
import { readBlocks, writeBlocks } from "rmscene-ts";
const blocks = readBlocks(data);
const unchanged = writeBlocks(blocks);The default writer preserves parsed block versions, optional-field presence, line timestamps,
unknown blocks, incomplete final block headers, extraData, and nested extraValueData. For every included fixture,
writeBlocks(readBlocks(data)) returns the exact original bytes.
Pass an explicit target software version when generating new content or intentionally normalizing field layout according to the Python writer:
import { simpleTextDocument, writeBlocks } from "rmscene-ts";
const blocks = simpleTextDocument("Hello", { version: "3.27.3.0" });
const data = writeBlocks(blocks, { version: "3.27.3.0" });The writer serializes blocks. It does not flatten a modified SceneTree back into blocks or update a
tablet's document manifests. Keep the original block array when making lossless edits.
Render SVG previews
import { readTree, renderSvg } from "rmscene-ts";
const tree = readTree(data);
const preview = renderSvg(tree, {
viewport: "content",
background: "white",
});
preview.svg;
preview.viewBox;
preview.strokeCount;
preview.text;The renderer reads page dimensions from SceneInfo, keeps the horizontally centered stroke
coordinate system, expands a content viewport beyond page bounds, uses Paper Pro RGBA colors when
present, escapes typed text, and preserves leading and trailing typed whitespace. A scene without SceneInfo.paperSize requires an explicit
paperSize: [width, height]; no device size is guessed.
This is a deterministic preview renderer. It preserves geometry, ordering, colors, text, visibility,
and bounds, but does not emulate every proprietary brush texture, pressure shader, or font metric in
xochitl.
Reader behavior
| Condition | Default mode | Strict mode |
|---|---|---|
| Wrong version 6 header | throws RmParseError | throws RmParseError |
| Malformed known block | returns UnreadableBlock, warns, continues at its declared end | throws RmParseError |
| Truncated final block header or payload | retains the incomplete bytes in an UnreadableBlock, warns | throws RmParseError |
| Unknown block type | retains raw payload in UnreadableBlock, warns | same |
| Unknown pen, color, paragraph style, or text code | retains numeric value, warns | same |
| Unread trailing data | retains bytes in extraData or extraValueData, warns | same |
Pens, colors, and paragraph styles are open numeric values. A known tool is represented as
{ value: 23, name: "SHADER" }; a future tool remains observable as { value: 255 }.
CrdtId.part2 is a bigint.
Paper Pro compatibility
| Input or environment | Support |
|---|---|
| .rm version 6 | Yes |
| reMarkable Paper Pro, software 3.27.3.0 | Tested with sanitized fixtures |
| Files in the rmscene 0.8.0 test suite | All 13 fixtures match Python goldens |
| Browser | ES2022 with TextEncoder, TextDecoder, DataView, and BigInt |
| Node | 20 and newer |
| Older .rm versions | No |
Tested Paper Pro scenes contain both 1620 x 2160 and 1404 x 1872 pages. Coordinates can be negative or exceed the page after the infinite canvas is scrolled. Highlighters can carry both a palette index and an actual RGBA color.
Forward-compatible trailing data is expected. Sanitized fixtures retain 111 unread bytes in Paper Pro
SceneInfo, 18 in RootText, and five nested bytes on each of nine SHADER lines.
What this port changes
The port is not a transliteration. These are the differences against rmscene 0.8.0, each covered by
tests in this repository.
Fixed for reMarkable Paper Pro
- Firmware trailing bytes are data, not corruption. Paper Pro writes fields the 0.8.0 model does
not know: about 111 unread bytes in every
SceneInfoblock and about 18 in everyRootText. The reader keeps them inextraDataandextraValueData, so a read and write round trip returns them untouched instead of dropping them. - Unknown pens, colors, and text codes stay observable. Firmware-controlled sets are open values,
{ value, name? }, never closed enums, so a pen or color that a later firmware invents survives a read and a write instead of failing validation. The text formatting codes 1 and 2, which 0.8.0 reports as unknown, keep their raw number for the caller to act on. - Page size comes from the file.
SceneInfo.paperSizedecides the page, so 1620 x 2160 Paper Pro pages are not clipped to the reMarkable 2 size. - Highlighter RGBA is exposed next to the palette index, because Paper Pro carries both.
- Truncated files keep their last bytes. A file that ends part way into the next block header keeps those exact bytes and writes them back unchanged, instead of being repaired or discarded.
A Rust parser, remarkable_lines, was evaluated as an
alternative before this port was written, and rejected: it failed on five of eight real Paper Pro
pages, with Block type '7' did not read expected size on the trailing bytes above, and
Invalid tool with value '23' on the shader pen that firmware 3.x ships and rmscene 0.8.0 already
knows. Both failure modes are regression-tested here.
Added on top of the Python library
- A byte-exact writer. With no target version, every block writes back the header versions, field
presence, timestamps, and trailing data it was read with, so
writeBlocks(readBlocks(data))returns the original bytes for all 17 fixtures. The Python writer normalizes to a target software version; that mode is still available here throughWriteOptions.versionand is verified against Python output hashes. - An SVG preview renderer.
rmscenehas none; rendering lives in the separatermctool. This package renders scenes deterministically in the browser too, including page bounds, off-page geometry, visibility, palette and RGBA colors, glyph ranges, and typed text with preserved whitespace. - Warnings as an API. Tolerant reading is inherited from the Python library, but recovery is
reported through an
onWarningcallback and astrictswitch instead of logging, so a caller can decide per file what to do. - Hardened bounds. Lengths and counts are checked before slicing or allocating, varuints are capped at the ten bytes a uint64 needs, and every block and nested field parses inside its own cursor, so a malformed child cannot eat the field that follows it.
- Paper Pro test coverage. Four sanitized Paper Pro fixtures, with every text character replaced by
x, sit next to the 13 upstream fixtures, and both sets are checked against Python goldens.
Platform
- No runtime dependencies, ESM and CommonJS builds with TypeScript declarations.
Uint8Arrayin and out, no filesystem, network, or device access, so it runs in a browser as well as in Node 20 and newer.CrdtId.part2is abigint, which the format needs and JavaScript numbers cannot hold.
Verification and development
The test suite contains all 13 upstream fixtures and four sanitized Paper Pro fixtures. It compares reader output and explicit writer versions with goldens produced by the pinned Python implementation, requires byte-exact lossless round trips, and snapshots deterministic SVG output. It also covers corruption, truncation, random input, unknown values, field presence, ESM, CommonJS, declarations, and the packed package surface.
npm ci
npm run check
npm audit
npm pack --dry-runUse npm run goldens for reader goldens and npm run goldens:writer for writer hashes with the exact
pinned ../rmscene checkout. Fixture provenance is in
tests/fixtures/README.md. The codec design is in
docs/architecture/rm-v6-codec.md.
The three packages
| Package | What it does |
| --- | --- |
| rmscene-ts (npm) | Reads, writes and renders .rm version 6 scene files. No filesystem, no network, browser-safe. |
| rmcommunication-ts (npm) | Talks to the tablet over pinned SSH: listings, verified rmdoc backups, page rendering, templates, PNG, PDF and EPUB import. |
| remarkable-cli (npm) | The rmcli command line over both libraries. |
None of them implements the reMarkable Cloud protocol.
Direct SSH/SFTP, document bundles, backups, templates, PNG output, mirror, and guarded page writeback
belong to rmcommunication-ts, not here.
License and lineage
MIT. This port is derived from rmscene by Rick Lupton; its
original copyright and MIT license are preserved in LICENSE. The upstream test fixtures
are redistributed under the same license.
