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.
@@ -0,0 +1,7 @@
1
+ // src/schema/limits.ts
2
+ var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
3
+ var MAX_STREAM_BYTES = 50 * 1024 * 1024 * 1024;
4
+ export {
5
+ MAX_PIXEL_BYTES,
6
+ MAX_STREAM_BYTES
7
+ };
@@ -15,11 +15,16 @@ function seededStream(seed, offset, a, b) {
15
15
  return lcg(seed * 999983 + offset + a * 1000003 + b * 7919 >>> 0);
16
16
  }
17
17
 
18
+ // src/schema/limits.ts
19
+ var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
20
+ var MAX_STREAM_BYTES = 50 * 1024 * 1024 * 1024;
21
+
18
22
  // src/schema/validate.ts
19
23
  var TYPE_CAPABILITIES = {
20
24
  "valid-image": { image: true },
21
25
  "invalid-uid-image": { image: true },
22
26
  "vendor-warnings-image": { image: true },
27
+ "large-image": { image: true },
23
28
  "fake-signature": { image: false },
24
29
  "non-dicom": { image: false },
25
30
  dicomdir: { image: false }
@@ -38,7 +43,6 @@ var VALID_TRANSFER_SYNTAXES = {
38
43
  "explicit-vr-little-endian": true,
39
44
  "implicit-vr-little-endian": true
40
45
  };
41
- var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
42
46
  var VALID_VIOLATIONS = {
43
47
  "uid-too-long": true,
44
48
  "non-conformant-uid": true,
@@ -56,6 +60,13 @@ function validatePositiveInt(value, path) {
56
60
  );
57
61
  }
58
62
  }
63
+ function validateNonNegativeInt(value, path) {
64
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
65
+ throw new Error(
66
+ `${path}: must be a non-negative integer; got ${JSON.stringify(value)}`
67
+ );
68
+ }
69
+ }
59
70
  function validateSeed(value, path) {
60
71
  if (typeof value !== "number" || !Number.isFinite(value)) {
61
72
  throw new Error(
@@ -68,6 +79,43 @@ function validateSizeKbLimit(kb, path) {
68
79
  throw new Error(`${path}: exceeds the 512 MB limit`);
69
80
  }
70
81
  }
82
+ function validateLargeTargetBytes(value, path) {
83
+ if (typeof value !== "number" || !Number.isInteger(value)) {
84
+ throw new Error(`${path}: must be an integer; got ${JSON.stringify(value)}`);
85
+ }
86
+ if (value <= MAX_PIXEL_BYTES) {
87
+ throw new Error(
88
+ `${path}: must exceed 512 MB \u2014 use targetSizeKb for smaller files`
89
+ );
90
+ }
91
+ if (value > MAX_STREAM_BYTES) {
92
+ throw new Error(`${path}: exceeds the 50 GB limit`);
93
+ }
94
+ }
95
+ function validateLargeImageEntry(e, path) {
96
+ for (const field of [
97
+ "rows",
98
+ "columns",
99
+ "frames",
100
+ "targetSizeKb",
101
+ "violations",
102
+ "transferSyntax"
103
+ ]) {
104
+ if (field in e) {
105
+ throw new Error(
106
+ `${path}.${field}: not supported for type "large-image" \u2014 use targetBytes`
107
+ );
108
+ }
109
+ }
110
+ if (!("targetBytes" in e)) {
111
+ throw new Error(`${path}.targetBytes: is required for type "large-image"`);
112
+ }
113
+ validateLargeTargetBytes(e.targetBytes, `${path}.targetBytes`);
114
+ if ("modality" in e) {
115
+ validateEnum(e.modality, VALID_MODALITIES, `${path}.modality`);
116
+ }
117
+ if ("tags" in e) validateTags(e.tags, `${path}.tags`);
118
+ }
71
119
  function validateEnum(value, valid, path) {
72
120
  if (typeof value !== "string" || !Object.hasOwn(valid, value)) {
73
121
  throw new Error(
@@ -91,6 +139,15 @@ function validateEntry(entry, path) {
91
139
  const type = e.type;
92
140
  const caps = TYPE_CAPABILITIES[type];
93
141
  if ("count" in e) validatePositiveInt(e.count, `${path}.count`);
142
+ if (type === "large-image") {
143
+ validateLargeImageEntry(e, path);
144
+ return e;
145
+ }
146
+ if ("targetBytes" in e) {
147
+ throw new Error(
148
+ `${path}.targetBytes: only supported for type "large-image"`
149
+ );
150
+ }
94
151
  for (const field of [
95
152
  "modality",
96
153
  "rows",
@@ -158,19 +215,20 @@ function validateTags(tags, path) {
158
215
  validateTagKey(key, path);
159
216
  }
160
217
  }
161
- function validateRange(value, path) {
218
+ function validateRange(value, path, options = {}) {
219
+ const checkInt = options.allowZero ? validateNonNegativeInt : validatePositiveInt;
162
220
  if (typeof value === "number") {
163
- validatePositiveInt(value, path);
221
+ checkInt(value, path);
164
222
  return value;
165
223
  }
166
224
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
167
225
  throw new Error(
168
- `${path}: must be a positive integer or { min, max }; got ${JSON.stringify(value)}`
226
+ `${path}: must be an integer or { min, max }; got ${JSON.stringify(value)}`
169
227
  );
170
228
  }
171
229
  const r = value;
172
- validatePositiveInt(r.min, `${path}.min`);
173
- validatePositiveInt(r.max, `${path}.max`);
230
+ checkInt(r.min, `${path}.min`);
231
+ checkInt(r.max, `${path}.max`);
174
232
  if (r.min > r.max) {
175
233
  throw new Error(`${path}: min (${r.min}) must be \u2264 max (${r.max})`);
176
234
  }
@@ -179,6 +237,9 @@ function validateRange(value, path) {
179
237
  function rangeMax(range) {
180
238
  return typeof range === "number" ? range : range.max;
181
239
  }
240
+ function rangeMin(range) {
241
+ return typeof range === "number" ? range : range.min;
242
+ }
182
243
  function validateParametricStudies(studies, path) {
183
244
  if (typeof studies !== "object" || studies === null || Array.isArray(studies)) {
184
245
  throw new Error(`${path}: must be an object`);
@@ -192,6 +253,32 @@ function validateParametricStudies(studies, path) {
192
253
  validateRange(s.fileSizeKb, `${path}.fileSizeKb`);
193
254
  validateSizeKbLimit(rangeMax(s.fileSizeKb), `${path}.fileSizeKb`);
194
255
  }
256
+ if ("largeFilesPerSeries" in s) {
257
+ validateRange(s.largeFilesPerSeries, `${path}.largeFilesPerSeries`, {
258
+ allowZero: true
259
+ });
260
+ if (!("largeFileBytes" in s)) {
261
+ throw new Error(
262
+ `${path}.largeFileBytes: is required when largeFilesPerSeries is set`
263
+ );
264
+ }
265
+ }
266
+ if ("largeFileBytes" in s) {
267
+ if (!("largeFilesPerSeries" in s)) {
268
+ throw new Error(
269
+ `${path}.largeFilesPerSeries: is required when largeFileBytes is set`
270
+ );
271
+ }
272
+ validateRange(s.largeFileBytes, `${path}.largeFileBytes`);
273
+ validateLargeTargetBytes(
274
+ rangeMin(s.largeFileBytes),
275
+ `${path}.largeFileBytes (min)`
276
+ );
277
+ validateLargeTargetBytes(
278
+ rangeMax(s.largeFileBytes),
279
+ `${path}.largeFileBytes (max)`
280
+ );
281
+ }
195
282
  if ("modalities" in s) {
196
283
  if (!Array.isArray(s.modalities) || s.modalities.length === 0) {
197
284
  throw new Error(`${path}.modalities: must be a non-empty array`);
@@ -255,6 +342,12 @@ function sample(next, range) {
255
342
  if (typeof range === "number") return range;
256
343
  return range.min + next() % (range.max - range.min + 1);
257
344
  }
345
+ var BYTES_PER_MB = 1024 * 1024;
346
+ function sampleBytes(next, range) {
347
+ if (typeof range === "number") return range;
348
+ const spanMb = Math.floor((range.max - range.min) / BYTES_PER_MB) + 1;
349
+ return range.min + next() % spanMb * BYTES_PER_MB;
350
+ }
258
351
  function pick(next, items) {
259
352
  return items[next() % items.length];
260
353
  }
@@ -295,6 +388,30 @@ function resolveParametricSpec(spec) {
295
388
  ...fileCount > 1 ? { count: fileCount } : {}
296
389
  });
297
390
  }
391
+ if (params.largeFilesPerSeries !== void 0 && params.largeFileBytes !== void 0) {
392
+ const largeCount = sample(next, params.largeFilesPerSeries);
393
+ const largeBase = {
394
+ type: "large-image",
395
+ targetBytes: 0,
396
+ ...modality !== void 0 ? { modality } : {}
397
+ };
398
+ if (typeof params.largeFileBytes === "number") {
399
+ if (largeCount > 0) {
400
+ entries2.push({
401
+ ...largeBase,
402
+ targetBytes: params.largeFileBytes,
403
+ ...largeCount > 1 ? { count: largeCount } : {}
404
+ });
405
+ }
406
+ } else {
407
+ for (let lf = 0; lf < largeCount; lf++) {
408
+ entries2.push({
409
+ ...largeBase,
410
+ targetBytes: sampleBytes(next, params.largeFileBytes)
411
+ });
412
+ }
413
+ }
414
+ }
298
415
  series.push({ entries: entries2 });
299
416
  }
300
417
  studies.push({
@@ -1,8 +1,13 @@
1
+ // src/schema/limits.ts
2
+ var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
3
+ var MAX_STREAM_BYTES = 50 * 1024 * 1024 * 1024;
4
+
1
5
  // src/schema/validate.ts
2
6
  var TYPE_CAPABILITIES = {
3
7
  "valid-image": { image: true },
4
8
  "invalid-uid-image": { image: true },
5
9
  "vendor-warnings-image": { image: true },
10
+ "large-image": { image: true },
6
11
  "fake-signature": { image: false },
7
12
  "non-dicom": { image: false },
8
13
  dicomdir: { image: false }
@@ -27,7 +32,6 @@ var VALID_TRANSFER_SYNTAXES = {
27
32
  "explicit-vr-little-endian": true,
28
33
  "implicit-vr-little-endian": true
29
34
  };
30
- var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
31
35
  var VALID_VIOLATIONS = {
32
36
  "uid-too-long": true,
33
37
  "non-conformant-uid": true,
@@ -45,6 +49,13 @@ function validatePositiveInt(value, path) {
45
49
  );
46
50
  }
47
51
  }
52
+ function validateNonNegativeInt(value, path) {
53
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
54
+ throw new Error(
55
+ `${path}: must be a non-negative integer; got ${JSON.stringify(value)}`
56
+ );
57
+ }
58
+ }
48
59
  function validateSeed(value, path) {
49
60
  if (typeof value !== "number" || !Number.isFinite(value)) {
50
61
  throw new Error(
@@ -57,6 +68,43 @@ function validateSizeKbLimit(kb, path) {
57
68
  throw new Error(`${path}: exceeds the 512 MB limit`);
58
69
  }
59
70
  }
71
+ function validateLargeTargetBytes(value, path) {
72
+ if (typeof value !== "number" || !Number.isInteger(value)) {
73
+ throw new Error(`${path}: must be an integer; got ${JSON.stringify(value)}`);
74
+ }
75
+ if (value <= MAX_PIXEL_BYTES) {
76
+ throw new Error(
77
+ `${path}: must exceed 512 MB \u2014 use targetSizeKb for smaller files`
78
+ );
79
+ }
80
+ if (value > MAX_STREAM_BYTES) {
81
+ throw new Error(`${path}: exceeds the 50 GB limit`);
82
+ }
83
+ }
84
+ function validateLargeImageEntry(e, path) {
85
+ for (const field of [
86
+ "rows",
87
+ "columns",
88
+ "frames",
89
+ "targetSizeKb",
90
+ "violations",
91
+ "transferSyntax"
92
+ ]) {
93
+ if (field in e) {
94
+ throw new Error(
95
+ `${path}.${field}: not supported for type "large-image" \u2014 use targetBytes`
96
+ );
97
+ }
98
+ }
99
+ if (!("targetBytes" in e)) {
100
+ throw new Error(`${path}.targetBytes: is required for type "large-image"`);
101
+ }
102
+ validateLargeTargetBytes(e.targetBytes, `${path}.targetBytes`);
103
+ if ("modality" in e) {
104
+ validateEnum(e.modality, VALID_MODALITIES, `${path}.modality`);
105
+ }
106
+ if ("tags" in e) validateTags(e.tags, `${path}.tags`);
107
+ }
60
108
  function validateEnum(value, valid, path) {
61
109
  if (typeof value !== "string" || !Object.hasOwn(valid, value)) {
62
110
  throw new Error(
@@ -80,6 +128,15 @@ function validateEntry(entry, path) {
80
128
  const type = e.type;
81
129
  const caps = TYPE_CAPABILITIES[type];
82
130
  if ("count" in e) validatePositiveInt(e.count, `${path}.count`);
131
+ if (type === "large-image") {
132
+ validateLargeImageEntry(e, path);
133
+ return e;
134
+ }
135
+ if ("targetBytes" in e) {
136
+ throw new Error(
137
+ `${path}.targetBytes: only supported for type "large-image"`
138
+ );
139
+ }
83
140
  for (const field of [
84
141
  "modality",
85
142
  "rows",
@@ -225,19 +282,20 @@ function validateDatasetSpec(raw) {
225
282
  ...r.pathQuirks !== void 0 ? { pathQuirks: r.pathQuirks } : {}
226
283
  };
227
284
  }
228
- function validateRange(value, path) {
285
+ function validateRange(value, path, options = {}) {
286
+ const checkInt = options.allowZero ? validateNonNegativeInt : validatePositiveInt;
229
287
  if (typeof value === "number") {
230
- validatePositiveInt(value, path);
288
+ checkInt(value, path);
231
289
  return value;
232
290
  }
233
291
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
234
292
  throw new Error(
235
- `${path}: must be a positive integer or { min, max }; got ${JSON.stringify(value)}`
293
+ `${path}: must be an integer or { min, max }; got ${JSON.stringify(value)}`
236
294
  );
237
295
  }
238
296
  const r = value;
239
- validatePositiveInt(r.min, `${path}.min`);
240
- validatePositiveInt(r.max, `${path}.max`);
297
+ checkInt(r.min, `${path}.min`);
298
+ checkInt(r.max, `${path}.max`);
241
299
  if (r.min > r.max) {
242
300
  throw new Error(`${path}: min (${r.min}) must be \u2264 max (${r.max})`);
243
301
  }
@@ -246,6 +304,9 @@ function validateRange(value, path) {
246
304
  function rangeMax(range) {
247
305
  return typeof range === "number" ? range : range.max;
248
306
  }
307
+ function rangeMin(range) {
308
+ return typeof range === "number" ? range : range.min;
309
+ }
249
310
  function validateParametricStudies(studies, path) {
250
311
  if (typeof studies !== "object" || studies === null || Array.isArray(studies)) {
251
312
  throw new Error(`${path}: must be an object`);
@@ -259,6 +320,32 @@ function validateParametricStudies(studies, path) {
259
320
  validateRange(s.fileSizeKb, `${path}.fileSizeKb`);
260
321
  validateSizeKbLimit(rangeMax(s.fileSizeKb), `${path}.fileSizeKb`);
261
322
  }
323
+ if ("largeFilesPerSeries" in s) {
324
+ validateRange(s.largeFilesPerSeries, `${path}.largeFilesPerSeries`, {
325
+ allowZero: true
326
+ });
327
+ if (!("largeFileBytes" in s)) {
328
+ throw new Error(
329
+ `${path}.largeFileBytes: is required when largeFilesPerSeries is set`
330
+ );
331
+ }
332
+ }
333
+ if ("largeFileBytes" in s) {
334
+ if (!("largeFilesPerSeries" in s)) {
335
+ throw new Error(
336
+ `${path}.largeFilesPerSeries: is required when largeFileBytes is set`
337
+ );
338
+ }
339
+ validateRange(s.largeFileBytes, `${path}.largeFileBytes`);
340
+ validateLargeTargetBytes(
341
+ rangeMin(s.largeFileBytes),
342
+ `${path}.largeFileBytes (min)`
343
+ );
344
+ validateLargeTargetBytes(
345
+ rangeMax(s.largeFileBytes),
346
+ `${path}.largeFileBytes (max)`
347
+ );
348
+ }
262
349
  if ("modalities" in s) {
263
350
  if (!Array.isArray(s.modalities) || s.modalities.length === 0) {
264
351
  throw new Error(`${path}.modalities: must be a non-empty array`);
@@ -39,8 +39,11 @@ function buildDicomdirBuffer() {
39
39
  return buf.subarray(0, off);
40
40
  }
41
41
 
42
- // src/schema/validate.ts
42
+ // src/schema/limits.ts
43
43
  var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
44
+ var MAX_STREAM_BYTES = 50 * 1024 * 1024 * 1024;
45
+
46
+ // src/schema/validate.ts
44
47
  var HEX_TAG_RE = /^[0-9a-fA-F]{8}$/;
45
48
 
46
49
  // src/syntheticFixtures/generator.ts
@@ -214,8 +217,16 @@ function buildBufferForSpec(spec, uid) {
214
217
  return buildNonDicomBuffer(spec.content);
215
218
  case "dicomdir":
216
219
  return buildDicomdirBuffer();
220
+ case "large-image":
221
+ throw new Error(
222
+ "large-image cannot be generated in memory \u2014 use writeCollectionFromSpec (disk-only streaming)"
223
+ );
217
224
  }
218
225
  }
219
226
  export {
220
- buildBufferForSpec
227
+ applyTagOverrides,
228
+ buildBaseImageDataset,
229
+ buildBufferForSpec,
230
+ buildMeta,
231
+ serializeDict
221
232
  };