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 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
- ## Synthetic fixtures
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
- Files are yielded one at a time safe to pipe without buffering the entire collection.
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
- ### CI caching
37
+ The full input shape (image types, sizing, modalities, grouping, violations, …) is the [schema reference](./docs/schema-reference.md).
567
38
 
568
- The fetch cache is keyed by SHA-256 so cached files are safe to restore across CI runs. To avoid re-downloading on every run, cache the default cache root:
39
+ ## Documentation
569
40
 
570
- ```yaml
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
- | `pnpm write:synthetic` | Write `examples/default.json` to `fixtures/generated` |
627
- | `pnpm fetch:public-case` | Fetch one public case by id |
628
- | `pnpm hooks:pre-commit` | Reproduce commit hook locally |
629
- | `pnpm hooks:pre-push` | Reproduce push hook locally |
630
-
631
- Git hooks: see [CONTRIBUTING.md](CONTRIBUTING.md).
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