dicom-synth 1.3.0 → 1.5.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,327 @@
1
+ // src/schema/parametric.ts
2
+ import { randomBytes as randomBytes2 } from "node:crypto";
3
+
4
+ // src/syntheticFixtures/uid.ts
5
+ import { randomBytes } from "node:crypto";
6
+ function lcg(seed) {
7
+ let s = seed >>> 0;
8
+ return () => {
9
+ s = Math.imul(1664525, s) + 1013904223 >>> 0;
10
+ return s;
11
+ };
12
+ }
13
+ var PARAMETRIC_STREAM_OFFSET = 2246822507;
14
+ function seededStream(seed, offset, a, b) {
15
+ return lcg(seed * 999983 + offset + a * 1000003 + b * 7919 >>> 0);
16
+ }
17
+
18
+ // src/schema/validate.ts
19
+ var TYPE_CAPABILITIES = {
20
+ "valid-image": { image: true },
21
+ "invalid-uid-image": { image: true },
22
+ "vendor-warnings-image": { image: true },
23
+ "fake-signature": { image: false },
24
+ "non-dicom": { image: false },
25
+ dicomdir: { image: false }
26
+ };
27
+ var VALID_MODALITIES = {
28
+ CT: true,
29
+ PT: true,
30
+ MR: true,
31
+ CR: true
32
+ };
33
+ var VALID_LAYOUTS = {
34
+ flat: true,
35
+ hierarchical: true
36
+ };
37
+ var VALID_TRANSFER_SYNTAXES = {
38
+ "explicit-vr-little-endian": true,
39
+ "implicit-vr-little-endian": true
40
+ };
41
+ var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
42
+ var VALID_VIOLATIONS = {
43
+ "uid-too-long": true,
44
+ "non-conformant-uid": true,
45
+ "missing-meta-header": true,
46
+ "malformed-sq-delimiter": true,
47
+ "vr-max-length-exceeded": true,
48
+ "missing-type1-tag": true
49
+ };
50
+ var HEX_TAG_RE = /^[0-9a-fA-F]{8}$/;
51
+ var KEYWORD_RE = /^[A-Z][A-Za-z0-9]*$/;
52
+ function validatePositiveInt(value, path) {
53
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
54
+ throw new Error(
55
+ `${path}: must be a positive integer; got ${JSON.stringify(value)}`
56
+ );
57
+ }
58
+ }
59
+ function validateSeed(value, path) {
60
+ if (typeof value !== "number" || !Number.isFinite(value)) {
61
+ throw new Error(
62
+ `${path}: must be a finite number; got ${JSON.stringify(value)}`
63
+ );
64
+ }
65
+ }
66
+ function validateSizeKbLimit(kb, path) {
67
+ if (kb * 1024 > MAX_PIXEL_BYTES) {
68
+ throw new Error(`${path}: exceeds the 512 MB limit`);
69
+ }
70
+ }
71
+ function validateEnum(value, valid, path) {
72
+ if (typeof value !== "string" || !Object.hasOwn(valid, value)) {
73
+ throw new Error(
74
+ `${path}: must be one of ${Object.keys(valid).join(", ")}; got ${JSON.stringify(value)}`
75
+ );
76
+ }
77
+ }
78
+ function validateTagKey(key, path) {
79
+ if (!HEX_TAG_RE.test(key) && !KEYWORD_RE.test(key)) {
80
+ throw new Error(
81
+ `${path}: invalid tag key "${key}" \u2014 must be a DICOM keyword name (e.g. "Modality") or an 8-hex-char tag (e.g. "00080060")`
82
+ );
83
+ }
84
+ }
85
+ function validateEntry(entry, path) {
86
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
87
+ throw new Error(`${path}: must be an object`);
88
+ }
89
+ const e = entry;
90
+ validateEnum(e.type, TYPE_CAPABILITIES, `${path}.type`);
91
+ const type = e.type;
92
+ const caps = TYPE_CAPABILITIES[type];
93
+ if ("count" in e) validatePositiveInt(e.count, `${path}.count`);
94
+ for (const field of [
95
+ "modality",
96
+ "rows",
97
+ "columns",
98
+ "frames",
99
+ "targetSizeKb",
100
+ "tags",
101
+ "violations",
102
+ "transferSyntax"
103
+ ]) {
104
+ if (!caps.image && field in e)
105
+ throw new Error(`${path}.${field}: not supported for type "${type}"`);
106
+ }
107
+ if ("transferSyntax" in e) {
108
+ validateEnum(
109
+ e.transferSyntax,
110
+ VALID_TRANSFER_SYNTAXES,
111
+ `${path}.transferSyntax`
112
+ );
113
+ }
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
+ if ("violations" in e) {
119
+ if (!Array.isArray(e.violations)) {
120
+ throw new Error(`${path}.violations: must be an array`);
121
+ }
122
+ for (const [i, v] of e.violations.entries()) {
123
+ validateEnum(v, VALID_VIOLATIONS, `${path}.violations[${i}]`);
124
+ }
125
+ }
126
+ const hasDims = "rows" in e || "columns" in e || "frames" in e;
127
+ if ("targetSizeKb" in e) {
128
+ if (hasDims) {
129
+ throw new Error(
130
+ `${path}: targetSizeKb cannot be combined with rows/columns/frames \u2014 use one sizing mechanism`
131
+ );
132
+ }
133
+ validatePositiveInt(e.targetSizeKb, `${path}.targetSizeKb`);
134
+ validateSizeKbLimit(e.targetSizeKb, `${path}.targetSizeKb`);
135
+ } else if (hasDims) {
136
+ if (!("rows" in e) || !("columns" in e)) {
137
+ throw new Error(`${path}: rows and columns must be provided together`);
138
+ }
139
+ validatePositiveInt(e.rows, `${path}.rows`);
140
+ validatePositiveInt(e.columns, `${path}.columns`);
141
+ if ("frames" in e) validatePositiveInt(e.frames, `${path}.frames`);
142
+ const rows = e.rows;
143
+ const columns = e.columns;
144
+ const frames = typeof e.frames === "number" ? e.frames : 1;
145
+ if (rows * columns * frames * 2 > MAX_PIXEL_BYTES) {
146
+ throw new Error(
147
+ `${path}: pixel data (${rows}\xD7${columns}\xD7${frames}\xD72 bytes) exceeds the 512 MB limit`
148
+ );
149
+ }
150
+ }
151
+ return e;
152
+ }
153
+ function validateTags(tags, path) {
154
+ if (typeof tags !== "object" || tags === null || Array.isArray(tags)) {
155
+ throw new Error(`${path}: must be an object`);
156
+ }
157
+ for (const key of Object.keys(tags)) {
158
+ validateTagKey(key, path);
159
+ }
160
+ }
161
+ function validateRange(value, path) {
162
+ if (typeof value === "number") {
163
+ validatePositiveInt(value, path);
164
+ return value;
165
+ }
166
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
167
+ throw new Error(
168
+ `${path}: must be a positive integer or { min, max }; got ${JSON.stringify(value)}`
169
+ );
170
+ }
171
+ const r = value;
172
+ validatePositiveInt(r.min, `${path}.min`);
173
+ validatePositiveInt(r.max, `${path}.max`);
174
+ if (r.min > r.max) {
175
+ throw new Error(`${path}: min (${r.min}) must be \u2264 max (${r.max})`);
176
+ }
177
+ return value;
178
+ }
179
+ function rangeMax(range) {
180
+ return typeof range === "number" ? range : range.max;
181
+ }
182
+ function validateParametricStudies(studies, path) {
183
+ if (typeof studies !== "object" || studies === null || Array.isArray(studies)) {
184
+ throw new Error(`${path}: must be an object`);
185
+ }
186
+ const s = studies;
187
+ for (const field of ["count", "seriesPerStudy", "filesPerSeries"]) {
188
+ if (!(field in s)) throw new Error(`${path}.${field}: is required`);
189
+ validateRange(s[field], `${path}.${field}`);
190
+ }
191
+ if ("fileSizeKb" in s) {
192
+ validateRange(s.fileSizeKb, `${path}.fileSizeKb`);
193
+ validateSizeKbLimit(rangeMax(s.fileSizeKb), `${path}.fileSizeKb`);
194
+ }
195
+ if ("modalities" in s) {
196
+ if (!Array.isArray(s.modalities) || s.modalities.length === 0) {
197
+ throw new Error(`${path}.modalities: must be a non-empty array`);
198
+ }
199
+ for (const [i, m] of s.modalities.entries()) {
200
+ validateEnum(m, VALID_MODALITIES, `${path}.modalities[${i}]`);
201
+ }
202
+ }
203
+ if ("tags" in s) validateTags(s.tags, `${path}.tags`);
204
+ return s;
205
+ }
206
+ function validateEdgeCase(edgeCase, path) {
207
+ const e = validateEntry(edgeCase, path);
208
+ if ("frequency" in e) {
209
+ if ("count" in e) {
210
+ throw new Error(
211
+ `${path}: frequency cannot be combined with count \u2014 frequency draws the count at resolution time`
212
+ );
213
+ }
214
+ const f = e.frequency;
215
+ if (typeof f !== "number" || !(f >= 0 && f <= 1)) {
216
+ throw new Error(
217
+ `${path}.frequency: must be a number between 0 and 1; got ${JSON.stringify(f)}`
218
+ );
219
+ }
220
+ }
221
+ return e;
222
+ }
223
+ function validateParametricSpec(raw) {
224
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
225
+ throw new Error("ParametricSpec: must be a JSON object");
226
+ }
227
+ const r = raw;
228
+ if (!("studies" in r)) {
229
+ throw new Error("ParametricSpec.studies: is required");
230
+ }
231
+ const studies = validateParametricStudies(r.studies, "studies");
232
+ const edgeCases = [];
233
+ if ("edgeCases" in r) {
234
+ if (!Array.isArray(r.edgeCases)) {
235
+ throw new Error("ParametricSpec.edgeCases: must be an array");
236
+ }
237
+ for (const [i, e] of r.edgeCases.entries()) {
238
+ edgeCases.push(validateEdgeCase(e, `edgeCases[${i}]`));
239
+ }
240
+ }
241
+ if ("seed" in r) validateSeed(r.seed, "ParametricSpec.seed");
242
+ if ("layout" in r) {
243
+ validateEnum(r.layout, VALID_LAYOUTS, "ParametricSpec.layout");
244
+ }
245
+ return {
246
+ studies,
247
+ ...edgeCases.length > 0 ? { edgeCases } : {},
248
+ ...r.seed !== void 0 ? { seed: r.seed } : {},
249
+ ...r.layout !== void 0 ? { layout: r.layout } : {}
250
+ };
251
+ }
252
+
253
+ // src/schema/parametric.ts
254
+ function sample(next, range) {
255
+ if (typeof range === "number") return range;
256
+ return range.min + next() % (range.max - range.min + 1);
257
+ }
258
+ function pick(next, items) {
259
+ return items[next() % items.length];
260
+ }
261
+ function fraction(next) {
262
+ return next() / 4294967296;
263
+ }
264
+ function resolveParametricSpec(spec) {
265
+ const validated = validateParametricSpec(spec);
266
+ const seed = validated.seed ?? randomBytes2(4).readUInt32BE(0);
267
+ const next = seededStream(seed, PARAMETRIC_STREAM_OFFSET, 0, 0);
268
+ const params = validated.studies;
269
+ const studies = [];
270
+ let totalFiles = 0;
271
+ const studyCount = sample(next, params.count);
272
+ for (let st = 0; st < studyCount; st++) {
273
+ const seriesCount = sample(next, params.seriesPerStudy);
274
+ const series = [];
275
+ for (let se = 0; se < seriesCount; se++) {
276
+ const fileCount = sample(next, params.filesPerSeries);
277
+ totalFiles += fileCount;
278
+ const modality = params.modalities ? pick(next, params.modalities) : void 0;
279
+ const base = {
280
+ type: "valid-image",
281
+ ...modality !== void 0 ? { modality } : {}
282
+ };
283
+ const entries2 = [];
284
+ if (params.fileSizeKb !== void 0 && typeof params.fileSizeKb !== "number") {
285
+ for (let f = 0; f < fileCount; f++) {
286
+ entries2.push({
287
+ ...base,
288
+ targetSizeKb: sample(next, params.fileSizeKb)
289
+ });
290
+ }
291
+ } else {
292
+ entries2.push({
293
+ ...base,
294
+ ...params.fileSizeKb !== void 0 ? { targetSizeKb: params.fileSizeKb } : {},
295
+ ...fileCount > 1 ? { count: fileCount } : {}
296
+ });
297
+ }
298
+ series.push({ entries: entries2 });
299
+ }
300
+ studies.push({
301
+ series,
302
+ ...params.tags !== void 0 ? { tags: params.tags } : {}
303
+ });
304
+ }
305
+ const entries = [];
306
+ for (const edgeCase of validated.edgeCases ?? []) {
307
+ const { frequency, ...entry } = edgeCase;
308
+ if (frequency === void 0) {
309
+ entries.push(entry);
310
+ continue;
311
+ }
312
+ let drawn = 0;
313
+ for (let i = 0; i < totalFiles; i++) {
314
+ if (fraction(next) < frequency) drawn++;
315
+ }
316
+ if (drawn > 0) entries.push({ ...entry, count: drawn });
317
+ }
318
+ return {
319
+ ...entries.length > 0 ? { entries } : {},
320
+ studies,
321
+ seed,
322
+ ...validated.layout !== void 0 ? { layout: validated.layout } : {}
323
+ };
324
+ }
325
+ export {
326
+ resolveParametricSpec
327
+ };
@@ -3,7 +3,6 @@ var TYPE_CAPABILITIES = {
3
3
  "valid-image": { image: true },
4
4
  "invalid-uid-image": { image: true },
5
5
  "vendor-warnings-image": { image: true },
6
- "large-image": { image: true },
7
6
  "fake-signature": { image: false },
8
7
  "non-dicom": { image: false },
9
8
  dicomdir: { image: false }
@@ -40,6 +39,25 @@ function validatePositiveInt(value, path) {
40
39
  );
41
40
  }
42
41
  }
42
+ function validateSeed(value, path) {
43
+ if (typeof value !== "number" || !Number.isFinite(value)) {
44
+ throw new Error(
45
+ `${path}: must be a finite number; got ${JSON.stringify(value)}`
46
+ );
47
+ }
48
+ }
49
+ function validateSizeKbLimit(kb, path) {
50
+ if (kb * 1024 > MAX_PIXEL_BYTES) {
51
+ throw new Error(`${path}: exceeds the 512 MB limit`);
52
+ }
53
+ }
54
+ function validateEnum(value, valid, path) {
55
+ if (typeof value !== "string" || !Object.hasOwn(valid, value)) {
56
+ throw new Error(
57
+ `${path}: must be one of ${Object.keys(valid).join(", ")}; got ${JSON.stringify(value)}`
58
+ );
59
+ }
60
+ }
43
61
  function validateTagKey(key, path) {
44
62
  if (!HEX_TAG_RE.test(key) && !KEYWORD_RE.test(key)) {
45
63
  throw new Error(
@@ -52,16 +70,16 @@ function validateEntry(entry, path) {
52
70
  throw new Error(`${path}: must be an object`);
53
71
  }
54
72
  const e = entry;
55
- if (typeof e.type !== "string" || !Object.hasOwn(TYPE_CAPABILITIES, e.type)) {
56
- throw new Error(
57
- `${path}.type: must be one of ${Object.keys(TYPE_CAPABILITIES).join(", ")}; got ${JSON.stringify(e.type)}`
58
- );
59
- }
73
+ validateEnum(e.type, TYPE_CAPABILITIES, `${path}.type`);
60
74
  const type = e.type;
61
75
  const caps = TYPE_CAPABILITIES[type];
62
76
  if ("count" in e) validatePositiveInt(e.count, `${path}.count`);
63
77
  for (const field of [
64
78
  "modality",
79
+ "rows",
80
+ "columns",
81
+ "frames",
82
+ "targetSizeKb",
65
83
  "tags",
66
84
  "violations",
67
85
  "transferSyntax"
@@ -70,18 +88,14 @@ function validateEntry(entry, path) {
70
88
  throw new Error(`${path}.${field}: not supported for type "${type}"`);
71
89
  }
72
90
  if ("transferSyntax" in e) {
73
- if (!Object.hasOwn(VALID_TRANSFER_SYNTAXES, e.transferSyntax)) {
74
- throw new Error(
75
- `${path}.transferSyntax: must be one of ${Object.keys(VALID_TRANSFER_SYNTAXES).join(", ")}`
76
- );
77
- }
91
+ validateEnum(
92
+ e.transferSyntax,
93
+ VALID_TRANSFER_SYNTAXES,
94
+ `${path}.transferSyntax`
95
+ );
78
96
  }
79
97
  if ("modality" in e) {
80
- if (typeof e.modality !== "string" || !Object.hasOwn(VALID_MODALITIES, e.modality)) {
81
- throw new Error(
82
- `${path}.modality: must be one of ${Object.keys(VALID_MODALITIES).join(", ")}; got ${JSON.stringify(e.modality)}`
83
- );
84
- }
98
+ validateEnum(e.modality, VALID_MODALITIES, `${path}.modality`);
85
99
  }
86
100
  if ("tags" in e) validateTags(e.tags, `${path}.tags`);
87
101
  if ("violations" in e) {
@@ -89,14 +103,22 @@ function validateEntry(entry, path) {
89
103
  throw new Error(`${path}.violations: must be an array`);
90
104
  }
91
105
  for (const [i, v] of e.violations.entries()) {
92
- if (typeof v !== "string" || !Object.hasOwn(VALID_VIOLATIONS, v)) {
93
- throw new Error(
94
- `${path}.violations[${i}]: must be one of ${Object.keys(VALID_VIOLATIONS).join(", ")}; got ${JSON.stringify(v)}`
95
- );
96
- }
106
+ validateEnum(v, VALID_VIOLATIONS, `${path}.violations[${i}]`);
97
107
  }
98
108
  }
99
- if (type === "large-image") {
109
+ const hasDims = "rows" in e || "columns" in e || "frames" in e;
110
+ if ("targetSizeKb" in e) {
111
+ if (hasDims) {
112
+ throw new Error(
113
+ `${path}: targetSizeKb cannot be combined with rows/columns/frames \u2014 use one sizing mechanism`
114
+ );
115
+ }
116
+ validatePositiveInt(e.targetSizeKb, `${path}.targetSizeKb`);
117
+ validateSizeKbLimit(e.targetSizeKb, `${path}.targetSizeKb`);
118
+ } else if (hasDims) {
119
+ if (!("rows" in e) || !("columns" in e)) {
120
+ throw new Error(`${path}: rows and columns must be provided together`);
121
+ }
100
122
  validatePositiveInt(e.rows, `${path}.rows`);
101
123
  validatePositiveInt(e.columns, `${path}.columns`);
102
124
  if ("frames" in e) validatePositiveInt(e.frames, `${path}.frames`);
@@ -177,28 +199,110 @@ function validateDatasetSpec(raw) {
177
199
  const studies = rawStudies.map(
178
200
  (study, i) => validateStudy(study, `studies[${i}]`)
179
201
  );
180
- if ("seed" in r) {
181
- if (typeof r.seed !== "number" || !Number.isFinite(r.seed)) {
202
+ if ("seed" in r) validateSeed(r.seed, "DatasetSpec.seed");
203
+ if ("layout" in r) {
204
+ validateEnum(r.layout, VALID_LAYOUTS, "DatasetSpec.layout");
205
+ }
206
+ return {
207
+ ...entries.length > 0 ? { entries } : {},
208
+ ...studies.length > 0 ? { studies } : {},
209
+ ...r.seed !== void 0 ? { seed: r.seed } : {},
210
+ ...r.layout !== void 0 ? { layout: r.layout } : {}
211
+ };
212
+ }
213
+ function validateRange(value, path) {
214
+ if (typeof value === "number") {
215
+ validatePositiveInt(value, path);
216
+ return value;
217
+ }
218
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
219
+ throw new Error(
220
+ `${path}: must be a positive integer or { min, max }; got ${JSON.stringify(value)}`
221
+ );
222
+ }
223
+ const r = value;
224
+ validatePositiveInt(r.min, `${path}.min`);
225
+ validatePositiveInt(r.max, `${path}.max`);
226
+ if (r.min > r.max) {
227
+ throw new Error(`${path}: min (${r.min}) must be \u2264 max (${r.max})`);
228
+ }
229
+ return value;
230
+ }
231
+ function rangeMax(range) {
232
+ return typeof range === "number" ? range : range.max;
233
+ }
234
+ function validateParametricStudies(studies, path) {
235
+ if (typeof studies !== "object" || studies === null || Array.isArray(studies)) {
236
+ throw new Error(`${path}: must be an object`);
237
+ }
238
+ const s = studies;
239
+ for (const field of ["count", "seriesPerStudy", "filesPerSeries"]) {
240
+ if (!(field in s)) throw new Error(`${path}.${field}: is required`);
241
+ validateRange(s[field], `${path}.${field}`);
242
+ }
243
+ if ("fileSizeKb" in s) {
244
+ validateRange(s.fileSizeKb, `${path}.fileSizeKb`);
245
+ validateSizeKbLimit(rangeMax(s.fileSizeKb), `${path}.fileSizeKb`);
246
+ }
247
+ if ("modalities" in s) {
248
+ if (!Array.isArray(s.modalities) || s.modalities.length === 0) {
249
+ throw new Error(`${path}.modalities: must be a non-empty array`);
250
+ }
251
+ for (const [i, m] of s.modalities.entries()) {
252
+ validateEnum(m, VALID_MODALITIES, `${path}.modalities[${i}]`);
253
+ }
254
+ }
255
+ if ("tags" in s) validateTags(s.tags, `${path}.tags`);
256
+ return s;
257
+ }
258
+ function validateEdgeCase(edgeCase, path) {
259
+ const e = validateEntry(edgeCase, path);
260
+ if ("frequency" in e) {
261
+ if ("count" in e) {
182
262
  throw new Error(
183
- `DatasetSpec.seed: must be a finite number; got ${JSON.stringify(r.seed)}`
263
+ `${path}: frequency cannot be combined with count \u2014 frequency draws the count at resolution time`
184
264
  );
185
265
  }
186
- }
187
- if ("layout" in r) {
188
- if (typeof r.layout !== "string" || !Object.hasOwn(VALID_LAYOUTS, r.layout)) {
266
+ const f = e.frequency;
267
+ if (typeof f !== "number" || !(f >= 0 && f <= 1)) {
189
268
  throw new Error(
190
- `DatasetSpec.layout: must be one of ${Object.keys(VALID_LAYOUTS).join(", ")}; got ${JSON.stringify(r.layout)}`
269
+ `${path}.frequency: must be a number between 0 and 1; got ${JSON.stringify(f)}`
191
270
  );
192
271
  }
193
272
  }
273
+ return e;
274
+ }
275
+ function validateParametricSpec(raw) {
276
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
277
+ throw new Error("ParametricSpec: must be a JSON object");
278
+ }
279
+ const r = raw;
280
+ if (!("studies" in r)) {
281
+ throw new Error("ParametricSpec.studies: is required");
282
+ }
283
+ const studies = validateParametricStudies(r.studies, "studies");
284
+ const edgeCases = [];
285
+ if ("edgeCases" in r) {
286
+ if (!Array.isArray(r.edgeCases)) {
287
+ throw new Error("ParametricSpec.edgeCases: must be an array");
288
+ }
289
+ for (const [i, e] of r.edgeCases.entries()) {
290
+ edgeCases.push(validateEdgeCase(e, `edgeCases[${i}]`));
291
+ }
292
+ }
293
+ if ("seed" in r) validateSeed(r.seed, "ParametricSpec.seed");
294
+ if ("layout" in r) {
295
+ validateEnum(r.layout, VALID_LAYOUTS, "ParametricSpec.layout");
296
+ }
194
297
  return {
195
- ...entries.length > 0 ? { entries } : {},
196
- ...studies.length > 0 ? { studies } : {},
298
+ studies,
299
+ ...edgeCases.length > 0 ? { edgeCases } : {},
197
300
  ...r.seed !== void 0 ? { seed: r.seed } : {},
198
301
  ...r.layout !== void 0 ? { layout: r.layout } : {}
199
302
  };
200
303
  }
201
304
  export {
202
305
  HEX_TAG_RE,
203
- validateDatasetSpec
306
+ validateDatasetSpec,
307
+ validateParametricSpec
204
308
  };
@@ -153,36 +153,46 @@ function serializeDict(meta, dataset) {
153
153
  );
154
154
  return Buffer.from(dicomDict.write({ allowInvalidVRLength: true }));
155
155
  }
156
- function buildValidImageBuffer(uid, modality, tags, transferSyntax) {
157
- const dataset = buildBaseImageDataset(uid, modality);
158
- applyTagOverrides(dataset, tags);
159
- return serializeDict(buildMeta(uid, modality, transferSyntax), dataset);
156
+ var MAX_DIMENSION = 65535;
157
+ function applySizing(dataset, meta, spec) {
158
+ if (spec.targetSizeKb !== void 0) {
159
+ const clampDim = (n) => Math.min(MAX_DIMENSION, Math.max(1, Math.round(n)));
160
+ const minimalPixelBytes = dataset.PixelData.byteLength;
161
+ const overhead = serializeDict(meta, dataset).length - minimalPixelBytes;
162
+ const cells = Math.max(
163
+ 1,
164
+ Math.round((spec.targetSizeKb * 1024 - overhead) / 2)
165
+ );
166
+ const rows = clampDim(Math.sqrt(cells));
167
+ const columns = clampDim(cells / rows);
168
+ dataset.Rows = rows;
169
+ dataset.Columns = columns;
170
+ dataset.PixelData = new ArrayBuffer(rows * columns * 2);
171
+ } else if (spec.rows !== void 0 && spec.columns !== void 0) {
172
+ dataset.Rows = spec.rows;
173
+ dataset.Columns = spec.columns;
174
+ if (spec.frames !== void 0) dataset.NumberOfFrames = spec.frames;
175
+ dataset.PixelData = new ArrayBuffer(
176
+ spec.rows * spec.columns * (spec.frames ?? 1) * 2
177
+ );
178
+ }
160
179
  }
161
- function buildInvalidUidImageBuffer(uid, modality, tags, transferSyntax) {
162
- const invalidUid = {
180
+ function buildImageBuffer(spec, uid) {
181
+ const modality = spec.modality ?? "CT";
182
+ const effectiveUid = spec.type === "invalid-uid-image" ? {
163
183
  study: `2.25.invalid.study.${uid.study.slice(-6)}`,
164
184
  series: `2.25.invalid.series.${uid.series.slice(-6)}`,
165
185
  sop: `2.25.invalid.sop.${uid.sop.slice(-6)}`
166
- };
167
- const dataset = buildBaseImageDataset(invalidUid, modality);
168
- applyTagOverrides(dataset, tags);
169
- return serializeDict(buildMeta(invalidUid, modality, transferSyntax), dataset);
170
- }
171
- function buildVendorWarningsImageBuffer(uid, modality, tags, transferSyntax) {
172
- const dataset = buildBaseImageDataset(uid, modality);
173
- dataset.Laterality = "";
174
- dataset.PatientWeight = "0";
175
- applyTagOverrides(dataset, tags);
176
- return serializeDict(buildMeta(uid, modality, transferSyntax), dataset);
177
- }
178
- function buildLargeImageBuffer(uid, modality, rows, columns, frames, tags, transferSyntax) {
179
- const dataset = buildBaseImageDataset(uid, modality);
180
- dataset.Rows = rows;
181
- dataset.Columns = columns;
182
- dataset.NumberOfFrames = frames;
183
- dataset.PixelData = new Uint8Array(rows * columns * frames * 2).buffer;
184
- applyTagOverrides(dataset, tags);
185
- return serializeDict(buildMeta(uid, modality, transferSyntax), dataset);
186
+ } : uid;
187
+ const dataset = buildBaseImageDataset(effectiveUid, modality);
188
+ if (spec.type === "vendor-warnings-image") {
189
+ dataset.Laterality = "";
190
+ dataset.PatientWeight = "0";
191
+ }
192
+ applyTagOverrides(dataset, spec.tags);
193
+ const meta = buildMeta(effectiveUid, modality, spec.transferSyntax);
194
+ applySizing(dataset, meta, spec);
195
+ return serializeDict(meta, dataset);
186
196
  }
187
197
  function buildFakeSignatureBuffer() {
188
198
  const buf = Buffer.alloc(200, 0);
@@ -195,36 +205,9 @@ function buildNonDicomBuffer(content = "not dicom") {
195
205
  function buildBufferForSpec(spec, uid) {
196
206
  switch (spec.type) {
197
207
  case "valid-image":
198
- return buildValidImageBuffer(
199
- uid,
200
- spec.modality ?? "CT",
201
- spec.tags,
202
- spec.transferSyntax
203
- );
204
208
  case "invalid-uid-image":
205
- return buildInvalidUidImageBuffer(
206
- uid,
207
- spec.modality ?? "CT",
208
- spec.tags,
209
- spec.transferSyntax
210
- );
211
209
  case "vendor-warnings-image":
212
- return buildVendorWarningsImageBuffer(
213
- uid,
214
- spec.modality ?? "CT",
215
- spec.tags,
216
- spec.transferSyntax
217
- );
218
- case "large-image":
219
- return buildLargeImageBuffer(
220
- uid,
221
- spec.modality ?? "CT",
222
- spec.rows,
223
- spec.columns,
224
- spec.frames ?? 1,
225
- spec.tags,
226
- spec.transferSyntax
227
- );
210
+ return buildImageBuffer(spec, uid);
228
211
  case "fake-signature":
229
212
  return buildFakeSignatureBuffer();
230
213
  case "non-dicom":
@@ -19,6 +19,7 @@ function seededUid(next, role) {
19
19
  return formatUid(next(), next(), role);
20
20
  }
21
21
  var GROUP_STREAM_OFFSET = 2654435769;
22
+ var PARAMETRIC_STREAM_OFFSET = 2246822507;
22
23
  function seededStream(seed, offset, a, b) {
23
24
  return lcg(seed * 999983 + offset + a * 1000003 + b * 7919 >>> 0);
24
25
  }
@@ -63,6 +64,8 @@ function makeUidGenerator(seed) {
63
64
  };
64
65
  }
65
66
  export {
67
+ PARAMETRIC_STREAM_OFFSET,
66
68
  makeGroupUidGenerator,
67
- makeUidGenerator
69
+ makeUidGenerator,
70
+ seededStream
68
71
  };
@@ -1,5 +1,6 @@
1
1
  export { type CollectionManifest, type GeneratedFile, generateCollectionFromSpec, generateFile, writeCollectionFromSpec, } from './collection/writer.js';
2
2
  export { defaultPublicCasesPath, loadCaseById, loadCasesFromJson, loadDefaultCases, type PublicCaseRecord, type PublicCaseSource, } from './public-fixtures/catalog.js';
3
3
  export { caseCachePath, fetchPublicCaseToCache, verifySha256, } from './public-fixtures/fetch.js';
4
- export type { DatasetLayout, DatasetSpec, DicomdirSpec, DicomTagOverrides, FakeSignatureSpec, FileSpec, ImageSpec, InvalidUidImageSpec, LargeImageSpec, Modality, NonDicomSpec, SeriesEntrySpec, SeriesSpec, StudySpec, TransferSyntax, ValidImageSpec, VendorWarningsImageSpec, ViolationClass, } from './schema/types.js';
5
- export { validateDatasetSpec } from './schema/validate.js';
4
+ export { resolveParametricSpec } from './schema/parametric.js';
5
+ export type { DatasetLayout, DatasetSpec, DicomdirSpec, DicomTagOverrides, FakeSignatureSpec, FileSpec, ImageSpec, InvalidUidImageSpec, Modality, NonDicomSpec, ParametricEdgeCase, ParametricSpec, ParametricStudies, Range, SeriesEntrySpec, SeriesSpec, StudySpec, TransferSyntax, ValidImageSpec, VendorWarningsImageSpec, ViolationClass, } from './schema/types.js';
6
+ export { validateDatasetSpec, validateParametricSpec, } from './schema/validate.js';