rmapi-js 12.0.2 → 13.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/dist/raw.js CHANGED
@@ -1,10 +1,24 @@
1
- import CRC32C from "crc-32/crc32c";
1
+ // core-js installs these only when the runtime lacks them, so node 26+ keeps
2
+ // its native implementations untouched
3
+ import "core-js/modules/es.async-disposable-stack.constructor.js";
4
+ import "core-js/modules/es.symbol.async-dispose.js";
5
+ import "core-js/modules/es.uint8-array.from-base64.js";
6
+ import "core-js/modules/es.uint8-array.from-hex.js";
7
+ import "core-js/modules/es.uint8-array.to-base64.js";
8
+ import "core-js/modules/es.uint8-array.to-hex.js";
9
+ import CRC32C from "crc-32/crc32c.js";
2
10
  import { z } from "zod";
3
11
  import { ValidationError } from "./error.js";
4
- import { HEADER_LENGTH, parseV5, serializeRm, VERSION_PREFIX, } from "./rm5.js";
5
- import { parseRmScene } from "./rm6.js";
12
+ import { HEADER_LENGTH, parseV5, serializeRm as serializeV5, VERSION_PREFIX, } from "./rm5.js";
13
+ import { parseRmScene, serializeRmScene } from "./rm6.js";
6
14
  import { concatArrays } from "./utils.js";
7
15
  const hashReg = /^[0-9a-f]{64}$/;
16
+ /** the dump format version; absent means the original text-only format */
17
+ export const CACHE_VERSION = 2;
18
+ /** marks a dumped cache entry stored as utf-8 text */
19
+ export const TEXT_PREFIX = "t";
20
+ /** marks a dumped cache entry stored as base64 */
21
+ export const BYTES_PREFIX = "b";
8
22
  /**
9
23
  * parse the bytes of a reMarkable `.rm` page file
10
24
  *
@@ -34,6 +48,19 @@ export function parseRm(data) {
34
48
  throw new Error(`unsupported .lines version '${versionChar}'`);
35
49
  }
36
50
  }
51
+ /**
52
+ * serialize a parsed reMarkable `.rm` page back to bytes
53
+ *
54
+ * The inverse of {@link parseRm | `parseRm`}: versions 3 and 5 render from a
55
+ * flat {@link RmPageV5 | `RmPageV5`}, version 6 from an
56
+ * {@link RmScene | `RmScene`}.
57
+ *
58
+ * @param page - the page to render
59
+ * @returns the `.rm` file bytes
60
+ */
61
+ export function serializeRm(page) {
62
+ return page.version === 6 ? serializeRmScene(page) : serializeV5(page);
63
+ }
37
64
  const tag = z
38
65
  .object({
39
66
  name: z.string(),
@@ -199,6 +226,33 @@ const content = z.union([
199
226
  documentContent,
200
227
  legacyDocumentContent,
201
228
  ]);
229
+ const highlightsReg = /\.highlights\/[^/]+\.json$/;
230
+ const highlightsFile = z
231
+ .object({
232
+ highlights: z.array(z.array(z
233
+ .object({
234
+ text: z.string(),
235
+ color: z.number().int(),
236
+ start: z.number().int(),
237
+ length: z.number().int(),
238
+ rects: z.array(z
239
+ .object({
240
+ x: z.number(),
241
+ y: z.number(),
242
+ width: z.number(),
243
+ height: z.number(),
244
+ })
245
+ .passthrough()),
246
+ })
247
+ .passthrough())),
248
+ })
249
+ .passthrough();
250
+ const pageMetadataReg = /\/[^/]+-metadata\.json$/;
251
+ const pageMetadata = z
252
+ .object({
253
+ layers: z.array(z.object({ name: z.string() }).passthrough()),
254
+ })
255
+ .passthrough();
202
256
  const metadata = z
203
257
  .object({
204
258
  lastModified: z.string().optional(),
@@ -306,9 +360,10 @@ function parseRawEntryLine(line) {
306
360
  * - `<docid>.content` - a json file roughly describing document properties (see {@link DocumentContent | `DocumentContent`})
307
361
  * - `<docid>.metadata` - metadata about the document (see {@link Metadata | `Metadata`})
308
362
  * - `<docid>.pagedata` - a text file where each line is the template of that page
363
+ * - `<docid>.template` - a template attached to the item (see {@link TemplateContent | `TemplateContent`})
309
364
  * - `<docid>/<pageid>.rm` - [speculative] raw remarkable vectors, text, etc
310
- * - `<docid>/<pageid>-metadata.json` - [speculative] metadata about the individual page
311
- * - `<docid>.highlights/<pageid>.json` - [speculative] highlights on the page
365
+ * - `<docid>/<pageid>-metadata.json` - page layer metadata (see {@link PageMetadata | `PageMetadata`})
366
+ * - `<docid>.highlights/<pageid>.json` - text highlights on the page (see {@link Highlight | `Highlight`})
312
367
  *
313
368
  * Some items will have both a `.pdf` and `.epub` file, likely due to preparing
314
369
  * for export. Collections only have `.content` and `.metadata` files, with
@@ -317,9 +372,12 @@ function parseRawEntryLine(line) {
317
372
  * ## Caching
318
373
  *
319
374
  * Since everything is tied to the hash of it's contents, we can agressively
320
- * cache results. We assume that text contents are "small" and so fully cache
321
- * them, where as binary files we treat as large and only store that we know
322
- * they exist to prevent future writes.
375
+ * cache results. Anything up to `maxCachedBytes` is kept as the bytes that were
376
+ * transferred; anything larger records only that the hash exists, which is
377
+ * enough to skip writing it again.
378
+ *
379
+ * A dump is tied to the account it came from, since a known hash is taken as
380
+ * proof the server already holds that file. Don't share one between accounts.
323
381
  *
324
382
  * By default, this only persists as long as the api instance is alive. However,
325
383
  * for performance reasons, you should call {@link dumpCache | `dumpCache`} to
@@ -336,20 +394,21 @@ export class RawRemarkable {
336
394
  /**
337
395
  * a cache of all hashes we know exist
338
396
  *
339
- * The backend is a readonly file system of hashes to content. After a hash has
340
- * been read or written successfully, we know it exists, and potentially it's
341
- * contents. We don't want to cache large binary files, but we can cache the
342
- * small text based metadata files. For binary files we write null, so we know
343
- * not to write a a cached value again, but we'll still need to read it.
397
+ * The backend is a readonly file system of hashes to content, so once a hash
398
+ * has been read or written successfully we know it exists, and often what it
399
+ * holds. Contents are kept as the bytes that were transferred; anything over
400
+ * `maxCachedBytes` is stored as null, recording existence without the
401
+ * payload.
344
402
  */
345
403
  #cache;
346
- constructor(authedFetch, cache, rawHost, uploadHost) {
404
+ #maxCachedBytes;
405
+ constructor(authedFetch, cache, rawHost, uploadHost, maxCachedBytes) {
347
406
  this.#authedFetch = authedFetch;
348
407
  this.#cache = cache;
349
408
  this.#rawHost = rawHost;
350
409
  this.#uploadHost = uploadHost;
410
+ this.#maxCachedBytes = maxCachedBytes;
351
411
  }
352
- /** make an authorized request to remarkable */
353
412
  /**
354
413
  * gets the root hash and the current generation
355
414
  *
@@ -395,16 +454,12 @@ export class RawRemarkable {
395
454
  async getHash({ id: fileName, hash }) {
396
455
  const cached = this.#cache.get(hash);
397
456
  if (cached != null) {
398
- const enc = new TextEncoder();
399
- return enc.encode(cached);
457
+ return cached.slice();
400
458
  }
401
459
  else {
460
+ // NOTE two simultaneous requests will fetch twice
402
461
  const res = await this.#getHash(fileName, hash);
403
- // mark that we know hash exists
404
- const cacheVal = this.#cache.get(hash);
405
- if (cacheVal === undefined) {
406
- this.#cache.set(hash, null);
407
- }
462
+ this.#cache.set(hash, res.byteLength <= this.#maxCachedBytes ? res.slice() : null);
408
463
  return res;
409
464
  }
410
465
  }
@@ -417,19 +472,8 @@ export class RawRemarkable {
417
472
  * @param ref - a reference to the stored file (see {@link getHash})
418
473
  * @returns the text
419
474
  */
420
- async getText({ id: fileName, hash }) {
421
- const cached = this.#cache.get(hash);
422
- if (cached != null) {
423
- return cached;
424
- }
425
- else {
426
- // NOTE two simultaneous requests will fetch twice
427
- const raw = await this.#getHash(fileName, hash);
428
- const dec = new TextDecoder();
429
- const res = dec.decode(raw);
430
- this.#cache.set(hash, res);
431
- return res;
432
- }
475
+ async getText(ref) {
476
+ return new TextDecoder().decode(await this.getHash(ref));
433
477
  }
434
478
  /**
435
479
  * get the entries associated with a list hash
@@ -437,12 +481,12 @@ export class RawRemarkable {
437
481
  * A list hash is the root hash, or any hash with the type 80000000. NOTE
438
482
  * these are hashed differently than files.
439
483
  *
440
- * @param ref - a reference whose `id` is `"root.docSchema"` for the root, or
441
- * `"<id>.docSchema"` for a sub-document's entry index
484
+ * @param ref - a reference whose `id` is the bare document id, or `"root"`
485
+ * for the root index
442
486
  * @returns the entries
443
487
  */
444
- async getEntries(ref) {
445
- const rawFile = await this.getText(ref);
488
+ async getEntries({ id, hash }) {
489
+ const rawFile = await this.getText({ id: `${id}.docSchema`, hash });
446
490
  const [version, ...rest] = rawFile.slice(0, -1).split("\n");
447
491
  if (version === "3") {
448
492
  return { entries: rest.map(parseRawEntryLine) };
@@ -508,11 +552,44 @@ export class RawRemarkable {
508
552
  const bytes = await this.getHash(ref);
509
553
  return parseRm(bytes);
510
554
  }
555
+ /**
556
+ * get the parsed text highlights of a page
557
+ *
558
+ * @param ref - a reference to a `<docid>.highlights/<pageid>.json` file
559
+ * @returns the page's highlights; [speculative] each inner array groups the
560
+ * fragments of one highlighted passage
561
+ */
562
+ async getHighlights(ref) {
563
+ const raw = await this.getText(ref);
564
+ const loaded = JSON.parse(raw);
565
+ return highlightsFile.parse(loaded).highlights;
566
+ }
567
+ /**
568
+ * get a template stored as a `<docid>.template` sidecar file
569
+ *
570
+ * @param ref - a reference to a `<docid>.template` file
571
+ * @returns the template content
572
+ */
573
+ async getTemplate(ref) {
574
+ const raw = await this.getText(ref);
575
+ const loaded = JSON.parse(raw);
576
+ return templateContent.parse(loaded);
577
+ }
578
+ /**
579
+ * get the parsed layer metadata of a page
580
+ *
581
+ * @param ref - a reference to a `<docid>/<pageid>-metadata.json` file
582
+ * @returns the page metadata
583
+ */
584
+ async getPageMetadata(ref) {
585
+ const raw = await this.getText(ref);
586
+ const loaded = JSON.parse(raw);
587
+ return pageMetadata.parse(loaded);
588
+ }
511
589
  /**
512
590
  * the same as {@link putFile | `putFile`} but rendering an `RmPage` to `.rm`
513
591
  * bytes
514
592
  *
515
- * Only version 3 and 5 pages can be rendered; version 6 pages are read-only.
516
593
  */
517
594
  async putRm(fileName, page) {
518
595
  if (!fileName.endsWith(".rm")) {
@@ -578,67 +655,117 @@ export class RawRemarkable {
578
655
  "x-goog-hash": `crc32c=${crcHash}`,
579
656
  },
580
657
  });
581
- // mark that we know this hash exists
582
- const cacheVal = this.#cache.get(hash);
583
- if (cacheVal === undefined) {
584
- this.#cache.set(hash, null);
585
- }
658
+ this.#cache.set(hash, bytes.byteLength <= this.#maxCachedBytes ? bytes.slice() : null);
586
659
  }
587
660
  }
588
661
  /**
589
- * put a raw onto the server
662
+ * put a raw file onto the server
590
663
  *
591
- * This returns the new expeced entry of the file you uploaded, and a promise
592
- * to finish the upload successful. By splitting these two operations you can
593
- * start using the uploaded entry while file finishes uploading.
664
+ * The returned entry is usable immediately, while the upload is still in
665
+ * flight; disposing it waits for the upload to finish. See
666
+ * {@link PendingEntry | `PendingEntry`}.
594
667
  *
595
668
  * NOTE: This won't update the state of the reMarkable until this entry is
596
669
  * incorporated into the root hash.
597
670
  *
598
671
  * @param fileName - the file name to upload (e.g. `<id>.pdf`)
599
672
  * @param bytes - the bytes to upload
600
- * @returns the new entry and a promise to finish the upload
673
+ * @returns the new entry, pending its upload
601
674
  */
602
675
  async putFile(fileName, bytes) {
603
676
  const hash = await digest(bytes);
604
- const res = {
677
+ const upload = this.#putFile(fileName, hash, bytes);
678
+ // an upload that fails before anyone disposes the entry would otherwise be
679
+ // an unhandled rejection; disposing re-awaits it and still throws
680
+ upload.catch(() => { });
681
+ return {
605
682
  id: fileName,
606
683
  hash,
607
684
  type: 0,
608
685
  subfiles: 0,
609
686
  size: bytes.length,
687
+ async [Symbol.asyncDispose]() {
688
+ await upload;
689
+ },
610
690
  };
611
- return [res, this.#putFile(fileName, hash, bytes)];
612
691
  }
613
- /** the same as {@link putFile | `putFile`} but with caching for text */
614
- async putText(fileName, text) {
615
- const enc = new TextEncoder();
616
- const bytes = enc.encode(text);
617
- const [ent, upload] = await this.putFile(fileName, bytes);
618
- return [
619
- ent,
620
- upload.then(() => {
621
- // on success, write to cache
622
- this.#cache.set(ent.hash, text);
623
- }),
624
- ];
625
- }
626
- /** the same as {@link putText | `putText`} but with extra validation for Content */
692
+ async #putEncoded(fileName, text) {
693
+ return await this.putFile(fileName, new TextEncoder().encode(text));
694
+ }
695
+ /** the same as {@link putFile | `putFile`} but with extra validation for Content */
627
696
  async putContent(fileName, content) {
628
697
  if (!fileName.endsWith(".content")) {
629
698
  throw new Error(`fileName ${fileName} did not end with '.content'`);
630
699
  }
631
700
  else {
632
- return await this.putText(fileName, JSON.stringify(content));
701
+ return await this.#putEncoded(fileName, JSON.stringify(content));
702
+ }
703
+ }
704
+ /**
705
+ * the same as {@link putFile | `putFile`} but for a `.pagedata` file
706
+ *
707
+ * @param fileName - the file to write, of the form `<docid>.pagedata`
708
+ * @param templates - one template name per page, in page order
709
+ */
710
+ async putPagedata(fileName, templates) {
711
+ if (!fileName.endsWith(".pagedata")) {
712
+ throw new Error(`fileName ${fileName} did not end with '.pagedata'`);
713
+ }
714
+ else {
715
+ return await this.#putEncoded(fileName, `${templates.join("\n")}\n`);
633
716
  }
634
717
  }
635
- /** the same as {@link putText | `putText`} but with extra validation for Metadata */
718
+ /**
719
+ * the same as {@link putFile | `putFile`} but with extra validation for page
720
+ * layer metadata
721
+ *
722
+ * @param fileName - the file to write, of the form
723
+ * `<docid>/<pageid>-metadata.json`
724
+ * @param meta - the page's layer metadata
725
+ */
726
+ async putPageMetadata(fileName, meta) {
727
+ if (!pageMetadataReg.test(fileName)) {
728
+ throw new ValidationError(fileName, pageMetadataReg, "fileName was not of the form '<docid>/<pageid>-metadata.json'");
729
+ }
730
+ else {
731
+ return await this.#putEncoded(fileName, JSON.stringify(meta));
732
+ }
733
+ }
734
+ /** the same as {@link putFile | `putFile`} but with extra validation for a template sidecar */
735
+ async putTemplate(fileName, template) {
736
+ if (!fileName.endsWith(".template")) {
737
+ throw new Error(`fileName ${fileName} did not end with '.template'`);
738
+ }
739
+ else {
740
+ return await this.#putEncoded(fileName, JSON.stringify(template));
741
+ }
742
+ }
743
+ /** the same as {@link putFile | `putFile`} but with extra validation for Metadata */
636
744
  async putMetadata(fileName, metadata) {
637
745
  if (!fileName.endsWith(".metadata")) {
638
746
  throw new Error(`fileName ${fileName} did not end with '.metadata'`);
639
747
  }
640
748
  else {
641
- return await this.putText(fileName, JSON.stringify(metadata));
749
+ return await this.#putEncoded(fileName, JSON.stringify(metadata));
750
+ }
751
+ }
752
+ /**
753
+ * the same as {@link putFile | `putFile`} but with extra validation for
754
+ * highlights
755
+ *
756
+ * Rewraps the array in the file's `highlights` envelope, which
757
+ * {@link getHighlights | `getHighlights`} strips.
758
+ *
759
+ * @param fileName - the file to write, of the form
760
+ * `<docid>.highlights/<pageid>.json`
761
+ * @param highlights - the page's highlights
762
+ */
763
+ async putHighlights(fileName, highlights) {
764
+ if (!highlightsReg.test(fileName)) {
765
+ throw new ValidationError(fileName, highlightsReg, "fileName was not of the form '<docid>.highlights/<pageid>.json'");
766
+ }
767
+ else {
768
+ return await this.#putEncoded(fileName, JSON.stringify({ highlights }));
642
769
  }
643
770
  }
644
771
  /**
@@ -658,11 +785,9 @@ export class RawRemarkable {
658
785
  * @param id - the id of the list to upload - this should be the item id if
659
786
  * uploading an item list, or "root" if uploading a new root list. Note the
660
787
  * asymmetry with {@link getEntries | `getEntries`}: `getEntries` takes the
661
- * full `"<id>.docSchema"` file name, whereas `putEntries` takes the bare id
662
- * and appends `.docSchema` (and special-cases `"root"`) itself.
663
788
  * @param entries - the entries to upload
664
789
  *
665
- * @returns the new list entry and a promise to finish the upload
790
+ * @returns the new list entry, pending its upload
666
791
  */
667
792
  async putEntries(id, entries, schemaVersion) {
668
793
  if (id === "root" && schemaVersion === 3) {
@@ -699,14 +824,18 @@ export class RawRemarkable {
699
824
  else {
700
825
  throw new Error(`unsupported schema version ${schemaVersion}`);
701
826
  }
702
- const res = {
827
+ const upload = this.#putFile(`${id}.docSchema`, hash, entryBuff);
828
+ upload.catch(() => { });
829
+ return {
703
830
  id,
704
831
  hash,
705
832
  type: schemaVersion > 3 ? 0 : 80000000,
706
833
  subfiles: sorted.length,
707
834
  size,
835
+ async [Symbol.asyncDispose]() {
836
+ await upload;
837
+ },
708
838
  };
709
- return [res, this.#putFile(`${id}.docSchema`, hash, entryBuff)];
710
839
  }
711
840
  /**
712
841
  * upload a file to the reMarkable cloud using the simple api
@@ -744,7 +873,23 @@ export class RawRemarkable {
744
873
  * @returns a serialized version of the cache to pass to a new api instance
745
874
  */
746
875
  dumpCache() {
747
- return JSON.stringify(Object.fromEntries(this.#cache));
876
+ const entries = {};
877
+ // utf-8 text is stored as itself; base64 would cost a third more for what
878
+ // is mostly json
879
+ const dec = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
880
+ for (const [hash, value] of this.#cache) {
881
+ if (value === null) {
882
+ entries[hash] = null;
883
+ continue;
884
+ }
885
+ try {
886
+ entries[hash] = `${TEXT_PREFIX}${dec.decode(value)}`;
887
+ }
888
+ catch {
889
+ entries[hash] = `${BYTES_PREFIX}${value.toBase64()}`;
890
+ }
891
+ }
892
+ return JSON.stringify({ version: CACHE_VERSION, entries });
748
893
  }
749
894
  /** completely clear the cache */
750
895
  clearCache() {
package/dist/rm5.d.ts CHANGED
@@ -62,7 +62,7 @@ export interface RmLayer {
62
62
  * a parsed version 3 or 5 page
63
63
  *
64
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`).
65
+ * in device pixels.
66
66
  */
67
67
  export interface RmPageV5 {
68
68
  /** the file format version */
package/dist/rm6.d.ts CHANGED
@@ -5,8 +5,9 @@
5
5
  * of length-prefixed, tagged blocks. This module reads every block faithfully
6
6
  * — preserving `CrdtId`s, `LwwValue` wrappers, and each block's unread tail —
7
7
  * into an {@link RmScene | `RmScene`}, whose methods resolve the CRDT into
8
- * ordered layers, strokes, and text. Because nothing is dropped, the blocks
9
- * round-trip back to bytes.
8
+ * ordered layers, strokes, and text. Nothing is dropped, so
9
+ * {@link serializeRmScene | `serializeRmScene`} reproduces the original bytes
10
+ * exactly, including blocks it could not parse.
10
11
  *
11
12
  * @packageDocumentation
12
13
  */
@@ -44,7 +45,7 @@ export interface RmV6Point {
44
45
  y: number;
45
46
  /** pen speed */
46
47
  speed: number;
47
- /** stroke width */
48
+ /** stroke width; integral for version 2 points, fractional for version 1 */
48
49
  width: number;
49
50
  /** pen direction, 0–255 mapping onto 0–2π */
50
51
  direction: number;
@@ -63,6 +64,10 @@ export interface RmV6Line {
63
64
  startingLength: number;
64
65
  /** the sampled points */
65
66
  points: RmV6Point[];
67
+ /** the stroke's timestamp id, if the file carried one */
68
+ timestampId?: CrdtId;
69
+ /** the stroke's move id, if the file carried one */
70
+ moveId?: CrdtId;
66
71
  /**
67
72
  * a packed little-endian uint32 color, only present for highlighter strokes
68
73
  *
@@ -128,6 +133,12 @@ export interface RmV6Text {
128
133
  }
129
134
  /** fields carried by every block */
130
135
  interface BlockCommon {
136
+ /**
137
+ * the header byte between the block length and the versions
138
+ *
139
+ * Zero in every well-formed file seen, but preserved rather than assumed.
140
+ */
141
+ reserved: number;
131
142
  /** the block's minimum reader version */
132
143
  minVersion: number;
133
144
  /** the block's current version (selects the point encoding for lines) */
@@ -167,14 +178,19 @@ export interface TreeNodeBlock extends BlockCommon {
167
178
  label: LwwValue<string>;
168
179
  /** whether the layer is visible */
169
180
  visible: LwwValue<boolean>;
170
- /** the anchor node's crdt id, if anchored */
171
- anchorId?: LwwValue<CrdtId>;
172
- /** the anchor type, if anchored */
173
- anchorType?: LwwValue<number>;
174
- /** the anchor threshold, if anchored */
175
- anchorThreshold?: LwwValue<number>;
176
- /** the anchor's horizontal origin, if anchored */
177
- anchorOriginX?: LwwValue<number>;
181
+ /** where the node is anchored, when it is */
182
+ anchor?: TreeNodeAnchor;
183
+ }
184
+ /** the four values a tree node carries when it's anchored, always together */
185
+ export interface TreeNodeAnchor {
186
+ /** the anchor node's crdt id */
187
+ id: LwwValue<CrdtId>;
188
+ /** the anchor type */
189
+ type: LwwValue<number>;
190
+ /** the anchor threshold */
191
+ threshold: LwwValue<number>;
192
+ /** the anchor's horizontal origin */
193
+ originX: LwwValue<number>;
178
194
  }
179
195
  /** the `0x05` scene line item block — a stroke */
180
196
  export interface SceneLineItemBlock extends BlockCommon {
@@ -242,8 +258,8 @@ export interface PageInfoBlock extends BlockCommon {
242
258
  textCharsCount: number;
243
259
  /** the number of text lines on the page */
244
260
  textLinesCount: number;
245
- /** the type-folio use count */
246
- typeFolioUseCount: number;
261
+ /** the type-folio use count, if the block carried one */
262
+ typeFolioUseCount?: number;
247
263
  }
248
264
  /** the `0x09` author ids block — the author-id to uuid table */
249
265
  export interface AuthorIdsBlock extends BlockCommon {
@@ -273,6 +289,13 @@ export interface UnknownBlock extends BlockCommon {
273
289
  blockType: number;
274
290
  /** the raw block body bytes */
275
291
  data: Uint8Array;
292
+ /**
293
+ * the length the block header declared, when it overran the file
294
+ *
295
+ * Corrupt files exist in the wild; keeping the original figure lets them
296
+ * round-trip byte for byte instead of being silently rewritten.
297
+ */
298
+ declaredLength?: number;
276
299
  }
277
300
  /** any parsed version 6 block */
278
301
  export type RmBlock = MigrationInfoBlock | SceneTreeBlock | TreeNodeBlock | SceneLineItemBlock | SceneGlyphItemBlock | SceneGroupItemBlock | SceneTextItemBlock | SceneTombstoneItemBlock | RootTextBlock | PageInfoBlock | AuthorIdsBlock | SceneInfoBlock | UnknownBlock;
@@ -329,6 +352,8 @@ export declare class RmScene {
329
352
  /** the page's document text, if any */
330
353
  text(): RmV6Text | undefined;
331
354
  }
355
+ /** serialize a parsed scene back to version 6 `.rm` bytes */
356
+ export declare function serializeRmScene(scene: RmScene): Uint8Array;
332
357
  /** parse a version 6 `.rm` file into a resolvable scene */
333
358
  export declare function parseRmScene(data: Uint8Array): RmScene;
334
359
  export {};