dicom-synth 1.8.0 → 1.10.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/bin/dicom-synth-describe.mjs +28 -9
- package/bin/dicom-synth-generate.mjs +90 -5
- package/dist/esm/collection/writer.js +313 -39
- package/dist/esm/describe/describe.js +232 -41
- package/dist/esm/index.js +631 -86
- package/dist/esm/schema/limits.js +7 -0
- package/dist/esm/schema/parametric.js +156 -7
- package/dist/esm/schema/validate.js +126 -7
- package/dist/esm/syntheticFixtures/generator.js +13 -2
- package/dist/esm/syntheticFixtures/streamWrite.js +356 -0
- package/dist/esm/syntheticFixtures/tagTemplate.js +49 -0
- package/dist/types/collection/writer.d.ts +9 -0
- package/dist/types/describe/describe.d.ts +1 -0
- package/dist/types/index.d.ts +5 -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/dist/types/syntheticFixtures/tagTemplate.d.ts +11 -0
- package/package.json +1 -1
|
@@ -15,11 +15,32 @@ function seededStream(seed, offset, a, b) {
|
|
|
15
15
|
return lcg(seed * 999983 + offset + a * 1000003 + b * 7919 >>> 0);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// src/syntheticFixtures/tagTemplate.ts
|
|
19
|
+
var TEMPLATE_VOCAB = [
|
|
20
|
+
"index",
|
|
21
|
+
"studyIndex",
|
|
22
|
+
"seriesIndex",
|
|
23
|
+
"instanceNumber"
|
|
24
|
+
];
|
|
25
|
+
var TEMPLATE_PART_RE = /\{\{|\}\}|\{([a-zA-Z]+)\}/g;
|
|
26
|
+
function findTemplateTokens(value) {
|
|
27
|
+
const names = [];
|
|
28
|
+
for (const m of value.matchAll(TEMPLATE_PART_RE)) {
|
|
29
|
+
if (m[1] !== void 0) names.push(m[1]);
|
|
30
|
+
}
|
|
31
|
+
return names;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/schema/limits.ts
|
|
35
|
+
var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
|
|
36
|
+
var MAX_STREAM_BYTES = 50 * 1024 * 1024 * 1024;
|
|
37
|
+
|
|
18
38
|
// src/schema/validate.ts
|
|
19
39
|
var TYPE_CAPABILITIES = {
|
|
20
40
|
"valid-image": { image: true },
|
|
21
41
|
"invalid-uid-image": { image: true },
|
|
22
42
|
"vendor-warnings-image": { image: true },
|
|
43
|
+
"large-image": { image: true },
|
|
23
44
|
"fake-signature": { image: false },
|
|
24
45
|
"non-dicom": { image: false },
|
|
25
46
|
dicomdir: { image: false }
|
|
@@ -38,7 +59,6 @@ var VALID_TRANSFER_SYNTAXES = {
|
|
|
38
59
|
"explicit-vr-little-endian": true,
|
|
39
60
|
"implicit-vr-little-endian": true
|
|
40
61
|
};
|
|
41
|
-
var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
|
|
42
62
|
var VALID_VIOLATIONS = {
|
|
43
63
|
"uid-too-long": true,
|
|
44
64
|
"non-conformant-uid": true,
|
|
@@ -56,6 +76,13 @@ function validatePositiveInt(value, path) {
|
|
|
56
76
|
);
|
|
57
77
|
}
|
|
58
78
|
}
|
|
79
|
+
function validateNonNegativeInt(value, path) {
|
|
80
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`${path}: must be a non-negative integer; got ${JSON.stringify(value)}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
59
86
|
function validateSeed(value, path) {
|
|
60
87
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
61
88
|
throw new Error(
|
|
@@ -68,6 +95,43 @@ function validateSizeKbLimit(kb, path) {
|
|
|
68
95
|
throw new Error(`${path}: exceeds the 512 MB limit`);
|
|
69
96
|
}
|
|
70
97
|
}
|
|
98
|
+
function validateLargeTargetBytes(value, path) {
|
|
99
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
100
|
+
throw new Error(`${path}: must be an integer; got ${JSON.stringify(value)}`);
|
|
101
|
+
}
|
|
102
|
+
if (value <= MAX_PIXEL_BYTES) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`${path}: must exceed 512 MB \u2014 use targetSizeKb for smaller files`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (value > MAX_STREAM_BYTES) {
|
|
108
|
+
throw new Error(`${path}: exceeds the 50 GB limit`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function validateLargeImageEntry(e, path) {
|
|
112
|
+
for (const field of [
|
|
113
|
+
"rows",
|
|
114
|
+
"columns",
|
|
115
|
+
"frames",
|
|
116
|
+
"targetSizeKb",
|
|
117
|
+
"violations",
|
|
118
|
+
"transferSyntax"
|
|
119
|
+
]) {
|
|
120
|
+
if (field in e) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`${path}.${field}: not supported for type "large-image" \u2014 use targetBytes`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!("targetBytes" in e)) {
|
|
127
|
+
throw new Error(`${path}.targetBytes: is required for type "large-image"`);
|
|
128
|
+
}
|
|
129
|
+
validateLargeTargetBytes(e.targetBytes, `${path}.targetBytes`);
|
|
130
|
+
if ("modality" in e) {
|
|
131
|
+
validateEnum(e.modality, VALID_MODALITIES, `${path}.modality`);
|
|
132
|
+
}
|
|
133
|
+
if ("tags" in e) validateTags(e.tags, `${path}.tags`);
|
|
134
|
+
}
|
|
71
135
|
function validateEnum(value, valid, path) {
|
|
72
136
|
if (typeof value !== "string" || !Object.hasOwn(valid, value)) {
|
|
73
137
|
throw new Error(
|
|
@@ -91,6 +155,15 @@ function validateEntry(entry, path) {
|
|
|
91
155
|
const type = e.type;
|
|
92
156
|
const caps = TYPE_CAPABILITIES[type];
|
|
93
157
|
if ("count" in e) validatePositiveInt(e.count, `${path}.count`);
|
|
158
|
+
if (type === "large-image") {
|
|
159
|
+
validateLargeImageEntry(e, path);
|
|
160
|
+
return e;
|
|
161
|
+
}
|
|
162
|
+
if ("targetBytes" in e) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`${path}.targetBytes: only supported for type "large-image"`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
94
167
|
for (const field of [
|
|
95
168
|
"modality",
|
|
96
169
|
"rows",
|
|
@@ -150,27 +223,44 @@ function validateEntry(entry, path) {
|
|
|
150
223
|
}
|
|
151
224
|
return e;
|
|
152
225
|
}
|
|
226
|
+
var MODALITY_TAG = "00080060";
|
|
153
227
|
function validateTags(tags, path) {
|
|
154
228
|
if (typeof tags !== "object" || tags === null || Array.isArray(tags)) {
|
|
155
229
|
throw new Error(`${path}: must be an object`);
|
|
156
230
|
}
|
|
157
|
-
for (const key of Object.
|
|
231
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
158
232
|
validateTagKey(key, path);
|
|
233
|
+
const isModalityKey = key === "Modality" || key.toUpperCase() === MODALITY_TAG;
|
|
234
|
+
if (isModalityKey && typeof value === "string" && Object.hasOwn(VALID_MODALITIES, value)) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`${path}.${key}: set a preset modality (${Object.keys(VALID_MODALITIES).join("/")}) via the "modality" field, not tags \u2014 it is coupled to SOPClassUID and type-1 attributes`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (typeof value === "string") {
|
|
240
|
+
for (const name of findTemplateTokens(value)) {
|
|
241
|
+
if (!TEMPLATE_VOCAB.includes(name)) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`${path}.${key}: unknown placeholder "{${name}}" \u2014 allowed: ${TEMPLATE_VOCAB.map((v) => `{${v}}`).join(", ")}`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
159
248
|
}
|
|
160
249
|
}
|
|
161
|
-
function validateRange(value, path) {
|
|
250
|
+
function validateRange(value, path, options = {}) {
|
|
251
|
+
const checkInt = options.allowZero ? validateNonNegativeInt : validatePositiveInt;
|
|
162
252
|
if (typeof value === "number") {
|
|
163
|
-
|
|
253
|
+
checkInt(value, path);
|
|
164
254
|
return value;
|
|
165
255
|
}
|
|
166
256
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
167
257
|
throw new Error(
|
|
168
|
-
`${path}: must be
|
|
258
|
+
`${path}: must be an integer or { min, max }; got ${JSON.stringify(value)}`
|
|
169
259
|
);
|
|
170
260
|
}
|
|
171
261
|
const r = value;
|
|
172
|
-
|
|
173
|
-
|
|
262
|
+
checkInt(r.min, `${path}.min`);
|
|
263
|
+
checkInt(r.max, `${path}.max`);
|
|
174
264
|
if (r.min > r.max) {
|
|
175
265
|
throw new Error(`${path}: min (${r.min}) must be \u2264 max (${r.max})`);
|
|
176
266
|
}
|
|
@@ -179,6 +269,9 @@ function validateRange(value, path) {
|
|
|
179
269
|
function rangeMax(range) {
|
|
180
270
|
return typeof range === "number" ? range : range.max;
|
|
181
271
|
}
|
|
272
|
+
function rangeMin(range) {
|
|
273
|
+
return typeof range === "number" ? range : range.min;
|
|
274
|
+
}
|
|
182
275
|
function validateParametricStudies(studies, path) {
|
|
183
276
|
if (typeof studies !== "object" || studies === null || Array.isArray(studies)) {
|
|
184
277
|
throw new Error(`${path}: must be an object`);
|
|
@@ -192,6 +285,32 @@ function validateParametricStudies(studies, path) {
|
|
|
192
285
|
validateRange(s.fileSizeKb, `${path}.fileSizeKb`);
|
|
193
286
|
validateSizeKbLimit(rangeMax(s.fileSizeKb), `${path}.fileSizeKb`);
|
|
194
287
|
}
|
|
288
|
+
if ("largeFilesPerSeries" in s) {
|
|
289
|
+
validateRange(s.largeFilesPerSeries, `${path}.largeFilesPerSeries`, {
|
|
290
|
+
allowZero: true
|
|
291
|
+
});
|
|
292
|
+
if (!("largeFileBytes" in s)) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
`${path}.largeFileBytes: is required when largeFilesPerSeries is set`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if ("largeFileBytes" in s) {
|
|
299
|
+
if (!("largeFilesPerSeries" in s)) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`${path}.largeFilesPerSeries: is required when largeFileBytes is set`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
validateRange(s.largeFileBytes, `${path}.largeFileBytes`);
|
|
305
|
+
validateLargeTargetBytes(
|
|
306
|
+
rangeMin(s.largeFileBytes),
|
|
307
|
+
`${path}.largeFileBytes (min)`
|
|
308
|
+
);
|
|
309
|
+
validateLargeTargetBytes(
|
|
310
|
+
rangeMax(s.largeFileBytes),
|
|
311
|
+
`${path}.largeFileBytes (max)`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
195
314
|
if ("modalities" in s) {
|
|
196
315
|
if (!Array.isArray(s.modalities) || s.modalities.length === 0) {
|
|
197
316
|
throw new Error(`${path}.modalities: must be a non-empty array`);
|
|
@@ -255,6 +374,12 @@ function sample(next, range) {
|
|
|
255
374
|
if (typeof range === "number") return range;
|
|
256
375
|
return range.min + next() % (range.max - range.min + 1);
|
|
257
376
|
}
|
|
377
|
+
var BYTES_PER_MB = 1024 * 1024;
|
|
378
|
+
function sampleBytes(next, range) {
|
|
379
|
+
if (typeof range === "number") return range;
|
|
380
|
+
const spanMb = Math.floor((range.max - range.min) / BYTES_PER_MB) + 1;
|
|
381
|
+
return range.min + next() % spanMb * BYTES_PER_MB;
|
|
382
|
+
}
|
|
258
383
|
function pick(next, items) {
|
|
259
384
|
return items[next() % items.length];
|
|
260
385
|
}
|
|
@@ -295,6 +420,30 @@ function resolveParametricSpec(spec) {
|
|
|
295
420
|
...fileCount > 1 ? { count: fileCount } : {}
|
|
296
421
|
});
|
|
297
422
|
}
|
|
423
|
+
if (params.largeFilesPerSeries !== void 0 && params.largeFileBytes !== void 0) {
|
|
424
|
+
const largeCount = sample(next, params.largeFilesPerSeries);
|
|
425
|
+
const largeBase = {
|
|
426
|
+
type: "large-image",
|
|
427
|
+
targetBytes: 0,
|
|
428
|
+
...modality !== void 0 ? { modality } : {}
|
|
429
|
+
};
|
|
430
|
+
if (typeof params.largeFileBytes === "number") {
|
|
431
|
+
if (largeCount > 0) {
|
|
432
|
+
entries2.push({
|
|
433
|
+
...largeBase,
|
|
434
|
+
targetBytes: params.largeFileBytes,
|
|
435
|
+
...largeCount > 1 ? { count: largeCount } : {}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
for (let lf = 0; lf < largeCount; lf++) {
|
|
440
|
+
entries2.push({
|
|
441
|
+
...largeBase,
|
|
442
|
+
targetBytes: sampleBytes(next, params.largeFileBytes)
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
298
447
|
series.push({ entries: entries2 });
|
|
299
448
|
}
|
|
300
449
|
studies.push({
|
|
@@ -1,8 +1,29 @@
|
|
|
1
|
+
// src/syntheticFixtures/tagTemplate.ts
|
|
2
|
+
var TEMPLATE_VOCAB = [
|
|
3
|
+
"index",
|
|
4
|
+
"studyIndex",
|
|
5
|
+
"seriesIndex",
|
|
6
|
+
"instanceNumber"
|
|
7
|
+
];
|
|
8
|
+
var TEMPLATE_PART_RE = /\{\{|\}\}|\{([a-zA-Z]+)\}/g;
|
|
9
|
+
function findTemplateTokens(value) {
|
|
10
|
+
const names = [];
|
|
11
|
+
for (const m of value.matchAll(TEMPLATE_PART_RE)) {
|
|
12
|
+
if (m[1] !== void 0) names.push(m[1]);
|
|
13
|
+
}
|
|
14
|
+
return names;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/schema/limits.ts
|
|
18
|
+
var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
|
|
19
|
+
var MAX_STREAM_BYTES = 50 * 1024 * 1024 * 1024;
|
|
20
|
+
|
|
1
21
|
// src/schema/validate.ts
|
|
2
22
|
var TYPE_CAPABILITIES = {
|
|
3
23
|
"valid-image": { image: true },
|
|
4
24
|
"invalid-uid-image": { image: true },
|
|
5
25
|
"vendor-warnings-image": { image: true },
|
|
26
|
+
"large-image": { image: true },
|
|
6
27
|
"fake-signature": { image: false },
|
|
7
28
|
"non-dicom": { image: false },
|
|
8
29
|
dicomdir: { image: false }
|
|
@@ -27,7 +48,6 @@ var VALID_TRANSFER_SYNTAXES = {
|
|
|
27
48
|
"explicit-vr-little-endian": true,
|
|
28
49
|
"implicit-vr-little-endian": true
|
|
29
50
|
};
|
|
30
|
-
var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
|
|
31
51
|
var VALID_VIOLATIONS = {
|
|
32
52
|
"uid-too-long": true,
|
|
33
53
|
"non-conformant-uid": true,
|
|
@@ -45,6 +65,13 @@ function validatePositiveInt(value, path) {
|
|
|
45
65
|
);
|
|
46
66
|
}
|
|
47
67
|
}
|
|
68
|
+
function validateNonNegativeInt(value, path) {
|
|
69
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`${path}: must be a non-negative integer; got ${JSON.stringify(value)}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
48
75
|
function validateSeed(value, path) {
|
|
49
76
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
50
77
|
throw new Error(
|
|
@@ -57,6 +84,43 @@ function validateSizeKbLimit(kb, path) {
|
|
|
57
84
|
throw new Error(`${path}: exceeds the 512 MB limit`);
|
|
58
85
|
}
|
|
59
86
|
}
|
|
87
|
+
function validateLargeTargetBytes(value, path) {
|
|
88
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
89
|
+
throw new Error(`${path}: must be an integer; got ${JSON.stringify(value)}`);
|
|
90
|
+
}
|
|
91
|
+
if (value <= MAX_PIXEL_BYTES) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`${path}: must exceed 512 MB \u2014 use targetSizeKb for smaller files`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (value > MAX_STREAM_BYTES) {
|
|
97
|
+
throw new Error(`${path}: exceeds the 50 GB limit`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function validateLargeImageEntry(e, path) {
|
|
101
|
+
for (const field of [
|
|
102
|
+
"rows",
|
|
103
|
+
"columns",
|
|
104
|
+
"frames",
|
|
105
|
+
"targetSizeKb",
|
|
106
|
+
"violations",
|
|
107
|
+
"transferSyntax"
|
|
108
|
+
]) {
|
|
109
|
+
if (field in e) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`${path}.${field}: not supported for type "large-image" \u2014 use targetBytes`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!("targetBytes" in e)) {
|
|
116
|
+
throw new Error(`${path}.targetBytes: is required for type "large-image"`);
|
|
117
|
+
}
|
|
118
|
+
validateLargeTargetBytes(e.targetBytes, `${path}.targetBytes`);
|
|
119
|
+
if ("modality" in e) {
|
|
120
|
+
validateEnum(e.modality, VALID_MODALITIES, `${path}.modality`);
|
|
121
|
+
}
|
|
122
|
+
if ("tags" in e) validateTags(e.tags, `${path}.tags`);
|
|
123
|
+
}
|
|
60
124
|
function validateEnum(value, valid, path) {
|
|
61
125
|
if (typeof value !== "string" || !Object.hasOwn(valid, value)) {
|
|
62
126
|
throw new Error(
|
|
@@ -80,6 +144,15 @@ function validateEntry(entry, path) {
|
|
|
80
144
|
const type = e.type;
|
|
81
145
|
const caps = TYPE_CAPABILITIES[type];
|
|
82
146
|
if ("count" in e) validatePositiveInt(e.count, `${path}.count`);
|
|
147
|
+
if (type === "large-image") {
|
|
148
|
+
validateLargeImageEntry(e, path);
|
|
149
|
+
return e;
|
|
150
|
+
}
|
|
151
|
+
if ("targetBytes" in e) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`${path}.targetBytes: only supported for type "large-image"`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
83
156
|
for (const field of [
|
|
84
157
|
"modality",
|
|
85
158
|
"rows",
|
|
@@ -139,12 +212,28 @@ function validateEntry(entry, path) {
|
|
|
139
212
|
}
|
|
140
213
|
return e;
|
|
141
214
|
}
|
|
215
|
+
var MODALITY_TAG = "00080060";
|
|
142
216
|
function validateTags(tags, path) {
|
|
143
217
|
if (typeof tags !== "object" || tags === null || Array.isArray(tags)) {
|
|
144
218
|
throw new Error(`${path}: must be an object`);
|
|
145
219
|
}
|
|
146
|
-
for (const key of Object.
|
|
220
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
147
221
|
validateTagKey(key, path);
|
|
222
|
+
const isModalityKey = key === "Modality" || key.toUpperCase() === MODALITY_TAG;
|
|
223
|
+
if (isModalityKey && typeof value === "string" && Object.hasOwn(VALID_MODALITIES, value)) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`${path}.${key}: set a preset modality (${Object.keys(VALID_MODALITIES).join("/")}) via the "modality" field, not tags \u2014 it is coupled to SOPClassUID and type-1 attributes`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (typeof value === "string") {
|
|
229
|
+
for (const name of findTemplateTokens(value)) {
|
|
230
|
+
if (!TEMPLATE_VOCAB.includes(name)) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`${path}.${key}: unknown placeholder "{${name}}" \u2014 allowed: ${TEMPLATE_VOCAB.map((v) => `{${v}}`).join(", ")}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
148
237
|
}
|
|
149
238
|
}
|
|
150
239
|
function validateSeries(series, path) {
|
|
@@ -225,19 +314,20 @@ function validateDatasetSpec(raw) {
|
|
|
225
314
|
...r.pathQuirks !== void 0 ? { pathQuirks: r.pathQuirks } : {}
|
|
226
315
|
};
|
|
227
316
|
}
|
|
228
|
-
function validateRange(value, path) {
|
|
317
|
+
function validateRange(value, path, options = {}) {
|
|
318
|
+
const checkInt = options.allowZero ? validateNonNegativeInt : validatePositiveInt;
|
|
229
319
|
if (typeof value === "number") {
|
|
230
|
-
|
|
320
|
+
checkInt(value, path);
|
|
231
321
|
return value;
|
|
232
322
|
}
|
|
233
323
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
234
324
|
throw new Error(
|
|
235
|
-
`${path}: must be
|
|
325
|
+
`${path}: must be an integer or { min, max }; got ${JSON.stringify(value)}`
|
|
236
326
|
);
|
|
237
327
|
}
|
|
238
328
|
const r = value;
|
|
239
|
-
|
|
240
|
-
|
|
329
|
+
checkInt(r.min, `${path}.min`);
|
|
330
|
+
checkInt(r.max, `${path}.max`);
|
|
241
331
|
if (r.min > r.max) {
|
|
242
332
|
throw new Error(`${path}: min (${r.min}) must be \u2264 max (${r.max})`);
|
|
243
333
|
}
|
|
@@ -246,6 +336,9 @@ function validateRange(value, path) {
|
|
|
246
336
|
function rangeMax(range) {
|
|
247
337
|
return typeof range === "number" ? range : range.max;
|
|
248
338
|
}
|
|
339
|
+
function rangeMin(range) {
|
|
340
|
+
return typeof range === "number" ? range : range.min;
|
|
341
|
+
}
|
|
249
342
|
function validateParametricStudies(studies, path) {
|
|
250
343
|
if (typeof studies !== "object" || studies === null || Array.isArray(studies)) {
|
|
251
344
|
throw new Error(`${path}: must be an object`);
|
|
@@ -259,6 +352,32 @@ function validateParametricStudies(studies, path) {
|
|
|
259
352
|
validateRange(s.fileSizeKb, `${path}.fileSizeKb`);
|
|
260
353
|
validateSizeKbLimit(rangeMax(s.fileSizeKb), `${path}.fileSizeKb`);
|
|
261
354
|
}
|
|
355
|
+
if ("largeFilesPerSeries" in s) {
|
|
356
|
+
validateRange(s.largeFilesPerSeries, `${path}.largeFilesPerSeries`, {
|
|
357
|
+
allowZero: true
|
|
358
|
+
});
|
|
359
|
+
if (!("largeFileBytes" in s)) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`${path}.largeFileBytes: is required when largeFilesPerSeries is set`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if ("largeFileBytes" in s) {
|
|
366
|
+
if (!("largeFilesPerSeries" in s)) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
`${path}.largeFilesPerSeries: is required when largeFileBytes is set`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
validateRange(s.largeFileBytes, `${path}.largeFileBytes`);
|
|
372
|
+
validateLargeTargetBytes(
|
|
373
|
+
rangeMin(s.largeFileBytes),
|
|
374
|
+
`${path}.largeFileBytes (min)`
|
|
375
|
+
);
|
|
376
|
+
validateLargeTargetBytes(
|
|
377
|
+
rangeMax(s.largeFileBytes),
|
|
378
|
+
`${path}.largeFileBytes (max)`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
262
381
|
if ("modalities" in s) {
|
|
263
382
|
if (!Array.isArray(s.modalities) || s.modalities.length === 0) {
|
|
264
383
|
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
|
};
|