dicom-synth 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -16
- package/bin/dicom-synth-generate.mjs +100 -40
- package/dist/esm/collection/writer.js +37 -54
- package/dist/esm/index.js +248 -85
- package/dist/esm/schema/parametric.js +327 -0
- package/dist/esm/schema/validate.js +136 -32
- package/dist/esm/syntheticFixtures/generator.js +37 -54
- package/dist/esm/syntheticFixtures/uid.js +4 -1
- package/dist/types/index.d.ts +3 -2
- package/dist/types/schema/parametric.d.ts +2 -0
- package/dist/types/schema/types.d.ts +26 -7
- package/dist/types/schema/validate.d.ts +2 -1
- package/dist/types/syntheticFixtures/uid.d.ts +2 -0
- 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" },
|
|
@@ -367,6 +381,42 @@ The validator throws with a human-readable message on any structural error.
|
|
|
367
381
|
|
|
368
382
|
---
|
|
369
383
|
|
|
384
|
+
## Parametric designer
|
|
385
|
+
|
|
386
|
+
A `ParametricSpec` describes a dataset by ranges instead of explicit entries. `resolveParametricSpec` resolves it — deterministically for a given seed — into a concrete `DatasetSpec` that can be inspected, saved, and re-run.
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
import { resolveParametricSpec, writeCollectionFromSpec } from 'dicom-synth'
|
|
390
|
+
|
|
391
|
+
const resolved = resolveParametricSpec({
|
|
392
|
+
seed: 42,
|
|
393
|
+
studies: {
|
|
394
|
+
count: { min: 2, max: 4 }, // ranges are inclusive
|
|
395
|
+
seriesPerStudy: { min: 1, max: 3 },
|
|
396
|
+
filesPerSeries: { min: 5, max: 20 },
|
|
397
|
+
fileSizeKb: { min: 100, max: 600 }, // sampled per file
|
|
398
|
+
modalities: ['CT', 'PT'], // one drawn per series
|
|
399
|
+
},
|
|
400
|
+
edgeCases: [{ type: 'invalid-uid-image', frequency: 0.05 }],
|
|
401
|
+
layout: 'hierarchical',
|
|
402
|
+
})
|
|
403
|
+
await writeCollectionFromSpec(resolved, './out')
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
- Every field of `studies` accepts a fixed number or an inclusive `{ min, max }` range (`Range`).
|
|
407
|
+
- `fileSizeKb` maps to `targetSizeKb`; a fixed value collapses each series into one counted entry, a range samples per file.
|
|
408
|
+
- `edgeCases` are `EntrySpec`s appended to the flat `entries` of the resolved spec. With `frequency` (0–1), the count is drawn per file across the whole dataset — the resolved spec records the actual draw. Without it, the entry is appended as-is (use `count` for absolute numbers). At small dataset sizes a low frequency may draw zero occurrences.
|
|
409
|
+
- The seed (drawn randomly when omitted) is embedded in the resolved spec, so resolution is always reproducible.
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
import type { ParametricSpec, ParametricStudies, ParametricEdgeCase, Range } from 'dicom-synth'
|
|
413
|
+
import { validateParametricSpec } from 'dicom-synth'
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
See `examples/parametric.json` for a JSON example and `examples/convert-data-shape.mjs` for converting an exported tree-shape JSON into a `DatasetSpec` (skips non-DICOM files).
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
370
420
|
## CLI
|
|
371
421
|
|
|
372
422
|
```bash
|
|
@@ -378,8 +428,21 @@ dicom-synth-generate --schema-inline '{"entries":[{"type":"valid-image","count":
|
|
|
378
428
|
|
|
379
429
|
# Default output directory is ./fixtures/generated
|
|
380
430
|
dicom-synth-generate --schema dataset.json
|
|
431
|
+
|
|
432
|
+
# Resolve and generate from a parametric spec; save the resolved DatasetSpec
|
|
433
|
+
dicom-synth-generate --parametric examples/parametric.json --out ./out --emit-spec resolved.json
|
|
434
|
+
|
|
435
|
+
# Preview the manifest (paths, types, sizes) without writing anything
|
|
436
|
+
dicom-synth-generate --parametric examples/parametric.json --dry-run
|
|
381
437
|
```
|
|
382
438
|
|
|
439
|
+
| Flag | Effect |
|
|
440
|
+
|---|---|
|
|
441
|
+
| `--schema <path>` / `--schema-inline <json>` / `--parametric <path>` | Input spec (mutually exclusive) |
|
|
442
|
+
| `--out <dir>` | Output directory (default `./fixtures/generated`) |
|
|
443
|
+
| `--emit-spec <path>` | Write the resolved `DatasetSpec` JSON (requires `--parametric`) |
|
|
444
|
+
| `--dry-run` | Generate in memory and print the manifest; nothing is written |
|
|
445
|
+
|
|
383
446
|
The default schema (`examples/default.json`) generates one each of `valid-image`, `invalid-uid-image`, and `vendor-warnings-image`:
|
|
384
447
|
|
|
385
448
|
```bash
|
|
@@ -457,7 +520,8 @@ pnpm fetch:public-case -- pydicom-CT-small
|
|
|
457
520
|
| Path | Responsibility |
|
|
458
521
|
|---|---|
|
|
459
522
|
| `src/schema/types.ts` | All public TypeScript types (`FileSpec`, `DatasetSpec`, `ViolationClass`, etc.) |
|
|
460
|
-
| `src/schema/validate.ts` | `validateDatasetSpec` —
|
|
523
|
+
| `src/schema/validate.ts` | `validateDatasetSpec` / `validateParametricSpec` — validate raw JSON values against the schema |
|
|
524
|
+
| `src/schema/parametric.ts` | `resolveParametricSpec` — seeded `ParametricSpec` → `DatasetSpec` resolution |
|
|
461
525
|
| `src/syntheticFixtures/generator.ts` | Internal buffer builders keyed on `FileSpec` type |
|
|
462
526
|
| `src/syntheticFixtures/uid.ts` | Seeded and random UID generation |
|
|
463
527
|
| `src/syntheticFixtures/violations.ts` | Post-processing violation injection |
|
|
@@ -467,6 +531,8 @@ pnpm fetch:public-case -- pydicom-CT-small
|
|
|
467
531
|
| `bin/dicom-synth-generate.mjs` | Published generate CLI (requires `pnpm build`) |
|
|
468
532
|
| `bin/dicom-synth-fetch.mjs` | Published fetch CLI (requires `pnpm build`) |
|
|
469
533
|
| `examples/default.json` | Default `DatasetSpec` — one each of `valid-image`, `invalid-uid-image`, `vendor-warnings-image` |
|
|
534
|
+
| `examples/parametric.json` | Example `ParametricSpec` for the `--parametric` CLI path |
|
|
535
|
+
| `examples/convert-data-shape.mjs` | Example converter: tree-shape JSON → `DatasetSpec` |
|
|
470
536
|
|
|
471
537
|
---
|
|
472
538
|
|
|
@@ -492,9 +558,8 @@ Git hooks: see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
|
492
558
|
|
|
493
559
|
## Future development
|
|
494
560
|
|
|
495
|
-
- **
|
|
496
|
-
- **
|
|
497
|
-
- **Dry-run mode** — emit the manifest (paths, types, grouping) without writing files
|
|
561
|
+
- **Describe tool** — scan an existing DICOM tree and emit a matching `DatasetSpec` (counts, grouping, sizes, modalities; no PHI)
|
|
562
|
+
- **Dimension edge-case recipes** — documented presets for geometry pathologies (1×65535 strips, high frame counts)
|
|
498
563
|
- **Compressed transfer syntaxes** — JPEG-LS, JPEG 2000, RLE
|
|
499
564
|
- **Private fixture catalogues** — same SHA-256 fetch/cache pattern for credentials-backed sources (e.g. S3)
|
|
500
565
|
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import { resolve } from 'node:path'
|
|
4
4
|
import {
|
|
5
|
+
generateCollectionFromSpec,
|
|
6
|
+
resolveParametricSpec,
|
|
5
7
|
validateDatasetSpec,
|
|
6
8
|
writeCollectionFromSpec,
|
|
7
9
|
} from '../dist/esm/index.js'
|
|
8
10
|
|
|
9
11
|
function usage() {
|
|
10
12
|
console.error(
|
|
11
|
-
'Usage: dicom-synth-generate --schema <path> [--out <dir>]\n' +
|
|
12
|
-
' dicom-synth-generate --schema-inline <json> [--out <dir>]'
|
|
13
|
+
'Usage: dicom-synth-generate --schema <path> [--out <dir>] [--dry-run]\n' +
|
|
14
|
+
' dicom-synth-generate --schema-inline <json> [--out <dir>] [--dry-run]\n' +
|
|
15
|
+
' dicom-synth-generate --parametric <path> [--out <dir>] [--emit-spec <path>] [--dry-run]',
|
|
13
16
|
)
|
|
14
17
|
process.exit(1)
|
|
15
18
|
}
|
|
@@ -19,36 +22,50 @@ const args = process.argv.slice(2)
|
|
|
19
22
|
if (args.length === 0) usage()
|
|
20
23
|
|
|
21
24
|
let rawSpec
|
|
25
|
+
let specSource
|
|
22
26
|
let outDir = resolve('./fixtures/generated')
|
|
27
|
+
let emitSpecPath
|
|
28
|
+
let dryRun = false
|
|
29
|
+
|
|
30
|
+
function readSpecArg(flag, value) {
|
|
31
|
+
if (rawSpec !== undefined) {
|
|
32
|
+
console.error(
|
|
33
|
+
'Error: --schema, --schema-inline and --parametric are mutually exclusive',
|
|
34
|
+
)
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
specSource = flag
|
|
38
|
+
if (flag === '--schema-inline') {
|
|
39
|
+
try {
|
|
40
|
+
rawSpec = JSON.parse(value)
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(`Error parsing inline schema: ${err.message}`)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
try {
|
|
47
|
+
rawSpec = JSON.parse(readFileSync(resolve(value), 'utf8'))
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error(`Error reading ${flag.slice(2)} file: ${err.message}`)
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
23
54
|
|
|
24
55
|
for (let i = 0; i < args.length; i++) {
|
|
25
56
|
if (
|
|
26
|
-
(args[i] === '--schema' ||
|
|
57
|
+
(args[i] === '--schema' ||
|
|
58
|
+
args[i] === '--schema-inline' ||
|
|
59
|
+
args[i] === '--parametric') &&
|
|
27
60
|
args[i + 1]
|
|
28
61
|
) {
|
|
29
|
-
|
|
30
|
-
console.error(
|
|
31
|
-
'Error: --schema and --schema-inline are mutually exclusive',
|
|
32
|
-
)
|
|
33
|
-
process.exit(1)
|
|
34
|
-
}
|
|
35
|
-
if (args[i] === '--schema') {
|
|
36
|
-
try {
|
|
37
|
-
rawSpec = JSON.parse(readFileSync(resolve(args[++i]), 'utf8'))
|
|
38
|
-
} catch (err) {
|
|
39
|
-
console.error(`Error reading schema file: ${err.message}`)
|
|
40
|
-
process.exit(1)
|
|
41
|
-
}
|
|
42
|
-
} else {
|
|
43
|
-
try {
|
|
44
|
-
rawSpec = JSON.parse(args[++i])
|
|
45
|
-
} catch (err) {
|
|
46
|
-
console.error(`Error parsing inline schema: ${err.message}`)
|
|
47
|
-
process.exit(1)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
62
|
+
readSpecArg(args[i], args[++i])
|
|
50
63
|
} else if (args[i] === '--out' && args[i + 1]) {
|
|
51
64
|
outDir = resolve(args[++i])
|
|
65
|
+
} else if (args[i] === '--emit-spec' && args[i + 1]) {
|
|
66
|
+
emitSpecPath = resolve(args[++i])
|
|
67
|
+
} else if (args[i] === '--dry-run') {
|
|
68
|
+
dryRun = true
|
|
52
69
|
} else {
|
|
53
70
|
console.error(`Unknown argument: ${args[i]}`)
|
|
54
71
|
usage()
|
|
@@ -57,29 +74,72 @@ for (let i = 0; i < args.length; i++) {
|
|
|
57
74
|
|
|
58
75
|
if (rawSpec === undefined) usage()
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
77
|
+
function printTypeCounts(counts) {
|
|
78
|
+
for (const [type, count] of Object.entries(counts)) {
|
|
79
|
+
console.log(` ${type}: ${count}`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (emitSpecPath !== undefined && specSource !== '--parametric') {
|
|
84
|
+
console.error('Error: --emit-spec requires --parametric')
|
|
65
85
|
process.exit(1)
|
|
66
86
|
}
|
|
67
87
|
|
|
68
|
-
let
|
|
88
|
+
let spec
|
|
69
89
|
try {
|
|
70
|
-
|
|
90
|
+
spec =
|
|
91
|
+
specSource === '--parametric'
|
|
92
|
+
? resolveParametricSpec(rawSpec)
|
|
93
|
+
: validateDatasetSpec(rawSpec)
|
|
71
94
|
} catch (err) {
|
|
72
|
-
console.error(`
|
|
95
|
+
console.error(`Schema validation error: ${err.message}`)
|
|
73
96
|
process.exit(1)
|
|
74
97
|
}
|
|
75
98
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
if (emitSpecPath !== undefined) {
|
|
100
|
+
try {
|
|
101
|
+
writeFileSync(emitSpecPath, `${JSON.stringify(spec, null, 2)}\n`)
|
|
102
|
+
console.log(`Wrote resolved spec to ${emitSpecPath}`)
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error(`Error writing resolved spec: ${err.message}`)
|
|
105
|
+
process.exit(1)
|
|
106
|
+
}
|
|
80
107
|
}
|
|
81
108
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
109
|
+
if (dryRun) {
|
|
110
|
+
const counts = {}
|
|
111
|
+
let fileCount = 0
|
|
112
|
+
let byteCount = 0
|
|
113
|
+
try {
|
|
114
|
+
for await (const file of generateCollectionFromSpec(spec)) {
|
|
115
|
+
console.log(
|
|
116
|
+
`${file.relativePath} (${file.type}, ${(file.buffer.length / 1024).toFixed(1)} KB)`,
|
|
117
|
+
)
|
|
118
|
+
counts[file.type] = (counts[file.type] ?? 0) + 1
|
|
119
|
+
fileCount++
|
|
120
|
+
byteCount += file.buffer.length
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error(`Error generating collection: ${err.message}`)
|
|
124
|
+
process.exit(1)
|
|
125
|
+
}
|
|
126
|
+
console.log(
|
|
127
|
+
`Dry run: ${fileCount} file(s), ${(byteCount / 1024).toFixed(1)} KB total — nothing written`,
|
|
128
|
+
)
|
|
129
|
+
printTypeCounts(counts)
|
|
130
|
+
} else {
|
|
131
|
+
let manifest
|
|
132
|
+
try {
|
|
133
|
+
manifest = await writeCollectionFromSpec(spec, outDir)
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(`Error writing collection: ${err.message}`)
|
|
136
|
+
process.exit(1)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const counts = {}
|
|
140
|
+
for (const entry of manifest) {
|
|
141
|
+
counts[entry.type] = (counts[entry.type] ?? 0) + 1
|
|
142
|
+
}
|
|
143
|
+
console.log(`Wrote ${manifest.length} file(s) to ${outDir}`)
|
|
144
|
+
printTypeCounts(counts)
|
|
85
145
|
}
|
|
@@ -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":
|