rmapi-js 11.1.2 → 11.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +104 -13
- package/dist/index.js +343 -124
- package/dist/raw.d.ts +61 -10
- package/dist/raw.js +62 -13
- package/dist/rm5.d.ts +111 -0
- package/dist/rm5.js +181 -0
- package/dist/rm6.d.ts +334 -0
- package/dist/rm6.js +728 -0
- package/dist/rmapi-js.esm.min.js +8 -8
- package/package.json +1 -1
package/dist/raw.d.ts
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
import { type RmPageV5 } from "./rm5.js";
|
|
2
|
+
import { type RmScene } from "./rm6.js";
|
|
3
|
+
/**
|
|
4
|
+
* a parsed reMarkable `.rm` page
|
|
5
|
+
*
|
|
6
|
+
* A discriminated union on `version`: versions 3/5 parse to the flat, renderable
|
|
7
|
+
* {@link RmPageV5 | `RmPageV5`}; version 6 parses to the richer
|
|
8
|
+
* {@link RmScene | `RmScene`} CRDT scene (`version: 6`).
|
|
9
|
+
*/
|
|
10
|
+
export type RmPage = RmPageV5 | RmScene;
|
|
11
|
+
/**
|
|
12
|
+
* parse the bytes of a reMarkable `.rm` page file
|
|
13
|
+
*
|
|
14
|
+
* Dispatches on the header version: versions 3/5 parse to a flat
|
|
15
|
+
* {@link RmPageV5 | `RmPageV5`}; version 6 parses to an
|
|
16
|
+
* {@link RmScene | `RmScene`}. Throws for an unknown version or malformed data.
|
|
17
|
+
*
|
|
18
|
+
* @param data - the raw `.rm` file bytes
|
|
19
|
+
* @returns the parsed page
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseRm(data: Uint8Array): RmPage;
|
|
1
22
|
/** request types */
|
|
2
23
|
export type RequestMethod = "POST" | "GET" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";
|
|
3
24
|
/** the supported upload mime types */
|
|
@@ -120,6 +141,14 @@ export interface CPagePage {
|
|
|
120
141
|
verticalScroll?: CPageNumberValue;
|
|
121
142
|
/** [unknown] */
|
|
122
143
|
deleted?: CPageNumberValue;
|
|
144
|
+
/**
|
|
145
|
+
* a per-page last-modified epoch-milliseconds timestamp
|
|
146
|
+
*
|
|
147
|
+
* The misspelling is reMarkable's own: the firmware writes this key as
|
|
148
|
+
* `modifed`. Unlike the sibling fields it is a bare string, not a
|
|
149
|
+
* timestamped value.
|
|
150
|
+
*/
|
|
151
|
+
modifed?: string;
|
|
123
152
|
}
|
|
124
153
|
/** [unknown] */
|
|
125
154
|
export interface CPageUUID {
|
|
@@ -379,6 +408,8 @@ export interface Metadata {
|
|
|
379
408
|
/** the visible name of the item, what it's called on the reMarkable */
|
|
380
409
|
visibleName: string;
|
|
381
410
|
}
|
|
411
|
+
/** parse and validate the json text of a `.metadata` file */
|
|
412
|
+
export declare function parseMetadata(text: string): Metadata;
|
|
382
413
|
/**
|
|
383
414
|
* access to the low-level reMarkable api
|
|
384
415
|
*
|
|
@@ -503,6 +534,21 @@ export interface RawRemarkableApi {
|
|
|
503
534
|
* @returns the metadata
|
|
504
535
|
*/
|
|
505
536
|
getMetadata(fileName: string, hash: string): Promise<Metadata>;
|
|
537
|
+
/**
|
|
538
|
+
* get the parsed reMarkable lines (`.rm`) drawing of a page hash
|
|
539
|
+
|
|
540
|
+
* @param fileName - typically `"<id>/<pageid>.rm"`
|
|
541
|
+
* @param hash - the hash to get the page for
|
|
542
|
+
* @returns the parsed page
|
|
543
|
+
*/
|
|
544
|
+
getRm(fileName: string, hash: string): Promise<RmPage>;
|
|
545
|
+
/**
|
|
546
|
+
* the same as {@link putFile | `putFile`} but rendering an `RmPage` to `.rm`
|
|
547
|
+
* bytes
|
|
548
|
+
*
|
|
549
|
+
* Only version 3 and 5 pages can be rendered; version 6 pages are read-only.
|
|
550
|
+
*/
|
|
551
|
+
putRm(fileName: string, page: RmPageV5): Promise<[RawEntry, Promise<void>]>;
|
|
506
552
|
/**
|
|
507
553
|
* update the current root hash
|
|
508
554
|
*
|
|
@@ -533,17 +579,17 @@ export interface RawRemarkableApi {
|
|
|
533
579
|
* NOTE: This won't update the state of the reMarkable until this entry is
|
|
534
580
|
* incorporated into the root hash.
|
|
535
581
|
*
|
|
536
|
-
* @param
|
|
582
|
+
* @param fileName - the file name to upload (e.g. `<id>.pdf`)
|
|
537
583
|
* @param bytes - the bytes to upload
|
|
538
584
|
* @returns the new entry and a promise to finish the upload
|
|
539
585
|
*/
|
|
540
|
-
putFile(
|
|
586
|
+
putFile(fileName: string, bytes: Uint8Array): Promise<[RawEntry, Promise<void>]>;
|
|
541
587
|
/** the same as {@link putFile | `putFile`} but with caching for text */
|
|
542
|
-
putText(
|
|
588
|
+
putText(fileName: string, content: string): Promise<[RawEntry, Promise<void>]>;
|
|
543
589
|
/** the same as {@link putText | `putText`} but with extra validation for Content */
|
|
544
|
-
putContent(
|
|
590
|
+
putContent(fileName: string, content: Content): Promise<[RawEntry, Promise<void>]>;
|
|
545
591
|
/** the same as {@link putText | `putText`} but with extra validation for Metadata */
|
|
546
|
-
putMetadata(
|
|
592
|
+
putMetadata(fileName: string, metadata: Metadata): Promise<[RawEntry, Promise<void>]>;
|
|
547
593
|
/**
|
|
548
594
|
* put a set of entries to make an entry list file
|
|
549
595
|
*
|
|
@@ -559,7 +605,10 @@ export interface RawRemarkableApi {
|
|
|
559
605
|
* warning is logged if a schema 3 root index is written.
|
|
560
606
|
*
|
|
561
607
|
* @param id - the id of the list to upload - this should be the item id if
|
|
562
|
-
* uploading an item list, or "root" if uploading a new root list.
|
|
608
|
+
* uploading an item list, or "root" if uploading a new root list. Note the
|
|
609
|
+
* asymmetry with {@link getEntries | `getEntries`}: `getEntries` takes the
|
|
610
|
+
* full `"<id>.docSchema"` file name, whereas `putEntries` takes the bare id
|
|
611
|
+
* and appends `.docSchema` (and special-cases `"root"`) itself.
|
|
563
612
|
* @param entries - the entries to upload
|
|
564
613
|
*
|
|
565
614
|
* @returns the new list entry and a promise to finish the upload
|
|
@@ -602,11 +651,13 @@ export declare class RawRemarkable implements RawRemarkableApi {
|
|
|
602
651
|
getEntries(fileName: string, hash: string): Promise<Entries>;
|
|
603
652
|
getContent(fileName: string, hash: string): Promise<Content>;
|
|
604
653
|
getMetadata(fileName: string, hash: string): Promise<Metadata>;
|
|
654
|
+
getRm(fileName: string, hash: string): Promise<RmPage>;
|
|
655
|
+
putRm(fileName: string, page: RmPageV5): Promise<[RawEntry, Promise<void>]>;
|
|
605
656
|
putRootHash(hash: string, generation: number, broadcast?: boolean): Promise<[string, number]>;
|
|
606
|
-
putFile(
|
|
607
|
-
putText(
|
|
608
|
-
putContent(
|
|
609
|
-
putMetadata(
|
|
657
|
+
putFile(fileName: string, bytes: Uint8Array): Promise<[RawEntry, Promise<void>]>;
|
|
658
|
+
putText(fileName: string, text: string): Promise<[RawEntry, Promise<void>]>;
|
|
659
|
+
putContent(fileName: string, content: Content): Promise<[RawEntry, Promise<void>]>;
|
|
660
|
+
putMetadata(fileName: string, metadata: Metadata): Promise<[RawEntry, Promise<void>]>;
|
|
610
661
|
putEntries(id: string, entries: readonly RawEntry[], schemaVersion: SchemaVersion): Promise<[RawEntry, Promise<void>]>;
|
|
611
662
|
uploadFile(visibleName: string, bytes: Uint8Array, mime: UploadMimeType): Promise<SimpleEntry>;
|
|
612
663
|
dumpCache(): string;
|
package/dist/raw.js
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
1
|
import CRC32C from "crc-32/crc32c";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { ValidationError } from "./error.js";
|
|
4
|
+
import { HEADER_LENGTH, parseV5, serializeRm, VERSION_PREFIX, } from "./rm5.js";
|
|
5
|
+
import { parseRmScene } from "./rm6.js";
|
|
4
6
|
import { concatArrays } from "./utils.js";
|
|
5
7
|
const hashReg = /^[0-9a-f]{64}$/;
|
|
8
|
+
/**
|
|
9
|
+
* parse the bytes of a reMarkable `.rm` page file
|
|
10
|
+
*
|
|
11
|
+
* Dispatches on the header version: versions 3/5 parse to a flat
|
|
12
|
+
* {@link RmPageV5 | `RmPageV5`}; version 6 parses to an
|
|
13
|
+
* {@link RmScene | `RmScene`}. Throws for an unknown version or malformed data.
|
|
14
|
+
*
|
|
15
|
+
* @param data - the raw `.rm` file bytes
|
|
16
|
+
* @returns the parsed page
|
|
17
|
+
*/
|
|
18
|
+
export function parseRm(data) {
|
|
19
|
+
if (data.length < HEADER_LENGTH) {
|
|
20
|
+
throw new Error("data is too short to be a reMarkable .lines file");
|
|
21
|
+
}
|
|
22
|
+
const header = new TextDecoder().decode(data.subarray(0, HEADER_LENGTH));
|
|
23
|
+
if (!header.startsWith(VERSION_PREFIX)) {
|
|
24
|
+
throw new Error(`unrecognized .lines header: ${JSON.stringify(header)}`);
|
|
25
|
+
}
|
|
26
|
+
const versionChar = header.charAt(VERSION_PREFIX.length);
|
|
27
|
+
if (versionChar === "6") {
|
|
28
|
+
return parseRmScene(data);
|
|
29
|
+
}
|
|
30
|
+
else if (versionChar === "3" || versionChar === "5") {
|
|
31
|
+
return parseV5(data, versionChar === "3" ? 3 : 5);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
throw new Error(`unsupported .lines version '${versionChar}'`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
6
37
|
const tag = z
|
|
7
38
|
.object({
|
|
8
39
|
name: z.string(),
|
|
@@ -51,6 +82,7 @@ const cPagePage = z
|
|
|
51
82
|
.object({ timestamp: z.string(), value: z.number().int() })
|
|
52
83
|
.passthrough()
|
|
53
84
|
.optional(),
|
|
85
|
+
modifed: z.string().optional(),
|
|
54
86
|
})
|
|
55
87
|
.passthrough();
|
|
56
88
|
const cPages = z
|
|
@@ -183,6 +215,11 @@ const metadata = z
|
|
|
183
215
|
source: z.string().optional(),
|
|
184
216
|
})
|
|
185
217
|
.passthrough();
|
|
218
|
+
/** parse and validate the json text of a `.metadata` file */
|
|
219
|
+
export function parseMetadata(text) {
|
|
220
|
+
const loaded = JSON.parse(text);
|
|
221
|
+
return metadata.parse(loaded);
|
|
222
|
+
}
|
|
186
223
|
const updatedRootHash = z
|
|
187
224
|
.object({
|
|
188
225
|
hash: z.string(),
|
|
@@ -344,6 +381,18 @@ export class RawRemarkable {
|
|
|
344
381
|
const loaded = JSON.parse(raw);
|
|
345
382
|
return metadata.parse(loaded);
|
|
346
383
|
}
|
|
384
|
+
async getRm(fileName, hash) {
|
|
385
|
+
const bytes = await this.getHash(fileName, hash);
|
|
386
|
+
return parseRm(bytes);
|
|
387
|
+
}
|
|
388
|
+
async putRm(fileName, page) {
|
|
389
|
+
if (!fileName.endsWith(".rm")) {
|
|
390
|
+
throw new Error(`fileName ${fileName} did not end with '.rm'`);
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
return await this.putFile(fileName, serializeRm(page));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
347
396
|
async putRootHash(hash, generation, broadcast = true) {
|
|
348
397
|
if (!Number.isSafeInteger(generation)) {
|
|
349
398
|
throw new Error(`generation ${generation} was not a safe integer`);
|
|
@@ -388,21 +437,21 @@ export class RawRemarkable {
|
|
|
388
437
|
}
|
|
389
438
|
}
|
|
390
439
|
}
|
|
391
|
-
async putFile(
|
|
440
|
+
async putFile(fileName, bytes) {
|
|
392
441
|
const hash = await digest(bytes);
|
|
393
442
|
const res = {
|
|
394
|
-
id,
|
|
443
|
+
id: fileName,
|
|
395
444
|
hash,
|
|
396
445
|
type: 0,
|
|
397
446
|
subfiles: 0,
|
|
398
447
|
size: bytes.length,
|
|
399
448
|
};
|
|
400
|
-
return [res, this.#putFile(
|
|
449
|
+
return [res, this.#putFile(fileName, hash, bytes)];
|
|
401
450
|
}
|
|
402
|
-
async putText(
|
|
451
|
+
async putText(fileName, text) {
|
|
403
452
|
const enc = new TextEncoder();
|
|
404
453
|
const bytes = enc.encode(text);
|
|
405
|
-
const [ent, upload] = await this.putFile(
|
|
454
|
+
const [ent, upload] = await this.putFile(fileName, bytes);
|
|
406
455
|
return [
|
|
407
456
|
ent,
|
|
408
457
|
upload.then(() => {
|
|
@@ -411,20 +460,20 @@ export class RawRemarkable {
|
|
|
411
460
|
}),
|
|
412
461
|
];
|
|
413
462
|
}
|
|
414
|
-
async putContent(
|
|
415
|
-
if (!
|
|
416
|
-
throw new Error(`
|
|
463
|
+
async putContent(fileName, content) {
|
|
464
|
+
if (!fileName.endsWith(".content")) {
|
|
465
|
+
throw new Error(`fileName ${fileName} did not end with '.content'`);
|
|
417
466
|
}
|
|
418
467
|
else {
|
|
419
|
-
return await this.putText(
|
|
468
|
+
return await this.putText(fileName, JSON.stringify(content));
|
|
420
469
|
}
|
|
421
470
|
}
|
|
422
|
-
async putMetadata(
|
|
423
|
-
if (!
|
|
424
|
-
throw new Error(`
|
|
471
|
+
async putMetadata(fileName, metadata) {
|
|
472
|
+
if (!fileName.endsWith(".metadata")) {
|
|
473
|
+
throw new Error(`fileName ${fileName} did not end with '.metadata'`);
|
|
425
474
|
}
|
|
426
475
|
else {
|
|
427
|
-
return await this.putText(
|
|
476
|
+
return await this.putText(fileName, JSON.stringify(metadata));
|
|
428
477
|
}
|
|
429
478
|
}
|
|
430
479
|
async putEntries(id, entries, schemaVersion) {
|
package/dist/rm5.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse and render the flat (version 3 and 5) reMarkable `.rm` page format.
|
|
3
|
+
*
|
|
4
|
+
* A `.rm` file is the vector drawing for a single notebook page. The version 3
|
|
5
|
+
* and 5 formats are a flat, little-endian struct of layers, each holding strokes
|
|
6
|
+
* ("lines"), each holding sampled points (they differ only by one extra
|
|
7
|
+
* per-stroke field in version 5). {@link parseV5 | `parseV5`} reads them into an
|
|
8
|
+
* {@link RmPageV5 | `RmPageV5`} and {@link serializeRm | `serializeRm`} renders
|
|
9
|
+
* it back to byte-exact bytes.
|
|
10
|
+
*
|
|
11
|
+
* The newer version 6 "scene tree" format lives in `./rm6.js`; the version
|
|
12
|
+
* dispatch that picks between them (`parseRm`) lives in `./raw.js`.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
/** the flat reMarkable `.lines` file versions read into an {@link RmPageV5} */
|
|
17
|
+
export type RmVersion = 3 | 5;
|
|
18
|
+
/** a single sampled point along a stroke */
|
|
19
|
+
export interface RmPoint {
|
|
20
|
+
/** the horizontal position in device pixels (see the page type for the origin) */
|
|
21
|
+
x: number;
|
|
22
|
+
/** the vertical position in device pixels (see the page type for the origin) */
|
|
23
|
+
y: number;
|
|
24
|
+
/** the pen speed at this point */
|
|
25
|
+
speed: number;
|
|
26
|
+
/** the pen tilt/heading in radians */
|
|
27
|
+
direction: number;
|
|
28
|
+
/** the stroke width at this point */
|
|
29
|
+
width: number;
|
|
30
|
+
/** the pen pressure, from 0 to 1 */
|
|
31
|
+
pressure: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* a single stroke (called a "line") within a layer
|
|
35
|
+
*
|
|
36
|
+
* `brushType` and `color` are kept as raw integers because both are overloaded
|
|
37
|
+
* across firmware versions (there are two pen-code families, and the color
|
|
38
|
+
* field is reinterpreted as a palette index when colored annotations are
|
|
39
|
+
* enabled), so the right meaning can only be resolved with the source firmware
|
|
40
|
+
* in mind.
|
|
41
|
+
*/
|
|
42
|
+
export interface RmLine {
|
|
43
|
+
/** the raw brush/pen type code */
|
|
44
|
+
brushType: number;
|
|
45
|
+
/** the raw color code */
|
|
46
|
+
color: number;
|
|
47
|
+
/** a per-stroke padding field, typically 0 */
|
|
48
|
+
padding?: number;
|
|
49
|
+
/** the base brush size */
|
|
50
|
+
brushBaseSize: number;
|
|
51
|
+
/** [unknown] a per-stroke field only present in version 5 */
|
|
52
|
+
unknown?: number;
|
|
53
|
+
/** the sampled points making up the stroke, in order */
|
|
54
|
+
points: RmPoint[];
|
|
55
|
+
}
|
|
56
|
+
/** a drawing layer, an ordered set of strokes */
|
|
57
|
+
export interface RmLayer {
|
|
58
|
+
/** the strokes on this layer, in order */
|
|
59
|
+
lines: RmLine[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* a parsed version 3 or 5 page
|
|
63
|
+
*
|
|
64
|
+
* Coordinates use a top-left origin: `x` in `[0, width]`, `y` in `[0, height]`,
|
|
65
|
+
* in device pixels. Only these pages can be re-rendered (via `raw.putRm`).
|
|
66
|
+
*/
|
|
67
|
+
export interface RmPageV5 {
|
|
68
|
+
/** the file format version */
|
|
69
|
+
version: 3 | 5;
|
|
70
|
+
/** the drawing layers, back to front */
|
|
71
|
+
layers: RmLayer[];
|
|
72
|
+
}
|
|
73
|
+
/** a decoded pen/tool for a stroke */
|
|
74
|
+
export type RmBrush = "brush" | "pencil" | "ballpoint" | "marker" | "fineliner" | "highlighter" | "eraser" | "mechanicalPencil" | "eraseArea" | "calligraphy" | "shader";
|
|
75
|
+
/**
|
|
76
|
+
* decode a raw {@link RmLine.brushType | `brushType`} code to a pen name
|
|
77
|
+
*
|
|
78
|
+
* Best-effort and advisory: the reMarkable pen codes come in two firmware
|
|
79
|
+
* families and new ones appear over time, so an unrecognized code returns
|
|
80
|
+
* `undefined`. The raw `brushType` int stays authoritative.
|
|
81
|
+
*/
|
|
82
|
+
export declare function decodeBrush(code: number): RmBrush | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* the default (monochrome) reMarkable palette, by raw {@link RmLine.color | `color`}
|
|
85
|
+
*
|
|
86
|
+
* Advisory only: when "colored annotations" are enabled or on version 6, the
|
|
87
|
+
* `color` field is reinterpreted, so resolve against the source firmware.
|
|
88
|
+
*/
|
|
89
|
+
export declare const rmColors: Readonly<Record<number, string>>;
|
|
90
|
+
/** the length of the fixed `.lines` header, in bytes */
|
|
91
|
+
export declare const HEADER_LENGTH = 43;
|
|
92
|
+
/** the ascii prefix of the header, immediately followed by the version digit */
|
|
93
|
+
export declare const VERSION_PREFIX = "reMarkable .lines file, version=";
|
|
94
|
+
/**
|
|
95
|
+
* parse a version 3 or 5 (flat struct) `.rm` page
|
|
96
|
+
*
|
|
97
|
+
* Named for version 5, the common case, but the two formats differ only by the
|
|
98
|
+
* extra {@link RmLine.unknown | `unknown`} field version 5 adds per stroke.
|
|
99
|
+
*/
|
|
100
|
+
export declare function parseV5(data: Uint8Array, version: 3 | 5): RmPageV5;
|
|
101
|
+
/**
|
|
102
|
+
* render a page back into reMarkable `.rm` file bytes
|
|
103
|
+
*
|
|
104
|
+
* The inverse of {@link parseV5 | `parseV5`}: `serializeRm(parseV5(bytes, v))`
|
|
105
|
+
* reproduces the original bytes for version 3 and 5 files. A missing `padding`
|
|
106
|
+
* or (version 5) `unknown` field is written as `0`.
|
|
107
|
+
*
|
|
108
|
+
* @param page - the page to render
|
|
109
|
+
* @returns the `.rm` file bytes
|
|
110
|
+
*/
|
|
111
|
+
export declare function serializeRm(page: RmPageV5): Uint8Array;
|
package/dist/rm5.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse and render the flat (version 3 and 5) reMarkable `.rm` page format.
|
|
3
|
+
*
|
|
4
|
+
* A `.rm` file is the vector drawing for a single notebook page. The version 3
|
|
5
|
+
* and 5 formats are a flat, little-endian struct of layers, each holding strokes
|
|
6
|
+
* ("lines"), each holding sampled points (they differ only by one extra
|
|
7
|
+
* per-stroke field in version 5). {@link parseV5 | `parseV5`} reads them into an
|
|
8
|
+
* {@link RmPageV5 | `RmPageV5`} and {@link serializeRm | `serializeRm`} renders
|
|
9
|
+
* it back to byte-exact bytes.
|
|
10
|
+
*
|
|
11
|
+
* The newer version 6 "scene tree" format lives in `./rm6.js`; the version
|
|
12
|
+
* dispatch that picks between them (`parseRm`) lives in `./raw.js`.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
const BRUSHES = {
|
|
17
|
+
0: "brush",
|
|
18
|
+
12: "brush",
|
|
19
|
+
1: "pencil",
|
|
20
|
+
14: "pencil",
|
|
21
|
+
2: "ballpoint",
|
|
22
|
+
15: "ballpoint",
|
|
23
|
+
3: "marker",
|
|
24
|
+
16: "marker",
|
|
25
|
+
4: "fineliner",
|
|
26
|
+
17: "fineliner",
|
|
27
|
+
5: "highlighter",
|
|
28
|
+
18: "highlighter",
|
|
29
|
+
6: "eraser",
|
|
30
|
+
7: "mechanicalPencil",
|
|
31
|
+
13: "mechanicalPencil",
|
|
32
|
+
8: "eraseArea",
|
|
33
|
+
21: "calligraphy",
|
|
34
|
+
23: "shader",
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* decode a raw {@link RmLine.brushType | `brushType`} code to a pen name
|
|
38
|
+
*
|
|
39
|
+
* Best-effort and advisory: the reMarkable pen codes come in two firmware
|
|
40
|
+
* families and new ones appear over time, so an unrecognized code returns
|
|
41
|
+
* `undefined`. The raw `brushType` int stays authoritative.
|
|
42
|
+
*/
|
|
43
|
+
export function decodeBrush(code) {
|
|
44
|
+
return BRUSHES[code];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* the default (monochrome) reMarkable palette, by raw {@link RmLine.color | `color`}
|
|
48
|
+
*
|
|
49
|
+
* Advisory only: when "colored annotations" are enabled or on version 6, the
|
|
50
|
+
* `color` field is reinterpreted, so resolve against the source firmware.
|
|
51
|
+
*/
|
|
52
|
+
export const rmColors = {
|
|
53
|
+
0: "black",
|
|
54
|
+
1: "gray",
|
|
55
|
+
2: "white",
|
|
56
|
+
};
|
|
57
|
+
/** the length of the fixed `.lines` header, in bytes */
|
|
58
|
+
export const HEADER_LENGTH = 43;
|
|
59
|
+
/** the ascii prefix of the header, immediately followed by the version digit */
|
|
60
|
+
export const VERSION_PREFIX = "reMarkable .lines file, version=";
|
|
61
|
+
/**
|
|
62
|
+
* parse a version 3 or 5 (flat struct) `.rm` page
|
|
63
|
+
*
|
|
64
|
+
* Named for version 5, the common case, but the two formats differ only by the
|
|
65
|
+
* extra {@link RmLine.unknown | `unknown`} field version 5 adds per stroke.
|
|
66
|
+
*/
|
|
67
|
+
export function parseV5(data, version) {
|
|
68
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
69
|
+
let offset = HEADER_LENGTH;
|
|
70
|
+
const readInt = () => {
|
|
71
|
+
const value = view.getInt32(offset, true);
|
|
72
|
+
offset += 4;
|
|
73
|
+
return value;
|
|
74
|
+
};
|
|
75
|
+
const readFloat = () => {
|
|
76
|
+
const value = view.getFloat32(offset, true);
|
|
77
|
+
offset += 4;
|
|
78
|
+
return value;
|
|
79
|
+
};
|
|
80
|
+
const numLayers = readInt();
|
|
81
|
+
const layers = [];
|
|
82
|
+
for (let layer = 0; layer < numLayers; layer++) {
|
|
83
|
+
const numLines = readInt();
|
|
84
|
+
const lines = [];
|
|
85
|
+
for (let line = 0; line < numLines; line++) {
|
|
86
|
+
const brushType = readInt();
|
|
87
|
+
const color = readInt();
|
|
88
|
+
const padding = readInt();
|
|
89
|
+
const brushBaseSize = readFloat();
|
|
90
|
+
const unknown = version === 5 ? readInt() : undefined;
|
|
91
|
+
const numPoints = readInt();
|
|
92
|
+
const points = new Array(numPoints);
|
|
93
|
+
for (let point = 0; point < numPoints; point++) {
|
|
94
|
+
points[point] = {
|
|
95
|
+
x: readFloat(),
|
|
96
|
+
y: readFloat(),
|
|
97
|
+
speed: readFloat(),
|
|
98
|
+
direction: readFloat(),
|
|
99
|
+
width: readFloat(),
|
|
100
|
+
pressure: readFloat(),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const parsed = {
|
|
104
|
+
brushType,
|
|
105
|
+
color,
|
|
106
|
+
padding,
|
|
107
|
+
brushBaseSize,
|
|
108
|
+
points,
|
|
109
|
+
};
|
|
110
|
+
if (unknown !== undefined) {
|
|
111
|
+
parsed.unknown = unknown;
|
|
112
|
+
}
|
|
113
|
+
lines.push(parsed);
|
|
114
|
+
}
|
|
115
|
+
layers.push({ lines });
|
|
116
|
+
}
|
|
117
|
+
return { version, layers };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* render a page back into reMarkable `.rm` file bytes
|
|
121
|
+
*
|
|
122
|
+
* The inverse of {@link parseV5 | `parseV5`}: `serializeRm(parseV5(bytes, v))`
|
|
123
|
+
* reproduces the original bytes for version 3 and 5 files. A missing `padding`
|
|
124
|
+
* or (version 5) `unknown` field is written as `0`.
|
|
125
|
+
*
|
|
126
|
+
* @param page - the page to render
|
|
127
|
+
* @returns the `.rm` file bytes
|
|
128
|
+
*/
|
|
129
|
+
export function serializeRm(page) {
|
|
130
|
+
const { version } = page;
|
|
131
|
+
// runtime backstop for untyped (JS) callers passing a v6 page
|
|
132
|
+
if (version !== 3 && version !== 5) {
|
|
133
|
+
throw new Error(`rendering version ${version} .lines files is not supported (only 3 and 5)`);
|
|
134
|
+
}
|
|
135
|
+
const strokeHeaderSize = version === 5 ? 24 : 20;
|
|
136
|
+
let size = HEADER_LENGTH + 4;
|
|
137
|
+
for (const layer of page.layers) {
|
|
138
|
+
size += 4;
|
|
139
|
+
for (const line of layer.lines) {
|
|
140
|
+
size += strokeHeaderSize + line.points.length * 24;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const bytes = new Uint8Array(size);
|
|
144
|
+
const view = new DataView(bytes.buffer);
|
|
145
|
+
const header = `${VERSION_PREFIX}${version}`.padEnd(HEADER_LENGTH, " ");
|
|
146
|
+
for (let index = 0; index < HEADER_LENGTH; index++) {
|
|
147
|
+
bytes[index] = header.charCodeAt(index);
|
|
148
|
+
}
|
|
149
|
+
let offset = HEADER_LENGTH;
|
|
150
|
+
const writeInt = (value) => {
|
|
151
|
+
view.setInt32(offset, value, true);
|
|
152
|
+
offset += 4;
|
|
153
|
+
};
|
|
154
|
+
const writeFloat = (value) => {
|
|
155
|
+
view.setFloat32(offset, value, true);
|
|
156
|
+
offset += 4;
|
|
157
|
+
};
|
|
158
|
+
writeInt(page.layers.length);
|
|
159
|
+
for (const layer of page.layers) {
|
|
160
|
+
writeInt(layer.lines.length);
|
|
161
|
+
for (const line of layer.lines) {
|
|
162
|
+
writeInt(line.brushType);
|
|
163
|
+
writeInt(line.color);
|
|
164
|
+
writeInt(line.padding ?? 0);
|
|
165
|
+
writeFloat(line.brushBaseSize);
|
|
166
|
+
if (version === 5) {
|
|
167
|
+
writeInt(line.unknown ?? 0);
|
|
168
|
+
}
|
|
169
|
+
writeInt(line.points.length);
|
|
170
|
+
for (const point of line.points) {
|
|
171
|
+
writeFloat(point.x);
|
|
172
|
+
writeFloat(point.y);
|
|
173
|
+
writeFloat(point.speed);
|
|
174
|
+
writeFloat(point.direction);
|
|
175
|
+
writeFloat(point.width);
|
|
176
|
+
writeFloat(point.pressure);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return bytes;
|
|
181
|
+
}
|