src_image_dicomBufferToView.js

import {logger} from '../utils/logger';
import {
  DicomParser,
  getSyntaxDecompressionName
} from '../dicom/dicomParser';
import {ImageFactory} from './imageFactory';
import {MaskFactory} from './maskFactory';
import {PixelBufferDecoder} from './decoder';
import {AnnotationGroupFactory} from './annotationGroupFactory';

// doc imports
/* eslint-disable no-unused-vars */
import {DataElement} from '../dicom/dataElement';
import {DicomData} from '../app/dataController';
/* eslint-enable no-unused-vars */

/**
 * Create a View from a DICOM buffer.
 */
export class DicomBufferToView {

  /**
   * Converter options.
   *
   * @type {object}
   */
  #options;

  /**
   * Set the converter options.
   *
   * @param {object} opt The input options.
   */
  setOptions(opt) {
    this.#options = opt;
  }

  /**
   * Pixel buffer decoder.
   * Define only once to allow optional asynchronous mode.
   *
   * @type {object}
   */
  #pixelDecoder = null;

  // local tmp storage
  #dicomParserStore = [];
  #finalBufferStore = [];
  #decompressedSizes = [];
  #factories = [];

  /**
   * Get the factory associated to input DICOM elements.
   *
   * @param {Object<string, DataElement>} elements The DICOM elements.
   * @returns {ImageFactory|MaskFactory|AnnotationGroupFactory|undefined}
   *   The associated factory.
   */
  #getFactory(elements) {
    let factory;
    const modalityElement = elements['00080060'];
    if (typeof modalityElement !== 'undefined') {
      const modality = modalityElement.value[0];
      if (modality === 'SEG') {
        // mask factory for DICOM SEG
        factory = new MaskFactory();
      } else if (modality === 'SR') {
        // annotation factory for DICOM SR
        factory = new AnnotationGroupFactory();
      }
    }
    // image factory for pixel data
    if (typeof factory === 'undefined') {
      const pixelElement = elements['7FE00010'];
      if (typeof pixelElement !== 'undefined') {
        factory = new ImageFactory();
      }
    }
    return factory;
  }

  /**
   * Generate the data object.
   *
   * @param {number} index The data index.
   * @param {string} origin The data origin.
   */
  #generateData(index, origin) {
    const dataElements = this.#dicomParserStore[index].getDicomElements();
    const factory = this.#factories[index];
    // exit if no factory
    if (typeof factory === 'undefined') {
      return;
    }
    // create data
    try {
      const data = new DicomData(dataElements);
      if (factory instanceof AnnotationGroupFactory) {
        data.annotationGroup = factory.create(dataElements);
      } else {
        data.image = factory.create(
          dataElements,
          this.#finalBufferStore[index],
          this.#options.numberOfFiles);
      }
      // call onloaditem
      this.onloaditem({
        data: data,
        source: origin,
        warn: factory.getWarning()
      });
    } catch (error) {
      this.onerror({
        error: error,
        source: origin
      });
      this.onloadend({
        source: origin
      });
    }
  }

  /**
   * Generate the image object from an uncompressed buffer.
   *
   * @param {number} index The data index.
   * @param {string} origin The data origin.
   */
  #generateImageUncompressed(index, origin) {
    // send progress
    this.onprogress({
      lengthComputable: true,
      loaded: 100,
      total: 100,
      index: index,
      source: origin
    });
    // generate image
    this.#generateData(index, origin);
    // send load events
    this.onload({
      source: origin
    });
    this.onloadend({
      source: origin
    });
  }

  /**
   * Generate the image object from an compressed buffer.
   *
   * @param {number} index The data index.
   * @param {Array} pixelBuffer The dicom parser.
   * @param {string} algoName The compression algorithm name.
   */
  #generateImageCompressed(index, pixelBuffer, algoName) {
    const dicomParser = this.#dicomParserStore[index];

    // gather pixel buffer meta data
    const bitsAllocated =
      dicomParser.getDicomElements()['00280100'].value[0];
    const pixelRepresentation =
      dicomParser.getDicomElements()['00280103'].value[0];
    const pixelMeta = {
      bitsAllocated: bitsAllocated,
      isSigned: (pixelRepresentation === 1)
    };
    const columnsElement = dicomParser.getDicomElements()['00280011'];
    const rowsElement = dicomParser.getDicomElements()['00280010'];
    if (typeof columnsElement !== 'undefined' &&
      typeof rowsElement !== 'undefined') {
      pixelMeta.sliceSize = columnsElement.value[0] * rowsElement.value[0];
    }
    const samplesPerPixelElement =
      dicomParser.getDicomElements()['00280002'];
    if (typeof samplesPerPixelElement !== 'undefined') {
      pixelMeta.samplesPerPixel = samplesPerPixelElement.value[0];
    }
    const planarConfigurationElement =
      dicomParser.getDicomElements()['00280006'];
    if (typeof planarConfigurationElement !== 'undefined') {
      pixelMeta.planarConfiguration = planarConfigurationElement.value[0];
    }

    const numberOfItems = pixelBuffer.length;

    // setup the decoder (one decoder per all converts)
    if (this.#pixelDecoder === null) {
      this.#pixelDecoder = new PixelBufferDecoder(
        algoName, numberOfItems);
      // callbacks
      // pixelDecoder.ondecodestart: nothing to do
      this.#pixelDecoder.ondecodeditem = (event) => {
        this.#onDecodedItem(event);
        // send onload and onloadend when all items have been decoded
        if (event.itemNumber + 1 === event.numberOfItems) {
          this.onload(event);
          this.onloadend(event);
        }
      };
      // pixelDecoder.ondecoded: nothing to do
      // pixelDecoder.ondecodeend: nothing to do
      this.#pixelDecoder.onerror = this.onerror;
      this.#pixelDecoder.onabort = this.onabort;
    }

    // launch decode
    for (let i = 0; i < numberOfItems; ++i) {
      this.#pixelDecoder.decode(pixelBuffer[i], pixelMeta,
        {
          itemNumber: i,
          numberOfItems: numberOfItems,
          index: index
        }
      );
    }
  }

  /**
   * Handle a decoded item event.
   *
   * @param {object} event The decoded item event.
   */
  #onDecodedItem(event) {
    // send progress
    this.onprogress({
      lengthComputable: true,
      loaded: event.itemNumber + 1,
      total: event.numberOfItems,
      index: event.index,
      source: origin
    });

    const dataIndex = event.index;

    // store decoded data
    const decodedData = event.data[0];
    if (event.numberOfItems !== 1) {
      // allocate buffer if not done yet
      if (typeof this.#decompressedSizes[dataIndex] === 'undefined') {
        this.#decompressedSizes[dataIndex] = decodedData.length;
        const fullSize = event.numberOfItems *
          this.#decompressedSizes[dataIndex];
        try {
          this.#finalBufferStore[dataIndex] =
            new decodedData.constructor(fullSize);
        } catch (error) {
          if (error instanceof RangeError) {
            const powerOf2 = Math.floor(Math.log(fullSize) / Math.log(2));
            logger.error('Cannot allocate ' +
              decodedData.constructor.name +
              ' of size: ' +
              fullSize + ' (>2^' + powerOf2 + ') for decompressed data.');
          }
          // abort
          this.#pixelDecoder.abort();
          // send events
          this.onerror({
            error: error,
            source: origin
          });
          this.onloadend({
            source: origin
          });
          // exit
          return;
        }
      }
      // hoping for all items to have the same size...
      if (decodedData.length !== this.#decompressedSizes[dataIndex]) {
        logger.warn('Unsupported varying decompressed data size: ' +
          decodedData.length + ' != ' + this.#decompressedSizes[dataIndex]);
      }
      // set buffer item data
      this.#finalBufferStore[dataIndex].set(
        decodedData, this.#decompressedSizes[dataIndex] * event.itemNumber);
    } else {
      this.#finalBufferStore[dataIndex] = decodedData;
    }

    // create image for the first item
    if (event.itemNumber === 0) {
      this.#generateData(dataIndex, origin);
    }
  }

  /**
   * Handle non image data.
   *
   * @param {number} index The data index.
   * @param {string} origin The data origin.
   */
  #handleNonImageData(index, origin) {
    this.#generateData(index, origin);
    // send load events
    this.onload({
      source: origin
    });
    this.onloadend({
      source: origin
    });
  }

  /**
   * Handle image data.
   *
   * @param {number} index The data index.
   * @param {string} origin The data origin.
   */
  #handleImageData(index, origin) {
    const dicomParser = this.#dicomParserStore[index];

    const pixelBuffer = dicomParser.getDicomElements()['7FE00010'].value;
    // help GC: discard pixel buffer from elements
    dicomParser.getDicomElements()['7FE00010'].value = [];
    this.#finalBufferStore[index] = pixelBuffer[0];

    // transfer syntax (always there)
    const syntax = dicomParser.getDicomElements()['00020010'].value[0];
    const algoName = getSyntaxDecompressionName(syntax);
    const needDecompression = typeof algoName !== 'undefined';

    if (needDecompression) {
      // generate image
      this.#generateImageCompressed(
        index,
        pixelBuffer,
        algoName);
    } else {
      this.#generateImageUncompressed(index, origin);
    }
  }

  /**
   * Get data from an input buffer using a DICOM parser.
   *
   * @param {ArrayBuffer} buffer The input data buffer.
   * @param {string} origin The data origin.
   * @param {number} dataIndex The data index.
   */
  convert(buffer, origin, dataIndex) {
    // start event
    this.onloadstart({
      source: origin,
      index: dataIndex
    });

    // DICOM parser
    const dicomParser = new DicomParser();

    if (typeof this.#options.defaultCharacterSet !== 'undefined') {
      dicomParser.setDefaultCharacterSet(this.#options.defaultCharacterSet);
    }
    // parse the buffer
    let factory;
    try {
      dicomParser.parse(buffer);
      // check elements
      factory = this.#getFactory(dicomParser.getDicomElements());
      if (typeof factory !== 'undefined') {
        factory.checkElements(dicomParser.getDicomElements());
      }
    } catch (error) {
      this.onerror({
        error: error,
        source: origin
      });
      this.onloadend({
        source: origin
      });
      return;
    }

    // store
    this.#dicomParserStore[dataIndex] = dicomParser;
    this.#factories[dataIndex] = factory;

    // handle parsed data
    if (factory instanceof AnnotationGroupFactory) {
      this.#handleNonImageData(dataIndex, origin);
    } else {
      this.#handleImageData(dataIndex, origin);
    }
  }

  /**
   * Abort a conversion.
   */
  abort() {
    // abort decoding, will trigger pixelDecoder.onabort
    if (this.#pixelDecoder) {
      this.#pixelDecoder.abort();
    }
  }

  /**
   * Handle a load start event.
   * Default does nothing.
   *
   * @param {object} _event The load start event.
   */
  onloadstart(_event) {}

  /**
   * Handle a load item event.
   * Default does nothing.
   *
   * @param {object} _event The load item event.
   */
  onloaditem(_event) {}

  /**
   * Handle a load progress event.
   * Default does nothing.
   *
   * @param {object} _event The progress event.
   */
  onprogress(_event) {}

  /**
   * Handle a load event.
   * Default does nothing.
   *
   * @param {object} _event The load event fired
   *   when a file has been loaded successfully.
   */
  onload(_event) {}
  /**
   * Handle a load end event.
   * Default does nothing.
   *
   * @param {object} _event The load end event fired
   *  when a file load has completed, successfully or not.
   */
  onloadend(_event) {}

  /**
   * Handle an error event.
   * Default does nothing.
   *
   * @param {object} _event The error event.
   */
  onerror(_event) {}

  /**
   * Handle an abort event.
   * Default does nothing.
   *
   * @param {object} _event The abort event.
   */
  onabort(_event) {}

} // class DicomBufferToView