rmapi-js 12.0.3 → 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,15 +1,24 @@
1
1
  // core-js installs these only when the runtime lacks them, so node 26+ keeps
2
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";
3
6
  import "core-js/modules/es.uint8-array.from-hex.js";
4
7
  import "core-js/modules/es.uint8-array.to-base64.js";
5
8
  import "core-js/modules/es.uint8-array.to-hex.js";
6
9
  import CRC32C from "crc-32/crc32c.js";
7
10
  import { z } from "zod";
8
11
  import { ValidationError } from "./error.js";
9
- import { HEADER_LENGTH, parseV5, serializeRm, VERSION_PREFIX, } from "./rm5.js";
10
- 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";
11
14
  import { concatArrays } from "./utils.js";
12
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";
13
22
  /**
14
23
  * parse the bytes of a reMarkable `.rm` page file
15
24
  *
@@ -39,6 +48,19 @@ export function parseRm(data) {
39
48
  throw new Error(`unsupported .lines version '${versionChar}'`);
40
49
  }
41
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
+ }
42
64
  const tag = z
43
65
  .object({
44
66
  name: z.string(),
@@ -204,6 +226,33 @@ const content = z.union([
204
226
  documentContent,
205
227
  legacyDocumentContent,
206
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();
207
256
  const metadata = z
208
257
  .object({
209
258
  lastModified: z.string().optional(),
@@ -311,9 +360,10 @@ function parseRawEntryLine(line) {
311
360
  * - `<docid>.content` - a json file roughly describing document properties (see {@link DocumentContent | `DocumentContent`})
312
361
  * - `<docid>.metadata` - metadata about the document (see {@link Metadata | `Metadata`})
313
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`})
314
364
  * - `<docid>/<pageid>.rm` - [speculative] raw remarkable vectors, text, etc
315
- * - `<docid>/<pageid>-metadata.json` - [speculative] metadata about the individual page
316
- * - `<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`})
317
367
  *
318
368
  * Some items will have both a `.pdf` and `.epub` file, likely due to preparing
319
369
  * for export. Collections only have `.content` and `.metadata` files, with
@@ -322,9 +372,12 @@ function parseRawEntryLine(line) {
322
372
  * ## Caching
323
373
  *
324
374
  * Since everything is tied to the hash of it's contents, we can agressively
325
- * cache results. We assume that text contents are "small" and so fully cache
326
- * them, where as binary files we treat as large and only store that we know
327
- * 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.
328
381
  *
329
382
  * By default, this only persists as long as the api instance is alive. However,
330
383
  * for performance reasons, you should call {@link dumpCache | `dumpCache`} to
@@ -341,20 +394,21 @@ export class RawRemarkable {
341
394
  /**
342
395
  * a cache of all hashes we know exist
343
396
  *
344
- * The backend is a readonly file system of hashes to content. After a hash has
345
- * been read or written successfully, we know it exists, and potentially it's
346
- * contents. We don't want to cache large binary files, but we can cache the
347
- * small text based metadata files. For binary files we write null, so we know
348
- * 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.
349
402
  */
350
403
  #cache;
351
- constructor(authedFetch, cache, rawHost, uploadHost) {
404
+ #maxCachedBytes;
405
+ constructor(authedFetch, cache, rawHost, uploadHost, maxCachedBytes) {
352
406
  this.#authedFetch = authedFetch;
353
407
  this.#cache = cache;
354
408
  this.#rawHost = rawHost;
355
409
  this.#uploadHost = uploadHost;
410
+ this.#maxCachedBytes = maxCachedBytes;
356
411
  }
357
- /** make an authorized request to remarkable */
358
412
  /**
359
413
  * gets the root hash and the current generation
360
414
  *
@@ -400,16 +454,12 @@ export class RawRemarkable {
400
454
  async getHash({ id: fileName, hash }) {
401
455
  const cached = this.#cache.get(hash);
402
456
  if (cached != null) {
403
- const enc = new TextEncoder();
404
- return enc.encode(cached);
457
+ return cached.slice();
405
458
  }
406
459
  else {
460
+ // NOTE two simultaneous requests will fetch twice
407
461
  const res = await this.#getHash(fileName, hash);
408
- // mark that we know hash exists
409
- const cacheVal = this.#cache.get(hash);
410
- if (cacheVal === undefined) {
411
- this.#cache.set(hash, null);
412
- }
462
+ this.#cache.set(hash, res.byteLength <= this.#maxCachedBytes ? res.slice() : null);
413
463
  return res;
414
464
  }
415
465
  }
@@ -422,19 +472,8 @@ export class RawRemarkable {
422
472
  * @param ref - a reference to the stored file (see {@link getHash})
423
473
  * @returns the text
424
474
  */
425
- async getText({ id: fileName, hash }) {
426
- const cached = this.#cache.get(hash);
427
- if (cached != null) {
428
- return cached;
429
- }
430
- else {
431
- // NOTE two simultaneous requests will fetch twice
432
- const raw = await this.#getHash(fileName, hash);
433
- const dec = new TextDecoder();
434
- const res = dec.decode(raw);
435
- this.#cache.set(hash, res);
436
- return res;
437
- }
475
+ async getText(ref) {
476
+ return new TextDecoder().decode(await this.getHash(ref));
438
477
  }
439
478
  /**
440
479
  * get the entries associated with a list hash
@@ -442,12 +481,12 @@ export class RawRemarkable {
442
481
  * A list hash is the root hash, or any hash with the type 80000000. NOTE
443
482
  * these are hashed differently than files.
444
483
  *
445
- * @param ref - a reference whose `id` is `"root.docSchema"` for the root, or
446
- * `"<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
447
486
  * @returns the entries
448
487
  */
449
- async getEntries(ref) {
450
- const rawFile = await this.getText(ref);
488
+ async getEntries({ id, hash }) {
489
+ const rawFile = await this.getText({ id: `${id}.docSchema`, hash });
451
490
  const [version, ...rest] = rawFile.slice(0, -1).split("\n");
452
491
  if (version === "3") {
453
492
  return { entries: rest.map(parseRawEntryLine) };
@@ -513,11 +552,44 @@ export class RawRemarkable {
513
552
  const bytes = await this.getHash(ref);
514
553
  return parseRm(bytes);
515
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
+ }
516
589
  /**
517
590
  * the same as {@link putFile | `putFile`} but rendering an `RmPage` to `.rm`
518
591
  * bytes
519
592
  *
520
- * Only version 3 and 5 pages can be rendered; version 6 pages are read-only.
521
593
  */
522
594
  async putRm(fileName, page) {
523
595
  if (!fileName.endsWith(".rm")) {
@@ -583,67 +655,117 @@ export class RawRemarkable {
583
655
  "x-goog-hash": `crc32c=${crcHash}`,
584
656
  },
585
657
  });
586
- // mark that we know this hash exists
587
- const cacheVal = this.#cache.get(hash);
588
- if (cacheVal === undefined) {
589
- this.#cache.set(hash, null);
590
- }
658
+ this.#cache.set(hash, bytes.byteLength <= this.#maxCachedBytes ? bytes.slice() : null);
591
659
  }
592
660
  }
593
661
  /**
594
- * put a raw onto the server
662
+ * put a raw file onto the server
595
663
  *
596
- * This returns the new expeced entry of the file you uploaded, and a promise
597
- * to finish the upload successful. By splitting these two operations you can
598
- * 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`}.
599
667
  *
600
668
  * NOTE: This won't update the state of the reMarkable until this entry is
601
669
  * incorporated into the root hash.
602
670
  *
603
671
  * @param fileName - the file name to upload (e.g. `<id>.pdf`)
604
672
  * @param bytes - the bytes to upload
605
- * @returns the new entry and a promise to finish the upload
673
+ * @returns the new entry, pending its upload
606
674
  */
607
675
  async putFile(fileName, bytes) {
608
676
  const hash = await digest(bytes);
609
- 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 {
610
682
  id: fileName,
611
683
  hash,
612
684
  type: 0,
613
685
  subfiles: 0,
614
686
  size: bytes.length,
687
+ async [Symbol.asyncDispose]() {
688
+ await upload;
689
+ },
615
690
  };
616
- return [res, this.#putFile(fileName, hash, bytes)];
617
691
  }
618
- /** the same as {@link putFile | `putFile`} but with caching for text */
619
- async putText(fileName, text) {
620
- const enc = new TextEncoder();
621
- const bytes = enc.encode(text);
622
- const [ent, upload] = await this.putFile(fileName, bytes);
623
- return [
624
- ent,
625
- upload.then(() => {
626
- // on success, write to cache
627
- this.#cache.set(ent.hash, text);
628
- }),
629
- ];
630
- }
631
- /** 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 */
632
696
  async putContent(fileName, content) {
633
697
  if (!fileName.endsWith(".content")) {
634
698
  throw new Error(`fileName ${fileName} did not end with '.content'`);
635
699
  }
636
700
  else {
637
- 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`);
638
716
  }
639
717
  }
640
- /** 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 */
641
744
  async putMetadata(fileName, metadata) {
642
745
  if (!fileName.endsWith(".metadata")) {
643
746
  throw new Error(`fileName ${fileName} did not end with '.metadata'`);
644
747
  }
645
748
  else {
646
- 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 }));
647
769
  }
648
770
  }
649
771
  /**
@@ -663,11 +785,9 @@ export class RawRemarkable {
663
785
  * @param id - the id of the list to upload - this should be the item id if
664
786
  * uploading an item list, or "root" if uploading a new root list. Note the
665
787
  * asymmetry with {@link getEntries | `getEntries`}: `getEntries` takes the
666
- * full `"<id>.docSchema"` file name, whereas `putEntries` takes the bare id
667
- * and appends `.docSchema` (and special-cases `"root"`) itself.
668
788
  * @param entries - the entries to upload
669
789
  *
670
- * @returns the new list entry and a promise to finish the upload
790
+ * @returns the new list entry, pending its upload
671
791
  */
672
792
  async putEntries(id, entries, schemaVersion) {
673
793
  if (id === "root" && schemaVersion === 3) {
@@ -704,14 +824,18 @@ export class RawRemarkable {
704
824
  else {
705
825
  throw new Error(`unsupported schema version ${schemaVersion}`);
706
826
  }
707
- const res = {
827
+ const upload = this.#putFile(`${id}.docSchema`, hash, entryBuff);
828
+ upload.catch(() => { });
829
+ return {
708
830
  id,
709
831
  hash,
710
832
  type: schemaVersion > 3 ? 0 : 80000000,
711
833
  subfiles: sorted.length,
712
834
  size,
835
+ async [Symbol.asyncDispose]() {
836
+ await upload;
837
+ },
713
838
  };
714
- return [res, this.#putFile(`${id}.docSchema`, hash, entryBuff)];
715
839
  }
716
840
  /**
717
841
  * upload a file to the reMarkable cloud using the simple api
@@ -749,7 +873,23 @@ export class RawRemarkable {
749
873
  * @returns a serialized version of the cache to pass to a new api instance
750
874
  */
751
875
  dumpCache() {
752
- 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 });
753
893
  }
754
894
  /** completely clear the cache */
755
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 {};