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
package/README.md
CHANGED
|
@@ -13,634 +13,50 @@ pnpm add dicom-synth
|
|
|
13
13
|
pnpm add dcmjs # peer dependency (^0.51.1)
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
## Quick start
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Generation is a three-layer API. Each layer is a thin wrapper over the one below.
|
|
21
|
-
|
|
22
|
-
### Layer 1 — single file, no I/O
|
|
23
|
-
|
|
24
|
-
```ts
|
|
25
|
-
import { generateFile } from 'dicom-synth'
|
|
26
|
-
|
|
27
|
-
const { buffer, filename, type, index } = await generateFile({ type: 'valid-image' })
|
|
28
|
-
// buffer: Buffer ready to write or pass to a parser
|
|
29
|
-
// filename: 'valid-image-000.dcm'
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
With options:
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
const { buffer } = await generateFile(
|
|
36
|
-
{ type: 'valid-image', tags: { PatientID: 'P001' }, violations: ['uid-too-long'] },
|
|
37
|
-
{ seed: 42, index: 7 },
|
|
38
|
-
)
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Write a single file to disk:
|
|
42
|
-
|
|
43
|
-
```ts
|
|
44
|
-
import { writeFileSync } from 'node:fs'
|
|
45
|
-
import { generateFile } from 'dicom-synth'
|
|
46
|
-
|
|
47
|
-
const { buffer, filename } = await generateFile({ type: 'valid-image' })
|
|
48
|
-
writeFileSync(`./out/${filename}`, buffer)
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Layer 2 — collection stream, no I/O
|
|
18
|
+
In-process — generate a deterministic collection and use it without touching disk:
|
|
52
19
|
|
|
53
20
|
```ts
|
|
54
21
|
import { generateCollectionFromSpec } from 'dicom-synth'
|
|
55
22
|
|
|
56
23
|
for await (const file of generateCollectionFromSpec({
|
|
57
|
-
entries: [
|
|
58
|
-
{ type: 'valid-image', count: 10 },
|
|
59
|
-
{ type: 'invalid-uid-image', count: 3 },
|
|
60
|
-
],
|
|
24
|
+
entries: [{ type: 'valid-image', count: 5 }],
|
|
61
25
|
seed: 42,
|
|
62
26
|
})) {
|
|
63
|
-
console.log(file.filename, file.buffer.length)
|
|
27
|
+
console.log(file.filename, file.buffer.length) // Buffer ready to parse or pipe
|
|
64
28
|
}
|
|
65
29
|
```
|
|
66
30
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
### Layer 3 — disk writer
|
|
70
|
-
|
|
71
|
-
```ts
|
|
72
|
-
import { writeCollectionFromSpec } from 'dicom-synth'
|
|
73
|
-
|
|
74
|
-
const manifest = await writeCollectionFromSpec(
|
|
75
|
-
{ entries: [{ type: 'valid-image', count: 5 }], seed: 42 },
|
|
76
|
-
'./fixtures/generated',
|
|
77
|
-
)
|
|
78
|
-
// manifest: Array<{ path: string; type: string; index: number }>
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## Usage patterns
|
|
84
|
-
|
|
85
|
-
### Ephemeral fixtures in tests (CI)
|
|
86
|
-
|
|
87
|
-
Generate into a temp directory per test run — nothing committed, works in any CI environment:
|
|
88
|
-
|
|
89
|
-
```ts
|
|
90
|
-
import { mkdtemp, rm } from 'node:fs/promises'
|
|
91
|
-
import { tmpdir } from 'node:os'
|
|
92
|
-
import { join } from 'node:path'
|
|
93
|
-
import { writeCollectionFromSpec } from 'dicom-synth'
|
|
94
|
-
|
|
95
|
-
const dir = await mkdtemp(join(tmpdir(), 'synth-'))
|
|
96
|
-
try {
|
|
97
|
-
await writeCollectionFromSpec(
|
|
98
|
-
{ entries: [{ type: 'valid-image', count: 3 }], seed: 42 },
|
|
99
|
-
dir,
|
|
100
|
-
)
|
|
101
|
-
// run pipeline against dir
|
|
102
|
-
} finally {
|
|
103
|
-
await rm(dir, { recursive: true, force: true })
|
|
104
|
-
}
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Use a fixed `seed` in CI so UID values are identical across runs and any diff is meaningful.
|
|
108
|
-
|
|
109
|
-
### Static generation (commit schema, not binaries)
|
|
110
|
-
|
|
111
|
-
Commit a `dataset.json` schema to your repo. Add a script to regenerate fixtures on demand:
|
|
112
|
-
|
|
113
|
-
```json
|
|
114
|
-
// package.json
|
|
115
|
-
{
|
|
116
|
-
"scripts": {
|
|
117
|
-
"fixtures:generate": "dicom-synth-generate --schema dataset.json --out fixtures/dicom"
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
```
|
|
31
|
+
Or from the command line — write fixtures to a directory:
|
|
121
32
|
|
|
122
33
|
```bash
|
|
123
|
-
pnpm fixtures:generate # regenerate
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
Commit `dataset.json` and `.gitignore` the output directory. Fixtures are regenerated locally or in a dedicated CI job — never stored as binaries in version control.
|
|
127
|
-
|
|
128
|
-
### Calling the CLI from a consumer project
|
|
129
|
-
|
|
130
|
-
After installing `dicom-synth`, the `dicom-synth-generate` binary is available via your package manager:
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
# pnpm
|
|
134
|
-
pnpm exec dicom-synth-generate --schema dataset.json --out ./out
|
|
135
|
-
|
|
136
|
-
# npm / npx
|
|
137
|
-
npx dicom-synth-generate --schema dataset.json --out ./out
|
|
138
|
-
|
|
139
|
-
# one-off without installing
|
|
140
|
-
pnpm --package=dicom-synth dlx dicom-synth-generate --schema dataset.json --out ./out
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### In-process pipeline (no disk I/O)
|
|
144
|
-
|
|
145
|
-
Use layer 2 when passing generated files directly to a parser or pipeline without writing to disk:
|
|
146
|
-
|
|
147
|
-
```ts
|
|
148
|
-
import { generateCollectionFromSpec, type GeneratedFile } from 'dicom-synth'
|
|
149
|
-
|
|
150
|
-
const files: GeneratedFile[] = []
|
|
151
|
-
for await (const file of generateCollectionFromSpec({ entries: [{ type: 'valid-image', count: 5 }] })) {
|
|
152
|
-
files.push(file)
|
|
153
|
-
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
---
|
|
157
|
-
|
|
158
|
-
## Schema reference
|
|
159
|
-
|
|
160
|
-
### `DatasetSpec`
|
|
161
|
-
|
|
162
|
-
```ts
|
|
163
|
-
import type { DatasetSpec } from 'dicom-synth'
|
|
164
|
-
|
|
165
|
-
type DatasetSpec = {
|
|
166
|
-
entries?: EntrySpec[] // flat entries — each file gets its own UIDs
|
|
167
|
-
studies?: StudySpec[] // grouped entries — shared study/series UIDs
|
|
168
|
-
seed?: number // optional: fixed seed for deterministic UID generation
|
|
169
|
-
layout?: 'flat' | 'hierarchical' // optional: directory layout for grouped files
|
|
170
|
-
}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
At least one of `entries` or `studies` must be present and non-empty; both may be used together.
|
|
174
|
-
|
|
175
|
-
`seed` makes UID generation fully deterministic across runs. Omit it for random UIDs.
|
|
176
|
-
|
|
177
|
-
### `EntrySpec`
|
|
178
|
-
|
|
179
|
-
An `EntrySpec` is a `FileSpec` plus an optional `count` field (how many times to generate it):
|
|
180
|
-
|
|
181
|
-
```json
|
|
182
|
-
{ "type": "valid-image", "count": 10 }
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
`count` defaults to 1 and must be a positive integer. It is a collection-layer concern — `generateFile` does not accept `count`.
|
|
186
|
-
|
|
187
|
-
### `FileSpec` type catalogue
|
|
188
|
-
|
|
189
|
-
| Type | Description | Conformance |
|
|
190
|
-
|---|---|---|
|
|
191
|
-
| `valid-image` | Standards-valid image (CT by default — see `modality`); numeric UIDs, complete meta header | strict |
|
|
192
|
-
| `invalid-uid-image` | Image with non-numeric UIDs (letters in UID components) | edge |
|
|
193
|
-
| `vendor-warnings-image` | Image with empty `Laterality` and `PatientWeight = 0` — produces dciodvfy warnings | edge |
|
|
194
|
-
| `fake-signature` | 200-byte buffer with `XXXX` at preamble offset — no DICM magic | edge |
|
|
195
|
-
| `non-dicom` | Arbitrary text buffer; no extension in filename | edge |
|
|
196
|
-
| `dicomdir` | Minimal DICOMDIR file | strict |
|
|
197
|
-
|
|
198
|
-
**`non-dicom` fields:**
|
|
199
|
-
|
|
200
|
-
```json
|
|
201
|
-
{ "type": "non-dicom", "content": "not a dicom file" }
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
`content` defaults to `"not dicom"`.
|
|
205
|
-
|
|
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):
|
|
211
|
-
|
|
212
|
-
```json
|
|
213
|
-
{ "type": "valid-image", "rows": 512, "columns": 512, "frames": 100 }
|
|
214
|
-
```
|
|
215
|
-
|
|
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.
|
|
227
|
-
|
|
228
|
-
### Modality presets (`modality`)
|
|
229
|
-
|
|
230
|
-
Available on all image types. Defaults to `CT`.
|
|
231
|
-
|
|
232
|
-
```json
|
|
233
|
-
{ "type": "valid-image", "modality": "PT" }
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
Each preset sets the matching SOP Class UID (dataset and meta header), the `Modality` tag, and minimal modality-specific type-1 attributes:
|
|
237
|
-
|
|
238
|
-
| Modality | SOP Class | Extra attributes |
|
|
239
|
-
|---|---|---|
|
|
240
|
-
| `CT` (default) | CT Image Storage `…1.1.2` | — |
|
|
241
|
-
| `PT` | PET Image Storage `…1.1.128` | `Units`, `DecayCorrection`, `CorrectedImage` |
|
|
242
|
-
| `MR` | MR Image Storage `…1.1.4` | `ScanningSequence`, `SequenceVariant` |
|
|
243
|
-
| `CR` | CR Image Storage `…1.1.1` | — |
|
|
244
|
-
|
|
245
|
-
Presets target shape/variance fidelity for pipeline testing, not clinical fidelity. Tag overrides win over preset attributes.
|
|
246
|
-
|
|
247
|
-
```ts
|
|
248
|
-
import type { Modality } from 'dicom-synth'
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
### Study/series grouping (`studies`)
|
|
252
|
-
|
|
253
|
-
Group files so they share `StudyInstanceUID` (per study) and `SeriesInstanceUID` (per series). `InstanceNumber` increments from 1 within each series. SOP Instance UIDs remain unique per file.
|
|
254
|
-
|
|
255
|
-
```json
|
|
256
|
-
{
|
|
257
|
-
"studies": [
|
|
258
|
-
{
|
|
259
|
-
"tags": { "PatientID": "P001" },
|
|
260
|
-
"series": [
|
|
261
|
-
{ "entries": [{ "type": "valid-image", "modality": "CT", "count": 50 }] },
|
|
262
|
-
{ "entries": [{ "type": "valid-image", "modality": "PT", "count": 120 }] }
|
|
263
|
-
]
|
|
264
|
-
},
|
|
265
|
-
{ "series": [{ "entries": [{ "type": "valid-image" }] }], "count": 3 }
|
|
266
|
-
],
|
|
267
|
-
"seed": 42,
|
|
268
|
-
"layout": "hierarchical"
|
|
269
|
-
}
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
- `StudySpec.count` generates N copies of the whole study, each with fresh UIDs.
|
|
273
|
-
- `tags` may be set at study, series, and entry level — merged into each file, with entry tags winning over series tags, which win over study tags.
|
|
274
|
-
- Only image-generating types can appear in a series (`fake-signature`, `non-dicom`, and `dicomdir` are rejected).
|
|
275
|
-
- With a `seed`, group UIDs are deterministic; without one they are random but still shared within each group.
|
|
276
|
-
|
|
277
|
-
```ts
|
|
278
|
-
import type { StudySpec, SeriesSpec, SeriesEntrySpec } from 'dicom-synth'
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
### Layout (`layout`)
|
|
282
|
-
|
|
283
|
-
| Value | Effect |
|
|
284
|
-
|---|---|
|
|
285
|
-
| `flat` (default) | All files in the output root, named `<type>-<index>[.dcm]` |
|
|
286
|
-
| `hierarchical` | Grouped files written as `study-001/series-001/00001.dcm`; flat `entries` stay in the root |
|
|
287
|
-
|
|
288
|
-
`GeneratedFile.relativePath` carries the layout-aware path for in-process consumers; `filename` is always the basename.
|
|
289
|
-
|
|
290
|
-
### Path quirks (`pathQuirks`)
|
|
291
|
-
|
|
292
|
-
Decorate written paths with edge-case names to exercise consumer file handling (e.g. the Chrome File System Access API). Quirks apply to the relative paths the layout already produces — DICOM byte content is unchanged.
|
|
293
|
-
|
|
294
|
-
```json
|
|
295
|
-
{
|
|
296
|
-
"studies": [{ "series": [{ "entries": [{ "type": "valid-image", "count": 3 }] }] }],
|
|
297
|
-
"layout": "hierarchical",
|
|
298
|
-
"pathQuirks": ["trailing-dot", "unicode"]
|
|
299
|
-
}
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
| Quirk | Effect |
|
|
303
|
-
|---|---|
|
|
304
|
-
| `trailing-dot` | Appends `.` to each directory component (`study-001./series-001./…`); on flat output, to the filename |
|
|
305
|
-
| `unicode` | Inserts a non-ASCII character into directory and file names |
|
|
306
|
-
| `deep-nesting` | Injects extra nested directory levels before the file |
|
|
307
|
-
| `long-name` | Pads the filename stem and the deepest directory component toward the 255-byte limit |
|
|
308
|
-
|
|
309
|
-
Decorations are stable per path component, so a series' files still share one directory. Quirks compose; the result is deterministic and independent of the order they're listed in.
|
|
310
|
-
|
|
311
|
-
> **Cross-platform:** these names are writable on POSIX (Linux/macOS, where CI runs) but Windows rewrites or rejects several (trailing dots, reserved names, long paths). The intent is to *feed* a downstream consumer (e.g. a browser), not to round-trip on Windows.
|
|
312
|
-
|
|
313
|
-
```ts
|
|
314
|
-
import type { PathQuirk } from 'dicom-synth'
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
### Tag overrides (`tags`)
|
|
318
|
-
|
|
319
|
-
Available on all image types.
|
|
320
|
-
|
|
321
|
-
```json
|
|
322
|
-
{
|
|
323
|
-
"type": "valid-image",
|
|
324
|
-
"tags": {
|
|
325
|
-
"PatientID": "P001",
|
|
326
|
-
"PatientName": "DOE^JANE",
|
|
327
|
-
"00081030": "Research Protocol A"
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
Keys are either DICOM keyword names (PascalCase, e.g. `"Modality"`) or 8-hex-character tag strings (e.g. `"00080060"`). Unknown hex tags produce a console warning and are skipped.
|
|
333
|
-
|
|
334
|
-
```ts
|
|
335
|
-
import type { DicomTagOverrides } from 'dicom-synth'
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
### Transfer syntax (`transferSyntax`)
|
|
339
|
-
|
|
340
|
-
Available on all image types.
|
|
341
|
-
|
|
342
|
-
| Value | `TransferSyntaxUID` in meta header |
|
|
343
|
-
|---|---|
|
|
344
|
-
| `explicit-vr-little-endian` (default) | `1.2.840.10008.1.2.1` |
|
|
345
|
-
| `implicit-vr-little-endian` | `1.2.840.10008.1.2` |
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
### Violation injection (`violations`)
|
|
349
|
-
|
|
350
|
-
Available on all image types. Violations are applied as post-processing transforms to an otherwise valid buffer.
|
|
351
|
-
|
|
352
|
-
| Violation | Effect | Detectable by dciodvfy |
|
|
353
|
-
|---|---|---|
|
|
354
|
-
| `uid-too-long` | Sets `SOPInstanceUID` to a 65-character value (limit is 64) | yes |
|
|
355
|
-
| `non-conformant-uid` | Sets `SOPInstanceUID` to a UID with a leading-zero component | yes |
|
|
356
|
-
| `vr-max-length-exceeded` | Sets `StudyDescription` to a 65-character value (LO limit is 64) | yes |
|
|
357
|
-
| `missing-type1-tag` | Removes `SOPClassUID` (Type 1 mandatory attribute) | yes |
|
|
358
|
-
| `missing-meta-header` | Strips the 128-byte preamble and DICM prefix | yes |
|
|
359
|
-
| `malformed-sq-delimiter` | Appends a sequence delimiter tag with a non-zero length field | yes |
|
|
360
|
-
|
|
361
|
-
Tag-level violations are applied before byte-level violations. Byte-level violations may produce a buffer that cannot be re-parsed.
|
|
362
|
-
|
|
363
|
-
```json
|
|
364
|
-
{ "type": "valid-image", "violations": ["uid-too-long", "missing-meta-header"] }
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
```ts
|
|
368
|
-
import type { ViolationClass } from 'dicom-synth'
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
### Schema validation
|
|
372
|
-
|
|
373
|
-
```ts
|
|
374
|
-
import { validateDatasetSpec } from 'dicom-synth'
|
|
375
|
-
|
|
376
|
-
const spec = validateDatasetSpec(JSON.parse(rawJson)) // throws on invalid input
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
The validator throws with a human-readable message on any structural error.
|
|
380
|
-
|
|
381
|
-
### Full schema example
|
|
382
|
-
|
|
383
|
-
```json
|
|
384
|
-
{
|
|
385
|
-
"entries": [
|
|
386
|
-
{ "type": "valid-image", "count": 10, "tags": { "PatientID": "P001" }, "transferSyntax": "explicit-vr-little-endian" },
|
|
387
|
-
{ "type": "invalid-uid-image", "count": 3 },
|
|
388
|
-
{ "type": "vendor-warnings-image", "count": 2 },
|
|
389
|
-
{ "type": "valid-image", "count": 1, "rows": 512, "columns": 512, "frames": 100 },
|
|
390
|
-
{ "type": "valid-image", "count": 1, "targetSizeKb": 250000 },
|
|
391
|
-
{ "type": "valid-image", "count": 1, "violations": ["uid-too-long", "missing-meta-header"] },
|
|
392
|
-
{ "type": "fake-signature", "count": 5 },
|
|
393
|
-
{ "type": "non-dicom", "count": 2, "content": "garbage" },
|
|
394
|
-
{ "type": "dicomdir", "count": 1 }
|
|
395
|
-
],
|
|
396
|
-
"studies": [
|
|
397
|
-
{
|
|
398
|
-
"series": [
|
|
399
|
-
{ "entries": [{ "type": "valid-image", "modality": "CT", "count": 50 }] },
|
|
400
|
-
{ "entries": [{ "type": "valid-image", "modality": "PT", "count": 120 }] }
|
|
401
|
-
]
|
|
402
|
-
}
|
|
403
|
-
],
|
|
404
|
-
"seed": 42,
|
|
405
|
-
"layout": "hierarchical"
|
|
406
|
-
}
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
---
|
|
410
|
-
|
|
411
|
-
## Parametric designer
|
|
412
|
-
|
|
413
|
-
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.
|
|
414
|
-
|
|
415
|
-
```ts
|
|
416
|
-
import { resolveParametricSpec, writeCollectionFromSpec } from 'dicom-synth'
|
|
417
|
-
|
|
418
|
-
const resolved = resolveParametricSpec({
|
|
419
|
-
seed: 42,
|
|
420
|
-
studies: {
|
|
421
|
-
count: { min: 2, max: 4 }, // ranges are inclusive
|
|
422
|
-
seriesPerStudy: { min: 1, max: 3 },
|
|
423
|
-
filesPerSeries: { min: 5, max: 20 },
|
|
424
|
-
fileSizeKb: { min: 100, max: 600 }, // sampled per file
|
|
425
|
-
modalities: ['CT', 'PT'], // one drawn per series
|
|
426
|
-
},
|
|
427
|
-
edgeCases: [{ type: 'invalid-uid-image', frequency: 0.05 }],
|
|
428
|
-
layout: 'hierarchical',
|
|
429
|
-
})
|
|
430
|
-
await writeCollectionFromSpec(resolved, './out')
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
- Every field of `studies` accepts a fixed number or an inclusive `{ min, max }` range (`Range`).
|
|
434
|
-
- `fileSizeKb` maps to `targetSizeKb`; a fixed value collapses each series into one counted entry, a range samples per file.
|
|
435
|
-
- `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.
|
|
436
|
-
- The seed (drawn randomly when omitted) is embedded in the resolved spec, so resolution is always reproducible.
|
|
437
|
-
|
|
438
|
-
```ts
|
|
439
|
-
import type { ParametricSpec, ParametricStudies, ParametricEdgeCase, Range } from 'dicom-synth'
|
|
440
|
-
import { validateParametricSpec } from 'dicom-synth'
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
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).
|
|
444
|
-
|
|
445
|
-
---
|
|
446
|
-
|
|
447
|
-
## Describe tool
|
|
448
|
-
|
|
449
|
-
`describeDirectory` scans an existing DICOM tree and emits a concrete `DatasetSpec` reproducing its shape — studies/series grouped by their UIDs, per-file size as `targetSizeKb`, and modality. This closes the loop: real tree → spec → regenerate.
|
|
450
|
-
|
|
451
|
-
```ts
|
|
452
|
-
import { describeDirectory, writeCollectionFromSpec } from 'dicom-synth'
|
|
453
|
-
|
|
454
|
-
const { spec, stats } = describeDirectory('./real-dataset')
|
|
455
|
-
// stats: { dicomFiles, skipped, unknownModality }
|
|
456
|
-
await writeCollectionFromSpec(spec, './reproduced')
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
- **Shape only by default** — no tags are copied; the regenerated dataset carries fresh UIDs and synthetic identifiers, so it cannot leak PHI from the source.
|
|
460
|
-
- **Opt-in tag preservation** — `keepTags` copies the named tags verbatim (some datasets are valuable for testing *precisely because* of their problematic tags):
|
|
461
|
-
|
|
462
|
-
```ts
|
|
463
|
-
describeDirectory('./real-dataset', { keepTags: ['Manufacturer', '00080060'] })
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
⚠️ Tags are copied exactly as found. If a kept tag contains PHI, handling that PHI is the **caller's responsibility** — the tool extracts only what you name.
|
|
467
|
-
- Files in a series collapse into one counted entry only when modality, size, and kept tags all match, so tag/size variation is preserved rather than flattened.
|
|
468
|
-
- Non-DICOM, unparseable, or ungroupable files (missing study/series UIDs) are skipped and counted. An unsupported `Modality` is emitted as the default (CT) and counted.
|
|
469
|
-
|
|
470
|
-
```ts
|
|
471
|
-
import type { DescribeOptions, DescribeResult } from 'dicom-synth'
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
---
|
|
475
|
-
|
|
476
|
-
## CLI
|
|
477
|
-
|
|
478
|
-
```bash
|
|
479
|
-
# From a schema file
|
|
480
|
-
dicom-synth-generate --schema dataset.json --out ./fixtures/generated
|
|
481
|
-
|
|
482
|
-
# Inline schema
|
|
483
34
|
dicom-synth-generate --schema-inline '{"entries":[{"type":"valid-image","count":5}]}' --out ./out
|
|
484
|
-
|
|
485
|
-
# Default output directory is ./fixtures/generated
|
|
486
|
-
dicom-synth-generate --schema dataset.json
|
|
487
|
-
|
|
488
|
-
# Resolve and generate from a parametric spec; save the resolved DatasetSpec
|
|
489
|
-
dicom-synth-generate --parametric examples/parametric.json --out ./out --emit-spec resolved.json
|
|
490
|
-
|
|
491
|
-
# Preview the manifest (paths, types, sizes) without writing anything
|
|
492
|
-
dicom-synth-generate --parametric examples/parametric.json --dry-run
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
| Flag | Effect |
|
|
496
|
-
|---|---|
|
|
497
|
-
| `--schema <path>` / `--schema-inline <json>` / `--parametric <path>` | Input spec (mutually exclusive) |
|
|
498
|
-
| `--out <dir>` | Output directory (default `./fixtures/generated`) |
|
|
499
|
-
| `--emit-spec <path>` | Write the resolved `DatasetSpec` JSON (requires `--parametric`) |
|
|
500
|
-
| `--dry-run` | Generate in memory and print the manifest; nothing is written |
|
|
501
|
-
|
|
502
|
-
The default schema (`examples/default.json`) generates one each of `valid-image`, `invalid-uid-image`, and `vendor-warnings-image`:
|
|
503
|
-
|
|
504
|
-
```bash
|
|
505
|
-
dicom-synth-generate --schema examples/default.json --out /tmp/out
|
|
506
|
-
# Wrote 3 file(s) to /tmp/out
|
|
507
|
-
# valid-image: 1
|
|
508
|
-
# invalid-uid-image: 1
|
|
509
|
-
# vendor-warnings-image: 1
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
Schema validation errors exit with code 1 and a descriptive message.
|
|
513
|
-
|
|
514
|
-
### Describe an existing tree
|
|
515
|
-
|
|
516
|
-
```bash
|
|
517
|
-
# Shape only (PHI-safe) — prints the DatasetSpec to stdout
|
|
518
|
-
dicom-synth-describe ./real-dataset
|
|
519
|
-
|
|
520
|
-
# Save to a file, and keep selected tags (caller owns any PHI in them)
|
|
521
|
-
dicom-synth-describe ./real-dataset --out spec.json --keep-tags Manufacturer,00080060
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
| Flag | Effect |
|
|
525
|
-
|---|---|
|
|
526
|
-
| `<dir>` | DICOM tree to scan (required) |
|
|
527
|
-
| `--out <path>` | Write the `DatasetSpec` JSON (default: stdout) |
|
|
528
|
-
| `--keep-tags <a,b,...>` | Copy the named tags (keywords or 8-hex) verbatim; omit for shape only |
|
|
529
|
-
|
|
530
|
-
Scan stats (DICOM/skipped/unsupported-modality counts) are written to stderr.
|
|
531
|
-
|
|
532
|
-
---
|
|
533
|
-
|
|
534
|
-
## Public fixtures
|
|
535
|
-
|
|
536
|
-
Catalog metadata lives in `data/public-cases.json`. Fetched binaries are cached at `~/.cache/dicom-synth-testcases/<sha256>/file.dcm`.
|
|
537
|
-
|
|
538
|
-
### Load and fetch
|
|
539
|
-
|
|
540
|
-
```ts
|
|
541
|
-
import {
|
|
542
|
-
loadDefaultCases,
|
|
543
|
-
loadCaseById,
|
|
544
|
-
loadCasesFromJson,
|
|
545
|
-
defaultPublicCasesPath,
|
|
546
|
-
fetchPublicCaseToCache,
|
|
547
|
-
caseCachePath,
|
|
548
|
-
} from 'dicom-synth'
|
|
549
|
-
|
|
550
|
-
// Load all bundled cases
|
|
551
|
-
const cases = loadDefaultCases()
|
|
552
|
-
|
|
553
|
-
// Load one bundled case by id
|
|
554
|
-
const record = loadCaseById(defaultPublicCasesPath(), 'pydicom-CT-small')
|
|
555
|
-
|
|
556
|
-
// Load a custom catalogue
|
|
557
|
-
const custom = loadCasesFromJson('./my-cases.json')
|
|
558
|
-
|
|
559
|
-
// Fetch to local cache, returns the file path
|
|
560
|
-
const path = await fetchPublicCaseToCache(record)
|
|
561
|
-
|
|
562
|
-
// Resolve the cache path for a known SHA-256 without fetching
|
|
563
|
-
const cached = caseCachePath(record.sha256)
|
|
564
35
|
```
|
|
565
36
|
|
|
566
|
-
|
|
37
|
+
The full input shape (image types, sizing, modalities, grouping, violations, …) is the [schema reference](./docs/schema-reference.md).
|
|
567
38
|
|
|
568
|
-
|
|
39
|
+
## Documentation
|
|
569
40
|
|
|
570
|
-
|
|
571
|
-
# GitHub Actions example
|
|
572
|
-
- uses: actions/cache@v4
|
|
573
|
-
with:
|
|
574
|
-
path: ~/.cache/dicom-synth-testcases
|
|
575
|
-
key: dicom-public-fixtures-${{ hashFiles('node_modules/dicom-synth/dist/data/public-cases.json') }}
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
### Published CLI
|
|
579
|
-
|
|
580
|
-
```bash
|
|
581
|
-
pnpm --package=dicom-synth@latest dlx dicom-synth-fetch pydicom-CT-small
|
|
582
|
-
```
|
|
583
|
-
|
|
584
|
-
### Local development
|
|
585
|
-
|
|
586
|
-
```bash
|
|
587
|
-
pnpm fetch:public-case -- pydicom-CT-small
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
---
|
|
591
|
-
|
|
592
|
-
## Source layout
|
|
593
|
-
|
|
594
|
-
| Path | Responsibility |
|
|
595
|
-
|---|---|
|
|
596
|
-
| `src/schema/types.ts` | All public TypeScript types (`FileSpec`, `DatasetSpec`, `ViolationClass`, etc.) |
|
|
597
|
-
| `src/schema/validate.ts` | `validateDatasetSpec` / `validateParametricSpec` — validate raw JSON values against the schema |
|
|
598
|
-
| `src/schema/parametric.ts` | `resolveParametricSpec` — seeded `ParametricSpec` → `DatasetSpec` resolution |
|
|
599
|
-
| `src/describe/describe.ts` | `describeDirectory` — scan a DICOM tree → `DatasetSpec` |
|
|
600
|
-
| `src/syntheticFixtures/generator.ts` | Internal buffer builders keyed on `FileSpec` type |
|
|
601
|
-
| `src/syntheticFixtures/uid.ts` | Seeded and random UID generation |
|
|
602
|
-
| `src/syntheticFixtures/violations.ts` | Post-processing violation injection |
|
|
603
|
-
| `src/collection/writer.ts` | Three-layer API — `generateFile`, `generateCollectionFromSpec`, `writeCollectionFromSpec` |
|
|
604
|
-
| `src/public-fixtures/catalog.ts` | Load bundled public case catalogue |
|
|
605
|
-
| `src/public-fixtures/fetch.ts` | Fetch, SHA-256 verify, content-addressed cache |
|
|
606
|
-
| `bin/dicom-synth-generate.mjs` | Published generate CLI (requires `pnpm build`) |
|
|
607
|
-
| `bin/dicom-synth-fetch.mjs` | Published fetch CLI (requires `pnpm build`) |
|
|
608
|
-
| `bin/dicom-synth-describe.mjs` | Published describe CLI (requires `pnpm build`) |
|
|
609
|
-
| `examples/default.json` | Default `DatasetSpec` — one each of `valid-image`, `invalid-uid-image`, `vendor-warnings-image` |
|
|
610
|
-
| `examples/parametric.json` | Example `ParametricSpec` for the `--parametric` CLI path |
|
|
611
|
-
| `examples/convert-data-shape.mjs` | Example converter: tree-shape JSON → `DatasetSpec` |
|
|
612
|
-
|
|
613
|
-
---
|
|
614
|
-
|
|
615
|
-
## Development
|
|
616
|
-
|
|
617
|
-
```bash
|
|
618
|
-
pnpm install
|
|
619
|
-
pnpm build
|
|
620
|
-
pnpm test
|
|
621
|
-
pnpm code:quality
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
| Script | Purpose |
|
|
41
|
+
| Guide | Covers |
|
|
625
42
|
|---|---|
|
|
626
|
-
|
|
|
627
|
-
|
|
|
628
|
-
|
|
|
629
|
-
| `
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
43
|
+
| [Synthetic fixtures](./docs/synthetic-fixtures.md) | The three-layer generation API (`generateFile`, `generateCollectionFromSpec`, `writeCollectionFromSpec`) and usage patterns |
|
|
44
|
+
| [Schema reference](./docs/schema-reference.md) | `DatasetSpec` and every field — types, sizing, modality, grouping, layout, path quirks, tags, transfer syntax, violations, validation |
|
|
45
|
+
| [Parametric designer](./docs/parametric.md) | Range-based `ParametricSpec` that resolves deterministically into a `DatasetSpec` |
|
|
46
|
+
| [Describe tool](./docs/describe.md) | Scan a real DICOM tree → `DatasetSpec` (shape-only by default; opt-in tag preservation) |
|
|
47
|
+
| [CLI reference](./docs/cli.md) | `dicom-synth-generate`, `dicom-synth-describe`, `dicom-synth-fetch` |
|
|
48
|
+
| [Public fixtures](./docs/public-fixtures.md) | Catalog-backed fetch/cache with SHA-256 verification |
|
|
49
|
+
| [Development](./docs/development.md) | Scripts, hooks, and source layout |
|
|
50
|
+
| [Examples](./examples/README.md) | Runnable example specs and the shape converter |
|
|
634
51
|
|
|
635
52
|
## Future development
|
|
636
53
|
|
|
54
|
+
- **Very large (streamed) files** — synthesise single DICOMs beyond the 512 MB in-memory cap via a disk-only streaming writer
|
|
637
55
|
- **Dimension edge-case recipes** — documented presets for geometry pathologies (1×65535 strips, high frame counts)
|
|
638
56
|
- **Per-file tag variance templating** — patterned overrides like `"PatientID": "P{index}"` for shape replication
|
|
639
57
|
- **Compressed transfer syntaxes** — JPEG-LS, JPEG 2000, RLE
|
|
640
58
|
- **Private fixture catalogues** — same SHA-256 fetch/cache pattern for credentials-backed sources (e.g. S3)
|
|
641
59
|
|
|
642
|
-
---
|
|
643
|
-
|
|
644
60
|
## License
|
|
645
61
|
|
|
646
62
|
Apache-2.0
|