dicom-synth 1.16.0 → 1.18.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.
@@ -69,6 +69,92 @@ var VALID_VIOLATIONS = {
69
69
  };
70
70
  var HEX_TAG_RE = /^[0-9a-fA-F]{8}$/;
71
71
  var KEYWORD_RE = /^[A-Z][A-Za-z0-9]*$/;
72
+ var VALID_VRS = /* @__PURE__ */ new Set([
73
+ "AE",
74
+ "AS",
75
+ "AT",
76
+ "CS",
77
+ "DA",
78
+ "DS",
79
+ "DT",
80
+ "FL",
81
+ "FD",
82
+ "IS",
83
+ "LO",
84
+ "LT",
85
+ "OB",
86
+ "OD",
87
+ "OF",
88
+ "OL",
89
+ "OV",
90
+ "OW",
91
+ "PN",
92
+ "SH",
93
+ "SL",
94
+ "SQ",
95
+ "SS",
96
+ "ST",
97
+ "SV",
98
+ "TM",
99
+ "UC",
100
+ "UI",
101
+ "UL",
102
+ "UN",
103
+ "UR",
104
+ "US",
105
+ "UT",
106
+ "UV"
107
+ ]);
108
+ function isPrivateHexTag(key) {
109
+ return HEX_TAG_RE.test(key) && Number.parseInt(key.slice(0, 4), 16) % 2 === 1;
110
+ }
111
+ function isRawTagValue(value) {
112
+ return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.vr === "string";
113
+ }
114
+ function validateRawTagValue(raw, path) {
115
+ for (const key of Object.keys(raw)) {
116
+ if (key !== "vr" && key !== "value") {
117
+ throw new Error(
118
+ `${path}: raw tag form accepts only "vr" and "value"; got "${key}"`
119
+ );
120
+ }
121
+ }
122
+ const vr = raw.vr;
123
+ if (!VALID_VRS.has(vr)) {
124
+ throw new Error(`${path}.vr: "${vr}" is not a standard DICOM VR`);
125
+ }
126
+ if (!("value" in raw)) {
127
+ throw new Error(`${path}.value: is required in the raw tag form`);
128
+ }
129
+ if (vr === "SQ") {
130
+ if (!Array.isArray(raw.value)) {
131
+ throw new Error(`${path}.value: an SQ raw tag requires an array of items`);
132
+ }
133
+ raw.value.forEach((item, i) => {
134
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
135
+ throw new Error(
136
+ `${path}.value[${i}]: must be an object of hex-tag entries`
137
+ );
138
+ }
139
+ for (const [tag, nested] of Object.entries(item)) {
140
+ if (!HEX_TAG_RE.test(tag)) {
141
+ throw new Error(
142
+ `${path}.value[${i}]: invalid item tag "${tag}" \u2014 SQ items use 8-hex-char tags`
143
+ );
144
+ }
145
+ if (!isRawTagValue(nested)) {
146
+ throw new Error(
147
+ `${path}.value[${i}].${tag}: SQ item entries must use the raw { vr, value } form`
148
+ );
149
+ }
150
+ validateRawTagValue(
151
+ nested,
152
+ `${path}.value[${i}].${tag}`
153
+ );
154
+ }
155
+ });
156
+ }
157
+ }
72
158
  function validatePositiveInt(value, path) {
73
159
  if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
74
160
  throw new Error(
@@ -273,13 +359,33 @@ function validateEntry(entry, path) {
273
359
  return e;
274
360
  }
275
361
  var MODALITY_TAG = "00080060";
276
- function validateTagValue(key, value, path) {
362
+ function validateModalityGuard(key, candidate, path) {
277
363
  const isModalityKey = key === "Modality" || key.toUpperCase() === MODALITY_TAG;
278
- if (isModalityKey && typeof value === "string" && Object.hasOwn(VALID_MODALITIES, value)) {
364
+ if (isModalityKey && typeof candidate === "string" && Object.hasOwn(VALID_MODALITIES, candidate)) {
279
365
  throw new Error(
280
366
  `${path}.${key}: set a preset modality (${Object.keys(VALID_MODALITIES).join("/")}) via the "modality" field, not tags \u2014 it is coupled to SOPClassUID and type-1 attributes`
281
367
  );
282
368
  }
369
+ }
370
+ function validateTagValue(key, value, path) {
371
+ if (isRawTagValue(value)) {
372
+ if (!HEX_TAG_RE.test(key)) {
373
+ throw new Error(
374
+ `${path}.${key}: the raw { vr, value } form requires an 8-hex-char tag key \u2014 keyword tags take plain values`
375
+ );
376
+ }
377
+ validateRawTagValue(value, `${path}.${key}`);
378
+ const raw = value.value;
379
+ const scalar = Array.isArray(raw) && raw.length === 1 ? raw[0] : raw;
380
+ validateModalityGuard(key, scalar, path);
381
+ return;
382
+ }
383
+ if (isPrivateHexTag(key)) {
384
+ throw new Error(
385
+ `${path}.${key}: private tags have no dictionary VR \u2014 use the raw form, e.g. { "vr": "LO", "value": \u2026 }`
386
+ );
387
+ }
388
+ validateModalityGuard(key, value, path);
283
389
  if (typeof value === "string") {
284
390
  for (const name of findTemplateTokens(value)) {
285
391
  if (!TEMPLATE_VOCAB.includes(name)) {
@@ -61,6 +61,92 @@ var VALID_VIOLATIONS = {
61
61
  };
62
62
  var HEX_TAG_RE = /^[0-9a-fA-F]{8}$/;
63
63
  var KEYWORD_RE = /^[A-Z][A-Za-z0-9]*$/;
64
+ var VALID_VRS = /* @__PURE__ */ new Set([
65
+ "AE",
66
+ "AS",
67
+ "AT",
68
+ "CS",
69
+ "DA",
70
+ "DS",
71
+ "DT",
72
+ "FL",
73
+ "FD",
74
+ "IS",
75
+ "LO",
76
+ "LT",
77
+ "OB",
78
+ "OD",
79
+ "OF",
80
+ "OL",
81
+ "OV",
82
+ "OW",
83
+ "PN",
84
+ "SH",
85
+ "SL",
86
+ "SQ",
87
+ "SS",
88
+ "ST",
89
+ "SV",
90
+ "TM",
91
+ "UC",
92
+ "UI",
93
+ "UL",
94
+ "UN",
95
+ "UR",
96
+ "US",
97
+ "UT",
98
+ "UV"
99
+ ]);
100
+ function isPrivateHexTag(key) {
101
+ return HEX_TAG_RE.test(key) && Number.parseInt(key.slice(0, 4), 16) % 2 === 1;
102
+ }
103
+ function isRawTagValue(value) {
104
+ return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.vr === "string";
105
+ }
106
+ function validateRawTagValue(raw, path) {
107
+ for (const key of Object.keys(raw)) {
108
+ if (key !== "vr" && key !== "value") {
109
+ throw new Error(
110
+ `${path}: raw tag form accepts only "vr" and "value"; got "${key}"`
111
+ );
112
+ }
113
+ }
114
+ const vr = raw.vr;
115
+ if (!VALID_VRS.has(vr)) {
116
+ throw new Error(`${path}.vr: "${vr}" is not a standard DICOM VR`);
117
+ }
118
+ if (!("value" in raw)) {
119
+ throw new Error(`${path}.value: is required in the raw tag form`);
120
+ }
121
+ if (vr === "SQ") {
122
+ if (!Array.isArray(raw.value)) {
123
+ throw new Error(`${path}.value: an SQ raw tag requires an array of items`);
124
+ }
125
+ raw.value.forEach((item, i) => {
126
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
127
+ throw new Error(
128
+ `${path}.value[${i}]: must be an object of hex-tag entries`
129
+ );
130
+ }
131
+ for (const [tag, nested] of Object.entries(item)) {
132
+ if (!HEX_TAG_RE.test(tag)) {
133
+ throw new Error(
134
+ `${path}.value[${i}]: invalid item tag "${tag}" \u2014 SQ items use 8-hex-char tags`
135
+ );
136
+ }
137
+ if (!isRawTagValue(nested)) {
138
+ throw new Error(
139
+ `${path}.value[${i}].${tag}: SQ item entries must use the raw { vr, value } form`
140
+ );
141
+ }
142
+ validateRawTagValue(
143
+ nested,
144
+ `${path}.value[${i}].${tag}`
145
+ );
146
+ }
147
+ });
148
+ }
149
+ }
64
150
  function validatePositiveInt(value, path) {
65
151
  if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
66
152
  throw new Error(
@@ -265,13 +351,33 @@ function validateEntry(entry, path) {
265
351
  return e;
266
352
  }
267
353
  var MODALITY_TAG = "00080060";
268
- function validateTagValue(key, value, path) {
354
+ function validateModalityGuard(key, candidate, path) {
269
355
  const isModalityKey = key === "Modality" || key.toUpperCase() === MODALITY_TAG;
270
- if (isModalityKey && typeof value === "string" && Object.hasOwn(VALID_MODALITIES, value)) {
356
+ if (isModalityKey && typeof candidate === "string" && Object.hasOwn(VALID_MODALITIES, candidate)) {
271
357
  throw new Error(
272
358
  `${path}.${key}: set a preset modality (${Object.keys(VALID_MODALITIES).join("/")}) via the "modality" field, not tags \u2014 it is coupled to SOPClassUID and type-1 attributes`
273
359
  );
274
360
  }
361
+ }
362
+ function validateTagValue(key, value, path) {
363
+ if (isRawTagValue(value)) {
364
+ if (!HEX_TAG_RE.test(key)) {
365
+ throw new Error(
366
+ `${path}.${key}: the raw { vr, value } form requires an 8-hex-char tag key \u2014 keyword tags take plain values`
367
+ );
368
+ }
369
+ validateRawTagValue(value, `${path}.${key}`);
370
+ const raw = value.value;
371
+ const scalar = Array.isArray(raw) && raw.length === 1 ? raw[0] : raw;
372
+ validateModalityGuard(key, scalar, path);
373
+ return;
374
+ }
375
+ if (isPrivateHexTag(key)) {
376
+ throw new Error(
377
+ `${path}.${key}: private tags have no dictionary VR \u2014 use the raw form, e.g. { "vr": "LO", "value": \u2026 }`
378
+ );
379
+ }
380
+ validateModalityGuard(key, value, path);
275
381
  if (typeof value === "string") {
276
382
  for (const name of findTemplateTokens(value)) {
277
383
  if (!TEMPLATE_VOCAB.includes(name)) {
@@ -682,6 +788,8 @@ function validateParametricSpec(raw) {
682
788
  export {
683
789
  HEX_TAG_RE,
684
790
  isImageType,
791
+ isPrivateHexTag,
792
+ isRawTagValue,
685
793
  positionalName,
686
794
  validateDatasetSpec,
687
795
  validateParametricSpec
@@ -45,6 +45,9 @@ var MAX_STREAM_BYTES = 50 * 1024 * 1024 * 1024;
45
45
 
46
46
  // src/schema/validate.ts
47
47
  var HEX_TAG_RE = /^[0-9a-fA-F]{8}$/;
48
+ function isRawTagValue(value) {
49
+ return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.vr === "string";
50
+ }
48
51
 
49
52
  // src/syntheticFixtures/generator.ts
50
53
  var MODALITY_PRESETS = {
@@ -93,27 +96,50 @@ function buildTagToKeyword() {
93
96
  return map;
94
97
  }
95
98
  var tagToKeyword = buildTagToKeyword();
99
+ function toRawElement(raw) {
100
+ const value = raw.vr === "SQ" ? raw.value.map(
101
+ (item) => Object.fromEntries(
102
+ Object.entries(item).map(([tag, nested]) => [
103
+ tag.toUpperCase(),
104
+ toRawElement(nested)
105
+ ])
106
+ )
107
+ ) : raw.value;
108
+ return { vr: raw.vr, Value: Array.isArray(value) ? value : [value] };
109
+ }
96
110
  function applyTagOverrides(dataset, tags) {
97
- if (!tags) return;
111
+ if (!tags) return void 0;
112
+ let raw;
98
113
  for (const [key, value] of Object.entries(tags)) {
99
- if (HEX_TAG_RE.test(key)) {
114
+ if (isRawTagValue(value)) {
115
+ raw ?? (raw = {});
116
+ raw[key.toUpperCase()] = toRawElement(value);
117
+ } else if (HEX_TAG_RE.test(key)) {
100
118
  const keyword = tagToKeyword.get(key.toUpperCase());
101
- if (keyword) {
102
- dataset[keyword] = value;
103
- } else {
104
- console.warn(`dicom-synth: unknown hex tag "${key}" \u2014 skipping`);
119
+ if (!keyword) {
120
+ throw new Error(
121
+ `dicom-synth: unknown hex tag "${key}" \u2014 dictionary tags take plain values; private/unknown tags require the raw { vr, value } form`
122
+ );
105
123
  }
124
+ dataset[keyword] = value;
106
125
  } else {
107
126
  dataset[key] = value;
108
127
  }
109
128
  }
129
+ return raw;
110
130
  }
111
- function syncMetaWithDataset(meta, dataset) {
112
- if (typeof dataset.SOPInstanceUID === "string") {
113
- meta.MediaStorageSOPInstanceUID = dataset.SOPInstanceUID;
131
+ function rawScalarString(rawElements, tag) {
132
+ const value = rawElements?.[tag]?.Value;
133
+ return value?.length === 1 && typeof value[0] === "string" ? value[0] : void 0;
134
+ }
135
+ function syncMetaWithDataset(meta, dataset, rawElements) {
136
+ const sopInstance = rawScalarString(rawElements, "00080018") ?? (typeof dataset.SOPInstanceUID === "string" ? dataset.SOPInstanceUID : void 0);
137
+ if (sopInstance !== void 0) {
138
+ meta.MediaStorageSOPInstanceUID = sopInstance;
114
139
  }
115
- if (typeof dataset.SOPClassUID === "string") {
116
- meta.MediaStorageSOPClassUID = dataset.SOPClassUID;
140
+ const sopClass = rawScalarString(rawElements, "00080016") ?? (typeof dataset.SOPClassUID === "string" ? dataset.SOPClassUID : void 0);
141
+ if (sopClass !== void 0) {
142
+ meta.MediaStorageSOPClassUID = sopClass;
117
143
  }
118
144
  }
119
145
  function buildMeta(uid, modality, transferSyntax = "explicit-vr-little-endian") {
@@ -153,7 +179,7 @@ function buildBaseImageDataset(uid, modality) {
153
179
  ...preset.attributes
154
180
  };
155
181
  }
156
- function serializeDict(meta, dataset) {
182
+ function serializeDict(meta, dataset, rawElements) {
157
183
  const dicomDict = new dcmjs.data.DicomDict(
158
184
  dcmjs.data.DicomMetaDictionary.denaturalizeDataset(
159
185
  meta
@@ -162,14 +188,17 @@ function serializeDict(meta, dataset) {
162
188
  dicomDict.dict = dcmjs.data.DicomMetaDictionary.denaturalizeDataset(
163
189
  dataset
164
190
  );
191
+ if (rawElements) {
192
+ Object.assign(dicomDict.dict, rawElements);
193
+ }
165
194
  return Buffer.from(dicomDict.write({ allowInvalidVRLength: true }));
166
195
  }
167
196
  var MAX_DIMENSION = 65535;
168
- function applySizing(dataset, meta, spec) {
197
+ function applySizing(dataset, meta, spec, rawElements) {
169
198
  if (spec.targetSizeKb !== void 0) {
170
199
  const clampDim = (n) => Math.min(MAX_DIMENSION, Math.max(1, Math.round(n)));
171
200
  const minimalPixelBytes = dataset.PixelData.byteLength;
172
- const overhead = serializeDict(meta, dataset).length - minimalPixelBytes;
201
+ const overhead = serializeDict(meta, dataset, rawElements).length - minimalPixelBytes;
173
202
  const cells = Math.max(
174
203
  1,
175
204
  Math.round((spec.targetSizeKb * 1024 - overhead) / 2)
@@ -200,11 +229,11 @@ function buildImageBuffer(spec, uid) {
200
229
  dataset.Laterality = "";
201
230
  dataset.PatientWeight = "0";
202
231
  }
203
- applyTagOverrides(dataset, spec.tags);
232
+ const rawElements = applyTagOverrides(dataset, spec.tags);
204
233
  const meta = buildMeta(effectiveUid, modality, spec.transferSyntax);
205
- syncMetaWithDataset(meta, dataset);
206
- applySizing(dataset, meta, spec);
207
- return serializeDict(meta, dataset);
234
+ syncMetaWithDataset(meta, dataset, rawElements);
235
+ applySizing(dataset, meta, spec, rawElements);
236
+ return { buffer: serializeDict(meta, dataset, rawElements), rawElements };
208
237
  }
209
238
  function buildFakeSignatureBuffer() {
210
239
  const buf = Buffer.alloc(200, 0);
@@ -226,11 +255,11 @@ function buildBufferForSpec(spec, uid) {
226
255
  case "vendor-warnings-image":
227
256
  return buildImageBuffer(spec, uid);
228
257
  case "fake-signature":
229
- return buildFakeSignatureBuffer();
258
+ return { buffer: buildFakeSignatureBuffer() };
230
259
  case "non-dicom":
231
- return buildNonDicomBuffer(spec);
260
+ return { buffer: buildNonDicomBuffer(spec) };
232
261
  case "dicomdir":
233
- return buildDicomdirBuffer();
262
+ return { buffer: buildDicomdirBuffer() };
234
263
  case "large-image":
235
264
  throw new Error(
236
265
  "large-image cannot be generated in memory \u2014 use writeCollectionFromSpec (disk-only streaming)"
@@ -31,6 +31,9 @@ import { resolve } from "node:path";
31
31
 
32
32
  // src/schema/validate.ts
33
33
  var HEX_TAG_RE = /^[0-9a-fA-F]{8}$/;
34
+ function isRawTagValue(value) {
35
+ return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.vr === "string";
36
+ }
34
37
 
35
38
  // src/syntheticFixtures/generator.ts
36
39
  var MODALITY_PRESETS = {
@@ -79,27 +82,50 @@ function buildTagToKeyword() {
79
82
  return map;
80
83
  }
81
84
  var tagToKeyword = buildTagToKeyword();
85
+ function toRawElement(raw) {
86
+ const value = raw.vr === "SQ" ? raw.value.map(
87
+ (item) => Object.fromEntries(
88
+ Object.entries(item).map(([tag, nested]) => [
89
+ tag.toUpperCase(),
90
+ toRawElement(nested)
91
+ ])
92
+ )
93
+ ) : raw.value;
94
+ return { vr: raw.vr, Value: Array.isArray(value) ? value : [value] };
95
+ }
82
96
  function applyTagOverrides(dataset, tags) {
83
- if (!tags) return;
97
+ if (!tags) return void 0;
98
+ let raw;
84
99
  for (const [key, value] of Object.entries(tags)) {
85
- if (HEX_TAG_RE.test(key)) {
100
+ if (isRawTagValue(value)) {
101
+ raw ?? (raw = {});
102
+ raw[key.toUpperCase()] = toRawElement(value);
103
+ } else if (HEX_TAG_RE.test(key)) {
86
104
  const keyword = tagToKeyword.get(key.toUpperCase());
87
- if (keyword) {
88
- dataset[keyword] = value;
89
- } else {
90
- console.warn(`dicom-synth: unknown hex tag "${key}" \u2014 skipping`);
105
+ if (!keyword) {
106
+ throw new Error(
107
+ `dicom-synth: unknown hex tag "${key}" \u2014 dictionary tags take plain values; private/unknown tags require the raw { vr, value } form`
108
+ );
91
109
  }
110
+ dataset[keyword] = value;
92
111
  } else {
93
112
  dataset[key] = value;
94
113
  }
95
114
  }
115
+ return raw;
96
116
  }
97
- function syncMetaWithDataset(meta, dataset) {
98
- if (typeof dataset.SOPInstanceUID === "string") {
99
- meta.MediaStorageSOPInstanceUID = dataset.SOPInstanceUID;
117
+ function rawScalarString(rawElements, tag) {
118
+ const value = rawElements?.[tag]?.Value;
119
+ return value?.length === 1 && typeof value[0] === "string" ? value[0] : void 0;
120
+ }
121
+ function syncMetaWithDataset(meta, dataset, rawElements) {
122
+ const sopInstance = rawScalarString(rawElements, "00080018") ?? (typeof dataset.SOPInstanceUID === "string" ? dataset.SOPInstanceUID : void 0);
123
+ if (sopInstance !== void 0) {
124
+ meta.MediaStorageSOPInstanceUID = sopInstance;
100
125
  }
101
- if (typeof dataset.SOPClassUID === "string") {
102
- meta.MediaStorageSOPClassUID = dataset.SOPClassUID;
126
+ const sopClass = rawScalarString(rawElements, "00080016") ?? (typeof dataset.SOPClassUID === "string" ? dataset.SOPClassUID : void 0);
127
+ if (sopClass !== void 0) {
128
+ meta.MediaStorageSOPClassUID = sopClass;
103
129
  }
104
130
  }
105
131
  function buildMeta(uid, modality, transferSyntax = "explicit-vr-little-endian") {
@@ -139,7 +165,7 @@ function buildBaseImageDataset(uid, modality) {
139
165
  ...preset.attributes
140
166
  };
141
167
  }
142
- function serializeDict(meta, dataset) {
168
+ function serializeDict(meta, dataset, rawElements) {
143
169
  const dicomDict = new dcmjs.data.DicomDict(
144
170
  dcmjs.data.DicomMetaDictionary.denaturalizeDataset(
145
171
  meta
@@ -148,6 +174,9 @@ function serializeDict(meta, dataset) {
148
174
  dicomDict.dict = dcmjs.data.DicomMetaDictionary.denaturalizeDataset(
149
175
  dataset
150
176
  );
177
+ if (rawElements) {
178
+ Object.assign(dicomDict.dict, rawElements);
179
+ }
151
180
  return Buffer.from(dicomDict.write({ allowInvalidVRLength: true }));
152
181
  }
153
182
 
@@ -211,12 +240,12 @@ function writeNative(outPath, params, dims, zeroChunk) {
211
240
  const { modality, uid, tags } = params;
212
241
  const meta = buildMeta(uid, modality);
213
242
  const dataset = buildBaseImageDataset(uid, modality);
214
- applyTagOverrides(dataset, tags);
215
- syncMetaWithDataset(meta, dataset);
243
+ const rawElements = applyTagOverrides(dataset, tags);
244
+ syncMetaWithDataset(meta, dataset, rawElements);
216
245
  dataset.Rows = dims.rows;
217
246
  dataset.Columns = dims.columns;
218
247
  const minimalPixelBytes = dataset.PixelData.byteLength;
219
- const probe = serializeDict(meta, dataset);
248
+ const probe = serializeDict(meta, dataset, rawElements);
220
249
  const pixelElement = Buffer.alloc(12);
221
250
  PIXEL_DATA_OW_HEADER.copy(pixelElement);
222
251
  pixelElement.writeUInt32LE(minimalPixelBytes, 8);
@@ -245,11 +274,11 @@ function writeEncapsulated(outPath, params, pixelBytes, fragmentBytes, zeroChunk
245
274
  TransferSyntaxUID: RLE_TRANSFER_SYNTAX_UID
246
275
  };
247
276
  const dataset = buildBaseImageDataset(uid, modality);
248
- applyTagOverrides(dataset, tags);
249
- syncMetaWithDataset(meta, dataset);
277
+ const rawElements = applyTagOverrides(dataset, tags);
278
+ syncMetaWithDataset(meta, dataset, rawElements);
250
279
  dataset.PixelData = [new Uint8Array([]).buffer, new Uint8Array([0, 0]).buffer];
251
280
  dataset._vrMap = { PixelData: "OB" };
252
- const probe = serializeDict(meta, dataset);
281
+ const probe = serializeDict(meta, dataset, rawElements);
253
282
  const idx = probe.indexOf(PIXEL_DATA_OB_HEADER);
254
283
  if (idx < 0 || probe.readUInt32LE(idx + 8) !== 4294967295) {
255
284
  throw new Error("failed to locate encapsulated PixelData element header");
@@ -38,7 +38,7 @@ var VIOLATION_LEVEL = {
38
38
  "missing-meta-header": "byte",
39
39
  "malformed-sq-delimiter": "byte"
40
40
  };
41
- function applyTagLevel(buffer, violations) {
41
+ function applyTagLevel(buffer, violations, rawElements) {
42
42
  if (violations.length === 0) return buffer;
43
43
  let parsed;
44
44
  try {
@@ -72,6 +72,9 @@ function applyTagLevel(buffer, violations) {
72
72
  }
73
73
  }
74
74
  parsed.dict = dcmjsAny.data.DicomMetaDictionary.denaturalizeDataset(natural);
75
+ if (rawElements) {
76
+ Object.assign(parsed.dict, rawElements);
77
+ }
75
78
  if (violations.includes("uid-too-long")) {
76
79
  ;
77
80
  parsed.dict["00080018"] = {
@@ -104,11 +107,14 @@ function applyByteLevel(buffer, violations) {
104
107
  }
105
108
  return result;
106
109
  }
107
- function applyViolations(buffer, violations) {
110
+ function applyViolations(buffer, violations, rawElements) {
108
111
  if (violations.length === 0) return buffer;
109
112
  const tagViolations = violations.filter((v) => VIOLATION_LEVEL[v] === "tag");
110
113
  const byteViolations = violations.filter((v) => VIOLATION_LEVEL[v] === "byte");
111
- return applyByteLevel(applyTagLevel(buffer, tagViolations), byteViolations);
114
+ return applyByteLevel(
115
+ applyTagLevel(buffer, tagViolations, rawElements),
116
+ byteViolations
117
+ );
112
118
  }
113
119
  export {
114
120
  applyViolations
@@ -4,6 +4,7 @@ export type DescribeOptions = {
4
4
  sizeTolerance?: number;
5
5
  preserveUids?: boolean;
6
6
  uidSalt?: string;
7
+ keepPrivateTags?: boolean;
7
8
  captureTree?: boolean;
8
9
  };
9
10
  export type DescribeResult = {
@@ -13,8 +14,18 @@ export type DescribeResult = {
13
14
  skipped: number;
14
15
  unknownModality: number;
15
16
  nonDicomFiles: number;
17
+ privateBinarySkipped: number;
16
18
  };
17
19
  };
18
- export declare function readHeaderOnly(path: string): Record<string, unknown> | null;
20
+ type RawDictElement = {
21
+ vr?: string;
22
+ Value?: unknown[];
23
+ };
24
+ export type ParsedFile = {
25
+ record: Record<string, unknown>;
26
+ rawDict: Record<string, RawDictElement>;
27
+ };
28
+ export declare function readHeaderOnly(path: string): ParsedFile | null;
19
29
  export declare function tagValuesEqual(a: unknown, b: unknown): boolean;
20
30
  export declare function describeDirectory(dir: string, options?: DescribeOptions): DescribeResult;
31
+ export {};
@@ -4,7 +4,7 @@ export { defaultPublicCasesPath, loadCaseById, loadCasesFromJson, loadDefaultCas
4
4
  export { caseCachePath, fetchPublicCaseToCache, verifySha256, } from './public-fixtures/fetch.js';
5
5
  export { MAX_PIXEL_BYTES, MAX_STREAM_BYTES } from './schema/limits.js';
6
6
  export { resolveParametricSpec } from './schema/parametric.js';
7
- export type { DatasetLayout, DatasetSpec, DicomdirSpec, DicomTagOverrides, EntrySpec, FakeSignatureSpec, FileSpec, ImageSpec, InvalidUidImageSpec, LargeFileSpec, Modality, NonDicomSpec, ParametricEdgeCase, ParametricSpec, ParametricStudies, PathQuirk, Range, SeriesEntrySpec, SeriesInstances, SeriesSpec, StudySpec, TransferSyntax, TreeDirNode, TreeFileNode, TreeNode, ValidImageSpec, VendorWarningsImageSpec, ViolationClass, } from './schema/types.js';
7
+ export type { DatasetLayout, DatasetSpec, DicomdirSpec, DicomTagOverrides, EntrySpec, FakeSignatureSpec, FileSpec, ImageSpec, InvalidUidImageSpec, LargeFileSpec, Modality, NonDicomSpec, ParametricEdgeCase, ParametricSpec, ParametricStudies, PathQuirk, Range, RawTagValue, SeriesEntrySpec, SeriesInstances, SeriesSpec, StudySpec, TransferSyntax, TreeDirNode, TreeFileNode, TreeNode, ValidImageSpec, VendorWarningsImageSpec, ViolationClass, } from './schema/types.js';
8
8
  export { validateDatasetSpec, validateParametricSpec, } from './schema/validate.js';
9
9
  export { type LargeFileEncoding, type LargeFileResult, type LargeImageSpec, type StreamLargeImageOptions, streamLargeImage, writeLargeImageFile, } from './syntheticFixtures/streamWrite.js';
10
10
  export { resolveTagTemplates, type TagContext, TEMPLATE_VOCAB, } from './syntheticFixtures/tagTemplate.js';
@@ -1,4 +1,8 @@
1
1
  export type TransferSyntax = 'explicit-vr-little-endian' | 'implicit-vr-little-endian';
2
+ export type RawTagValue = {
3
+ vr: string;
4
+ value: unknown;
5
+ };
2
6
  export type DicomTagOverrides = {
3
7
  PatientID?: string;
4
8
  PatientName?: string;
@@ -1,6 +1,11 @@
1
1
  import type { DatasetSpec, FileSpec, ParametricSpec } from './types.js';
2
2
  export declare function isImageType(type: FileSpec['type']): boolean;
3
3
  export declare const HEX_TAG_RE: RegExp;
4
+ export declare function isPrivateHexTag(key: string): boolean;
5
+ export declare function isRawTagValue(value: unknown): value is {
6
+ vr: string;
7
+ value: unknown;
8
+ };
4
9
  export declare function positionalName(prefix: string, ordinal: number): string;
5
10
  export declare function validateDatasetSpec(raw: unknown): DatasetSpec;
6
11
  export declare function validateParametricSpec(raw: unknown): ParametricSpec;
@@ -1,9 +1,17 @@
1
1
  import type { DicomTagOverrides, FileSpec, Modality, NonDicomSpec, TransferSyntax } from '../schema/types.js';
2
2
  import type { UidSet } from './uid.js';
3
- export declare function applyTagOverrides(dataset: Record<string, unknown>, tags: DicomTagOverrides | undefined): void;
4
- export declare function syncMetaWithDataset(meta: Record<string, unknown>, dataset: Record<string, unknown>): void;
3
+ export type RawElement = {
4
+ vr: string;
5
+ Value: unknown[];
6
+ };
7
+ export type RawElementMap = Record<string, RawElement>;
8
+ export declare function applyTagOverrides(dataset: Record<string, unknown>, tags: DicomTagOverrides | undefined): RawElementMap | undefined;
9
+ export declare function syncMetaWithDataset(meta: Record<string, unknown>, dataset: Record<string, unknown>, rawElements?: RawElementMap): void;
5
10
  export declare function buildMeta(uid: UidSet, modality: Modality, transferSyntax?: TransferSyntax): Record<string, unknown>;
6
11
  export declare function buildBaseImageDataset(uid: UidSet, modality: Modality): Record<string, unknown>;
7
- export declare function serializeDict(meta: Record<string, unknown>, dataset: Record<string, unknown>): Buffer;
12
+ export declare function serializeDict(meta: Record<string, unknown>, dataset: Record<string, unknown>, rawElements?: RawElementMap): Buffer;
8
13
  export declare function sizedNonDicomBytes(spec: NonDicomSpec): number | undefined;
9
- export declare function buildBufferForSpec(spec: FileSpec, uid: UidSet): Buffer;
14
+ export declare function buildBufferForSpec(spec: FileSpec, uid: UidSet): {
15
+ buffer: Buffer;
16
+ rawElements?: RawElementMap;
17
+ };
@@ -1,2 +1,3 @@
1
1
  import type { ViolationClass } from '../schema/types.js';
2
- export declare function applyViolations(buffer: Buffer, violations: ViolationClass[]): Buffer;
2
+ import type { RawElementMap } from './generator.js';
3
+ export declare function applyViolations(buffer: Buffer, violations: ViolationClass[], rawElements?: RawElementMap): Buffer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dicom-synth",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "description": "Toolkit for synthetic DICOM fixtures and public fixture fetch/cache.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -49,6 +49,7 @@
49
49
  "build:data": "mkdir -p dist/data && cp data/public-cases.json dist/data/",
50
50
  "typecheck": "tsc --noEmit",
51
51
  "test": "vitest run",
52
+ "test:coverage": "vitest run --coverage",
52
53
  "test:watch": "vitest",
53
54
  "lint": "eslint src test scripts --ext .ts",
54
55
  "lint:fix": "eslint src test scripts --ext .ts --fix",
@@ -82,6 +83,7 @@
82
83
  "@types/node": "^22.15.3",
83
84
  "@typescript-eslint/eslint-plugin": "^8.46.1",
84
85
  "@typescript-eslint/parser": "^8.46.1",
86
+ "@vitest/coverage-v8": "3.2.6",
85
87
  "dcmjs": "^0.51.1",
86
88
  "esbuild": "^0.28.1",
87
89
  "eslint": "^9.38.0",