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 +115 -39
- package/dist/esm/collection/writer.js +73 -27
- package/dist/esm/index.js +104 -45
- package/dist/esm/schema/validate.js +31 -18
- package/dist/esm/syntheticFixtures/generator.js +73 -27
- package/dist/types/index.d.ts +1 -1
- package/dist/types/schema/types.d.ts +12 -10
- package/package.json +1 -1
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
59
|
-
{ type: 'invalid-uid-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
167
|
-
|
|
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-
|
|
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-
|
|
188
|
-
| `invalid-uid-
|
|
189
|
-
| `vendor-warnings-
|
|
190
|
-
| `large-
|
|
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-
|
|
199
|
+
**`large-image` fields:**
|
|
196
200
|
|
|
197
201
|
```json
|
|
198
|
-
{ "type": "large-
|
|
202
|
+
{ "type": "large-image", "rows": 512, "columns": 512, "frames": 100 }
|
|
199
203
|
```
|
|
200
204
|
|
|
201
|
-
`frames` defaults to 1. A 512×512×100
|
|
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-
|
|
279
|
+
Available on `valid-image`, `invalid-uid-image`, `vendor-warnings-image`, and `large-image`.
|
|
214
280
|
|
|
215
281
|
```json
|
|
216
282
|
{
|
|
217
|
-
"type": "valid-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
281
|
-
{ "type": "invalid-uid-
|
|
282
|
-
{ "type": "vendor-warnings-
|
|
283
|
-
{ "type": "large-
|
|
284
|
-
{ "type": "valid-
|
|
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
|
-
"
|
|
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-
|
|
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-
|
|
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-
|
|
314
|
-
# invalid-uid-
|
|
315
|
-
# vendor-warnings-
|
|
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-
|
|
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
|
-
- **
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
100
|
-
SOPClassUID:
|
|
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
|
|
132
|
-
const dataset =
|
|
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
|
|
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 =
|
|
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
|
|
147
|
-
const dataset =
|
|
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
|
|
154
|
-
const dataset =
|
|
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-
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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-
|
|
50
|
-
"invalid-uid-
|
|
51
|
-
"vendor-warnings-
|
|
52
|
-
"large-
|
|
53
|
-
"fake-signature": {
|
|
54
|
-
"non-dicom": {
|
|
55
|
-
dicomdir: {
|
|
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 (!
|
|
119
|
+
if (!Object.hasOwn(VALID_TRANSFER_SYNTAXES, e.transferSyntax)) {
|
|
105
120
|
throw new Error(
|
|
106
|
-
`${path}.transferSyntax:
|
|
121
|
+
`${path}.transferSyntax: must be one of ${Object.keys(VALID_TRANSFER_SYNTAXES).join(", ")}`
|
|
107
122
|
);
|
|
108
123
|
}
|
|
109
|
-
|
|
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}.
|
|
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-
|
|
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].
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
284
|
-
SOPClassUID:
|
|
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
|
|
316
|
-
const dataset =
|
|
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
|
|
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 =
|
|
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
|
|
331
|
-
const dataset =
|
|
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
|
|
338
|
-
const dataset =
|
|
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-
|
|
357
|
-
return
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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-
|
|
4
|
-
"invalid-uid-
|
|
5
|
-
"vendor-warnings-
|
|
6
|
-
"large-
|
|
7
|
-
"fake-signature": {
|
|
8
|
-
"non-dicom": {
|
|
9
|
-
dicomdir: {
|
|
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 (!
|
|
73
|
+
if (!Object.hasOwn(VALID_TRANSFER_SYNTAXES, e.transferSyntax)) {
|
|
59
74
|
throw new Error(
|
|
60
|
-
`${path}.transferSyntax:
|
|
75
|
+
`${path}.transferSyntax: must be one of ${Object.keys(VALID_TRANSFER_SYNTAXES).join(", ")}`
|
|
61
76
|
);
|
|
62
77
|
}
|
|
63
|
-
|
|
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}.
|
|
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-
|
|
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].
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
95
|
-
SOPClassUID:
|
|
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
|
|
127
|
-
const dataset =
|
|
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
|
|
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 =
|
|
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
|
|
142
|
-
const dataset =
|
|
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
|
|
149
|
-
const dataset =
|
|
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-
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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,
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { type CollectionManifest, type GeneratedFile, generateCollectionFromSpec, generateFile, writeCollectionFromSpec, } from './collection/writer.js';
|
|
2
2
|
export { defaultPublicCasesPath, loadCaseById, loadCasesFromJson, loadDefaultCases, type PublicCaseRecord, type PublicCaseSource, } from './public-fixtures/catalog.js';
|
|
3
3
|
export { caseCachePath, fetchPublicCaseToCache, verifySha256, } from './public-fixtures/fetch.js';
|
|
4
|
-
export type { DatasetLayout, DatasetSpec, DicomdirSpec, DicomTagOverrides, FakeSignatureSpec, FileSpec, ImageSpec,
|
|
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
|
|
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
|
|
16
|
-
type: 'valid-
|
|
17
|
+
export type ValidImageSpec = BaseImageSpec & {
|
|
18
|
+
type: 'valid-image';
|
|
17
19
|
};
|
|
18
|
-
export type
|
|
19
|
-
type: 'invalid-uid-
|
|
20
|
+
export type InvalidUidImageSpec = BaseImageSpec & {
|
|
21
|
+
type: 'invalid-uid-image';
|
|
20
22
|
};
|
|
21
|
-
export type
|
|
22
|
-
type: 'vendor-warnings-
|
|
23
|
+
export type VendorWarningsImageSpec = BaseImageSpec & {
|
|
24
|
+
type: 'vendor-warnings-image';
|
|
23
25
|
};
|
|
24
|
-
export type
|
|
25
|
-
type: 'large-
|
|
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 =
|
|
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;
|