dicom-synth 1.7.0 → 1.9.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.
@@ -2,21 +2,46 @@
2
2
  import { readFileSync, writeFileSync } from 'node:fs'
3
3
  import { resolve } from 'node:path'
4
4
  import {
5
- generateCollectionFromSpec,
5
+ MAX_PIXEL_BYTES,
6
+ MAX_STREAM_BYTES,
7
+ previewCollection,
6
8
  resolveParametricSpec,
7
9
  validateDatasetSpec,
10
+ withResolvedSeed,
8
11
  writeCollectionFromSpec,
12
+ writeLargeImageFile,
9
13
  } from '../dist/esm/index.js'
10
14
 
11
15
  function usage() {
12
16
  console.error(
13
- 'Usage: dicom-synth-generate --schema <path> [--out <dir>] [--dry-run]\n' +
14
- ' dicom-synth-generate --schema-inline <json> [--out <dir>] [--dry-run]\n' +
15
- ' dicom-synth-generate --parametric <path> [--out <dir>] [--emit-spec <path>] [--dry-run]',
17
+ 'Usage: dicom-synth-generate --schema <path> [--out <dir>] [--emit-spec <path>] [--dry-run]\n' +
18
+ ' dicom-synth-generate --schema-inline <json> [--out <dir>] [--emit-spec <path>] [--dry-run]\n' +
19
+ ' dicom-synth-generate --parametric <path> [--out <dir>] [--emit-spec <path>] [--dry-run]\n' +
20
+ ' dicom-synth-generate --large-file <path> --size <600mb|50gb> [--modality CT|PT|MR|CR]\n' +
21
+ '\n' +
22
+ 'A seed is drawn when the spec omits one; pass --emit-spec to capture the\n' +
23
+ 'effective spec (seed included) so the dataset can be re-created.\n' +
24
+ '\n' +
25
+ '--large-file streams a single DICOM (512 MB–50 GB) to disk; files above\n' +
26
+ '~4 GB use encapsulated fragments (valid container, zero pixel content).',
16
27
  )
17
28
  process.exit(1)
18
29
  }
19
30
 
31
+ const SIZE_UNITS = { b: 1, kb: 1024, mb: 1024 * 1024, gb: 1024 * 1024 * 1024 }
32
+
33
+ function parseSizeToBytes(s) {
34
+ // Unit is required so a bare number (e.g. "600", meant as MB) is not silently
35
+ // read as 600 bytes.
36
+ const m = /^([0-9]*\.?[0-9]+)\s*(b|kb|mb|gb)$/i.exec(s.trim())
37
+ if (!m) {
38
+ throw new Error(
39
+ `invalid --size "${s}" — include a unit, e.g. 600mb or 50gb`,
40
+ )
41
+ }
42
+ return Math.round(Number.parseFloat(m[1]) * SIZE_UNITS[m[2].toLowerCase()])
43
+ }
44
+
20
45
  const args = process.argv.slice(2)
21
46
 
22
47
  if (args.length === 0) usage()
@@ -26,6 +51,9 @@ let specSource
26
51
  let outDir = resolve('./fixtures/generated')
27
52
  let emitSpecPath
28
53
  let dryRun = false
54
+ let largeFilePath
55
+ let largeFileSize
56
+ let largeFileModality
29
57
 
30
58
  function readSpecArg(flag, value) {
31
59
  if (rawSpec !== undefined) {
@@ -66,12 +94,73 @@ for (let i = 0; i < args.length; i++) {
66
94
  emitSpecPath = resolve(args[++i])
67
95
  } else if (args[i] === '--dry-run') {
68
96
  dryRun = true
97
+ } else if (args[i] === '--large-file' && args[i + 1]) {
98
+ largeFilePath = resolve(args[++i])
99
+ } else if (args[i] === '--size' && args[i + 1]) {
100
+ largeFileSize = args[++i]
101
+ } else if (args[i] === '--modality' && args[i + 1]) {
102
+ largeFileModality = args[++i]
69
103
  } else {
70
104
  console.error(`Unknown argument: ${args[i]}`)
71
105
  usage()
72
106
  }
73
107
  }
74
108
 
109
+ if (largeFilePath !== undefined) {
110
+ if (rawSpec !== undefined) {
111
+ console.error('Error: --large-file cannot be combined with a spec mode')
112
+ process.exit(1)
113
+ }
114
+ if (largeFileSize === undefined) {
115
+ console.error('Error: --large-file requires --size')
116
+ usage()
117
+ }
118
+ if (
119
+ largeFileModality !== undefined &&
120
+ !['CT', 'PT', 'MR', 'CR'].includes(largeFileModality)
121
+ ) {
122
+ console.error(`Error: invalid --modality "${largeFileModality}"`)
123
+ process.exit(1)
124
+ }
125
+ let targetBytes
126
+ try {
127
+ targetBytes = parseSizeToBytes(largeFileSize)
128
+ } catch (err) {
129
+ console.error(`Error: ${err.message}`)
130
+ process.exit(1)
131
+ }
132
+ // Validate bounds here so --dry-run reflects what a real run would accept.
133
+ if (targetBytes <= MAX_PIXEL_BYTES) {
134
+ console.error(
135
+ 'Error: --size must exceed the 512 MB in-memory limit — use a spec with targetSizeKb for smaller files',
136
+ )
137
+ process.exit(1)
138
+ }
139
+ if (targetBytes > MAX_STREAM_BYTES) {
140
+ console.error('Error: --size exceeds the 50 GB limit')
141
+ process.exit(1)
142
+ }
143
+ if (dryRun) {
144
+ console.log(
145
+ `Dry run: would write ~${(targetBytes / 1024 / 1024).toFixed(1)} MB to ${largeFilePath} — nothing written`,
146
+ )
147
+ process.exit(0)
148
+ }
149
+ try {
150
+ const result = writeLargeImageFile(
151
+ { targetBytes, modality: largeFileModality },
152
+ largeFilePath,
153
+ )
154
+ console.log(
155
+ `Wrote ${(result.bytesWritten / 1024 / 1024).toFixed(1)} MB ${result.encoding} DICOM to ${result.path}`,
156
+ )
157
+ } catch (err) {
158
+ console.error(`Error writing large file: ${err.message}`)
159
+ process.exit(1)
160
+ }
161
+ process.exit(0)
162
+ }
163
+
75
164
  if (rawSpec === undefined) usage()
76
165
 
77
166
  function printTypeCounts(counts) {
@@ -80,22 +169,27 @@ function printTypeCounts(counts) {
80
169
  }
81
170
  }
82
171
 
83
- if (emitSpecPath !== undefined && specSource !== '--parametric') {
84
- console.error('Error: --emit-spec requires --parametric')
85
- process.exit(1)
86
- }
172
+ const seedProvided =
173
+ rawSpec !== null && typeof rawSpec === 'object' && 'seed' in rawSpec
87
174
 
88
175
  let spec
89
176
  try {
90
177
  spec =
91
178
  specSource === '--parametric'
92
179
  ? resolveParametricSpec(rawSpec)
93
- : validateDatasetSpec(rawSpec)
180
+ : withResolvedSeed(validateDatasetSpec(rawSpec))
94
181
  } catch (err) {
95
182
  console.error(`Schema validation error: ${err.message}`)
96
183
  process.exit(1)
97
184
  }
98
185
 
186
+ // Surface the drawn seed so a run is reproducible even without --emit-spec.
187
+ if (!seedProvided) {
188
+ console.error(
189
+ `Generated with seed ${spec.seed} (pass --emit-spec to capture)`,
190
+ )
191
+ }
192
+
99
193
  if (emitSpecPath !== undefined) {
100
194
  try {
101
195
  writeFileSync(emitSpecPath, `${JSON.stringify(spec, null, 2)}\n`)
@@ -111,13 +205,13 @@ if (dryRun) {
111
205
  let fileCount = 0
112
206
  let byteCount = 0
113
207
  try {
114
- for await (const file of generateCollectionFromSpec(spec)) {
208
+ for await (const file of previewCollection(spec)) {
115
209
  console.log(
116
- `${file.relativePath} (${file.type}, ${(file.buffer.length / 1024).toFixed(1)} KB)`,
210
+ `${file.relativePath} (${file.type}, ${(file.approxBytes / 1024).toFixed(1)} KB)`,
117
211
  )
118
212
  counts[file.type] = (counts[file.type] ?? 0) + 1
119
213
  fileCount++
120
- byteCount += file.buffer.length
214
+ byteCount += file.approxBytes
121
215
  }
122
216
  } catch (err) {
123
217
  console.error(`Error generating collection: ${err.message}`)
@@ -1,7 +1,12 @@
1
1
  // src/collection/writer.ts
2
- import { mkdirSync as mkdirSync2 } from "node:fs";
2
+ import { randomBytes as randomBytes2 } from "node:crypto";
3
+ import { mkdirSync as mkdirSync3 } from "node:fs";
3
4
  import { writeFile } from "node:fs/promises";
4
- import { dirname, resolve as resolve2 } from "node:path";
5
+ import { dirname, resolve as resolve3 } from "node:path";
6
+
7
+ // src/schema/limits.ts
8
+ var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
9
+ var MAX_STREAM_BYTES = 50 * 1024 * 1024 * 1024;
5
10
 
6
11
  // src/loadDcmjs.ts
7
12
  import dcmjsDefaultImport, * as dcmjsNamespace from "dcmjs";
@@ -45,7 +50,6 @@ function buildDicomdirBuffer() {
45
50
  }
46
51
 
47
52
  // src/schema/validate.ts
48
- var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
49
53
  var HEX_TAG_RE = /^[0-9a-fA-F]{8}$/;
50
54
 
51
55
  // src/syntheticFixtures/generator.ts
@@ -219,9 +223,17 @@ function buildBufferForSpec(spec, uid) {
219
223
  return buildNonDicomBuffer(spec.content);
220
224
  case "dicomdir":
221
225
  return buildDicomdirBuffer();
226
+ case "large-image":
227
+ throw new Error(
228
+ "large-image cannot be generated in memory \u2014 use writeCollectionFromSpec (disk-only streaming)"
229
+ );
222
230
  }
223
231
  }
224
232
 
233
+ // src/syntheticFixtures/streamWrite.ts
234
+ import { closeSync, mkdirSync as mkdirSync2, openSync, writeSync } from "node:fs";
235
+ import { resolve as resolve2 } from "node:path";
236
+
225
237
  // src/syntheticFixtures/uid.ts
226
238
  import { randomBytes } from "node:crypto";
227
239
  var ROLE_CODE = { study: 1, series: 2, sop: 3 };
@@ -287,6 +299,153 @@ function makeUidGenerator(seed) {
287
299
  };
288
300
  }
289
301
 
302
+ // src/syntheticFixtures/streamWrite.ts
303
+ var OW_MAX_BYTES = 4294967294;
304
+ var FRAGMENT_MAX_BYTES = 4294967294;
305
+ var DEFAULT_CHUNK_BYTES = 8 * 1024 * 1024;
306
+ var RLE_TRANSFER_SYNTAX_UID = "1.2.840.10008.1.2.5";
307
+ var PIXEL_DATA_OW_HEADER = Buffer.from([224, 127, 16, 0, 79, 87]);
308
+ var PIXEL_DATA_OB_HEADER = Buffer.from([224, 127, 16, 0, 79, 66]);
309
+ var ITEM_TAG_GROUP = 65534;
310
+ var ITEM_TAG_ELEMENT = 57344;
311
+ var SEQUENCE_DELIMITER_ELEMENT = 57565;
312
+ function itemTag(element, byteLength = 0) {
313
+ const b = Buffer.alloc(8);
314
+ b.writeUInt16LE(ITEM_TAG_GROUP, 0);
315
+ b.writeUInt16LE(element, 2);
316
+ b.writeUInt32LE(byteLength, 4);
317
+ return b;
318
+ }
319
+ function nativeDimensions(pixelBytes) {
320
+ const cells = Math.max(1, Math.floor(pixelBytes / 2));
321
+ const rows = Math.min(65535, Math.max(1, Math.round(Math.sqrt(cells))));
322
+ const columns = Math.min(65535, Math.max(1, Math.floor(cells / rows)));
323
+ return { rows, columns, bytes: rows * columns * 2 };
324
+ }
325
+ function streamZeros(fd, totalBytes, zeroChunk) {
326
+ let remaining = totalBytes;
327
+ while (remaining > 0) {
328
+ const n = Math.min(zeroChunk.length, remaining);
329
+ writeSync(fd, zeroChunk, 0, n);
330
+ remaining -= n;
331
+ }
332
+ }
333
+ function writeNative(outPath, params, dims, zeroChunk) {
334
+ const { modality, uid, tags } = params;
335
+ const meta = buildMeta(uid, modality);
336
+ const dataset = buildBaseImageDataset(uid, modality);
337
+ applyTagOverrides(dataset, tags);
338
+ dataset.Rows = dims.rows;
339
+ dataset.Columns = dims.columns;
340
+ const minimalPixelBytes = dataset.PixelData.byteLength;
341
+ const probe = serializeDict(meta, dataset);
342
+ const pixelElement = Buffer.alloc(12);
343
+ PIXEL_DATA_OW_HEADER.copy(pixelElement);
344
+ pixelElement.writeUInt32LE(minimalPixelBytes, 8);
345
+ const owIdx = probe.indexOf(pixelElement);
346
+ if (owIdx < 0) {
347
+ throw new Error("failed to locate native PixelData element header");
348
+ }
349
+ const dataStart = owIdx + 12;
350
+ const header = Buffer.from(probe.subarray(0, dataStart));
351
+ header.writeUInt32LE(dims.bytes, owIdx + 8);
352
+ const trailing = probe.subarray(dataStart + minimalPixelBytes);
353
+ const fd = openSync(outPath, "w");
354
+ try {
355
+ writeSync(fd, header);
356
+ streamZeros(fd, dims.bytes, zeroChunk);
357
+ if (trailing.length > 0) writeSync(fd, trailing);
358
+ } finally {
359
+ closeSync(fd);
360
+ }
361
+ return header.length + dims.bytes + trailing.length;
362
+ }
363
+ function writeEncapsulated(outPath, params, pixelBytes, fragmentBytes, zeroChunk) {
364
+ const { modality, uid, tags } = params;
365
+ const meta = {
366
+ ...buildMeta(uid, modality),
367
+ TransferSyntaxUID: RLE_TRANSFER_SYNTAX_UID
368
+ };
369
+ const dataset = buildBaseImageDataset(uid, modality);
370
+ applyTagOverrides(dataset, tags);
371
+ dataset.PixelData = [new Uint8Array([]).buffer, new Uint8Array([0, 0]).buffer];
372
+ dataset._vrMap = { PixelData: "OB" };
373
+ const probe = serializeDict(meta, dataset);
374
+ const idx = probe.indexOf(PIXEL_DATA_OB_HEADER);
375
+ if (idx < 0 || probe.readUInt32LE(idx + 8) !== 4294967295) {
376
+ throw new Error("failed to locate encapsulated PixelData element header");
377
+ }
378
+ const prefix = Buffer.from(probe.subarray(0, idx + 12));
379
+ const fd = openSync(outPath, "w");
380
+ let bytesWritten = 0;
381
+ try {
382
+ writeSync(fd, prefix);
383
+ bytesWritten += prefix.length;
384
+ const bot = itemTag(ITEM_TAG_ELEMENT, 0);
385
+ writeSync(fd, bot);
386
+ bytesWritten += bot.length;
387
+ let remaining = pixelBytes;
388
+ while (remaining > 0) {
389
+ const fragLen = Math.min(fragmentBytes, remaining);
390
+ const fh = itemTag(ITEM_TAG_ELEMENT, fragLen);
391
+ writeSync(fd, fh);
392
+ streamZeros(fd, fragLen, zeroChunk);
393
+ bytesWritten += fh.length + fragLen;
394
+ remaining -= fragLen;
395
+ }
396
+ const delimiter = itemTag(SEQUENCE_DELIMITER_ELEMENT);
397
+ writeSync(fd, delimiter);
398
+ bytesWritten += delimiter.length;
399
+ } finally {
400
+ closeSync(fd);
401
+ }
402
+ return bytesWritten;
403
+ }
404
+ function streamLargeImage(outPath, options) {
405
+ const {
406
+ targetBytes,
407
+ chunkBytes = DEFAULT_CHUNK_BYTES,
408
+ owMaxBytes = OW_MAX_BYTES,
409
+ fragmentBytes = FRAGMENT_MAX_BYTES
410
+ } = options;
411
+ const modality = options.modality ?? "CT";
412
+ const uid = options.uid ?? makeUidGenerator(options.seed)(0);
413
+ const identity = { modality, uid, tags: options.tags };
414
+ mkdirSync2(resolve2(outPath, ".."), { recursive: true });
415
+ const zeroChunk = Buffer.alloc(Math.max(1, chunkBytes));
416
+ const probe = serializeDict(
417
+ buildMeta(uid, modality),
418
+ buildBaseImageDataset(uid, modality)
419
+ );
420
+ const nativeHeaderLen = probe.length - 2;
421
+ const requestedPixel = Math.max(2, targetBytes - nativeHeaderLen);
422
+ if (requestedPixel <= owMaxBytes) {
423
+ const dims = nativeDimensions(requestedPixel);
424
+ const bytesWritten2 = writeNative(outPath, identity, dims, zeroChunk);
425
+ return {
426
+ path: outPath,
427
+ bytesWritten: bytesWritten2,
428
+ encoding: "native",
429
+ pixelBytes: dims.bytes
430
+ };
431
+ }
432
+ const estimateFrags = Math.max(
433
+ 1,
434
+ Math.ceil((targetBytes - nativeHeaderLen - 16) / fragmentBytes)
435
+ );
436
+ const overhead = nativeHeaderLen + 8 + estimateFrags * 8 + 8;
437
+ let pixelBytes = Math.max(2, targetBytes - overhead);
438
+ pixelBytes -= pixelBytes % 2;
439
+ const bytesWritten = writeEncapsulated(
440
+ outPath,
441
+ identity,
442
+ pixelBytes,
443
+ fragmentBytes - fragmentBytes % 2,
444
+ zeroChunk
445
+ );
446
+ return { path: outPath, bytesWritten, encoding: "encapsulated", pixelBytes };
447
+ }
448
+
290
449
  // src/syntheticFixtures/violations.ts
291
450
  var dcmjsAny = dcmjs;
292
451
  function parseBuffer(buffer) {
@@ -400,11 +559,13 @@ function studyFileCount(studies) {
400
559
  0
401
560
  );
402
561
  }
562
+ function resolveUid(seed, index, override) {
563
+ return { ...makeUidGenerator(seed)(index), ...override };
564
+ }
403
565
  async function generateFile(spec, options) {
404
566
  const index = options?.index ?? 0;
405
567
  const padWidth = options?.padWidth ?? 3;
406
- const uidGen = makeUidGenerator(options?.seed);
407
- const uid = { ...uidGen(index), ...options?.uid };
568
+ const uid = resolveUid(options?.seed, index, options?.uid);
408
569
  let buffer = buildBufferForSpec(spec, uid);
409
570
  const violations = "violations" in spec ? spec.violations : void 0;
410
571
  if (violations?.length) {
@@ -469,25 +630,45 @@ function hierarchicalName(studyOrdinal, seriesIndex, instanceNumber) {
469
630
  relativePath: `study-${pad3(studyOrdinal + 1)}/series-${pad3(seriesIndex + 1)}/${name}`
470
631
  };
471
632
  }
472
- async function* generateCollectionFromSpec(spec) {
633
+ function withResolvedSeed(spec) {
634
+ return spec.seed === void 0 ? { ...spec, seed: randomBytes2(4).readUInt32BE(0) } : spec;
635
+ }
636
+ function computeNames(type, index, padWidth, grouped, quirks) {
637
+ let names;
638
+ if (grouped) {
639
+ names = hierarchicalName(
640
+ grouped.studyOrdinal,
641
+ grouped.seriesIndex,
642
+ grouped.instanceNumber
643
+ );
644
+ } else {
645
+ const name = filename(type, index, padWidth);
646
+ names = { filename: name, relativePath: name };
647
+ }
648
+ return quirks.length === 0 ? names : applyPathQuirks(names.relativePath, quirks);
649
+ }
650
+ function* planCollection(spec) {
473
651
  const flatEntries = spec.entries ?? [];
474
652
  const studies = spec.studies ?? [];
475
653
  const totalFiles = entryCount(flatEntries) + studyFileCount(studies);
476
654
  const padWidth = String(totalFiles).length;
477
655
  const hierarchical = spec.layout === "hierarchical";
478
656
  const quirks = spec.pathQuirks ?? [];
479
- const decorate = (file) => quirks.length === 0 ? file : { ...file, ...applyPathQuirks(file.relativePath, quirks) };
480
657
  let globalIndex = 0;
481
658
  for (const entry of flatEntries) {
482
659
  const { count = 1, ...fileSpec } = entry;
483
660
  for (let i = 0; i < count; i++) {
484
- yield decorate(
485
- await generateFile(fileSpec, {
486
- index: globalIndex,
487
- seed: spec.seed,
488
- padWidth
489
- })
490
- );
661
+ yield {
662
+ fileSpec,
663
+ index: globalIndex,
664
+ ...computeNames(
665
+ fileSpec.type,
666
+ globalIndex,
667
+ padWidth,
668
+ void 0,
669
+ quirks
670
+ )
671
+ };
491
672
  globalIndex++;
492
673
  }
493
674
  }
@@ -510,25 +691,18 @@ async function* generateCollectionFromSpec(spec) {
510
691
  ...series.tags,
511
692
  ...fileSpec.tags
512
693
  };
513
- const file = await generateFile(
514
- { ...fileSpec, tags },
515
- {
516
- index: globalIndex,
517
- seed: spec.seed,
694
+ yield {
695
+ fileSpec: { ...fileSpec, tags },
696
+ index: globalIndex,
697
+ uidOverride: { study: studyUid, series: seriesUid },
698
+ ...computeNames(
699
+ fileSpec.type,
700
+ globalIndex,
518
701
  padWidth,
519
- uid: { study: studyUid, series: seriesUid }
520
- }
521
- );
522
- yield decorate(
523
- hierarchical ? {
524
- ...file,
525
- ...hierarchicalName(
526
- studyOrdinal,
527
- seriesIndex,
528
- instanceNumber
529
- )
530
- } : file
531
- );
702
+ hierarchical ? { studyOrdinal, seriesIndex, instanceNumber } : void 0,
703
+ quirks
704
+ )
705
+ };
532
706
  globalIndex++;
533
707
  }
534
708
  }
@@ -537,25 +711,84 @@ async function* generateCollectionFromSpec(spec) {
537
711
  }
538
712
  }
539
713
  }
714
+ async function materialisePlan(plan, seed) {
715
+ const file = await generateFile(plan.fileSpec, {
716
+ index: plan.index,
717
+ seed,
718
+ uid: plan.uidOverride
719
+ });
720
+ return { ...file, filename: plan.filename, relativePath: plan.relativePath };
721
+ }
722
+ async function* generateCollectionFromSpec(spec) {
723
+ for (const plan of planCollection(spec)) {
724
+ yield await materialisePlan(plan, spec.seed);
725
+ }
726
+ }
727
+ async function* previewCollection(spec) {
728
+ for (const plan of planCollection(spec)) {
729
+ if (plan.fileSpec.type === "large-image") {
730
+ yield {
731
+ relativePath: plan.relativePath,
732
+ type: "large-image",
733
+ index: plan.index,
734
+ approxBytes: plan.fileSpec.targetBytes
735
+ };
736
+ } else {
737
+ const file = await materialisePlan(plan, spec.seed);
738
+ yield {
739
+ relativePath: plan.relativePath,
740
+ type: plan.fileSpec.type,
741
+ index: plan.index,
742
+ approxBytes: file.buffer.length
743
+ };
744
+ }
745
+ }
746
+ }
540
747
  async function writeCollectionFromSpec(spec, outDir) {
541
- const root = resolve2(outDir);
542
- mkdirSync2(root, { recursive: true });
748
+ const root = resolve3(outDir);
749
+ mkdirSync3(root, { recursive: true });
543
750
  const manifest = [];
544
751
  const createdDirs = /* @__PURE__ */ new Set([root]);
545
- for await (const file of generateCollectionFromSpec(spec)) {
546
- const filePath = resolve2(root, file.relativePath);
752
+ const ensureDir = (filePath) => {
547
753
  const dir = dirname(filePath);
548
754
  if (!createdDirs.has(dir)) {
549
- mkdirSync2(dir, { recursive: true });
755
+ mkdirSync3(dir, { recursive: true });
550
756
  createdDirs.add(dir);
551
757
  }
552
- await writeFile(filePath, file.buffer);
553
- manifest.push({ path: filePath, type: file.type, index: file.index });
758
+ };
759
+ for (const plan of planCollection(spec)) {
760
+ const filePath = resolve3(root, plan.relativePath);
761
+ ensureDir(filePath);
762
+ if (plan.fileSpec.type === "large-image") {
763
+ const large = plan.fileSpec;
764
+ if (large.targetBytes > MAX_STREAM_BYTES) {
765
+ throw new Error(
766
+ `large-image targetBytes (${large.targetBytes}) exceeds the 50 GB limit`
767
+ );
768
+ }
769
+ const uid = resolveUid(spec.seed, plan.index, plan.uidOverride);
770
+ streamLargeImage(filePath, {
771
+ targetBytes: large.targetBytes,
772
+ modality: large.modality,
773
+ uid,
774
+ tags: large.tags
775
+ });
776
+ } else {
777
+ const file = await materialisePlan(plan, spec.seed);
778
+ await writeFile(filePath, file.buffer);
779
+ }
780
+ manifest.push({
781
+ path: filePath,
782
+ type: plan.fileSpec.type,
783
+ index: plan.index
784
+ });
554
785
  }
555
786
  return manifest;
556
787
  }
557
788
  export {
558
789
  generateCollectionFromSpec,
559
790
  generateFile,
791
+ previewCollection,
792
+ withResolvedSeed,
560
793
  writeCollectionFromSpec
561
794
  };