ivmartel/dwv

View on GitHub
src/image/domReader.js

Summary

Maintainability
C
1 day
Test Coverage
import {Size} from '../image/size';
import {Spacing} from '../image/spacing';
import {Geometry} from '../image/geometry';
import {Image} from '../image/image';
import {Point3D} from '../math/point';

/**
 * Create a simple array buffer from an ImageData buffer.
 *
 * @param {object} imageData The ImageData taken from a context.
 * @returns {Uint8Array} The image buffer.
 */
function imageDataToBuffer(imageData) {
  // remove alpha
  // TODO support passing the full image data
  const dataLen = imageData.data.length;
  const buffer = new Uint8Array((dataLen / 4) * 3);
  let j = 0;
  for (let i = 0; i < dataLen; i += 4) {
    buffer[j] = imageData.data[i];
    buffer[j + 1] = imageData.data[i + 1];
    buffer[j + 2] = imageData.data[i + 2];
    j += 3;
  }
  return buffer;
}

/**
 * Get an image from an input context imageData.
 *
 * @param {number} width The width of the coresponding image.
 * @param {number} height The height of the coresponding image.
 * @param {number} sliceIndex The slice index of the imageData.
 * @param {object} imageBuffer The image buffer.
 * @param {number} numberOfFrames The final number of frames.
 * @param {string} imageUid The image UID.
 * @returns {object} The corresponding view.
 */
function getDefaultImage(
  width, height, sliceIndex,
  imageBuffer, numberOfFrames,
  imageUid) {
  // image size
  const imageSize = new Size([width, height, 1]);
  // default spacing
  // TODO: misleading...
  const imageSpacing = new Spacing([1, 1, 1]);
  // default origin
  const origin = new Point3D(0, 0, sliceIndex);
  // create image
  const geometry = new Geometry(origin, imageSize, imageSpacing);
  const image = new Image(geometry, imageBuffer, [imageUid]);
  image.setPhotometricInterpretation('RGB');
  // meta information
  const meta = {};
  meta.BitsStored = 8;
  if (typeof numberOfFrames !== 'undefined') {
    meta.numberOfFiles = numberOfFrames;
  }
  image.setMeta(meta);
  // return
  return image;
}

/**
 * Get data from an input image using a canvas.
 *
 * @param {HTMLImageElement} domImage The DOM Image,
 *   an HTMLImageElement with extra info.
 * @param {string|File} origin The data origin.
 * @param {number} index The data index.
 * @returns {object} A load data event.
 */
export function getViewFromDOMImage(domImage, origin, index) {
  // image size
  const width = domImage.width;
  const height = domImage.height;

  // draw the image in the canvas in order to get its data
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d');
  ctx.drawImage(domImage, 0, 0);
  // get the image data
  const imageData = ctx.getImageData(0, 0, width, height);

  // image properties
  const info = {};
  if (typeof origin === 'string') {
    info['origin'] = {value: origin};
  } else {
    info['fileName'] = {value: origin.name};
    info['fileType'] = {value: origin.type};
    info['fileLastModifiedDate'] = {value: origin.lastModified};
  }
  info['imageWidth'] = {value: width};
  info['imageHeight'] = {value: height};

  const sliceIndex = index ? index : 0;
  info['imageUid'] = {value: sliceIndex};

  // create view
  const imageBuffer = imageDataToBuffer(imageData);
  const image = getDefaultImage(
    width, height, sliceIndex, imageBuffer, 1, sliceIndex.toString());

  // return
  return {
    data: {
      image: image,
      info: info
    },
    source: origin
  };
}

/**
 * Get data from an input image using a canvas.
 *
 * @param {object} video The DOM Video, an HTMLVideoElement with extra info.
 * @param {Function} onloaditem On load callback.
 * @param {object} onload The function to call once the data is loaded.
 * @param {object} onprogress The function to call to report progress.
 * @param {object} onloadend The function to call to report load end.
 * @param {string|File} origin The data origin.
 * @param {number} dataIndex The data index.
 */
export function getViewFromDOMVideo(
  video, onloaditem, onload, onprogress, onloadend,
  origin, dataIndex) {
  // video size
  const width = video.videoWidth;
  const height = video.videoHeight;

  // default frame rate...
  const frameRate = 30;
  // number of frames
  const numberOfFrames = Math.ceil(video.duration * frameRate);

  // video properties
  const info = {};
  if (typeof origin === 'string') {
    info['origin'] = {value: origin};
  } else {
    info['fileName'] = {value: origin.name};
    info['fileType'] = {value: origin.type};
    info['fileLastModifiedDate'] = {value: origin.lastModified};
  }
  info['imageWidth'] = {value: width};
  info['imageHeight'] = {value: height};
  info['numberOfFrames'] = {value: numberOfFrames};
  info['imageUid'] = {value: 0};

  // draw the image in the canvas in order to get its data
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d');

  // using seeked to loop through all video frames
  video.addEventListener('seeked', onseeked, false);

  // current frame index
  let frameIndex = 0;
  // video image
  let image = null;

  /**
   * Draw the context and store it as a frame.
   */
  function storeFrame() {
    // send progress
    onprogress({
      lengthComputable: true,
      loaded: frameIndex,
      total: numberOfFrames,
      index: dataIndex,
      source: origin
    });
    // draw image
    ctx.drawImage(video, 0, 0);
    // context to image buffer
    const imgBuffer = imageDataToBuffer(
      ctx.getImageData(0, 0, width, height));
    if (frameIndex === 0) {
      // create view
      image = getDefaultImage(
        width, height, 1, imgBuffer, numberOfFrames, dataIndex.toString());
      // call callback
      onloaditem({
        data: {
          image: image,
          info: info
        },
        source: origin
      });
    } else {
      image.appendFrameBuffer(imgBuffer, frameIndex);
    }
    // increment index
    ++frameIndex;
  }

  let nextTime = 0;

  /**
   * Handle seeked event.
   *
   * @param {object} event The seeked event.
   */
  function onseeked(event) {
    // store
    storeFrame();
    // set the next time
    // (not using currentTime, it seems to get offseted)
    nextTime += 1 / frameRate;
    if (nextTime <= event.target.duration) {
      this.currentTime = nextTime;
    } else {
      onload({
        source: origin
      });
      onloadend({
        source: origin
      });
      // stop listening
      video.removeEventListener('seeked', onseeked);
    }
  }

  // trigger the first seek
  video.currentTime = nextTime;
}