dicom-synth 1.1.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.
@@ -1,12 +1,22 @@
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
16
+ };
17
+ var VALID_LAYOUTS = {
18
+ flat: true,
19
+ hierarchical: true
10
20
  };
11
21
  var VALID_TRANSFER_SYNTAXES = {
12
22
  "explicit-vr-little-endian": true,
@@ -23,6 +33,13 @@ var VALID_VIOLATIONS = {
23
33
  };
24
34
  var HEX_TAG_RE = /^[0-9a-fA-F]{8}$/;
25
35
  var KEYWORD_RE = /^[A-Z][A-Za-z0-9]*$/;
36
+ function validatePositiveInt(value, path) {
37
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
38
+ throw new Error(
39
+ `${path}: must be a positive integer; got ${JSON.stringify(value)}`
40
+ );
41
+ }
42
+ }
26
43
  function validateTagKey(key, path) {
27
44
  if (!HEX_TAG_RE.test(key) && !KEYWORD_RE.test(key)) {
28
45
  throw new Error(
@@ -42,37 +59,31 @@ function validateEntry(entry, path) {
42
59
  }
43
60
  const type = e.type;
44
61
  const caps = TYPE_CAPABILITIES[type];
45
- if ("count" in e) {
46
- if (typeof e.count !== "number" || !Number.isInteger(e.count) || e.count < 1) {
47
- throw new Error(
48
- `${path}.count: must be a positive integer; got ${JSON.stringify(e.count)}`
49
- );
50
- }
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}"`);
51
71
  }
52
72
  if ("transferSyntax" in e) {
53
- if (!caps.transferSyntax) {
54
- throw new Error(
55
- `${path}.transferSyntax: not supported for type "${type}"`
56
- );
57
- }
58
73
  if (!Object.hasOwn(VALID_TRANSFER_SYNTAXES, e.transferSyntax)) {
59
74
  throw new Error(
60
75
  `${path}.transferSyntax: must be one of ${Object.keys(VALID_TRANSFER_SYNTAXES).join(", ")}`
61
76
  );
62
77
  }
63
78
  }
64
- if (!caps.tags && "tags" in e)
65
- throw new Error(`${path}.tags: not supported for type "${type}"`);
66
- if (!caps.violations && "violations" in e)
67
- throw new Error(`${path}.violations: not supported for type "${type}"`);
68
- if ("tags" in e) {
69
- if (typeof e.tags !== "object" || e.tags === null || Array.isArray(e.tags)) {
70
- throw new Error(`${path}.tags: must be an object`);
71
- }
72
- for (const key of Object.keys(e.tags)) {
73
- validateTagKey(key, `${path}.tags`);
79
+ if ("modality" in e) {
80
+ if (typeof e.modality !== "string" || !Object.hasOwn(VALID_MODALITIES, e.modality)) {
81
+ throw new Error(
82
+ `${path}.modality: must be one of ${Object.keys(VALID_MODALITIES).join(", ")}; got ${JSON.stringify(e.modality)}`
83
+ );
74
84
  }
75
85
  }
86
+ if ("tags" in e) validateTags(e.tags, `${path}.tags`);
76
87
  if ("violations" in e) {
77
88
  if (!Array.isArray(e.violations)) {
78
89
  throw new Error(`${path}.violations: must be an array`);
@@ -85,18 +96,10 @@ function validateEntry(entry, path) {
85
96
  }
86
97
  }
87
98
  }
88
- if (type === "large-ct") {
89
- if (typeof e.rows !== "number" || !Number.isInteger(e.rows) || e.rows < 1) {
90
- throw new Error(`${path}.rows: must be a positive integer`);
91
- }
92
- if (typeof e.columns !== "number" || !Number.isInteger(e.columns) || e.columns < 1) {
93
- throw new Error(`${path}.columns: must be a positive integer`);
94
- }
95
- if ("frames" in e) {
96
- if (typeof e.frames !== "number" || !Number.isInteger(e.frames) || e.frames < 1) {
97
- throw new Error(`${path}.frames: must be a positive integer`);
98
- }
99
- }
99
+ if (type === "large-image") {
100
+ validatePositiveInt(e.rows, `${path}.rows`);
101
+ validatePositiveInt(e.columns, `${path}.columns`);
102
+ if ("frames" in e) validatePositiveInt(e.frames, `${path}.frames`);
100
103
  const rows = e.rows;
101
104
  const columns = e.columns;
102
105
  const frames = typeof e.frames === "number" ? e.frames : 1;
@@ -108,20 +111,72 @@ function validateEntry(entry, path) {
108
111
  }
109
112
  return e;
110
113
  }
114
+ function validateTags(tags, path) {
115
+ if (typeof tags !== "object" || tags === null || Array.isArray(tags)) {
116
+ throw new Error(`${path}: must be an object`);
117
+ }
118
+ for (const key of Object.keys(tags)) {
119
+ validateTagKey(key, path);
120
+ }
121
+ }
122
+ function validateSeries(series, path) {
123
+ if (typeof series !== "object" || series === null || Array.isArray(series)) {
124
+ throw new Error(`${path}: must be an object`);
125
+ }
126
+ const s = series;
127
+ if (!Array.isArray(s.entries) || s.entries.length === 0) {
128
+ throw new Error(`${path}.entries: must be a non-empty array`);
129
+ }
130
+ for (let i = 0; i < s.entries.length; i++) {
131
+ const e = validateEntry(s.entries[i], `${path}.entries[${i}]`);
132
+ if (!TYPE_CAPABILITIES[e.type].image) {
133
+ throw new Error(
134
+ `${path}.entries[${i}].type: "${e.type}" cannot be used inside a series \u2014 only image-generating types can be grouped`
135
+ );
136
+ }
137
+ }
138
+ if ("tags" in s) validateTags(s.tags, `${path}.tags`);
139
+ return s;
140
+ }
141
+ function validateStudy(study, path) {
142
+ if (typeof study !== "object" || study === null || Array.isArray(study)) {
143
+ throw new Error(`${path}: must be an object`);
144
+ }
145
+ const st = study;
146
+ if (!Array.isArray(st.series) || st.series.length === 0) {
147
+ throw new Error(`${path}.series: must be a non-empty array`);
148
+ }
149
+ for (let i = 0; i < st.series.length; i++) {
150
+ validateSeries(st.series[i], `${path}.series[${i}]`);
151
+ }
152
+ if ("count" in st) validatePositiveInt(st.count, `${path}.count`);
153
+ if ("tags" in st) validateTags(st.tags, `${path}.tags`);
154
+ return st;
155
+ }
111
156
  function validateDatasetSpec(raw) {
112
157
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
113
158
  throw new Error("DatasetSpec: must be a JSON object");
114
159
  }
115
160
  const r = raw;
116
- if (!Array.isArray(r.entries)) {
161
+ if ("entries" in r && !Array.isArray(r.entries)) {
117
162
  throw new Error("DatasetSpec.entries: must be an array");
118
163
  }
119
- if (r.entries.length === 0) {
120
- throw new Error("DatasetSpec.entries: must not be empty");
164
+ if ("studies" in r && !Array.isArray(r.studies)) {
165
+ throw new Error("DatasetSpec.studies: must be an array");
166
+ }
167
+ const rawEntries = r.entries ?? [];
168
+ const rawStudies = r.studies ?? [];
169
+ if (rawEntries.length === 0 && rawStudies.length === 0) {
170
+ throw new Error(
171
+ 'DatasetSpec: must contain at least one of "entries" or "studies" (non-empty)'
172
+ );
121
173
  }
122
- const entries = r.entries.map(
174
+ const entries = rawEntries.map(
123
175
  (entry, i) => validateEntry(entry, `entries[${i}]`)
124
176
  );
177
+ const studies = rawStudies.map(
178
+ (study, i) => validateStudy(study, `studies[${i}]`)
179
+ );
125
180
  if ("seed" in r) {
126
181
  if (typeof r.seed !== "number" || !Number.isFinite(r.seed)) {
127
182
  throw new Error(
@@ -129,9 +184,18 @@ function validateDatasetSpec(raw) {
129
184
  );
130
185
  }
131
186
  }
187
+ if ("layout" in r) {
188
+ if (typeof r.layout !== "string" || !Object.hasOwn(VALID_LAYOUTS, r.layout)) {
189
+ throw new Error(
190
+ `DatasetSpec.layout: must be one of ${Object.keys(VALID_LAYOUTS).join(", ")}; got ${JSON.stringify(r.layout)}`
191
+ );
192
+ }
193
+ }
132
194
  return {
133
- entries,
134
- ...r.seed !== void 0 ? { seed: r.seed } : {}
195
+ ...entries.length > 0 ? { entries } : {},
196
+ ...studies.length > 0 ? { studies } : {},
197
+ ...r.seed !== void 0 ? { seed: r.seed } : {},
198
+ ...r.layout !== void 0 ? { layout: r.layout } : {}
135
199
  };
136
200
  }
137
201
  export {
@@ -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,
@@ -18,6 +18,33 @@ function randomUid(role) {
18
18
  function seededUid(next, role) {
19
19
  return formatUid(next(), next(), role);
20
20
  }
21
+ var GROUP_STREAM_OFFSET = 2654435769;
22
+ function seededStream(seed, offset, a, b) {
23
+ return lcg(seed * 999983 + offset + a * 1000003 + b * 7919 >>> 0);
24
+ }
25
+ function makeGroupUidGenerator(seed) {
26
+ if (seed === void 0) {
27
+ return {
28
+ study: () => randomUid("study"),
29
+ series: () => randomUid("series")
30
+ };
31
+ }
32
+ return {
33
+ study: (studyIndex) => seededUid(
34
+ seededStream(seed, GROUP_STREAM_OFFSET, studyIndex + 1, 0),
35
+ "study"
36
+ ),
37
+ series: (studyIndex, seriesIndex) => seededUid(
38
+ seededStream(
39
+ seed,
40
+ GROUP_STREAM_OFFSET,
41
+ studyIndex + 1,
42
+ seriesIndex + 1
43
+ ),
44
+ "series"
45
+ )
46
+ };
47
+ }
21
48
  function makeUidGenerator(seed) {
22
49
  if (seed === void 0) {
23
50
  return () => ({
@@ -27,7 +54,7 @@ function makeUidGenerator(seed) {
27
54
  });
28
55
  }
29
56
  return (fileIndex) => {
30
- const next = lcg(seed * 999983 + fileIndex * 1000003 >>> 0);
57
+ const next = seededStream(seed, 0, fileIndex, 0);
31
58
  return {
32
59
  study: seededUid(next, "study"),
33
60
  series: seededUid(next, "series"),
@@ -36,5 +63,6 @@ function makeUidGenerator(seed) {
36
63
  };
37
64
  }
38
65
  export {
66
+ makeGroupUidGenerator,
39
67
  makeUidGenerator
40
68
  };
@@ -1,6 +1,8 @@
1
1
  import type { DatasetSpec, FileSpec } from '../schema/types.js';
2
+ import type { UidSet } from '../syntheticFixtures/uid.js';
2
3
  export type GeneratedFile = {
3
4
  filename: string;
5
+ relativePath: string;
4
6
  buffer: Buffer;
5
7
  type: FileSpec['type'];
6
8
  index: number;
@@ -14,6 +16,7 @@ export declare function generateFile(spec: FileSpec, options?: {
14
16
  index?: number;
15
17
  seed?: number;
16
18
  padWidth?: number;
19
+ uid?: Partial<UidSet>;
17
20
  }): Promise<GeneratedFile>;
18
21
  export declare function generateCollectionFromSpec(spec: DatasetSpec): AsyncGenerator<GeneratedFile>;
19
22
  export declare function writeCollectionFromSpec(spec: DatasetSpec, outDir: string): Promise<CollectionManifest>;
@@ -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 { DatasetSpec, DicomdirSpec, DicomTagOverrides, FakeSignatureSpec, FileSpec, InvalidUidCtSpec, LargeCtSpec, NonDicomSpec, 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,12 +39,28 @@ export type NonDicomSpec = {
37
39
  export type DicomdirSpec = {
38
40
  type: 'dicomdir';
39
41
  };
40
- export type FileSpec = ValidCtSpec | InvalidUidCtSpec | VendorCtSpec | LargeCtSpec | FakeSignatureSpec | NonDicomSpec | DicomdirSpec;
42
+ export type ImageSpec = ValidImageSpec | InvalidUidImageSpec | VendorWarningsImageSpec | LargeImageSpec;
43
+ export type FileSpec = ImageSpec | FakeSignatureSpec | NonDicomSpec | DicomdirSpec;
41
44
  export type EntrySpec = FileSpec & {
42
45
  count?: number;
43
46
  };
47
+ export type SeriesEntrySpec = ImageSpec & {
48
+ count?: number;
49
+ };
50
+ export type SeriesSpec = {
51
+ entries: SeriesEntrySpec[];
52
+ tags?: DicomTagOverrides;
53
+ };
54
+ export type StudySpec = {
55
+ series: SeriesSpec[];
56
+ tags?: DicomTagOverrides;
57
+ count?: number;
58
+ };
59
+ export type DatasetLayout = 'flat' | 'hierarchical';
44
60
  export type DatasetSpec = {
45
- entries: EntrySpec[];
61
+ entries?: EntrySpec[];
62
+ studies?: StudySpec[];
46
63
  seed?: number;
64
+ layout?: DatasetLayout;
47
65
  };
48
66
  export {};
@@ -3,5 +3,10 @@ export type UidSet = {
3
3
  series: string;
4
4
  sop: string;
5
5
  };
6
+ export type GroupUidGenerator = {
7
+ study: (studyIndex: number) => string;
8
+ series: (studyIndex: number, seriesIndex: number) => string;
9
+ };
10
+ export declare function makeGroupUidGenerator(seed?: number): GroupUidGenerator;
6
11
  export type UidGenerator = (fileIndex: number) => UidSet;
7
12
  export declare function makeUidGenerator(seed?: number): UidGenerator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dicom-synth",
3
- "version": "1.1.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",