src_image_decoder.js

import {ThreadPool, WorkerTask} from '../utils/thread';

/**
 * The JPEG baseline decoder.
 *
 * Ref: {@link https://github.com/mozilla/pdf.js/blob/master/src/core/jpg.js}.
 *
 * @external JpegImage
 */
/* global JpegImage */
// @ts-ignore
const hasJpegBaselineDecoder = (typeof JpegImage !== 'undefined');

/**
 * The JPEG decoder namespace.
 *
 * Ref: {@link https://github.com/rii-mango/JPEGLosslessDecoderJS}.
 *
 * @external jpeg
 */
/* global jpeg */
const hasJpegLosslessDecoder =
  // @ts-ignore
  (typeof jpeg !== 'undefined') && (typeof jpeg.lossless !== 'undefined');

/**
 * The JPEG 2000 decoder.
 *
 * Ref: {@link https://github.com/jpambrun/jpx-medical/blob/master/jpx.js}.
 *
 * @external JpxImage
 */
/* global JpxImage */
// @ts-ignore
const hasJpeg2000Decoder = (typeof JpxImage !== 'undefined');

/* global dwvdecoder */

/**
 * Decoder scripts to be passed to web workers for image decoding.
 */
export const decoderScripts = {
  jpeg2000: '',
  'jpeg-lossless': '',
  'jpeg-baseline': '',
  rle: ''
};

/**
 * Asynchronous pixel buffer decoder.
 */
class AsynchPixelBufferDecoder {

  /**
   * The associated worker script.
   *
   * @type {string}
   */
  #script;

  /**
   * Associated thread pool.
   *
   * @type {ThreadPool}
   */
  #pool = new ThreadPool(10);

  /**
   * Flag to know if callbacks are set.
   *
   * @type {boolean}
   */
  #areCallbacksSet = false;

  /**
   * @param {string} script The path to the decoder script to be used
   *   by the web worker.
   * @param {number} _numberOfData The anticipated number of data to decode.
   */
  constructor(script, _numberOfData) {
    this.#script = script;
  }

  /**
   * Decode a pixel buffer.
   *
   * @param {Array} pixelBuffer The pixel buffer.
   * @param {object} pixelMeta The input meta data.
   * @param {object} info Information object about the input data.
   */
  decode(pixelBuffer, pixelMeta, info) {
    if (!this.#areCallbacksSet) {
      this.#areCallbacksSet = true;
      // set event handlers
      this.#pool.onworkstart = this.ondecodestart;
      this.#pool.onworkitem = this.ondecodeditem;
      this.#pool.onwork = this.ondecoded;
      this.#pool.onworkend = this.ondecodeend;
      this.#pool.onerror = this.onerror;
      this.#pool.onabort = this.onabort;
    }
    // create worker task
    const workerTask = new WorkerTask(
      this.#script,
      {
        buffer: pixelBuffer,
        meta: pixelMeta
      },
      info
    );
    // add it the queue and run it
    this.#pool.addWorkerTask(workerTask);
  }

  /**
   * Abort decoding.
   */
  abort() {
    // abort the thread pool, will trigger pool.onabort
    this.#pool.abort();
  }

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

  /**
   * Handle a decode item event.
   * Default does nothing.
   *
   * @param {object} _event The decode item event fired
   *   when a decode item ended successfully.
   */
  ondecodeditem(_event) {}

  /**
   * Handle a decode event.
   * Default does nothing.
   *
   * @param {object} _event The decode event fired
   *   when a file has been decoded successfully.
   */
  ondecoded(_event) {}

  /**
   * Handle a decode end event.
   * Default does nothing.
   *
   * @param {object} _event The decode end event fired
   *  when a file decoding has completed, successfully or not.
   */
  ondecodeend(_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 AsynchPixelBufferDecoder

/**
 * Synchronous pixel buffer decoder.
 */
class SynchPixelBufferDecoder {

  /**
   * Name of the compression algorithm.
   *
   * @type {string}
   */
  #algoName;

  /**
   * Number of data.
   *
   * @type {number}
   */
  #numberOfData;

  /**
   * @param {string} algoName The decompression algorithm name.
   * @param {number} numberOfData The anticipated number of data to decode.
   */
  constructor(algoName, numberOfData) {
    this.#algoName = algoName;
    this.#numberOfData = numberOfData;
  }

  // decode count
  #decodeCount = 0;

  /**
   * Decode a pixel buffer.
   *
   * @param {Array} pixelBuffer The pixel buffer.
   * @param {object} pixelMeta The input meta data.
   * @param {object} info Information object about the input data.
   * @external jpeg
   * @external JpegImage
   * @external JpxImage
   */
  decode(pixelBuffer, pixelMeta, info) {
    ++this.#decodeCount;

    let decoder = null;
    let decodedBuffer = null;
    if (this.#algoName === 'jpeg-lossless') {
      if (!hasJpegLosslessDecoder) {
        throw new Error('No JPEG Lossless decoder provided');
      }
      // bytes per element
      const bpe = pixelMeta.bitsAllocated / 8;
      const buf = new Uint8Array(pixelBuffer);
      // @ts-ignore
      decoder = new jpeg.lossless.Decoder();
      const decoded = decoder.decode(buf.buffer, 0, buf.buffer.byteLength, bpe);
      if (pixelMeta.bitsAllocated === 8) {
        if (pixelMeta.isSigned) {
          decodedBuffer = new Int8Array(decoded.buffer);
        } else {
          decodedBuffer = new Uint8Array(decoded.buffer);
        }
      } else if (pixelMeta.bitsAllocated === 16) {
        if (pixelMeta.isSigned) {
          decodedBuffer = new Int16Array(decoded.buffer);
        } else {
          decodedBuffer = new Uint16Array(decoded.buffer);
        }
      }
    } else if (this.#algoName === 'jpeg-baseline') {
      if (!hasJpegBaselineDecoder) {
        throw new Error('No JPEG Baseline decoder provided');
      }
      // @ts-ignore
      decoder = new JpegImage();
      decoder.parse(pixelBuffer);
      decodedBuffer = decoder.getData(decoder.width, decoder.height);
    } else if (this.#algoName === 'jpeg2000') {
      if (!hasJpeg2000Decoder) {
        throw new Error('No JPEG 2000 decoder provided');
      }
      // decompress pixel buffer into Int16 image
      // @ts-ignore
      decoder = new JpxImage();
      decoder.parse(pixelBuffer);
      // set the pixel buffer
      decodedBuffer = decoder.tiles[0].items;
    } else if (this.#algoName === 'rle') {
      // decode DICOM buffer
      // @ts-ignore
      decoder = new dwvdecoder.RleDecoder();
      // set the pixel buffer
      decodedBuffer = decoder.decode(
        pixelBuffer,
        pixelMeta.bitsAllocated,
        pixelMeta.isSigned,
        pixelMeta.sliceSize,
        pixelMeta.samplesPerPixel,
        pixelMeta.planarConfiguration);
    }
    // send decode events
    this.ondecodeditem({
      data: [decodedBuffer],
      index: info.index,
      numberOfItems: info.numberOfItems,
      itemNumber: info.itemNumber
    });
    // decode end?
    if (this.#decodeCount === this.#numberOfData) {
      this.ondecoded({});
      this.ondecodeend({});
    }
  }

  /**
   * Abort decoding.
   */
  abort() {
    // nothing to do in the synchronous case.
    // callback
    this.onabort({});
    this.ondecodeend({});
  }

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

  /**
   * Handle a decode item event.
   * Default does nothing.
   *
   * @param {object} _event The decode item event fired
   *   when a decode item ended successfully.
   */
  ondecodeditem(_event) {}

  /**
   * Handle a decode event.
   * Default does nothing.
   *
   * @param {object} _event The decode event fired
   *   when a file has been decoded successfully.
   */
  ondecoded(_event) {}

  /**
   * Handle a decode end event.
   * Default does nothing.
   *
   * @param {object} _event The decode end event fired
   *  when a file decoding has completed, successfully or not.
   */
  ondecodeend(_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 SynchPixelBufferDecoder

/**
 * Decode a pixel buffer.
 *
 * If the 'decoderScripts' variable does not contain the desired,
 * algorythm the decoder will switch to the synchronous mode.
 */
export class PixelBufferDecoder {

  /**
   * Flag to know if callbacks are set.
   *
   * @type {boolean}
   */
  #areCallbacksSet = false;

  /**
   * Pixel decoder.
   * Defined only once.
   *
   * @type {object}
   */
  #pixelDecoder = null;

  /**
   * @param {string} algoName The decompression algorithm name.
   * @param {number} numberOfData The anticipated number of data to decode.
   */
  constructor(algoName, numberOfData) {
    // initialise the asynch decoder (if possible)
    if (typeof decoderScripts !== 'undefined' &&
      typeof decoderScripts[algoName] !== 'undefined') {
      this.#pixelDecoder = new AsynchPixelBufferDecoder(
        decoderScripts[algoName], numberOfData);
    } else {
      this.#pixelDecoder = new SynchPixelBufferDecoder(
        algoName, numberOfData);
    }
  }

  /**
   * Get data from an input buffer using a DICOM parser.
   *
   * @param {Array} pixelBuffer The input data buffer.
   * @param {object} pixelMeta The input meta data.
   * @param {object} info Information object about the input data.
   */
  decode(pixelBuffer, pixelMeta, info) {
    if (!this.#areCallbacksSet) {
      this.#areCallbacksSet = true;
      // set callbacks
      this.#pixelDecoder.ondecodestart = this.ondecodestart;
      this.#pixelDecoder.ondecodeditem = this.ondecodeditem;
      this.#pixelDecoder.ondecoded = this.ondecoded;
      this.#pixelDecoder.ondecodeend = this.ondecodeend;
      this.#pixelDecoder.onerror = this.onerror;
      this.#pixelDecoder.onabort = this.onabort;
    }
    // decode and call the callback
    this.#pixelDecoder.decode(pixelBuffer, pixelMeta, info);
  }

  /**
   * Abort decoding.
   */
  abort() {
    // decoder classes should define an abort
    this.#pixelDecoder.abort();
  }

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

  /**
   * Handle a decode item event.
   * Default does nothing.
   *
   * @param {object} _event The decode item event fired
   *   when a decode item ended successfully.
   */
  ondecodeditem(_event) {}

  /**
   * Handle a decode event.
   * Default does nothing.
   *
   * @param {object} _event The decode event fired
   *   when a file has been decoded successfully.
   */
  ondecoded(_event) {}

  /**
   * Handle a decode end event.
   * Default does nothing.
   *
   * @param {object} _event The decode end event fired
   *  when a file decoding has completed, successfully or not.
   */
  ondecodeend(_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 PixelBufferDecoder