rmapi-js 11.1.2 → 12.0.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/README.md +3 -2
- package/dist/index.d.ts +154 -91
- package/dist/index.js +756 -178
- package/dist/raw.d.ts +97 -65
- package/dist/raw.js +276 -23
- 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.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(),
|
|
@@ -196,7 +233,7 @@ const rootHash = z
|
|
|
196
233
|
schemaVersion: z.number().int().nonnegative(),
|
|
197
234
|
})
|
|
198
235
|
.passthrough();
|
|
199
|
-
const
|
|
236
|
+
const nativeItemRef = z
|
|
200
237
|
.object({
|
|
201
238
|
docID: z.string(),
|
|
202
239
|
hash: z.string(),
|
|
@@ -230,6 +267,65 @@ function parseRawEntryLine(line) {
|
|
|
230
267
|
throw new Error(`line '${line}' was not formatted correctly`);
|
|
231
268
|
}
|
|
232
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* access to the low-level reMarkable api
|
|
272
|
+
*
|
|
273
|
+
* This class gives more granualar access to the reMarkable cloud, but is more
|
|
274
|
+
* dangerous.
|
|
275
|
+
*
|
|
276
|
+
* ## Overview
|
|
277
|
+
*
|
|
278
|
+
* reMarkable uses an immutable file system, where each file is referenced by
|
|
279
|
+
* the 32 byte sha256 hash of its contents. Each file also has an id used to
|
|
280
|
+
* keep track of updates, so to "update" a file, you upload a new file, and
|
|
281
|
+
* change the hash associated with it's id.
|
|
282
|
+
*
|
|
283
|
+
* Each "item" (a document or a collection) is actually a list of files.
|
|
284
|
+
* The whole reMarkable state is then a list of these lists. Finally, the hash
|
|
285
|
+
* of that list is called the rootHash. To update anything, you have to update
|
|
286
|
+
* the root hash to point to a new list of updated items.
|
|
287
|
+
*
|
|
288
|
+
* This can be dangerous, as corrupting the root hash can destroy all of your
|
|
289
|
+
* files. It is therefore highly recommended to save your current root hash
|
|
290
|
+
* ({@link getRootHash | `getRootHash`}) before using this api to attempt file
|
|
291
|
+
* writes, so you can recover a previous "snapshot" should anything go wrong.
|
|
292
|
+
*
|
|
293
|
+
* ## Items
|
|
294
|
+
*
|
|
295
|
+
* Each item is a collection of individual files. Using
|
|
296
|
+
* {@link getEntries | `getEntries`} on the root hash will give you a list
|
|
297
|
+
* entries that correspond to items. Using `getEntries` on any of those items
|
|
298
|
+
* will get you the files that make up that item.
|
|
299
|
+
*
|
|
300
|
+
* The documented files are:
|
|
301
|
+
* - `<docid>.pdf` - a raw pdf document
|
|
302
|
+
* - `<docid>.epub` - a raw epub document
|
|
303
|
+
* - `<docid>.content` - a json file roughly describing document properties (see {@link DocumentContent | `DocumentContent`})
|
|
304
|
+
* - `<docid>.metadata` - metadata about the document (see {@link Metadata | `Metadata`})
|
|
305
|
+
* - `<docid>.pagedata` - a text file where each line is the template of that page
|
|
306
|
+
* - `<docid>/<pageid>.rm` - [speculative] raw remarkable vectors, text, etc
|
|
307
|
+
* - `<docid>/<pageid>-metadata.json` - [speculative] metadata about the individual page
|
|
308
|
+
* - `<docid>.highlights/<pageid>.json` - [speculative] highlights on the page
|
|
309
|
+
*
|
|
310
|
+
* Some items will have both a `.pdf` and `.epub` file, likely due to preparing
|
|
311
|
+
* for export. Collections only have `.content` and `.metadata` files, with
|
|
312
|
+
* `.content` only containing tags.
|
|
313
|
+
*
|
|
314
|
+
* ## Caching
|
|
315
|
+
*
|
|
316
|
+
* Since everything is tied to the hash of it's contents, we can agressively
|
|
317
|
+
* cache results. We assume that text contents are "small" and so fully cache
|
|
318
|
+
* them, where as binary files we treat as large and only store that we know
|
|
319
|
+
* they exist to prevent future writes.
|
|
320
|
+
*
|
|
321
|
+
* By default, this only persists as long as the api instance is alive. However,
|
|
322
|
+
* for performance reasons, you should call {@link dumpCache | `dumpCache`} to
|
|
323
|
+
* persist the cache between sessions.
|
|
324
|
+
*
|
|
325
|
+
* @remarks
|
|
326
|
+
*
|
|
327
|
+
* Generally all hashes are 64 character hex strings, and all ids are uuid4.
|
|
328
|
+
*/
|
|
233
329
|
export class RawRemarkable {
|
|
234
330
|
#authedFetch;
|
|
235
331
|
#rawHost;
|
|
@@ -251,6 +347,14 @@ export class RawRemarkable {
|
|
|
251
347
|
this.#uploadHost = uploadHost;
|
|
252
348
|
}
|
|
253
349
|
/** make an authorized request to remarkable */
|
|
350
|
+
/**
|
|
351
|
+
* gets the root hash and the current generation
|
|
352
|
+
*
|
|
353
|
+
* When calling `putRootHash`, you should pass the generation you got from
|
|
354
|
+
* this call. That way you tell reMarkable you're updating the previous state.
|
|
355
|
+
*
|
|
356
|
+
* @returns the root hash and the current generation
|
|
357
|
+
*/
|
|
254
358
|
async getRootHash() {
|
|
255
359
|
const res = await this.#authedFetch("GET", `${this.#rawHost}/sync/v4/root`);
|
|
256
360
|
const raw = await res.text();
|
|
@@ -275,7 +379,17 @@ export class RawRemarkable {
|
|
|
275
379
|
const raw = await resp.arrayBuffer();
|
|
276
380
|
return new Uint8Array(raw);
|
|
277
381
|
}
|
|
278
|
-
|
|
382
|
+
/**
|
|
383
|
+
* get the raw binary data associated with a hash
|
|
384
|
+
*
|
|
385
|
+
* @param ref - a reference to the stored file. Its `id` is the logical file
|
|
386
|
+
* name (`<id>.<ext>` for files, or `<id>.docSchema` / `"root.docSchema"`
|
|
387
|
+
* for entry indexes), which reMarkable validates against the rm-filename
|
|
388
|
+
* header. Sub-entries from {@link getEntries | `getEntries`} can be passed
|
|
389
|
+
* directly.
|
|
390
|
+
* @returns the data
|
|
391
|
+
*/
|
|
392
|
+
async getHash({ id: fileName, hash }) {
|
|
279
393
|
const cached = this.#cache.get(hash);
|
|
280
394
|
if (cached != null) {
|
|
281
395
|
const enc = new TextEncoder();
|
|
@@ -291,7 +405,16 @@ export class RawRemarkable {
|
|
|
291
405
|
return res;
|
|
292
406
|
}
|
|
293
407
|
}
|
|
294
|
-
|
|
408
|
+
/**
|
|
409
|
+
* get raw text data associated with a hash
|
|
410
|
+
*
|
|
411
|
+
* We assume text data are small, and so cache the entire text. If you want to
|
|
412
|
+
* avoid this, use {@link getHash | `getHash`} combined with a TextDecoder.
|
|
413
|
+
*
|
|
414
|
+
* @param ref - a reference to the stored file (see {@link getHash})
|
|
415
|
+
* @returns the text
|
|
416
|
+
*/
|
|
417
|
+
async getText({ id: fileName, hash }) {
|
|
295
418
|
const cached = this.#cache.get(hash);
|
|
296
419
|
if (cached != null) {
|
|
297
420
|
return cached;
|
|
@@ -305,8 +428,18 @@ export class RawRemarkable {
|
|
|
305
428
|
return res;
|
|
306
429
|
}
|
|
307
430
|
}
|
|
308
|
-
|
|
309
|
-
|
|
431
|
+
/**
|
|
432
|
+
* get the entries associated with a list hash
|
|
433
|
+
*
|
|
434
|
+
* A list hash is the root hash, or any hash with the type 80000000. NOTE
|
|
435
|
+
* these are hashed differently than files.
|
|
436
|
+
*
|
|
437
|
+
* @param ref - a reference whose `id` is `"root.docSchema"` for the root, or
|
|
438
|
+
* `"<id>.docSchema"` for a sub-document's entry index
|
|
439
|
+
* @returns the entries
|
|
440
|
+
*/
|
|
441
|
+
async getEntries(ref) {
|
|
442
|
+
const rawFile = await this.getText(ref);
|
|
310
443
|
const [version, ...rest] = rawFile.slice(0, -1).split("\n");
|
|
311
444
|
if (version === "3") {
|
|
312
445
|
return { entries: rest.map(parseRawEntryLine) };
|
|
@@ -334,16 +467,77 @@ export class RawRemarkable {
|
|
|
334
467
|
throw new Error(`schema version ${version} not supported`);
|
|
335
468
|
}
|
|
336
469
|
}
|
|
337
|
-
|
|
338
|
-
|
|
470
|
+
/**
|
|
471
|
+
* get the parsed and validated `Content` of a content hash
|
|
472
|
+
*
|
|
473
|
+
* Use {@link getText | `getText`} combined with `JSON.parse` to bypass
|
|
474
|
+
* validation
|
|
475
|
+
*
|
|
476
|
+
* @param ref - a reference to the stored file, typically `"<id>.content"`
|
|
477
|
+
* @returns the content
|
|
478
|
+
*/
|
|
479
|
+
async getContent(ref) {
|
|
480
|
+
const raw = await this.getText(ref);
|
|
339
481
|
const loaded = JSON.parse(raw);
|
|
340
482
|
return content.parse(loaded);
|
|
341
483
|
}
|
|
342
|
-
|
|
343
|
-
|
|
484
|
+
/**
|
|
485
|
+
* get the parsed and validated `Metadata` of a metadata hash
|
|
486
|
+
*
|
|
487
|
+
* Use {@link getText | `getText`} combined with `JSON.parse` to bypass
|
|
488
|
+
* validation
|
|
489
|
+
*
|
|
490
|
+
* @param ref - a reference to the stored file, typically `"<id>.metadata"`
|
|
491
|
+
* @returns the metadata
|
|
492
|
+
*/
|
|
493
|
+
async getMetadata(ref) {
|
|
494
|
+
const raw = await this.getText(ref);
|
|
344
495
|
const loaded = JSON.parse(raw);
|
|
345
496
|
return metadata.parse(loaded);
|
|
346
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* get the parsed reMarkable lines (`.rm`) drawing of a page hash
|
|
500
|
+
*
|
|
501
|
+
* @param ref - a reference to the stored file, typically `"<id>/<pageid>.rm"`
|
|
502
|
+
* @returns the parsed page
|
|
503
|
+
*/
|
|
504
|
+
async getRm(ref) {
|
|
505
|
+
const bytes = await this.getHash(ref);
|
|
506
|
+
return parseRm(bytes);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* the same as {@link putFile | `putFile`} but rendering an `RmPage` to `.rm`
|
|
510
|
+
* bytes
|
|
511
|
+
*
|
|
512
|
+
* Only version 3 and 5 pages can be rendered; version 6 pages are read-only.
|
|
513
|
+
*/
|
|
514
|
+
async putRm(fileName, page) {
|
|
515
|
+
if (!fileName.endsWith(".rm")) {
|
|
516
|
+
throw new Error(`fileName ${fileName} did not end with '.rm'`);
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
return await this.putFile(fileName, serializeRm(page));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* update the current root hash
|
|
524
|
+
*
|
|
525
|
+
* This will fail if generation doesn't match the current server generation.
|
|
526
|
+
* This ensures that you are updating what you expect. IF you get a
|
|
527
|
+
* {@link GenerationError | `GenerationError`}, that indicates that the server
|
|
528
|
+
* was updated after you last got the generation. You should call
|
|
529
|
+
* {@link getRootHash | `getRootHash`} and then recompute the changes you want
|
|
530
|
+
* from the new root hash. If you ignore the update hash value and just call
|
|
531
|
+
* `putRootHash` again, you will overwrite the changes made by the other
|
|
532
|
+
* update.
|
|
533
|
+
*
|
|
534
|
+
* @param hash - the new root hash
|
|
535
|
+
* @param generation - the generation of the current root hash
|
|
536
|
+
* @param broadcast - [unknown] an option in the request
|
|
537
|
+
*
|
|
538
|
+
* @throws GenerationError if the generation doesn't match the current server generation
|
|
539
|
+
* @returns the new root hash and the new generation
|
|
540
|
+
*/
|
|
347
541
|
async putRootHash(hash, generation, broadcast = true) {
|
|
348
542
|
if (!Number.isSafeInteger(generation)) {
|
|
349
543
|
throw new Error(`generation ${generation} was not a safe integer`);
|
|
@@ -388,21 +582,36 @@ export class RawRemarkable {
|
|
|
388
582
|
}
|
|
389
583
|
}
|
|
390
584
|
}
|
|
391
|
-
|
|
585
|
+
/**
|
|
586
|
+
* put a raw onto the server
|
|
587
|
+
*
|
|
588
|
+
* This returns the new expeced entry of the file you uploaded, and a promise
|
|
589
|
+
* to finish the upload successful. By splitting these two operations you can
|
|
590
|
+
* start using the uploaded entry while file finishes uploading.
|
|
591
|
+
*
|
|
592
|
+
* NOTE: This won't update the state of the reMarkable until this entry is
|
|
593
|
+
* incorporated into the root hash.
|
|
594
|
+
*
|
|
595
|
+
* @param fileName - the file name to upload (e.g. `<id>.pdf`)
|
|
596
|
+
* @param bytes - the bytes to upload
|
|
597
|
+
* @returns the new entry and a promise to finish the upload
|
|
598
|
+
*/
|
|
599
|
+
async putFile(fileName, bytes) {
|
|
392
600
|
const hash = await digest(bytes);
|
|
393
601
|
const res = {
|
|
394
|
-
id,
|
|
602
|
+
id: fileName,
|
|
395
603
|
hash,
|
|
396
604
|
type: 0,
|
|
397
605
|
subfiles: 0,
|
|
398
606
|
size: bytes.length,
|
|
399
607
|
};
|
|
400
|
-
return [res, this.#putFile(
|
|
608
|
+
return [res, this.#putFile(fileName, hash, bytes)];
|
|
401
609
|
}
|
|
402
|
-
|
|
610
|
+
/** the same as {@link putFile | `putFile`} but with caching for text */
|
|
611
|
+
async putText(fileName, text) {
|
|
403
612
|
const enc = new TextEncoder();
|
|
404
613
|
const bytes = enc.encode(text);
|
|
405
|
-
const [ent, upload] = await this.putFile(
|
|
614
|
+
const [ent, upload] = await this.putFile(fileName, bytes);
|
|
406
615
|
return [
|
|
407
616
|
ent,
|
|
408
617
|
upload.then(() => {
|
|
@@ -411,22 +620,47 @@ export class RawRemarkable {
|
|
|
411
620
|
}),
|
|
412
621
|
];
|
|
413
622
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
623
|
+
/** the same as {@link putText | `putText`} but with extra validation for Content */
|
|
624
|
+
async putContent(fileName, content) {
|
|
625
|
+
if (!fileName.endsWith(".content")) {
|
|
626
|
+
throw new Error(`fileName ${fileName} did not end with '.content'`);
|
|
417
627
|
}
|
|
418
628
|
else {
|
|
419
|
-
return await this.putText(
|
|
629
|
+
return await this.putText(fileName, JSON.stringify(content));
|
|
420
630
|
}
|
|
421
631
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
632
|
+
/** the same as {@link putText | `putText`} but with extra validation for Metadata */
|
|
633
|
+
async putMetadata(fileName, metadata) {
|
|
634
|
+
if (!fileName.endsWith(".metadata")) {
|
|
635
|
+
throw new Error(`fileName ${fileName} did not end with '.metadata'`);
|
|
425
636
|
}
|
|
426
637
|
else {
|
|
427
|
-
return await this.putText(
|
|
638
|
+
return await this.putText(fileName, JSON.stringify(metadata));
|
|
428
639
|
}
|
|
429
640
|
}
|
|
641
|
+
/**
|
|
642
|
+
* put a set of entries to make an entry list file
|
|
643
|
+
*
|
|
644
|
+
* To fully upload an item:
|
|
645
|
+
* 1. upload all the constituent files and metadata
|
|
646
|
+
* 2. call this with all of the entries
|
|
647
|
+
* 3. append this entry to the root entry and call this again to update this root list
|
|
648
|
+
* 4. put the new root hash
|
|
649
|
+
*
|
|
650
|
+
* NOTE: reMarkable currently rejects newly written schema 3 root indexes
|
|
651
|
+
* with a 400 "Software must be updated" error, even for accounts that still
|
|
652
|
+
* report schema 3, so the root list should always be written as schema 4. A
|
|
653
|
+
* warning is logged if a schema 3 root index is written.
|
|
654
|
+
*
|
|
655
|
+
* @param id - the id of the list to upload - this should be the item id if
|
|
656
|
+
* uploading an item list, or "root" if uploading a new root list. Note the
|
|
657
|
+
* asymmetry with {@link getEntries | `getEntries`}: `getEntries` takes the
|
|
658
|
+
* full `"<id>.docSchema"` file name, whereas `putEntries` takes the bare id
|
|
659
|
+
* and appends `.docSchema` (and special-cases `"root"`) itself.
|
|
660
|
+
* @param entries - the entries to upload
|
|
661
|
+
*
|
|
662
|
+
* @returns the new list entry and a promise to finish the upload
|
|
663
|
+
*/
|
|
430
664
|
async putEntries(id, entries, schemaVersion) {
|
|
431
665
|
if (id === "root" && schemaVersion === 3) {
|
|
432
666
|
console.warn('writing a schema 3 root index, which reMarkable rejects with a 400 "Software must be updated" error; write the root index with schema version 4 instead');
|
|
@@ -471,6 +705,19 @@ export class RawRemarkable {
|
|
|
471
705
|
};
|
|
472
706
|
return [res, this.#putFile(`${id}.docSchema`, hash, entryBuff)];
|
|
473
707
|
}
|
|
708
|
+
/**
|
|
709
|
+
* upload a file to the reMarkable cloud using the simple api
|
|
710
|
+
*
|
|
711
|
+
* This api is the same as used by the native reMarkable extension and works
|
|
712
|
+
* even if the backend schema version is version 4. Setting mime to "folder"
|
|
713
|
+
* allows folder creation.
|
|
714
|
+
*
|
|
715
|
+
* @param visibleName - the name of the file as it should appear on the reMarkable
|
|
716
|
+
* @param bytes - the bytes of the file to upload
|
|
717
|
+
* @param mime - the mime type of the file to upload
|
|
718
|
+
|
|
719
|
+
* @returns a simple entry with the id and hash of the uploaded file
|
|
720
|
+
*/
|
|
474
721
|
async uploadFile(visibleName, bytes, mime) {
|
|
475
722
|
const enc = new TextEncoder();
|
|
476
723
|
const meta = enc
|
|
@@ -485,12 +732,18 @@ export class RawRemarkable {
|
|
|
485
732
|
},
|
|
486
733
|
});
|
|
487
734
|
const loaded = (await resp.json());
|
|
488
|
-
const { docID, hash } =
|
|
735
|
+
const { docID, hash } = nativeItemRef.parse(loaded);
|
|
489
736
|
return { id: docID, hash };
|
|
490
737
|
}
|
|
738
|
+
/**
|
|
739
|
+
* dump the current cache to a string to preserve between session
|
|
740
|
+
*
|
|
741
|
+
* @returns a serialized version of the cache to pass to a new api instance
|
|
742
|
+
*/
|
|
491
743
|
dumpCache() {
|
|
492
744
|
return JSON.stringify(Object.fromEntries(this.#cache));
|
|
493
745
|
}
|
|
746
|
+
/** completely clear the cache */
|
|
494
747
|
clearCache() {
|
|
495
748
|
this.#cache.clear();
|
|
496
749
|
}
|
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;
|