src/image/image.js

// namespaces
var dwv = dwv || {};
/** @namespace */
dwv.image = dwv.image || {};

/**
 * Image class.
 * Usable once created, optional are:
 * - rescale slope and intercept (default 1:0),
 * - photometric interpretation (default MONOCHROME2),
 * - planar configuration (default RGBRGB...).
 *
 * @class
 * @param {dwv.image.Geometry} geometry The geometry of the image.
 * @param {Array} buffer The image data as a one dimensional buffer.
 * @param {Array} imageUids An array of Uids indexed to slice number.
 */
dwv.image.Image = function (geometry, buffer, imageUids) {

  /**
   * Constant rescale slope and intercept (default).
   *
   * @private
   * @type {object}
   */
  var rsi = new dwv.image.RescaleSlopeAndIntercept(1, 0);
  /**
   * Varying rescale slope and intercept.
   *
   * @private
   * @type {Array}
   */
  var rsis = null;
  /**
   * Flag to know if the RSIs are all identity (1,0).
   *
   * @private
   * @type {boolean}
   */
  var isIdentityRSI = true;
  /**
   * Flag to know if the RSIs are all equals.
   *
   * @private
   * @type {boolean}
   */
  var isConstantRSI = true;
  /**
   * Photometric interpretation (MONOCHROME, RGB...).
   *
   * @private
   * @type {string}
   */
  var photometricInterpretation = 'MONOCHROME2';
  /**
   * Planar configuration for RGB data (0:RGBRGBRGBRGB... or
   *   1:RRR...GGG...BBB...).
   *
   * @private
   * @type {number}
   */
  var planarConfiguration = 0;
  /**
   * Number of components.
   *
   * @private
   * @type {number}
   */
  var numberOfComponents = buffer.length / (
    geometry.getSize().getTotalSize());
  /**
   * Meta information.
   *
   * @private
   * @type {object}
   */
  var meta = {};

  /**
   * Data range.
   *
   * @private
   * @type {object}
   */
  var dataRange = null;
  /**
   * Rescaled data range.
   *
   * @private
   * @type {object}
   */
  var rescaledDataRange = null;
  /**
   * Histogram.
   *
   * @private
   * @type {Array}
   */
  var histogram = null;

  /**
   * Listener handler.
   *
   * @private
   * @type {object}
   */
  var listenerHandler = new dwv.utils.ListenerHandler();

  /**
   * Get the image UID at a given index.
   *
   * @param {dwv.math.Index} index The index at which to get the id.
   * @returns {string} The UID.
   */
  this.getImageUid = function (index) {
    var uid = imageUids[0];
    if (imageUids.length !== 1 && typeof index !== 'undefined') {
      uid = imageUids[this.getSecondaryOffset(index)];
    }
    return uid;
  };

  /**
   * Get the geometry of the image.
   *
   * @returns {dwv.image.Geometry} The geometry.
   */
  this.getGeometry = function () {
    return geometry;
  };

  /**
   * Get the data buffer of the image.
   *
   * @todo dangerous...
   * @returns {Array} The data buffer of the image.
   */
  this.getBuffer = function () {
    return buffer;
  };

  /**
   * Can the image values be quantified?
   *
   * @returns {boolean} True if only one component.
   */
  this.canQuantify = function () {
    return this.getNumberOfComponents() === 1;
  };

  /**
   * Can window and level be applied to the data?
   *
   * @returns {boolean} True if the data is monochrome.
   */
  this.canWindowLevel = function () {
    return this.getPhotometricInterpretation()
      .match(/MONOCHROME/) !== null;
  };

  /**
   * Can the data be scrolled?
   *
   * @param {dwv.math.Matrix33} viewOrientation The view orientation.
   * @returns {boolean} True if the data has a third dimension greater than one
   *   after applying the view orientation.
   */
  this.canScroll = function (viewOrientation) {
    var size = this.getGeometry().getSize();
    // also check the numberOfFiles in case we are in the middle of a load
    var nFiles = 1;
    if (typeof meta.numberOfFiles !== 'undefined') {
      nFiles = meta.numberOfFiles;
    }
    return size.canScroll(viewOrientation) || nFiles !== 1;
  };

  /**
   * Get the secondary offset max.
   *
   * @returns {number} The maximum offset.
   */
  function getSecondaryOffsetMax() {
    return geometry.getSize().getTotalSize(2);
  }

  /**
   * Get the secondary offset: an offset that takes into account
   *   the slice and above dimension numbers.
   *
   * @param {dwv.math.Index} index The index.
   * @returns {number} The offset.
   */
  this.getSecondaryOffset = function (index) {
    return geometry.getSize().indexToOffset(index, 2);
  };

  /**
   * Get the rescale slope and intercept.
   *
   * @param {dwv.math.Index} index The index (only needed for non constant rsi).
   * @returns {object} The rescale slope and intercept.
   */
  this.getRescaleSlopeAndIntercept = function (index) {
    var res = rsi;
    if (!this.isConstantRSI()) {
      if (typeof index === 'undefined') {
        throw new Error('Cannot get non constant RSI with empty slice index.');
      }
      var offset = this.getSecondaryOffset(index);
      if (typeof rsis[offset] !== 'undefined') {
        res = rsis[offset];
      } else {
        dwv.logger.warn('undefined non constant rsi at ' + offset);
      }
    }
    return res;
  };

  /**
   * Get the rsi at a specified (secondary) offset.
   *
   * @param {number} offset The desired (secondary) offset.
   * @returns {object} The coresponding rsi.
   */
  function getRescaleSlopeAndInterceptAtOffset(offset) {
    return rsis[offset];
  }

  /**
   * Set the rescale slope and intercept.
   *
   * @param {object} inRsi The input rescale slope and intercept.
   * @param {number} offset The rsi offset (only needed for non constant rsi).
   */
  this.setRescaleSlopeAndIntercept = function (inRsi, offset) {
    // update identity flag
    isIdentityRSI = isIdentityRSI && inRsi.isID();
    // update constant flag
    if (!isConstantRSI) {
      if (typeof index === 'undefined') {
        throw new Error(
          'Cannot store non constant RSI with empty slice index.');
      }
      rsis.splice(offset, 0, inRsi);
    } else {
      if (!rsi.equals(inRsi)) {
        if (typeof index === 'undefined') {
          // no slice index, replace existing
          rsi = inRsi;
        } else {
          // first non constant rsi
          isConstantRSI = false;
          // switch to non constant mode
          rsis = [];
          // initialise RSIs
          for (var i = 0, leni = getSecondaryOffsetMax(); i < leni; ++i) {
            rsis.push(i);
          }
          // store
          rsi = null;
          rsis.splice(offset, 0, inRsi);
        }
      }
    }
  };
  /**
   * Are all the RSIs identity (1,0).
   *
   * @returns {boolean} True if they are.
   */
  this.isIdentityRSI = function () {
    return isIdentityRSI;
  };
  /**
   * Are all the RSIs equal.
   *
   * @returns {boolean} True if they are.
   */
  this.isConstantRSI = function () {
    return isConstantRSI;
  };
  /**
   * Get the photometricInterpretation of the image.
   *
   * @returns {string} The photometricInterpretation of the image.
   */
  this.getPhotometricInterpretation = function () {
    return photometricInterpretation;
  };
  /**
   * Set the photometricInterpretation of the image.
   *
   * @param {string} interp The photometricInterpretation of the image.
   */
  this.setPhotometricInterpretation = function (interp) {
    photometricInterpretation = interp;
  };
  /**
   * Get the planarConfiguration of the image.
   *
   * @returns {number} The planarConfiguration of the image.
   */
  this.getPlanarConfiguration = function () {
    return planarConfiguration;
  };
  /**
   * Set the planarConfiguration of the image.
   *
   * @param {number} config The planarConfiguration of the image.
   */
  this.setPlanarConfiguration = function (config) {
    planarConfiguration = config;
  };
  /**
   * Get the numberOfComponents of the image.
   *
   * @returns {number} The numberOfComponents of the image.
   */
  this.getNumberOfComponents = function () {
    return numberOfComponents;
  };

  /**
   * Get the meta information of the image.
   *
   * @returns {object} The meta information of the image.
   */
  this.getMeta = function () {
    return meta;
  };
  /**
   * Set the meta information of the image.
   *
   * @param {object} rhs The meta information of the image.
   */
  this.setMeta = function (rhs) {
    meta = rhs;
  };

  /**
   * Get value at offset. Warning: No size check...
   *
   * @param {number} offset The desired offset.
   * @returns {number} The value at offset.
   */
  this.getValueAtOffset = function (offset) {
    return buffer[offset];
  };

  /**
   * Clone the image.
   *
   * @returns {Image} A clone of this image.
   */
  this.clone = function () {
    // clone the image buffer
    var clonedBuffer = buffer.slice(0);
    // create the image copy
    var copy = new dwv.image.Image(this.getGeometry(), clonedBuffer, imageUids);
    // copy the RSI(s)
    if (this.isConstantRSI()) {
      copy.setRescaleSlopeAndIntercept(this.getRescaleSlopeAndIntercept());
    } else {
      for (var i = 0; i < getSecondaryOffsetMax(); ++i) {
        copy.setRescaleSlopeAndIntercept(
          getRescaleSlopeAndInterceptAtOffset(i), i);
      }
    }
    // copy extras
    copy.setPhotometricInterpretation(this.getPhotometricInterpretation());
    copy.setPlanarConfiguration(this.getPlanarConfiguration());
    copy.setMeta(this.getMeta());
    // return
    return copy;
  };

  /**
   * Re-allocate buffer memory to an input size.
   *
   * @param {number} size The new size.
   */
  function realloc(size) {
    // save buffer
    var tmpBuffer = buffer;
    // create new
    buffer = dwv.dicom.getTypedArray(
      buffer.BYTES_PER_ELEMENT * 8,
      meta.IsSigned ? 1 : 0,
      size);
    if (buffer === null) {
      throw new Error('Cannot reallocate data for image.');
    }
    // put old in new
    buffer.set(tmpBuffer);
    // clean
    tmpBuffer = null;
  }

  /**
   * Append a slice to the image.
   *
   * @param {Image} rhs The slice to append.
   * @param {number} timeId An optional time ID.
   */
  this.appendSlice = function (rhs, timeId) {
    if (typeof timeId === 'undefined') {
      timeId = 0;
    }
    // check input
    if (rhs === null) {
      throw new Error('Cannot append null slice');
    }
    var rhsSize = rhs.getGeometry().getSize();
    var size = geometry.getSize();
    if (rhsSize.get(2) !== 1) {
      throw new Error('Cannot append more than one slice');
    }
    if (size.get(0) !== rhsSize.get(0)) {
      throw new Error('Cannot append a slice with different number of columns');
    }
    if (size.get(1) !== rhsSize.get(1)) {
      throw new Error('Cannot append a slice with different number of rows');
    }
    if (!geometry.getOrientation().equals(
      rhs.getGeometry().getOrientation(), 0.0001)) {
      throw new Error('Cannot append a slice with different orientation');
    }
    if (photometricInterpretation !== rhs.getPhotometricInterpretation()) {
      throw new Error(
        'Cannot append a slice with different photometric interpretation');
    }
    // all meta should be equal
    for (var key in meta) {
      if (key === 'windowPresets') {
        continue;
      }
      if (meta[key] !== rhs.getMeta()[key]) {
        throw new Error('Cannot append a slice with different ' + key);
      }
    }

    // calculate slice size
    var sliceSize = numberOfComponents * size.getDimSize(2);

    // create full buffer if not done yet
    if (typeof meta.numberOfFiles === 'undefined') {
      throw new Error('Missing number of files for buffer manipulation.');
    }
    var fullBufferSize = sliceSize * meta.numberOfFiles;
    if (size.length() === 4) {
      fullBufferSize *= size.get(3);
    }
    if (buffer.length !== fullBufferSize) {
      realloc(fullBufferSize);
    }

    var newSliceIndex = geometry.getSliceIndex(rhs.getGeometry().getOrigin());
    var values = new Array(geometry.getSize().length());
    values.fill(0);
    values[2] = newSliceIndex;
    if (size.length() === 4) {
      values[3] = timeId;
    }
    var index = new dwv.math.Index(values);
    var primaryOffset = size.indexToOffset(index);
    var secondaryOffset = this.getSecondaryOffset(index);

    // first frame special slice by slice append
    if (timeId === 0) {
      // store slice
      var oldNumberOfSlices = size.get(2);
      // move content if needed
      var start = primaryOffset;
      var end;
      if (newSliceIndex === 0) {
        // insert slice before current data
        end = start + oldNumberOfSlices * sliceSize;
        buffer.set(
          buffer.subarray(start, end),
          primaryOffset + sliceSize
        );
      } else if (newSliceIndex < oldNumberOfSlices) {
        // insert slice in between current data
        end = start + (oldNumberOfSlices - newSliceIndex) * sliceSize;
        buffer.set(
          buffer.subarray(start, end),
          primaryOffset + sliceSize
        );
      }
    }

    // add new slice content
    buffer.set(rhs.getBuffer(), primaryOffset);

    // update geometry
    if (timeId === 0) {
      geometry.appendOrigin(rhs.getGeometry().getOrigin(), newSliceIndex);
    }
    // update rsi
    // (rhs should just have one rsi)
    this.setRescaleSlopeAndIntercept(
      rhs.getRescaleSlopeAndIntercept(), secondaryOffset);

    // current number of images
    var numberOfImages = imageUids.length;

    // insert sop instance UIDs
    imageUids.splice(secondaryOffset, 0, rhs.getImageUid());

    // update window presets
    if (typeof meta.windowPresets !== 'undefined') {
      var windowPresets = meta.windowPresets;
      var rhsPresets = rhs.getMeta().windowPresets;
      var keys = Object.keys(rhsPresets);
      var pkey = null;
      for (var i = 0; i < keys.length; ++i) {
        pkey = keys[i];
        var rhsPreset = rhsPresets[pkey];
        var windowPreset = windowPresets[pkey];
        if (typeof windowPreset !== 'undefined') {
          // if not set or false, check perslice
          if (typeof windowPreset.perslice === 'undefined' ||
            windowPreset.perslice === false) {
            // if different preset.wl, mark it as perslice
            if (!windowPreset.wl[0].equals(rhsPreset.wl[0])) {
              windowPreset.perslice = true;
              // fill wl array with copy of wl[0]
              // (loop on number of images minus the existing one)
              for (var j = 0; j < numberOfImages - 1; ++j) {
                windowPreset.wl.push(windowPreset.wl[0]);
              }
            }
          }
          // store (first) rhs preset.wl if needed
          if (typeof windowPreset.perslice !== 'undefined' &&
            windowPreset.perslice === true) {
            windowPresets[pkey].wl.splice(
              secondaryOffset, 0, rhsPreset.wl[0]);
          }
        } else {
          // if not defined (it should be), store all
          windowPresets[pkey] = rhsPresets[pkey];
        }
      }
    }
  };

  /**
   * Append a frame buffer to the image.
   *
   * @param {object} frameBuffer The frame buffer to append.
   * @param {number} frameIndex The frame index.
   */
  this.appendFrameBuffer = function (frameBuffer, frameIndex) {
    // create full buffer if not done yet
    var size = geometry.getSize();
    var frameSize = numberOfComponents * size.getDimSize(2);
    if (typeof meta.numberOfFiles === 'undefined') {
      throw new Error('Missing number of files for frame buffer manipulation.');
    }
    var fullBufferSize = frameSize * meta.numberOfFiles;
    if (buffer.length !== fullBufferSize) {
      realloc(fullBufferSize);
    }
    // append
    if (frameIndex >= meta.numberOfFiles) {
      throw new Error(
        'Cannot append a frame at an index above the number of frames');
    }
    buffer.set(frameBuffer, frameSize * frameIndex);
    // update geometry
    this.appendFrame();
  };

  /**
   * Append a frame to the image.
   */
  this.appendFrame = function () {
    geometry.appendFrame();
    fireEvent({type: 'appendframe'});
    // memory will be updated at the first appendSlice or appendFrameBuffer
  };

  /**
   * Get the data range.
   *
   * @returns {object} The data range.
   */
  this.getDataRange = function () {
    if (!dataRange) {
      dataRange = this.calculateDataRange();
    }
    return dataRange;
  };

  /**
   * Get the rescaled data range.
   *
   * @returns {object} The rescaled data range.
   */
  this.getRescaledDataRange = function () {
    if (!rescaledDataRange) {
      rescaledDataRange = this.calculateRescaledDataRange();
    }
    return rescaledDataRange;
  };

  /**
   * Get the histogram.
   *
   * @returns {Array} The histogram.
   */
  this.getHistogram = function () {
    if (!histogram) {
      var res = this.calculateHistogram();
      dataRange = res.dataRange;
      rescaledDataRange = res.rescaledDataRange;
      histogram = res.histogram;
    }
    return histogram;
  };

  /**
   * Add an event listener to this class.
   *
   * @param {string} type The event type.
   * @param {object} callback The method associated with the provided
   *   event type, will be called with the fired event.
   */
  this.addEventListener = function (type, callback) {
    listenerHandler.add(type, callback);
  };

  /**
   * Remove an event listener from this class.
   *
   * @param {string} type The event type.
   * @param {object} callback The method associated with the provided
   *   event type.
   */
  this.removeEventListener = function (type, callback) {
    listenerHandler.remove(type, callback);
  };

  /**
   * Fire an event: call all associated listeners with the input event object.
   *
   * @param {object} event The event to fire.
   * @private
   */
  function fireEvent(event) {
    listenerHandler.fireEvent(event);
  }
};

/**
 * Get the value of the image at a specific coordinate.
 *
 * @param {number} i The X index.
 * @param {number} j The Y index.
 * @param {number} k The Z index.
 * @param {number} f The frame number.
 * @returns {number} The value at the desired position.
 * Warning: No size check...
 */
dwv.image.Image.prototype.getValue = function (i, j, k, f) {
  var frame = (f || 0);
  var index = new dwv.math.Index([i, j, k, frame]);
  return this.getValueAtOffset(
    this.getGeometry().getSize().indexToOffset(index));
};

/**
 * Get the value of the image at a specific index.
 *
 * @param {dwv.math.Index} index The index.
 * @returns {number} The value at the desired position.
 * Warning: No size check...
 */
dwv.image.Image.prototype.getValueAtIndex = function (index) {
  return this.getValueAtOffset(
    this.getGeometry().getSize().indexToOffset(index));
};

/**
 * Get the rescaled value of the image at a specific position.
 *
 * @param {number} i The X index.
 * @param {number} j The Y index.
 * @param {number} k The Z index.
 * @param {number} f The frame number.
 * @returns {number} The rescaled value at the desired position.
 * Warning: No size check...
 */
dwv.image.Image.prototype.getRescaledValue = function (i, j, k, f) {
  if (typeof f === 'undefined') {
    f = 0;
  }
  var val = this.getValue(i, j, k, f);
  if (!this.isIdentityRSI()) {
    if (this.isConstantRSI()) {
      val = this.getRescaleSlopeAndIntercept().apply(val);
    } else {
      var values = [i, j, k, f];
      var index = new dwv.math.Index(values);
      val = this.getRescaleSlopeAndIntercept(index).apply(val);
    }
  }
  return val;
};

/**
 * Get the rescaled value of the image at a specific index.
 *
 * @param {dwv.math.Index} index The index.
 * @returns {number} The rescaled value at the desired position.
 * Warning: No size check...
 */
dwv.image.Image.prototype.getRescaledValueAtIndex = function (index) {
  return this.getRescaledValueAtOffset(
    this.getGeometry().getSize().indexToOffset(index)
  );
};

/**
 * Get the rescaled value of the image at a specific offset.
 *
 * @param {number} offset The desired offset.
 * @returns {number} The rescaled value at the desired offset.
 * Warning: No size check...
 */
dwv.image.Image.prototype.getRescaledValueAtOffset = function (offset) {
  var val = this.getValueAtOffset(offset);
  if (!this.isIdentityRSI()) {
    if (this.isConstantRSI()) {
      val = this.getRescaleSlopeAndIntercept().apply(val);
    } else {
      var index = this.getGeometry().getSize().offsetToIndex(offset);
      val = this.getRescaleSlopeAndIntercept(index).apply(val);
    }
  }
  return val;
};

/**
 * Calculate the data range of the image.
 * WARNING: for speed reasons, only calculated on the first frame...
 *
 * @returns {object} The range {min, max}.
 */
dwv.image.Image.prototype.calculateDataRange = function () {
  var min = this.getValueAtOffset(0);
  var max = min;
  var value = 0;
  var size = this.getGeometry().getSize();
  var leni = size.getTotalSize();
  // max to 3D
  if (size.length() >= 3) {
    leni = size.getDimSize(3);
  }
  for (var i = 0; i < leni; ++i) {
    value = this.getValueAtOffset(i);
    if (value > max) {
      max = value;
    }
    if (value < min) {
      min = value;
    }
  }
  // return
  return {min: min, max: max};
};

/**
 * Calculate the rescaled data range of the image.
 * WARNING: for speed reasons, only calculated on the first frame...
 *
 * @returns {object} The range {min, max}.
 */
dwv.image.Image.prototype.calculateRescaledDataRange = function () {
  if (this.isIdentityRSI()) {
    return this.getDataRange();
  } else if (this.isConstantRSI()) {
    var range = this.getDataRange();
    var resmin = this.getRescaleSlopeAndIntercept().apply(range.min);
    var resmax = this.getRescaleSlopeAndIntercept().apply(range.max);
    return {
      min: ((resmin < resmax) ? resmin : resmax),
      max: ((resmin > resmax) ? resmin : resmax)
    };
  } else {
    var rmin = this.getRescaledValueAtOffset(0);
    var rmax = rmin;
    var rvalue = 0;
    var size = this.getGeometry().getSize();
    var leni = size.getTotalSize();
    // max to 3D
    if (size.length() === 3) {
      leni = size.getDimSize(3);
    }
    for (var i = 0; i < leni; ++i) {
      rvalue = this.getRescaledValueAtOffset(i);
      if (rvalue > rmax) {
        rmax = rvalue;
      }
      if (rvalue < rmin) {
        rmin = rvalue;
      }
    }
    // return
    return {min: rmin, max: rmax};
  }
};

/**
 * Calculate the histogram of the image.
 *
 * @returns {object} The histogram, data range and rescaled data range.
 */
dwv.image.Image.prototype.calculateHistogram = function () {
  var size = this.getGeometry().getSize();
  var histo = [];
  var min = this.getValueAtOffset(0);
  var max = min;
  var value = 0;
  var rmin = this.getRescaledValueAtOffset(0);
  var rmax = rmin;
  var rvalue = 0;
  for (var i = 0, leni = size.getTotalSize(); i < leni; ++i) {
    value = this.getValueAtOffset(i);
    if (value > max) {
      max = value;
    }
    if (value < min) {
      min = value;
    }
    rvalue = this.getRescaledValueAtOffset(i);
    if (rvalue > rmax) {
      rmax = rvalue;
    }
    if (rvalue < rmin) {
      rmin = rvalue;
    }
    histo[rvalue] = (histo[rvalue] || 0) + 1;
  }
  // set data range
  var dataRange = {min: min, max: max};
  var rescaledDataRange = {min: rmin, max: rmax};
  // generate data for plotting
  var histogram = [];
  for (var b = rmin; b <= rmax; ++b) {
    histogram.push([b, (histo[b] || 0)]);
  }
  // return
  return {
    dataRange: dataRange,
    rescaledDataRange: rescaledDataRange,
    histogram: histogram
  };
};

/**
 * Convolute the image with a given 2D kernel.
 *
 * Note: Uses raw buffer values.
 *
 * @param {Array} weights The weights of the 2D kernel as a 3x3 matrix.
 * @returns {Image} The convoluted image.
 */
dwv.image.Image.prototype.convolute2D = function (weights) {
  if (weights.length !== 9) {
    throw new Error(
      'The convolution matrix does not have a length of 9; it has ' +
      weights.length);
  }

  var newImage = this.clone();
  var newBuffer = newImage.getBuffer();

  var imgSize = this.getGeometry().getSize();
  var dimOffset = imgSize.getDimSize(2) * this.getNumberOfComponents();
  for (var k = 0; k < imgSize.get(2); ++k) {
    this.convoluteBuffer(weights, newBuffer, k * dimOffset);
  }

  return newImage;
};

/**
 * Convolute an image buffer with a given 2D kernel.
 *
 * Note: Uses raw buffer values.
 *
 * @param {Array} weights The weights of the 2D kernel as a 3x3 matrix.
 * @param {Array} buffer The buffer to convolute.
 * @param {number} startOffset The index to start at.
 */
dwv.image.Image.prototype.convoluteBuffer = function (
  weights, buffer, startOffset) {
  var imgSize = this.getGeometry().getSize();
  var ncols = imgSize.get(0);
  var nrows = imgSize.get(1);
  var ncomp = this.getNumberOfComponents();

  // number of component and planar configuration vars
  var factor = 1;
  var componentOffset = 1;
  if (ncomp === 3) {
    if (this.getPlanarConfiguration() === 0) {
      factor = 3;
    } else {
      componentOffset = imgSize.getDimSize(2);
    }
  }

  // allow special indent for matrices
  /*jshint indent:false */

  // default weight offset matrix
  var wOff = [];
  wOff[0] = (-ncols - 1) * factor;
  wOff[1] = (-ncols) * factor;
  wOff[2] = (-ncols + 1) * factor;
  wOff[3] = -factor;
  wOff[4] = 0;
  wOff[5] = 1 * factor;
  wOff[6] = (ncols - 1) * factor;
  wOff[7] = (ncols) * factor;
  wOff[8] = (ncols + 1) * factor;

  // border weight offset matrices
  // borders are extended (see http://en.wikipedia.org/wiki/Kernel_%28image_processing%29)

  // i=0, j=0
  var wOff00 = [];
  wOff00[0] = wOff[4]; wOff00[1] = wOff[4]; wOff00[2] = wOff[5];
  wOff00[3] = wOff[4]; wOff00[4] = wOff[4]; wOff00[5] = wOff[5];
  wOff00[6] = wOff[7]; wOff00[7] = wOff[7]; wOff00[8] = wOff[8];
  // i=0, j=*
  var wOff0x = [];
  wOff0x[0] = wOff[1]; wOff0x[1] = wOff[1]; wOff0x[2] = wOff[2];
  wOff0x[3] = wOff[4]; wOff0x[4] = wOff[4]; wOff0x[5] = wOff[5];
  wOff0x[6] = wOff[7]; wOff0x[7] = wOff[7]; wOff0x[8] = wOff[8];
  // i=0, j=nrows
  var wOff0n = [];
  wOff0n[0] = wOff[1]; wOff0n[1] = wOff[1]; wOff0n[2] = wOff[2];
  wOff0n[3] = wOff[4]; wOff0n[4] = wOff[4]; wOff0n[5] = wOff[5];
  wOff0n[6] = wOff[4]; wOff0n[7] = wOff[4]; wOff0n[8] = wOff[5];

  // i=*, j=0
  var wOffx0 = [];
  wOffx0[0] = wOff[3]; wOffx0[1] = wOff[4]; wOffx0[2] = wOff[5];
  wOffx0[3] = wOff[3]; wOffx0[4] = wOff[4]; wOffx0[5] = wOff[5];
  wOffx0[6] = wOff[6]; wOffx0[7] = wOff[7]; wOffx0[8] = wOff[8];
  // i=*, j=* -> wOff
  // i=*, j=nrows
  var wOffxn = [];
  wOffxn[0] = wOff[0]; wOffxn[1] = wOff[1]; wOffxn[2] = wOff[2];
  wOffxn[3] = wOff[3]; wOffxn[4] = wOff[4]; wOffxn[5] = wOff[5];
  wOffxn[6] = wOff[3]; wOffxn[7] = wOff[4]; wOffxn[8] = wOff[5];

  // i=ncols, j=0
  var wOffn0 = [];
  wOffn0[0] = wOff[3]; wOffn0[1] = wOff[4]; wOffn0[2] = wOff[4];
  wOffn0[3] = wOff[3]; wOffn0[4] = wOff[4]; wOffn0[5] = wOff[4];
  wOffn0[6] = wOff[6]; wOffn0[7] = wOff[7]; wOffn0[8] = wOff[7];
  // i=ncols, j=*
  var wOffnx = [];
  wOffnx[0] = wOff[0]; wOffnx[1] = wOff[1]; wOffnx[2] = wOff[1];
  wOffnx[3] = wOff[3]; wOffnx[4] = wOff[4]; wOffnx[5] = wOff[4];
  wOffnx[6] = wOff[6]; wOffnx[7] = wOff[7]; wOffnx[8] = wOff[7];
  // i=ncols, j=nrows
  var wOffnn = [];
  wOffnn[0] = wOff[0]; wOffnn[1] = wOff[1]; wOffnn[2] = wOff[1];
  wOffnn[3] = wOff[3]; wOffnn[4] = wOff[4]; wOffnn[5] = wOff[4];
  wOffnn[6] = wOff[3]; wOffnn[7] = wOff[4]; wOffnn[8] = wOff[4];

  // restore indent for rest of method
  /*jshint indent:4 */

  // loop vars
  var pixelOffset = startOffset;
  var newValue = 0;
  var wOffFinal = [];
  for (var c = 0; c < ncomp; ++c) {
    // component offset
    pixelOffset += c * componentOffset;
    for (var j = 0; j < nrows; ++j) {
      for (var i = 0; i < ncols; ++i) {
        wOffFinal = wOff;
        // special border cases
        if (i === 0 && j === 0) {
          wOffFinal = wOff00;
        } else if (i === 0 && j === (nrows - 1)) {
          wOffFinal = wOff0n;
        } else if (i === (ncols - 1) && j === 0) {
          wOffFinal = wOffn0;
        } else if (i === (ncols - 1) && j === (nrows - 1)) {
          wOffFinal = wOffnn;
        } else if (i === 0 && j !== (nrows - 1) && j !== 0) {
          wOffFinal = wOff0x;
        } else if (i === (ncols - 1) && j !== (nrows - 1) && j !== 0) {
          wOffFinal = wOffnx;
        } else if (i !== 0 && i !== (ncols - 1) && j === 0) {
          wOffFinal = wOffx0;
        } else if (i !== 0 && i !== (ncols - 1) && j === (nrows - 1)) {
          wOffFinal = wOffxn;
        }
        // calculate the weighed sum of the source image pixels that
        // fall under the convolution matrix
        newValue = 0;
        for (var wi = 0; wi < 9; ++wi) {
          newValue += this.getValueAtOffset(
            pixelOffset + wOffFinal[wi]) * weights[wi];
        }
        buffer[pixelOffset] = newValue;
        // increment pixel offset
        pixelOffset += factor;
      }
    }
  }
};

/**
 * Transform an image using a specific operator.
 * WARNING: no size check!
 *
 * @param {Function} operator The operator to use when transforming.
 * @returns {Image} The transformed image.
 * Note: Uses the raw buffer values.
 */
dwv.image.Image.prototype.transform = function (operator) {
  var newImage = this.clone();
  var newBuffer = newImage.getBuffer();
  for (var i = 0, leni = newBuffer.length; i < leni; ++i) {
    newBuffer[i] = operator(newImage.getValueAtOffset(i));
  }
  return newImage;
};

/**
 * Compose this image with another one and using a specific operator.
 * WARNING: no size check!
 *
 * @param {Image} rhs The image to compose with.
 * @param {Function} operator The operator to use when composing.
 * @returns {Image} The composed image.
 * Note: Uses the raw buffer values.
 */
dwv.image.Image.prototype.compose = function (rhs, operator) {
  var newImage = this.clone();
  var newBuffer = newImage.getBuffer();
  for (var i = 0, leni = newBuffer.length; i < leni; ++i) {
    // using the operator on the local buffer, i.e. the
    // latest (not original) data
    newBuffer[i] = Math.floor(
      operator(this.getValueAtOffset(i), rhs.getValueAtOffset(i))
    );
  }
  return newImage;
};