@manycore/aholo-splat-transform 1.2.7 → 1.2.8

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/file/spz.js CHANGED
@@ -1,16 +1,21 @@
1
+ import { constants as zlibConstants, zstdCompressSync, zstdDecompressSync } from 'node:zlib';
1
2
  import { SH_C0, SH_MAPS } from '../constant.js';
2
3
  import { BufferReader, fromHalf, clamp, StreamChunkDecoder, mortonSort } from '../utils/index.js';
3
4
  const SPZ_MAGIC = 0x5053474e; // NGSP = Niantic gaussian splat
4
5
  const SPZ_VERSION = 3;
6
+ const ZSTD_COMPRESSION_LEVEL = 12;
5
7
  const FLAG_ANTIALIASED = 0x1;
6
8
  const COLOR_SCALE = SH_C0 / 0.15;
7
9
  const rotation = new Array(4);
8
10
  const SH_SCALE1 = 1 << 3;
9
11
  const SH_SCALE2 = 1 << 4;
10
12
  export class SpzFile {
11
- compressLevel;
12
- constructor(compressLevel) {
13
+ constructor(compressLevel, spzVersion = SPZ_VERSION) {
14
+ if (spzVersion !== 3 && spzVersion !== 4) {
15
+ throw new Error(`Unsupported SPZ version: ${spzVersion}`);
16
+ }
13
17
  this.compressLevel = compressLevel;
18
+ this.spzVersion = spzVersion;
14
19
  }
15
20
  async read(stream, _contentLength, data) {
16
21
  const setCenter = data.setCenter.bind(data);
@@ -50,15 +55,10 @@ export class SpzFile {
50
55
  if (header.getUint32(0, true) !== SPZ_MAGIC) {
51
56
  throw new Error('Invalid SPZ file');
52
57
  }
53
- version = header.getUint32(4, true);
58
+ ({ version, counts, shDegree, fractionalBits, flags, extra: reserved } = readSpzHeader(header));
54
59
  if (version < 1 || version > 3) {
55
60
  throw new Error(`Unsupported SPZ version: ${version}`);
56
61
  }
57
- counts = header.getUint32(8, true);
58
- shDegree = header.getUint8(12);
59
- fractionalBits = header.getUint8(13);
60
- flags = header.getUint8(14);
61
- reserved = header.getUint8(15);
62
62
  isF16 = version < 2;
63
63
  useSmallestThreeQuat = version >= 3;
64
64
  fraction = 1 << fractionalBits;
@@ -175,6 +175,13 @@ export class SpzFile {
175
175
  },
176
176
  },
177
177
  ]);
178
+ const peeked = await peekStream(stream, 8);
179
+ stream = peeked.stream;
180
+ if (isSpzV4(peeked.prefix)) {
181
+ await readSpzV4Stream(stream, reader, decoder);
182
+ data.finishBlock();
183
+ return;
184
+ }
178
185
  let source;
179
186
  if (this.compressLevel === -1) {
180
187
  source = stream.getReader();
@@ -193,6 +200,14 @@ export class SpzFile {
193
200
  data.finishBlock();
194
201
  }
195
202
  async write(writeStream, data, indices = mortonSort(data)) {
203
+ if (this.spzVersion === 4) {
204
+ await this.writeV4(writeStream, data, indices);
205
+ }
206
+ else {
207
+ await this.writeV3(writeStream, data, indices);
208
+ }
209
+ }
210
+ async writeV3(writeStream, data, indices) {
196
211
  let writer;
197
212
  let pipePromise;
198
213
  if (this.compressLevel === -1) {
@@ -209,192 +224,360 @@ export class SpzFile {
209
224
  const shDegree = data.shDegree;
210
225
  const fractionalBits = 12;
211
226
  const flags = FLAG_ANTIALIASED;
212
- const reserved = 0;
213
- const fraction = 1 << fractionalBits;
214
- const shCounts = SH_MAPS[shDegree];
227
+ const shCounts = getShCounts(shDegree);
228
+ const context = createSpzEncodeContext(data, indices, fractionalBits, shCounts);
215
229
  // header
216
- {
217
- const buffer = new Uint8Array(16);
218
- const header = new DataView(buffer.buffer);
219
- header.setUint32(0, SPZ_MAGIC, true);
220
- header.setUint32(4, version, true);
221
- header.setUint32(8, counts, true);
222
- header.setUint8(12, shDegree);
223
- header.setUint8(13, fractionalBits);
224
- header.setUint8(14, flags);
225
- header.setUint8(15, reserved);
226
- writer.write(buffer);
230
+ writer.write(createSpzHeader(version, counts, shDegree, fractionalBits, flags, 0));
231
+ for (const attribute of getSpzAttributes(shDegree)) {
232
+ await writeSpzAttribute(writer, context, attribute);
227
233
  }
228
- const single = {
234
+ await writer.close();
235
+ await pipePromise;
236
+ }
237
+ async writeV4(writeStream, data, indices) {
238
+ const version = 4;
239
+ const counts = data.counts;
240
+ const shDegree = data.shDegree;
241
+ const fractionalBits = 12;
242
+ const flags = FLAG_ANTIALIASED;
243
+ const shCounts = getShCounts(shDegree);
244
+ const context = createSpzEncodeContext(data, indices, fractionalBits, shCounts);
245
+ const compressed = [];
246
+ const uncompressedSizes = [];
247
+ for (const attribute of getSpzAttributes(shDegree)) {
248
+ const chunk = createSpzAttributeChunk(context, attribute, 0, counts);
249
+ uncompressedSizes.push(chunk.byteLength);
250
+ compressed.push(zstdCompressSync(chunk, {
251
+ params: {
252
+ [zlibConstants.ZSTD_c_compressionLevel]: ZSTD_COMPRESSION_LEVEL,
253
+ },
254
+ }));
255
+ }
256
+ const tocByteOffset = 32;
257
+ const tocSize = compressed.length * 16;
258
+ const header = createSpzHeader(version, counts, shDegree, fractionalBits, flags, compressed.length, 32);
259
+ new DataView(header.buffer).setUint32(16, tocByteOffset, true);
260
+ const toc = new Uint8Array(tocSize);
261
+ const tocView = new DataView(toc.buffer);
262
+ for (let i = 0; i < compressed.length; i++) {
263
+ const entryOffset = i * 16;
264
+ writeUint64(tocView, entryOffset, compressed[i].byteLength);
265
+ writeUint64(tocView, entryOffset + 8, uncompressedSizes[i]);
266
+ }
267
+ const writer = writeStream.getWriter();
268
+ await writer.write(header);
269
+ await writer.write(toc);
270
+ for (const chunk of compressed) {
271
+ await writer.write(chunk);
272
+ }
273
+ await writer.close();
274
+ }
275
+ }
276
+ function getShCounts(shDegree) {
277
+ const shCounts = SH_MAPS[shDegree];
278
+ if (shCounts === undefined) {
279
+ throw new Error(`Unsupported SPZ SH degree: ${shDegree}`);
280
+ }
281
+ return shCounts;
282
+ }
283
+ function createSpzEncodeContext(data, indices, fractionalBits, shCounts) {
284
+ return {
285
+ data,
286
+ indices,
287
+ fractionalBits,
288
+ fraction: 1 << fractionalBits,
289
+ shCounts,
290
+ single: {
229
291
  x: 0, y: 0, z: 0,
230
292
  sx: 0, sy: 0, sz: 0,
231
293
  qx: 0, qy: 0, qz: 0, qw: 0,
232
294
  r: 0, g: 0, b: 0, a: 0,
233
295
  shN: new Array(shCounts),
234
- };
235
- // center
236
- {
237
- const ItemSize = 9;
238
- const chunkSize = 4096;
239
- const chunkCounts = Math.ceil(data.counts / chunkSize);
240
- for (let i = 0; i < chunkCounts; i++) {
241
- if (writer.desiredSize <= 0) {
242
- await writer.ready;
243
- }
244
- const currentChunkSize = Math.min(chunkSize, data.counts - i * chunkSize);
245
- const chunk = new Uint8Array(currentChunkSize * ItemSize);
246
- const offset = i * chunkSize;
247
- for (let j = 0; j < currentChunkSize; j++) {
248
- data.getCenter(indices[offset + j], single);
249
- const o = j * ItemSize;
250
- const ix = clamp(single.x * fraction, -0x7fffff, 0x7fffff);
251
- chunk[o + 0] = ix & 0xff;
252
- chunk[o + 1] = (ix >> 8) & 0xff;
253
- chunk[o + 2] = (ix >> 16) & 0xff;
254
- const iy = clamp(single.y * fraction, -0x7fffff, 0x7fffff);
255
- chunk[o + 3] = iy & 0xff;
256
- chunk[o + 4] = (iy >> 8) & 0xff;
257
- chunk[o + 5] = (iy >> 16) & 0xff;
258
- const iz = clamp(single.z * fraction, -0x7fffff, 0x7fffff);
259
- chunk[o + 6] = iz & 0xff;
260
- chunk[o + 7] = (iz >> 8) & 0xff;
261
- chunk[o + 8] = (iz >> 16) & 0xff;
262
- }
263
- writer.write(chunk);
296
+ },
297
+ };
298
+ }
299
+ function getSpzAttributes(shDegree) {
300
+ return shDegree > 0 ? ['position', 'alpha', 'color', 'scale', 'quat', 'sh'] : ['position', 'alpha', 'color', 'scale', 'quat'];
301
+ }
302
+ function getSpzAttributeInfo(attribute, shCounts) {
303
+ switch (attribute) {
304
+ case 'position':
305
+ return { itemSize: 9, chunkSize: 4096 };
306
+ case 'alpha':
307
+ return { itemSize: 1, chunkSize: 65536 };
308
+ case 'color':
309
+ case 'scale':
310
+ return { itemSize: 3, chunkSize: 16384 };
311
+ case 'quat':
312
+ return { itemSize: 4, chunkSize: 16384 };
313
+ case 'sh':
314
+ return { itemSize: shCounts, chunkSize: 1024 };
315
+ }
316
+ }
317
+ function createSpzAttributeChunk(context, attribute, offset, counts) {
318
+ const { data, indices, single, shCounts } = context;
319
+ const { itemSize } = getSpzAttributeInfo(attribute, shCounts);
320
+ const chunk = new Uint8Array(counts * itemSize);
321
+ for (let i = 0; i < counts; i++) {
322
+ const index = indices[offset + i];
323
+ switch (attribute) {
324
+ case 'position': {
325
+ data.getCenter(index, single);
326
+ const o = i * itemSize;
327
+ const ix = clamp(single.x * context.fraction, -0x7fffff, 0x7fffff);
328
+ chunk[o + 0] = ix & 0xff;
329
+ chunk[o + 1] = (ix >> 8) & 0xff;
330
+ chunk[o + 2] = (ix >> 16) & 0xff;
331
+ const iy = clamp(single.y * context.fraction, -0x7fffff, 0x7fffff);
332
+ chunk[o + 3] = iy & 0xff;
333
+ chunk[o + 4] = (iy >> 8) & 0xff;
334
+ chunk[o + 5] = (iy >> 16) & 0xff;
335
+ const iz = clamp(single.z * context.fraction, -0x7fffff, 0x7fffff);
336
+ chunk[o + 6] = iz & 0xff;
337
+ chunk[o + 7] = (iz >> 8) & 0xff;
338
+ chunk[o + 8] = (iz >> 16) & 0xff;
339
+ break;
264
340
  }
265
- }
266
- // alpha
267
- {
268
- const chunkSize = 65536;
269
- const chunkCounts = Math.ceil(data.counts / chunkSize);
270
- for (let i = 0; i < chunkCounts; i++) {
271
- if (writer.desiredSize <= 0) {
272
- await writer.ready;
273
- }
274
- const currentChunkSize = Math.min(chunkSize, data.counts - i * chunkSize);
275
- const chunk = new Uint8Array(currentChunkSize);
276
- const offset = i * chunkSize;
277
- for (let j = 0; j < currentChunkSize; j++) {
278
- data.getAlpha(indices[offset + j], single);
279
- chunk[j] = clamp(Math.round(single.a * 255), 0, 255);
280
- }
281
- writer.write(chunk);
341
+ case 'alpha':
342
+ data.getAlpha(index, single);
343
+ chunk[i] = clamp(Math.round(single.a * 255), 0, 255);
344
+ break;
345
+ case 'color': {
346
+ data.getColor(index, single);
347
+ const o = i * itemSize;
348
+ chunk[o + 0] = clamp(Math.round(((single.r - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
349
+ chunk[o + 1] = clamp(Math.round(((single.g - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
350
+ chunk[o + 2] = clamp(Math.round(((single.b - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
351
+ break;
282
352
  }
283
- }
284
- // color
285
- {
286
- const ItemSize = 3;
287
- const chunkSize = 16384;
288
- const chunkCounts = Math.ceil(data.counts / chunkSize);
289
- for (let i = 0; i < chunkCounts; i++) {
290
- if (writer.desiredSize <= 0) {
291
- await writer.ready;
292
- }
293
- const currentChunkSize = Math.min(chunkSize, data.counts - i * chunkSize);
294
- const chunk = new Uint8Array(currentChunkSize * ItemSize);
295
- const offset = i * chunkSize;
296
- for (let j = 0; j < currentChunkSize; j++) {
297
- data.getColor(indices[offset + j], single);
298
- const o = j * ItemSize;
299
- chunk[o + 0] = clamp(Math.round(((single.r - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
300
- chunk[o + 1] = clamp(Math.round(((single.g - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
301
- chunk[o + 2] = clamp(Math.round(((single.b - 0.5) / COLOR_SCALE + 0.5) * 255), 0, 255);
302
- }
303
- writer.write(chunk);
353
+ case 'scale': {
354
+ data.getScale(index, single);
355
+ const o = i * itemSize;
356
+ chunk[o + 0] = clamp(Math.round((Math.log(single.sx) + 10) * 16), 0, 255);
357
+ chunk[o + 1] = clamp(Math.round((Math.log(single.sy) + 10) * 16), 0, 255);
358
+ chunk[o + 2] = clamp(Math.round((Math.log(single.sz) + 10) * 16), 0, 255);
359
+ break;
304
360
  }
305
- }
306
- // scale
307
- {
308
- const ItemSize = 3;
309
- const chunkSize = 16384;
310
- const chunkCounts = Math.ceil(data.counts / chunkSize);
311
- for (let i = 0; i < chunkCounts; i++) {
312
- if (writer.desiredSize <= 0) {
313
- await writer.ready;
361
+ case 'quat': {
362
+ data.getQuat(index, single);
363
+ const o = i * itemSize;
364
+ rotation[0] = single.qx;
365
+ rotation[1] = single.qy;
366
+ rotation[2] = single.qz;
367
+ rotation[3] = single.qw;
368
+ let iLargest = 0;
369
+ for (let j = 1; j < 4; ++j) {
370
+ if (Math.abs(rotation[j]) > Math.abs(rotation[iLargest])) {
371
+ iLargest = j;
372
+ }
314
373
  }
315
- const currentChunkSize = Math.min(chunkSize, data.counts - i * chunkSize);
316
- const chunk = new Uint8Array(currentChunkSize * ItemSize);
317
- const offset = i * chunkSize;
318
- for (let j = 0; j < currentChunkSize; j++) {
319
- data.getScale(indices[offset + j], single);
320
- const o = j * ItemSize;
321
- chunk[o + 0] = clamp(Math.round((Math.log(single.sx) + 10) * 16), 0, 255);
322
- chunk[o + 1] = clamp(Math.round((Math.log(single.sy) + 10) * 16), 0, 255);
323
- chunk[o + 2] = clamp(Math.round((Math.log(single.sz) + 10) * 16), 0, 255);
374
+ const negate = rotation[iLargest] < 0 ? 1 : 0;
375
+ let comp = iLargest;
376
+ for (let j = 0; j < 4; ++j) {
377
+ if (j !== iLargest) {
378
+ const negbit = (rotation[j] < 0 ? 1 : 0) ^ negate;
379
+ const mag = Math.floor(((1 << 9) - 1) * (Math.abs(rotation[j]) / Math.SQRT1_2) + 0.5);
380
+ comp = (comp << 10) | (negbit << 9) | mag;
381
+ }
324
382
  }
325
- writer.write(chunk);
383
+ chunk[o + 0] = comp & 0xff;
384
+ chunk[o + 1] = (comp >> 8) & 0xff;
385
+ chunk[o + 2] = (comp >> 16) & 0xff;
386
+ chunk[o + 3] = (comp >> 24) & 0xff;
387
+ break;
326
388
  }
327
- }
328
- // quat
329
- {
330
- const ItemSize = 4;
331
- const chunkSize = 16384;
332
- const chunkCounts = Math.ceil(data.counts / chunkSize);
333
- for (let i = 0; i < chunkCounts; i++) {
334
- if (writer.desiredSize <= 0) {
335
- await writer.ready;
336
- }
337
- const currentChunkSize = Math.min(chunkSize, data.counts - i * chunkSize);
338
- const chunk = new Uint8Array(currentChunkSize * ItemSize);
339
- const offset = i * chunkSize;
340
- for (let j = 0; j < currentChunkSize; j++) {
341
- data.getQuat(indices[offset + j], single);
342
- const o = j * ItemSize;
343
- rotation[0] = single.qx;
344
- rotation[1] = single.qy;
345
- rotation[2] = single.qz;
346
- rotation[3] = single.qw;
347
- let iLargest = 0;
348
- for (let i = 1; i < 4; ++i) {
349
- if (Math.abs(rotation[i]) > Math.abs(rotation[iLargest])) {
350
- iLargest = i;
351
- }
389
+ case 'sh': {
390
+ data.getShN(index, single.shN);
391
+ const o = i * itemSize;
392
+ for (let j = 0; j < itemSize; j++) {
393
+ if (j < 9) {
394
+ chunk[o + j] = clamp(Math.floor((Math.round(single.shN[j] * 128) + 128 + SH_SCALE1 / 2) / SH_SCALE1) * SH_SCALE1, 0, 255);
395
+ continue;
352
396
  }
353
- const negate = rotation[iLargest] < 0 ? 1 : 0;
354
- let comp = iLargest;
355
- for (let i = 0; i < 4; ++i) {
356
- if (i !== iLargest) {
357
- const negbit = (rotation[i] < 0 ? 1 : 0) ^ negate;
358
- const mag = Math.floor(((1 << 9) - 1) * (Math.abs(rotation[i]) / Math.SQRT1_2) + 0.5);
359
- comp = (comp << 10) | (negbit << 9) | mag;
360
- }
361
- }
362
- chunk[o + 0] = comp & 0xff;
363
- chunk[o + 1] = (comp >> 8) & 0xff;
364
- chunk[o + 2] = (comp >> 16) & 0xff;
365
- chunk[o + 3] = (comp >> 24) & 0xff;
397
+ chunk[o + j] = clamp(Math.floor((Math.round(single.shN[j] * 128) + 128 + SH_SCALE2 / 2) / SH_SCALE2) * SH_SCALE2, 0, 255);
366
398
  }
367
- writer.write(chunk);
399
+ break;
368
400
  }
369
401
  }
370
- // shN
371
- if (shDegree > 0) {
372
- const shN = single.shN;
373
- const ItemSize = shCounts;
374
- const chunkSize = 1024;
375
- const chunkCounts = Math.ceil(data.counts / chunkSize);
376
- for (let i = 0; i < chunkCounts; i++) {
377
- if (writer.desiredSize <= 0) {
378
- await writer.ready;
379
- }
380
- const currentChunkSize = Math.min(chunkSize, data.counts - i * chunkSize);
381
- const chunk = new Uint8Array(currentChunkSize * ItemSize);
382
- const offset = i * chunkSize;
383
- for (let j = 0; j < currentChunkSize; j++) {
384
- data.getShN(indices[offset + j], shN);
385
- const o = j * ItemSize;
386
- for (let k = 0; k < ItemSize; k++) {
387
- if (k < 9) {
388
- chunk[o + k] = clamp(Math.floor((Math.round(shN[k] * 128) + 128 + SH_SCALE1 / 2) / SH_SCALE1) * SH_SCALE1, 0, 255);
389
- continue;
390
- }
391
- chunk[o + k] = clamp(Math.floor((Math.round(shN[k] * 128) + 128 + SH_SCALE2 / 2) / SH_SCALE2) * SH_SCALE2, 0, 255);
392
- }
402
+ }
403
+ return chunk;
404
+ }
405
+ async function writeSpzAttribute(writer, context, attribute) {
406
+ const { chunkSize } = getSpzAttributeInfo(attribute, context.shCounts);
407
+ const chunkCounts = Math.ceil(context.data.counts / chunkSize);
408
+ for (let i = 0; i < chunkCounts; i++) {
409
+ if (writer.desiredSize <= 0) {
410
+ await writer.ready;
411
+ }
412
+ const offset = i * chunkSize;
413
+ const counts = Math.min(chunkSize, context.data.counts - offset);
414
+ writer.write(createSpzAttributeChunk(context, attribute, offset, counts));
415
+ }
416
+ }
417
+ function readUint64(view, offset) {
418
+ const low = view.getUint32(offset, true);
419
+ const high = view.getUint32(offset + 4, true);
420
+ const value = high * 0x100000000 + low;
421
+ if (!Number.isSafeInteger(value)) {
422
+ throw new Error(`SPZ stream size is too large: ${value}`);
423
+ }
424
+ return value;
425
+ }
426
+ function writeUint64(view, offset, value) {
427
+ if (!Number.isSafeInteger(value) || value < 0) {
428
+ throw new Error(`Invalid SPZ stream size: ${value}`);
429
+ }
430
+ view.setUint32(offset, value >>> 0, true);
431
+ view.setUint32(offset + 4, Math.floor(value / 0x100000000), true);
432
+ }
433
+ function createSpzHeader(version, counts, shDegree, fractionalBits, flags, extra, byteLength = 16) {
434
+ const header = new DataView(new ArrayBuffer(byteLength));
435
+ header.setUint32(0, SPZ_MAGIC, true);
436
+ header.setUint32(4, version, true);
437
+ header.setUint32(8, counts, true);
438
+ header.setUint8(12, shDegree);
439
+ header.setUint8(13, fractionalBits);
440
+ header.setUint8(14, flags);
441
+ header.setUint8(15, extra);
442
+ return new Uint8Array(header.buffer);
443
+ }
444
+ function readSpzHeader(view) {
445
+ return {
446
+ version: view.getUint32(4, true),
447
+ counts: view.getUint32(8, true),
448
+ shDegree: view.getUint8(12),
449
+ fractionalBits: view.getUint8(13),
450
+ flags: view.getUint8(14),
451
+ extra: view.getUint8(15),
452
+ };
453
+ }
454
+ function getSpzV4AttributeSizes(counts, shDegree) {
455
+ const shCounts = getShCounts(shDegree);
456
+ const sizes = [
457
+ counts * 9, // position
458
+ counts, // alpha
459
+ counts * 3, // color
460
+ counts * 3, // scale
461
+ counts * 4, // quat
462
+ ];
463
+ if (shDegree > 0) {
464
+ sizes.push(counts * shCounts); // sh
465
+ }
466
+ return sizes;
467
+ }
468
+ function isSpzV4(buffer) {
469
+ if (buffer.byteLength < 8) {
470
+ return false;
471
+ }
472
+ const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
473
+ return view.getUint32(0, true) === SPZ_MAGIC && view.getUint32(4, true) === 4;
474
+ }
475
+ async function readSpzV4Stream(stream, reader, decoder) {
476
+ const read = createExactReader(stream);
477
+ const header = await read(32);
478
+ const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
479
+ const { counts, shDegree, fractionalBits, flags, extra: numStreams } = readSpzHeader(view);
480
+ const tocByteOffset = view.getUint32(16, true);
481
+ const expectedSizes = getSpzV4AttributeSizes(counts, shDegree);
482
+ if (numStreams !== expectedSizes.length) {
483
+ throw new Error(`Invalid SPZ v4 stream count: ${numStreams}`);
484
+ }
485
+ if (tocByteOffset < 32) {
486
+ throw new Error(`Invalid SPZ v4 TOC offset: ${tocByteOffset}`);
487
+ }
488
+ if (tocByteOffset > 32) {
489
+ await read(tocByteOffset - 32);
490
+ }
491
+ const toc = await read(numStreams * 16);
492
+ const tocView = new DataView(toc.buffer, toc.byteOffset, toc.byteLength);
493
+ // Reuse the legacy v3 attribute decoder after parsing the v4 container.
494
+ reader.write(createSpzHeader(SPZ_VERSION, counts, shDegree, fractionalBits, flags & FLAG_ANTIALIASED, 0));
495
+ decoder.flush();
496
+ for (let i = 0; i < numStreams; i++) {
497
+ const entryOffset = i * 16;
498
+ const compressedSize = readUint64(tocView, entryOffset);
499
+ const uncompressedSize = readUint64(tocView, entryOffset + 8);
500
+ if (uncompressedSize !== expectedSizes[i]) {
501
+ throw new Error(`Invalid SPZ v4 stream size at index ${i}`);
502
+ }
503
+ const compressed = await read(compressedSize);
504
+ const decompressed = zstdDecompressSync(compressed, {
505
+ maxOutputLength: uncompressedSize,
506
+ });
507
+ if (decompressed.byteLength !== uncompressedSize) {
508
+ throw new Error(`Invalid SPZ v4 decompressed size at index ${i}`);
509
+ }
510
+ reader.write(new Uint8Array(decompressed.buffer, decompressed.byteOffset, decompressed.byteLength));
511
+ decoder.flush();
512
+ }
513
+ }
514
+ // Return a reader that resolves exactly byteLength bytes and keeps leftover bytes for the next read.
515
+ function createExactReader(stream) {
516
+ const reader = stream.getReader();
517
+ let chunk;
518
+ let chunkOffset = 0;
519
+ return async (byteLength) => {
520
+ const result = new Uint8Array(byteLength);
521
+ let offset = 0;
522
+ while (offset < byteLength) {
523
+ if (!chunk || chunkOffset >= chunk.byteLength) {
524
+ const { done, value } = await reader.read();
525
+ if (done || !value) {
526
+ throw new Error('Invalid SPZ v4 file: stream ended unexpectedly');
393
527
  }
394
- writer.write(chunk);
528
+ chunk = value;
529
+ chunkOffset = 0;
395
530
  }
531
+ const copyLength = Math.min(byteLength - offset, chunk.byteLength - chunkOffset);
532
+ result.set(chunk.subarray(chunkOffset, chunkOffset + copyLength), offset);
533
+ chunkOffset += copyLength;
534
+ offset += copyLength;
535
+ }
536
+ return result;
537
+ };
538
+ }
539
+ // Peek leading bytes for format detection, then replay the consumed chunks through a replacement stream.
540
+ async function peekStream(stream, byteLength) {
541
+ const reader = stream.getReader();
542
+ const chunks = [];
543
+ let size = 0;
544
+ while (size < byteLength) {
545
+ const { done, value } = await reader.read();
546
+ if (done || !value) {
547
+ break;
548
+ }
549
+ chunks.push(value);
550
+ size += value.byteLength;
551
+ }
552
+ const prefix = new Uint8Array(Math.min(size, byteLength));
553
+ let offset = 0;
554
+ for (const chunk of chunks) {
555
+ const copyLength = Math.min(chunk.byteLength, prefix.byteLength - offset);
556
+ prefix.set(chunk.subarray(0, copyLength), offset);
557
+ offset += copyLength;
558
+ if (offset === prefix.byteLength) {
559
+ break;
396
560
  }
397
- await writer.close();
398
- await pipePromise;
399
561
  }
562
+ return {
563
+ prefix,
564
+ stream: new ReadableStream({
565
+ start(controller) {
566
+ for (const chunk of chunks) {
567
+ controller.enqueue(chunk);
568
+ }
569
+ },
570
+ async pull(controller) {
571
+ const { done, value } = await reader.read();
572
+ if (done) {
573
+ controller.close();
574
+ return;
575
+ }
576
+ controller.enqueue(value);
577
+ },
578
+ cancel(reason) {
579
+ return reader.cancel(reason);
580
+ },
581
+ }),
582
+ };
400
583
  }
@@ -86,13 +86,14 @@ export function generateLod(splat, levelParameters, blockPrecision, minSize, max
86
86
  return { splats, blocks };
87
87
  }
88
88
  export class WebPLosslessProfile {
89
- lossless = true;
89
+ constructor() {
90
+ this.lossless = true;
91
+ }
90
92
  }
91
93
  export class WebPQualityProfile {
92
- quality;
93
- lossless = false;
94
94
  constructor(quality) {
95
95
  this.quality = quality;
96
+ this.lossless = false;
96
97
  }
97
98
  ;
98
99
  }
@@ -4,6 +4,7 @@ export interface Config {
4
4
  output: string;
5
5
  enableMortonSort?: boolean;
6
6
  compressLevel?: number;
7
+ spzVersion?: number;
7
8
  }
8
9
  export declare class WriteTask extends BaseTask<Config> {
9
10
  exec(config: Config, { logger, resources }: Context): Promise<void>;