dicom-synth 1.2.0 → 1.3.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
@@ -4,7 +4,7 @@ Schema-driven **synthetic DICOM fixture generation** and **public fixture** fetc
4
4
 
5
5
  ## Disclaimer
6
6
 
7
- Still in early release. APIs may change without notice.
7
+ ⚠️ Still in early release. APIs may change without notice. Version 2+ will be the first stable release ⚠️
8
8
 
9
9
  ## Install
10
10
 
@@ -24,16 +24,16 @@ Generation is a three-layer API. Each layer is a thin wrapper over the one below
24
24
  ```ts
25
25
  import { generateFile } from 'dicom-synth'
26
26
 
27
- const { buffer, filename, type, index } = await generateFile({ type: 'valid-ct' })
27
+ const { buffer, filename, type, index } = await generateFile({ type: 'valid-image' })
28
28
  // buffer: Buffer ready to write or pass to a parser
29
- // filename: 'valid-ct-000.dcm'
29
+ // filename: 'valid-image-000.dcm'
30
30
  ```
31
31
 
32
32
  With options:
33
33
 
34
34
  ```ts
35
35
  const { buffer } = await generateFile(
36
- { type: 'valid-ct', tags: { PatientID: 'P001' }, violations: ['uid-too-long'] },
36
+ { type: 'valid-image', tags: { PatientID: 'P001' }, violations: ['uid-too-long'] },
37
37
  { seed: 42, index: 7 },
38
38
  )
39
39
  ```
@@ -44,7 +44,7 @@ Write a single file to disk:
44
44
  import { writeFileSync } from 'node:fs'
45
45
  import { generateFile } from 'dicom-synth'
46
46
 
47
- const { buffer, filename } = await generateFile({ type: 'valid-ct' })
47
+ const { buffer, filename } = await generateFile({ type: 'valid-image' })
48
48
  writeFileSync(`./out/${filename}`, buffer)
49
49
  ```
50
50
 
@@ -55,8 +55,8 @@ import { generateCollectionFromSpec } from 'dicom-synth'
55
55
 
56
56
  for await (const file of generateCollectionFromSpec({
57
57
  entries: [
58
- { type: 'valid-ct', count: 10 },
59
- { type: 'invalid-uid-ct', count: 3 },
58
+ { type: 'valid-image', count: 10 },
59
+ { type: 'invalid-uid-image', count: 3 },
60
60
  ],
61
61
  seed: 42,
62
62
  })) {
@@ -72,7 +72,7 @@ Files are yielded one at a time — safe to pipe without buffering the entire co
72
72
  import { writeCollectionFromSpec } from 'dicom-synth'
73
73
 
74
74
  const manifest = await writeCollectionFromSpec(
75
- { entries: [{ type: 'valid-ct', count: 5 }], seed: 42 },
75
+ { entries: [{ type: 'valid-image', count: 5 }], seed: 42 },
76
76
  './fixtures/generated',
77
77
  )
78
78
  // manifest: Array<{ path: string; type: string; index: number }>
@@ -95,7 +95,7 @@ import { writeCollectionFromSpec } from 'dicom-synth'
95
95
  const dir = await mkdtemp(join(tmpdir(), 'synth-'))
96
96
  try {
97
97
  await writeCollectionFromSpec(
98
- { entries: [{ type: 'valid-ct', count: 3 }], seed: 42 },
98
+ { entries: [{ type: 'valid-image', count: 3 }], seed: 42 },
99
99
  dir,
100
100
  )
101
101
  // run pipeline against dir
@@ -148,7 +148,7 @@ Use layer 2 when passing generated files directly to a parser or pipeline withou
148
148
  import { generateCollectionFromSpec, type GeneratedFile } from 'dicom-synth'
149
149
 
150
150
  const files: GeneratedFile[] = []
151
- for await (const file of generateCollectionFromSpec({ entries: [{ type: 'valid-ct', count: 5 }] })) {
151
+ for await (const file of generateCollectionFromSpec({ entries: [{ type: 'valid-image', count: 5 }] })) {
152
152
  files.push(file)
153
153
  }
154
154
  ```
@@ -163,11 +163,15 @@ for await (const file of generateCollectionFromSpec({ entries: [{ type: 'valid-c
163
163
  import type { DatasetSpec } from 'dicom-synth'
164
164
 
165
165
  type DatasetSpec = {
166
- entries: EntrySpec[] // one or more entry specs; must not be empty
167
- seed?: number // optional: fixed seed for deterministic UID generation
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
168
170
  }
169
171
  ```
170
172
 
173
+ At least one of `entries` or `studies` must be present and non-empty; both may be used together.
174
+
171
175
  `seed` makes UID generation fully deterministic across runs. Omit it for random UIDs.
172
176
 
173
177
  ### `EntrySpec`
@@ -175,7 +179,7 @@ type DatasetSpec = {
175
179
  An `EntrySpec` is a `FileSpec` plus an optional `count` field (how many times to generate it):
176
180
 
177
181
  ```json
178
- { "type": "valid-ct", "count": 10 }
182
+ { "type": "valid-image", "count": 10 }
179
183
  ```
180
184
 
181
185
  `count` defaults to 1 and must be a positive integer. It is a collection-layer concern — `generateFile` does not accept `count`.
@@ -184,21 +188,21 @@ An `EntrySpec` is a `FileSpec` plus an optional `count` field (how many times to
184
188
 
185
189
  | Type | Description | Conformance |
186
190
  |---|---|---|
187
- | `valid-ct` | Standards-valid CT image; numeric UIDs, complete meta header | strict |
188
- | `invalid-uid-ct` | CT with non-numeric UIDs (letters in UID components) | edge |
189
- | `vendor-warnings-ct` | CT with empty `Laterality` and `PatientWeight = 0` — produces dciodvfy warnings | edge |
190
- | `large-ct` | CT with configurable pixel dimensions; `rows` and `columns` required | strict |
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
+ | `large-image` | Image with configurable pixel dimensions; `rows` and `columns` required | strict |
191
195
  | `fake-signature` | 200-byte buffer with `XXXX` at preamble offset — no DICM magic | edge |
192
196
  | `non-dicom` | Arbitrary text buffer; no extension in filename | edge |
193
197
  | `dicomdir` | Minimal DICOMDIR file | strict |
194
198
 
195
- **`large-ct` fields:**
199
+ **`large-image` fields:**
196
200
 
197
201
  ```json
198
- { "type": "large-ct", "rows": 512, "columns": 512, "frames": 100 }
202
+ { "type": "large-image", "rows": 512, "columns": 512, "frames": 100 }
199
203
  ```
200
204
 
201
- `frames` defaults to 1. A 512×512×100 CT produces a buffer of ~52 MB.
205
+ `frames` defaults to 1. A 512×512×100 image produces a buffer of ~52 MB.
202
206
 
203
207
  **`non-dicom` fields:**
204
208
 
@@ -208,13 +212,75 @@ An `EntrySpec` is a `FileSpec` plus an optional `count` field (how many times to
208
212
 
209
213
  `content` defaults to `"not dicom"`.
210
214
 
215
+ ### Modality presets (`modality`)
216
+
217
+ Available on `valid-image`, `invalid-uid-image`, `vendor-warnings-image`, and `large-image`. Defaults to `CT`.
218
+
219
+ ```json
220
+ { "type": "valid-image", "modality": "PT" }
221
+ ```
222
+
223
+ Each preset sets the matching SOP Class UID (dataset and meta header), the `Modality` tag, and minimal modality-specific type-1 attributes:
224
+
225
+ | Modality | SOP Class | Extra attributes |
226
+ |---|---|---|
227
+ | `CT` (default) | CT Image Storage `…1.1.2` | — |
228
+ | `PT` | PET Image Storage `…1.1.128` | `Units`, `DecayCorrection`, `CorrectedImage` |
229
+ | `MR` | MR Image Storage `…1.1.4` | `ScanningSequence`, `SequenceVariant` |
230
+ | `CR` | CR Image Storage `…1.1.1` | — |
231
+
232
+ Presets target shape/variance fidelity for pipeline testing, not clinical fidelity. Tag overrides win over preset attributes.
233
+
234
+ ```ts
235
+ import type { Modality } from 'dicom-synth'
236
+ ```
237
+
238
+ ### Study/series grouping (`studies`)
239
+
240
+ 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.
241
+
242
+ ```json
243
+ {
244
+ "studies": [
245
+ {
246
+ "tags": { "PatientID": "P001" },
247
+ "series": [
248
+ { "entries": [{ "type": "valid-image", "modality": "CT", "count": 50 }] },
249
+ { "entries": [{ "type": "valid-image", "modality": "PT", "count": 120 }] }
250
+ ]
251
+ },
252
+ { "series": [{ "entries": [{ "type": "valid-image" }] }], "count": 3 }
253
+ ],
254
+ "seed": 42,
255
+ "layout": "hierarchical"
256
+ }
257
+ ```
258
+
259
+ - `StudySpec.count` generates N copies of the whole study, each with fresh UIDs.
260
+ - `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.
261
+ - Only image-generating types can appear in a series (`fake-signature`, `non-dicom`, and `dicomdir` are rejected).
262
+ - With a `seed`, group UIDs are deterministic; without one they are random but still shared within each group.
263
+
264
+ ```ts
265
+ import type { StudySpec, SeriesSpec, SeriesEntrySpec } from 'dicom-synth'
266
+ ```
267
+
268
+ ### Layout (`layout`)
269
+
270
+ | Value | Effect |
271
+ |---|---|
272
+ | `flat` (default) | All files in the output root, named `<type>-<index>[.dcm]` |
273
+ | `hierarchical` | Grouped files written as `study-001/series-001/00001.dcm`; flat `entries` stay in the root |
274
+
275
+ `GeneratedFile.relativePath` carries the layout-aware path for in-process consumers; `filename` is always the basename.
276
+
211
277
  ### Tag overrides (`tags`)
212
278
 
213
- Available on `valid-ct`, `invalid-uid-ct`, `vendor-warnings-ct`, and `large-ct`.
279
+ Available on `valid-image`, `invalid-uid-image`, `vendor-warnings-image`, and `large-image`.
214
280
 
215
281
  ```json
216
282
  {
217
- "type": "valid-ct",
283
+ "type": "valid-image",
218
284
  "tags": {
219
285
  "PatientID": "P001",
220
286
  "PatientName": "DOE^JANE",
@@ -231,7 +297,7 @@ import type { DicomTagOverrides } from 'dicom-synth'
231
297
 
232
298
  ### Transfer syntax (`transferSyntax`)
233
299
 
234
- Available on `valid-ct`, `invalid-uid-ct`, `vendor-warnings-ct`, and `large-ct`.
300
+ Available on `valid-image`, `invalid-uid-image`, `vendor-warnings-image`, and `large-image`.
235
301
 
236
302
  | Value | `TransferSyntaxUID` in meta header |
237
303
  |---|---|
@@ -241,7 +307,7 @@ Available on `valid-ct`, `invalid-uid-ct`, `vendor-warnings-ct`, and `large-ct`.
241
307
 
242
308
  ### Violation injection (`violations`)
243
309
 
244
- Available on `valid-ct`, `invalid-uid-ct`, `vendor-warnings-ct`, and `large-ct`. Violations are applied as post-processing transforms to an otherwise valid buffer.
310
+ Available on `valid-image`, `invalid-uid-image`, `vendor-warnings-image`, and `large-image`. Violations are applied as post-processing transforms to an otherwise valid buffer.
245
311
 
246
312
  | Violation | Effect | Detectable by dciodvfy |
247
313
  |---|---|---|
@@ -255,7 +321,7 @@ Available on `valid-ct`, `invalid-uid-ct`, `vendor-warnings-ct`, and `large-ct`.
255
321
  Tag-level violations are applied before byte-level violations. Byte-level violations may produce a buffer that cannot be re-parsed.
256
322
 
257
323
  ```json
258
- { "type": "valid-ct", "violations": ["uid-too-long", "missing-meta-header"] }
324
+ { "type": "valid-image", "violations": ["uid-too-long", "missing-meta-header"] }
259
325
  ```
260
326
 
261
327
  ```ts
@@ -277,16 +343,25 @@ The validator throws with a human-readable message on any structural error.
277
343
  ```json
278
344
  {
279
345
  "entries": [
280
- { "type": "valid-ct", "count": 10, "tags": { "PatientID": "P001" }, "transferSyntax": "explicit-vr-little-endian" },
281
- { "type": "invalid-uid-ct", "count": 3 },
282
- { "type": "vendor-warnings-ct", "count": 2 },
283
- { "type": "large-ct", "count": 1, "rows": 512, "columns": 512, "frames": 100 },
284
- { "type": "valid-ct", "count": 1, "violations": ["uid-too-long", "missing-meta-header"] },
346
+ { "type": "valid-image", "count": 10, "tags": { "PatientID": "P001" }, "transferSyntax": "explicit-vr-little-endian" },
347
+ { "type": "invalid-uid-image", "count": 3 },
348
+ { "type": "vendor-warnings-image", "count": 2 },
349
+ { "type": "large-image", "count": 1, "rows": 512, "columns": 512, "frames": 100 },
350
+ { "type": "valid-image", "count": 1, "violations": ["uid-too-long", "missing-meta-header"] },
285
351
  { "type": "fake-signature", "count": 5 },
286
352
  { "type": "non-dicom", "count": 2, "content": "garbage" },
287
353
  { "type": "dicomdir", "count": 1 }
288
354
  ],
289
- "seed": 42
355
+ "studies": [
356
+ {
357
+ "series": [
358
+ { "entries": [{ "type": "valid-image", "modality": "CT", "count": 50 }] },
359
+ { "entries": [{ "type": "valid-image", "modality": "PT", "count": 120 }] }
360
+ ]
361
+ }
362
+ ],
363
+ "seed": 42,
364
+ "layout": "hierarchical"
290
365
  }
291
366
  ```
292
367
 
@@ -299,20 +374,20 @@ The validator throws with a human-readable message on any structural error.
299
374
  dicom-synth-generate --schema dataset.json --out ./fixtures/generated
300
375
 
301
376
  # Inline schema
302
- dicom-synth-generate --schema-inline '{"entries":[{"type":"valid-ct","count":5}]}' --out ./out
377
+ dicom-synth-generate --schema-inline '{"entries":[{"type":"valid-image","count":5}]}' --out ./out
303
378
 
304
379
  # Default output directory is ./fixtures/generated
305
380
  dicom-synth-generate --schema dataset.json
306
381
  ```
307
382
 
308
- The default schema (`examples/default.json`) generates one each of `valid-ct`, `invalid-uid-ct`, and `vendor-warnings-ct`:
383
+ The default schema (`examples/default.json`) generates one each of `valid-image`, `invalid-uid-image`, and `vendor-warnings-image`:
309
384
 
310
385
  ```bash
311
386
  dicom-synth-generate --schema examples/default.json --out /tmp/out
312
387
  # Wrote 3 file(s) to /tmp/out
313
- # valid-ct: 1
314
- # invalid-uid-ct: 1
315
- # vendor-warnings-ct: 1
388
+ # valid-image: 1
389
+ # invalid-uid-image: 1
390
+ # vendor-warnings-image: 1
316
391
  ```
317
392
 
318
393
  Schema validation errors exit with code 1 and a descriptive message.
@@ -391,7 +466,7 @@ pnpm fetch:public-case -- pydicom-CT-small
391
466
  | `src/public-fixtures/fetch.ts` | Fetch, SHA-256 verify, content-addressed cache |
392
467
  | `bin/dicom-synth-generate.mjs` | Published generate CLI (requires `pnpm build`) |
393
468
  | `bin/dicom-synth-fetch.mjs` | Published fetch CLI (requires `pnpm build`) |
394
- | `examples/default.json` | Default `DatasetSpec` — one each of `valid-ct`, `invalid-uid-ct`, `vendor-warnings-ct` |
469
+ | `examples/default.json` | Default `DatasetSpec` — one each of `valid-image`, `invalid-uid-image`, `vendor-warnings-image` |
395
470
 
396
471
  ---
397
472
 
@@ -417,9 +492,10 @@ Git hooks: see [CONTRIBUTING.md](CONTRIBUTING.md).
417
492
 
418
493
  ## Future development
419
494
 
420
- - **Multi-series study layouts** — shared `StudyInstanceUID` across files via a `studies` top-level key; no breaking schema change needed
495
+ - **Size targeting** — generate a file of approximately N KB via a `targetSizeKb` field
496
+ - **Parametric dataset designer** — range-based spec (`{min, max}` for counts/sizes) that resolves deterministically into a concrete `DatasetSpec`
497
+ - **Dry-run mode** — emit the manifest (paths, types, grouping) without writing files
421
498
  - **Compressed transfer syntaxes** — JPEG-LS, JPEG 2000, RLE
422
- - **Modality presets** — MR, PET, CR profiles beyond CT
423
499
  - **Private fixture catalogues** — same SHA-256 fetch/cache pattern for credentials-backed sources (e.g. S3)
424
500
 
425
501
  ---
@@ -49,7 +49,35 @@ var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
49
49
  var HEX_TAG_RE = /^[0-9a-fA-F]{8}$/;
50
50
 
51
51
  // src/syntheticFixtures/generator.ts
52
- var CT_SOP_CLASS = "1.2.840.10008.5.1.4.1.1.2";
52
+ var MODALITY_PRESETS = {
53
+ CT: {
54
+ sopClassUid: "1.2.840.10008.5.1.4.1.1.2",
55
+ // CT Image Storage
56
+ attributes: {}
57
+ },
58
+ PT: {
59
+ sopClassUid: "1.2.840.10008.5.1.4.1.1.128",
60
+ // PET Image Storage
61
+ attributes: {
62
+ Units: "BQML",
63
+ DecayCorrection: "NONE",
64
+ CorrectedImage: ["ATTN", "DECY"]
65
+ }
66
+ },
67
+ MR: {
68
+ sopClassUid: "1.2.840.10008.5.1.4.1.1.4",
69
+ // MR Image Storage
70
+ attributes: {
71
+ ScanningSequence: "SE",
72
+ SequenceVariant: "NONE"
73
+ }
74
+ },
75
+ CR: {
76
+ sopClassUid: "1.2.840.10008.5.1.4.1.1.1",
77
+ // CR Image Storage
78
+ attributes: {}
79
+ }
80
+ };
53
81
  var TRANSFER_SYNTAX_UID = {
54
82
  "explicit-vr-little-endian": "1.2.840.10008.1.2.1",
55
83
  "implicit-vr-little-endian": "1.2.840.10008.1.2"
@@ -82,22 +110,23 @@ function applyTagOverrides(dataset, tags) {
82
110
  }
83
111
  }
84
112
  }
85
- function buildMeta(uid, transferSyntax = "explicit-vr-little-endian") {
113
+ function buildMeta(uid, modality, transferSyntax = "explicit-vr-little-endian") {
86
114
  return {
87
115
  FileMetaInformationVersion: new Uint8Array([0, 1]).buffer,
88
- MediaStorageSOPClassUID: CT_SOP_CLASS,
116
+ MediaStorageSOPClassUID: MODALITY_PRESETS[modality].sopClassUid,
89
117
  MediaStorageSOPInstanceUID: uid.sop,
90
118
  TransferSyntaxUID: TRANSFER_SYNTAX_UID[transferSyntax],
91
119
  ImplementationClassUID: "1.2.3.4",
92
120
  ImplementationVersionName: "SYNTH"
93
121
  };
94
122
  }
95
- function buildBaseCtDataset(uid) {
123
+ function buildBaseImageDataset(uid, modality) {
124
+ const preset = MODALITY_PRESETS[modality];
96
125
  return {
97
126
  PatientName: "SYNTH^SUBJECT",
98
127
  PatientID: "SYNTH_PID",
99
- Modality: "CT",
100
- SOPClassUID: CT_SOP_CLASS,
128
+ Modality: modality,
129
+ SOPClassUID: preset.sopClassUid,
101
130
  SOPInstanceUID: uid.sop,
102
131
  SeriesInstanceUID: uid.series,
103
132
  StudyInstanceUID: uid.study,
@@ -114,7 +143,8 @@ function buildBaseCtDataset(uid) {
114
143
  PixelData: new Uint8Array([0, 0]).buffer,
115
144
  // Declares PixelData's VR explicitly — without it dcmjs logs
116
145
  // "No value representation given for PixelData" and falls back to OW anyway.
117
- _vrMap: { PixelData: "OW" }
146
+ _vrMap: { PixelData: "OW" },
147
+ ...preset.attributes
118
148
  };
119
149
  }
120
150
  function serializeDict(meta, dataset) {
@@ -128,36 +158,36 @@ function serializeDict(meta, dataset) {
128
158
  );
129
159
  return Buffer.from(dicomDict.write({ allowInvalidVRLength: true }));
130
160
  }
131
- function buildValidCtBuffer(uid, tags, transferSyntax) {
132
- const dataset = buildBaseCtDataset(uid);
161
+ function buildValidImageBuffer(uid, modality, tags, transferSyntax) {
162
+ const dataset = buildBaseImageDataset(uid, modality);
133
163
  applyTagOverrides(dataset, tags);
134
- return serializeDict(buildMeta(uid, transferSyntax), dataset);
164
+ return serializeDict(buildMeta(uid, modality, transferSyntax), dataset);
135
165
  }
136
- function buildInvalidUidCtBuffer(uid, tags, transferSyntax) {
166
+ function buildInvalidUidImageBuffer(uid, modality, tags, transferSyntax) {
137
167
  const invalidUid = {
138
168
  study: `2.25.invalid.study.${uid.study.slice(-6)}`,
139
169
  series: `2.25.invalid.series.${uid.series.slice(-6)}`,
140
170
  sop: `2.25.invalid.sop.${uid.sop.slice(-6)}`
141
171
  };
142
- const dataset = buildBaseCtDataset(invalidUid);
172
+ const dataset = buildBaseImageDataset(invalidUid, modality);
143
173
  applyTagOverrides(dataset, tags);
144
- return serializeDict(buildMeta(invalidUid, transferSyntax), dataset);
174
+ return serializeDict(buildMeta(invalidUid, modality, transferSyntax), dataset);
145
175
  }
146
- function buildVendorCtBuffer(uid, tags, transferSyntax) {
147
- const dataset = buildBaseCtDataset(uid);
176
+ function buildVendorWarningsImageBuffer(uid, modality, tags, transferSyntax) {
177
+ const dataset = buildBaseImageDataset(uid, modality);
148
178
  dataset.Laterality = "";
149
179
  dataset.PatientWeight = "0";
150
180
  applyTagOverrides(dataset, tags);
151
- return serializeDict(buildMeta(uid, transferSyntax), dataset);
181
+ return serializeDict(buildMeta(uid, modality, transferSyntax), dataset);
152
182
  }
153
- function buildLargeCtBuffer(uid, rows, columns, frames, tags, transferSyntax) {
154
- const dataset = buildBaseCtDataset(uid);
183
+ function buildLargeImageBuffer(uid, modality, rows, columns, frames, tags, transferSyntax) {
184
+ const dataset = buildBaseImageDataset(uid, modality);
155
185
  dataset.Rows = rows;
156
186
  dataset.Columns = columns;
157
187
  dataset.NumberOfFrames = frames;
158
188
  dataset.PixelData = new Uint8Array(rows * columns * frames * 2).buffer;
159
189
  applyTagOverrides(dataset, tags);
160
- return serializeDict(buildMeta(uid, transferSyntax), dataset);
190
+ return serializeDict(buildMeta(uid, modality, transferSyntax), dataset);
161
191
  }
162
192
  function buildFakeSignatureBuffer() {
163
193
  const buf = Buffer.alloc(200, 0);
@@ -169,15 +199,31 @@ function buildNonDicomBuffer(content = "not dicom") {
169
199
  }
170
200
  function buildBufferForSpec(spec, uid) {
171
201
  switch (spec.type) {
172
- case "valid-ct":
173
- return buildValidCtBuffer(uid, spec.tags, spec.transferSyntax);
174
- case "invalid-uid-ct":
175
- return buildInvalidUidCtBuffer(uid, spec.tags, spec.transferSyntax);
176
- case "vendor-warnings-ct":
177
- return buildVendorCtBuffer(uid, spec.tags, spec.transferSyntax);
178
- case "large-ct":
179
- return buildLargeCtBuffer(
202
+ case "valid-image":
203
+ return buildValidImageBuffer(
204
+ uid,
205
+ spec.modality ?? "CT",
206
+ spec.tags,
207
+ spec.transferSyntax
208
+ );
209
+ case "invalid-uid-image":
210
+ return buildInvalidUidImageBuffer(
211
+ uid,
212
+ spec.modality ?? "CT",
213
+ spec.tags,
214
+ spec.transferSyntax
215
+ );
216
+ case "vendor-warnings-image":
217
+ return buildVendorWarningsImageBuffer(
218
+ uid,
219
+ spec.modality ?? "CT",
220
+ spec.tags,
221
+ spec.transferSyntax
222
+ );
223
+ case "large-image":
224
+ return buildLargeImageBuffer(
180
225
  uid,
226
+ spec.modality ?? "CT",
181
227
  spec.rows,
182
228
  spec.columns,
183
229
  spec.frames ?? 1,
package/dist/esm/index.js CHANGED
@@ -46,13 +46,19 @@ function buildDicomdirBuffer() {
46
46
 
47
47
  // src/schema/validate.ts
48
48
  var TYPE_CAPABILITIES = {
49
- "valid-ct": { tags: true, violations: true, transferSyntax: true },
50
- "invalid-uid-ct": { tags: true, violations: true, transferSyntax: true },
51
- "vendor-warnings-ct": { tags: true, violations: true, transferSyntax: true },
52
- "large-ct": { tags: true, violations: true, transferSyntax: true },
53
- "fake-signature": { tags: false, violations: false, transferSyntax: false },
54
- "non-dicom": { tags: false, violations: false, transferSyntax: false },
55
- dicomdir: { tags: false, violations: false, transferSyntax: false }
49
+ "valid-image": { image: true },
50
+ "invalid-uid-image": { image: true },
51
+ "vendor-warnings-image": { image: true },
52
+ "large-image": { image: true },
53
+ "fake-signature": { image: false },
54
+ "non-dicom": { image: false },
55
+ dicomdir: { image: false }
56
+ };
57
+ var VALID_MODALITIES = {
58
+ CT: true,
59
+ PT: true,
60
+ MR: true,
61
+ CR: true
56
62
  };
57
63
  var VALID_LAYOUTS = {
58
64
  flat: true,
@@ -100,22 +106,29 @@ function validateEntry(entry, path) {
100
106
  const type = e.type;
101
107
  const caps = TYPE_CAPABILITIES[type];
102
108
  if ("count" in e) validatePositiveInt(e.count, `${path}.count`);
109
+ for (const field of [
110
+ "modality",
111
+ "tags",
112
+ "violations",
113
+ "transferSyntax"
114
+ ]) {
115
+ if (!caps.image && field in e)
116
+ throw new Error(`${path}.${field}: not supported for type "${type}"`);
117
+ }
103
118
  if ("transferSyntax" in e) {
104
- if (!caps.transferSyntax) {
119
+ if (!Object.hasOwn(VALID_TRANSFER_SYNTAXES, e.transferSyntax)) {
105
120
  throw new Error(
106
- `${path}.transferSyntax: not supported for type "${type}"`
121
+ `${path}.transferSyntax: must be one of ${Object.keys(VALID_TRANSFER_SYNTAXES).join(", ")}`
107
122
  );
108
123
  }
109
- if (!Object.hasOwn(VALID_TRANSFER_SYNTAXES, e.transferSyntax)) {
124
+ }
125
+ if ("modality" in e) {
126
+ if (typeof e.modality !== "string" || !Object.hasOwn(VALID_MODALITIES, e.modality)) {
110
127
  throw new Error(
111
- `${path}.transferSyntax: must be one of ${Object.keys(VALID_TRANSFER_SYNTAXES).join(", ")}`
128
+ `${path}.modality: must be one of ${Object.keys(VALID_MODALITIES).join(", ")}; got ${JSON.stringify(e.modality)}`
112
129
  );
113
130
  }
114
131
  }
115
- if (!caps.tags && "tags" in e)
116
- throw new Error(`${path}.tags: not supported for type "${type}"`);
117
- if (!caps.violations && "violations" in e)
118
- throw new Error(`${path}.violations: not supported for type "${type}"`);
119
132
  if ("tags" in e) validateTags(e.tags, `${path}.tags`);
120
133
  if ("violations" in e) {
121
134
  if (!Array.isArray(e.violations)) {
@@ -129,7 +142,7 @@ function validateEntry(entry, path) {
129
142
  }
130
143
  }
131
144
  }
132
- if (type === "large-ct") {
145
+ if (type === "large-image") {
133
146
  validatePositiveInt(e.rows, `${path}.rows`);
134
147
  validatePositiveInt(e.columns, `${path}.columns`);
135
148
  if ("frames" in e) validatePositiveInt(e.frames, `${path}.frames`);
@@ -162,9 +175,9 @@ function validateSeries(series, path) {
162
175
  }
163
176
  for (let i = 0; i < s.entries.length; i++) {
164
177
  const e = validateEntry(s.entries[i], `${path}.entries[${i}]`);
165
- if (!TYPE_CAPABILITIES[e.type].tags) {
178
+ if (!TYPE_CAPABILITIES[e.type].image) {
166
179
  throw new Error(
167
- `${path}.entries[${i}].type: "${e.type}" cannot be used inside a series \u2014 only types that support tags can be grouped`
180
+ `${path}.entries[${i}].type: "${e.type}" cannot be used inside a series \u2014 only image-generating types can be grouped`
168
181
  );
169
182
  }
170
183
  }
@@ -233,7 +246,35 @@ function validateDatasetSpec(raw) {
233
246
  }
234
247
 
235
248
  // src/syntheticFixtures/generator.ts
236
- var CT_SOP_CLASS = "1.2.840.10008.5.1.4.1.1.2";
249
+ var MODALITY_PRESETS = {
250
+ CT: {
251
+ sopClassUid: "1.2.840.10008.5.1.4.1.1.2",
252
+ // CT Image Storage
253
+ attributes: {}
254
+ },
255
+ PT: {
256
+ sopClassUid: "1.2.840.10008.5.1.4.1.1.128",
257
+ // PET Image Storage
258
+ attributes: {
259
+ Units: "BQML",
260
+ DecayCorrection: "NONE",
261
+ CorrectedImage: ["ATTN", "DECY"]
262
+ }
263
+ },
264
+ MR: {
265
+ sopClassUid: "1.2.840.10008.5.1.4.1.1.4",
266
+ // MR Image Storage
267
+ attributes: {
268
+ ScanningSequence: "SE",
269
+ SequenceVariant: "NONE"
270
+ }
271
+ },
272
+ CR: {
273
+ sopClassUid: "1.2.840.10008.5.1.4.1.1.1",
274
+ // CR Image Storage
275
+ attributes: {}
276
+ }
277
+ };
237
278
  var TRANSFER_SYNTAX_UID = {
238
279
  "explicit-vr-little-endian": "1.2.840.10008.1.2.1",
239
280
  "implicit-vr-little-endian": "1.2.840.10008.1.2"
@@ -266,22 +307,23 @@ function applyTagOverrides(dataset, tags) {
266
307
  }
267
308
  }
268
309
  }
269
- function buildMeta(uid, transferSyntax = "explicit-vr-little-endian") {
310
+ function buildMeta(uid, modality, transferSyntax = "explicit-vr-little-endian") {
270
311
  return {
271
312
  FileMetaInformationVersion: new Uint8Array([0, 1]).buffer,
272
- MediaStorageSOPClassUID: CT_SOP_CLASS,
313
+ MediaStorageSOPClassUID: MODALITY_PRESETS[modality].sopClassUid,
273
314
  MediaStorageSOPInstanceUID: uid.sop,
274
315
  TransferSyntaxUID: TRANSFER_SYNTAX_UID[transferSyntax],
275
316
  ImplementationClassUID: "1.2.3.4",
276
317
  ImplementationVersionName: "SYNTH"
277
318
  };
278
319
  }
279
- function buildBaseCtDataset(uid) {
320
+ function buildBaseImageDataset(uid, modality) {
321
+ const preset = MODALITY_PRESETS[modality];
280
322
  return {
281
323
  PatientName: "SYNTH^SUBJECT",
282
324
  PatientID: "SYNTH_PID",
283
- Modality: "CT",
284
- SOPClassUID: CT_SOP_CLASS,
325
+ Modality: modality,
326
+ SOPClassUID: preset.sopClassUid,
285
327
  SOPInstanceUID: uid.sop,
286
328
  SeriesInstanceUID: uid.series,
287
329
  StudyInstanceUID: uid.study,
@@ -298,7 +340,8 @@ function buildBaseCtDataset(uid) {
298
340
  PixelData: new Uint8Array([0, 0]).buffer,
299
341
  // Declares PixelData's VR explicitly — without it dcmjs logs
300
342
  // "No value representation given for PixelData" and falls back to OW anyway.
301
- _vrMap: { PixelData: "OW" }
343
+ _vrMap: { PixelData: "OW" },
344
+ ...preset.attributes
302
345
  };
303
346
  }
304
347
  function serializeDict(meta, dataset) {
@@ -312,36 +355,36 @@ function serializeDict(meta, dataset) {
312
355
  );
313
356
  return Buffer.from(dicomDict.write({ allowInvalidVRLength: true }));
314
357
  }
315
- function buildValidCtBuffer(uid, tags, transferSyntax) {
316
- const dataset = buildBaseCtDataset(uid);
358
+ function buildValidImageBuffer(uid, modality, tags, transferSyntax) {
359
+ const dataset = buildBaseImageDataset(uid, modality);
317
360
  applyTagOverrides(dataset, tags);
318
- return serializeDict(buildMeta(uid, transferSyntax), dataset);
361
+ return serializeDict(buildMeta(uid, modality, transferSyntax), dataset);
319
362
  }
320
- function buildInvalidUidCtBuffer(uid, tags, transferSyntax) {
363
+ function buildInvalidUidImageBuffer(uid, modality, tags, transferSyntax) {
321
364
  const invalidUid = {
322
365
  study: `2.25.invalid.study.${uid.study.slice(-6)}`,
323
366
  series: `2.25.invalid.series.${uid.series.slice(-6)}`,
324
367
  sop: `2.25.invalid.sop.${uid.sop.slice(-6)}`
325
368
  };
326
- const dataset = buildBaseCtDataset(invalidUid);
369
+ const dataset = buildBaseImageDataset(invalidUid, modality);
327
370
  applyTagOverrides(dataset, tags);
328
- return serializeDict(buildMeta(invalidUid, transferSyntax), dataset);
371
+ return serializeDict(buildMeta(invalidUid, modality, transferSyntax), dataset);
329
372
  }
330
- function buildVendorCtBuffer(uid, tags, transferSyntax) {
331
- const dataset = buildBaseCtDataset(uid);
373
+ function buildVendorWarningsImageBuffer(uid, modality, tags, transferSyntax) {
374
+ const dataset = buildBaseImageDataset(uid, modality);
332
375
  dataset.Laterality = "";
333
376
  dataset.PatientWeight = "0";
334
377
  applyTagOverrides(dataset, tags);
335
- return serializeDict(buildMeta(uid, transferSyntax), dataset);
378
+ return serializeDict(buildMeta(uid, modality, transferSyntax), dataset);
336
379
  }
337
- function buildLargeCtBuffer(uid, rows, columns, frames, tags, transferSyntax) {
338
- const dataset = buildBaseCtDataset(uid);
380
+ function buildLargeImageBuffer(uid, modality, rows, columns, frames, tags, transferSyntax) {
381
+ const dataset = buildBaseImageDataset(uid, modality);
339
382
  dataset.Rows = rows;
340
383
  dataset.Columns = columns;
341
384
  dataset.NumberOfFrames = frames;
342
385
  dataset.PixelData = new Uint8Array(rows * columns * frames * 2).buffer;
343
386
  applyTagOverrides(dataset, tags);
344
- return serializeDict(buildMeta(uid, transferSyntax), dataset);
387
+ return serializeDict(buildMeta(uid, modality, transferSyntax), dataset);
345
388
  }
346
389
  function buildFakeSignatureBuffer() {
347
390
  const buf = Buffer.alloc(200, 0);
@@ -353,15 +396,31 @@ function buildNonDicomBuffer(content = "not dicom") {
353
396
  }
354
397
  function buildBufferForSpec(spec, uid) {
355
398
  switch (spec.type) {
356
- case "valid-ct":
357
- return buildValidCtBuffer(uid, spec.tags, spec.transferSyntax);
358
- case "invalid-uid-ct":
359
- return buildInvalidUidCtBuffer(uid, spec.tags, spec.transferSyntax);
360
- case "vendor-warnings-ct":
361
- return buildVendorCtBuffer(uid, spec.tags, spec.transferSyntax);
362
- case "large-ct":
363
- return buildLargeCtBuffer(
399
+ case "valid-image":
400
+ return buildValidImageBuffer(
401
+ uid,
402
+ spec.modality ?? "CT",
403
+ spec.tags,
404
+ spec.transferSyntax
405
+ );
406
+ case "invalid-uid-image":
407
+ return buildInvalidUidImageBuffer(
408
+ uid,
409
+ spec.modality ?? "CT",
410
+ spec.tags,
411
+ spec.transferSyntax
412
+ );
413
+ case "vendor-warnings-image":
414
+ return buildVendorWarningsImageBuffer(
415
+ uid,
416
+ spec.modality ?? "CT",
417
+ spec.tags,
418
+ spec.transferSyntax
419
+ );
420
+ case "large-image":
421
+ return buildLargeImageBuffer(
364
422
  uid,
423
+ spec.modality ?? "CT",
365
424
  spec.rows,
366
425
  spec.columns,
367
426
  spec.frames ?? 1,
@@ -1,12 +1,18 @@
1
1
  // src/schema/validate.ts
2
2
  var TYPE_CAPABILITIES = {
3
- "valid-ct": { tags: true, violations: true, transferSyntax: true },
4
- "invalid-uid-ct": { tags: true, violations: true, transferSyntax: true },
5
- "vendor-warnings-ct": { tags: true, violations: true, transferSyntax: true },
6
- "large-ct": { tags: true, violations: true, transferSyntax: true },
7
- "fake-signature": { tags: false, violations: false, transferSyntax: false },
8
- "non-dicom": { tags: false, violations: false, transferSyntax: false },
9
- dicomdir: { tags: false, violations: false, transferSyntax: false }
3
+ "valid-image": { image: true },
4
+ "invalid-uid-image": { image: true },
5
+ "vendor-warnings-image": { image: true },
6
+ "large-image": { image: true },
7
+ "fake-signature": { image: false },
8
+ "non-dicom": { image: false },
9
+ dicomdir: { image: false }
10
+ };
11
+ var VALID_MODALITIES = {
12
+ CT: true,
13
+ PT: true,
14
+ MR: true,
15
+ CR: true
10
16
  };
11
17
  var VALID_LAYOUTS = {
12
18
  flat: true,
@@ -54,22 +60,29 @@ function validateEntry(entry, path) {
54
60
  const type = e.type;
55
61
  const caps = TYPE_CAPABILITIES[type];
56
62
  if ("count" in e) validatePositiveInt(e.count, `${path}.count`);
63
+ for (const field of [
64
+ "modality",
65
+ "tags",
66
+ "violations",
67
+ "transferSyntax"
68
+ ]) {
69
+ if (!caps.image && field in e)
70
+ throw new Error(`${path}.${field}: not supported for type "${type}"`);
71
+ }
57
72
  if ("transferSyntax" in e) {
58
- if (!caps.transferSyntax) {
73
+ if (!Object.hasOwn(VALID_TRANSFER_SYNTAXES, e.transferSyntax)) {
59
74
  throw new Error(
60
- `${path}.transferSyntax: not supported for type "${type}"`
75
+ `${path}.transferSyntax: must be one of ${Object.keys(VALID_TRANSFER_SYNTAXES).join(", ")}`
61
76
  );
62
77
  }
63
- if (!Object.hasOwn(VALID_TRANSFER_SYNTAXES, e.transferSyntax)) {
78
+ }
79
+ if ("modality" in e) {
80
+ if (typeof e.modality !== "string" || !Object.hasOwn(VALID_MODALITIES, e.modality)) {
64
81
  throw new Error(
65
- `${path}.transferSyntax: must be one of ${Object.keys(VALID_TRANSFER_SYNTAXES).join(", ")}`
82
+ `${path}.modality: must be one of ${Object.keys(VALID_MODALITIES).join(", ")}; got ${JSON.stringify(e.modality)}`
66
83
  );
67
84
  }
68
85
  }
69
- if (!caps.tags && "tags" in e)
70
- throw new Error(`${path}.tags: not supported for type "${type}"`);
71
- if (!caps.violations && "violations" in e)
72
- throw new Error(`${path}.violations: not supported for type "${type}"`);
73
86
  if ("tags" in e) validateTags(e.tags, `${path}.tags`);
74
87
  if ("violations" in e) {
75
88
  if (!Array.isArray(e.violations)) {
@@ -83,7 +96,7 @@ function validateEntry(entry, path) {
83
96
  }
84
97
  }
85
98
  }
86
- if (type === "large-ct") {
99
+ if (type === "large-image") {
87
100
  validatePositiveInt(e.rows, `${path}.rows`);
88
101
  validatePositiveInt(e.columns, `${path}.columns`);
89
102
  if ("frames" in e) validatePositiveInt(e.frames, `${path}.frames`);
@@ -116,9 +129,9 @@ function validateSeries(series, path) {
116
129
  }
117
130
  for (let i = 0; i < s.entries.length; i++) {
118
131
  const e = validateEntry(s.entries[i], `${path}.entries[${i}]`);
119
- if (!TYPE_CAPABILITIES[e.type].tags) {
132
+ if (!TYPE_CAPABILITIES[e.type].image) {
120
133
  throw new Error(
121
- `${path}.entries[${i}].type: "${e.type}" cannot be used inside a series \u2014 only types that support tags can be grouped`
134
+ `${path}.entries[${i}].type: "${e.type}" cannot be used inside a series \u2014 only image-generating types can be grouped`
122
135
  );
123
136
  }
124
137
  }
@@ -44,7 +44,35 @@ var MAX_PIXEL_BYTES = 512 * 1024 * 1024;
44
44
  var HEX_TAG_RE = /^[0-9a-fA-F]{8}$/;
45
45
 
46
46
  // src/syntheticFixtures/generator.ts
47
- var CT_SOP_CLASS = "1.2.840.10008.5.1.4.1.1.2";
47
+ var MODALITY_PRESETS = {
48
+ CT: {
49
+ sopClassUid: "1.2.840.10008.5.1.4.1.1.2",
50
+ // CT Image Storage
51
+ attributes: {}
52
+ },
53
+ PT: {
54
+ sopClassUid: "1.2.840.10008.5.1.4.1.1.128",
55
+ // PET Image Storage
56
+ attributes: {
57
+ Units: "BQML",
58
+ DecayCorrection: "NONE",
59
+ CorrectedImage: ["ATTN", "DECY"]
60
+ }
61
+ },
62
+ MR: {
63
+ sopClassUid: "1.2.840.10008.5.1.4.1.1.4",
64
+ // MR Image Storage
65
+ attributes: {
66
+ ScanningSequence: "SE",
67
+ SequenceVariant: "NONE"
68
+ }
69
+ },
70
+ CR: {
71
+ sopClassUid: "1.2.840.10008.5.1.4.1.1.1",
72
+ // CR Image Storage
73
+ attributes: {}
74
+ }
75
+ };
48
76
  var TRANSFER_SYNTAX_UID = {
49
77
  "explicit-vr-little-endian": "1.2.840.10008.1.2.1",
50
78
  "implicit-vr-little-endian": "1.2.840.10008.1.2"
@@ -77,22 +105,23 @@ function applyTagOverrides(dataset, tags) {
77
105
  }
78
106
  }
79
107
  }
80
- function buildMeta(uid, transferSyntax = "explicit-vr-little-endian") {
108
+ function buildMeta(uid, modality, transferSyntax = "explicit-vr-little-endian") {
81
109
  return {
82
110
  FileMetaInformationVersion: new Uint8Array([0, 1]).buffer,
83
- MediaStorageSOPClassUID: CT_SOP_CLASS,
111
+ MediaStorageSOPClassUID: MODALITY_PRESETS[modality].sopClassUid,
84
112
  MediaStorageSOPInstanceUID: uid.sop,
85
113
  TransferSyntaxUID: TRANSFER_SYNTAX_UID[transferSyntax],
86
114
  ImplementationClassUID: "1.2.3.4",
87
115
  ImplementationVersionName: "SYNTH"
88
116
  };
89
117
  }
90
- function buildBaseCtDataset(uid) {
118
+ function buildBaseImageDataset(uid, modality) {
119
+ const preset = MODALITY_PRESETS[modality];
91
120
  return {
92
121
  PatientName: "SYNTH^SUBJECT",
93
122
  PatientID: "SYNTH_PID",
94
- Modality: "CT",
95
- SOPClassUID: CT_SOP_CLASS,
123
+ Modality: modality,
124
+ SOPClassUID: preset.sopClassUid,
96
125
  SOPInstanceUID: uid.sop,
97
126
  SeriesInstanceUID: uid.series,
98
127
  StudyInstanceUID: uid.study,
@@ -109,7 +138,8 @@ function buildBaseCtDataset(uid) {
109
138
  PixelData: new Uint8Array([0, 0]).buffer,
110
139
  // Declares PixelData's VR explicitly — without it dcmjs logs
111
140
  // "No value representation given for PixelData" and falls back to OW anyway.
112
- _vrMap: { PixelData: "OW" }
141
+ _vrMap: { PixelData: "OW" },
142
+ ...preset.attributes
113
143
  };
114
144
  }
115
145
  function serializeDict(meta, dataset) {
@@ -123,36 +153,36 @@ function serializeDict(meta, dataset) {
123
153
  );
124
154
  return Buffer.from(dicomDict.write({ allowInvalidVRLength: true }));
125
155
  }
126
- function buildValidCtBuffer(uid, tags, transferSyntax) {
127
- const dataset = buildBaseCtDataset(uid);
156
+ function buildValidImageBuffer(uid, modality, tags, transferSyntax) {
157
+ const dataset = buildBaseImageDataset(uid, modality);
128
158
  applyTagOverrides(dataset, tags);
129
- return serializeDict(buildMeta(uid, transferSyntax), dataset);
159
+ return serializeDict(buildMeta(uid, modality, transferSyntax), dataset);
130
160
  }
131
- function buildInvalidUidCtBuffer(uid, tags, transferSyntax) {
161
+ function buildInvalidUidImageBuffer(uid, modality, tags, transferSyntax) {
132
162
  const invalidUid = {
133
163
  study: `2.25.invalid.study.${uid.study.slice(-6)}`,
134
164
  series: `2.25.invalid.series.${uid.series.slice(-6)}`,
135
165
  sop: `2.25.invalid.sop.${uid.sop.slice(-6)}`
136
166
  };
137
- const dataset = buildBaseCtDataset(invalidUid);
167
+ const dataset = buildBaseImageDataset(invalidUid, modality);
138
168
  applyTagOverrides(dataset, tags);
139
- return serializeDict(buildMeta(invalidUid, transferSyntax), dataset);
169
+ return serializeDict(buildMeta(invalidUid, modality, transferSyntax), dataset);
140
170
  }
141
- function buildVendorCtBuffer(uid, tags, transferSyntax) {
142
- const dataset = buildBaseCtDataset(uid);
171
+ function buildVendorWarningsImageBuffer(uid, modality, tags, transferSyntax) {
172
+ const dataset = buildBaseImageDataset(uid, modality);
143
173
  dataset.Laterality = "";
144
174
  dataset.PatientWeight = "0";
145
175
  applyTagOverrides(dataset, tags);
146
- return serializeDict(buildMeta(uid, transferSyntax), dataset);
176
+ return serializeDict(buildMeta(uid, modality, transferSyntax), dataset);
147
177
  }
148
- function buildLargeCtBuffer(uid, rows, columns, frames, tags, transferSyntax) {
149
- const dataset = buildBaseCtDataset(uid);
178
+ function buildLargeImageBuffer(uid, modality, rows, columns, frames, tags, transferSyntax) {
179
+ const dataset = buildBaseImageDataset(uid, modality);
150
180
  dataset.Rows = rows;
151
181
  dataset.Columns = columns;
152
182
  dataset.NumberOfFrames = frames;
153
183
  dataset.PixelData = new Uint8Array(rows * columns * frames * 2).buffer;
154
184
  applyTagOverrides(dataset, tags);
155
- return serializeDict(buildMeta(uid, transferSyntax), dataset);
185
+ return serializeDict(buildMeta(uid, modality, transferSyntax), dataset);
156
186
  }
157
187
  function buildFakeSignatureBuffer() {
158
188
  const buf = Buffer.alloc(200, 0);
@@ -164,15 +194,31 @@ function buildNonDicomBuffer(content = "not dicom") {
164
194
  }
165
195
  function buildBufferForSpec(spec, uid) {
166
196
  switch (spec.type) {
167
- case "valid-ct":
168
- return buildValidCtBuffer(uid, spec.tags, spec.transferSyntax);
169
- case "invalid-uid-ct":
170
- return buildInvalidUidCtBuffer(uid, spec.tags, spec.transferSyntax);
171
- case "vendor-warnings-ct":
172
- return buildVendorCtBuffer(uid, spec.tags, spec.transferSyntax);
173
- case "large-ct":
174
- return buildLargeCtBuffer(
197
+ case "valid-image":
198
+ return buildValidImageBuffer(
199
+ uid,
200
+ spec.modality ?? "CT",
201
+ spec.tags,
202
+ spec.transferSyntax
203
+ );
204
+ case "invalid-uid-image":
205
+ return buildInvalidUidImageBuffer(
206
+ uid,
207
+ spec.modality ?? "CT",
208
+ spec.tags,
209
+ spec.transferSyntax
210
+ );
211
+ case "vendor-warnings-image":
212
+ return buildVendorWarningsImageBuffer(
213
+ uid,
214
+ spec.modality ?? "CT",
215
+ spec.tags,
216
+ spec.transferSyntax
217
+ );
218
+ case "large-image":
219
+ return buildLargeImageBuffer(
175
220
  uid,
221
+ spec.modality ?? "CT",
176
222
  spec.rows,
177
223
  spec.columns,
178
224
  spec.frames ?? 1,
@@ -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, InvalidUidCtSpec, LargeCtSpec, NonDicomSpec, SeriesEntrySpec, SeriesSpec, StudySpec, TransferSyntax, ValidCtSpec, VendorCtSpec, ViolationClass, } from './schema/types.js';
4
+ export type { DatasetLayout, DatasetSpec, DicomdirSpec, DicomTagOverrides, FakeSignatureSpec, FileSpec, ImageSpec, InvalidUidImageSpec, LargeImageSpec, Modality, NonDicomSpec, SeriesEntrySpec, SeriesSpec, StudySpec, TransferSyntax, ValidImageSpec, VendorWarningsImageSpec, ViolationClass, } from './schema/types.js';
5
5
  export { validateDatasetSpec } from './schema/validate.js';
@@ -7,22 +7,24 @@ export type DicomTagOverrides = {
7
7
  [tag: string]: unknown;
8
8
  };
9
9
  export type ViolationClass = 'uid-too-long' | 'non-conformant-uid' | 'missing-meta-header' | 'malformed-sq-delimiter' | 'vr-max-length-exceeded' | 'missing-type1-tag';
10
- type BaseCtSpec = {
10
+ export type Modality = 'CT' | 'PT' | 'MR' | 'CR';
11
+ type BaseImageSpec = {
12
+ modality?: Modality;
11
13
  tags?: DicomTagOverrides;
12
14
  transferSyntax?: TransferSyntax;
13
15
  violations?: ViolationClass[];
14
16
  };
15
- export type ValidCtSpec = BaseCtSpec & {
16
- type: 'valid-ct';
17
+ export type ValidImageSpec = BaseImageSpec & {
18
+ type: 'valid-image';
17
19
  };
18
- export type InvalidUidCtSpec = BaseCtSpec & {
19
- type: 'invalid-uid-ct';
20
+ export type InvalidUidImageSpec = BaseImageSpec & {
21
+ type: 'invalid-uid-image';
20
22
  };
21
- export type VendorCtSpec = BaseCtSpec & {
22
- type: 'vendor-warnings-ct';
23
+ export type VendorWarningsImageSpec = BaseImageSpec & {
24
+ type: 'vendor-warnings-image';
23
25
  };
24
- export type LargeCtSpec = BaseCtSpec & {
25
- type: 'large-ct';
26
+ export type LargeImageSpec = BaseImageSpec & {
27
+ type: 'large-image';
26
28
  rows: number;
27
29
  columns: number;
28
30
  frames?: number;
@@ -37,7 +39,7 @@ export type NonDicomSpec = {
37
39
  export type DicomdirSpec = {
38
40
  type: 'dicomdir';
39
41
  };
40
- export type ImageSpec = ValidCtSpec | InvalidUidCtSpec | VendorCtSpec | LargeCtSpec;
42
+ export type ImageSpec = ValidImageSpec | InvalidUidImageSpec | VendorWarningsImageSpec | LargeImageSpec;
41
43
  export type FileSpec = ImageSpec | FakeSignatureSpec | NonDicomSpec | DicomdirSpec;
42
44
  export type EntrySpec = FileSpec & {
43
45
  count?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dicom-synth",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Toolkit for synthetic DICOM fixtures and public fixture fetch/cache.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",