dcmjs 0.49.3 → 0.50.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.
Files changed (55) hide show
  1. package/README.md +50 -0
  2. package/build/dcmjs.es.js +1071 -112
  3. package/build/dcmjs.es.js.map +1 -1
  4. package/build/dcmjs.js +1071 -112
  5. package/build/dcmjs.js.map +1 -1
  6. package/build/dcmjs.min.js +2 -2
  7. package/build/dcmjs.min.js.map +1 -1
  8. package/generate/dictionary.mjs +56029 -0
  9. package/package.json +18 -2
  10. package/.babelrc +0 -9
  11. package/.github/workflows/lint-and-format.yml +0 -27
  12. package/.github/workflows/publish-package.yml +0 -45
  13. package/.github/workflows/tests.yml +0 -24
  14. package/.prettierrc +0 -5
  15. package/.vscode/extensions.json +0 -7
  16. package/.vscode/settings.json +0 -8
  17. package/changelog.md +0 -31
  18. package/docs/ArrayBufferExpanderListener.md +0 -303
  19. package/docs/AsyncDicomReader-skill.md +0 -730
  20. package/eslint.config.mjs +0 -30
  21. package/generate-dictionary.js +0 -145
  22. package/jest.setup.js +0 -39
  23. package/netlify.toml +0 -22
  24. package/rollup.config.mjs +0 -57
  25. package/test/ArrayBufferExpanderListener.test.js +0 -365
  26. package/test/DICOMWEB.test.js +0 -1
  27. package/test/DicomMetaDictionary.test.js +0 -73
  28. package/test/SequenceOfItems.test.js +0 -86
  29. package/test/adapters.test.js +0 -43
  30. package/test/anonymizer.test.js +0 -176
  31. package/test/arrayItem.json +0 -351
  32. package/test/async-data.test.js +0 -575
  33. package/test/data-encoding.test.js +0 -59
  34. package/test/data-options.test.js +0 -199
  35. package/test/data.test.js +0 -1776
  36. package/test/derivations.test.js +0 -1
  37. package/test/helper/DicomDataReadBufferStreamBuilder.js +0 -89
  38. package/test/information-filter.test.js +0 -165
  39. package/test/integration/DicomMessage.readFile.test.js +0 -50
  40. package/test/lossless-read-write.test.js +0 -1407
  41. package/test/mocks/minimal_fields_dataset.json +0 -17
  42. package/test/mocks/null_number_vrs_dataset.json +0 -102
  43. package/test/normalizers.test.js +0 -38
  44. package/test/odd-frame-bit-data.js +0 -138
  45. package/test/rawTags.js +0 -170
  46. package/test/readBufferStream.test.js +0 -158
  47. package/test/sample-dicom.json +0 -904
  48. package/test/sample-op.lei +0 -0
  49. package/test/sample-sr.json +0 -997
  50. package/test/sr-tid.test.js +0 -251
  51. package/test/testUtils.js +0 -85
  52. package/test/utilities/deepEqual.test.js +0 -87
  53. package/test/utilities.test.js +0 -205
  54. package/test/video-test-dict.js +0 -40
  55. package/test/writeBufferStream.test.js +0 -149
@@ -1,1407 +0,0 @@
1
- import fs from "fs";
2
- import crypto from "crypto";
3
- import dcmjs from "../src/index.js";
4
- import { deepEqual } from "../src/utilities/deepEqual";
5
- import {
6
- DEFLATED_EXPLICIT_LITTLE_ENDIAN,
7
- UNDEFINED_LENGTH,
8
- TagHex
9
- } from "../src/constants/dicom.js";
10
-
11
- import { getTestDataset } from "./testUtils";
12
- import { DicomMetaDictionary } from "../src/DicomMetaDictionary";
13
-
14
- const { DicomDict, DicomMessage } = dcmjs.data;
15
-
16
- // Implicit VR Little Endian - dataset uses tag (4) + length (4) + value
17
- const IMPLICIT_LITTLE_ENDIAN_UID = "1.2.840.10008.1.2";
18
- // VRs that use 32-bit length in Explicit VR (reserved 2 + length 4 after VR)
19
- const EXPLICIT_VR_LENGTH32 = [
20
- "OB",
21
- "OW",
22
- "OF",
23
- "SQ",
24
- "UN",
25
- "UC",
26
- "UR",
27
- "UT",
28
- "OD"
29
- ];
30
-
31
- /**
32
- * Parses a raw DICOM buffer and returns the PixelData element's length and raw bytes.
33
- * Reads up to PixelData, then reads the tag's length field (checking it's not -1) and value.
34
- * Supports both Implicit and Explicit VR Little Endian for the dataset.
35
- *
36
- * @param {ArrayBuffer|Uint8Array} buffer - Raw DICOM file buffer
37
- * @param {string} [transferSyntaxUID] - Optional. If provided, used to decide Implicit vs Explicit VR (e.g. from meta); avoids parsing meta.
38
- * @returns {{ length: number, data: ArrayBuffer } | null} - PixelData length and bytes, or null if not found
39
- */
40
- function readPixelDataFromRawBuffer(buffer, transferSyntaxUIDHint) {
41
- // Ensure we have a contiguous ArrayBuffer (getBuffer may return view with shared buffer)
42
- let arrayBuf;
43
- if (buffer instanceof ArrayBuffer) {
44
- arrayBuf = buffer;
45
- } else {
46
- const len = buffer.byteLength ?? buffer.length;
47
- arrayBuf = buffer.buffer.slice(
48
- buffer.byteOffset ?? 0,
49
- (buffer.byteOffset ?? 0) + len
50
- );
51
- }
52
- const view = new DataView(arrayBuf);
53
- const isLittleEndian = true;
54
-
55
- let offset = 132; // Skip preamble (128) + "DICM" (4)
56
- if (offset + 4 > arrayBuf.byteLength) return null;
57
-
58
- // Parse meta header - first element is FileMetaInformationGroupLength (Explicit VR)
59
- offset += 4; // tag
60
- offset += 2; // VR
61
- offset += 2; // elem length (2 for UL)
62
- const metaLength = view.getUint32(offset, isLittleEndian);
63
- offset += 4;
64
- const metaEnd = Math.min(offset + metaLength, arrayBuf.byteLength);
65
-
66
- // Parse meta to find Transfer Syntax UID (0002,0010) for dataset VR format (unless hint provided)
67
- let transferSyntaxUID = transferSyntaxUIDHint ?? IMPLICIT_LITTLE_ENDIAN_UID;
68
- if (transferSyntaxUIDHint == null) {
69
- while (offset < metaEnd && offset + 8 <= arrayBuf.byteLength) {
70
- const group = view.getUint16(offset, isLittleEndian);
71
- const element = view.getUint16(offset + 2, isLittleEndian);
72
- offset += 4;
73
- const vr = String.fromCharCode(
74
- view.getUint8(offset),
75
- view.getUint8(offset + 1)
76
- );
77
- offset += 2;
78
- let elemLen;
79
- if (EXPLICIT_VR_LENGTH32.includes(vr)) {
80
- offset += 2; // reserved
81
- elemLen = view.getUint32(offset, isLittleEndian);
82
- offset += 4;
83
- } else {
84
- elemLen = view.getUint16(offset, isLittleEndian);
85
- offset += 2;
86
- }
87
- if (offset + elemLen > arrayBuf.byteLength) break;
88
- if (group === 0x0002 && element === 0x0010) {
89
- // Transfer Syntax UID - value is ASCII
90
- transferSyntaxUID = String.fromCharCode(
91
- ...new Uint8Array(arrayBuf, offset, elemLen)
92
- )
93
- .replace(/\0/g, "")
94
- .trim();
95
- break;
96
- }
97
- offset += elemLen;
98
- }
99
- }
100
- offset = metaEnd;
101
-
102
- // Detect Implicit vs Explicit from Transfer Syntax UID; fallback: Explicit if first dataset tag has 2-char VR (A-Z) after tag
103
- let implicitDataset = transferSyntaxUID === IMPLICIT_LITTLE_ENDIAN_UID;
104
- if (offset + 6 <= arrayBuf.byteLength) {
105
- const b0 = view.getUint8(offset + 4);
106
- const b1 = view.getUint8(offset + 5);
107
- const looksLikeExplicitVR =
108
- b0 >= 0x41 && b0 <= 0x5a && b1 >= 0x41 && b1 <= 0x5a;
109
- if (looksLikeExplicitVR) implicitDataset = false;
110
- }
111
- const PIXEL_DATA_TAG = 0x7fe00010;
112
- const SEQUENCE_ITEM_TAG = 0xfffee000;
113
- const SEQUENCE_DELIMITER_TAG = 0xfffee0dd;
114
- const ITEM_DELIMITATION_TAG = 0xfffee00d;
115
-
116
- while (offset < arrayBuf.byteLength - 8) {
117
- const group = view.getUint16(offset, isLittleEndian);
118
- const element = view.getUint16(offset + 2, isLittleEndian);
119
- const tagValue = (group << 16) | element;
120
- offset += 4;
121
-
122
- let length;
123
- if (implicitDataset) {
124
- length = view.getUint32(offset, isLittleEndian);
125
- offset += 4;
126
- } else {
127
- const vr = String.fromCharCode(
128
- view.getUint8(offset),
129
- view.getUint8(offset + 1)
130
- );
131
- offset += 2;
132
- if (EXPLICIT_VR_LENGTH32.includes(vr)) {
133
- offset += 2; // reserved
134
- length = view.getUint32(offset, isLittleEndian);
135
- offset += 4;
136
- } else {
137
- length = view.getUint16(offset, isLittleEndian);
138
- offset += 2;
139
- }
140
- }
141
-
142
- if (tagValue === PIXEL_DATA_TAG) {
143
- if (length === UNDEFINED_LENGTH || length === -1) {
144
- return { length: -1, data: null };
145
- }
146
- if (offset + length > arrayBuf.byteLength) return null;
147
- const data = arrayBuf.slice(offset, offset + length);
148
- return { length, data };
149
- }
150
-
151
- if (length === UNDEFINED_LENGTH) {
152
- // Skip undefined-length sequence (Item FFFE,E000 or Delimiter FFFE,E0DD)
153
- while (offset < arrayBuf.byteLength - 8) {
154
- const itemGroup = view.getUint16(offset, isLittleEndian);
155
- const itemElement = view.getUint16(offset + 2, isLittleEndian);
156
- const itemTagValue = (itemGroup << 16) | itemElement;
157
- offset += 4;
158
- const itemLength = view.getUint32(offset, isLittleEndian);
159
- offset += 4;
160
- if (itemTagValue === SEQUENCE_DELIMITER_TAG) break;
161
- if (itemTagValue === SEQUENCE_ITEM_TAG) {
162
- if (
163
- itemLength === UNDEFINED_LENGTH ||
164
- itemLength === 0xffffffff
165
- ) {
166
- // Skip undefined-length item until Item Delimitation (FFFE,E00D); item uses same VR as dataset
167
- let itemOffset = offset;
168
- while (itemOffset < arrayBuf.byteLength - 8) {
169
- const delGroup = view.getUint16(
170
- itemOffset,
171
- isLittleEndian
172
- );
173
- const delElement = view.getUint16(
174
- itemOffset + 2,
175
- isLittleEndian
176
- );
177
- const delTag = (delGroup << 16) | delElement;
178
- itemOffset += 4;
179
- let delLen;
180
- if (implicitDataset) {
181
- delLen = view.getUint32(
182
- itemOffset,
183
- isLittleEndian
184
- );
185
- itemOffset += 4;
186
- } else {
187
- const vr = String.fromCharCode(
188
- view.getUint8(itemOffset),
189
- view.getUint8(itemOffset + 1)
190
- );
191
- itemOffset += 2;
192
- if (EXPLICIT_VR_LENGTH32.includes(vr)) {
193
- itemOffset += 2;
194
- delLen = view.getUint32(
195
- itemOffset,
196
- isLittleEndian
197
- );
198
- itemOffset += 4;
199
- } else {
200
- delLen = view.getUint16(
201
- itemOffset,
202
- isLittleEndian
203
- );
204
- itemOffset += 2;
205
- }
206
- }
207
- if (delTag === ITEM_DELIMITATION_TAG) break;
208
- itemOffset += delLen > 0 ? delLen : 0;
209
- }
210
- offset = itemOffset;
211
- } else {
212
- offset += itemLength;
213
- }
214
- }
215
- }
216
- } else if (length > 0) {
217
- offset += length;
218
- }
219
- }
220
- return null;
221
- }
222
-
223
- function getPixelDataBytes(pixelDataValue) {
224
- const chunks = [];
225
- const flatten = arr => {
226
- for (const item of arr) {
227
- if (item instanceof ArrayBuffer) {
228
- chunks.push(new Uint8Array(item));
229
- } else if (Array.isArray(item)) {
230
- flatten(item);
231
- }
232
- }
233
- };
234
- flatten(pixelDataValue);
235
- const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);
236
- const result = new Uint8Array(totalLength);
237
- let offset = 0;
238
- for (const chunk of chunks) {
239
- result.set(chunk, offset);
240
- offset += chunk.byteLength;
241
- }
242
- return result.buffer;
243
- }
244
-
245
- function hashArrayBuffer(buffer) {
246
- return crypto
247
- .createHash("sha256")
248
- .update(new Uint8Array(buffer))
249
- .digest("hex");
250
- }
251
-
252
- describe("lossless-read-write", () => {
253
- describe("storeRaw option", () => {
254
- const dataset = {
255
- "00080008": {
256
- vr: "CS",
257
- Value: ["DERIVED"]
258
- },
259
- "00082112": {
260
- vr: "SQ",
261
- Value: [
262
- {
263
- "00081150": {
264
- vr: "UI",
265
- Value: ["1.2.840.10008.5.1.4.1.1.7"]
266
- }
267
- }
268
- ]
269
- },
270
- "00180050": {
271
- vr: "DS",
272
- Value: [1]
273
- },
274
- "00181708": {
275
- vr: "IS",
276
- Value: [426]
277
- },
278
- "00189328": {
279
- vr: "FD",
280
- Value: [30.98]
281
- },
282
- "0020000D": {
283
- vr: "UI",
284
- Value: [
285
- "1.3.6.1.4.1.5962.99.1.2280943358.716200484.1363785608958.3.0"
286
- ]
287
- },
288
- "00400254": {
289
- vr: "LO",
290
- Value: ["DUCTO/GALACTOGRAM 1 DUCT LT"]
291
- },
292
- "7FE00010": {
293
- vr: "OW",
294
- Value: [new Uint8Array([0x00, 0x00]).buffer]
295
- }
296
- };
297
-
298
- test("storeRaw flag on VR should be respected by read", () => {
299
- const tagsWithoutRaw = ["00082112", "7FE00010"];
300
-
301
- const dicomDict = new DicomDict({});
302
- dicomDict.dict = dataset;
303
-
304
- // write and re-read
305
- const outputDicomDict = DicomMessage.readFile(dicomDict.write());
306
- for (const tag in outputDicomDict.dict) {
307
- if (tagsWithoutRaw.includes(tag)) {
308
- expect(outputDicomDict.dict[tag]._rawValue).toBeFalsy();
309
- } else {
310
- expect(outputDicomDict.dict[tag]._rawValue).toBeTruthy();
311
- }
312
- }
313
- });
314
-
315
- test("forceStoreRaw read option should override VR setting", () => {
316
- const dicomDict = new DicomDict({});
317
- dicomDict.dict = dataset;
318
-
319
- // write and re-read
320
- const outputDicomDict = DicomMessage.readFile(dicomDict.write(), {
321
- forceStoreRaw: true
322
- });
323
-
324
- for (const tag in outputDicomDict.dict) {
325
- expect(outputDicomDict.dict[tag]._rawValue).toBeTruthy();
326
- }
327
- });
328
- });
329
-
330
- test("test DS value with additional allowed characters is written to file", () => {
331
- const dataset = {
332
- "00181041": {
333
- _rawValue: [" +1.4000 ", "-0.00", "1.2345e2", "1E34"],
334
- Value: [1.4, -0, 123.45, 1e34],
335
- vr: "DS"
336
- }
337
- };
338
-
339
- const dicomDict = new DicomDict({});
340
- dicomDict.dict = dataset;
341
-
342
- // write and re-read
343
- const outputDicomDict = DicomMessage.readFile(dicomDict.write());
344
-
345
- // expect raw value to be unchanged, and Value parsed as Number to lose precision
346
- expect(outputDicomDict.dict["00181041"]._rawValue).toEqual([
347
- " +1.4000 ",
348
- "-0.00",
349
- "1.2345e2",
350
- "1E34"
351
- ]);
352
- expect(outputDicomDict.dict["00181041"].Value).toEqual([
353
- 1.4, -0, 123.45, 1e34
354
- ]);
355
- });
356
-
357
- test("test DS value that exceeds Number.MAX_SAFE_INTEGER is written to file", () => {
358
- const dataset = {
359
- "00181041": {
360
- _rawValue: ["9007199254740993"],
361
- Value: [9007199254740993],
362
- vr: "DS"
363
- }
364
- };
365
-
366
- const dicomDict = new DicomDict({});
367
- dicomDict.dict = dataset;
368
-
369
- // write and re-read
370
- const outputDicomDict = DicomMessage.readFile(dicomDict.write());
371
-
372
- // expect raw value to be unchanged, and Value parsed as Number to lose precision
373
- expect(outputDicomDict.dict["00181041"]._rawValue).toEqual([
374
- "9007199254740993"
375
- ]);
376
- expect(outputDicomDict.dict["00181041"].Value).toEqual([
377
- 9007199254740992
378
- ]);
379
- });
380
-
381
- test("test DS with multiplicity > 1 and added space for even padding is read and written correctly", () => {
382
- const dataset = {
383
- "00200037": {
384
- vr: "DS",
385
- Value: [
386
- 0.99924236548978, -0.0322633220972, -0.0217663285287,
387
- 0.02949870928067, 0.99267261121054, -0.1171789789306
388
- ]
389
- }
390
- };
391
-
392
- const dicomDict = new DicomDict({});
393
- dicomDict.dict = dataset;
394
-
395
- // write and re-read
396
- const outputDicomDict = DicomMessage.readFile(dicomDict.write());
397
-
398
- // ensure _rawValue strings have no added trailing spaces
399
- const expectedDataset = {
400
- "00200037": {
401
- vr: "DS",
402
- Value: [
403
- 0.99924236548978, -0.0322633220972, -0.0217663285287,
404
- 0.02949870928067, 0.99267261121054, -0.1171789789306
405
- ],
406
- _rawValue: [
407
- "0.99924236548978",
408
- "-0.0322633220972",
409
- "-0.0217663285287",
410
- "0.02949870928067",
411
- "0.99267261121054",
412
- "-0.1171789789306"
413
- ]
414
- }
415
- };
416
-
417
- expect(deepEqual(expectedDataset, outputDicomDict.dict)).toBeTruthy();
418
-
419
- // re-write should succeeed
420
- const outputDicomDictPass2 = DicomMessage.readFile(
421
- outputDicomDict.write()
422
- );
423
-
424
- // dataset should still be equal
425
- expect(
426
- deepEqual(expectedDataset, outputDicomDictPass2.dict)
427
- ).toBeTruthy();
428
- });
429
-
430
- test("test DS with multiplicity > 1 with padding byte on last element within VR max length is losslessly read", () => {
431
- const dataset = {
432
- "00200037": {
433
- vr: "DS",
434
- Value: [
435
- 0.99924236548978, -0.0322633220972, -0.0217663285287, 0
436
- ],
437
- _rawValue: [
438
- "0.99924236548978",
439
- "-0.0322633220972",
440
- "-0.0217663285287",
441
- " +0.00 "
442
- ]
443
- }
444
- };
445
-
446
- const dicomDict = new DicomDict({});
447
- dicomDict.dict = dataset;
448
-
449
- // write and re-read
450
- const outputDicomDict = DicomMessage.readFile(dicomDict.write());
451
-
452
- // ensure _rawValue strings have no added trailing spaces and retain original encoding details for + and spaces
453
- const expectedDataset = {
454
- "00200037": {
455
- vr: "DS",
456
- Value: [
457
- 0.99924236548978, -0.0322633220972, -0.0217663285287, 0
458
- ],
459
- _rawValue: [
460
- "0.99924236548978",
461
- "-0.0322633220972",
462
- "-0.0217663285287",
463
- " +0.00"
464
- ]
465
- }
466
- };
467
-
468
- expect(outputDicomDict.dict).toEqual(expectedDataset);
469
-
470
- // re-write should succeeed
471
- const outputDicomDictPass2 = DicomMessage.readFile(
472
- outputDicomDict.write()
473
- );
474
-
475
- // dataset should still be equal
476
- expect(outputDicomDictPass2.dict).toEqual(expectedDataset);
477
- });
478
-
479
- test("test IS with multiplicity > 1 and added space for even padding is read and written correctly", () => {
480
- const dataset = {
481
- "00081160": {
482
- vr: "IS",
483
- Value: [1234, 5678]
484
- }
485
- };
486
-
487
- const dicomDict = new DicomDict({});
488
- dicomDict.dict = dataset;
489
-
490
- // write and re-read
491
- const outputDicomDict = DicomMessage.readFile(dicomDict.write());
492
-
493
- // last _rawValue strings does allow trailing space as it does not exceed max length
494
- const expectedDataset = {
495
- "00081160": {
496
- vr: "IS",
497
- Value: [1234, 5678],
498
- _rawValue: ["1234", "5678"]
499
- }
500
- };
501
-
502
- expect(outputDicomDict.dict).toEqual(expectedDataset);
503
-
504
- // re-write should succeeed
505
- const outputDicomDictPass2 = DicomMessage.readFile(
506
- outputDicomDict.write()
507
- );
508
-
509
- // dataset should still be equal
510
- expect(outputDicomDictPass2.dict).toEqual(expectedDataset);
511
- });
512
-
513
- describe("Multiplicity for non-binary String VRs", () => {
514
- const maxLengthCases = [
515
- {
516
- vr: "AE",
517
- Value: ["MAX_LENGTH_CHARS", "MAX_LENGTH_CHARS"],
518
- _rawValue: ["MAX_LENGTH_CHARS", "MAX_LENGTH_CHARS"]
519
- },
520
- {
521
- vr: "AS",
522
- Value: ["120D", "045Y"],
523
- _rawValue: ["120D", "045Y"]
524
- },
525
- {
526
- vr: "AT",
527
- Value: [0x00207e14, 0x0012839a],
528
- _rawValue: [0x00207e14, 0x0012839a]
529
- },
530
- {
531
- vr: "CS",
532
- Value: ["MAX_LENGTH_CHARS", "MAX_LENGTH_CHARS"],
533
- _rawValue: ["MAX_LENGTH_CHARS", "MAX_LENGTH_CHARS"]
534
- },
535
- {
536
- vr: "DA",
537
- Value: ["20230826", "20230826"],
538
- _rawValue: ["20230826", "20230826"]
539
- },
540
- {
541
- vr: "DS",
542
- Value: [123456789012.345, 123456789012.345],
543
- _rawValue: ["123456789012.345", "123456789012.345"]
544
- },
545
- {
546
- vr: "DT",
547
- Value: [
548
- "20230826123045.123456+0100",
549
- "20230826123045.123456+0100"
550
- ],
551
- _rawValue: [
552
- "20230826123045.123456+0100",
553
- "20230826123045.123456+0100"
554
- ]
555
- },
556
- {
557
- vr: "IS",
558
- Value: [123456789012, 123456789012],
559
- _rawValue: ["123456789012", "123456789012"]
560
- },
561
- {
562
- vr: "LO",
563
- Value: [
564
- "ABCDEFGHIJKLMNOPQRSTUVWXABCDEFGHIJKLMNOPQRSTUVWXABCDEFGHIJKLMNOP",
565
- "ABCDEFGHIJKLMNOPQRSTUVWXABCDEFGHIJKLMNOPQRSTUVWXABCDEFGHIJKLMNOP"
566
- ],
567
- _rawValue: [
568
- "ABCDEFGHIJKLMNOPQRSTUVWXABCDEFGHIJKLMNOPQRSTUVWXABCDEFGHIJKLMNOP",
569
- "ABCDEFGHIJKLMNOPQRSTUVWXABCDEFGHIJKLMNOPQRSTUVWXABCDEFGHIJKLMNOP"
570
- ]
571
- },
572
- {
573
- vr: "SH",
574
- Value: ["ABCDEFGHIJKLMNOP", "ABCDEFGHIJKLMNOP"],
575
- _rawValue: ["ABCDEFGHIJKLMNOP", "ABCDEFGHIJKLMNOP"]
576
- },
577
- {
578
- vr: "UI",
579
- Value: [
580
- "1.2.840.12345678901234567890123456789012345678901234567890123456",
581
- "1.2.840.12345678901234567890123456789012345678901234567890123456"
582
- ],
583
- _rawValue: [
584
- "1.2.840.12345678901234567890123456789012345678901234567890123456",
585
- "1.2.840.12345678901234567890123456789012345678901234567890123456"
586
- ]
587
- },
588
- {
589
- vr: "TM",
590
- Value: ["142530.1234567", "142530.1234567"],
591
- _rawValue: ["142530.1234567", "142530.1234567"]
592
- }
593
- ];
594
-
595
- test.each(maxLengthCases)(
596
- `Test multiple values with VR max length handle pad byte correctly during read and write - $vr`,
597
- dataElement => {
598
- const dataset = {
599
- "00081160": {
600
- vr: dataElement.vr,
601
- Value: dataElement.Value
602
- }
603
- };
604
-
605
- const dicomDict = new DicomDict({});
606
- dicomDict.dict = dataset;
607
-
608
- // write and re-read
609
- const outputDicomDict = DicomMessage.readFile(
610
- dicomDict.write()
611
- );
612
-
613
- // expect full _rawValue to match following read
614
- const expectedDataset = {
615
- "00081160": {
616
- ...dataElement
617
- }
618
- };
619
-
620
- expect(outputDicomDict.dict).toEqual(expectedDataset);
621
-
622
- // re-write should succeed without max length issues
623
- const outputDicomDictPass2 = DicomMessage.readFile(
624
- outputDicomDict.write()
625
- );
626
-
627
- // dataset should still be equal
628
- expect(expectedDataset).toEqual(outputDicomDictPass2.dict);
629
- }
630
- );
631
- });
632
-
633
- describe("Individual VR comparisons", () => {
634
- const unchangedTestCases = [
635
- {
636
- vr: "AE",
637
- _rawValue: [" TEST_AE "], // spaces non-significant for interpretation but allowed
638
- Value: ["TEST_AE"]
639
- },
640
- {
641
- vr: "AS",
642
- _rawValue: ["045Y"],
643
- Value: ["045Y"]
644
- },
645
- {
646
- vr: "AT",
647
- _rawValue: [0x00207e14, 0x0012839a],
648
- Value: [0x00207e14, 0x0012839a]
649
- },
650
- {
651
- vr: "CS",
652
- _rawValue: ["ORIGINAL ", " PRIMARY"], // spaces non-significant for interpretation but allowed
653
- Value: ["ORIGINAL", "PRIMARY"]
654
- },
655
- {
656
- vr: "DA",
657
- _rawValue: ["20240101"],
658
- Value: ["20240101"]
659
- },
660
- {
661
- vr: "DS",
662
- _rawValue: ["0000123.45"], // leading zeros allowed
663
- Value: [123.45]
664
- },
665
- {
666
- vr: "DT",
667
- _rawValue: ["20240101123045.1 "], // trailing spaces allowed
668
- Value: ["20240101123045.1 "]
669
- },
670
- {
671
- vr: "FL",
672
- _rawValue: [3.125],
673
- Value: [3.125]
674
- },
675
- {
676
- vr: "FD",
677
- _rawValue: [3.14159265358979], // trailing spaces allowed
678
- Value: [3.14159265358979]
679
- },
680
- {
681
- vr: "IS",
682
- _rawValue: [" -123 "], // leading/trailing spaces & sign allowed
683
- Value: [-123]
684
- },
685
- {
686
- vr: "LO",
687
- _rawValue: [" A long string with spaces "], // leading/trailing spaces allowed
688
- Value: ["A long string with spaces"]
689
- },
690
- {
691
- vr: "LT",
692
- _rawValue: [
693
- " It may contain the Graphic Character set and the Control Characters, CR\r, LF\n, FF\f, and ESC\x1b. "
694
- ], // leading spaces significant, trailing spaces allowed
695
- Value: [
696
- " It may contain the Graphic Character set and the Control Characters, CR\r, LF\n, FF\f, and ESC\x1b."
697
- ]
698
- },
699
- {
700
- vr: "OB",
701
- _rawValue: [
702
- new Uint8Array([
703
- 0x13, 0x40, 0x80, 0x88, 0x88, 0x90, 0x88, 0x88
704
- ]).buffer
705
- ],
706
- Value: [
707
- new Uint8Array([
708
- 0x13, 0x40, 0x80, 0x88, 0x88, 0x90, 0x88, 0x88
709
- ]).buffer
710
- ]
711
- },
712
- {
713
- vr: "OD",
714
- _rawValue: [
715
- new Uint8Array([
716
- 0x00, 0x00, 0x00, 0x54, 0x34, 0x6f, 0x9d, 0x41
717
- ]).buffer
718
- ],
719
- Value: [
720
- new Uint8Array([
721
- 0x00, 0x00, 0x00, 0x54, 0x34, 0x6f, 0x9d, 0x41
722
- ]).buffer
723
- ]
724
- },
725
- {
726
- vr: "OF",
727
- _rawValue: [
728
- new Uint8Array([
729
- 0x00, 0x00, 0x28, 0x41, 0x00, 0x00, 0x30, 0xc0, 0x00,
730
- 0x00, 0xf6, 0x42
731
- ]).buffer
732
- ],
733
- Value: [
734
- new Uint8Array([
735
- 0x00, 0x00, 0x28, 0x41, 0x00, 0x00, 0x30, 0xc0, 0x00,
736
- 0x00, 0xf6, 0x42
737
- ]).buffer
738
- ]
739
- },
740
- // TODO: VRs currently unimplemented
741
- // {
742
- // vr: 'OL',
743
- // _rawValue: [new Uint8Array([0x00, 0x00, 0x30, 0xC0, 0x00, 0x00, 0x28, 0x41, 0x00, 0x00, 0xF6, 0x42, 0x00, 0x00, 0x28, 0x41]).buffer],
744
- // Value: [new Uint8Array([0x00, 0x00, 0x30, 0xC0, 0x00, 0x00, 0x28, 0x41, 0x00, 0x00, 0xF6, 0x42, 0x00, 0x00, 0x28, 0x41]).buffer],
745
- // },
746
- // {
747
- // vr: 'OV',
748
- // _rawValue: [new Uint8Array([0x00, 0x00, 0x30, 0xC0, 0x00, 0x00, 0x28, 0x41]).buffer],
749
- // Value: [new Uint8Array([0x00, 0x00, 0x30, 0xC0, 0x00, 0x00, 0x28, 0x41]).buffer],
750
- // },
751
- {
752
- vr: "OW",
753
- _rawValue: [
754
- new Uint8Array([
755
- 0x13, 0x40, 0x80, 0x88, 0x88, 0x90, 0x88, 0x88
756
- ]).buffer
757
- ],
758
- Value: [
759
- new Uint8Array([
760
- 0x13, 0x40, 0x80, 0x88, 0x88, 0x90, 0x88, 0x88
761
- ]).buffer
762
- ]
763
- },
764
- {
765
- vr: "PN",
766
- _rawValue: "Doe^John^A^Jr.^MD ", // trailing spaces allowed
767
- Value: [{ Alphabetic: "Doe^John^A^Jr.^MD " }]
768
- },
769
- {
770
- vr: "SH",
771
- _rawValue: [" CT_SCAN_01 "], // leading/trailing spaces allowed
772
- Value: ["CT_SCAN_01"]
773
- },
774
- {
775
- vr: "SL",
776
- _rawValue: [-2147483648],
777
- Value: [-2147483648]
778
- },
779
- {
780
- vr: "SS",
781
- _rawValue: [-32768, 1234, 832],
782
- Value: [-32768, 1234, 832]
783
- },
784
- {
785
- vr: "ST",
786
- _rawValue: [
787
- "Patient complains of headaches over the last week. "
788
- ], // trailing spaces allowed
789
- Value: ["Patient complains of headaches over the last week."]
790
- },
791
- // TODO: VR currently unimplemented
792
- // {
793
- // vr: 'SV',
794
- // _rawValue: [9007199254740993], // trailing spaces allowed
795
- // Value: [9007199254740993],
796
- // },
797
- {
798
- vr: "TM",
799
- _rawValue: ["42530.123456 "], // trailing spaces allowed
800
- Value: ["42530.123456"]
801
- },
802
- {
803
- vr: "UC",
804
- _rawValue: [
805
- "Detailed description of procedure or clinical notes that could be very long. "
806
- ], // trailing spaces allowed
807
- Value: [
808
- "Detailed description of procedure or clinical notes that could be very long."
809
- ]
810
- },
811
- {
812
- vr: "UI",
813
- _rawValue: ["1.2.840.10008.1.2.1"],
814
- Value: ["1.2.840.10008.1.2.1"]
815
- },
816
- {
817
- vr: "UL",
818
- _rawValue: [4294967295],
819
- Value: [4294967295]
820
- },
821
- {
822
- vr: "UR",
823
- _rawValue: ["http://dicom.nema.org "], // trailing spaces ignored but allowed
824
- Value: ["http://dicom.nema.org "]
825
- },
826
- {
827
- vr: "US",
828
- _rawValue: [65535],
829
- Value: [65535]
830
- },
831
- {
832
- vr: "UT",
833
- _rawValue: [
834
- " This is a detailed explanation that can span multiple lines and paragraphs in the DICOM dataset. "
835
- ], // leading spaces significant, trailing spaces allowed
836
- Value: [
837
- " This is a detailed explanation that can span multiple lines and paragraphs in the DICOM dataset."
838
- ]
839
- }
840
- // TODO: VR currently unimplemented
841
- // {
842
- // vr: 'UV',
843
- // _rawValue: [18446744073709551616], // 2^64
844
- // Value: [18446744073709551616],
845
- // },
846
- ];
847
- test.each(unchangedTestCases)(
848
- `Test unchanged value is retained following read and write - $vr`,
849
- dataElement => {
850
- const dataset = {
851
- "00181041": {
852
- ...dataElement
853
- }
854
- };
855
-
856
- const dicomDict = new DicomDict({});
857
- dicomDict.dict = dataset;
858
-
859
- // write and re-read
860
- const outputDicomDict = DicomMessage.readFile(
861
- dicomDict.write(),
862
- { forceStoreRaw: true }
863
- );
864
-
865
- // expect raw value to be unchanged, and Value parsed as Number to lose precision
866
- expect(outputDicomDict.dict["00181041"]._rawValue).toEqual(
867
- dataElement._rawValue
868
- );
869
- expect(outputDicomDict.dict["00181041"].Value).toEqual(
870
- dataElement.Value
871
- );
872
- }
873
- );
874
-
875
- const changedTestCases = [
876
- {
877
- vr: "AE",
878
- _rawValue: [" TEST_AE "], // spaces non-significant for interpretation but allowed
879
- Value: ["NEW_AE"]
880
- },
881
- {
882
- vr: "AS",
883
- _rawValue: ["045Y"],
884
- Value: ["999Y"]
885
- },
886
- {
887
- vr: "AT",
888
- _rawValue: [0x00207e14, 0x0012839a],
889
- Value: [0x00200010]
890
- },
891
- {
892
- vr: "CS",
893
- _rawValue: ["ORIGINAL ", " PRIMARY "], // spaces non-significant for interpretation but allowed
894
- Value: ["ORIGINAL", "PRIMARY", "SECONDARY"]
895
- },
896
- {
897
- vr: "DA",
898
- _rawValue: ["20240101"],
899
- Value: ["20231225"]
900
- },
901
- {
902
- vr: "DS",
903
- _rawValue: ["0000123.45"], // leading zeros allowed
904
- Value: [123.456],
905
- newRawValue: ["123.456 "]
906
- },
907
- {
908
- vr: "DT",
909
- _rawValue: ["20240101123045.1 "], // trailing spaces allowed
910
- Value: ["20240101123045.3"]
911
- },
912
- {
913
- vr: "FL",
914
- _rawValue: [3.125],
915
- Value: [22]
916
- },
917
- {
918
- vr: "FD",
919
- _rawValue: [3.14159265358979], // trailing spaces allowed
920
- Value: [50.1242]
921
- },
922
- {
923
- vr: "IS",
924
- _rawValue: [" -123 "], // leading/trailing spaces & sign allowed
925
- Value: [0],
926
- newRawValue: ["0 "]
927
- },
928
- {
929
- vr: "LO",
930
- _rawValue: [" A long string with spaces "], // leading/trailing spaces allowed
931
- Value: ["A changed string that is still long."]
932
- },
933
- {
934
- vr: "LT",
935
- _rawValue: [
936
- " It may contain the Graphic Character set and the Control Characters, CR\r, LF\n, FF\f, and ESC\x1b. "
937
- ], // leading spaces significant, trailing spaces allowed
938
- Value: [" A modified string of text"]
939
- },
940
- {
941
- vr: "OB",
942
- _rawValue: [
943
- new Uint8Array([
944
- 0x13, 0x40, 0x80, 0x88, 0x88, 0x90, 0x88, 0x88
945
- ]).buffer
946
- ],
947
- Value: [new Uint8Array([0x01, 0x02]).buffer]
948
- },
949
- {
950
- vr: "OD",
951
- _rawValue: [
952
- new Uint8Array([
953
- 0x00, 0x00, 0x00, 0x54, 0x34, 0x6f, 0x9d, 0x41
954
- ]).buffer
955
- ],
956
- Value: [
957
- new Uint8Array([
958
- 0x00, 0x00, 0x00, 0x54, 0x35, 0x6e, 0x9e, 0x42
959
- ]).buffer
960
- ]
961
- },
962
- {
963
- vr: "OF",
964
- _rawValue: [
965
- new Uint8Array([
966
- 0x00, 0x00, 0x28, 0x41, 0x00, 0x00, 0x30, 0xc0, 0x00,
967
- 0x00, 0xf6, 0x42
968
- ]).buffer
969
- ],
970
- Value: [
971
- new Uint8Array([
972
- 0x00, 0x00, 0x28, 0x41, 0x00, 0x00, 0x30, 0xc0, 0x00,
973
- 0x00, 0xf6, 0x43
974
- ]).buffer
975
- ]
976
- },
977
- // TODO: VRs currently unimplemented
978
- // {
979
- // vr: 'OL',
980
- // _rawValue: [new Uint8Array([0x00, 0x00, 0x30, 0xC0, 0x00, 0x00, 0x28, 0x41, 0x00, 0x00, 0xF6, 0x42, 0x00, 0x00, 0x28, 0x41]).buffer],
981
- // },
982
- // {
983
- // vr: 'OV',
984
- // _rawValue: [new Uint8Array([0x00, 0x00, 0x30, 0xC0, 0x00, 0x00, 0x28, 0x41]).buffer],
985
- // },
986
- {
987
- vr: "OW",
988
- _rawValue: [
989
- new Uint8Array([
990
- 0x13, 0x40, 0x80, 0x88, 0x89, 0x91, 0x89, 0x89
991
- ]).buffer
992
- ],
993
- Value: [
994
- new Uint8Array([
995
- 0x13, 0x40, 0x80, 0x88, 0x88, 0x90, 0x88, 0x88
996
- ]).buffer
997
- ]
998
- },
999
- {
1000
- vr: "PN",
1001
- _rawValue: "Doe^John^A^Jr.^MD ", // trailing spaces allowed
1002
- Value: [{ Alphabetic: "Doe^Jane^A^Jr.^MD" }],
1003
- newRawValue: "Doe^Jane^A^Jr.^MD"
1004
- },
1005
- {
1006
- vr: "SH",
1007
- _rawValue: [" CT_SCAN_01 "], // leading/trailing spaces allowed
1008
- Value: ["MR_SCAN_91"]
1009
- },
1010
- {
1011
- vr: "SL",
1012
- _rawValue: [-2147483648],
1013
- Value: [-2147481234]
1014
- },
1015
- {
1016
- vr: "SS",
1017
- _rawValue: [-32768, 1234, 832],
1018
- Value: [1234]
1019
- },
1020
- {
1021
- vr: "ST",
1022
- _rawValue: [
1023
- "Patient complains of headaches over the last week. "
1024
- ], // trailing spaces allowed
1025
- Value: ["Patient complains of headaches"]
1026
- },
1027
- // TODO: VR currently unimplemented
1028
- // {
1029
- // vr: 'SV',
1030
- // _rawValue: [9007199254740993], // trailing spaces allowed
1031
- // },
1032
- {
1033
- vr: "TM",
1034
- _rawValue: ["42530.123456 "], // trailing spaces allowed
1035
- Value: ["42530"],
1036
- newRawValue: ["42530 "]
1037
- },
1038
- {
1039
- vr: "UC",
1040
- _rawValue: [
1041
- "Detailed description of procedure or clinical notes that could be very long. "
1042
- ], // trailing spaces allowed
1043
- Value: ["Detailed description of procedure and other things"]
1044
- },
1045
- {
1046
- vr: "UI",
1047
- _rawValue: ["1.2.840.10008.1.2.1"],
1048
- Value: ["1.2.840.10008.1.2.2"]
1049
- },
1050
- {
1051
- vr: "UL",
1052
- _rawValue: [4294967295],
1053
- Value: [1]
1054
- },
1055
- {
1056
- vr: "UR",
1057
- _rawValue: ["http://dicom.nema.org "], // trailing spaces ignored but allowed
1058
- Value: ["https://github.com/dcmjs-org"]
1059
- },
1060
- {
1061
- vr: "US",
1062
- _rawValue: [65535],
1063
- Value: [1]
1064
- },
1065
- {
1066
- vr: "UT",
1067
- _rawValue: [
1068
- " This is a detailed explanation that can span multiple lines and paragraphs in the DICOM dataset. "
1069
- ], // leading spaces significant, trailing spaces allowed
1070
- Value: [""]
1071
- }
1072
- // TODO: VR currently unimplemented
1073
- // {
1074
- // vr: 'UV',
1075
- // _rawValue: [18446744073709551616], // 2^64
1076
- // },
1077
- ];
1078
-
1079
- test.each(changedTestCases)(
1080
- `Test changed value overwrites original value following read and write - $vr`,
1081
- dataElement => {
1082
- const dataset = {
1083
- "00181041": {
1084
- ...dataElement
1085
- }
1086
- };
1087
-
1088
- const dicomDict = new DicomDict({});
1089
- dicomDict.dict = dataset;
1090
-
1091
- // write and re-read
1092
- const outputDicomDict = DicomMessage.readFile(
1093
- dicomDict.write(),
1094
- { forceStoreRaw: true }
1095
- );
1096
-
1097
- // expect raw value to be updated to match new Value parsed as Number to lose precision
1098
- expect(outputDicomDict.dict["00181041"]._rawValue).toEqual(
1099
- dataElement.newRawValue ?? dataElement.Value
1100
- );
1101
- expect(outputDicomDict.dict["00181041"].Value).toEqual(
1102
- dataElement.Value
1103
- );
1104
- }
1105
- );
1106
- });
1107
-
1108
- describe("sequences", () => {
1109
- test("nested sequences should support lossless round trip", () => {
1110
- const dataset = {
1111
- 52009229: {
1112
- vr: "SQ",
1113
- Value: [
1114
- {
1115
- "0020000E": {
1116
- vr: "UI",
1117
- Value: [
1118
- "1.3.6.1.4.1.5962.99.1.2280943358.716200484.1363785608958.1.1"
1119
- ]
1120
- },
1121
- "00089123": {
1122
- vr: "SQ",
1123
- Value: [
1124
- {
1125
- "00181030": {
1126
- vr: "AE",
1127
- _rawValue: [" TEST_AE "],
1128
- Value: ["TEST_AE"]
1129
- },
1130
- "00180050": {
1131
- vr: "DS",
1132
- Value: [5.0],
1133
- _rawValue: ["5.000 "]
1134
- }
1135
- },
1136
- {
1137
- "00181030": {
1138
- vr: "AE",
1139
- _rawValue: [" TEST_AE "],
1140
- Value: ["TEST_AE"]
1141
- },
1142
- "00180050": {
1143
- vr: "DS",
1144
- Value: [6.0],
1145
- _rawValue: ["6.000 "]
1146
- }
1147
- }
1148
- ]
1149
- }
1150
- },
1151
- {
1152
- "0020000E": {
1153
- vr: "UI",
1154
- Value: [
1155
- "1.3.6.1.4.1.5962.99.1.2280943358.716200484.1363785608958.1.2"
1156
- ]
1157
- },
1158
- "00089123": {
1159
- vr: "SQ",
1160
- Value: [
1161
- {
1162
- "00181030": {
1163
- vr: "LO",
1164
- Value: ["ABDOMEN MRI"]
1165
- },
1166
- "00180050": {
1167
- vr: "IS",
1168
- _rawValue: [" -123 "], // leading/trailing spaces & sign allowed
1169
- Value: [-123]
1170
- }
1171
- }
1172
- ]
1173
- }
1174
- }
1175
- ]
1176
- }
1177
- };
1178
-
1179
- const dicomDict = new DicomDict({});
1180
- dicomDict.dict = dataset;
1181
-
1182
- // confirm after write raw values are re-encoded
1183
- const outputBuffer = dicomDict.write();
1184
- const outputDicomDict = DicomMessage.readFile(outputBuffer);
1185
-
1186
- // lossless read/write should match entire data set
1187
- deepEqual(dicomDict.dict, outputDicomDict.dict);
1188
- });
1189
- });
1190
-
1191
- test("File dataset should be equal after read and write", async () => {
1192
- const inputBuffer = await getDcmjsDataFile(
1193
- "unknown-VR",
1194
- "sample-dicom-with-un-vr.dcm"
1195
- );
1196
- const dicomDict = DicomMessage.readFile(inputBuffer);
1197
-
1198
- // confirm raw string representation of DS contains extra additional metadata
1199
- // represented by bytes [30 2E 31 34 30 5C 30 2E 31 34 30 20]
1200
- expect(dicomDict.dict["00280030"]._rawValue).toEqual([
1201
- "0.140",
1202
- "0.140 "
1203
- ]);
1204
- expect(dicomDict.dict["00280030"].Value).toEqual([0.14, 0.14]);
1205
-
1206
- // confirm after write raw values are re-encoded
1207
- const outputBuffer = dicomDict.write();
1208
- const outputDicomDict = DicomMessage.readFile(outputBuffer);
1209
-
1210
- // explicitly verify for DS for clarity
1211
- expect(outputDicomDict.dict["00280030"]._rawValue).toEqual([
1212
- "0.140",
1213
- "0.140 "
1214
- ]);
1215
- expect(outputDicomDict.dict["00280030"].Value).toEqual([0.14, 0.14]);
1216
-
1217
- // lossless read/write should match entire data set
1218
- deepEqual(dicomDict.dict, outputDicomDict.dict);
1219
- });
1220
-
1221
- test("0 length PN tag should be retained following naturalize and de-naturalize", async () => {
1222
- const inputBuffer = await getDcmjsDataFile(
1223
- "empty-tag-round-trip",
1224
- "empty-person-name.dcm"
1225
- );
1226
- const origDicomDict = DicomMessage.readFile(inputBuffer);
1227
- const origNaturalizedDataset = DicomMetaDictionary.naturalizeDataset(
1228
- origDicomDict.dict
1229
- );
1230
-
1231
- // confirm starting dataset contains empty tag value for referring physician person name
1232
- expect(origDicomDict.dict["00080090"]._rawValue).toEqual("");
1233
- expect(origNaturalizedDataset.ReferringPhysicianName).toEqual([]);
1234
-
1235
- // re-encode the unnaturalized object
1236
- origDicomDict.dict = DicomMetaDictionary.denaturalizeDataset(
1237
- origNaturalizedDataset
1238
- );
1239
- const outputBuffer = origDicomDict.write();
1240
- const newDicomDict = DicomMessage.readFile(outputBuffer);
1241
- const newNaturalizedDataset = DicomMetaDictionary.naturalizeDataset(
1242
- origDicomDict.dict
1243
- );
1244
-
1245
- // confirm output referring physician name remains the same
1246
- expect(newDicomDict.dict["00080090"]._rawValue).toEqual("");
1247
- expect(newNaturalizedDataset.ReferringPhysicianName).toEqual([]);
1248
-
1249
- // confirm no other changes to the rest of the file
1250
- deepEqual(origDicomDict, newDicomDict);
1251
- });
1252
-
1253
- test("uncompressed data should be read correctly as arraybuffer", () => {
1254
- const buffer = fs.readFileSync("test/sample-dicom.dcm");
1255
- const dicomDict = DicomMessage.readFile(buffer.buffer);
1256
- // console.warn("fullData=", fullData);
1257
- const { dict } = dicomDict;
1258
- const [originalPixelArray] = dict["7FE00010"].Value;
1259
- expect(originalPixelArray).toBeInstanceOf(ArrayBuffer);
1260
- expect(originalPixelArray.byteLength).toBe(512 * 512 * 2);
1261
- const uint = new Uint16Array(originalPixelArray);
1262
- expect(uint[39138]).toBe(1);
1263
-
1264
- const natural = DicomMetaDictionary.naturalizeDataset(dict);
1265
- dicomDict.dict = DicomMetaDictionary.denaturalizeDataset(natural);
1266
-
1267
- const outputBuffer = dicomDict.write();
1268
- const outputDicomDict = DicomMessage.readFile(outputBuffer);
1269
-
1270
- const [outputPixelArray] = outputDicomDict.dict["7FE00010"].Value;
1271
- expect(outputPixelArray).toBeInstanceOf(ArrayBuffer);
1272
- const uintOut = new Uint16Array(outputPixelArray);
1273
- expect(uintOut.length).toBe(uint.length);
1274
- for (let i = 0; i < uint.length; i++) {
1275
- expect(uintOut[i]).toBe(uint[i]);
1276
- }
1277
- });
1278
-
1279
- test("uncompressed PixelData written with explicit length (524288) for streaming read", () => {
1280
- // test/sample-dicom.dcm is uncompressed data
1281
- const buffer = fs.readFileSync("test/sample-dicom.dcm");
1282
- const dicomDict = DicomMessage.readFile(buffer.buffer);
1283
- const { dict } = dicomDict;
1284
-
1285
- // Get original pixel data and compute hash
1286
- const originalPixelBytes = getPixelDataBytes(
1287
- dict[TagHex.PixelData].Value
1288
- );
1289
- const originalHash = hashArrayBuffer(originalPixelBytes);
1290
-
1291
- // Write to memory buffer
1292
- const natural = DicomMetaDictionary.naturalizeDataset(dict);
1293
- dicomDict.dict = DicomMetaDictionary.denaturalizeDataset(natural);
1294
- const outputBuffer = dicomDict.write();
1295
- const writtenDict = DicomMessage.readFile(outputBuffer);
1296
-
1297
- // PixelData must be written with explicit length 524288 (512*512*2), NOT undefined length, so a streaming reader can read it
1298
- const writtenPixelBytes = getPixelDataBytes(
1299
- writtenDict.dict[TagHex.PixelData].Value
1300
- );
1301
- expect(writtenPixelBytes.byteLength).toBe(524288);
1302
-
1303
- // Hash of pixel data must match original
1304
- const writtenHash = hashArrayBuffer(writtenPixelBytes);
1305
- expect(writtenHash).toBe(originalHash);
1306
- });
1307
-
1308
- test("Deflated Explicit VR Little Endian: PixelData written with explicit length (unencapsulated)", () => {
1309
- // When Transfer Syntax is DEFLATED_EXPLICIT_LITTLE_ENDIAN, pixel data is unencapsulated
1310
- // (raw bytes). The fix ensures unencapsulatedTransferSyntaxes includes it so PixelData
1311
- // is written with explicit length, not as encapsulated (undefined length). This test
1312
- // fails on master (without the fix) and passes with the fix.
1313
- const pixelBytes = new ArrayBuffer(4);
1314
- new Uint8Array(pixelBytes).set([0x01, 0x02, 0x03, 0x04]);
1315
- const transferSyntaxUid = DEFLATED_EXPLICIT_LITTLE_ENDIAN;
1316
- const meta = {
1317
- [TagHex.FileMetaInformationGroupLength]: {
1318
- vr: "UL",
1319
- Value: [32]
1320
- },
1321
- [TagHex.TransferSyntaxUID]: {
1322
- vr: "UI",
1323
- Value: [transferSyntaxUid]
1324
- }
1325
- };
1326
- const dict = {
1327
- [TagHex.PixelData]: {
1328
- vr: "OW",
1329
- Value: [pixelBytes]
1330
- }
1331
- };
1332
- const dicomDict = new DicomDict(meta);
1333
- dicomDict.dict = dict;
1334
-
1335
- const outputBuffer = dicomDict.write();
1336
- const arrayBuf =
1337
- outputBuffer instanceof ArrayBuffer
1338
- ? outputBuffer
1339
- : outputBuffer.buffer.slice(
1340
- outputBuffer.byteOffset,
1341
- outputBuffer.byteOffset + outputBuffer.byteLength
1342
- );
1343
- const pixelInfo = readPixelDataFromRawBuffer(
1344
- arrayBuf,
1345
- transferSyntaxUid
1346
- );
1347
- expect(pixelInfo).not.toBeNull();
1348
- expect(pixelInfo.length).not.toBe(-1);
1349
- expect(pixelInfo.data).not.toBeNull();
1350
- expect(pixelInfo.data.byteLength).toBe(4);
1351
- });
1352
-
1353
- test("compressed data should be read correctly as arraybuffer", () => {
1354
- const buffer = fs.readFileSync("test/sample-op.dcm");
1355
- const dicomDict = DicomMessage.readFile(buffer.buffer);
1356
- // console.warn("fullData=", fullData);
1357
- const { dict } = dicomDict;
1358
- const [originalPixelArray] = dict["7FE00010"].Value;
1359
- expect(originalPixelArray).toBeInstanceOf(ArrayBuffer);
1360
- // Values from dcmdump
1361
- expect(originalPixelArray.byteLength).toBe(101304);
1362
- const originalPixelBytes = new Uint8Array(originalPixelArray);
1363
- expect(originalPixelBytes[0]).toBe(255);
1364
- expect(originalPixelBytes[1]).toBe(216);
1365
-
1366
- const outputBuffer = dicomDict.write({ fragmentMultiframe: false });
1367
- const outputDicomDict = DicomMessage.readFile(outputBuffer);
1368
-
1369
- const [outputPixelArray] = outputDicomDict.dict["7FE00010"].Value;
1370
- expect(outputPixelArray).toBeInstanceOf(ArrayBuffer);
1371
- const outputPixelBytes = new Uint8Array(outputPixelArray);
1372
- expect(outputPixelBytes.length).toBe(originalPixelBytes.length);
1373
- for (let i = 0; i < originalPixelBytes.length; i++) {
1374
- expect(outputPixelBytes[i]).toBe(originalPixelBytes[i]);
1375
- }
1376
- });
1377
-
1378
- test("0 length US should use default value for both Value and rawValue", async () => {
1379
- const inputBuffer = await getDcmjsDataFile(
1380
- "empty-tag-round-trip",
1381
- "zero-length-US.dcm"
1382
- );
1383
- const origDicomDict = DicomMessage.readFile(inputBuffer);
1384
-
1385
- // expect sequence to be in file
1386
- expect(origDicomDict.dict["00180012"].Value).toBeTruthy();
1387
-
1388
- // Fetch bolus agent number from first sequence element
1389
- const contrastBolusAgentSq = origDicomDict.dict["00180012"].Value;
1390
- const bolusAgentNum = contrastBolusAgentSq[0]["00189337"];
1391
-
1392
- // verify default values parsed correctly
1393
- expect(bolusAgentNum.Value).toEqual([0]);
1394
- expect(bolusAgentNum._rawValue).toEqual([0]);
1395
- });
1396
- });
1397
-
1398
- const getDcmjsDataFile = async (release, fileName) => {
1399
- const url =
1400
- "https://github.com/dcmjs-org/data/releases/download/" +
1401
- release +
1402
- "/" +
1403
- fileName;
1404
- const dcmPath = await getTestDataset(url, fileName);
1405
-
1406
- return fs.readFileSync(dcmPath).buffer;
1407
- };