rmapi-js 11.1.2 → 11.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +104 -13
- package/dist/index.js +343 -124
- package/dist/raw.d.ts +61 -10
- package/dist/raw.js +62 -13
- package/dist/rm5.d.ts +111 -0
- package/dist/rm5.js +181 -0
- package/dist/rm6.d.ts +334 -0
- package/dist/rm6.js +728 -0
- package/dist/rmapi-js.esm.min.js +8 -8
- package/package.json +1 -1
package/dist/rm6.js
ADDED
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse (and re-serialize) reMarkable `.rm` version 6 "scene tree" files.
|
|
3
|
+
*
|
|
4
|
+
* Version 6 is a CRDT scene tree, not a flat struct: a header then a sequence
|
|
5
|
+
* of length-prefixed, tagged blocks. This module reads every block faithfully
|
|
6
|
+
* — preserving `CrdtId`s, `LwwValue` wrappers, and each block's unread tail —
|
|
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.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
/** the scene-tree root node id */
|
|
14
|
+
export const ROOT_ID = { authorId: 0, counter: 1 };
|
|
15
|
+
/** the CRDT sequence end marker / unset id */
|
|
16
|
+
export const END_MARKER = { authorId: 0, counter: 0 };
|
|
17
|
+
/** a stable string key for a {@link CrdtId | `CrdtId`} */
|
|
18
|
+
export function crdtKey(id) {
|
|
19
|
+
return `${id.authorId}:${id.counter}`;
|
|
20
|
+
}
|
|
21
|
+
const HEADER_LENGTH = 43;
|
|
22
|
+
const V6_HEADER = "reMarkable .lines file, version=6";
|
|
23
|
+
const TAG_BYTE1 = 0x1;
|
|
24
|
+
const TAG_BYTE4 = 0x4;
|
|
25
|
+
const TAG_BYTE8 = 0x8;
|
|
26
|
+
const TAG_LENGTH4 = 0xc;
|
|
27
|
+
const TAG_ID = 0xf;
|
|
28
|
+
/** a cursor over the tagged block stream, tracking block/subblock boundaries */
|
|
29
|
+
class Reader {
|
|
30
|
+
#view;
|
|
31
|
+
#offset;
|
|
32
|
+
#dataEnd;
|
|
33
|
+
/** stack of subblock/block end offsets; the innermost bound is the last */
|
|
34
|
+
#bounds = [];
|
|
35
|
+
constructor(data, offset) {
|
|
36
|
+
this.#view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
37
|
+
this.#offset = offset;
|
|
38
|
+
this.#dataEnd = data.byteLength;
|
|
39
|
+
}
|
|
40
|
+
get offset() {
|
|
41
|
+
return this.#offset;
|
|
42
|
+
}
|
|
43
|
+
get atFileEnd() {
|
|
44
|
+
return this.#offset >= this.#dataEnd;
|
|
45
|
+
}
|
|
46
|
+
#boundary() {
|
|
47
|
+
return this.#bounds.length > 0
|
|
48
|
+
? this.#bounds[this.#bounds.length - 1]
|
|
49
|
+
: this.#dataEnd;
|
|
50
|
+
}
|
|
51
|
+
bytesRemaining() {
|
|
52
|
+
return this.#boundary() - this.#offset;
|
|
53
|
+
}
|
|
54
|
+
u8() {
|
|
55
|
+
const value = this.#view.getUint8(this.#offset);
|
|
56
|
+
this.#offset += 1;
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
u16() {
|
|
60
|
+
const value = this.#view.getUint16(this.#offset, true);
|
|
61
|
+
this.#offset += 2;
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
u32() {
|
|
65
|
+
const value = this.#view.getUint32(this.#offset, true);
|
|
66
|
+
this.#offset += 4;
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
f32() {
|
|
70
|
+
const value = this.#view.getFloat32(this.#offset, true);
|
|
71
|
+
this.#offset += 4;
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
f64() {
|
|
75
|
+
const value = this.#view.getFloat64(this.#offset, true);
|
|
76
|
+
this.#offset += 8;
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
varuint() {
|
|
80
|
+
let result = 0;
|
|
81
|
+
let shift = 0;
|
|
82
|
+
let byte;
|
|
83
|
+
do {
|
|
84
|
+
byte = this.u8();
|
|
85
|
+
result += (byte & 0x7f) * 2 ** shift;
|
|
86
|
+
shift += 7;
|
|
87
|
+
} while (byte & 0x80);
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
crdtId() {
|
|
91
|
+
return { authorId: this.u8(), counter: this.varuint() };
|
|
92
|
+
}
|
|
93
|
+
bytes(length) {
|
|
94
|
+
const start = this.#view.byteOffset + this.#offset;
|
|
95
|
+
const slice = new Uint8Array(this.#view.buffer, start, length);
|
|
96
|
+
this.#offset += length;
|
|
97
|
+
return slice.slice();
|
|
98
|
+
}
|
|
99
|
+
/** peek the next tag as `[index, type]` without consuming it */
|
|
100
|
+
peekTag() {
|
|
101
|
+
if (this.#offset >= this.#boundary())
|
|
102
|
+
return undefined;
|
|
103
|
+
const save = this.#offset;
|
|
104
|
+
const raw = this.varuint();
|
|
105
|
+
this.#offset = save;
|
|
106
|
+
return [Math.floor(raw / 16), raw % 16];
|
|
107
|
+
}
|
|
108
|
+
hasTag(index, type) {
|
|
109
|
+
const tag = this.peekTag();
|
|
110
|
+
return tag !== undefined && tag[0] === index && tag[1] === type;
|
|
111
|
+
}
|
|
112
|
+
#expectTag(index, type) {
|
|
113
|
+
const raw = this.varuint();
|
|
114
|
+
if (Math.floor(raw / 16) !== index || raw % 16 !== type) {
|
|
115
|
+
throw new Error(`unexpected v6 tag ${raw} (wanted ${index}/${type})`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
readInt(index) {
|
|
119
|
+
this.#expectTag(index, TAG_BYTE4);
|
|
120
|
+
return this.u32();
|
|
121
|
+
}
|
|
122
|
+
readFloat(index) {
|
|
123
|
+
this.#expectTag(index, TAG_BYTE4);
|
|
124
|
+
return this.f32();
|
|
125
|
+
}
|
|
126
|
+
readDouble(index) {
|
|
127
|
+
this.#expectTag(index, TAG_BYTE8);
|
|
128
|
+
return this.f64();
|
|
129
|
+
}
|
|
130
|
+
readId(index) {
|
|
131
|
+
this.#expectTag(index, TAG_ID);
|
|
132
|
+
return this.crdtId();
|
|
133
|
+
}
|
|
134
|
+
readBool(index) {
|
|
135
|
+
this.#expectTag(index, TAG_BYTE1);
|
|
136
|
+
return this.u8() !== 0;
|
|
137
|
+
}
|
|
138
|
+
readByte(index) {
|
|
139
|
+
this.#expectTag(index, TAG_BYTE1);
|
|
140
|
+
return this.u8();
|
|
141
|
+
}
|
|
142
|
+
/** enter a Length4 subblock, run `fn`, then seek to the subblock end */
|
|
143
|
+
subblock(index, fn) {
|
|
144
|
+
this.#expectTag(index, TAG_LENGTH4);
|
|
145
|
+
const length = this.u32();
|
|
146
|
+
const subEnd = this.#offset + length;
|
|
147
|
+
this.#bounds.push(subEnd);
|
|
148
|
+
try {
|
|
149
|
+
return fn();
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
this.#bounds.pop();
|
|
153
|
+
this.#offset = subEnd;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
seek(offset) {
|
|
157
|
+
this.#offset = offset;
|
|
158
|
+
}
|
|
159
|
+
/** run `fn` bounded by `end`, returning the unread tail as extra data */
|
|
160
|
+
bounded(end, fn) {
|
|
161
|
+
this.#bounds.push(end);
|
|
162
|
+
try {
|
|
163
|
+
const value = fn();
|
|
164
|
+
if (this.#offset > end) {
|
|
165
|
+
throw new Error("block body overran its declared length");
|
|
166
|
+
}
|
|
167
|
+
return [value, this.bytes(end - this.#offset)];
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
this.#bounds.pop();
|
|
171
|
+
this.#offset = end;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
readLww(index, readValue) {
|
|
175
|
+
return this.subblock(index, () => {
|
|
176
|
+
const timestamp = this.readId(1);
|
|
177
|
+
const value = readValue();
|
|
178
|
+
return { timestamp, value };
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/** a Length4 string subblock: varuint length, ascii flag, utf-8 bytes */
|
|
182
|
+
readString(index) {
|
|
183
|
+
return this.subblock(index, () => {
|
|
184
|
+
const length = this.varuint();
|
|
185
|
+
this.u8(); // is-ascii flag
|
|
186
|
+
return new TextDecoder().decode(this.bytes(length));
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/** a Length4 string-with-format: a string, or an int format code */
|
|
190
|
+
readStringWithFormat(index) {
|
|
191
|
+
return this.subblock(index, () => {
|
|
192
|
+
const length = this.varuint();
|
|
193
|
+
this.u8(); // is-ascii flag
|
|
194
|
+
const text = new TextDecoder().decode(this.bytes(length));
|
|
195
|
+
if (this.hasTag(2, TAG_BYTE4)) {
|
|
196
|
+
return this.readInt(2);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
return text;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** read the `SceneItem` envelope shared by item blocks */
|
|
205
|
+
function readItemEnvelope(reader, readValue) {
|
|
206
|
+
const parentId = reader.readId(1);
|
|
207
|
+
const itemId = reader.readId(2);
|
|
208
|
+
const leftId = reader.readId(3);
|
|
209
|
+
const rightId = reader.readId(4);
|
|
210
|
+
const deletedLength = reader.readInt(5);
|
|
211
|
+
let value;
|
|
212
|
+
if (reader.hasTag(6, TAG_LENGTH4)) {
|
|
213
|
+
value = reader.subblock(6, () => {
|
|
214
|
+
const itemType = reader.u8();
|
|
215
|
+
return readValue(itemType);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return { parentId, item: { itemId, leftId, rightId, deletedLength, value } };
|
|
219
|
+
}
|
|
220
|
+
function readLineValue(reader, version) {
|
|
221
|
+
const tool = reader.readInt(1);
|
|
222
|
+
const color = reader.readInt(2);
|
|
223
|
+
const thicknessScale = reader.readDouble(3);
|
|
224
|
+
const startingLength = reader.readFloat(4);
|
|
225
|
+
const points = reader.subblock(5, () => {
|
|
226
|
+
const pointSize = version === 1 ? 24 : 14;
|
|
227
|
+
const total = reader.bytesRemaining();
|
|
228
|
+
const count = Math.floor(total / pointSize);
|
|
229
|
+
const list = new Array(count);
|
|
230
|
+
for (let index = 0; index < count; index++) {
|
|
231
|
+
const x = reader.f32();
|
|
232
|
+
const y = reader.f32();
|
|
233
|
+
if (version === 1) {
|
|
234
|
+
list[index] = {
|
|
235
|
+
x,
|
|
236
|
+
y,
|
|
237
|
+
speed: reader.f32() * 4,
|
|
238
|
+
direction: (reader.f32() * 255) / (2 * Math.PI),
|
|
239
|
+
width: Math.round(reader.f32() * 4),
|
|
240
|
+
pressure: reader.f32() * 255,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const speed = reader.u16();
|
|
245
|
+
const width = reader.u16();
|
|
246
|
+
const direction = reader.u8();
|
|
247
|
+
const pressure = reader.u8();
|
|
248
|
+
list[index] = { x, y, speed, width, direction, pressure };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return list;
|
|
252
|
+
});
|
|
253
|
+
const line = {
|
|
254
|
+
tool,
|
|
255
|
+
color,
|
|
256
|
+
thicknessScale,
|
|
257
|
+
startingLength,
|
|
258
|
+
points,
|
|
259
|
+
};
|
|
260
|
+
// optional trailing timestamp / move id / color are skipped by seeking to the
|
|
261
|
+
// subblock end; only the highlighter rgba color is captured
|
|
262
|
+
if (reader.hasTag(6, TAG_ID))
|
|
263
|
+
reader.readId(6);
|
|
264
|
+
if (reader.hasTag(7, TAG_ID))
|
|
265
|
+
reader.readId(7);
|
|
266
|
+
if (reader.hasTag(8, TAG_BYTE4))
|
|
267
|
+
line.colorRgba = reader.readInt(8);
|
|
268
|
+
return line;
|
|
269
|
+
}
|
|
270
|
+
function readGlyphValue(reader) {
|
|
271
|
+
const start = reader.hasTag(2, TAG_BYTE4) ? reader.readInt(2) : undefined;
|
|
272
|
+
const explicitLength = reader.hasTag(3, TAG_BYTE4)
|
|
273
|
+
? reader.readInt(3)
|
|
274
|
+
: undefined;
|
|
275
|
+
const color = reader.readInt(4);
|
|
276
|
+
const text = reader.readString(5);
|
|
277
|
+
const rectangles = reader.subblock(6, () => {
|
|
278
|
+
const count = reader.varuint();
|
|
279
|
+
const rects = new Array(count);
|
|
280
|
+
for (let index = 0; index < count; index++) {
|
|
281
|
+
rects[index] = {
|
|
282
|
+
x: reader.f64(),
|
|
283
|
+
y: reader.f64(),
|
|
284
|
+
w: reader.f64(),
|
|
285
|
+
h: reader.f64(),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return rects;
|
|
289
|
+
});
|
|
290
|
+
const glyph = {
|
|
291
|
+
length: explicitLength ?? text.length,
|
|
292
|
+
color,
|
|
293
|
+
text,
|
|
294
|
+
rectangles,
|
|
295
|
+
};
|
|
296
|
+
if (start !== undefined)
|
|
297
|
+
glyph.start = start;
|
|
298
|
+
if (reader.hasTag(10, TAG_BYTE4))
|
|
299
|
+
glyph.colorRgba = reader.readInt(10);
|
|
300
|
+
return glyph;
|
|
301
|
+
}
|
|
302
|
+
function readText(reader) {
|
|
303
|
+
const items = [];
|
|
304
|
+
const styles = new Map();
|
|
305
|
+
reader.subblock(2, () => {
|
|
306
|
+
reader.subblock(1, () => {
|
|
307
|
+
reader.subblock(1, () => {
|
|
308
|
+
const count = reader.varuint();
|
|
309
|
+
for (let index = 0; index < count; index++) {
|
|
310
|
+
items.push(reader.subblock(0, () => {
|
|
311
|
+
const itemId = reader.readId(2);
|
|
312
|
+
const leftId = reader.readId(3);
|
|
313
|
+
const rightId = reader.readId(4);
|
|
314
|
+
const deletedLength = reader.readInt(5);
|
|
315
|
+
const value = reader.hasTag(6, TAG_LENGTH4)
|
|
316
|
+
? reader.readStringWithFormat(6)
|
|
317
|
+
: "";
|
|
318
|
+
return { itemId, leftId, rightId, deletedLength, value };
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
reader.subblock(2, () => {
|
|
324
|
+
reader.subblock(1, () => {
|
|
325
|
+
const count = reader.varuint();
|
|
326
|
+
for (let index = 0; index < count; index++) {
|
|
327
|
+
const charId = reader.crdtId();
|
|
328
|
+
const timestamp = reader.readId(1);
|
|
329
|
+
const style = reader.subblock(2, () => {
|
|
330
|
+
reader.u8(); // constant 17
|
|
331
|
+
return reader.u8();
|
|
332
|
+
});
|
|
333
|
+
styles.set(crdtKey(charId), { timestamp, value: style });
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
const [posX, posY] = reader.subblock(3, () => [reader.f64(), reader.f64()]);
|
|
339
|
+
const width = reader.readFloat(4);
|
|
340
|
+
return { items, styles, posX, posY, width };
|
|
341
|
+
}
|
|
342
|
+
/** read the body of a block of a given type (bounds already set to block end) */
|
|
343
|
+
function readBlockBody(reader, blockType, version) {
|
|
344
|
+
switch (blockType) {
|
|
345
|
+
case 0x00:
|
|
346
|
+
return {
|
|
347
|
+
type: "migrationInfo",
|
|
348
|
+
migrationId: reader.readId(1),
|
|
349
|
+
isDevice: reader.readBool(2),
|
|
350
|
+
};
|
|
351
|
+
case 0x01:
|
|
352
|
+
return {
|
|
353
|
+
type: "sceneTree",
|
|
354
|
+
treeId: reader.readId(1),
|
|
355
|
+
nodeId: reader.readId(2),
|
|
356
|
+
isUpdate: reader.readBool(3),
|
|
357
|
+
parentId: reader.subblock(4, () => reader.readId(1)),
|
|
358
|
+
};
|
|
359
|
+
case 0x02: {
|
|
360
|
+
const nodeId = reader.readId(1);
|
|
361
|
+
const label = reader.readLww(2, () => reader.readString(2));
|
|
362
|
+
const visible = reader.readLww(3, () => reader.readBool(2));
|
|
363
|
+
const node = {
|
|
364
|
+
type: "treeNode",
|
|
365
|
+
nodeId,
|
|
366
|
+
label,
|
|
367
|
+
visible,
|
|
368
|
+
};
|
|
369
|
+
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));
|
|
374
|
+
}
|
|
375
|
+
return node;
|
|
376
|
+
}
|
|
377
|
+
case 0x03:
|
|
378
|
+
return {
|
|
379
|
+
type: "sceneGlyphItem",
|
|
380
|
+
...readItemEnvelope(reader, (itemType) => itemType === 0x01 ? readGlyphValue(reader) : undefined),
|
|
381
|
+
};
|
|
382
|
+
case 0x04:
|
|
383
|
+
return {
|
|
384
|
+
type: "sceneGroupItem",
|
|
385
|
+
...readItemEnvelope(reader, (itemType) => itemType === 0x02 ? reader.readId(2) : undefined),
|
|
386
|
+
};
|
|
387
|
+
case 0x05:
|
|
388
|
+
return {
|
|
389
|
+
type: "sceneLineItem",
|
|
390
|
+
...readItemEnvelope(reader, (itemType) => itemType === 0x03 ? readLineValue(reader, version) : undefined),
|
|
391
|
+
};
|
|
392
|
+
case 0x06:
|
|
393
|
+
return {
|
|
394
|
+
type: "sceneTextItem",
|
|
395
|
+
...readItemEnvelope(reader, () => undefined),
|
|
396
|
+
};
|
|
397
|
+
case 0x07: {
|
|
398
|
+
const blockId = reader.readId(1);
|
|
399
|
+
return { type: "rootText", blockId, text: readText(reader) };
|
|
400
|
+
}
|
|
401
|
+
case 0x08:
|
|
402
|
+
return {
|
|
403
|
+
type: "sceneTombstone",
|
|
404
|
+
...readItemEnvelope(reader, () => undefined),
|
|
405
|
+
};
|
|
406
|
+
case 0x09: {
|
|
407
|
+
const authors = new Map();
|
|
408
|
+
const count = reader.varuint();
|
|
409
|
+
for (let index = 0; index < count; index++) {
|
|
410
|
+
reader.subblock(0, () => {
|
|
411
|
+
const uuidLength = reader.varuint();
|
|
412
|
+
const uuid = reader.bytes(uuidLength);
|
|
413
|
+
const authorId = reader.u16();
|
|
414
|
+
authors.set(authorId, uuidToString(uuid));
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return { type: "authorIds", authors };
|
|
418
|
+
}
|
|
419
|
+
case 0x0a:
|
|
420
|
+
return {
|
|
421
|
+
type: "pageInfo",
|
|
422
|
+
loadsCount: reader.readInt(1),
|
|
423
|
+
mergesCount: reader.readInt(2),
|
|
424
|
+
textCharsCount: reader.readInt(3),
|
|
425
|
+
textLinesCount: reader.readInt(4),
|
|
426
|
+
typeFolioUseCount: reader.hasTag(5, TAG_BYTE4) ? reader.readInt(5) : 0,
|
|
427
|
+
};
|
|
428
|
+
case 0x0d: {
|
|
429
|
+
const info = {
|
|
430
|
+
type: "sceneInfo",
|
|
431
|
+
currentLayer: reader.readLww(1, () => reader.readId(2)),
|
|
432
|
+
};
|
|
433
|
+
if (reader.hasTag(2, TAG_LENGTH4)) {
|
|
434
|
+
info.backgroundVisible = reader.readLww(2, () => reader.readBool(2));
|
|
435
|
+
}
|
|
436
|
+
if (reader.hasTag(3, TAG_LENGTH4)) {
|
|
437
|
+
info.rootDocumentVisible = reader.readLww(3, () => reader.readBool(2));
|
|
438
|
+
}
|
|
439
|
+
if (reader.hasTag(5, TAG_LENGTH4)) {
|
|
440
|
+
info.paperSize = reader.subblock(5, () => [reader.u32(), reader.u32()]);
|
|
441
|
+
}
|
|
442
|
+
return info;
|
|
443
|
+
}
|
|
444
|
+
default:
|
|
445
|
+
throw new Error(`unknown v6 block type 0x${blockType.toString(16)}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
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("")}`;
|
|
471
|
+
}
|
|
472
|
+
/** parse the raw block list of a version 6 `.rm` file */
|
|
473
|
+
function parseV6Blocks(data) {
|
|
474
|
+
const header = new TextDecoder().decode(data.subarray(0, HEADER_LENGTH));
|
|
475
|
+
if (!header.startsWith(V6_HEADER)) {
|
|
476
|
+
throw new Error(`not a version 6 .lines file: ${JSON.stringify(header)}`);
|
|
477
|
+
}
|
|
478
|
+
const reader = new Reader(data, HEADER_LENGTH);
|
|
479
|
+
const blocks = [];
|
|
480
|
+
while (!reader.atFileEnd) {
|
|
481
|
+
if (reader.bytesRemaining() < 8)
|
|
482
|
+
break;
|
|
483
|
+
const length = reader.u32();
|
|
484
|
+
reader.u8(); // unknown, always 0
|
|
485
|
+
const minVersion = reader.u8();
|
|
486
|
+
const currentVersion = reader.u8();
|
|
487
|
+
const blockType = reader.u8();
|
|
488
|
+
const blockStart = reader.offset;
|
|
489
|
+
const blockEnd = blockStart + length;
|
|
490
|
+
let block;
|
|
491
|
+
try {
|
|
492
|
+
const [body, extraData] = reader.bounded(blockEnd, () => readBlockBody(reader, blockType, currentVersion));
|
|
493
|
+
block = { ...body, minVersion, currentVersion, extraData };
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
// couldn't parse this block; keep its raw bytes so the file still
|
|
497
|
+
// round-trips and later blocks still parse
|
|
498
|
+
reader.seek(blockStart);
|
|
499
|
+
block = {
|
|
500
|
+
type: "unknown",
|
|
501
|
+
blockType,
|
|
502
|
+
data: reader.bytes(length),
|
|
503
|
+
minVersion,
|
|
504
|
+
currentVersion,
|
|
505
|
+
extraData: new Uint8Array(),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
reader.seek(blockEnd);
|
|
509
|
+
blocks.push(block);
|
|
510
|
+
}
|
|
511
|
+
return blocks;
|
|
512
|
+
}
|
|
513
|
+
function compareRank(a, b) {
|
|
514
|
+
const [a0, a1] = a;
|
|
515
|
+
const [b0, b1] = b;
|
|
516
|
+
return a0 - b0 || a1 - b1;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* order a group's children by the CRDT left/right sequence
|
|
520
|
+
*
|
|
521
|
+
* Topological sort over `left -> item -> right` edges (unset/unknown links map
|
|
522
|
+
* to synthetic start/end bounds), breaking ties between concurrent inserts by
|
|
523
|
+
* higher author id then lower counter, matching reMarkable's ordering.
|
|
524
|
+
*/
|
|
525
|
+
function toposort(items) {
|
|
526
|
+
if (items.length <= 1)
|
|
527
|
+
return [...items];
|
|
528
|
+
const START = "\x00start";
|
|
529
|
+
const END = "\x00end";
|
|
530
|
+
const present = new Set(items.map((item) => crdtKey(item.itemId)));
|
|
531
|
+
const succ = new Map([
|
|
532
|
+
[START, []],
|
|
533
|
+
[END, []],
|
|
534
|
+
]);
|
|
535
|
+
const indeg = new Map([
|
|
536
|
+
[START, 0],
|
|
537
|
+
[END, 0],
|
|
538
|
+
]);
|
|
539
|
+
for (const item of items) {
|
|
540
|
+
succ.set(crdtKey(item.itemId), []);
|
|
541
|
+
indeg.set(crdtKey(item.itemId), 0);
|
|
542
|
+
}
|
|
543
|
+
const resolve = (id, fallback) => {
|
|
544
|
+
const key = crdtKey(id);
|
|
545
|
+
return key === crdtKey(END_MARKER) || !present.has(key) ? fallback : key;
|
|
546
|
+
};
|
|
547
|
+
const edge = (from, to) => {
|
|
548
|
+
succ.get(from).push(to);
|
|
549
|
+
indeg.set(to, (indeg.get(to) ?? 0) + 1);
|
|
550
|
+
};
|
|
551
|
+
for (const item of items) {
|
|
552
|
+
edge(resolve(item.leftId, START), crdtKey(item.itemId));
|
|
553
|
+
edge(crdtKey(item.itemId), resolve(item.rightId, END));
|
|
554
|
+
}
|
|
555
|
+
const byKey = new Map(items.map((item) => [crdtKey(item.itemId), item]));
|
|
556
|
+
const rank = (key) => {
|
|
557
|
+
if (key === START)
|
|
558
|
+
return [-Infinity, -Infinity];
|
|
559
|
+
if (key === END)
|
|
560
|
+
return [Infinity, Infinity];
|
|
561
|
+
const item = byKey.get(key);
|
|
562
|
+
return [-item.itemId.authorId, item.itemId.counter];
|
|
563
|
+
};
|
|
564
|
+
const ready = new Set();
|
|
565
|
+
for (const [node, degree] of indeg) {
|
|
566
|
+
if (degree === 0)
|
|
567
|
+
ready.add(node);
|
|
568
|
+
}
|
|
569
|
+
const order = [];
|
|
570
|
+
const placed = new Set();
|
|
571
|
+
while (ready.size > 0) {
|
|
572
|
+
let best;
|
|
573
|
+
for (const key of ready) {
|
|
574
|
+
if (best === undefined || compareRank(rank(key), rank(best)) < 0) {
|
|
575
|
+
best = key;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
ready.delete(best);
|
|
579
|
+
placed.add(best);
|
|
580
|
+
const item = byKey.get(best);
|
|
581
|
+
if (item)
|
|
582
|
+
order.push(item);
|
|
583
|
+
for (const next of succ.get(best) ?? []) {
|
|
584
|
+
indeg.set(next, indeg.get(next) - 1);
|
|
585
|
+
if (indeg.get(next) === 0)
|
|
586
|
+
ready.add(next);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// if a cycle prevented full placement, append the rest in file order
|
|
590
|
+
if (order.length < items.length) {
|
|
591
|
+
for (const item of items) {
|
|
592
|
+
if (!placed.has(crdtKey(item.itemId)))
|
|
593
|
+
order.push(item);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return order;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* a parsed version 6 scene
|
|
600
|
+
*
|
|
601
|
+
* The raw {@link RmBlock | `blocks`} are the faithful source of truth (they
|
|
602
|
+
* preserve the CRDT ids and re-serialize); the methods resolve them into
|
|
603
|
+
* ordered layers, strokes, and text.
|
|
604
|
+
*/
|
|
605
|
+
export class RmScene {
|
|
606
|
+
/** the discriminant for the {@link RmPage} union */
|
|
607
|
+
version = 6;
|
|
608
|
+
/** every parsed block, in file order */
|
|
609
|
+
blocks;
|
|
610
|
+
/** the author-id to uuid table */
|
|
611
|
+
authors;
|
|
612
|
+
/** the page size, if the scene info block carried one */
|
|
613
|
+
paperSize;
|
|
614
|
+
#nodes = new Map();
|
|
615
|
+
#text;
|
|
616
|
+
constructor(blocks) {
|
|
617
|
+
this.blocks = blocks;
|
|
618
|
+
this.authors = new Map();
|
|
619
|
+
const node = (id) => {
|
|
620
|
+
let group = this.#nodes.get(crdtKey(id));
|
|
621
|
+
if (group === undefined) {
|
|
622
|
+
group = { id, parentId: END_MARKER, children: [] };
|
|
623
|
+
this.#nodes.set(crdtKey(id), group);
|
|
624
|
+
}
|
|
625
|
+
return group;
|
|
626
|
+
};
|
|
627
|
+
node(ROOT_ID);
|
|
628
|
+
for (const block of blocks) {
|
|
629
|
+
if (block.type === "sceneTree") {
|
|
630
|
+
node(block.treeId).parentId = block.parentId;
|
|
631
|
+
}
|
|
632
|
+
else if (block.type === "treeNode") {
|
|
633
|
+
const group = node(block.nodeId);
|
|
634
|
+
group.label = block.label.value;
|
|
635
|
+
group.visible = block.visible.value;
|
|
636
|
+
}
|
|
637
|
+
else if (block.type === "sceneGroupItem" && block.item.value) {
|
|
638
|
+
node(block.parentId).children.push({
|
|
639
|
+
...block.item,
|
|
640
|
+
value: { kind: "group", id: block.item.value },
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
else if (block.type === "sceneLineItem" && block.item.value) {
|
|
644
|
+
node(block.parentId).children.push({
|
|
645
|
+
...block.item,
|
|
646
|
+
value: { kind: "line", line: block.item.value },
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
else if (block.type === "sceneGlyphItem" && block.item.value) {
|
|
650
|
+
node(block.parentId).children.push({
|
|
651
|
+
...block.item,
|
|
652
|
+
value: { kind: "glyph", glyph: block.item.value },
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
else if (block.type === "rootText") {
|
|
656
|
+
this.#text = block.text;
|
|
657
|
+
}
|
|
658
|
+
else if (block.type === "authorIds") {
|
|
659
|
+
for (const [authorId, uuid] of block.authors) {
|
|
660
|
+
this.authors.set(authorId, uuid);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
else if (block.type === "sceneInfo" && block.paperSize) {
|
|
664
|
+
this.paperSize = block.paperSize;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
#resolveGroup(group) {
|
|
669
|
+
const items = [];
|
|
670
|
+
for (const child of toposort(group.children)) {
|
|
671
|
+
if (child.value.kind === "group") {
|
|
672
|
+
const nested = this.#nodes.get(crdtKey(child.value.id));
|
|
673
|
+
if (nested)
|
|
674
|
+
items.push({ kind: "layer", layer: this.#resolveGroup(nested) });
|
|
675
|
+
}
|
|
676
|
+
else if (child.value.kind === "line") {
|
|
677
|
+
items.push({ kind: "line", line: child.value.line });
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
items.push({ kind: "glyph", glyph: child.value.glyph });
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const layer = { id: group.id, items };
|
|
684
|
+
if (group.label !== undefined)
|
|
685
|
+
layer.label = group.label;
|
|
686
|
+
if (group.visible !== undefined)
|
|
687
|
+
layer.visible = group.visible;
|
|
688
|
+
return layer;
|
|
689
|
+
}
|
|
690
|
+
/** the drawing layers, in order, each with its items */
|
|
691
|
+
layers() {
|
|
692
|
+
const root = this.#nodes.get(crdtKey(ROOT_ID));
|
|
693
|
+
if (root === undefined)
|
|
694
|
+
return [];
|
|
695
|
+
const out = [];
|
|
696
|
+
for (const child of toposort(root.children)) {
|
|
697
|
+
if (child.value.kind === "group") {
|
|
698
|
+
const group = this.#nodes.get(crdtKey(child.value.id));
|
|
699
|
+
if (group)
|
|
700
|
+
out.push(this.#resolveGroup(group));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return out;
|
|
704
|
+
}
|
|
705
|
+
/** every stroke on the page, in draw order, flattened across layers */
|
|
706
|
+
strokes() {
|
|
707
|
+
const out = [];
|
|
708
|
+
const walk = (layer) => {
|
|
709
|
+
for (const item of layer.items) {
|
|
710
|
+
if (item.kind === "line")
|
|
711
|
+
out.push(item.line);
|
|
712
|
+
else if (item.kind === "layer")
|
|
713
|
+
walk(item.layer);
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
for (const layer of this.layers())
|
|
717
|
+
walk(layer);
|
|
718
|
+
return out;
|
|
719
|
+
}
|
|
720
|
+
/** the page's document text, if any */
|
|
721
|
+
text() {
|
|
722
|
+
return this.#text;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
/** parse a version 6 `.rm` file into a resolvable scene */
|
|
726
|
+
export function parseRmScene(data) {
|
|
727
|
+
return new RmScene(parseV6Blocks(data));
|
|
728
|
+
}
|