dicom-synth 1.3.0 → 1.4.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 +26 -12
- package/dist/esm/collection/writer.js +37 -54
- package/dist/esm/index.js +56 -56
- package/dist/esm/schema/validate.js +19 -2
- package/dist/esm/syntheticFixtures/generator.js +37 -54
- package/dist/types/index.d.ts +1 -1
- package/dist/types/schema/types.d.ts +5 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -191,30 +191,43 @@ An `EntrySpec` is a `FileSpec` plus an optional `count` field (how many times to
|
|
|
191
191
|
| `valid-image` | Standards-valid image (CT by default — see `modality`); numeric UIDs, complete meta header | strict |
|
|
192
192
|
| `invalid-uid-image` | Image with non-numeric UIDs (letters in UID components) | edge |
|
|
193
193
|
| `vendor-warnings-image` | Image with empty `Laterality` and `PatientWeight = 0` — produces dciodvfy warnings | edge |
|
|
194
|
-
| `large-image` | Image with configurable pixel dimensions; `rows` and `columns` required | strict |
|
|
195
194
|
| `fake-signature` | 200-byte buffer with `XXXX` at preamble offset — no DICM magic | edge |
|
|
196
195
|
| `non-dicom` | Arbitrary text buffer; no extension in filename | edge |
|
|
197
196
|
| `dicomdir` | Minimal DICOMDIR file | strict |
|
|
198
197
|
|
|
199
|
-
**`
|
|
198
|
+
**`non-dicom` fields:**
|
|
200
199
|
|
|
201
200
|
```json
|
|
202
|
-
{ "type": "
|
|
201
|
+
{ "type": "non-dicom", "content": "not a dicom file" }
|
|
203
202
|
```
|
|
204
203
|
|
|
205
|
-
`
|
|
204
|
+
`content` defaults to `"not dicom"`.
|
|
206
205
|
|
|
207
|
-
|
|
206
|
+
### Sizing (`rows`/`columns`/`frames` or `targetSizeKb`)
|
|
207
|
+
|
|
208
|
+
Available on all image types. Two mutually exclusive mechanisms; omitting both produces a minimal 1×1 image.
|
|
209
|
+
|
|
210
|
+
**Explicit pixel dimensions** — for geometry-shaped cases (large frames, strips, multi-frame):
|
|
208
211
|
|
|
209
212
|
```json
|
|
210
|
-
{ "type": "
|
|
213
|
+
{ "type": "valid-image", "rows": 512, "columns": 512, "frames": 100 }
|
|
211
214
|
```
|
|
212
215
|
|
|
213
|
-
`
|
|
216
|
+
`rows` and `columns` must be provided together; `frames` defaults to 1. A 512×512×100 image produces a buffer of ~52 MB.
|
|
217
|
+
|
|
218
|
+
**Target file size** — for byte-shaped cases (upload limits, memory ceilings):
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{ "type": "valid-image", "targetSizeKb": 250000 }
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Pads `PixelData` so the file lands within ±2% or ±4 KB of the target (whichever is larger), accounting for tag and transfer-syntax overhead. `Rows`/`Columns` stay consistent with the pixel data, so the file remains conformant.
|
|
225
|
+
|
|
226
|
+
Both mechanisms are capped at 512 MB of pixel data to prevent accidental OOM. A target smaller than the file's fixed overhead (roughly 1 KB) produces the minimal possible file, slightly above target. When sizing fields are set they control `Rows`, `Columns`, and `PixelData` — tag overrides of those keywords do not apply.
|
|
214
227
|
|
|
215
228
|
### Modality presets (`modality`)
|
|
216
229
|
|
|
217
|
-
Available on
|
|
230
|
+
Available on all image types. Defaults to `CT`.
|
|
218
231
|
|
|
219
232
|
```json
|
|
220
233
|
{ "type": "valid-image", "modality": "PT" }
|
|
@@ -276,7 +289,7 @@ import type { StudySpec, SeriesSpec, SeriesEntrySpec } from 'dicom-synth'
|
|
|
276
289
|
|
|
277
290
|
### Tag overrides (`tags`)
|
|
278
291
|
|
|
279
|
-
Available on
|
|
292
|
+
Available on all image types.
|
|
280
293
|
|
|
281
294
|
```json
|
|
282
295
|
{
|
|
@@ -297,7 +310,7 @@ import type { DicomTagOverrides } from 'dicom-synth'
|
|
|
297
310
|
|
|
298
311
|
### Transfer syntax (`transferSyntax`)
|
|
299
312
|
|
|
300
|
-
Available on
|
|
313
|
+
Available on all image types.
|
|
301
314
|
|
|
302
315
|
| Value | `TransferSyntaxUID` in meta header |
|
|
303
316
|
|---|---|
|
|
@@ -307,7 +320,7 @@ Available on `valid-image`, `invalid-uid-image`, `vendor-warnings-image`, and `l
|
|
|
307
320
|
|
|
308
321
|
### Violation injection (`violations`)
|
|
309
322
|
|
|
310
|
-
Available on
|
|
323
|
+
Available on all image types. Violations are applied as post-processing transforms to an otherwise valid buffer.
|
|
311
324
|
|
|
312
325
|
| Violation | Effect | Detectable by dciodvfy |
|
|
313
326
|
|---|---|---|
|
|
@@ -346,7 +359,8 @@ The validator throws with a human-readable message on any structural error.
|
|
|
346
359
|
{ "type": "valid-image", "count": 10, "tags": { "PatientID": "P001" }, "transferSyntax": "explicit-vr-little-endian" },
|
|
347
360
|
{ "type": "invalid-uid-image", "count": 3 },
|
|
348
361
|
{ "type": "vendor-warnings-image", "count": 2 },
|
|
349
|
-
{ "type": "
|
|
362
|
+
{ "type": "valid-image", "count": 1, "rows": 512, "columns": 512, "frames": 100 },
|
|
363
|
+
{ "type": "valid-image", "count": 1, "targetSizeKb": 250000 },
|
|
350
364
|
{ "type": "valid-image", "count": 1, "violations": ["uid-too-long", "missing-meta-header"] },
|
|
351
365
|
{ "type": "fake-signature", "count": 5 },
|
|
352
366
|
{ "type": "non-dicom", "count": 2, "content": "garbage" },
|
|
@@ -158,36 +158,46 @@ function serializeDict(meta, dataset) {
|
|
|
158
158
|
);
|
|
159
159
|
return Buffer.from(dicomDict.write({ allowInvalidVRLength: true }));
|
|
160
160
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
var MAX_DIMENSION = 65535;
|
|
162
|
+
function applySizing(dataset, meta, spec) {
|
|
163
|
+
if (spec.targetSizeKb !== void 0) {
|
|
164
|
+
const clampDim = (n) => Math.min(MAX_DIMENSION, Math.max(1, Math.round(n)));
|
|
165
|
+
const minimalPixelBytes = dataset.PixelData.byteLength;
|
|
166
|
+
const overhead = serializeDict(meta, dataset).length - minimalPixelBytes;
|
|
167
|
+
const cells = Math.max(
|
|
168
|
+
1,
|
|
169
|
+
Math.round((spec.targetSizeKb * 1024 - overhead) / 2)
|
|
170
|
+
);
|
|
171
|
+
const rows = clampDim(Math.sqrt(cells));
|
|
172
|
+
const columns = clampDim(cells / rows);
|
|
173
|
+
dataset.Rows = rows;
|
|
174
|
+
dataset.Columns = columns;
|
|
175
|
+
dataset.PixelData = new ArrayBuffer(rows * columns * 2);
|
|
176
|
+
} else if (spec.rows !== void 0 && spec.columns !== void 0) {
|
|
177
|
+
dataset.Rows = spec.rows;
|
|
178
|
+
dataset.Columns = spec.columns;
|
|
179
|
+
if (spec.frames !== void 0) dataset.NumberOfFrames = spec.frames;
|
|
180
|
+
dataset.PixelData = new ArrayBuffer(
|
|
181
|
+
spec.rows * spec.columns * (spec.frames ?? 1) * 2
|
|
182
|
+
);
|
|
183
|
+
}
|
|
165
184
|
}
|
|
166
|
-
function
|
|
167
|
-
const
|
|
185
|
+
function buildImageBuffer(spec, uid) {
|
|
186
|
+
const modality = spec.modality ?? "CT";
|
|
187
|
+
const effectiveUid = spec.type === "invalid-uid-image" ? {
|
|
168
188
|
study: `2.25.invalid.study.${uid.study.slice(-6)}`,
|
|
169
189
|
series: `2.25.invalid.series.${uid.series.slice(-6)}`,
|
|
170
190
|
sop: `2.25.invalid.sop.${uid.sop.slice(-6)}`
|
|
171
|
-
};
|
|
172
|
-
const dataset = buildBaseImageDataset(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
dataset
|
|
180
|
-
|
|
181
|
-
return serializeDict(buildMeta(uid, modality, transferSyntax), dataset);
|
|
182
|
-
}
|
|
183
|
-
function buildLargeImageBuffer(uid, modality, rows, columns, frames, tags, transferSyntax) {
|
|
184
|
-
const dataset = buildBaseImageDataset(uid, modality);
|
|
185
|
-
dataset.Rows = rows;
|
|
186
|
-
dataset.Columns = columns;
|
|
187
|
-
dataset.NumberOfFrames = frames;
|
|
188
|
-
dataset.PixelData = new Uint8Array(rows * columns * frames * 2).buffer;
|
|
189
|
-
applyTagOverrides(dataset, tags);
|
|
190
|
-
return serializeDict(buildMeta(uid, modality, transferSyntax), dataset);
|
|
191
|
+
} : uid;
|
|
192
|
+
const dataset = buildBaseImageDataset(effectiveUid, modality);
|
|
193
|
+
if (spec.type === "vendor-warnings-image") {
|
|
194
|
+
dataset.Laterality = "";
|
|
195
|
+
dataset.PatientWeight = "0";
|
|
196
|
+
}
|
|
197
|
+
applyTagOverrides(dataset, spec.tags);
|
|
198
|
+
const meta = buildMeta(effectiveUid, modality, spec.transferSyntax);
|
|
199
|
+
applySizing(dataset, meta, spec);
|
|
200
|
+
return serializeDict(meta, dataset);
|
|
191
201
|
}
|
|
192
202
|
function buildFakeSignatureBuffer() {
|
|
193
203
|
const buf = Buffer.alloc(200, 0);
|
|
@@ -200,36 +210,9 @@ function buildNonDicomBuffer(content = "not dicom") {
|
|
|
200
210
|
function buildBufferForSpec(spec, uid) {
|
|
201
211
|
switch (spec.type) {
|
|
202
212
|
case "valid-image":
|
|
203
|
-
return buildValidImageBuffer(
|
|
204
|
-
uid,
|
|
205
|
-
spec.modality ?? "CT",
|
|
206
|
-
spec.tags,
|
|
207
|
-
spec.transferSyntax
|
|
208
|
-
);
|
|
209
213
|
case "invalid-uid-image":
|
|
210
|
-
return buildInvalidUidImageBuffer(
|
|
211
|
-
uid,
|
|
212
|
-
spec.modality ?? "CT",
|
|
213
|
-
spec.tags,
|
|
214
|
-
spec.transferSyntax
|
|
215
|
-
);
|
|
216
214
|
case "vendor-warnings-image":
|
|
217
|
-
return
|
|
218
|
-
uid,
|
|
219
|
-
spec.modality ?? "CT",
|
|
220
|
-
spec.tags,
|
|
221
|
-
spec.transferSyntax
|
|
222
|
-
);
|
|
223
|
-
case "large-image":
|
|
224
|
-
return buildLargeImageBuffer(
|
|
225
|
-
uid,
|
|
226
|
-
spec.modality ?? "CT",
|
|
227
|
-
spec.rows,
|
|
228
|
-
spec.columns,
|
|
229
|
-
spec.frames ?? 1,
|
|
230
|
-
spec.tags,
|
|
231
|
-
spec.transferSyntax
|
|
232
|
-
);
|
|
215
|
+
return buildImageBuffer(spec, uid);
|
|
233
216
|
case "fake-signature":
|
|
234
217
|
return buildFakeSignatureBuffer();
|
|
235
218
|
case "non-dicom":
|
package/dist/esm/index.js
CHANGED
|
@@ -49,7 +49,6 @@ var TYPE_CAPABILITIES = {
|
|
|
49
49
|
"valid-image": { image: true },
|
|
50
50
|
"invalid-uid-image": { image: true },
|
|
51
51
|
"vendor-warnings-image": { image: true },
|
|
52
|
-
"large-image": { image: true },
|
|
53
52
|
"fake-signature": { image: false },
|
|
54
53
|
"non-dicom": { image: false },
|
|
55
54
|
dicomdir: { image: false }
|
|
@@ -108,6 +107,10 @@ function validateEntry(entry, path) {
|
|
|
108
107
|
if ("count" in e) validatePositiveInt(e.count, `${path}.count`);
|
|
109
108
|
for (const field of [
|
|
110
109
|
"modality",
|
|
110
|
+
"rows",
|
|
111
|
+
"columns",
|
|
112
|
+
"frames",
|
|
113
|
+
"targetSizeKb",
|
|
111
114
|
"tags",
|
|
112
115
|
"violations",
|
|
113
116
|
"transferSyntax"
|
|
@@ -142,7 +145,21 @@ function validateEntry(entry, path) {
|
|
|
142
145
|
}
|
|
143
146
|
}
|
|
144
147
|
}
|
|
145
|
-
|
|
148
|
+
const hasDims = "rows" in e || "columns" in e || "frames" in e;
|
|
149
|
+
if ("targetSizeKb" in e) {
|
|
150
|
+
if (hasDims) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`${path}: targetSizeKb cannot be combined with rows/columns/frames \u2014 use one sizing mechanism`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
validatePositiveInt(e.targetSizeKb, `${path}.targetSizeKb`);
|
|
156
|
+
if (e.targetSizeKb * 1024 > MAX_PIXEL_BYTES) {
|
|
157
|
+
throw new Error(`${path}.targetSizeKb: exceeds the 512 MB limit`);
|
|
158
|
+
}
|
|
159
|
+
} else if (hasDims) {
|
|
160
|
+
if (!("rows" in e) || !("columns" in e)) {
|
|
161
|
+
throw new Error(`${path}: rows and columns must be provided together`);
|
|
162
|
+
}
|
|
146
163
|
validatePositiveInt(e.rows, `${path}.rows`);
|
|
147
164
|
validatePositiveInt(e.columns, `${path}.columns`);
|
|
148
165
|
if ("frames" in e) validatePositiveInt(e.frames, `${path}.frames`);
|
|
@@ -355,36 +372,46 @@ function serializeDict(meta, dataset) {
|
|
|
355
372
|
);
|
|
356
373
|
return Buffer.from(dicomDict.write({ allowInvalidVRLength: true }));
|
|
357
374
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
375
|
+
var MAX_DIMENSION = 65535;
|
|
376
|
+
function applySizing(dataset, meta, spec) {
|
|
377
|
+
if (spec.targetSizeKb !== void 0) {
|
|
378
|
+
const clampDim = (n) => Math.min(MAX_DIMENSION, Math.max(1, Math.round(n)));
|
|
379
|
+
const minimalPixelBytes = dataset.PixelData.byteLength;
|
|
380
|
+
const overhead = serializeDict(meta, dataset).length - minimalPixelBytes;
|
|
381
|
+
const cells = Math.max(
|
|
382
|
+
1,
|
|
383
|
+
Math.round((spec.targetSizeKb * 1024 - overhead) / 2)
|
|
384
|
+
);
|
|
385
|
+
const rows = clampDim(Math.sqrt(cells));
|
|
386
|
+
const columns = clampDim(cells / rows);
|
|
387
|
+
dataset.Rows = rows;
|
|
388
|
+
dataset.Columns = columns;
|
|
389
|
+
dataset.PixelData = new ArrayBuffer(rows * columns * 2);
|
|
390
|
+
} else if (spec.rows !== void 0 && spec.columns !== void 0) {
|
|
391
|
+
dataset.Rows = spec.rows;
|
|
392
|
+
dataset.Columns = spec.columns;
|
|
393
|
+
if (spec.frames !== void 0) dataset.NumberOfFrames = spec.frames;
|
|
394
|
+
dataset.PixelData = new ArrayBuffer(
|
|
395
|
+
spec.rows * spec.columns * (spec.frames ?? 1) * 2
|
|
396
|
+
);
|
|
397
|
+
}
|
|
362
398
|
}
|
|
363
|
-
function
|
|
364
|
-
const
|
|
399
|
+
function buildImageBuffer(spec, uid) {
|
|
400
|
+
const modality = spec.modality ?? "CT";
|
|
401
|
+
const effectiveUid = spec.type === "invalid-uid-image" ? {
|
|
365
402
|
study: `2.25.invalid.study.${uid.study.slice(-6)}`,
|
|
366
403
|
series: `2.25.invalid.series.${uid.series.slice(-6)}`,
|
|
367
404
|
sop: `2.25.invalid.sop.${uid.sop.slice(-6)}`
|
|
368
|
-
};
|
|
369
|
-
const dataset = buildBaseImageDataset(
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
dataset
|
|
377
|
-
|
|
378
|
-
return serializeDict(buildMeta(uid, modality, transferSyntax), dataset);
|
|
379
|
-
}
|
|
380
|
-
function buildLargeImageBuffer(uid, modality, rows, columns, frames, tags, transferSyntax) {
|
|
381
|
-
const dataset = buildBaseImageDataset(uid, modality);
|
|
382
|
-
dataset.Rows = rows;
|
|
383
|
-
dataset.Columns = columns;
|
|
384
|
-
dataset.NumberOfFrames = frames;
|
|
385
|
-
dataset.PixelData = new Uint8Array(rows * columns * frames * 2).buffer;
|
|
386
|
-
applyTagOverrides(dataset, tags);
|
|
387
|
-
return serializeDict(buildMeta(uid, modality, transferSyntax), dataset);
|
|
405
|
+
} : uid;
|
|
406
|
+
const dataset = buildBaseImageDataset(effectiveUid, modality);
|
|
407
|
+
if (spec.type === "vendor-warnings-image") {
|
|
408
|
+
dataset.Laterality = "";
|
|
409
|
+
dataset.PatientWeight = "0";
|
|
410
|
+
}
|
|
411
|
+
applyTagOverrides(dataset, spec.tags);
|
|
412
|
+
const meta = buildMeta(effectiveUid, modality, spec.transferSyntax);
|
|
413
|
+
applySizing(dataset, meta, spec);
|
|
414
|
+
return serializeDict(meta, dataset);
|
|
388
415
|
}
|
|
389
416
|
function buildFakeSignatureBuffer() {
|
|
390
417
|
const buf = Buffer.alloc(200, 0);
|
|
@@ -397,36 +424,9 @@ function buildNonDicomBuffer(content = "not dicom") {
|
|
|
397
424
|
function buildBufferForSpec(spec, uid) {
|
|
398
425
|
switch (spec.type) {
|
|
399
426
|
case "valid-image":
|
|
400
|
-
return buildValidImageBuffer(
|
|
401
|
-
uid,
|
|
402
|
-
spec.modality ?? "CT",
|
|
403
|
-
spec.tags,
|
|
404
|
-
spec.transferSyntax
|
|
405
|
-
);
|
|
406
427
|
case "invalid-uid-image":
|
|
407
|
-
return buildInvalidUidImageBuffer(
|
|
408
|
-
uid,
|
|
409
|
-
spec.modality ?? "CT",
|
|
410
|
-
spec.tags,
|
|
411
|
-
spec.transferSyntax
|
|
412
|
-
);
|
|
413
428
|
case "vendor-warnings-image":
|
|
414
|
-
return
|
|
415
|
-
uid,
|
|
416
|
-
spec.modality ?? "CT",
|
|
417
|
-
spec.tags,
|
|
418
|
-
spec.transferSyntax
|
|
419
|
-
);
|
|
420
|
-
case "large-image":
|
|
421
|
-
return buildLargeImageBuffer(
|
|
422
|
-
uid,
|
|
423
|
-
spec.modality ?? "CT",
|
|
424
|
-
spec.rows,
|
|
425
|
-
spec.columns,
|
|
426
|
-
spec.frames ?? 1,
|
|
427
|
-
spec.tags,
|
|
428
|
-
spec.transferSyntax
|
|
429
|
-
);
|
|
429
|
+
return buildImageBuffer(spec, uid);
|
|
430
430
|
case "fake-signature":
|
|
431
431
|
return buildFakeSignatureBuffer();
|
|
432
432
|
case "non-dicom":
|
|
@@ -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 }
|
|
@@ -62,6 +61,10 @@ function validateEntry(entry, path) {
|
|
|
62
61
|
if ("count" in e) validatePositiveInt(e.count, `${path}.count`);
|
|
63
62
|
for (const field of [
|
|
64
63
|
"modality",
|
|
64
|
+
"rows",
|
|
65
|
+
"columns",
|
|
66
|
+
"frames",
|
|
67
|
+
"targetSizeKb",
|
|
65
68
|
"tags",
|
|
66
69
|
"violations",
|
|
67
70
|
"transferSyntax"
|
|
@@ -96,7 +99,21 @@ function validateEntry(entry, path) {
|
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
|
-
|
|
102
|
+
const hasDims = "rows" in e || "columns" in e || "frames" in e;
|
|
103
|
+
if ("targetSizeKb" in e) {
|
|
104
|
+
if (hasDims) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`${path}: targetSizeKb cannot be combined with rows/columns/frames \u2014 use one sizing mechanism`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
validatePositiveInt(e.targetSizeKb, `${path}.targetSizeKb`);
|
|
110
|
+
if (e.targetSizeKb * 1024 > MAX_PIXEL_BYTES) {
|
|
111
|
+
throw new Error(`${path}.targetSizeKb: exceeds the 512 MB limit`);
|
|
112
|
+
}
|
|
113
|
+
} else if (hasDims) {
|
|
114
|
+
if (!("rows" in e) || !("columns" in e)) {
|
|
115
|
+
throw new Error(`${path}: rows and columns must be provided together`);
|
|
116
|
+
}
|
|
100
117
|
validatePositiveInt(e.rows, `${path}.rows`);
|
|
101
118
|
validatePositiveInt(e.columns, `${path}.columns`);
|
|
102
119
|
if ("frames" in e) validatePositiveInt(e.frames, `${path}.frames`);
|
|
@@ -153,36 +153,46 @@ function serializeDict(meta, dataset) {
|
|
|
153
153
|
);
|
|
154
154
|
return Buffer.from(dicomDict.write({ allowInvalidVRLength: true }));
|
|
155
155
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
162
|
-
const
|
|
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(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
dataset
|
|
175
|
-
|
|
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
|
|
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":
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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,
|
|
4
|
+
export type { DatasetLayout, DatasetSpec, DicomdirSpec, DicomTagOverrides, FakeSignatureSpec, FileSpec, ImageSpec, InvalidUidImageSpec, Modality, NonDicomSpec, SeriesEntrySpec, SeriesSpec, StudySpec, TransferSyntax, ValidImageSpec, VendorWarningsImageSpec, ViolationClass, } from './schema/types.js';
|
|
5
5
|
export { validateDatasetSpec } from './schema/validate.js';
|
|
@@ -10,6 +10,10 @@ export type ViolationClass = 'uid-too-long' | 'non-conformant-uid' | 'missing-me
|
|
|
10
10
|
export type Modality = 'CT' | 'PT' | 'MR' | 'CR';
|
|
11
11
|
type BaseImageSpec = {
|
|
12
12
|
modality?: Modality;
|
|
13
|
+
rows?: number;
|
|
14
|
+
columns?: number;
|
|
15
|
+
frames?: number;
|
|
16
|
+
targetSizeKb?: number;
|
|
13
17
|
tags?: DicomTagOverrides;
|
|
14
18
|
transferSyntax?: TransferSyntax;
|
|
15
19
|
violations?: ViolationClass[];
|
|
@@ -23,12 +27,6 @@ export type InvalidUidImageSpec = BaseImageSpec & {
|
|
|
23
27
|
export type VendorWarningsImageSpec = BaseImageSpec & {
|
|
24
28
|
type: 'vendor-warnings-image';
|
|
25
29
|
};
|
|
26
|
-
export type LargeImageSpec = BaseImageSpec & {
|
|
27
|
-
type: 'large-image';
|
|
28
|
-
rows: number;
|
|
29
|
-
columns: number;
|
|
30
|
-
frames?: number;
|
|
31
|
-
};
|
|
32
30
|
export type FakeSignatureSpec = {
|
|
33
31
|
type: 'fake-signature';
|
|
34
32
|
};
|
|
@@ -39,7 +37,7 @@ export type NonDicomSpec = {
|
|
|
39
37
|
export type DicomdirSpec = {
|
|
40
38
|
type: 'dicomdir';
|
|
41
39
|
};
|
|
42
|
-
export type ImageSpec = ValidImageSpec | InvalidUidImageSpec | VendorWarningsImageSpec
|
|
40
|
+
export type ImageSpec = ValidImageSpec | InvalidUidImageSpec | VendorWarningsImageSpec;
|
|
43
41
|
export type FileSpec = ImageSpec | FakeSignatureSpec | NonDicomSpec | DicomdirSpec;
|
|
44
42
|
export type EntrySpec = FileSpec & {
|
|
45
43
|
count?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dicom-synth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Toolkit for synthetic DICOM fixtures and public fixture fetch/cache.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -90,6 +90,6 @@
|
|
|
90
90
|
"semantic-release": "^25.0.3",
|
|
91
91
|
"tsx": "^4.19.3",
|
|
92
92
|
"typescript": "^5.8.3",
|
|
93
|
-
"vitest": "^3.2.
|
|
93
|
+
"vitest": "^3.2.6"
|
|
94
94
|
}
|
|
95
95
|
}
|