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.
- package/README.md +50 -0
- package/build/dcmjs.es.js +1071 -112
- package/build/dcmjs.es.js.map +1 -1
- package/build/dcmjs.js +1071 -112
- package/build/dcmjs.js.map +1 -1
- package/build/dcmjs.min.js +2 -2
- package/build/dcmjs.min.js.map +1 -1
- package/generate/dictionary.mjs +56029 -0
- package/package.json +18 -2
- package/.babelrc +0 -9
- package/.github/workflows/lint-and-format.yml +0 -27
- package/.github/workflows/publish-package.yml +0 -45
- package/.github/workflows/tests.yml +0 -24
- package/.prettierrc +0 -5
- package/.vscode/extensions.json +0 -7
- package/.vscode/settings.json +0 -8
- package/changelog.md +0 -31
- package/docs/ArrayBufferExpanderListener.md +0 -303
- package/docs/AsyncDicomReader-skill.md +0 -730
- package/eslint.config.mjs +0 -30
- package/generate-dictionary.js +0 -145
- package/jest.setup.js +0 -39
- package/netlify.toml +0 -22
- package/rollup.config.mjs +0 -57
- package/test/ArrayBufferExpanderListener.test.js +0 -365
- package/test/DICOMWEB.test.js +0 -1
- package/test/DicomMetaDictionary.test.js +0 -73
- package/test/SequenceOfItems.test.js +0 -86
- package/test/adapters.test.js +0 -43
- package/test/anonymizer.test.js +0 -176
- package/test/arrayItem.json +0 -351
- package/test/async-data.test.js +0 -575
- package/test/data-encoding.test.js +0 -59
- package/test/data-options.test.js +0 -199
- package/test/data.test.js +0 -1776
- package/test/derivations.test.js +0 -1
- package/test/helper/DicomDataReadBufferStreamBuilder.js +0 -89
- package/test/information-filter.test.js +0 -165
- package/test/integration/DicomMessage.readFile.test.js +0 -50
- package/test/lossless-read-write.test.js +0 -1407
- package/test/mocks/minimal_fields_dataset.json +0 -17
- package/test/mocks/null_number_vrs_dataset.json +0 -102
- package/test/normalizers.test.js +0 -38
- package/test/odd-frame-bit-data.js +0 -138
- package/test/rawTags.js +0 -170
- package/test/readBufferStream.test.js +0 -158
- package/test/sample-dicom.json +0 -904
- package/test/sample-op.lei +0 -0
- package/test/sample-sr.json +0 -997
- package/test/sr-tid.test.js +0 -251
- package/test/testUtils.js +0 -85
- package/test/utilities/deepEqual.test.js +0 -87
- package/test/utilities.test.js +0 -205
- package/test/video-test-dict.js +0 -40
- package/test/writeBufferStream.test.js +0 -149
|
@@ -1,730 +0,0 @@
|
|
|
1
|
-
# AsyncDicomReader Skill Guide
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
The `AsyncDicomReader` is an asynchronous binary DICOM file reader that provides streaming capabilities for parsing DICOM files. It's designed to handle
|
|
6
|
-
large DICOM files efficiently by reading and processing data incrementally rather than loading entire files into memory at once.
|
|
7
|
-
|
|
8
|
-
## Key Features
|
|
9
|
-
|
|
10
|
-
- **Asynchronous/Streaming**: Reads DICOM files incrementally using async/await patterns
|
|
11
|
-
- **Memory Efficient**: Uses buffer streaming with automatic clearing to reduce memory footprint
|
|
12
|
-
- **Multiple Transfer Syntaxes**: Supports Explicit Little Endian, Explicit Big Endian, and Implicit Little Endian as well as compressed syntaxes.
|
|
13
|
-
- **Pixel Data Handling**: Handles both compressed and uncompressed pixel data
|
|
14
|
-
- **Sequence Support**: Properly parses DICOM sequences with defined and undefined lengths
|
|
15
|
-
- **Character Set Support**: Automatically handles different character encodings, but not multiple character encodings.
|
|
16
|
-
- **Error Handling**: Configurable error handling for malformed files
|
|
17
|
-
|
|
18
|
-
## Current Limitations
|
|
19
|
-
|
|
20
|
-
- Files must contain the standard DICM preamble
|
|
21
|
-
- Interface is preliminary and subject to change
|
|
22
|
-
|
|
23
|
-
## Basic Usage
|
|
24
|
-
|
|
25
|
-
### Reading a Complete DICOM File
|
|
26
|
-
|
|
27
|
-
```javascript
|
|
28
|
-
import { AsyncDicomReader } from 'dcmjs';
|
|
29
|
-
|
|
30
|
-
async function readDicomFile(arrayBuffer) {
|
|
31
|
-
// Create reader instance
|
|
32
|
-
const reader = new AsyncDicomReader();
|
|
33
|
-
|
|
34
|
-
// Set the data source
|
|
35
|
-
reader.stream.setData(arrayBuffer);
|
|
36
|
-
|
|
37
|
-
// Read the entire file
|
|
38
|
-
await reader.readFile();
|
|
39
|
-
|
|
40
|
-
// Access the metadata and dataset
|
|
41
|
-
console.log('Meta information:', reader.meta);
|
|
42
|
-
console.log('Dataset:', reader.dict);
|
|
43
|
-
|
|
44
|
-
return reader;
|
|
45
|
-
}
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### Reading with Custom Options
|
|
49
|
-
|
|
50
|
-
```javascript
|
|
51
|
-
async function readDicomFileWithOptions(arrayBuffer) {
|
|
52
|
-
const reader = new AsyncDicomReader({
|
|
53
|
-
isLittleEndian: true,
|
|
54
|
-
clearBuffers: true // Automatically clear consumed buffers
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
reader.stream.setData(arrayBuffer);
|
|
58
|
-
|
|
59
|
-
// Read with error tolerance and custom meta size limit
|
|
60
|
-
await reader.readFile({
|
|
61
|
-
ignoreErrors: true,
|
|
62
|
-
maxSizeMeta: 1024 * 20, // 20KB max for meta header
|
|
63
|
-
listener: customListener // Optional custom listener
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
return reader;
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Using Custom Listeners
|
|
71
|
-
|
|
72
|
-
The reader uses a listener pattern to build the DICOM dataset. You can implement custom listeners to control how data is processed:
|
|
73
|
-
|
|
74
|
-
```javascript
|
|
75
|
-
import { DicomMetadataListener } from 'dcmjs';
|
|
76
|
-
|
|
77
|
-
class CustomListener extends DicomMetadataListener {
|
|
78
|
-
addTag(tag, tagInfo) {
|
|
79
|
-
console.log(`Processing tag: ${tag} (${tagInfo.name})`);
|
|
80
|
-
super.addTag(tag, tagInfo);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
value(val) {
|
|
84
|
-
// Process each value as it's read
|
|
85
|
-
super.value(val);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function readWithCustomListener(arrayBuffer) {
|
|
90
|
-
const reader = new AsyncDicomReader();
|
|
91
|
-
reader.stream.setData(arrayBuffer);
|
|
92
|
-
|
|
93
|
-
await reader.readFile({
|
|
94
|
-
listener: new CustomListener()
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
return reader;
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## Architecture
|
|
102
|
-
|
|
103
|
-
### Core Components
|
|
104
|
-
|
|
105
|
-
1. **BufferStream**: Manages the underlying data buffer with automatic memory management
|
|
106
|
-
2. **Listener Pattern**: Uses listeners to build the dataset incrementally
|
|
107
|
-
3. **Tag Reading**: Parses DICOM tags with proper VR (Value Representation) handling
|
|
108
|
-
4. **Transfer Syntax Detection**: Automatically detects and adapts to file transfer syntax
|
|
109
|
-
|
|
110
|
-
### Reading Process
|
|
111
|
-
|
|
112
|
-
The reader follows this sequence:
|
|
113
|
-
|
|
114
|
-
1. **Preamble & Marker** (128 bytes + "DICM")
|
|
115
|
-
2. **File Meta Information** (Group 0x0002)
|
|
116
|
-
3. **Transfer Syntax Detection** (from meta info)
|
|
117
|
-
4. **Dataset Reading** (main DICOM data)
|
|
118
|
-
|
|
119
|
-
```
|
|
120
|
-
┌─────────────────┐
|
|
121
|
-
│ 128B Preamble │
|
|
122
|
-
├─────────────────┤
|
|
123
|
-
│ DICM Marker │
|
|
124
|
-
├─────────────────┤
|
|
125
|
-
│ Meta Info │ (Group 0x0002, Explicit Little Endian)
|
|
126
|
-
├─────────────────┤
|
|
127
|
-
│ Dataset │ (Uses detected Transfer Syntax)
|
|
128
|
-
└─────────────────┘
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## Advanced Usage
|
|
132
|
-
|
|
133
|
-
### Reading Specific Sections
|
|
134
|
-
|
|
135
|
-
#### Reading Only Meta Information
|
|
136
|
-
|
|
137
|
-
```javascript
|
|
138
|
-
async function readMetaOnly(arrayBuffer) {
|
|
139
|
-
const reader = new AsyncDicomReader();
|
|
140
|
-
reader.stream.setData(arrayBuffer);
|
|
141
|
-
|
|
142
|
-
// Read preamble
|
|
143
|
-
const hasPreamble = await reader.readPreamble();
|
|
144
|
-
if (!hasPreamble) {
|
|
145
|
-
throw new Error('No DICM marker found');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Read only meta information
|
|
149
|
-
const meta = await reader.readMeta();
|
|
150
|
-
|
|
151
|
-
console.log('Transfer Syntax:', meta['00020010'].Value[0]);
|
|
152
|
-
console.log('SOP Class UID:', meta['00020002'].Value[0]);
|
|
153
|
-
|
|
154
|
-
return meta;
|
|
155
|
-
}
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
#### Reading Until Specific Tag
|
|
159
|
-
|
|
160
|
-
```javascript
|
|
161
|
-
async function readUntilTag(arrayBuffer, targetTag) {
|
|
162
|
-
const reader = new AsyncDicomReader();
|
|
163
|
-
reader.stream.setData(arrayBuffer);
|
|
164
|
-
|
|
165
|
-
await reader.readPreamble();
|
|
166
|
-
await reader.readMeta();
|
|
167
|
-
|
|
168
|
-
const listener = new DicomMetadataListener();
|
|
169
|
-
|
|
170
|
-
await reader.read(listener, {
|
|
171
|
-
untilTag: targetTag,
|
|
172
|
-
includeUntilTagValue: false
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
return listener.pop();
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### Handling Pixel Data
|
|
180
|
-
|
|
181
|
-
The reader automatically detects compressed vs uncompressed pixel data:
|
|
182
|
-
|
|
183
|
-
#### Uncompressed Pixel Data
|
|
184
|
-
|
|
185
|
-
```javascript
|
|
186
|
-
// Uncompressed pixel data is delivered as arrays
|
|
187
|
-
// Each frame is ALWAYS delivered as an array (ArrayBuffer[]), even for single frames
|
|
188
|
-
// Binary data can be fragmented - a single frame may be split across multiple ArrayBuffer fragments
|
|
189
|
-
// Multiple fragments are combined into one array per frame
|
|
190
|
-
// Large frames may be split into multiple fragments based on maxFragmentSize (default 128MB)
|
|
191
|
-
|
|
192
|
-
class PixelDataListener extends DicomMetadataListener {
|
|
193
|
-
constructor() {
|
|
194
|
-
super();
|
|
195
|
-
this.frames = [];
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
value(val) {
|
|
199
|
-
if (val instanceof ArrayBuffer || ArrayBuffer.isView(val)) {
|
|
200
|
-
this.frames.push(val);
|
|
201
|
-
}
|
|
202
|
-
super.value(val);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
#### Compressed Pixel Data
|
|
208
|
-
|
|
209
|
-
```javascript
|
|
210
|
-
// Compressed data is read as encapsulated format
|
|
211
|
-
// Each frame is ALWAYS delivered as an array (ArrayBuffer[]), even for single frames
|
|
212
|
-
// Video transfer syntaxes are handled as though they were a single frame
|
|
213
|
-
// Binary data can be delivered fragmented - a single frame may be split across multiple ArrayBuffer fragments
|
|
214
|
-
// Multiple fragments are combined into one array per frame
|
|
215
|
-
// Large frames may be split into multiple fragments based on maxFragmentSize (default 128MB)
|
|
216
|
-
|
|
217
|
-
async function readCompressedImage(arrayBuffer) {
|
|
218
|
-
const reader = new AsyncDicomReader();
|
|
219
|
-
const listener = new PixelDataListener();
|
|
220
|
-
|
|
221
|
-
reader.stream.setData(arrayBuffer);
|
|
222
|
-
await reader.readFile({ listener });
|
|
223
|
-
|
|
224
|
-
// listener.frames contains each compressed frame (each frame is an ArrayBuffer[])
|
|
225
|
-
return listener.frames;
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
### Working with Sequences
|
|
230
|
-
|
|
231
|
-
Sequences are automatically parsed with proper nesting:
|
|
232
|
-
|
|
233
|
-
```javascript
|
|
234
|
-
// Sequences are represented as arrays of objects
|
|
235
|
-
// Example: Referenced Series Sequence
|
|
236
|
-
|
|
237
|
-
async function readWithSequences(arrayBuffer) {
|
|
238
|
-
const reader = new AsyncDicomReader();
|
|
239
|
-
reader.stream.setData(arrayBuffer);
|
|
240
|
-
await reader.readFile();
|
|
241
|
-
|
|
242
|
-
// Access sequence items
|
|
243
|
-
const referencedSeries = reader.dict['00081115']; // Referenced Series Seq
|
|
244
|
-
if (referencedSeries) {
|
|
245
|
-
referencedSeries.Value.forEach((item, index) => {
|
|
246
|
-
console.log(`Series ${index}:`, item['0020000E']); // Series Instance UID
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return reader;
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
## Performance Considerations
|
|
255
|
-
|
|
256
|
-
### Memory Management
|
|
257
|
-
|
|
258
|
-
The reader uses automatic buffer clearing to minimize memory usage:
|
|
259
|
-
|
|
260
|
-
```javascript
|
|
261
|
-
// Enable buffer clearing (default)
|
|
262
|
-
const reader = new AsyncDicomReader({
|
|
263
|
-
clearBuffers: true
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// The stream automatically calls consume() to clear processed data
|
|
267
|
-
// This prevents memory buildup during large file reads
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
### Streaming Large Files
|
|
271
|
-
|
|
272
|
-
For very large files, the async nature prevents blocking:
|
|
273
|
-
|
|
274
|
-
```javascript
|
|
275
|
-
async function streamLargeFile(fileHandle) {
|
|
276
|
-
const reader = new AsyncDicomReader();
|
|
277
|
-
|
|
278
|
-
// Read file in chunks
|
|
279
|
-
const chunkSize = 1024 * 1024; // 1MB chunks
|
|
280
|
-
let offset = 0;
|
|
281
|
-
const chunks = [];
|
|
282
|
-
|
|
283
|
-
while (true) {
|
|
284
|
-
const { done, value } = await fileHandle.read(chunkSize);
|
|
285
|
-
if (done) break;
|
|
286
|
-
|
|
287
|
-
chunks.push(value);
|
|
288
|
-
offset += value.byteLength;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Combine chunks
|
|
292
|
-
const fullBuffer = new Uint8Array(offset);
|
|
293
|
-
let position = 0;
|
|
294
|
-
for (const chunk of chunks) {
|
|
295
|
-
fullBuffer.set(new Uint8Array(chunk), position);
|
|
296
|
-
position += chunk.byteLength;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
reader.stream.setData(fullBuffer.buffer);
|
|
300
|
-
await reader.readFile();
|
|
301
|
-
|
|
302
|
-
return reader;
|
|
303
|
-
}
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
## Error Handling
|
|
307
|
-
|
|
308
|
-
### Handling Malformed Files
|
|
309
|
-
|
|
310
|
-
```javascript
|
|
311
|
-
async function readWithErrorHandling(arrayBuffer) {
|
|
312
|
-
const reader = new AsyncDicomReader();
|
|
313
|
-
reader.stream.setData(arrayBuffer);
|
|
314
|
-
|
|
315
|
-
try {
|
|
316
|
-
await reader.readFile({
|
|
317
|
-
ignoreErrors: true, // Continue reading despite errors
|
|
318
|
-
maxSizeMeta: 1024 * 50 // Allow larger meta headers
|
|
319
|
-
});
|
|
320
|
-
} catch (error) {
|
|
321
|
-
console.error('Failed to read DICOM file:', error);
|
|
322
|
-
|
|
323
|
-
// Partial data may still be available
|
|
324
|
-
if (reader.meta) {
|
|
325
|
-
console.log('Meta information was read:', reader.meta);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
throw error;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return reader;
|
|
332
|
-
}
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
### Validation
|
|
336
|
-
|
|
337
|
-
```javascript
|
|
338
|
-
async function validateDicomFile(arrayBuffer) {
|
|
339
|
-
const reader = new AsyncDicomReader();
|
|
340
|
-
reader.stream.setData(arrayBuffer);
|
|
341
|
-
|
|
342
|
-
// Check for DICM marker
|
|
343
|
-
const hasPreamble = await reader.readPreamble();
|
|
344
|
-
if (!hasPreamble) {
|
|
345
|
-
return {
|
|
346
|
-
valid: false,
|
|
347
|
-
error: 'Missing DICM preamble'
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Validate meta information
|
|
352
|
-
try {
|
|
353
|
-
const meta = await reader.readMeta();
|
|
354
|
-
|
|
355
|
-
if (!meta['00020010']) {
|
|
356
|
-
return {
|
|
357
|
-
valid: false,
|
|
358
|
-
error: 'Missing Transfer Syntax UID'
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return {
|
|
363
|
-
valid: true,
|
|
364
|
-
transferSyntax: meta['00020010'].Value[0]
|
|
365
|
-
};
|
|
366
|
-
} catch (error) {
|
|
367
|
-
return {
|
|
368
|
-
valid: false,
|
|
369
|
-
error: error.message
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
## API Reference
|
|
376
|
-
|
|
377
|
-
### Constructor
|
|
378
|
-
|
|
379
|
-
```javascript
|
|
380
|
-
new AsyncDicomReader(options)
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
**Options:**
|
|
384
|
-
- `isLittleEndian` (boolean): Force specific endianness
|
|
385
|
-
- `clearBuffers` (boolean): Enable automatic buffer clearing (default: true)
|
|
386
|
-
- Additional options passed to `ReadBufferStream`
|
|
387
|
-
|
|
388
|
-
### Methods
|
|
389
|
-
|
|
390
|
-
#### `async readPreamble()`
|
|
391
|
-
Reads the 128-byte preamble and DICM marker.
|
|
392
|
-
|
|
393
|
-
**Returns:** `Promise<boolean>` - true if DICM marker found
|
|
394
|
-
|
|
395
|
-
#### `async readFile(options)`
|
|
396
|
-
Reads the entire DICOM file including meta information and dataset.
|
|
397
|
-
|
|
398
|
-
**Options:**
|
|
399
|
-
- `listener` (DicomMetadataListener): Custom listener for dataset building
|
|
400
|
-
- `ignoreErrors` (boolean): Continue reading despite errors
|
|
401
|
-
- `maxSizeMeta` (number): Maximum bytes to read for meta header
|
|
402
|
-
|
|
403
|
-
**Returns:** `Promise<AsyncDicomReader>` - The reader instance
|
|
404
|
-
|
|
405
|
-
#### `async readMeta(options)`
|
|
406
|
-
Reads only the file meta information (Group 0x0002).
|
|
407
|
-
|
|
408
|
-
**Options:**
|
|
409
|
-
- `maxSizeMeta` (number): Maximum bytes for meta header (default: 10KB)
|
|
410
|
-
- `ignoreErrors` (boolean): Allow reading files with malformed meta
|
|
411
|
-
|
|
412
|
-
**Returns:** `Promise<Object>` - Meta information dictionary
|
|
413
|
-
|
|
414
|
-
#### `async read(listener, options)`
|
|
415
|
-
Reads the dataset portion using the provided listener.
|
|
416
|
-
|
|
417
|
-
**Parameters:**
|
|
418
|
-
- `listener` (DicomMetadataListener): Listener to build dataset
|
|
419
|
-
- `options.untilOffset` (number): Stop reading at specific byte offset
|
|
420
|
-
|
|
421
|
-
**Returns:** `Promise<Object>` - Parsed dataset
|
|
422
|
-
|
|
423
|
-
#### `readTagHeader(options)`
|
|
424
|
-
Reads a single tag header (tag, VR, length).
|
|
425
|
-
|
|
426
|
-
**Options:**
|
|
427
|
-
- `untilTag` (string): Stop when reaching this tag
|
|
428
|
-
- `includeUntilTagValue` (boolean): Include the stop tag's value
|
|
429
|
-
|
|
430
|
-
**Returns:** `Object` - Tag information with structure:
|
|
431
|
-
```javascript
|
|
432
|
-
{
|
|
433
|
-
tag: string, // Tag as hex string (e.g., "00100010")
|
|
434
|
-
tagObj: Tag, // Tag object
|
|
435
|
-
vr: string, // VR as string (e.g., "PN")
|
|
436
|
-
vrObj: VR, // ValueRepresentation object
|
|
437
|
-
length: number, // Value length (-1 for undefined)
|
|
438
|
-
vm: string, // Value multiplicity
|
|
439
|
-
name: string // Tag name from dictionary
|
|
440
|
-
}
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
### Properties
|
|
444
|
-
|
|
445
|
-
- `syntax` (string): Current transfer syntax UID
|
|
446
|
-
- `meta` (Object): File meta information dictionary
|
|
447
|
-
- `dict` (Object): Dataset dictionary
|
|
448
|
-
- `stream` (ReadBufferStream): Underlying buffer stream
|
|
449
|
-
- `listener` (DicomMetadataListener): Current listener instance
|
|
450
|
-
|
|
451
|
-
## Common Patterns
|
|
452
|
-
|
|
453
|
-
### Extract Specific Tags
|
|
454
|
-
|
|
455
|
-
```javascript
|
|
456
|
-
async function extractTags(arrayBuffer, tagList) {
|
|
457
|
-
const reader = new AsyncDicomReader();
|
|
458
|
-
reader.stream.setData(arrayBuffer);
|
|
459
|
-
await reader.readFile();
|
|
460
|
-
|
|
461
|
-
const result = {};
|
|
462
|
-
for (const tag of tagList) {
|
|
463
|
-
if (reader.dict[tag]) {
|
|
464
|
-
result[tag] = reader.dict[tag].Value;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
return result;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// Usage
|
|
472
|
-
const tags = await extractTags(buffer, [
|
|
473
|
-
'00100010', // Patient Name
|
|
474
|
-
'00100020', // Patient ID
|
|
475
|
-
'0020000D', // Study Instance UID
|
|
476
|
-
]);
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
### Convert to JSON
|
|
480
|
-
|
|
481
|
-
```javascript
|
|
482
|
-
async function dicomToJson(arrayBuffer) {
|
|
483
|
-
const reader = new AsyncDicomReader();
|
|
484
|
-
reader.stream.setData(arrayBuffer);
|
|
485
|
-
await reader.readFile();
|
|
486
|
-
|
|
487
|
-
return {
|
|
488
|
-
meta: reader.meta,
|
|
489
|
-
dataset: reader.dict
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
```
|
|
493
|
-
|
|
494
|
-
### Read Multiple Files
|
|
495
|
-
|
|
496
|
-
```javascript
|
|
497
|
-
async function readMultipleFiles(arrayBuffers) {
|
|
498
|
-
const results = await Promise.all(
|
|
499
|
-
arrayBuffers.map(async (buffer) => {
|
|
500
|
-
const reader = new AsyncDicomReader();
|
|
501
|
-
reader.stream.setData(buffer);
|
|
502
|
-
await reader.readFile();
|
|
503
|
-
return {
|
|
504
|
-
meta: reader.meta,
|
|
505
|
-
dataset: reader.dict
|
|
506
|
-
};
|
|
507
|
-
})
|
|
508
|
-
);
|
|
509
|
-
|
|
510
|
-
return results;
|
|
511
|
-
}
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
## Bulkdata format
|
|
515
|
-
|
|
516
|
-
The bulkdata format in JSON is
|
|
517
|
-
```
|
|
518
|
-
"54001010": {
|
|
519
|
-
"vr": "OW",
|
|
520
|
-
"BulkDataURI": "./bulkdata/group1"
|
|
521
|
-
}
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
where the bulkdata uri can be an absolute or relative URI.
|
|
525
|
-
|
|
526
|
-
The encoding of the bulkdata URI is multipart/related. It is recommended to gzip the entire bulkdata instance for storage.
|
|
527
|
-
|
|
528
|
-
## Sequence of listener calls
|
|
529
|
-
|
|
530
|
-
The sequence of listener calls generated by `AsyncDicomReader` is:
|
|
531
|
-
|
|
532
|
-
```
|
|
533
|
-
// Start of dict parsing (after fmi)
|
|
534
|
-
listener.dict ||= {};
|
|
535
|
-
listener.startObject(this.dict);
|
|
536
|
-
// listener.current.level is 0 - top level object awaiting tags
|
|
537
|
-
|
|
538
|
-
// For every tag:
|
|
539
|
-
listener.addTag(tagHexString, tagInfo);
|
|
540
|
-
// listener.current.level is 1 - top level attributing awaiting value
|
|
541
|
-
|
|
542
|
-
if( isSequence ) {
|
|
543
|
-
// For sequences, recursively:
|
|
544
|
-
listener.startObject([]);
|
|
545
|
-
// listener.current.level will be 2
|
|
546
|
-
for(const child of sequence) {
|
|
547
|
-
listener.startObject();
|
|
548
|
-
// listener.current.level will be 3
|
|
549
|
-
... deliver child to listener
|
|
550
|
-
listener.pop();
|
|
551
|
-
}
|
|
552
|
-
listener.pop();
|
|
553
|
-
} else {
|
|
554
|
-
// For each value in the tag:
|
|
555
|
-
values.forEach(value => listener.value(value));
|
|
556
|
-
// listener.current should record the value in some way
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
listener.pop();
|
|
560
|
-
```
|
|
561
|
-
|
|
562
|
-
## Writing new listeners
|
|
563
|
-
|
|
564
|
-
Only the modified methods in the listener should be overwritten, as absent methods
|
|
565
|
-
simply mean to call the next listener. Listeners should implement the `addTag(next,tag,tagInfo)` version
|
|
566
|
-
of the methods and get added to the constructor of `DicomMetadataListener` or
|
|
567
|
-
another root listener.
|
|
568
|
-
|
|
569
|
-
### Raw Binary Data Feature
|
|
570
|
-
|
|
571
|
-
Starting with the `expectsRaw` feature, listeners can request raw binary data for specific tags by returning an object with `expectsRaw: true` from the `addTag` method. This is useful when you need to:
|
|
572
|
-
|
|
573
|
-
- Process tag data in its raw binary form without parsing
|
|
574
|
-
- Implement custom parsing logic for specific tags
|
|
575
|
-
- Extract binary data for external processing
|
|
576
|
-
- Optimize performance by skipping unnecessary parsing
|
|
577
|
-
|
|
578
|
-
**How it works:**
|
|
579
|
-
|
|
580
|
-
1. The `addTag` method returns an object with `expectsRaw: true`
|
|
581
|
-
2. AsyncDicomReader delivers the raw binary data as `ArrayBuffer` chunks via `listener.value()`
|
|
582
|
-
3. Binary data can be delivered fragmented - a single tag's data may be split across multiple ArrayBuffer fragments
|
|
583
|
-
4. Multiple fragments are delivered sequentially via multiple `listener.value()` calls
|
|
584
|
-
5. Data is split into fragments based on `maxFragmentSize` (default 128MB) with buffer consumption between chunks for memory efficiency
|
|
585
|
-
6. This only works for non-pixel data tags with positive length
|
|
586
|
-
7. Pixel data and sequences continue to use their specialized handlers
|
|
587
|
-
|
|
588
|
-
**Example:**
|
|
589
|
-
|
|
590
|
-
```javascript
|
|
591
|
-
class RawBinaryListener extends DicomMetadataListener {
|
|
592
|
-
constructor(tagsToReceiveRaw = []) {
|
|
593
|
-
super();
|
|
594
|
-
this.tagsToReceiveRaw = new Set(tagsToReceiveRaw);
|
|
595
|
-
this.rawDataReceived = {};
|
|
596
|
-
this.rawChunks = {};
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
addTag(tag, tagInfo) {
|
|
600
|
-
// Call the parent implementation
|
|
601
|
-
const result = super.addTag(tag, tagInfo);
|
|
602
|
-
|
|
603
|
-
// Request raw binary data for specific tags
|
|
604
|
-
if (this.tagsToReceiveRaw.has(tag)) {
|
|
605
|
-
// Initialize chunk collector for this tag
|
|
606
|
-
this.rawChunks[tag] = [];
|
|
607
|
-
return { expectsRaw: true };
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
return result;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
value(v) {
|
|
614
|
-
// Track raw binary data received (may be delivered in multiple chunks)
|
|
615
|
-
if (this.current && this.tagsToReceiveRaw.has(this.current.tag)) {
|
|
616
|
-
if (v instanceof ArrayBuffer) {
|
|
617
|
-
this.rawChunks[this.current.tag].push(v);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
return super.value(v);
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
pop() {
|
|
624
|
-
// When tag is complete, combine chunks if needed
|
|
625
|
-
if (this.current && this.tagsToReceiveRaw.has(this.current.tag)) {
|
|
626
|
-
const tag = this.current.tag;
|
|
627
|
-
const chunks = this.rawChunks[tag];
|
|
628
|
-
|
|
629
|
-
if (chunks.length === 1) {
|
|
630
|
-
this.rawDataReceived[tag] = chunks[0];
|
|
631
|
-
} else if (chunks.length > 1) {
|
|
632
|
-
// Combine multiple chunks into a single ArrayBuffer
|
|
633
|
-
const totalSize = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
634
|
-
const combined = new Uint8Array(totalSize);
|
|
635
|
-
let offset = 0;
|
|
636
|
-
for (const chunk of chunks) {
|
|
637
|
-
combined.set(new Uint8Array(chunk), offset);
|
|
638
|
-
offset += chunk.byteLength;
|
|
639
|
-
}
|
|
640
|
-
this.rawDataReceived[tag] = combined.buffer;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
delete this.rawChunks[tag];
|
|
644
|
-
}
|
|
645
|
-
return super.pop();
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Usage
|
|
650
|
-
const listener = new RawBinaryListener([
|
|
651
|
-
'00100010', // Patient Name
|
|
652
|
-
'00080060', // Modality
|
|
653
|
-
'00201041', // Slice Location
|
|
654
|
-
]);
|
|
655
|
-
|
|
656
|
-
const reader = new AsyncDicomReader();
|
|
657
|
-
reader.stream.setData(arrayBuffer);
|
|
658
|
-
await reader.readFile({ listener });
|
|
659
|
-
|
|
660
|
-
// Access raw binary data (now combined from all chunks)
|
|
661
|
-
console.log(listener.rawDataReceived);
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
**Limitations:**
|
|
665
|
-
- Only works for non-pixel data tags (pixel data uses dedicated handlers)
|
|
666
|
-
- Only works for tags with positive length (not undefined length)
|
|
667
|
-
- The tag must have actual data present in the file
|
|
668
|
-
|
|
669
|
-
## Image Frames
|
|
670
|
-
|
|
671
|
-
Image frames are numbered starting at 1, with the index in the pixel data
|
|
672
|
-
tag starting at 0. **Each frame is ALWAYS delivered as an array (ArrayBuffer[]), even for single frames.**
|
|
673
|
-
|
|
674
|
-
The value of both uncompressed and compressed image frames is one of:
|
|
675
|
-
|
|
676
|
-
- BulkDataURI
|
|
677
|
-
- ArrayBuffer[] containing the frame data (always an array, even for single frames)
|
|
678
|
-
|
|
679
|
-
**Important behaviors:**
|
|
680
|
-
|
|
681
|
-
- **Video transfer syntaxes**: Video is handled as though it were a single frame. All video frames are delivered as a single array containing all frame fragments.
|
|
682
|
-
- **Fragmented binary data**: Binary data can be delivered fragmented. A single frame may be split across multiple ArrayBuffer fragments based on `maxFragmentSize` (default 128MB). Multiple fragments are combined into one array per frame.
|
|
683
|
-
- **Frame delivery**: Each frame is delivered via `listener.startObject([])` followed by one or more `listener.value(fragment)` calls (one per fragment), then `listener.pop()`.
|
|
684
|
-
|
|
685
|
-
## Best Practices
|
|
686
|
-
|
|
687
|
-
2. **Use error handling** for production code - not all DICOM files are well-formed
|
|
688
|
-
3. **Implement custom listeners** storing data as bulkdata.
|
|
689
|
-
4. **Enable buffer clearing** (default) for memory efficiency
|
|
690
|
-
5. **Validate transfer syntax** before processing pixel data
|
|
691
|
-
6. **Use ignoreErrors cautiously** - it may skip important data
|
|
692
|
-
|
|
693
|
-
## Troubleshooting
|
|
694
|
-
|
|
695
|
-
### Issue: "Invalid DICOM file, meta length tag is malformed"
|
|
696
|
-
**Solution:** Use `ignoreErrors: true` option or check if file has proper DICM preamble
|
|
697
|
-
|
|
698
|
-
### Issue: Out of memory errors
|
|
699
|
-
**Solution:** Ensure `clearBuffers: true` and process pixel data incrementally with custom listener
|
|
700
|
-
|
|
701
|
-
### Issue: "Can't handle tag with -1 length and not sequence"
|
|
702
|
-
**Solution:** File may have non-standard undefined length tags - this is invalid DICOM
|
|
703
|
-
|
|
704
|
-
### Issue: Incorrect character encoding
|
|
705
|
-
**Solution:** Check Specific Character Set (0008,0005) tag and verify encoding mapping
|
|
706
|
-
|
|
707
|
-
## Related Components
|
|
708
|
-
|
|
709
|
-
- **DicomMessage**: Synchronous DICOM reader for smaller files
|
|
710
|
-
- **ReadBufferStream**: Underlying streaming buffer implementation
|
|
711
|
-
- **ValueRepresentation**: VR parsing and value extraction
|
|
712
|
-
- **DicomMetadataListener**: Default listener for building datasets
|
|
713
|
-
- **Tag**: DICOM tag parsing and manipulation
|
|
714
|
-
|
|
715
|
-
## Future Enhancements
|
|
716
|
-
|
|
717
|
-
The AsyncDicomReader is marked as preliminary. Future versions may include:
|
|
718
|
-
|
|
719
|
-
- Compressed transfer syntax support (JPEG, JPEG-LS, RLE, JPEG 2000)
|
|
720
|
-
- Files without DICM preamble
|
|
721
|
-
- Improved streaming for network sources
|
|
722
|
-
- Progress callbacks
|
|
723
|
-
- Cancelable operations
|
|
724
|
-
- More robust error recovery
|
|
725
|
-
|
|
726
|
-
## References
|
|
727
|
-
|
|
728
|
-
- DICOM Standard PS3.10 (Media Storage and File Format)
|
|
729
|
-
- DICOM Standard PS3.5 (Data Structures and Encoding)
|
|
730
|
-
- [dcmjs Documentation](https://github.com/dcmjs-org/dcmjs)
|