rmapi-js 12.0.3 → 14.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/rm6.js CHANGED
@@ -5,11 +5,13 @@
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
  */
14
+ import { rmBrushCode, rmColorCode, } from "./codes.js";
13
15
  /** the scene-tree root node id */
14
16
  export const ROOT_ID = { authorId: 0, counter: 1 };
15
17
  /** the CRDT sequence end marker / unset id */
@@ -146,7 +148,11 @@ class Reader {
146
148
  const subEnd = this.#offset + length;
147
149
  this.#bounds.push(subEnd);
148
150
  try {
149
- return fn();
151
+ const value = fn();
152
+ if (this.#offset !== subEnd) {
153
+ throw new Error(`subblock ${index} left ${subEnd - this.#offset} bytes unread`);
154
+ }
155
+ return value;
150
156
  }
151
157
  finally {
152
158
  this.#bounds.pop();
@@ -201,6 +207,125 @@ class Reader {
201
207
  });
202
208
  }
203
209
  }
210
+ /** the tagged-write counterpart to {@link Reader | `Reader`} */
211
+ class Writer {
212
+ #buffer = new Uint8Array(4096);
213
+ #view = new DataView(this.#buffer.buffer);
214
+ #length = 0;
215
+ #ensure(extra) {
216
+ if (this.#length + extra <= this.#buffer.length)
217
+ return;
218
+ let size = this.#buffer.length;
219
+ while (size < this.#length + extra)
220
+ size *= 2;
221
+ const grown = new Uint8Array(size);
222
+ grown.set(this.#buffer);
223
+ this.#buffer = grown;
224
+ this.#view = new DataView(grown.buffer);
225
+ }
226
+ get length() {
227
+ return this.#length;
228
+ }
229
+ u8(value) {
230
+ this.#ensure(1);
231
+ this.#view.setUint8(this.#length, value);
232
+ this.#length += 1;
233
+ }
234
+ u16(value) {
235
+ this.#ensure(2);
236
+ this.#view.setUint16(this.#length, value, true);
237
+ this.#length += 2;
238
+ }
239
+ u32(value) {
240
+ this.#ensure(4);
241
+ this.#view.setUint32(this.#length, value, true);
242
+ this.#length += 4;
243
+ }
244
+ f32(value) {
245
+ this.#ensure(4);
246
+ this.#view.setFloat32(this.#length, value, true);
247
+ this.#length += 4;
248
+ }
249
+ f64(value) {
250
+ this.#ensure(8);
251
+ this.#view.setFloat64(this.#length, value, true);
252
+ this.#length += 8;
253
+ }
254
+ varuint(value) {
255
+ let rest = value;
256
+ do {
257
+ let byte = rest % 128;
258
+ rest = Math.floor(rest / 128);
259
+ if (rest > 0)
260
+ byte |= 0x80;
261
+ this.u8(byte);
262
+ } while (rest > 0);
263
+ }
264
+ crdtId(id) {
265
+ this.u8(id.authorId);
266
+ this.varuint(id.counter);
267
+ }
268
+ bytes(data) {
269
+ this.#ensure(data.length);
270
+ this.#buffer.set(data, this.#length);
271
+ this.#length += data.length;
272
+ }
273
+ patchU32(at, value) {
274
+ this.#view.setUint32(at, value, true);
275
+ }
276
+ finish() {
277
+ return this.#buffer.slice(0, this.#length);
278
+ }
279
+ tag(index, type) {
280
+ this.varuint(index * 16 + type);
281
+ }
282
+ writeInt(index, value) {
283
+ this.tag(index, TAG_BYTE4);
284
+ this.u32(value);
285
+ }
286
+ writeFloat(index, value) {
287
+ this.tag(index, TAG_BYTE4);
288
+ this.f32(value);
289
+ }
290
+ writeDouble(index, value) {
291
+ this.tag(index, TAG_BYTE8);
292
+ this.f64(value);
293
+ }
294
+ writeId(index, id) {
295
+ this.tag(index, TAG_ID);
296
+ this.crdtId(id);
297
+ }
298
+ writeBool(index, value) {
299
+ this.tag(index, TAG_BYTE1);
300
+ this.u8(value ? 1 : 0);
301
+ }
302
+ writeByte(index, value) {
303
+ this.tag(index, TAG_BYTE1);
304
+ this.u8(value);
305
+ }
306
+ subblock(index, write) {
307
+ this.tag(index, TAG_LENGTH4);
308
+ const at = this.length;
309
+ this.u32(0);
310
+ write();
311
+ this.patchU32(at, this.length - at - 4);
312
+ }
313
+ writeLww(index, lww, write) {
314
+ this.subblock(index, () => {
315
+ this.writeId(1, lww.timestamp);
316
+ write(lww.value);
317
+ });
318
+ }
319
+ writeString(index, text) {
320
+ this.subblock(index, () => {
321
+ const encoded = new TextEncoder().encode(text);
322
+ this.varuint(encoded.length);
323
+ // the device writes this flag as 1 even for non-ascii text
324
+ this.u8(1);
325
+ this.bytes(encoded);
326
+ });
327
+ }
328
+ }
204
329
  /** read the `SceneItem` envelope shared by item blocks */
205
330
  function readItemEnvelope(reader, readValue) {
206
331
  const parentId = reader.readId(1);
@@ -218,8 +343,8 @@ function readItemEnvelope(reader, readValue) {
218
343
  return { parentId, item: { itemId, leftId, rightId, deletedLength, value } };
219
344
  }
220
345
  function readLineValue(reader, version) {
221
- const tool = reader.readInt(1);
222
- const color = reader.readInt(2);
346
+ const tool = rmBrushCode.parse(reader.readInt(1));
347
+ const color = rmColorCode.parse(reader.readInt(2));
223
348
  const thicknessScale = reader.readDouble(3);
224
349
  const startingLength = reader.readFloat(4);
225
350
  const points = reader.subblock(5, () => {
@@ -236,7 +361,7 @@ function readLineValue(reader, version) {
236
361
  y,
237
362
  speed: reader.f32() * 4,
238
363
  direction: (reader.f32() * 255) / (2 * Math.PI),
239
- width: Math.round(reader.f32() * 4),
364
+ width: reader.f32() * 4,
240
365
  pressure: reader.f32() * 255,
241
366
  };
242
367
  }
@@ -257,12 +382,10 @@ function readLineValue(reader, version) {
257
382
  startingLength,
258
383
  points,
259
384
  };
260
- // optional trailing timestamp / move id / color are skipped by seeking to the
261
- // subblock end; only the highlighter rgba color is captured
262
385
  if (reader.hasTag(6, TAG_ID))
263
- reader.readId(6);
386
+ line.timestampId = reader.readId(6);
264
387
  if (reader.hasTag(7, TAG_ID))
265
- reader.readId(7);
388
+ line.moveId = reader.readId(7);
266
389
  if (reader.hasTag(8, TAG_BYTE4))
267
390
  line.colorRgba = reader.readInt(8);
268
391
  return line;
@@ -272,7 +395,7 @@ function readGlyphValue(reader) {
272
395
  const explicitLength = reader.hasTag(3, TAG_BYTE4)
273
396
  ? reader.readInt(3)
274
397
  : undefined;
275
- const color = reader.readInt(4);
398
+ const color = rmColorCode.parse(reader.readInt(4));
276
399
  const text = reader.readString(5);
277
400
  const rectangles = reader.subblock(6, () => {
278
401
  const count = reader.varuint();
@@ -367,10 +490,12 @@ function readBlockBody(reader, blockType, version) {
367
490
  visible,
368
491
  };
369
492
  if (reader.bytesRemaining() > 0 && reader.hasTag(7, TAG_LENGTH4)) {
370
- node.anchorId = reader.readLww(7, () => reader.readId(2));
371
- node.anchorType = reader.readLww(8, () => reader.readByte(2));
372
- node.anchorThreshold = reader.readLww(9, () => reader.readFloat(2));
373
- node.anchorOriginX = reader.readLww(10, () => reader.readFloat(2));
493
+ node.anchor = {
494
+ id: reader.readLww(7, () => reader.readId(2)),
495
+ type: reader.readLww(8, () => reader.readByte(2)),
496
+ threshold: reader.readLww(9, () => reader.readFloat(2)),
497
+ originX: reader.readLww(10, () => reader.readFloat(2)),
498
+ };
374
499
  }
375
500
  return node;
376
501
  }
@@ -416,15 +541,19 @@ function readBlockBody(reader, blockType, version) {
416
541
  }
417
542
  return { type: "authorIds", authors };
418
543
  }
419
- case 0x0a:
420
- return {
544
+ case 0x0a: {
545
+ const info = {
421
546
  type: "pageInfo",
422
547
  loadsCount: reader.readInt(1),
423
548
  mergesCount: reader.readInt(2),
424
549
  textCharsCount: reader.readInt(3),
425
550
  textLinesCount: reader.readInt(4),
426
- typeFolioUseCount: reader.hasTag(5, TAG_BYTE4) ? reader.readInt(5) : 0,
427
551
  };
552
+ if (reader.hasTag(5, TAG_BYTE4)) {
553
+ info.typeFolioUseCount = reader.readInt(5);
554
+ }
555
+ return info;
556
+ }
428
557
  case 0x0d: {
429
558
  const info = {
430
559
  type: "sceneInfo",
@@ -445,29 +574,22 @@ function readBlockBody(reader, blockType, version) {
445
574
  throw new Error(`unknown v6 block type 0x${blockType.toString(16)}`);
446
575
  }
447
576
  }
577
+ /**
578
+ * the byte order of a little-endian (bytes_le) uuid
579
+ *
580
+ * The permutation is its own inverse, so one table serves both directions.
581
+ */
582
+ const UUID_LE_ORDER = [3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15];
583
+ function swapUuidBytes(bytes) {
584
+ const out = new Uint8Array(16);
585
+ for (let index = 0; index < 16; index++) {
586
+ out[index] = bytes[UUID_LE_ORDER[index]] ?? 0;
587
+ }
588
+ return out;
589
+ }
448
590
  function uuidToString(bytes) {
449
- // the uuid is stored little-endian (bytes_le); reverse the standard fields
450
- const b = [...bytes];
451
- const le = [
452
- b[3],
453
- b[2],
454
- b[1],
455
- b[0],
456
- b[5],
457
- b[4],
458
- b[7],
459
- b[6],
460
- b[8],
461
- b[9],
462
- b[10],
463
- b[11],
464
- b[12],
465
- b[13],
466
- b[14],
467
- b[15],
468
- ];
469
- const hex = le.map((byte) => (byte ?? 0).toString(16).padStart(2, "0"));
470
- return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
591
+ const hex = swapUuidBytes(bytes).toHex();
592
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
471
593
  }
472
594
  /** parse the raw block list of a version 6 `.rm` file */
473
595
  function parseV6Blocks(data) {
@@ -481,16 +603,36 @@ function parseV6Blocks(data) {
481
603
  if (reader.bytesRemaining() < 8)
482
604
  break;
483
605
  const length = reader.u32();
484
- reader.u8(); // unknown, always 0
606
+ const reserved = reader.u8();
485
607
  const minVersion = reader.u8();
486
608
  const currentVersion = reader.u8();
487
609
  const blockType = reader.u8();
488
610
  const blockStart = reader.offset;
611
+ if (length > reader.bytesRemaining()) {
612
+ // the block overruns the file; keep what's left verbatim and stop
613
+ blocks.push({
614
+ type: "unknown",
615
+ blockType,
616
+ data: reader.bytes(reader.bytesRemaining()),
617
+ declaredLength: length,
618
+ reserved,
619
+ minVersion,
620
+ currentVersion,
621
+ extraData: new Uint8Array(),
622
+ });
623
+ break;
624
+ }
489
625
  const blockEnd = blockStart + length;
490
626
  let block;
491
627
  try {
492
628
  const [body, extraData] = reader.bounded(blockEnd, () => readBlockBody(reader, blockType, currentVersion));
493
- block = { ...body, minVersion, currentVersion, extraData };
629
+ block = {
630
+ ...body,
631
+ reserved,
632
+ minVersion,
633
+ currentVersion,
634
+ extraData,
635
+ };
494
636
  }
495
637
  catch {
496
638
  // couldn't parse this block; keep its raw bytes so the file still
@@ -500,6 +642,7 @@ function parseV6Blocks(data) {
500
642
  type: "unknown",
501
643
  blockType,
502
644
  data: reader.bytes(length),
645
+ reserved,
503
646
  minVersion,
504
647
  currentVersion,
505
648
  extraData: new Uint8Array(),
@@ -722,6 +865,243 @@ export class RmScene {
722
865
  return this.#text;
723
866
  }
724
867
  }
868
+ function writeItemEnvelope(writer, parentId, item, itemType, writeValue) {
869
+ writer.writeId(1, parentId);
870
+ writer.writeId(2, item.itemId);
871
+ writer.writeId(3, item.leftId);
872
+ writer.writeId(4, item.rightId);
873
+ writer.writeInt(5, item.deletedLength);
874
+ const { value } = item;
875
+ if (value !== undefined) {
876
+ writer.subblock(6, () => {
877
+ writer.u8(itemType);
878
+ writeValue(value);
879
+ });
880
+ }
881
+ }
882
+ function writeLineValue(writer, line, version) {
883
+ writer.writeInt(1, line.tool);
884
+ writer.writeInt(2, line.color);
885
+ writer.writeDouble(3, line.thicknessScale);
886
+ writer.writeFloat(4, line.startingLength);
887
+ writer.subblock(5, () => {
888
+ for (const point of line.points) {
889
+ writer.f32(point.x);
890
+ writer.f32(point.y);
891
+ if (version === 1) {
892
+ writer.f32(point.speed / 4);
893
+ writer.f32((point.direction * 2 * Math.PI) / 255);
894
+ writer.f32(point.width / 4);
895
+ writer.f32(point.pressure / 255);
896
+ }
897
+ else {
898
+ writer.u16(point.speed);
899
+ writer.u16(point.width);
900
+ writer.u8(point.direction);
901
+ writer.u8(point.pressure);
902
+ }
903
+ }
904
+ });
905
+ if (line.timestampId !== undefined)
906
+ writer.writeId(6, line.timestampId);
907
+ if (line.moveId !== undefined)
908
+ writer.writeId(7, line.moveId);
909
+ if (line.colorRgba !== undefined)
910
+ writer.writeInt(8, line.colorRgba);
911
+ }
912
+ function writeGlyphValue(writer, glyph) {
913
+ if (glyph.start !== undefined)
914
+ writer.writeInt(2, glyph.start);
915
+ writer.writeInt(3, glyph.length);
916
+ writer.writeInt(4, glyph.color);
917
+ writer.writeString(5, glyph.text);
918
+ writer.subblock(6, () => {
919
+ writer.varuint(glyph.rectangles.length);
920
+ for (const rect of glyph.rectangles) {
921
+ writer.f64(rect.x);
922
+ writer.f64(rect.y);
923
+ writer.f64(rect.w);
924
+ writer.f64(rect.h);
925
+ }
926
+ });
927
+ if (glyph.colorRgba !== undefined)
928
+ writer.writeInt(10, glyph.colorRgba);
929
+ }
930
+ function writeText(writer, text) {
931
+ writer.subblock(2, () => {
932
+ writer.subblock(1, () => {
933
+ writer.subblock(1, () => {
934
+ writer.varuint(text.items.length);
935
+ for (const item of text.items) {
936
+ writer.subblock(0, () => {
937
+ writer.writeId(2, item.itemId);
938
+ writer.writeId(3, item.leftId);
939
+ writer.writeId(4, item.rightId);
940
+ writer.writeInt(5, item.deletedLength);
941
+ const { value } = item;
942
+ if (value !== "" && value !== undefined) {
943
+ writer.subblock(6, () => {
944
+ if (typeof value === "number") {
945
+ writer.varuint(0);
946
+ writer.u8(1);
947
+ writer.writeInt(2, value);
948
+ }
949
+ else {
950
+ const encoded = new TextEncoder().encode(value);
951
+ writer.varuint(encoded.length);
952
+ writer.u8(1);
953
+ writer.bytes(encoded);
954
+ }
955
+ });
956
+ }
957
+ });
958
+ }
959
+ });
960
+ });
961
+ writer.subblock(2, () => {
962
+ writer.subblock(1, () => {
963
+ writer.varuint(text.styles.size);
964
+ for (const [key, style] of text.styles) {
965
+ const [authorId, counter] = key.split(":");
966
+ writer.crdtId({
967
+ authorId: Number(authorId),
968
+ counter: Number(counter),
969
+ });
970
+ writer.writeId(1, style.timestamp);
971
+ writer.subblock(2, () => {
972
+ writer.u8(17);
973
+ writer.u8(style.value);
974
+ });
975
+ }
976
+ });
977
+ });
978
+ });
979
+ writer.subblock(3, () => {
980
+ writer.f64(text.posX);
981
+ writer.f64(text.posY);
982
+ });
983
+ writer.writeFloat(4, text.width);
984
+ }
985
+ function uuidToBytes(uuid) {
986
+ return swapUuidBytes(Uint8Array.fromHex(uuid.replaceAll("-", "")));
987
+ }
988
+ const BLOCK_TYPES = {
989
+ migrationInfo: 0x00,
990
+ sceneTree: 0x01,
991
+ treeNode: 0x02,
992
+ sceneGlyphItem: 0x03,
993
+ sceneGroupItem: 0x04,
994
+ sceneLineItem: 0x05,
995
+ sceneTextItem: 0x06,
996
+ rootText: 0x07,
997
+ sceneTombstone: 0x08,
998
+ authorIds: 0x09,
999
+ pageInfo: 0x0a,
1000
+ sceneInfo: 0x0d,
1001
+ };
1002
+ function writeBlockBody(writer, block) {
1003
+ switch (block.type) {
1004
+ case "migrationInfo":
1005
+ writer.writeId(1, block.migrationId);
1006
+ writer.writeBool(2, block.isDevice);
1007
+ break;
1008
+ case "sceneTree":
1009
+ writer.writeId(1, block.treeId);
1010
+ writer.writeId(2, block.nodeId);
1011
+ writer.writeBool(3, block.isUpdate);
1012
+ writer.subblock(4, () => writer.writeId(1, block.parentId));
1013
+ break;
1014
+ case "treeNode":
1015
+ writer.writeId(1, block.nodeId);
1016
+ writer.writeLww(2, block.label, (value) => writer.writeString(2, value));
1017
+ writer.writeLww(3, block.visible, (value) => writer.writeBool(2, value));
1018
+ if (block.anchor !== undefined) {
1019
+ const { id, type, threshold, originX } = block.anchor;
1020
+ writer.writeLww(7, id, (value) => writer.writeId(2, value));
1021
+ writer.writeLww(8, type, (value) => writer.writeByte(2, value));
1022
+ writer.writeLww(9, threshold, (value) => writer.writeFloat(2, value));
1023
+ writer.writeLww(10, originX, (value) => writer.writeFloat(2, value));
1024
+ }
1025
+ break;
1026
+ case "sceneGlyphItem":
1027
+ writeItemEnvelope(writer, block.parentId, block.item, 0x01, (glyph) => writeGlyphValue(writer, glyph));
1028
+ break;
1029
+ case "sceneGroupItem":
1030
+ writeItemEnvelope(writer, block.parentId, block.item, 0x02, (id) => writer.writeId(2, id));
1031
+ break;
1032
+ case "sceneLineItem":
1033
+ writeItemEnvelope(writer, block.parentId, block.item, 0x03, (line) => writeLineValue(writer, line, block.currentVersion));
1034
+ break;
1035
+ case "sceneTextItem":
1036
+ case "sceneTombstone":
1037
+ writeItemEnvelope(writer, block.parentId, block.item, 0x00, () => undefined);
1038
+ break;
1039
+ case "rootText":
1040
+ writer.writeId(1, block.blockId);
1041
+ writeText(writer, block.text);
1042
+ break;
1043
+ case "authorIds":
1044
+ writer.varuint(block.authors.size);
1045
+ for (const [authorId, uuid] of block.authors) {
1046
+ writer.subblock(0, () => {
1047
+ const bytes = uuidToBytes(uuid);
1048
+ writer.varuint(bytes.length);
1049
+ writer.bytes(bytes);
1050
+ writer.u16(authorId);
1051
+ });
1052
+ }
1053
+ break;
1054
+ case "pageInfo":
1055
+ writer.writeInt(1, block.loadsCount);
1056
+ writer.writeInt(2, block.mergesCount);
1057
+ writer.writeInt(3, block.textCharsCount);
1058
+ writer.writeInt(4, block.textLinesCount);
1059
+ if (block.typeFolioUseCount !== undefined) {
1060
+ writer.writeInt(5, block.typeFolioUseCount);
1061
+ }
1062
+ break;
1063
+ case "sceneInfo":
1064
+ writer.writeLww(1, block.currentLayer, (value) => writer.writeId(2, value));
1065
+ if (block.backgroundVisible !== undefined) {
1066
+ writer.writeLww(2, block.backgroundVisible, (value) => writer.writeBool(2, value));
1067
+ }
1068
+ if (block.rootDocumentVisible !== undefined) {
1069
+ writer.writeLww(3, block.rootDocumentVisible, (value) => writer.writeBool(2, value));
1070
+ }
1071
+ if (block.paperSize !== undefined) {
1072
+ writer.subblock(5, () => {
1073
+ writer.u32(block.paperSize[0]);
1074
+ writer.u32(block.paperSize[1]);
1075
+ });
1076
+ }
1077
+ break;
1078
+ case "unknown":
1079
+ writer.bytes(block.data);
1080
+ break;
1081
+ }
1082
+ }
1083
+ /** serialize a parsed scene back to version 6 `.rm` bytes */
1084
+ export function serializeRmScene(scene) {
1085
+ const writer = new Writer();
1086
+ const header = V6_HEADER.padEnd(HEADER_LENGTH, " ");
1087
+ writer.bytes(new TextEncoder().encode(header));
1088
+ for (const block of scene.blocks) {
1089
+ const lengthAt = writer.length;
1090
+ writer.u32(0);
1091
+ writer.u8(block.reserved);
1092
+ writer.u8(block.minVersion);
1093
+ writer.u8(block.currentVersion);
1094
+ writer.u8(block.type === "unknown" ? block.blockType : BLOCK_TYPES[block.type]);
1095
+ const bodyStart = writer.length;
1096
+ writeBlockBody(writer, block);
1097
+ if (block.type !== "unknown")
1098
+ writer.bytes(block.extraData);
1099
+ writer.patchU32(lengthAt, block.type === "unknown" && block.declaredLength !== undefined
1100
+ ? block.declaredLength
1101
+ : writer.length - bodyStart);
1102
+ }
1103
+ return writer.finish();
1104
+ }
725
1105
  /** parse a version 6 `.rm` file into a resolvable scene */
726
1106
  export function parseRmScene(data) {
727
1107
  return new RmScene(parseV6Blocks(data));