dicom-synth 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -601
- package/bin/dicom-synth-generate.mjs +106 -12
- package/dist/esm/collection/writer.js +272 -39
- package/dist/esm/describe/describe.js +159 -29
- package/dist/esm/index.js +526 -76
- package/dist/esm/schema/limits.js +7 -0
- package/dist/esm/schema/parametric.js +123 -6
- package/dist/esm/schema/validate.js +93 -6
- package/dist/esm/syntheticFixtures/generator.js +13 -2
- package/dist/esm/syntheticFixtures/streamWrite.js +356 -0
- package/dist/types/collection/writer.d.ts +8 -0
- package/dist/types/describe/describe.d.ts +1 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/schema/limits.d.ts +2 -0
- package/dist/types/schema/types.d.ts +10 -2
- package/dist/types/syntheticFixtures/generator.d.ts +5 -1
- package/dist/types/syntheticFixtures/streamWrite.d.ts +49 -0
- package/package.json +1 -1
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// src/syntheticFixtures/streamWrite.ts
|
|
2
|
+
import { closeSync, mkdirSync as mkdirSync2, openSync, writeSync } from "node:fs";
|
|
3
|
+
import { resolve as resolve2 } from "node:path";
|
|
4
|
+
|
|
5
|
+
// src/schema/limits.ts
|
|
6
|
+
var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
|
|
7
|
+
var MAX_STREAM_BYTES = 50 * 1024 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
// src/loadDcmjs.ts
|
|
10
|
+
import dcmjsDefaultImport, * as dcmjsNamespace from "dcmjs";
|
|
11
|
+
function loadDcmjs() {
|
|
12
|
+
if (dcmjsNamespace.data?.DicomDict) {
|
|
13
|
+
return dcmjsNamespace;
|
|
14
|
+
}
|
|
15
|
+
const d = dcmjsDefaultImport;
|
|
16
|
+
if (d?.data?.DicomDict) {
|
|
17
|
+
return d;
|
|
18
|
+
}
|
|
19
|
+
if (d?.default?.data?.DicomDict) {
|
|
20
|
+
return d.default;
|
|
21
|
+
}
|
|
22
|
+
throw new Error(
|
|
23
|
+
"dcmjs failed to load (missing data.DicomDict). Install dcmjs >= 0.29."
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
var dcmjs = loadDcmjs();
|
|
27
|
+
|
|
28
|
+
// src/nonStandardDicom/dicomdir.ts
|
|
29
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
30
|
+
import { resolve } from "node:path";
|
|
31
|
+
|
|
32
|
+
// src/schema/validate.ts
|
|
33
|
+
var HEX_TAG_RE = /^[0-9a-fA-F]{8}$/;
|
|
34
|
+
|
|
35
|
+
// src/syntheticFixtures/generator.ts
|
|
36
|
+
var MODALITY_PRESETS = {
|
|
37
|
+
CT: {
|
|
38
|
+
sopClassUid: "1.2.840.10008.5.1.4.1.1.2",
|
|
39
|
+
// CT Image Storage
|
|
40
|
+
attributes: {}
|
|
41
|
+
},
|
|
42
|
+
PT: {
|
|
43
|
+
sopClassUid: "1.2.840.10008.5.1.4.1.1.128",
|
|
44
|
+
// PET Image Storage
|
|
45
|
+
attributes: {
|
|
46
|
+
Units: "BQML",
|
|
47
|
+
DecayCorrection: "NONE",
|
|
48
|
+
CorrectedImage: ["ATTN", "DECY"]
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
MR: {
|
|
52
|
+
sopClassUid: "1.2.840.10008.5.1.4.1.1.4",
|
|
53
|
+
// MR Image Storage
|
|
54
|
+
attributes: {
|
|
55
|
+
ScanningSequence: "SE",
|
|
56
|
+
SequenceVariant: "NONE"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
CR: {
|
|
60
|
+
sopClassUid: "1.2.840.10008.5.1.4.1.1.1",
|
|
61
|
+
// CR Image Storage
|
|
62
|
+
attributes: {}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var TRANSFER_SYNTAX_UID = {
|
|
66
|
+
"explicit-vr-little-endian": "1.2.840.10008.1.2.1",
|
|
67
|
+
"implicit-vr-little-endian": "1.2.840.10008.1.2"
|
|
68
|
+
};
|
|
69
|
+
function buildTagToKeyword() {
|
|
70
|
+
const map = /* @__PURE__ */ new Map();
|
|
71
|
+
const nm = dcmjs.data?.DicomMetaDictionary?.nameMap;
|
|
72
|
+
if (!nm) return map;
|
|
73
|
+
for (const [keyword, info] of Object.entries(nm)) {
|
|
74
|
+
if (info?.tag) {
|
|
75
|
+
const hex = info.tag.replace(/[(,)]/g, "").toUpperCase();
|
|
76
|
+
if (hex.length === 8) map.set(hex, keyword);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return map;
|
|
80
|
+
}
|
|
81
|
+
var tagToKeyword = buildTagToKeyword();
|
|
82
|
+
function applyTagOverrides(dataset, tags) {
|
|
83
|
+
if (!tags) return;
|
|
84
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
85
|
+
if (HEX_TAG_RE.test(key)) {
|
|
86
|
+
const keyword = tagToKeyword.get(key.toUpperCase());
|
|
87
|
+
if (keyword) {
|
|
88
|
+
dataset[keyword] = value;
|
|
89
|
+
} else {
|
|
90
|
+
console.warn(`dicom-synth: unknown hex tag "${key}" \u2014 skipping`);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
dataset[key] = value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function buildMeta(uid, modality, transferSyntax = "explicit-vr-little-endian") {
|
|
98
|
+
return {
|
|
99
|
+
FileMetaInformationVersion: new Uint8Array([0, 1]).buffer,
|
|
100
|
+
MediaStorageSOPClassUID: MODALITY_PRESETS[modality].sopClassUid,
|
|
101
|
+
MediaStorageSOPInstanceUID: uid.sop,
|
|
102
|
+
TransferSyntaxUID: TRANSFER_SYNTAX_UID[transferSyntax],
|
|
103
|
+
ImplementationClassUID: "1.2.3.4",
|
|
104
|
+
ImplementationVersionName: "SYNTH"
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function buildBaseImageDataset(uid, modality) {
|
|
108
|
+
const preset = MODALITY_PRESETS[modality];
|
|
109
|
+
return {
|
|
110
|
+
PatientName: "SYNTH^SUBJECT",
|
|
111
|
+
PatientID: "SYNTH_PID",
|
|
112
|
+
Modality: modality,
|
|
113
|
+
SOPClassUID: preset.sopClassUid,
|
|
114
|
+
SOPInstanceUID: uid.sop,
|
|
115
|
+
SeriesInstanceUID: uid.series,
|
|
116
|
+
StudyInstanceUID: uid.study,
|
|
117
|
+
StudyDescription: "Synthetic study",
|
|
118
|
+
SeriesDescription: "Synthetic series",
|
|
119
|
+
Rows: 1,
|
|
120
|
+
Columns: 1,
|
|
121
|
+
BitsAllocated: 16,
|
|
122
|
+
BitsStored: 16,
|
|
123
|
+
HighBit: 15,
|
|
124
|
+
SamplesPerPixel: 1,
|
|
125
|
+
PhotometricInterpretation: "MONOCHROME2",
|
|
126
|
+
PixelRepresentation: 0,
|
|
127
|
+
PixelData: new Uint8Array([0, 0]).buffer,
|
|
128
|
+
// Declares PixelData's VR explicitly — without it dcmjs logs
|
|
129
|
+
// "No value representation given for PixelData" and falls back to OW anyway.
|
|
130
|
+
_vrMap: { PixelData: "OW" },
|
|
131
|
+
...preset.attributes
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function serializeDict(meta, dataset) {
|
|
135
|
+
const dicomDict = new dcmjs.data.DicomDict(
|
|
136
|
+
dcmjs.data.DicomMetaDictionary.denaturalizeDataset(
|
|
137
|
+
meta
|
|
138
|
+
)
|
|
139
|
+
);
|
|
140
|
+
dicomDict.dict = dcmjs.data.DicomMetaDictionary.denaturalizeDataset(
|
|
141
|
+
dataset
|
|
142
|
+
);
|
|
143
|
+
return Buffer.from(dicomDict.write({ allowInvalidVRLength: true }));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/syntheticFixtures/uid.ts
|
|
147
|
+
import { randomBytes } from "node:crypto";
|
|
148
|
+
var ROLE_CODE = { study: 1, series: 2, sop: 3 };
|
|
149
|
+
function lcg(seed) {
|
|
150
|
+
let s = seed >>> 0;
|
|
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]}`;
|
|
158
|
+
}
|
|
159
|
+
function randomUid(role) {
|
|
160
|
+
const buf = randomBytes(8);
|
|
161
|
+
return formatUid(buf.readUInt32BE(0), buf.readUInt32BE(4), role);
|
|
162
|
+
}
|
|
163
|
+
function seededUid(next, role) {
|
|
164
|
+
return formatUid(next(), next(), role);
|
|
165
|
+
}
|
|
166
|
+
function seededStream(seed, offset, a, b) {
|
|
167
|
+
return lcg(seed * 999983 + offset + a * 1000003 + b * 7919 >>> 0);
|
|
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
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return (fileIndex) => {
|
|
178
|
+
const next = seededStream(seed, 0, fileIndex, 0);
|
|
179
|
+
return {
|
|
180
|
+
study: seededUid(next, "study"),
|
|
181
|
+
series: seededUid(next, "series"),
|
|
182
|
+
sop: seededUid(next, "sop")
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/syntheticFixtures/streamWrite.ts
|
|
188
|
+
var OW_MAX_BYTES = 4294967294;
|
|
189
|
+
var FRAGMENT_MAX_BYTES = 4294967294;
|
|
190
|
+
var DEFAULT_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
191
|
+
var RLE_TRANSFER_SYNTAX_UID = "1.2.840.10008.1.2.5";
|
|
192
|
+
var PIXEL_DATA_OW_HEADER = Buffer.from([224, 127, 16, 0, 79, 87]);
|
|
193
|
+
var PIXEL_DATA_OB_HEADER = Buffer.from([224, 127, 16, 0, 79, 66]);
|
|
194
|
+
var ITEM_TAG_GROUP = 65534;
|
|
195
|
+
var ITEM_TAG_ELEMENT = 57344;
|
|
196
|
+
var SEQUENCE_DELIMITER_ELEMENT = 57565;
|
|
197
|
+
function itemTag(element, byteLength = 0) {
|
|
198
|
+
const b = Buffer.alloc(8);
|
|
199
|
+
b.writeUInt16LE(ITEM_TAG_GROUP, 0);
|
|
200
|
+
b.writeUInt16LE(element, 2);
|
|
201
|
+
b.writeUInt32LE(byteLength, 4);
|
|
202
|
+
return b;
|
|
203
|
+
}
|
|
204
|
+
function nativeDimensions(pixelBytes) {
|
|
205
|
+
const cells = Math.max(1, Math.floor(pixelBytes / 2));
|
|
206
|
+
const rows = Math.min(65535, Math.max(1, Math.round(Math.sqrt(cells))));
|
|
207
|
+
const columns = Math.min(65535, Math.max(1, Math.floor(cells / rows)));
|
|
208
|
+
return { rows, columns, bytes: rows * columns * 2 };
|
|
209
|
+
}
|
|
210
|
+
function streamZeros(fd, totalBytes, zeroChunk) {
|
|
211
|
+
let remaining = totalBytes;
|
|
212
|
+
while (remaining > 0) {
|
|
213
|
+
const n = Math.min(zeroChunk.length, remaining);
|
|
214
|
+
writeSync(fd, zeroChunk, 0, n);
|
|
215
|
+
remaining -= n;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function writeNative(outPath, params, dims, zeroChunk) {
|
|
219
|
+
const { modality, uid, tags } = params;
|
|
220
|
+
const meta = buildMeta(uid, modality);
|
|
221
|
+
const dataset = buildBaseImageDataset(uid, modality);
|
|
222
|
+
applyTagOverrides(dataset, tags);
|
|
223
|
+
dataset.Rows = dims.rows;
|
|
224
|
+
dataset.Columns = dims.columns;
|
|
225
|
+
const minimalPixelBytes = dataset.PixelData.byteLength;
|
|
226
|
+
const probe = serializeDict(meta, dataset);
|
|
227
|
+
const pixelElement = Buffer.alloc(12);
|
|
228
|
+
PIXEL_DATA_OW_HEADER.copy(pixelElement);
|
|
229
|
+
pixelElement.writeUInt32LE(minimalPixelBytes, 8);
|
|
230
|
+
const owIdx = probe.indexOf(pixelElement);
|
|
231
|
+
if (owIdx < 0) {
|
|
232
|
+
throw new Error("failed to locate native PixelData element header");
|
|
233
|
+
}
|
|
234
|
+
const dataStart = owIdx + 12;
|
|
235
|
+
const header = Buffer.from(probe.subarray(0, dataStart));
|
|
236
|
+
header.writeUInt32LE(dims.bytes, owIdx + 8);
|
|
237
|
+
const trailing = probe.subarray(dataStart + minimalPixelBytes);
|
|
238
|
+
const fd = openSync(outPath, "w");
|
|
239
|
+
try {
|
|
240
|
+
writeSync(fd, header);
|
|
241
|
+
streamZeros(fd, dims.bytes, zeroChunk);
|
|
242
|
+
if (trailing.length > 0) writeSync(fd, trailing);
|
|
243
|
+
} finally {
|
|
244
|
+
closeSync(fd);
|
|
245
|
+
}
|
|
246
|
+
return header.length + dims.bytes + trailing.length;
|
|
247
|
+
}
|
|
248
|
+
function writeEncapsulated(outPath, params, pixelBytes, fragmentBytes, zeroChunk) {
|
|
249
|
+
const { modality, uid, tags } = params;
|
|
250
|
+
const meta = {
|
|
251
|
+
...buildMeta(uid, modality),
|
|
252
|
+
TransferSyntaxUID: RLE_TRANSFER_SYNTAX_UID
|
|
253
|
+
};
|
|
254
|
+
const dataset = buildBaseImageDataset(uid, modality);
|
|
255
|
+
applyTagOverrides(dataset, tags);
|
|
256
|
+
dataset.PixelData = [new Uint8Array([]).buffer, new Uint8Array([0, 0]).buffer];
|
|
257
|
+
dataset._vrMap = { PixelData: "OB" };
|
|
258
|
+
const probe = serializeDict(meta, dataset);
|
|
259
|
+
const idx = probe.indexOf(PIXEL_DATA_OB_HEADER);
|
|
260
|
+
if (idx < 0 || probe.readUInt32LE(idx + 8) !== 4294967295) {
|
|
261
|
+
throw new Error("failed to locate encapsulated PixelData element header");
|
|
262
|
+
}
|
|
263
|
+
const prefix = Buffer.from(probe.subarray(0, idx + 12));
|
|
264
|
+
const fd = openSync(outPath, "w");
|
|
265
|
+
let bytesWritten = 0;
|
|
266
|
+
try {
|
|
267
|
+
writeSync(fd, prefix);
|
|
268
|
+
bytesWritten += prefix.length;
|
|
269
|
+
const bot = itemTag(ITEM_TAG_ELEMENT, 0);
|
|
270
|
+
writeSync(fd, bot);
|
|
271
|
+
bytesWritten += bot.length;
|
|
272
|
+
let remaining = pixelBytes;
|
|
273
|
+
while (remaining > 0) {
|
|
274
|
+
const fragLen = Math.min(fragmentBytes, remaining);
|
|
275
|
+
const fh = itemTag(ITEM_TAG_ELEMENT, fragLen);
|
|
276
|
+
writeSync(fd, fh);
|
|
277
|
+
streamZeros(fd, fragLen, zeroChunk);
|
|
278
|
+
bytesWritten += fh.length + fragLen;
|
|
279
|
+
remaining -= fragLen;
|
|
280
|
+
}
|
|
281
|
+
const delimiter = itemTag(SEQUENCE_DELIMITER_ELEMENT);
|
|
282
|
+
writeSync(fd, delimiter);
|
|
283
|
+
bytesWritten += delimiter.length;
|
|
284
|
+
} finally {
|
|
285
|
+
closeSync(fd);
|
|
286
|
+
}
|
|
287
|
+
return bytesWritten;
|
|
288
|
+
}
|
|
289
|
+
function streamLargeImage(outPath, options) {
|
|
290
|
+
const {
|
|
291
|
+
targetBytes,
|
|
292
|
+
chunkBytes = DEFAULT_CHUNK_BYTES,
|
|
293
|
+
owMaxBytes = OW_MAX_BYTES,
|
|
294
|
+
fragmentBytes = FRAGMENT_MAX_BYTES
|
|
295
|
+
} = options;
|
|
296
|
+
const modality = options.modality ?? "CT";
|
|
297
|
+
const uid = options.uid ?? makeUidGenerator(options.seed)(0);
|
|
298
|
+
const identity = { modality, uid, tags: options.tags };
|
|
299
|
+
mkdirSync2(resolve2(outPath, ".."), { recursive: true });
|
|
300
|
+
const zeroChunk = Buffer.alloc(Math.max(1, chunkBytes));
|
|
301
|
+
const probe = serializeDict(
|
|
302
|
+
buildMeta(uid, modality),
|
|
303
|
+
buildBaseImageDataset(uid, modality)
|
|
304
|
+
);
|
|
305
|
+
const nativeHeaderLen = probe.length - 2;
|
|
306
|
+
const requestedPixel = Math.max(2, targetBytes - nativeHeaderLen);
|
|
307
|
+
if (requestedPixel <= owMaxBytes) {
|
|
308
|
+
const dims = nativeDimensions(requestedPixel);
|
|
309
|
+
const bytesWritten2 = writeNative(outPath, identity, dims, zeroChunk);
|
|
310
|
+
return {
|
|
311
|
+
path: outPath,
|
|
312
|
+
bytesWritten: bytesWritten2,
|
|
313
|
+
encoding: "native",
|
|
314
|
+
pixelBytes: dims.bytes
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const estimateFrags = Math.max(
|
|
318
|
+
1,
|
|
319
|
+
Math.ceil((targetBytes - nativeHeaderLen - 16) / fragmentBytes)
|
|
320
|
+
);
|
|
321
|
+
const overhead = nativeHeaderLen + 8 + estimateFrags * 8 + 8;
|
|
322
|
+
let pixelBytes = Math.max(2, targetBytes - overhead);
|
|
323
|
+
pixelBytes -= pixelBytes % 2;
|
|
324
|
+
const bytesWritten = writeEncapsulated(
|
|
325
|
+
outPath,
|
|
326
|
+
identity,
|
|
327
|
+
pixelBytes,
|
|
328
|
+
fragmentBytes - fragmentBytes % 2,
|
|
329
|
+
zeroChunk
|
|
330
|
+
);
|
|
331
|
+
return { path: outPath, bytesWritten, encoding: "encapsulated", pixelBytes };
|
|
332
|
+
}
|
|
333
|
+
function writeLargeImageFile(spec, outPath, options = {}) {
|
|
334
|
+
const { targetBytes } = spec;
|
|
335
|
+
if (!Number.isInteger(targetBytes)) {
|
|
336
|
+
throw new Error("writeLargeImageFile: targetBytes must be an integer");
|
|
337
|
+
}
|
|
338
|
+
if (targetBytes <= MAX_PIXEL_BYTES) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
"writeLargeImageFile is for files above the 512 MB in-memory limit \u2014 use targetSizeKb for smaller files"
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
if (targetBytes > MAX_STREAM_BYTES) {
|
|
344
|
+
throw new Error("writeLargeImageFile: targetBytes exceeds the 50 GB limit");
|
|
345
|
+
}
|
|
346
|
+
return streamLargeImage(outPath, {
|
|
347
|
+
targetBytes,
|
|
348
|
+
modality: spec.modality,
|
|
349
|
+
seed: spec.seed,
|
|
350
|
+
chunkBytes: options.chunkBytes
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
export {
|
|
354
|
+
streamLargeImage,
|
|
355
|
+
writeLargeImageFile
|
|
356
|
+
};
|
|
@@ -18,5 +18,13 @@ export declare function generateFile(spec: FileSpec, options?: {
|
|
|
18
18
|
padWidth?: number;
|
|
19
19
|
uid?: Partial<UidSet>;
|
|
20
20
|
}): Promise<GeneratedFile>;
|
|
21
|
+
export declare function withResolvedSeed(spec: DatasetSpec): DatasetSpec;
|
|
21
22
|
export declare function generateCollectionFromSpec(spec: DatasetSpec): AsyncGenerator<GeneratedFile>;
|
|
23
|
+
export type CollectionPreviewItem = {
|
|
24
|
+
relativePath: string;
|
|
25
|
+
type: FileSpec['type'];
|
|
26
|
+
index: number;
|
|
27
|
+
approxBytes: number;
|
|
28
|
+
};
|
|
29
|
+
export declare function previewCollection(spec: DatasetSpec): AsyncGenerator<CollectionPreviewItem>;
|
|
22
30
|
export declare function writeCollectionFromSpec(spec: DatasetSpec, outDir: string): Promise<CollectionManifest>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
export { type CollectionManifest, type GeneratedFile, generateCollectionFromSpec, generateFile, writeCollectionFromSpec, } from './collection/writer.js';
|
|
1
|
+
export { type CollectionManifest, type CollectionPreviewItem, type GeneratedFile, generateCollectionFromSpec, generateFile, previewCollection, 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
|
+
export { MAX_PIXEL_BYTES, MAX_STREAM_BYTES } from './schema/limits.js';
|
|
5
6
|
export { resolveParametricSpec } from './schema/parametric.js';
|
|
6
|
-
export type { DatasetLayout, DatasetSpec, DicomdirSpec, DicomTagOverrides, FakeSignatureSpec, FileSpec, ImageSpec, InvalidUidImageSpec, 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, SeriesSpec, StudySpec, TransferSyntax, ValidImageSpec, VendorWarningsImageSpec, ViolationClass, } from './schema/types.js';
|
|
7
8
|
export { validateDatasetSpec, validateParametricSpec, } from './schema/validate.js';
|
|
9
|
+
export { type LargeFileEncoding, type LargeFileResult, type LargeImageSpec, type StreamLargeImageOptions, streamLargeImage, writeLargeImageFile, } from './syntheticFixtures/streamWrite.js';
|
|
@@ -37,12 +37,18 @@ export type NonDicomSpec = {
|
|
|
37
37
|
export type DicomdirSpec = {
|
|
38
38
|
type: 'dicomdir';
|
|
39
39
|
};
|
|
40
|
+
export type LargeFileSpec = {
|
|
41
|
+
type: 'large-image';
|
|
42
|
+
targetBytes: number;
|
|
43
|
+
modality?: Modality;
|
|
44
|
+
tags?: DicomTagOverrides;
|
|
45
|
+
};
|
|
40
46
|
export type ImageSpec = ValidImageSpec | InvalidUidImageSpec | VendorWarningsImageSpec;
|
|
41
|
-
export type FileSpec = ImageSpec | FakeSignatureSpec | NonDicomSpec | DicomdirSpec;
|
|
47
|
+
export type FileSpec = ImageSpec | FakeSignatureSpec | NonDicomSpec | DicomdirSpec | LargeFileSpec;
|
|
42
48
|
export type EntrySpec = FileSpec & {
|
|
43
49
|
count?: number;
|
|
44
50
|
};
|
|
45
|
-
export type SeriesEntrySpec = ImageSpec & {
|
|
51
|
+
export type SeriesEntrySpec = (ImageSpec | LargeFileSpec) & {
|
|
46
52
|
count?: number;
|
|
47
53
|
};
|
|
48
54
|
export type SeriesSpec = {
|
|
@@ -74,6 +80,8 @@ export type ParametricStudies = {
|
|
|
74
80
|
fileSizeKb?: Range;
|
|
75
81
|
modalities?: Modality[];
|
|
76
82
|
tags?: DicomTagOverrides;
|
|
83
|
+
largeFilesPerSeries?: Range;
|
|
84
|
+
largeFileBytes?: Range;
|
|
77
85
|
};
|
|
78
86
|
export type ParametricEdgeCase = EntrySpec & {
|
|
79
87
|
frequency?: number;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
import type { FileSpec } from '../schema/types.js';
|
|
1
|
+
import type { DicomTagOverrides, FileSpec, Modality, TransferSyntax } from '../schema/types.js';
|
|
2
2
|
import type { UidSet } from './uid.js';
|
|
3
|
+
export declare function applyTagOverrides(dataset: Record<string, unknown>, tags: DicomTagOverrides | undefined): void;
|
|
4
|
+
export declare function buildMeta(uid: UidSet, modality: Modality, transferSyntax?: TransferSyntax): Record<string, unknown>;
|
|
5
|
+
export declare function buildBaseImageDataset(uid: UidSet, modality: Modality): Record<string, unknown>;
|
|
6
|
+
export declare function serializeDict(meta: Record<string, unknown>, dataset: Record<string, unknown>): Buffer;
|
|
3
7
|
export declare function buildBufferForSpec(spec: FileSpec, uid: UidSet): Buffer;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { DicomTagOverrides, Modality } from '../schema/types.js';
|
|
2
|
+
import { type UidSet } from './uid.js';
|
|
3
|
+
export type LargeImageSpec = {
|
|
4
|
+
/** Approximate total file size in bytes (must exceed the 512 MB cap). */
|
|
5
|
+
targetBytes: number;
|
|
6
|
+
modality?: Modality;
|
|
7
|
+
/** Drawn when omitted; fixes the instance UIDs for reproducibility. */
|
|
8
|
+
seed?: number;
|
|
9
|
+
};
|
|
10
|
+
export type LargeFileEncoding = 'native' | 'encapsulated';
|
|
11
|
+
export type LargeFileResult = {
|
|
12
|
+
path: string;
|
|
13
|
+
bytesWritten: number;
|
|
14
|
+
encoding: LargeFileEncoding;
|
|
15
|
+
pixelBytes: number;
|
|
16
|
+
};
|
|
17
|
+
export type StreamLargeImageOptions = {
|
|
18
|
+
/** Approximate total file size in bytes (no floor — small values are valid). */
|
|
19
|
+
targetBytes: number;
|
|
20
|
+
modality?: Modality;
|
|
21
|
+
/** Drawn when omitted; fixes the instance UIDs for reproducibility. */
|
|
22
|
+
seed?: number;
|
|
23
|
+
/** Explicit UID set — used as-is (e.g. collection threading shared study/series UIDs). */
|
|
24
|
+
uid?: UidSet;
|
|
25
|
+
/** Tag overrides applied to the dataset (e.g. InstanceNumber, study/series tags). */
|
|
26
|
+
tags?: DicomTagOverrides;
|
|
27
|
+
chunkBytes?: number;
|
|
28
|
+
owMaxBytes?: number;
|
|
29
|
+
fragmentBytes?: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Low-level streaming writer: chooses the encoding from the requested size and
|
|
33
|
+
* the (overridable) ceilings, and applies **no policy bounds** — `targetBytes`
|
|
34
|
+
* may be tiny. This is the entry point for exercising the streamed path inline
|
|
35
|
+
* in tests/CI: override `owMaxBytes`/`fragmentBytes` to force either encoding at
|
|
36
|
+
* KB scale. Normal callers want {@link writeLargeImageFile}, which adds the
|
|
37
|
+
* 512 MB–50 GB policy bounds.
|
|
38
|
+
*/
|
|
39
|
+
export declare function streamLargeImage(outPath: string, options: StreamLargeImageOptions): LargeFileResult;
|
|
40
|
+
/**
|
|
41
|
+
* Stream a single DICOM image of approximately `targetBytes` to disk without
|
|
42
|
+
* holding it in memory. Disk-only: the in-memory `generateFile` path keeps the
|
|
43
|
+
* 512 MB cap. Files up to ~4 GB use a conformant native (OW) encoding; larger
|
|
44
|
+
* files (to 50 GB) use encapsulated fragments — a valid container with
|
|
45
|
+
* non-conformant zero pixel content.
|
|
46
|
+
*/
|
|
47
|
+
export declare function writeLargeImageFile(spec: LargeImageSpec, outPath: string, options?: {
|
|
48
|
+
chunkBytes?: number;
|
|
49
|
+
}): LargeFileResult;
|