dicom-synth 1.10.0 → 1.12.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 +23 -5
- package/dist/esm/collection/writer.js +100 -95
- package/dist/esm/describe/describe.js +234 -48
- package/dist/esm/index.js +345 -137
- package/dist/esm/schema/parametric.js +31 -16
- package/dist/esm/schema/validate.js +115 -19
- package/dist/esm/syntheticFixtures/streamWrite.js +20 -35
- package/dist/esm/syntheticFixtures/uid.js +28 -47
- package/dist/types/collection/writer.d.ts +2 -0
- package/dist/types/describe/describe.d.ts +2 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/schema/types.d.ts +11 -1
- package/dist/types/syntheticFixtures/streamWrite.d.ts +6 -2
- package/dist/types/syntheticFixtures/uid.d.ts +4 -2
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
3
3
|
|
|
4
4
|
// src/syntheticFixtures/uid.ts
|
|
5
|
-
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
6
6
|
function lcg(seed) {
|
|
7
7
|
let s = seed >>> 0;
|
|
8
8
|
return () => {
|
|
@@ -90,6 +90,13 @@ function validateSeed(value, path) {
|
|
|
90
90
|
);
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
function validateUidSalt(value, path) {
|
|
94
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`${path}: must be a non-empty string; got ${JSON.stringify(value)}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
93
100
|
function validateSizeKbLimit(kb, path) {
|
|
94
101
|
if (kb * 1024 > MAX_PIXEL_BYTES) {
|
|
95
102
|
throw new Error(`${path}: exceeds the 512 MB limit`);
|
|
@@ -224,27 +231,30 @@ function validateEntry(entry, path) {
|
|
|
224
231
|
return e;
|
|
225
232
|
}
|
|
226
233
|
var MODALITY_TAG = "00080060";
|
|
234
|
+
function validateTagValue(key, value, path) {
|
|
235
|
+
const isModalityKey = key === "Modality" || key.toUpperCase() === MODALITY_TAG;
|
|
236
|
+
if (isModalityKey && typeof value === "string" && Object.hasOwn(VALID_MODALITIES, value)) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`${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`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (typeof value === "string") {
|
|
242
|
+
for (const name of findTemplateTokens(value)) {
|
|
243
|
+
if (!TEMPLATE_VOCAB.includes(name)) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`${path}.${key}: unknown placeholder "{${name}}" \u2014 allowed: ${TEMPLATE_VOCAB.map((v) => `{${v}}`).join(", ")}`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
227
251
|
function validateTags(tags, path) {
|
|
228
252
|
if (typeof tags !== "object" || tags === null || Array.isArray(tags)) {
|
|
229
253
|
throw new Error(`${path}: must be an object`);
|
|
230
254
|
}
|
|
231
255
|
for (const [key, value] of Object.entries(tags)) {
|
|
232
256
|
validateTagKey(key, path);
|
|
233
|
-
|
|
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
|
-
}
|
|
257
|
+
validateTagValue(key, value, path);
|
|
248
258
|
}
|
|
249
259
|
}
|
|
250
260
|
function validateRange(value, path, options = {}) {
|
|
@@ -358,6 +368,7 @@ function validateParametricSpec(raw) {
|
|
|
358
368
|
}
|
|
359
369
|
}
|
|
360
370
|
if ("seed" in r) validateSeed(r.seed, "ParametricSpec.seed");
|
|
371
|
+
if ("uidSalt" in r) validateUidSalt(r.uidSalt, "ParametricSpec.uidSalt");
|
|
361
372
|
if ("layout" in r) {
|
|
362
373
|
validateEnum(r.layout, VALID_LAYOUTS, "ParametricSpec.layout");
|
|
363
374
|
}
|
|
@@ -365,6 +376,7 @@ function validateParametricSpec(raw) {
|
|
|
365
376
|
studies,
|
|
366
377
|
...edgeCases.length > 0 ? { edgeCases } : {},
|
|
367
378
|
...r.seed !== void 0 ? { seed: r.seed } : {},
|
|
379
|
+
...r.uidSalt !== void 0 ? { uidSalt: r.uidSalt } : {},
|
|
368
380
|
...r.layout !== void 0 ? { layout: r.layout } : {}
|
|
369
381
|
};
|
|
370
382
|
}
|
|
@@ -468,6 +480,9 @@ function resolveParametricSpec(spec) {
|
|
|
468
480
|
...entries.length > 0 ? { entries } : {},
|
|
469
481
|
studies,
|
|
470
482
|
seed,
|
|
483
|
+
// Pass an explicit salt through; when absent, UIDs derive from `seed` so the
|
|
484
|
+
// resolved spec stays deterministic ("same seed → identical resolved spec").
|
|
485
|
+
...validated.uidSalt !== void 0 ? { uidSalt: validated.uidSalt } : {},
|
|
471
486
|
...validated.layout !== void 0 ? { layout: validated.layout } : {}
|
|
472
487
|
};
|
|
473
488
|
}
|
|
@@ -79,6 +79,13 @@ function validateSeed(value, path) {
|
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
function validateUidSalt(value, path) {
|
|
83
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`${path}: must be a non-empty string; got ${JSON.stringify(value)}`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
82
89
|
function validateSizeKbLimit(kb, path) {
|
|
83
90
|
if (kb * 1024 > MAX_PIXEL_BYTES) {
|
|
84
91
|
throw new Error(`${path}: exceeds the 512 MB limit`);
|
|
@@ -213,26 +220,100 @@ function validateEntry(entry, path) {
|
|
|
213
220
|
return e;
|
|
214
221
|
}
|
|
215
222
|
var MODALITY_TAG = "00080060";
|
|
223
|
+
function validateTagValue(key, value, path) {
|
|
224
|
+
const isModalityKey = key === "Modality" || key.toUpperCase() === MODALITY_TAG;
|
|
225
|
+
if (isModalityKey && typeof value === "string" && Object.hasOwn(VALID_MODALITIES, value)) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`${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`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (typeof value === "string") {
|
|
231
|
+
for (const name of findTemplateTokens(value)) {
|
|
232
|
+
if (!TEMPLATE_VOCAB.includes(name)) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`${path}.${key}: unknown placeholder "{${name}}" \u2014 allowed: ${TEMPLATE_VOCAB.map((v) => `{${v}}`).join(", ")}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
216
240
|
function validateTags(tags, path) {
|
|
217
241
|
if (typeof tags !== "object" || tags === null || Array.isArray(tags)) {
|
|
218
242
|
throw new Error(`${path}: must be an object`);
|
|
219
243
|
}
|
|
220
244
|
for (const [key, value] of Object.entries(tags)) {
|
|
221
245
|
validateTagKey(key, path);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
246
|
+
validateTagValue(key, value, path);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function validateArrayOfLength(value, count, path) {
|
|
250
|
+
if (!Array.isArray(value)) {
|
|
251
|
+
throw new Error(`${path}: must be an array`);
|
|
252
|
+
}
|
|
253
|
+
if (value.length !== count) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`${path}: length (${value.length}) must equal count (${count})`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
function validateInstances(instances, path) {
|
|
261
|
+
if (typeof instances !== "object" || instances === null || Array.isArray(instances)) {
|
|
262
|
+
throw new Error(`${path}: must be an object`);
|
|
263
|
+
}
|
|
264
|
+
const inst = instances;
|
|
265
|
+
validatePositiveInt(inst.count, `${path}.count`);
|
|
266
|
+
const count = inst.count;
|
|
267
|
+
const hasKb = "targetSizeKb" in inst;
|
|
268
|
+
const hasBytes = "targetBytes" in inst;
|
|
269
|
+
if (hasKb === hasBytes) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`${path}: exactly one of "targetSizeKb" or "targetBytes" is required`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (hasKb) {
|
|
275
|
+
const sizes = validateArrayOfLength(
|
|
276
|
+
inst.targetSizeKb,
|
|
277
|
+
count,
|
|
278
|
+
`${path}.targetSizeKb`
|
|
279
|
+
);
|
|
280
|
+
sizes.forEach((kb, i) => {
|
|
281
|
+
validatePositiveInt(kb, `${path}.targetSizeKb[${i}]`);
|
|
282
|
+
validateSizeKbLimit(kb, `${path}.targetSizeKb[${i}]`);
|
|
283
|
+
});
|
|
284
|
+
} else {
|
|
285
|
+
const sizes = validateArrayOfLength(
|
|
286
|
+
inst.targetBytes,
|
|
287
|
+
count,
|
|
288
|
+
`${path}.targetBytes`
|
|
289
|
+
);
|
|
290
|
+
sizes.forEach((b, i) => {
|
|
291
|
+
validateLargeTargetBytes(b, `${path}.targetBytes[${i}]`);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
if ("modality" in inst) {
|
|
295
|
+
if (Array.isArray(inst.modality)) {
|
|
296
|
+
validateArrayOfLength(inst.modality, count, `${path}.modality`).forEach(
|
|
297
|
+
(m, i) => {
|
|
298
|
+
validateEnum(m, VALID_MODALITIES, `${path}.modality[${i}]`);
|
|
299
|
+
}
|
|
226
300
|
);
|
|
301
|
+
} else {
|
|
302
|
+
validateEnum(inst.modality, VALID_MODALITIES, `${path}.modality`);
|
|
227
303
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
304
|
+
}
|
|
305
|
+
if ("tags" in inst) {
|
|
306
|
+
const tags = inst.tags;
|
|
307
|
+
if (typeof tags !== "object" || tags === null || Array.isArray(tags)) {
|
|
308
|
+
throw new Error(`${path}.tags: must be an object`);
|
|
309
|
+
}
|
|
310
|
+
for (const [key, arr] of Object.entries(tags)) {
|
|
311
|
+
validateTagKey(key, `${path}.tags`);
|
|
312
|
+
validateArrayOfLength(arr, count, `${path}.tags.${key}`).forEach(
|
|
313
|
+
(value, i) => {
|
|
314
|
+
validateTagValue(key, value, `${path}.tags[${i}]`);
|
|
234
315
|
}
|
|
235
|
-
|
|
316
|
+
);
|
|
236
317
|
}
|
|
237
318
|
}
|
|
238
319
|
}
|
|
@@ -241,16 +322,27 @@ function validateSeries(series, path) {
|
|
|
241
322
|
throw new Error(`${path}: must be an object`);
|
|
242
323
|
}
|
|
243
324
|
const s = series;
|
|
244
|
-
|
|
245
|
-
|
|
325
|
+
const hasEntries = "entries" in s;
|
|
326
|
+
const hasInstances = "instances" in s;
|
|
327
|
+
if (hasEntries === hasInstances) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`${path}: must have exactly one of "entries" or "instances"`
|
|
330
|
+
);
|
|
246
331
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
);
|
|
332
|
+
if (hasEntries) {
|
|
333
|
+
if (!Array.isArray(s.entries) || s.entries.length === 0) {
|
|
334
|
+
throw new Error(`${path}.entries: must be a non-empty array`);
|
|
335
|
+
}
|
|
336
|
+
for (let i = 0; i < s.entries.length; i++) {
|
|
337
|
+
const e = validateEntry(s.entries[i], `${path}.entries[${i}]`);
|
|
338
|
+
if (!TYPE_CAPABILITIES[e.type].image) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`${path}.entries[${i}].type: "${e.type}" cannot be used inside a series \u2014 only image-generating types can be grouped`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
253
343
|
}
|
|
344
|
+
} else {
|
|
345
|
+
validateInstances(s.instances, `${path}.instances`);
|
|
254
346
|
}
|
|
255
347
|
if ("tags" in s) validateTags(s.tags, `${path}.tags`);
|
|
256
348
|
return s;
|
|
@@ -295,6 +387,7 @@ function validateDatasetSpec(raw) {
|
|
|
295
387
|
(study, i) => validateStudy(study, `studies[${i}]`)
|
|
296
388
|
);
|
|
297
389
|
if ("seed" in r) validateSeed(r.seed, "DatasetSpec.seed");
|
|
390
|
+
if ("uidSalt" in r) validateUidSalt(r.uidSalt, "DatasetSpec.uidSalt");
|
|
298
391
|
if ("layout" in r) {
|
|
299
392
|
validateEnum(r.layout, VALID_LAYOUTS, "DatasetSpec.layout");
|
|
300
393
|
}
|
|
@@ -310,6 +403,7 @@ function validateDatasetSpec(raw) {
|
|
|
310
403
|
...entries.length > 0 ? { entries } : {},
|
|
311
404
|
...studies.length > 0 ? { studies } : {},
|
|
312
405
|
...r.seed !== void 0 ? { seed: r.seed } : {},
|
|
406
|
+
...r.uidSalt !== void 0 ? { uidSalt: r.uidSalt } : {},
|
|
313
407
|
...r.layout !== void 0 ? { layout: r.layout } : {},
|
|
314
408
|
...r.pathQuirks !== void 0 ? { pathQuirks: r.pathQuirks } : {}
|
|
315
409
|
};
|
|
@@ -425,6 +519,7 @@ function validateParametricSpec(raw) {
|
|
|
425
519
|
}
|
|
426
520
|
}
|
|
427
521
|
if ("seed" in r) validateSeed(r.seed, "ParametricSpec.seed");
|
|
522
|
+
if ("uidSalt" in r) validateUidSalt(r.uidSalt, "ParametricSpec.uidSalt");
|
|
428
523
|
if ("layout" in r) {
|
|
429
524
|
validateEnum(r.layout, VALID_LAYOUTS, "ParametricSpec.layout");
|
|
430
525
|
}
|
|
@@ -432,6 +527,7 @@ function validateParametricSpec(raw) {
|
|
|
432
527
|
studies,
|
|
433
528
|
...edgeCases.length > 0 ? { edgeCases } : {},
|
|
434
529
|
...r.seed !== void 0 ? { seed: r.seed } : {},
|
|
530
|
+
...r.uidSalt !== void 0 ? { uidSalt: r.uidSalt } : {},
|
|
435
531
|
...r.layout !== void 0 ? { layout: r.layout } : {}
|
|
436
532
|
};
|
|
437
533
|
}
|
|
@@ -144,44 +144,28 @@ function serializeDict(meta, dataset) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
// src/syntheticFixtures/uid.ts
|
|
147
|
-
import { randomBytes } from "node:crypto";
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return ()
|
|
152
|
-
s = Math.imul(1664525, s) + 1013904223 >>> 0;
|
|
153
|
-
return s;
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
function formatUid(a, b, role) {
|
|
157
|
-
return `2.25.${a}.${b}.${ROLE_CODE[role]}`;
|
|
147
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
148
|
+
function hashUid(salt, key) {
|
|
149
|
+
const digest = createHash("sha256").update(salt).update(" ").update(key).digest();
|
|
150
|
+
const n = BigInt(`0x${digest.subarray(0, 16).toString("hex")}`);
|
|
151
|
+
return `2.25.${n.toString()}`;
|
|
158
152
|
}
|
|
159
|
-
function randomUid(
|
|
160
|
-
const
|
|
161
|
-
return
|
|
153
|
+
function randomUid() {
|
|
154
|
+
const n = BigInt(`0x${randomBytes(16).toString("hex")}`);
|
|
155
|
+
return `2.25.${n.toString()}`;
|
|
162
156
|
}
|
|
163
|
-
function
|
|
164
|
-
return
|
|
157
|
+
function seedToSalt(seed) {
|
|
158
|
+
return seed === void 0 ? void 0 : `seed:${seed}`;
|
|
165
159
|
}
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
function makeUidGenerator(seed) {
|
|
170
|
-
if (seed === void 0) {
|
|
171
|
-
return () => ({
|
|
172
|
-
study: randomUid("study"),
|
|
173
|
-
series: randomUid("series"),
|
|
174
|
-
sop: randomUid("sop")
|
|
175
|
-
});
|
|
160
|
+
function makeUidGenerator(salt) {
|
|
161
|
+
if (salt === void 0) {
|
|
162
|
+
return () => ({ study: randomUid(), series: randomUid(), sop: randomUid() });
|
|
176
163
|
}
|
|
177
|
-
return (fileIndex) => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
sop: seededUid(next, "sop")
|
|
183
|
-
};
|
|
184
|
-
};
|
|
164
|
+
return (fileIndex) => ({
|
|
165
|
+
study: hashUid(salt, `study:flat:${fileIndex}`),
|
|
166
|
+
series: hashUid(salt, `series:flat:${fileIndex}`),
|
|
167
|
+
sop: hashUid(salt, `sop:${fileIndex}`)
|
|
168
|
+
});
|
|
185
169
|
}
|
|
186
170
|
|
|
187
171
|
// src/syntheticFixtures/streamWrite.ts
|
|
@@ -294,7 +278,7 @@ function streamLargeImage(outPath, options) {
|
|
|
294
278
|
fragmentBytes = FRAGMENT_MAX_BYTES
|
|
295
279
|
} = options;
|
|
296
280
|
const modality = options.modality ?? "CT";
|
|
297
|
-
const uid = options.uid ?? makeUidGenerator(options.seed)(0);
|
|
281
|
+
const uid = options.uid ?? makeUidGenerator(options.uidSalt ?? seedToSalt(options.seed))(0);
|
|
298
282
|
const identity = { modality, uid, tags: options.tags };
|
|
299
283
|
mkdirSync2(resolve2(outPath, ".."), { recursive: true });
|
|
300
284
|
const zeroChunk = Buffer.alloc(Math.max(1, chunkBytes));
|
|
@@ -346,6 +330,7 @@ function writeLargeImageFile(spec, outPath, options = {}) {
|
|
|
346
330
|
return streamLargeImage(outPath, {
|
|
347
331
|
targetBytes,
|
|
348
332
|
modality: spec.modality,
|
|
333
|
+
uidSalt: spec.uidSalt,
|
|
349
334
|
seed: spec.seed,
|
|
350
335
|
chunkBytes: options.chunkBytes
|
|
351
336
|
});
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
// src/syntheticFixtures/uid.ts
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
|
-
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
function hashUid(salt, key) {
|
|
4
|
+
const digest = createHash("sha256").update(salt).update(" ").update(key).digest();
|
|
5
|
+
const n = BigInt(`0x${digest.subarray(0, 16).toString("hex")}`);
|
|
6
|
+
return `2.25.${n.toString()}`;
|
|
7
|
+
}
|
|
8
|
+
function randomUid() {
|
|
9
|
+
const n = BigInt(`0x${randomBytes(16).toString("hex")}`);
|
|
10
|
+
return `2.25.${n.toString()}`;
|
|
11
|
+
}
|
|
12
|
+
function seedToSalt(seed) {
|
|
13
|
+
return seed === void 0 ? void 0 : `seed:${seed}`;
|
|
14
|
+
}
|
|
4
15
|
function lcg(seed) {
|
|
5
16
|
let s = seed >>> 0;
|
|
6
17
|
return () => {
|
|
@@ -8,64 +19,34 @@ function lcg(seed) {
|
|
|
8
19
|
return s;
|
|
9
20
|
};
|
|
10
21
|
}
|
|
11
|
-
function formatUid(a, b, role) {
|
|
12
|
-
return `2.25.${a}.${b}.${ROLE_CODE[role]}`;
|
|
13
|
-
}
|
|
14
|
-
function randomUid(role) {
|
|
15
|
-
const buf = randomBytes(8);
|
|
16
|
-
return formatUid(buf.readUInt32BE(0), buf.readUInt32BE(4), role);
|
|
17
|
-
}
|
|
18
|
-
function seededUid(next, role) {
|
|
19
|
-
return formatUid(next(), next(), role);
|
|
20
|
-
}
|
|
21
|
-
var GROUP_STREAM_OFFSET = 2654435769;
|
|
22
22
|
var PARAMETRIC_STREAM_OFFSET = 2246822507;
|
|
23
23
|
function seededStream(seed, offset, a, b) {
|
|
24
24
|
return lcg(seed * 999983 + offset + a * 1000003 + b * 7919 >>> 0);
|
|
25
25
|
}
|
|
26
|
-
function makeGroupUidGenerator(
|
|
27
|
-
if (
|
|
28
|
-
return {
|
|
29
|
-
study: () => randomUid("study"),
|
|
30
|
-
series: () => randomUid("series")
|
|
31
|
-
};
|
|
26
|
+
function makeGroupUidGenerator(salt) {
|
|
27
|
+
if (salt === void 0) {
|
|
28
|
+
return { study: () => randomUid(), series: () => randomUid() };
|
|
32
29
|
}
|
|
33
30
|
return {
|
|
34
|
-
study: (studyIndex) =>
|
|
35
|
-
|
|
36
|
-
"study"
|
|
37
|
-
),
|
|
38
|
-
series: (studyIndex, seriesIndex) => seededUid(
|
|
39
|
-
seededStream(
|
|
40
|
-
seed,
|
|
41
|
-
GROUP_STREAM_OFFSET,
|
|
42
|
-
studyIndex + 1,
|
|
43
|
-
seriesIndex + 1
|
|
44
|
-
),
|
|
45
|
-
"series"
|
|
46
|
-
)
|
|
31
|
+
study: (studyIndex) => hashUid(salt, `study:${studyIndex}`),
|
|
32
|
+
series: (studyIndex, seriesIndex) => hashUid(salt, `series:${studyIndex}:${seriesIndex}`)
|
|
47
33
|
};
|
|
48
34
|
}
|
|
49
|
-
function makeUidGenerator(
|
|
50
|
-
if (
|
|
51
|
-
return () => ({
|
|
52
|
-
study: randomUid("study"),
|
|
53
|
-
series: randomUid("series"),
|
|
54
|
-
sop: randomUid("sop")
|
|
55
|
-
});
|
|
35
|
+
function makeUidGenerator(salt) {
|
|
36
|
+
if (salt === void 0) {
|
|
37
|
+
return () => ({ study: randomUid(), series: randomUid(), sop: randomUid() });
|
|
56
38
|
}
|
|
57
|
-
return (fileIndex) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
sop: seededUid(next, "sop")
|
|
63
|
-
};
|
|
64
|
-
};
|
|
39
|
+
return (fileIndex) => ({
|
|
40
|
+
study: hashUid(salt, `study:flat:${fileIndex}`),
|
|
41
|
+
series: hashUid(salt, `series:flat:${fileIndex}`),
|
|
42
|
+
sop: hashUid(salt, `sop:${fileIndex}`)
|
|
43
|
+
});
|
|
65
44
|
}
|
|
66
45
|
export {
|
|
67
46
|
PARAMETRIC_STREAM_OFFSET,
|
|
47
|
+
hashUid,
|
|
68
48
|
makeGroupUidGenerator,
|
|
69
49
|
makeUidGenerator,
|
|
50
|
+
seedToSalt,
|
|
70
51
|
seededStream
|
|
71
52
|
};
|
|
@@ -16,11 +16,13 @@ export type CollectionManifest = Array<{
|
|
|
16
16
|
export declare function generateFile(spec: FileSpec, options?: {
|
|
17
17
|
index?: number;
|
|
18
18
|
seed?: number;
|
|
19
|
+
uidSalt?: string;
|
|
19
20
|
padWidth?: number;
|
|
20
21
|
uid?: Partial<UidSet>;
|
|
21
22
|
context?: Partial<TagContext>;
|
|
22
23
|
}): Promise<GeneratedFile>;
|
|
23
24
|
export declare function withResolvedSeed(spec: DatasetSpec): DatasetSpec;
|
|
25
|
+
export declare function withResolvedSalt(spec: DatasetSpec): DatasetSpec;
|
|
24
26
|
export declare function generateCollectionFromSpec(spec: DatasetSpec): AsyncGenerator<GeneratedFile>;
|
|
25
27
|
export type CollectionPreviewItem = {
|
|
26
28
|
relativePath: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DatasetSpec } from '../schema/types.js';
|
|
2
2
|
export type DescribeOptions = {
|
|
3
3
|
keepTags?: string[];
|
|
4
|
+
sizeTolerance?: number;
|
|
4
5
|
};
|
|
5
6
|
export type DescribeResult = {
|
|
6
7
|
spec: DatasetSpec;
|
|
@@ -11,4 +12,5 @@ export type DescribeResult = {
|
|
|
11
12
|
};
|
|
12
13
|
};
|
|
13
14
|
export declare function readHeaderOnly(path: string): Record<string, unknown> | null;
|
|
15
|
+
export declare function tagValuesEqual(a: unknown, b: unknown): boolean;
|
|
14
16
|
export declare function describeDirectory(dir: string, options?: DescribeOptions): DescribeResult;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
export { type CollectionManifest, type CollectionPreviewItem, type GeneratedFile, generateCollectionFromSpec, generateFile, previewCollection, withResolvedSeed, writeCollectionFromSpec, } from './collection/writer.js';
|
|
1
|
+
export { type CollectionManifest, type CollectionPreviewItem, type GeneratedFile, generateCollectionFromSpec, generateFile, previewCollection, withResolvedSalt, withResolvedSeed, writeCollectionFromSpec, } from './collection/writer.js';
|
|
2
2
|
export { type DescribeOptions, type DescribeResult, describeDirectory, } from './describe/describe.js';
|
|
3
3
|
export { defaultPublicCasesPath, loadCaseById, loadCasesFromJson, loadDefaultCases, type PublicCaseRecord, type PublicCaseSource, } from './public-fixtures/catalog.js';
|
|
4
4
|
export { caseCachePath, fetchPublicCaseToCache, verifySha256, } from './public-fixtures/fetch.js';
|
|
5
5
|
export { MAX_PIXEL_BYTES, MAX_STREAM_BYTES } from './schema/limits.js';
|
|
6
6
|
export { resolveParametricSpec } from './schema/parametric.js';
|
|
7
|
-
export type { DatasetLayout, DatasetSpec, DicomdirSpec, DicomTagOverrides, FakeSignatureSpec, FileSpec, ImageSpec, InvalidUidImageSpec, LargeFileSpec, Modality, NonDicomSpec, ParametricEdgeCase, ParametricSpec, ParametricStudies, PathQuirk, Range, SeriesEntrySpec, SeriesSpec, StudySpec, TransferSyntax, ValidImageSpec, VendorWarningsImageSpec, ViolationClass, } from './schema/types.js';
|
|
7
|
+
export type { DatasetLayout, DatasetSpec, DicomdirSpec, DicomTagOverrides, FakeSignatureSpec, FileSpec, ImageSpec, InvalidUidImageSpec, LargeFileSpec, Modality, NonDicomSpec, ParametricEdgeCase, ParametricSpec, ParametricStudies, PathQuirk, Range, SeriesEntrySpec, SeriesInstances, SeriesSpec, StudySpec, TransferSyntax, ValidImageSpec, VendorWarningsImageSpec, ViolationClass, } from './schema/types.js';
|
|
8
8
|
export { validateDatasetSpec, validateParametricSpec, } from './schema/validate.js';
|
|
9
9
|
export { type LargeFileEncoding, type LargeFileResult, type LargeImageSpec, type StreamLargeImageOptions, streamLargeImage, writeLargeImageFile, } from './syntheticFixtures/streamWrite.js';
|
|
10
10
|
export { resolveTagTemplates, type TagContext, TEMPLATE_VOCAB, } from './syntheticFixtures/tagTemplate.js';
|
|
11
|
+
export { hashUid, seedToSalt } from './syntheticFixtures/uid.js';
|
|
@@ -51,8 +51,16 @@ export type EntrySpec = FileSpec & {
|
|
|
51
51
|
export type SeriesEntrySpec = (ImageSpec | LargeFileSpec) & {
|
|
52
52
|
count?: number;
|
|
53
53
|
};
|
|
54
|
+
export type SeriesInstances = {
|
|
55
|
+
count: number;
|
|
56
|
+
targetSizeKb?: number[];
|
|
57
|
+
targetBytes?: number[];
|
|
58
|
+
modality?: Modality | Modality[];
|
|
59
|
+
tags?: Record<string, unknown[]>;
|
|
60
|
+
};
|
|
54
61
|
export type SeriesSpec = {
|
|
55
|
-
entries
|
|
62
|
+
entries?: SeriesEntrySpec[];
|
|
63
|
+
instances?: SeriesInstances;
|
|
56
64
|
tags?: DicomTagOverrides;
|
|
57
65
|
};
|
|
58
66
|
export type StudySpec = {
|
|
@@ -66,6 +74,7 @@ export type DatasetSpec = {
|
|
|
66
74
|
entries?: EntrySpec[];
|
|
67
75
|
studies?: StudySpec[];
|
|
68
76
|
seed?: number;
|
|
77
|
+
uidSalt?: string;
|
|
69
78
|
layout?: DatasetLayout;
|
|
70
79
|
pathQuirks?: PathQuirk[];
|
|
71
80
|
};
|
|
@@ -88,6 +97,7 @@ export type ParametricEdgeCase = EntrySpec & {
|
|
|
88
97
|
};
|
|
89
98
|
export type ParametricSpec = {
|
|
90
99
|
seed?: number;
|
|
100
|
+
uidSalt?: string;
|
|
91
101
|
studies: ParametricStudies;
|
|
92
102
|
edgeCases?: ParametricEdgeCase[];
|
|
93
103
|
layout?: DatasetLayout;
|
|
@@ -4,7 +4,9 @@ export type LargeImageSpec = {
|
|
|
4
4
|
/** Approximate total file size in bytes (must exceed the 512 MB cap). */
|
|
5
5
|
targetBytes: number;
|
|
6
6
|
modality?: Modality;
|
|
7
|
-
/**
|
|
7
|
+
/** UID namespace (hash-derived UIDs); random when both salt and seed omitted. */
|
|
8
|
+
uidSalt?: string;
|
|
9
|
+
/** Bridged to a salt when uidSalt is omitted; fixes the instance UIDs. */
|
|
8
10
|
seed?: number;
|
|
9
11
|
};
|
|
10
12
|
export type LargeFileEncoding = 'native' | 'encapsulated';
|
|
@@ -18,7 +20,9 @@ export type StreamLargeImageOptions = {
|
|
|
18
20
|
/** Approximate total file size in bytes (no floor — small values are valid). */
|
|
19
21
|
targetBytes: number;
|
|
20
22
|
modality?: Modality;
|
|
21
|
-
/**
|
|
23
|
+
/** UID namespace (hash-derived UIDs); random when both salt and seed omitted. */
|
|
24
|
+
uidSalt?: string;
|
|
25
|
+
/** Bridged to a salt when uidSalt is omitted; fixes the instance UIDs. */
|
|
22
26
|
seed?: number;
|
|
23
27
|
/** Explicit UID set — used as-is (e.g. collection threading shared study/series UIDs). */
|
|
24
28
|
uid?: UidSet;
|
|
@@ -3,12 +3,14 @@ export type UidSet = {
|
|
|
3
3
|
series: string;
|
|
4
4
|
sop: string;
|
|
5
5
|
};
|
|
6
|
+
export declare function hashUid(salt: string, key: string): string;
|
|
7
|
+
export declare function seedToSalt(seed: number | undefined): string | undefined;
|
|
6
8
|
export declare const PARAMETRIC_STREAM_OFFSET = 2246822507;
|
|
7
9
|
export declare function seededStream(seed: number, offset: number, a: number, b: number): () => number;
|
|
8
10
|
export type GroupUidGenerator = {
|
|
9
11
|
study: (studyIndex: number) => string;
|
|
10
12
|
series: (studyIndex: number, seriesIndex: number) => string;
|
|
11
13
|
};
|
|
12
|
-
export declare function makeGroupUidGenerator(
|
|
14
|
+
export declare function makeGroupUidGenerator(salt?: string): GroupUidGenerator;
|
|
13
15
|
export type UidGenerator = (fileIndex: number) => UidSet;
|
|
14
|
-
export declare function makeUidGenerator(
|
|
16
|
+
export declare function makeUidGenerator(salt?: string): UidGenerator;
|