dcmjs

repository·master·Indexed 43 days ago

https://github.com/dcmjs-org/dcmjs

A JavaScript implementation for DICOM manipulation compatible with browser and Node.js environments. It provides tools for bidirectional conversion between binary DICOM and JSON representations, with a focus on modern standards such as Enhanced Multiframe, Segmentation, and Structured Reports. Version 0.49.2 includes the AsyncDicomReader for memory-efficient streaming parsing and the DicomMetadataListener for incremental dataset building.

Tokens
20.5K
Snippets
61
Records
95
Agent score
55%

What's inside dcmjs

  1. Use AsyncDicomReader for streaming DICOM parsing

    master

    The AsyncDicomReader is an asynchronous, memory-efficient reader designed for large DICOM files. It uses a streaming approach to process data incrementally using async/await patterns, which helps reduce the memory footprint by avoiding loading entire files into memory at once.

    Key Features:

    • Asynchronous/Streaming: Incremental reading via async/await.
    • Memory Efficient: Uses BufferStream with automatic clearing of consumed buffers.
    • Transfer Syntax Support: Handles Explicit/Implicit Little/Big Endian and compressed syntaxes.
    • Sequence Support: Parses DICOM sequences with defined/undefined lengths.
    • Pixel Data: Handles both compressed and uncompressed pixel data.

    Limitations:

    • Files must contain the standard DICM preamble.
    • The interface is preliminary and subject to change.
    import { AsyncDicomReader } from 'dcmjs';
    
    async function readDicomFile(arrayBuffer) {
      const reader = new AsyncDicomReader();
      reader.stream.setData(arrayBuffer);
      await reader.readFile();
      
      console.log('Meta information:', reader.meta);
      console.log('Dataset:', reader.dict);
      return reader;
    }
  2. Handle Pixel Data in AsyncDicomReader

    master

    The reader handles both uncompressed and compressed pixel data.

    Important Note on Pixel Data Delivery:

    • For both compressed and uncompressed data, each frame is always delivered as an array of ArrayBuffer objects (ArrayBuffer[]), even if there is only a single frame.
    • Binary data may be fragmented; a single frame might be split across multiple ArrayBuffer fragments. The reader combines these fragments into one array per frame.
    • Large frames may be split based on maxFragmentSize (default 128MB).
    • For compressed data, it is read as an encapsulated format.
    • Video transfer syntaxes are treated as a single frame.
    class PixelDataListener extends DicomMetadataListener {
      constructor() {
        super();
        this.frames = [];
      }
      
      value(val) {
        // val is an ArrayBuffer[] representing a frame
        if (val instanceof ArrayBuffer || ArrayBuffer.isView(val)) {
          this.frames.push(val);
        }
        super.value(val);
      }
    }
    
    async function readCompressedImage(arrayBuffer) {
      const reader = new AsyncDicomReader();
      const listener = new PixelDataListener();
      
      reader.stream.setData(arrayBuffer);
      await reader.readFile({ listener });
      
      return listener.frames;
    }
  3. Implement a Raw Binary Listener for specific tags

    master

    If you need to process tag data in its raw binary form (e.g., to avoid parsing overhead or for custom logic), you can implement a custom listener that returns { expectsRaw: true } from the addTag method.

    How it works:

    1. addTag returns { expectsRaw: true } for the target tag.
    2. AsyncDicomReader delivers raw ArrayBuffer chunks via listener.value().
    3. Data may be delivered in multiple fragments (up to maxFragmentSize, default 128MB).
    4. You must collect these fragments and combine them (e.g., using Uint8Array.set) when pop() is called.

    Limitations:

    • Only works for non-pixel data tags.
    • Only works for tags with positive length (not undefined length).
    • The tag must have actual data present in the file.
    class RawBinaryListener extends DicomMetadataListener {
      constructor(tagsToReceiveRaw = []) {
        super();
        this.tagsToReceiveRaw = new Set(tagsToReceiveRaw);
        this.rawDataReceived = {};
        this.rawChunks = {};
      }
    
      addTag(tag, tagInfo) {
        const result = super.addTag(tag, tagInfo);
        if (this.tagsToReceiveRaw.has(tag)) {
          this.rawChunks[tag] = [];
          return { expectsRaw: true };
        }
        return result;
      }
    
      value(v) {
        if (this.current && this.tagsToReceiveRaw.has(this.current.tag)) {
          if (v instanceof ArrayBuffer) {
            this.rawChunks[this.current.tag].push(v);
          }
        }
        return super.value(v);
      }
      
      pop() {
        if (this.current && this.tagsToReceiveRaw.has(this.current.tag)) {
          const tag = this.current.tag;
          const chunks = this.rawChunks[tag];
          
          if (chunks.length === 1) {
            this.rawDataReceived[tag] = chunks[0];
          } else if (chunks.length > 1) {
            const totalSize = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
            const combined = new Uint8Array(totalSize);
            let offset = 0;
            for (const chunk of chunks) {
              combined.set(new Uint8Array(chunk), offset);
              offset += chunk.byteLength;
            }
            this.rawDataReceived[tag] = combined.buffer;
          }
          delete this.rawChunks[tag];
        }
        return super.pop();
      }
    }
    
    // Usage
    const listener = new RawBinaryListener(['00100010', '00080060', '00201041']);
    const reader = new AsyncDicomReader();
    reader.stream.setData(arrayBuffer);
    await reader.readFile({ listener });
    console.log(listener.rawDataReceived);
  4. Understand Image Frame delivery

    master

    When using a listener to process image frames, note the following behaviors:

    • Frame Indexing: Image frames are numbered starting at 1, while the index in the pixel data tag starts at 0.
    • Data Format: Every frame is always delivered as an array of ArrayBuffer objects (ArrayBuffer[]), even if there is only a single frame.
    • Video Transfer Syntaxes: Video is treated as a single frame; all video frames are delivered as one single array containing all fragments.
    • Fragmentation: Binary data may be split into multiple fragments based on maxFragmentSize (default 128MB). Multiple fragments are combined into one array per frame.
    • Lifecycle: Each frame is delivered via: listener.startObject([]) $\rightarrow$ one or more listener.value(fragment) calls $\rightarrow$ listener.pop().
  5. How AsyncDicomReader architecture works

    master

    The AsyncDicomReader follows a specific sequence to parse a DICOM file:

    1. Preamble & Marker: Reads the first 128 bytes plus the DICM marker.
    2. File Meta Information: Parses Group 0x0002.
    3. Transfer Syntax Detection: Automatically detects the transfer syntax from the meta information.
    4. Dataset Reading: Parses the main DICOM data using the detected transfer syntax.

    Core Components:

    • BufferStream: Manages the underlying data buffer with automatic memory management.
    • Listener Pattern: Uses listeners to build the dataset incrementally.
    • Tag Reading: Parses DICOM tags with proper VR (Value Representation) handling.
    • Transfer Syntax Detection: Automatically adapts to the file's transfer syntax.
  6. Configure filter order in DicomMetadataListener

    master

    When using DicomMetadataListener, the order in which you pass filters to the constructor determines the execution sequence. This is critical when using ArrayBufferExpanderFilter alongside custom logic.

    • To process individual fragments: Place ArrayBufferExpanderFilter before your custom filter. The expander will break the array into fragments, and your filter will receive each fragment one by one.
    • To process the original array: Place ArrayBufferExpanderFilter after your custom filter. Your filter will receive the full ArrayBuffer[] before the expansion occurs.
    // Case 1: Custom filter sees individual fragments
    const listener = new DicomMetadataListener(
      ArrayBufferExpanderFilter,
      customFilter
    );
    
    // Case 2: Custom filter sees the original ArrayBuffer[]
    const listener2 = new DicomMetadataListener(
      customFilter,
      ArrayBufferExpanderFilter
    );
  7. What dcmjs does and does not support

    master

    Supported DICOM features

    dcmjs focuses on modern DICOM objects and manipulation:

    • Enhanced Multiframe Images
    • Segmentation Objects
    • Parametric Maps
    • Structured Reports
    • Bidirectional conversion between Part 10 binary DICOM and DICOM standard JSON (DICOMweb).
    • Bidirectional conversion between DICOM standard JSON and a high-level "naturalized" JSON form.

    Unsupported features

    dcmjs is not intended for the following:

    • DIMSE networking (C-STORE, C-FIND, etc.): Use dcmjs-dimse instead.
    • Image rendering: Use dcmjs-imaging.
    • Transcoding encapsulated transfer syntax: Use dcmjs-codecs.
    • 3D rendering: Use vtk.js.
    • Deidentification/Data organization: Use dcm-organize.
    • Physical Media (optical disks).
  8. How ArrayBufferExpanderFilter works and its detection logic

    master

    The ArrayBufferExpanderFilter intercepts the value(next, v) call. If the value v is detected as an array where every element is an ArrayBuffer or a typed array view (e.g., Uint8Array), the filter performs the following sequence using the DicomMetadataListener context:

    1. Calls this.startObject([]) to create a new array context.
    2. Calls this.value(fragment) for each individual ArrayBuffer in the array.
    3. Calls this.pop() to finalize the array structure.
    4. Assigns the resulting array to the tag's Value field.

    Detection Criteria: A value is considered an ArrayBuffer[] if it is an Array and item instanceof ArrayBuffer || ArrayBuffer.isView(item) is true for all elements.

    Limitations:

    • It only expands arrays of ArrayBuffers; it does not expand arrays of other types (like strings).
    • It is not recursive (only one level deep).
    • It requires the complete array to be present; it cannot partially expand a stream.
  9. Install dcmjs in Node.js

    master

    You can add dcmjs to your Node.js project using pnpm, npm, or yarn.

    To install the latest stable release:

    pnpm add dcmjs

    To install the latest development code from the master branch:

    pnpm add dcmjs@dev
    pnpm add dcmjs       # latest stable release
    pnpm add dcmjs@dev   # latest code merged to master
  10. Use ArrayBufferExpanderFilter to handle fragmented pixel data

    master

    The ArrayBufferExpanderFilter is a filter for DicomMetadataListener that converts ArrayBuffer[] values into an expanded sequence of startObject([]), value(fragment), and pop() calls.

    This is essential when working with AsyncDicomReader because compressed or fragmented pixel data might be delivered as a single array of buffers. The expander ensures that subsequent filters in the chain receive these as individual fragments, allowing for consistent handling of compressed frames, multi-frame images with offset tables, and video sequences.

    import { AsyncDicomReader } from 'dcmjs';
    import { DicomMetadataListener, ArrayBufferExpanderFilter } from 'dcmjs/utilities';
    
    async function readDicomWithExpansion(arrayBuffer) {
      const reader = new AsyncDicomReader();
      reader.stream.setData(arrayBuffer);
      
      // Create listener with the expander filter
      const listener = new DicomMetadataListener(ArrayBufferExpanderFilter);
      
      // Read the file using the listener
      await reader.readFile({ listener });
      
      // Access the result
      return listener.dict;
    }
  11. Handle errors and validate DICOM files

    master

    When working with potentially malformed files, use ignoreErrors: true in readFile() to attempt to recover partial data. You can also manually validate a file by checking for the DICM preamble and the presence of required meta information like the Transfer Syntax UID.

    async function readWithErrorHandling(arrayBuffer) {
      const reader = new AsyncDicomReader();
      reader.stream.setData(arrayBuffer);
      
      try {
        await reader.readFile({
          ignoreErrors: true,
          maxSizeMeta: 1024 * 50
        });
      } catch (error) {
        console.error('Failed to read DICOM file:', error);
        if (reader.meta) {
          console.log('Meta information was read:', reader.meta);
        }
        throw error;
      }
      return reader;
    }
    
    async function validateDicomFile(arrayBuffer) {
      const reader = new AsyncDicomReader();
      reader.stream.setData(arrayBuffer);
      
      const hasPreamble = await reader.readPreamble();
      if (!hasPreamble) return { valid: false, error: 'Missing DICM preamble' };
      
      try {
        const meta = await reader.readMeta();
        if (!meta['00020010']) return { valid: false, error: 'Missing Transfer Syntax UID' };
        return { valid: true, transferSyntax: meta['00020010'].Value[0] };
      } catch (error) {
        return { valid: false, error: error.message };
      }
    }