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.
- package/README.md +17 -601
- package/bin/dicom-synth-generate.mjs +106 -12
- package/dist/esm/collection/writer.js +272 -39
- package/dist/esm/describe/describe.js +159 -29
- package/dist/esm/index.js +526 -76
- package/dist/esm/schema/limits.js +7 -0
- package/dist/esm/schema/parametric.js +123 -6
- package/dist/esm/schema/validate.js +93 -6
- package/dist/esm/syntheticFixtures/generator.js +13 -2
- package/dist/esm/syntheticFixtures/streamWrite.js +356 -0
- package/dist/types/collection/writer.d.ts +8 -0
- package/dist/types/describe/describe.d.ts +1 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/schema/limits.d.ts +2 -0
- package/dist/types/schema/types.d.ts +10 -2
- package/dist/types/syntheticFixtures/generator.d.ts +5 -1
- package/dist/types/syntheticFixtures/streamWrite.d.ts +49 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
226
|
+
`${path}: must be an integer or { min, max }; got ${JSON.stringify(value)}`
|
|
169
227
|
);
|
|
170
228
|
}
|
|
171
229
|
const r = value;
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
|
293
|
+
`${path}: must be an integer or { min, max }; got ${JSON.stringify(value)}`
|
|
236
294
|
);
|
|
237
295
|
}
|
|
238
296
|
const r = value;
|
|
239
|
-
|
|
240
|
-
|
|
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/
|
|
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
|
-
|
|
227
|
+
applyTagOverrides,
|
|
228
|
+
buildBaseImageDataset,
|
|
229
|
+
buildBufferForSpec,
|
|
230
|
+
buildMeta,
|
|
231
|
+
serializeDict
|
|
221
232
|
};
|