dicom-synth 1.18.0 → 1.19.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 +1 -1
- package/dist/esm/collection/generate.js +977 -0
- package/dist/esm/collection/writer.js +243 -109
- package/dist/esm/describe/describe.js +142 -5
- package/dist/esm/index.js +396 -223
- package/dist/esm/nonStandardDicom/dicomdir.js +14 -15
- package/dist/esm/platform/bytes.js +39 -0
- package/dist/esm/platform/random.js +22 -0
- package/dist/esm/platform/sha256.js +131 -0
- package/dist/esm/schema/parametric.js +73 -4
- package/dist/esm/syntheticFixtures/generator.js +31 -14
- package/dist/esm/syntheticFixtures/streamWrite.js +156 -13
- package/dist/esm/syntheticFixtures/uid.js +148 -4
- package/dist/esm/syntheticFixtures/violations.js +18 -6
- package/dist/types/collection/generate.d.ts +43 -0
- package/dist/types/collection/writer.d.ts +0 -27
- package/dist/types/index.d.ts +2 -1
- package/dist/types/nonStandardDicom/dicomdir.d.ts +1 -6
- package/dist/types/platform/bytes.d.ts +4 -0
- package/dist/types/platform/random.d.ts +2 -0
- package/dist/types/platform/sha256.d.ts +1 -0
- package/dist/types/syntheticFixtures/generator.d.ts +2 -2
- package/dist/types/syntheticFixtures/violations.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
// src/platform/bytes.ts
|
|
2
|
+
function bytesToHex(bytes) {
|
|
3
|
+
let hex = "";
|
|
4
|
+
for (const byte of bytes) {
|
|
5
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
6
|
+
}
|
|
7
|
+
return hex;
|
|
8
|
+
}
|
|
9
|
+
function utf8Bytes(text) {
|
|
10
|
+
return new TextEncoder().encode(text);
|
|
11
|
+
}
|
|
12
|
+
function concatBytes(...parts) {
|
|
13
|
+
const out = new Uint8Array(parts.reduce((sum, p) => sum + p.length, 0));
|
|
14
|
+
let offset = 0;
|
|
15
|
+
for (const part of parts) {
|
|
16
|
+
out.set(part, offset);
|
|
17
|
+
offset += part.length;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
function patternBytes(length, pattern) {
|
|
22
|
+
const out = new Uint8Array(length);
|
|
23
|
+
const seed = utf8Bytes(pattern);
|
|
24
|
+
if (length === 0 || seed.length === 0) return out;
|
|
25
|
+
out.set(seed.subarray(0, Math.min(seed.length, length)));
|
|
26
|
+
let filled = Math.min(seed.length, length);
|
|
27
|
+
while (filled < length) {
|
|
28
|
+
const chunk = Math.min(filled, length - filled);
|
|
29
|
+
out.copyWithin(filled, 0, chunk);
|
|
30
|
+
filled += chunk;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/platform/random.ts
|
|
36
|
+
function randomUint32() {
|
|
37
|
+
return globalThis.crypto.getRandomValues(new Uint32Array(1))[0];
|
|
38
|
+
}
|
|
39
|
+
function randomHex(byteLength) {
|
|
40
|
+
return bytesToHex(
|
|
41
|
+
globalThis.crypto.getRandomValues(new Uint8Array(byteLength))
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/syntheticFixtures/tagTemplate.ts
|
|
46
|
+
var TEMPLATE_VOCAB = [
|
|
47
|
+
"index",
|
|
48
|
+
"studyIndex",
|
|
49
|
+
"seriesIndex",
|
|
50
|
+
"instanceNumber"
|
|
51
|
+
];
|
|
52
|
+
var TEMPLATE_PART_RE = /\{\{|\}\}|\{([a-zA-Z]+)\}/g;
|
|
53
|
+
function resolveTagTemplates(tags, context) {
|
|
54
|
+
if (!tags) return tags;
|
|
55
|
+
const resolved = {};
|
|
56
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
57
|
+
resolved[key] = typeof value === "string" ? resolveString(value, context) : value;
|
|
58
|
+
}
|
|
59
|
+
return resolved;
|
|
60
|
+
}
|
|
61
|
+
function resolveString(value, context) {
|
|
62
|
+
return value.replace(TEMPLATE_PART_RE, (match, name) => {
|
|
63
|
+
if (name === void 0) return match === "{{" ? "{" : "}";
|
|
64
|
+
if (!TEMPLATE_VOCAB.includes(name)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`tag template: unknown placeholder "{${name}}" \u2014 allowed: ${TEMPLATE_VOCAB.map((v) => `{${v}}`).join(", ")}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const resolved = context[name];
|
|
70
|
+
if (resolved === void 0) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`tag template: placeholder "{${name}}" is not available here \u2014 it only applies inside grouped studies`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return String(resolved);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/schema/limits.ts
|
|
80
|
+
var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
|
|
81
|
+
var MAX_STREAM_BYTES = 50 * 1024 * 1024 * 1024;
|
|
82
|
+
|
|
83
|
+
// src/schema/validate.ts
|
|
84
|
+
var TYPE_CAPABILITIES = {
|
|
85
|
+
"valid-image": { image: true },
|
|
86
|
+
"invalid-uid-image": { image: true },
|
|
87
|
+
"vendor-warnings-image": { image: true },
|
|
88
|
+
"large-image": { image: true },
|
|
89
|
+
"fake-signature": { image: false },
|
|
90
|
+
"non-dicom": { image: false },
|
|
91
|
+
dicomdir: { image: false }
|
|
92
|
+
};
|
|
93
|
+
function isImageType(type) {
|
|
94
|
+
return TYPE_CAPABILITIES[type].image;
|
|
95
|
+
}
|
|
96
|
+
var HEX_TAG_RE = /^[0-9a-fA-F]{8}$/;
|
|
97
|
+
function isRawTagValue(value) {
|
|
98
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.vr === "string";
|
|
99
|
+
}
|
|
100
|
+
function positionalName(prefix, ordinal) {
|
|
101
|
+
return `${prefix}-${String(ordinal).padStart(3, "0")}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/loadDcmjs.ts
|
|
105
|
+
import dcmjsDefaultImport, * as dcmjsNamespace from "dcmjs";
|
|
106
|
+
function loadDcmjs() {
|
|
107
|
+
if (dcmjsNamespace.data?.DicomDict) {
|
|
108
|
+
return dcmjsNamespace;
|
|
109
|
+
}
|
|
110
|
+
const d = dcmjsDefaultImport;
|
|
111
|
+
if (d?.data?.DicomDict) {
|
|
112
|
+
return d;
|
|
113
|
+
}
|
|
114
|
+
if (d?.default?.data?.DicomDict) {
|
|
115
|
+
return d.default;
|
|
116
|
+
}
|
|
117
|
+
throw new Error(
|
|
118
|
+
"dcmjs failed to load (missing data.DicomDict). Install dcmjs >= 0.29."
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
var dcmjs = loadDcmjs();
|
|
122
|
+
|
|
123
|
+
// src/nonStandardDicom/dicomdir.ts
|
|
124
|
+
var DICOMDIR_SOP_CLASS_UID = "1.2.840.10008.1.3.10";
|
|
125
|
+
function buildDicomdirBuffer() {
|
|
126
|
+
const uid = DICOMDIR_SOP_CLASS_UID;
|
|
127
|
+
const buf = new Uint8Array(256);
|
|
128
|
+
const view = new DataView(buf.buffer);
|
|
129
|
+
buf.set(utf8Bytes("DICM"), 128);
|
|
130
|
+
let off = 132;
|
|
131
|
+
view.setUint16(off, 2, true);
|
|
132
|
+
off += 2;
|
|
133
|
+
view.setUint16(off, 2, true);
|
|
134
|
+
off += 2;
|
|
135
|
+
buf.set(utf8Bytes("UI"), off);
|
|
136
|
+
off += 2;
|
|
137
|
+
view.setUint16(off, uid.length, true);
|
|
138
|
+
off += 2;
|
|
139
|
+
buf.set(utf8Bytes(uid), off);
|
|
140
|
+
off += uid.length;
|
|
141
|
+
return buf.subarray(0, off);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/syntheticFixtures/generator.ts
|
|
145
|
+
var MODALITY_PRESETS = {
|
|
146
|
+
CT: {
|
|
147
|
+
sopClassUid: "1.2.840.10008.5.1.4.1.1.2",
|
|
148
|
+
// CT Image Storage
|
|
149
|
+
attributes: {}
|
|
150
|
+
},
|
|
151
|
+
PT: {
|
|
152
|
+
sopClassUid: "1.2.840.10008.5.1.4.1.1.128",
|
|
153
|
+
// PET Image Storage
|
|
154
|
+
attributes: {
|
|
155
|
+
Units: "BQML",
|
|
156
|
+
DecayCorrection: "NONE",
|
|
157
|
+
CorrectedImage: ["ATTN", "DECY"]
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
MR: {
|
|
161
|
+
sopClassUid: "1.2.840.10008.5.1.4.1.1.4",
|
|
162
|
+
// MR Image Storage
|
|
163
|
+
attributes: {
|
|
164
|
+
ScanningSequence: "SE",
|
|
165
|
+
SequenceVariant: "NONE"
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
CR: {
|
|
169
|
+
sopClassUid: "1.2.840.10008.5.1.4.1.1.1",
|
|
170
|
+
// CR Image Storage
|
|
171
|
+
attributes: {}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
var TRANSFER_SYNTAX_UID = {
|
|
175
|
+
"explicit-vr-little-endian": "1.2.840.10008.1.2.1",
|
|
176
|
+
"implicit-vr-little-endian": "1.2.840.10008.1.2"
|
|
177
|
+
};
|
|
178
|
+
function buildTagToKeyword() {
|
|
179
|
+
const map = /* @__PURE__ */ new Map();
|
|
180
|
+
const nm = dcmjs.data?.DicomMetaDictionary?.nameMap;
|
|
181
|
+
if (!nm) return map;
|
|
182
|
+
for (const [keyword, info] of Object.entries(nm)) {
|
|
183
|
+
if (info?.tag) {
|
|
184
|
+
const hex = info.tag.replace(/[(,)]/g, "").toUpperCase();
|
|
185
|
+
if (hex.length === 8) map.set(hex, keyword);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return map;
|
|
189
|
+
}
|
|
190
|
+
var tagToKeyword = buildTagToKeyword();
|
|
191
|
+
function toRawElement(raw) {
|
|
192
|
+
const value = raw.vr === "SQ" ? raw.value.map(
|
|
193
|
+
(item) => Object.fromEntries(
|
|
194
|
+
Object.entries(item).map(([tag, nested]) => [
|
|
195
|
+
tag.toUpperCase(),
|
|
196
|
+
toRawElement(nested)
|
|
197
|
+
])
|
|
198
|
+
)
|
|
199
|
+
) : raw.value;
|
|
200
|
+
return { vr: raw.vr, Value: Array.isArray(value) ? value : [value] };
|
|
201
|
+
}
|
|
202
|
+
function applyTagOverrides(dataset, tags) {
|
|
203
|
+
if (!tags) return void 0;
|
|
204
|
+
let raw;
|
|
205
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
206
|
+
if (isRawTagValue(value)) {
|
|
207
|
+
raw ?? (raw = {});
|
|
208
|
+
raw[key.toUpperCase()] = toRawElement(value);
|
|
209
|
+
} else if (HEX_TAG_RE.test(key)) {
|
|
210
|
+
const keyword = tagToKeyword.get(key.toUpperCase());
|
|
211
|
+
if (!keyword) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`dicom-synth: unknown hex tag "${key}" \u2014 dictionary tags take plain values; private/unknown tags require the raw { vr, value } form`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
dataset[keyword] = value;
|
|
217
|
+
} else {
|
|
218
|
+
dataset[key] = value;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return raw;
|
|
222
|
+
}
|
|
223
|
+
function rawScalarString(rawElements, tag) {
|
|
224
|
+
const value = rawElements?.[tag]?.Value;
|
|
225
|
+
return value?.length === 1 && typeof value[0] === "string" ? value[0] : void 0;
|
|
226
|
+
}
|
|
227
|
+
function syncMetaWithDataset(meta, dataset, rawElements) {
|
|
228
|
+
const sopInstance = rawScalarString(rawElements, "00080018") ?? (typeof dataset.SOPInstanceUID === "string" ? dataset.SOPInstanceUID : void 0);
|
|
229
|
+
if (sopInstance !== void 0) {
|
|
230
|
+
meta.MediaStorageSOPInstanceUID = sopInstance;
|
|
231
|
+
}
|
|
232
|
+
const sopClass = rawScalarString(rawElements, "00080016") ?? (typeof dataset.SOPClassUID === "string" ? dataset.SOPClassUID : void 0);
|
|
233
|
+
if (sopClass !== void 0) {
|
|
234
|
+
meta.MediaStorageSOPClassUID = sopClass;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function buildMeta(uid, modality, transferSyntax = "explicit-vr-little-endian") {
|
|
238
|
+
return {
|
|
239
|
+
FileMetaInformationVersion: new Uint8Array([0, 1]).buffer,
|
|
240
|
+
MediaStorageSOPClassUID: MODALITY_PRESETS[modality].sopClassUid,
|
|
241
|
+
MediaStorageSOPInstanceUID: uid.sop,
|
|
242
|
+
TransferSyntaxUID: TRANSFER_SYNTAX_UID[transferSyntax],
|
|
243
|
+
ImplementationClassUID: "1.2.3.4",
|
|
244
|
+
ImplementationVersionName: "SYNTH"
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function buildBaseImageDataset(uid, modality) {
|
|
248
|
+
const preset = MODALITY_PRESETS[modality];
|
|
249
|
+
return {
|
|
250
|
+
PatientName: "SYNTH^SUBJECT",
|
|
251
|
+
PatientID: "SYNTH_PID",
|
|
252
|
+
Modality: modality,
|
|
253
|
+
SOPClassUID: preset.sopClassUid,
|
|
254
|
+
SOPInstanceUID: uid.sop,
|
|
255
|
+
SeriesInstanceUID: uid.series,
|
|
256
|
+
StudyInstanceUID: uid.study,
|
|
257
|
+
StudyDescription: "Synthetic study",
|
|
258
|
+
SeriesDescription: "Synthetic series",
|
|
259
|
+
Rows: 1,
|
|
260
|
+
Columns: 1,
|
|
261
|
+
BitsAllocated: 16,
|
|
262
|
+
BitsStored: 16,
|
|
263
|
+
HighBit: 15,
|
|
264
|
+
SamplesPerPixel: 1,
|
|
265
|
+
PhotometricInterpretation: "MONOCHROME2",
|
|
266
|
+
PixelRepresentation: 0,
|
|
267
|
+
PixelData: new Uint8Array([0, 0]).buffer,
|
|
268
|
+
// Declares PixelData's VR explicitly — without it dcmjs logs
|
|
269
|
+
// "No value representation given for PixelData" and falls back to OW anyway.
|
|
270
|
+
_vrMap: { PixelData: "OW" },
|
|
271
|
+
...preset.attributes
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function serializeDict(meta, dataset, rawElements) {
|
|
275
|
+
const dicomDict = new dcmjs.data.DicomDict(
|
|
276
|
+
dcmjs.data.DicomMetaDictionary.denaturalizeDataset(
|
|
277
|
+
meta
|
|
278
|
+
)
|
|
279
|
+
);
|
|
280
|
+
dicomDict.dict = dcmjs.data.DicomMetaDictionary.denaturalizeDataset(
|
|
281
|
+
dataset
|
|
282
|
+
);
|
|
283
|
+
if (rawElements) {
|
|
284
|
+
Object.assign(dicomDict.dict, rawElements);
|
|
285
|
+
}
|
|
286
|
+
return new Uint8Array(dicomDict.write({ allowInvalidVRLength: true }));
|
|
287
|
+
}
|
|
288
|
+
var MAX_DIMENSION = 65535;
|
|
289
|
+
function applySizing(dataset, meta, spec, rawElements) {
|
|
290
|
+
if (spec.targetSizeKb !== void 0) {
|
|
291
|
+
const clampDim = (n) => Math.min(MAX_DIMENSION, Math.max(1, Math.round(n)));
|
|
292
|
+
const minimalPixelBytes = dataset.PixelData.byteLength;
|
|
293
|
+
const overhead = serializeDict(meta, dataset, rawElements).length - minimalPixelBytes;
|
|
294
|
+
const cells = Math.max(
|
|
295
|
+
1,
|
|
296
|
+
Math.round((spec.targetSizeKb * 1024 - overhead) / 2)
|
|
297
|
+
);
|
|
298
|
+
const rows = clampDim(Math.sqrt(cells));
|
|
299
|
+
const columns = clampDim(cells / rows);
|
|
300
|
+
dataset.Rows = rows;
|
|
301
|
+
dataset.Columns = columns;
|
|
302
|
+
dataset.PixelData = new ArrayBuffer(rows * columns * 2);
|
|
303
|
+
} else if (spec.rows !== void 0 && spec.columns !== void 0) {
|
|
304
|
+
dataset.Rows = spec.rows;
|
|
305
|
+
dataset.Columns = spec.columns;
|
|
306
|
+
if (spec.frames !== void 0) dataset.NumberOfFrames = spec.frames;
|
|
307
|
+
dataset.PixelData = new ArrayBuffer(
|
|
308
|
+
spec.rows * spec.columns * (spec.frames ?? 1) * 2
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function buildImageBuffer(spec, uid) {
|
|
313
|
+
const modality = spec.modality ?? "CT";
|
|
314
|
+
const effectiveUid = spec.type === "invalid-uid-image" ? {
|
|
315
|
+
study: `2.25.invalid.study.${uid.study.slice(-6)}`,
|
|
316
|
+
series: `2.25.invalid.series.${uid.series.slice(-6)}`,
|
|
317
|
+
sop: `2.25.invalid.sop.${uid.sop.slice(-6)}`
|
|
318
|
+
} : uid;
|
|
319
|
+
const dataset = buildBaseImageDataset(effectiveUid, modality);
|
|
320
|
+
if (spec.type === "vendor-warnings-image") {
|
|
321
|
+
dataset.Laterality = "";
|
|
322
|
+
dataset.PatientWeight = "0";
|
|
323
|
+
}
|
|
324
|
+
const rawElements = applyTagOverrides(dataset, spec.tags);
|
|
325
|
+
const meta = buildMeta(effectiveUid, modality, spec.transferSyntax);
|
|
326
|
+
syncMetaWithDataset(meta, dataset, rawElements);
|
|
327
|
+
applySizing(dataset, meta, spec, rawElements);
|
|
328
|
+
return { buffer: serializeDict(meta, dataset, rawElements), rawElements };
|
|
329
|
+
}
|
|
330
|
+
function buildFakeSignatureBuffer() {
|
|
331
|
+
const buf = new Uint8Array(200);
|
|
332
|
+
buf.set(utf8Bytes("XXXX"), 128);
|
|
333
|
+
return buf;
|
|
334
|
+
}
|
|
335
|
+
function sizedNonDicomBytes(spec) {
|
|
336
|
+
return spec.targetBytes ?? (spec.targetSizeKb !== void 0 ? spec.targetSizeKb * 1024 : void 0);
|
|
337
|
+
}
|
|
338
|
+
function buildNonDicomBuffer(spec) {
|
|
339
|
+
const bytes = sizedNonDicomBytes(spec);
|
|
340
|
+
if (bytes !== void 0) return patternBytes(bytes, "not dicom ");
|
|
341
|
+
return utf8Bytes(spec.content ?? "not dicom");
|
|
342
|
+
}
|
|
343
|
+
function buildBufferForSpec(spec, uid) {
|
|
344
|
+
switch (spec.type) {
|
|
345
|
+
case "valid-image":
|
|
346
|
+
case "invalid-uid-image":
|
|
347
|
+
case "vendor-warnings-image":
|
|
348
|
+
return buildImageBuffer(spec, uid);
|
|
349
|
+
case "fake-signature":
|
|
350
|
+
return { buffer: buildFakeSignatureBuffer() };
|
|
351
|
+
case "non-dicom":
|
|
352
|
+
return { buffer: buildNonDicomBuffer(spec) };
|
|
353
|
+
case "dicomdir":
|
|
354
|
+
return { buffer: buildDicomdirBuffer() };
|
|
355
|
+
case "large-image":
|
|
356
|
+
throw new Error(
|
|
357
|
+
"large-image cannot be generated in memory \u2014 use writeCollectionFromSpec (disk-only streaming)"
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/platform/sha256.ts
|
|
363
|
+
var K = new Uint32Array([
|
|
364
|
+
1116352408,
|
|
365
|
+
1899447441,
|
|
366
|
+
3049323471,
|
|
367
|
+
3921009573,
|
|
368
|
+
961987163,
|
|
369
|
+
1508970993,
|
|
370
|
+
2453635748,
|
|
371
|
+
2870763221,
|
|
372
|
+
3624381080,
|
|
373
|
+
310598401,
|
|
374
|
+
607225278,
|
|
375
|
+
1426881987,
|
|
376
|
+
1925078388,
|
|
377
|
+
2162078206,
|
|
378
|
+
2614888103,
|
|
379
|
+
3248222580,
|
|
380
|
+
3835390401,
|
|
381
|
+
4022224774,
|
|
382
|
+
264347078,
|
|
383
|
+
604807628,
|
|
384
|
+
770255983,
|
|
385
|
+
1249150122,
|
|
386
|
+
1555081692,
|
|
387
|
+
1996064986,
|
|
388
|
+
2554220882,
|
|
389
|
+
2821834349,
|
|
390
|
+
2952996808,
|
|
391
|
+
3210313671,
|
|
392
|
+
3336571891,
|
|
393
|
+
3584528711,
|
|
394
|
+
113926993,
|
|
395
|
+
338241895,
|
|
396
|
+
666307205,
|
|
397
|
+
773529912,
|
|
398
|
+
1294757372,
|
|
399
|
+
1396182291,
|
|
400
|
+
1695183700,
|
|
401
|
+
1986661051,
|
|
402
|
+
2177026350,
|
|
403
|
+
2456956037,
|
|
404
|
+
2730485921,
|
|
405
|
+
2820302411,
|
|
406
|
+
3259730800,
|
|
407
|
+
3345764771,
|
|
408
|
+
3516065817,
|
|
409
|
+
3600352804,
|
|
410
|
+
4094571909,
|
|
411
|
+
275423344,
|
|
412
|
+
430227734,
|
|
413
|
+
506948616,
|
|
414
|
+
659060556,
|
|
415
|
+
883997877,
|
|
416
|
+
958139571,
|
|
417
|
+
1322822218,
|
|
418
|
+
1537002063,
|
|
419
|
+
1747873779,
|
|
420
|
+
1955562222,
|
|
421
|
+
2024104815,
|
|
422
|
+
2227730452,
|
|
423
|
+
2361852424,
|
|
424
|
+
2428436474,
|
|
425
|
+
2756734187,
|
|
426
|
+
3204031479,
|
|
427
|
+
3329325298
|
|
428
|
+
]);
|
|
429
|
+
var rotr = (x, n) => x >>> n | x << 32 - n;
|
|
430
|
+
function sha256(input) {
|
|
431
|
+
const data2 = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
432
|
+
const padded = new Uint8Array((data2.length + 8 >> 6) + 1 << 6);
|
|
433
|
+
padded.set(data2);
|
|
434
|
+
padded[data2.length] = 128;
|
|
435
|
+
new DataView(padded.buffer).setBigUint64(
|
|
436
|
+
padded.length - 8,
|
|
437
|
+
BigInt(data2.length * 8),
|
|
438
|
+
false
|
|
439
|
+
);
|
|
440
|
+
const h = new Uint32Array([
|
|
441
|
+
1779033703,
|
|
442
|
+
3144134277,
|
|
443
|
+
1013904242,
|
|
444
|
+
2773480762,
|
|
445
|
+
1359893119,
|
|
446
|
+
2600822924,
|
|
447
|
+
528734635,
|
|
448
|
+
1541459225
|
|
449
|
+
]);
|
|
450
|
+
const w = new Uint32Array(64);
|
|
451
|
+
const view = new DataView(padded.buffer);
|
|
452
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
453
|
+
for (let t = 0; t < 16; t++) w[t] = view.getUint32(offset + t * 4, false);
|
|
454
|
+
for (let t = 16; t < 64; t++) {
|
|
455
|
+
const s0 = rotr(w[t - 15], 7) ^ rotr(w[t - 15], 18) ^ w[t - 15] >>> 3;
|
|
456
|
+
const s1 = rotr(w[t - 2], 17) ^ rotr(w[t - 2], 19) ^ w[t - 2] >>> 10;
|
|
457
|
+
w[t] = w[t - 16] + s0 + w[t - 7] + s1 >>> 0;
|
|
458
|
+
}
|
|
459
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
460
|
+
for (let t = 0; t < 64; t++) {
|
|
461
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
462
|
+
const ch = e & f ^ ~e & g;
|
|
463
|
+
const temp1 = hh + S1 + ch + K[t] + w[t] >>> 0;
|
|
464
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
465
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
466
|
+
const temp2 = S0 + maj >>> 0;
|
|
467
|
+
hh = g;
|
|
468
|
+
g = f;
|
|
469
|
+
f = e;
|
|
470
|
+
e = d + temp1 >>> 0;
|
|
471
|
+
d = c;
|
|
472
|
+
c = b;
|
|
473
|
+
b = a;
|
|
474
|
+
a = temp1 + temp2 >>> 0;
|
|
475
|
+
}
|
|
476
|
+
h[0] = h[0] + a >>> 0;
|
|
477
|
+
h[1] = h[1] + b >>> 0;
|
|
478
|
+
h[2] = h[2] + c >>> 0;
|
|
479
|
+
h[3] = h[3] + d >>> 0;
|
|
480
|
+
h[4] = h[4] + e >>> 0;
|
|
481
|
+
h[5] = h[5] + f >>> 0;
|
|
482
|
+
h[6] = h[6] + g >>> 0;
|
|
483
|
+
h[7] = h[7] + hh >>> 0;
|
|
484
|
+
}
|
|
485
|
+
const out = new Uint8Array(32);
|
|
486
|
+
const outView = new DataView(out.buffer);
|
|
487
|
+
for (let i = 0; i < 8; i++) outView.setUint32(i * 4, h[i], false);
|
|
488
|
+
return out;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/syntheticFixtures/uid.ts
|
|
492
|
+
function hashUid(salt, key) {
|
|
493
|
+
const digest = sha256(`${salt} ${key}`);
|
|
494
|
+
const n = BigInt(`0x${bytesToHex(digest.subarray(0, 16))}`);
|
|
495
|
+
return `2.25.${n.toString()}`;
|
|
496
|
+
}
|
|
497
|
+
function randomUid() {
|
|
498
|
+
const n = BigInt(`0x${randomHex(16)}`);
|
|
499
|
+
return `2.25.${n.toString()}`;
|
|
500
|
+
}
|
|
501
|
+
function seedToSalt(seed) {
|
|
502
|
+
return seed === void 0 ? void 0 : `seed:${seed}`;
|
|
503
|
+
}
|
|
504
|
+
function makeGroupUidGenerator(salt) {
|
|
505
|
+
if (salt === void 0) {
|
|
506
|
+
return { study: () => randomUid(), series: () => randomUid() };
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
study: (studyIndex) => hashUid(salt, `study:${studyIndex}`),
|
|
510
|
+
series: (studyIndex, seriesIndex) => hashUid(salt, `series:${studyIndex}:${seriesIndex}`)
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function makeUidGenerator(salt) {
|
|
514
|
+
if (salt === void 0) {
|
|
515
|
+
return () => ({ study: randomUid(), series: randomUid(), sop: randomUid() });
|
|
516
|
+
}
|
|
517
|
+
return (fileIndex) => ({
|
|
518
|
+
study: hashUid(salt, `study:flat:${fileIndex}`),
|
|
519
|
+
series: hashUid(salt, `series:flat:${fileIndex}`),
|
|
520
|
+
sop: hashUid(salt, `sop:${fileIndex}`)
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/syntheticFixtures/violations.ts
|
|
525
|
+
var dcmjsAny = dcmjs;
|
|
526
|
+
function parseBuffer(buffer) {
|
|
527
|
+
const ab = buffer.buffer.slice(
|
|
528
|
+
buffer.byteOffset,
|
|
529
|
+
buffer.byteOffset + buffer.byteLength
|
|
530
|
+
);
|
|
531
|
+
return dcmjsAny.data.DicomMessage.readFile(ab);
|
|
532
|
+
}
|
|
533
|
+
function reserialize(parsed) {
|
|
534
|
+
return new Uint8Array(parsed.write({ allowInvalidVRLength: true }));
|
|
535
|
+
}
|
|
536
|
+
var OVERLONG_UID = `2.25.${"0".repeat(60)}`;
|
|
537
|
+
var VIOLATION_LEVEL = {
|
|
538
|
+
"uid-too-long": "tag",
|
|
539
|
+
"non-conformant-uid": "tag",
|
|
540
|
+
"vr-max-length-exceeded": "tag",
|
|
541
|
+
"missing-type1-tag": "tag",
|
|
542
|
+
"missing-meta-header": "byte",
|
|
543
|
+
"malformed-sq-delimiter": "byte"
|
|
544
|
+
};
|
|
545
|
+
function applyTagLevel(buffer, violations, rawElements) {
|
|
546
|
+
if (violations.length === 0) return buffer;
|
|
547
|
+
let parsed;
|
|
548
|
+
try {
|
|
549
|
+
parsed = parseBuffer(buffer);
|
|
550
|
+
} catch {
|
|
551
|
+
console.warn(
|
|
552
|
+
"dicom-synth: could not parse buffer for tag-level violations \u2014 skipping"
|
|
553
|
+
);
|
|
554
|
+
return buffer;
|
|
555
|
+
}
|
|
556
|
+
const natural = dcmjsAny.data.DicomMetaDictionary.naturalizeDataset(
|
|
557
|
+
parsed.dict
|
|
558
|
+
);
|
|
559
|
+
for (const v of violations) {
|
|
560
|
+
switch (v) {
|
|
561
|
+
case "uid-too-long":
|
|
562
|
+
break;
|
|
563
|
+
case "non-conformant-uid":
|
|
564
|
+
natural.SOPInstanceUID = "2.25.01.234.567";
|
|
565
|
+
break;
|
|
566
|
+
case "vr-max-length-exceeded":
|
|
567
|
+
natural.StudyDescription = "X".repeat(65);
|
|
568
|
+
break;
|
|
569
|
+
case "missing-type1-tag":
|
|
570
|
+
delete natural.SOPClassUID;
|
|
571
|
+
break;
|
|
572
|
+
default:
|
|
573
|
+
throw new Error(
|
|
574
|
+
`dicom-synth: unhandled tag-level violation "${v}"`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
parsed.dict = dcmjsAny.data.DicomMetaDictionary.denaturalizeDataset(natural);
|
|
579
|
+
if (rawElements) {
|
|
580
|
+
Object.assign(parsed.dict, rawElements);
|
|
581
|
+
}
|
|
582
|
+
if (violations.includes("uid-too-long")) {
|
|
583
|
+
;
|
|
584
|
+
parsed.dict["00080018"] = {
|
|
585
|
+
vr: "UI",
|
|
586
|
+
Value: [OVERLONG_UID]
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
return reserialize(parsed);
|
|
590
|
+
}
|
|
591
|
+
function applyByteLevel(buffer, violations) {
|
|
592
|
+
let result = buffer;
|
|
593
|
+
for (const v of violations) {
|
|
594
|
+
switch (v) {
|
|
595
|
+
case "missing-meta-header":
|
|
596
|
+
result = result.subarray(132);
|
|
597
|
+
break;
|
|
598
|
+
case "malformed-sq-delimiter": {
|
|
599
|
+
const delimiter = new Uint8Array(8);
|
|
600
|
+
const view = new DataView(delimiter.buffer);
|
|
601
|
+
view.setUint16(0, 65534, true);
|
|
602
|
+
view.setUint16(2, 57565, true);
|
|
603
|
+
view.setUint32(4, 4, true);
|
|
604
|
+
result = concatBytes(result, delimiter);
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
default:
|
|
608
|
+
throw new Error(
|
|
609
|
+
`dicom-synth: unhandled byte-level violation "${v}"`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return result;
|
|
614
|
+
}
|
|
615
|
+
function applyViolations(buffer, violations, rawElements) {
|
|
616
|
+
if (violations.length === 0) return buffer;
|
|
617
|
+
const tagViolations = violations.filter((v) => VIOLATION_LEVEL[v] === "tag");
|
|
618
|
+
const byteViolations = violations.filter((v) => VIOLATION_LEVEL[v] === "byte");
|
|
619
|
+
return applyByteLevel(
|
|
620
|
+
applyTagLevel(buffer, tagViolations, rawElements),
|
|
621
|
+
byteViolations
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/collection/generate.ts
|
|
626
|
+
var NO_EXTENSION_TYPES = /* @__PURE__ */ new Set([
|
|
627
|
+
"fake-signature",
|
|
628
|
+
"non-dicom"
|
|
629
|
+
]);
|
|
630
|
+
function filename(type, index, padWidth) {
|
|
631
|
+
const padded = String(index).padStart(padWidth, "0");
|
|
632
|
+
const base = `${type}-${padded}`;
|
|
633
|
+
return NO_EXTENSION_TYPES.has(type) ? base : `${base}.dcm`;
|
|
634
|
+
}
|
|
635
|
+
function entryCount(entries) {
|
|
636
|
+
return entries.reduce((sum, e) => sum + (e.count ?? 1), 0);
|
|
637
|
+
}
|
|
638
|
+
function seriesFileCount(series) {
|
|
639
|
+
return series.instances ? series.instances.count : entryCount(series.entries ?? []);
|
|
640
|
+
}
|
|
641
|
+
function studyFileCount(studies) {
|
|
642
|
+
return studies.reduce(
|
|
643
|
+
(sum, study) => sum + (study.count ?? 1) * study.series.reduce((s, series) => s + seriesFileCount(series), 0),
|
|
644
|
+
0
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
function isDirNode(node) {
|
|
648
|
+
return "children" in node;
|
|
649
|
+
}
|
|
650
|
+
function treeFileCount(nodes) {
|
|
651
|
+
return nodes.reduce(
|
|
652
|
+
(sum, node) => sum + (isDirNode(node) ? treeFileCount(node.children) : node.count ?? 1),
|
|
653
|
+
0
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
function* seriesFiles(series) {
|
|
657
|
+
if (series.instances) {
|
|
658
|
+
const inst = series.instances;
|
|
659
|
+
for (let i = 0; i < inst.count; i++) {
|
|
660
|
+
const modality = Array.isArray(inst.modality) ? inst.modality[i] : inst.modality;
|
|
661
|
+
const baseSpec = {
|
|
662
|
+
...inst.targetBytes ? { type: "large-image", targetBytes: inst.targetBytes[i] } : { type: "valid-image", targetSizeKb: inst.targetSizeKb?.[i] },
|
|
663
|
+
...modality !== void 0 ? { modality } : {}
|
|
664
|
+
};
|
|
665
|
+
const instanceTags = {};
|
|
666
|
+
if (inst.tags) {
|
|
667
|
+
for (const [key, values] of Object.entries(inst.tags)) {
|
|
668
|
+
instanceTags[key] = values[i];
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
yield { baseSpec, instanceTags };
|
|
672
|
+
}
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
for (const entry of series.entries ?? []) {
|
|
676
|
+
const { count = 1, tags, ...baseSpec } = entry;
|
|
677
|
+
for (let i = 0; i < count; i++) {
|
|
678
|
+
yield { baseSpec, instanceTags: tags ?? {} };
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function resolveUid(salt, index, override) {
|
|
683
|
+
return { ...makeUidGenerator(salt)(index), ...override };
|
|
684
|
+
}
|
|
685
|
+
function effectiveSalt(spec) {
|
|
686
|
+
return spec.uidSalt ?? seedToSalt(spec.seed);
|
|
687
|
+
}
|
|
688
|
+
async function generateFile(spec, options) {
|
|
689
|
+
const index = options?.index ?? 0;
|
|
690
|
+
const padWidth = options?.padWidth ?? 3;
|
|
691
|
+
const uid = resolveUid(effectiveSalt(options ?? {}), index, options?.uid);
|
|
692
|
+
const resolvedSpec = "tags" in spec && spec.tags ? {
|
|
693
|
+
...spec,
|
|
694
|
+
tags: resolveTagTemplates(spec.tags, { index, ...options?.context })
|
|
695
|
+
} : spec;
|
|
696
|
+
const built = buildBufferForSpec(resolvedSpec, uid);
|
|
697
|
+
let buffer = built.buffer;
|
|
698
|
+
const violations = "violations" in spec ? spec.violations : void 0;
|
|
699
|
+
if (violations?.length) {
|
|
700
|
+
buffer = applyViolations(buffer, violations, built.rawElements);
|
|
701
|
+
}
|
|
702
|
+
const name = filename(spec.type, index, padWidth);
|
|
703
|
+
return {
|
|
704
|
+
filename: name,
|
|
705
|
+
relativePath: name,
|
|
706
|
+
buffer,
|
|
707
|
+
type: spec.type,
|
|
708
|
+
index
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
function hashString(s) {
|
|
712
|
+
let h = 2166136261;
|
|
713
|
+
for (let i = 0; i < s.length; i++) {
|
|
714
|
+
h ^= s.charCodeAt(i);
|
|
715
|
+
h = Math.imul(h, 16777619);
|
|
716
|
+
}
|
|
717
|
+
return h >>> 0;
|
|
718
|
+
}
|
|
719
|
+
var UNICODE_TOKENS = ["\xE9", "\xFC", "\xF1", "\u65E5", "\u2122", "\u03A9"];
|
|
720
|
+
var NEST_DEPTH = 8;
|
|
721
|
+
var LONG_NAME_TARGET = 200;
|
|
722
|
+
function applyPathQuirks(relativePath, quirks) {
|
|
723
|
+
const segments = relativePath.split("/");
|
|
724
|
+
let dirs = segments.slice(0, -1);
|
|
725
|
+
const leaf = segments[segments.length - 1];
|
|
726
|
+
const dot = leaf.lastIndexOf(".");
|
|
727
|
+
let stem = dot > 0 ? leaf.slice(0, dot) : leaf;
|
|
728
|
+
const ext = dot > 0 ? leaf.slice(dot) : "";
|
|
729
|
+
const unicode = (s) => `${s}${UNICODE_TOKENS[hashString(s) % UNICODE_TOKENS.length]}`;
|
|
730
|
+
const has = (q) => quirks.includes(q);
|
|
731
|
+
if (has("deep-nesting")) {
|
|
732
|
+
dirs = [...dirs, ...Array.from({ length: NEST_DEPTH }, (_, i) => `d${i}`)];
|
|
733
|
+
}
|
|
734
|
+
if (has("unicode")) {
|
|
735
|
+
dirs = dirs.map(unicode);
|
|
736
|
+
stem = unicode(stem);
|
|
737
|
+
}
|
|
738
|
+
if (has("long-name")) {
|
|
739
|
+
const pad = (s) => s.length < LONG_NAME_TARGET ? s + "x".repeat(LONG_NAME_TARGET - s.length) : s;
|
|
740
|
+
stem = pad(stem);
|
|
741
|
+
if (dirs.length > 0) {
|
|
742
|
+
const last = dirs.length - 1;
|
|
743
|
+
dirs = dirs.map((d, i) => i === last ? pad(d) : d);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
let newLeaf = stem + ext;
|
|
747
|
+
if (has("trailing-dot")) {
|
|
748
|
+
if (dirs.length > 0) dirs = dirs.map((d) => `${d}.`);
|
|
749
|
+
else newLeaf += ".";
|
|
750
|
+
}
|
|
751
|
+
return { relativePath: [...dirs, newLeaf].join("/"), filename: newLeaf };
|
|
752
|
+
}
|
|
753
|
+
function decorateNames(names, quirks) {
|
|
754
|
+
return quirks.length === 0 ? names : applyPathQuirks(names.relativePath, quirks);
|
|
755
|
+
}
|
|
756
|
+
function hierarchicalName(studyOrdinal, seriesIndex, instanceNumber) {
|
|
757
|
+
const pad3 = (n) => String(n).padStart(3, "0");
|
|
758
|
+
const name = `${String(instanceNumber).padStart(5, "0")}.dcm`;
|
|
759
|
+
return {
|
|
760
|
+
filename: name,
|
|
761
|
+
relativePath: `study-${pad3(studyOrdinal + 1)}/series-${pad3(seriesIndex + 1)}/${name}`
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function withResolvedSeed(spec) {
|
|
765
|
+
return spec.seed === void 0 ? { ...spec, seed: randomUint32() } : spec;
|
|
766
|
+
}
|
|
767
|
+
function withResolvedSalt(spec) {
|
|
768
|
+
return spec.uidSalt === void 0 ? { ...spec, uidSalt: randomHex(16) } : spec;
|
|
769
|
+
}
|
|
770
|
+
function computeNames(type, index, padWidth, grouped, quirks) {
|
|
771
|
+
let names;
|
|
772
|
+
if (grouped) {
|
|
773
|
+
names = hierarchicalName(
|
|
774
|
+
grouped.studyOrdinal,
|
|
775
|
+
grouped.seriesIndex,
|
|
776
|
+
grouped.instanceNumber
|
|
777
|
+
);
|
|
778
|
+
} else {
|
|
779
|
+
const name = filename(type, index, padWidth);
|
|
780
|
+
names = { filename: name, relativePath: name };
|
|
781
|
+
}
|
|
782
|
+
return decorateNames(names, quirks);
|
|
783
|
+
}
|
|
784
|
+
function* planTree(nodes, scope, env) {
|
|
785
|
+
let dirOrdinal = 0;
|
|
786
|
+
for (const node of nodes) {
|
|
787
|
+
if (isDirNode(node)) {
|
|
788
|
+
dirOrdinal++;
|
|
789
|
+
const dirName = node.name ?? positionalName("dir", dirOrdinal);
|
|
790
|
+
const child = {
|
|
791
|
+
...scope,
|
|
792
|
+
dirs: [...scope.dirs, dirName],
|
|
793
|
+
tags: { ...scope.tags, ...node.tags }
|
|
794
|
+
};
|
|
795
|
+
if (node.role === "study") {
|
|
796
|
+
const ordinal = env.counters.study++;
|
|
797
|
+
child.study = {
|
|
798
|
+
ordinal,
|
|
799
|
+
uid: env.groupUids.study(ordinal),
|
|
800
|
+
seriesCount: 0
|
|
801
|
+
};
|
|
802
|
+
} else if (node.role === "series" && child.study) {
|
|
803
|
+
const index = child.study.seriesCount++;
|
|
804
|
+
child.series = {
|
|
805
|
+
index,
|
|
806
|
+
uid: env.groupUids.series(child.study.ordinal, index),
|
|
807
|
+
instanceCount: 0
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
yield* planTree(node.children, child, env);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
const { count = 1, name, ...fileSpec } = node;
|
|
814
|
+
const image = isImageType(fileSpec.type);
|
|
815
|
+
for (let i = 0; i < count; i++) {
|
|
816
|
+
const index = env.counters.file++;
|
|
817
|
+
const instanceNumber = image && scope.series ? ++scope.series.instanceCount : void 0;
|
|
818
|
+
const leaf = name ?? filename(fileSpec.type, index, env.padWidth);
|
|
819
|
+
const names = decorateNames(
|
|
820
|
+
{ filename: leaf, relativePath: [...scope.dirs, leaf].join("/") },
|
|
821
|
+
env.quirks
|
|
822
|
+
);
|
|
823
|
+
const tags = image ? {
|
|
824
|
+
...instanceNumber !== void 0 ? { InstanceNumber: instanceNumber } : {},
|
|
825
|
+
...scope.tags,
|
|
826
|
+
..."tags" in fileSpec ? fileSpec.tags : void 0
|
|
827
|
+
} : void 0;
|
|
828
|
+
yield {
|
|
829
|
+
fileSpec: tags ? { ...fileSpec, tags } : fileSpec,
|
|
830
|
+
index,
|
|
831
|
+
...image && scope.study ? {
|
|
832
|
+
uidOverride: {
|
|
833
|
+
study: scope.study.uid,
|
|
834
|
+
...scope.series ? { series: scope.series.uid } : {}
|
|
835
|
+
}
|
|
836
|
+
} : {},
|
|
837
|
+
context: {
|
|
838
|
+
index,
|
|
839
|
+
...scope.study ? { studyIndex: scope.study.ordinal } : {},
|
|
840
|
+
...scope.series ? { seriesIndex: scope.series.index } : {},
|
|
841
|
+
...instanceNumber !== void 0 ? { instanceNumber } : {}
|
|
842
|
+
},
|
|
843
|
+
filename: names.filename,
|
|
844
|
+
relativePath: names.relativePath
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
function* planCollection(spec) {
|
|
850
|
+
const flatEntries = spec.entries ?? [];
|
|
851
|
+
const studies = spec.studies ?? [];
|
|
852
|
+
const tree = spec.tree ?? [];
|
|
853
|
+
const totalFiles = entryCount(flatEntries) + studyFileCount(studies) + treeFileCount(tree);
|
|
854
|
+
const padWidth = String(totalFiles).length;
|
|
855
|
+
const hierarchical = spec.layout === "hierarchical";
|
|
856
|
+
const quirks = spec.pathQuirks ?? [];
|
|
857
|
+
let globalIndex = 0;
|
|
858
|
+
for (const entry of flatEntries) {
|
|
859
|
+
const { count = 1, ...fileSpec } = entry;
|
|
860
|
+
for (let i = 0; i < count; i++) {
|
|
861
|
+
yield {
|
|
862
|
+
fileSpec,
|
|
863
|
+
index: globalIndex,
|
|
864
|
+
context: { index: globalIndex },
|
|
865
|
+
...computeNames(
|
|
866
|
+
fileSpec.type,
|
|
867
|
+
globalIndex,
|
|
868
|
+
padWidth,
|
|
869
|
+
void 0,
|
|
870
|
+
quirks
|
|
871
|
+
)
|
|
872
|
+
};
|
|
873
|
+
globalIndex++;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const groupUids = makeGroupUidGenerator(effectiveSalt(spec));
|
|
877
|
+
let studyOrdinal = 0;
|
|
878
|
+
for (const study of studies) {
|
|
879
|
+
const copies = study.count ?? 1;
|
|
880
|
+
for (let copy = 0; copy < copies; copy++) {
|
|
881
|
+
const studyUid = groupUids.study(studyOrdinal);
|
|
882
|
+
for (const [seriesIndex, series] of study.series.entries()) {
|
|
883
|
+
const seriesUid = groupUids.series(studyOrdinal, seriesIndex);
|
|
884
|
+
let instanceNumber = 0;
|
|
885
|
+
for (const { baseSpec, instanceTags } of seriesFiles(series)) {
|
|
886
|
+
instanceNumber++;
|
|
887
|
+
const tags = {
|
|
888
|
+
InstanceNumber: instanceNumber,
|
|
889
|
+
...study.tags,
|
|
890
|
+
...series.tags,
|
|
891
|
+
...instanceTags
|
|
892
|
+
};
|
|
893
|
+
yield {
|
|
894
|
+
fileSpec: { ...baseSpec, tags },
|
|
895
|
+
index: globalIndex,
|
|
896
|
+
uidOverride: { study: studyUid, series: seriesUid },
|
|
897
|
+
context: {
|
|
898
|
+
index: globalIndex,
|
|
899
|
+
studyIndex: studyOrdinal,
|
|
900
|
+
seriesIndex,
|
|
901
|
+
instanceNumber
|
|
902
|
+
},
|
|
903
|
+
...computeNames(
|
|
904
|
+
baseSpec.type,
|
|
905
|
+
globalIndex,
|
|
906
|
+
padWidth,
|
|
907
|
+
hierarchical ? { studyOrdinal, seriesIndex, instanceNumber } : void 0,
|
|
908
|
+
quirks
|
|
909
|
+
)
|
|
910
|
+
};
|
|
911
|
+
globalIndex++;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
studyOrdinal++;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (tree.length > 0) {
|
|
918
|
+
yield* planTree(
|
|
919
|
+
tree,
|
|
920
|
+
{ dirs: [], tags: {} },
|
|
921
|
+
{
|
|
922
|
+
groupUids,
|
|
923
|
+
padWidth,
|
|
924
|
+
quirks,
|
|
925
|
+
counters: { file: globalIndex, study: studyOrdinal }
|
|
926
|
+
}
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
async function materialisePlan(plan, salt) {
|
|
931
|
+
const file = await generateFile(plan.fileSpec, {
|
|
932
|
+
index: plan.index,
|
|
933
|
+
uidSalt: salt,
|
|
934
|
+
uid: plan.uidOverride,
|
|
935
|
+
context: plan.context
|
|
936
|
+
});
|
|
937
|
+
return { ...file, filename: plan.filename, relativePath: plan.relativePath };
|
|
938
|
+
}
|
|
939
|
+
async function* generateCollectionFromSpec(spec) {
|
|
940
|
+
const salt = effectiveSalt(spec);
|
|
941
|
+
for (const plan of planCollection(spec)) {
|
|
942
|
+
yield await materialisePlan(plan, salt);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
async function* previewCollection(spec) {
|
|
946
|
+
const salt = effectiveSalt(spec);
|
|
947
|
+
for (const plan of planCollection(spec)) {
|
|
948
|
+
const knownBytes = plan.fileSpec.type === "large-image" ? plan.fileSpec.targetBytes : plan.fileSpec.type === "non-dicom" ? sizedNonDicomBytes(plan.fileSpec) : void 0;
|
|
949
|
+
if (knownBytes !== void 0) {
|
|
950
|
+
yield {
|
|
951
|
+
relativePath: plan.relativePath,
|
|
952
|
+
type: plan.fileSpec.type,
|
|
953
|
+
index: plan.index,
|
|
954
|
+
approxBytes: knownBytes
|
|
955
|
+
};
|
|
956
|
+
} else {
|
|
957
|
+
const file = await materialisePlan(plan, salt);
|
|
958
|
+
yield {
|
|
959
|
+
relativePath: plan.relativePath,
|
|
960
|
+
type: plan.fileSpec.type,
|
|
961
|
+
index: plan.index,
|
|
962
|
+
approxBytes: file.buffer.length
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
export {
|
|
968
|
+
effectiveSalt,
|
|
969
|
+
generateCollectionFromSpec,
|
|
970
|
+
generateFile,
|
|
971
|
+
materialisePlan,
|
|
972
|
+
planCollection,
|
|
973
|
+
previewCollection,
|
|
974
|
+
resolveUid,
|
|
975
|
+
withResolvedSalt,
|
|
976
|
+
withResolvedSeed
|
|
977
|
+
};
|