src/dicom/dicomElementsWrapper.js

// namespaces
var dwv = dwv || {};
dwv.dicom = dwv.dicom || {};

/**
 * DicomElements wrapper.
 *
 * @class
 * @param {Array} dicomElements The elements to wrap.
 */
dwv.dicom.DicomElementsWrapper = function (dicomElements) {

  /**
   * Get a DICOM Element value from a group/element key.
   *
   * @param {string} groupElementKey The key to retrieve.
   * @returns {object} The DICOM element.
   */
  this.getDEFromKey = function (groupElementKey) {
    return dicomElements[groupElementKey];
  };

  /**
   * Get a DICOM Element value from a group/element key.
   *
   * @param {string} groupElementKey The key to retrieve.
   * @param {boolean} asArray Get the value as an Array.
   * @returns {object} The DICOM element value.
   */
  this.getFromKey = function (groupElementKey, asArray) {
    // default
    if (typeof asArray === 'undefined') {
      asArray = false;
    }
    var value = null;
    var dElement = dicomElements[groupElementKey];
    if (typeof dElement !== 'undefined') {
      // raw value if only one
      if (dElement.value.length === 1 && asArray === false) {
        value = dElement.value[0];
      } else {
        value = dElement.value;
      }
    }
    return value;
  };

  /**
   * Dump the DICOM tags to an object.
   *
   * @returns {object} The DICOM tags as an object.
   */
  this.dumpToObject = function () {
    var keys = Object.keys(dicomElements);
    var obj = {};
    var dicomElement = null;
    for (var i = 0, leni = keys.length; i < leni; ++i) {
      dicomElement = dicomElements[keys[i]];
      obj[this.getTagName(dicomElement.tag)] =
        this.getElementAsObject(dicomElement);
    }
    return obj;
  };

  /**
   * Get a tag string name from the dictionary.
   *
   * @param {object} tag The DICOM tag object.
   * @returns {string} The tag name.
   */
  this.getTagName = function (tag) {
    var tagObj = new dwv.dicom.Tag(tag.group, tag.element);
    var name = tagObj.getNameFromDictionary();
    if (name === null) {
      name = tagObj.getKey2();
    }
    return name;
  };

  /**
   * Get a DICOM element as a simple object.
   *
   * @param {object} dicomElement The DICOM element.
   * @returns {object} The element as a simple object.
   */
  this.getElementAsObject = function (dicomElement) {
    // element value
    var value = null;

    var isPixel = dicomElement.tag.group === '0x7FE0' &&
      dicomElement.tag.element === '0x0010';

    var vr = dicomElement.vr;
    if (vr === 'SQ' &&
      typeof dicomElement.value !== 'undefined' &&
      !isPixel) {
      value = [];
      var items = dicomElement.value;
      var itemValues = null;
      for (var i = 0; i < items.length; ++i) {
        itemValues = {};
        var keys = Object.keys(items[i]);
        for (var k = 0; k < keys.length; ++k) {
          var itemElement = items[i][keys[k]];
          var key = this.getTagName(itemElement.tag);
          // do not inclure Item elements
          if (key !== 'Item') {
            itemValues[key] = this.getElementAsObject(itemElement);
          }
        }
        value.push(itemValues);
      }
    } else {
      value = this.getElementValueAsString(dicomElement);
    }

    // return
    return {
      value: value,
      group: dicomElement.tag.group,
      element: dicomElement.tag.element,
      vr: vr,
      vl: dicomElement.vl
    };
  };

  /**
   * Dump the DICOM tags to a string.
   *
   * @returns {string} The dumped file.
   */
  this.dump = function () {
    var keys = Object.keys(dicomElements);
    var result = '\n';
    result += '# Dicom-File-Format\n';
    result += '\n';
    result += '# Dicom-Meta-Information-Header\n';
    result += '# Used TransferSyntax: ';
    if (dwv.dicom.isNativeLittleEndian()) {
      result += 'Little Endian Explicit\n';
    } else {
      result += 'NOT Little Endian Explicit\n';
    }
    var dicomElement = null;
    var checkHeader = true;
    for (var i = 0, leni = keys.length; i < leni; ++i) {
      dicomElement = dicomElements[keys[i]];
      if (checkHeader && dicomElement.tag.group !== '0x0002') {
        result += '\n';
        result += '# Dicom-Data-Set\n';
        result += '# Used TransferSyntax: ';
        var syntax = dwv.dicom.cleanString(dicomElements.x00020010.value[0]);
        result += dwv.dicom.getTransferSyntaxName(syntax);
        result += '\n';
        checkHeader = false;
      }
      result += this.getElementAsString(dicomElement) + '\n';
    }
    return result;
  };

};

/**
 * Get a data element value as a string.
 *
 * @param {object} dicomElement The DICOM element.
 * @param {boolean} pretty When set to true, returns a 'pretified' content.
 * @returns {string} A string representation of the DICOM element.
 */
dwv.dicom.DicomElementsWrapper.prototype.getElementValueAsString = function (
  dicomElement, pretty) {
  var str = '';
  var strLenLimit = 65;

  // dafault to pretty output
  if (typeof pretty === 'undefined') {
    pretty = true;
  }
  // check dicom element input
  if (typeof dicomElement === 'undefined' || dicomElement === null) {
    return str;
  }

  // Polyfill for Number.isInteger.
  var isInteger = Number.isInteger || function (value) {
    return typeof value === 'number' &&
      isFinite(value) &&
      Math.floor(value) === value;
  };

  // TODO Support sequences.

  if (dicomElement.vr !== 'SQ' &&
    dicomElement.value.length === 1 && dicomElement.value[0] === '') {
    str += '(no value available)';
  } else if (dicomElement.tag.group === '0x7FE0' &&
    dicomElement.tag.element === '0x0010' &&
    dicomElement.vl === 'u/l') {
    str = '(PixelSequence)';
  } else if (dicomElement.vr === 'DA' && pretty) {
    var daValue = dicomElement.value[0];
    // Two possible date formats:
    // - standard 'YYYYMMDD'
    // - non-standard 'YYYY.MM.DD' (previous ACR-NEMA)
    var monthBeginIndex = 4;
    var dayBeginIndex = 6;
    if (daValue.length !== 8) {
      monthBeginIndex = 5;
      dayBeginIndex = 8;
    }
    var da = new Date(
      parseInt(daValue.substr(0, 4), 10),
      parseInt(daValue.substr(monthBeginIndex, 2), 10) - 1, // 0-11 range
      parseInt(daValue.substr(dayBeginIndex, 2), 10));
    str = da.toLocaleDateString();
  } else if (dicomElement.vr === 'TM' && pretty) {
    var tmValue = dicomElement.value[0];
    var tmHour = tmValue.substr(0, 2);
    var tmMinute = tmValue.length >= 4 ? tmValue.substr(2, 2) : '00';
    var tmSeconds = tmValue.length >= 6 ? tmValue.substr(4, 2) : '00';
    str = tmHour + ':' + tmMinute + ':' + tmSeconds;
  } else {
    var isOtherVR = false;
    if (dicomElement.vr.length !== 0) {
      isOtherVR = (dicomElement.vr[0].toUpperCase() === 'O');
    }
    var isFloatNumberVR = (dicomElement.vr === 'FL' ||
      dicomElement.vr === 'FD' ||
      dicomElement.vr === 'DS');
    var valueStr = '';
    for (var k = 0, lenk = dicomElement.value.length; k < lenk; ++k) {
      valueStr = '';
      if (k !== 0) {
        valueStr += '\\';
      }
      if (isFloatNumberVR) {
        var val = dicomElement.value[k];
        if (typeof val === 'string') {
          val = dwv.dicom.cleanString(val);
        }
        var num = Number(val);
        if (!isInteger(num) && pretty) {
          valueStr += num.toPrecision(4);
        } else {
          valueStr += num.toString();
        }
      } else if (isOtherVR) {
        var tmp = dicomElement.value[k].toString(16);
        if (dicomElement.vr === 'OB') {
          tmp = '00'.substr(0, 2 - tmp.length) + tmp;
        } else {
          tmp = '0000'.substr(0, 4 - tmp.length) + tmp;
        }
        valueStr += tmp;
      } else if (typeof dicomElement.value[k] === 'string') {
        valueStr += dwv.dicom.cleanString(dicomElement.value[k]);
      } else {
        valueStr += dicomElement.value[k];
      }
      // check length
      if (str.length + valueStr.length <= strLenLimit) {
        str += valueStr;
      } else {
        str += '...';
        break;
      }
    }
  }
  return str;
};

/**
 * Get a data element value as a string.
 *
 * @param {string} groupElementKey The key to retrieve.
 * @returns {string} The element as a string.
 */
dwv.dicom.DicomElementsWrapper.prototype.getElementValueAsStringFromKey =
function (groupElementKey) {
  return this.getElementValueAsString(this.getDEFromKey(groupElementKey));
};

/**
 * Get a data element as a string.
 *
 * @param {object} dicomElement The DICOM element.
 * @param {string} prefix A string to prepend this one.
 * @returns {string} The element as a string.
 */
dwv.dicom.DicomElementsWrapper.prototype.getElementAsString = function (
  dicomElement, prefix) {
  // default prefix
  prefix = prefix || '';

  // get tag anme from dictionary
  var tag = new dwv.dicom.Tag(
    dicomElement.tag.group, dicomElement.tag.element);
  var tagName = tag.getNameFromDictionary();

  var deSize = dicomElement.value.length;
  var isOtherVR = false;
  if (dicomElement.vr.length !== 0) {
    isOtherVR = (dicomElement.vr[0].toUpperCase() === 'O');
  }

  // no size for delimitations
  if (dicomElement.tag.group === '0xFFFE' && (
    dicomElement.tag.element === '0xE00D' ||
    dicomElement.tag.element === '0xE0DD')) {
    deSize = 0;
  } else if (isOtherVR) {
    deSize = 1;
  }

  var isPixSequence = (dicomElement.tag.group === '0x7FE0' &&
    dicomElement.tag.element === '0x0010' &&
    dicomElement.vl === 'u/l');

  var line = null;

  // (group,element)
  line = '(';
  line += dicomElement.tag.group.substr(2, 5).toLowerCase();
  line += ',';
  line += dicomElement.tag.element.substr(2, 5).toLowerCase();
  line += ') ';
  // value representation
  line += dicomElement.vr;
  // value
  if (dicomElement.vr !== 'SQ' &&
    dicomElement.value.length === 1 &&
    dicomElement.value[0] === '') {
    line += ' (no value available)';
    deSize = 0;
  } else {
    // simple number display
    if (dicomElement.vr === 'na') {
      line += ' ';
      line += dicomElement.value[0];
    } else if (isPixSequence) {
      // pixel sequence
      line += ' (PixelSequence #=' + deSize + ')';
    } else if (dicomElement.vr === 'SQ') {
      line += ' (Sequence with';
      if (dicomElement.vl === 'u/l') {
        line += ' undefined';
      } else {
        line += ' explicit';
      }
      line += ' length #=';
      line += dicomElement.value.length;
      line += ')';
    } else if (isOtherVR ||
        dicomElement.vr === 'pi' ||
        dicomElement.vr === 'UL' ||
        dicomElement.vr === 'US' ||
        dicomElement.vr === 'SL' ||
        dicomElement.vr === 'SS' ||
        dicomElement.vr === 'FL' ||
        dicomElement.vr === 'FD' ||
        dicomElement.vr === 'AT') {
      // 'O'ther array, limited display length
      line += ' ';
      line += this.getElementValueAsString(dicomElement, false);
    } else {
      // default
      line += ' [';
      line += this.getElementValueAsString(dicomElement, false);
      line += ']';
    }
  }

  // align #
  var nSpaces = 55 - line.length;
  if (nSpaces > 0) {
    for (var s = 0; s < nSpaces; ++s) {
      line += ' ';
    }
  }
  line += ' # ';
  if (dicomElement.vl < 100) {
    line += ' ';
  }
  if (dicomElement.vl < 10) {
    line += ' ';
  }
  line += dicomElement.vl;
  line += ', ';
  line += deSize; //dictElement[1];
  line += ' ';
  if (tagName !== null) {
    line += tagName;
  } else {
    line += 'Unknown Tag & Data';
  }

  var message = null;

  // continue for sequence
  if (dicomElement.vr === 'SQ') {
    var item = null;
    for (var l = 0, lenl = dicomElement.value.length; l < lenl; ++l) {
      item = dicomElement.value[l];
      var itemKeys = Object.keys(item);
      if (itemKeys.length === 0) {
        continue;
      }

      // get the item element
      var itemElement = item.xFFFEE000;
      message = '(Item with';
      if (itemElement.vl === 'u/l') {
        message += ' undefined';
      } else {
        message += ' explicit';
      }
      message += ' length #=' + (itemKeys.length - 1) + ')';
      itemElement.value = [message];
      itemElement.vr = 'na';

      line += '\n';
      line += this.getElementAsString(itemElement, prefix + '  ');

      for (var m = 0, lenm = itemKeys.length; m < lenm; ++m) {
        if (itemKeys[m] !== 'xFFFEE000') {
          line += '\n';
          line += this.getElementAsString(item[itemKeys[m]], prefix + '    ');
        }
      }

      message = '(ItemDelimitationItem';
      if (itemElement.vl !== 'u/l') {
        message += ' for re-encoding';
      }
      message += ')';
      var itemDelimElement = {
        tag: {group: '0xFFFE', element: '0xE00D'},
        vr: 'na',
        vl: '0',
        value: [message]
      };
      line += '\n';
      line += this.getElementAsString(itemDelimElement, prefix + '  ');

    }

    message = '(SequenceDelimitationItem';
    if (dicomElement.vl !== 'u/l') {
      message += ' for re-encod.';
    }
    message += ')';
    var sqDelimElement = {
      tag: {group: '0xFFFE', element: '0xE0DD'},
      vr: 'na',
      vl: '0',
      value: [message]
    };
    line += '\n';
    line += this.getElementAsString(sqDelimElement, prefix);
  } else if (isPixSequence) {
    // pixel sequence
    var pixItem = null;
    for (var n = 0, lenn = dicomElement.value.length; n < lenn; ++n) {
      pixItem = dicomElement.value[n];
      line += '\n';
      pixItem.vr = 'pi';
      line += this.getElementAsString(pixItem, prefix + '  ');
    }

    var pixDelimElement = {
      tag: {group: '0xFFFE', element: '0xE0DD'},
      vr: 'na',
      vl: '0',
      value: ['(SequenceDelimitationItem)']
    };
    line += '\n';
    line += this.getElementAsString(pixDelimElement, prefix);
  }

  return prefix + line;
};

/**
 * Get a DICOM Element value from a group and an element.
 *
 * @param {number} group The group.
 * @param {number} element The element.
 * @returns {object} The DICOM element value.
 */
dwv.dicom.DicomElementsWrapper.prototype.getFromGroupElement = function (
  group, element) {
  return this.getFromKey(new dwv.dicom.Tag(group, element).getKey());
};

/**
 * Get a DICOM Element value from a tag name.
 * Uses the DICOM dictionary.
 *
 * @param {string} name The tag name.
 * @returns {object} The DICOM element value.
 */
dwv.dicom.DicomElementsWrapper.prototype.getFromName = function (name) {
  var value = null;
  var tag = dwv.dicom.getTagFromDictionary(name);
  // check that we are not at the end of the dictionary
  if (tag !== null) {
    value = this.getFromKey(tag.getKey());
  }
  return value;
};

/**
 * Get the pixel spacing from the different spacing tags.
 *
 * @returns {object} The read spacing or the default [1,1].
 */
dwv.dicom.DicomElementsWrapper.prototype.getPixelSpacing = function () {
  // default
  var rowSpacing = 1;
  var columnSpacing = 1;

  // 1. PixelSpacing
  // 2. ImagerPixelSpacing
  // 3. NominalScannedPixelSpacing
  // 4. PixelAspectRatio
  var keys = ['x00280030', 'x00181164', 'x00182010', 'x00280034'];
  for (var k = 0; k < keys.length; ++k) {
    var spacing = this.getFromKey(keys[k], true);
    if (spacing && spacing.length === 2) {
      rowSpacing = parseFloat(spacing[0]);
      columnSpacing = parseFloat(spacing[1]);
      break;
    }
  }

  // check
  if (columnSpacing === 0) {
    dwv.logger.warn('Zero column spacing.');
    columnSpacing = 1;
  }
  if (rowSpacing === 0) {
    dwv.logger.warn('Zero row spacing.');
    rowSpacing = 1;
  }

  // return
  // (slice spacing will be calculated using the image position patient)
  return new dwv.image.Spacing([columnSpacing, rowSpacing, 1]);
};

/**
 * Get the file list from a DICOMDIR
 *
 * @param {object} data The buffer data of the DICOMDIR
 * @returns {Array} The file list as an array ordered by
 *   STUDY > SERIES > IMAGES.
 */
dwv.dicom.getFileListFromDicomDir = function (data) {
  // parse file
  var parser = new dwv.dicom.DicomParser();
  parser.parse(data);
  var elements = parser.getRawDicomElements();

  // Directory Record Sequence
  if (typeof elements.x00041220 === 'undefined' ||
    typeof elements.x00041220.value === 'undefined') {
    dwv.logger.warn('No Directory Record Sequence found in DICOMDIR.');
    return;
  }
  var dirSeq = elements.x00041220.value;

  if (dirSeq.length === 0) {
    dwv.logger.warn('The Directory Record Sequence of the DICOMDIR is empty.');
    return;
  }

  var records = [];
  var series = null;
  var study = null;
  for (var i = 0; i < dirSeq.length; ++i) {
    // Directory Record Type
    if (typeof dirSeq[i].x00041430 === 'undefined' ||
      typeof dirSeq[i].x00041430.value === 'undefined') {
      continue;
    }
    var recType = dwv.dicom.cleanString(dirSeq[i].x00041430.value[0]);

    // supposed to come in order...
    if (recType === 'STUDY') {
      study = [];
      records.push(study);
    } else if (recType === 'SERIES') {
      series = [];
      study.push(series);
    } else if (recType === 'IMAGE') {
      // Referenced File ID
      if (typeof dirSeq[i].x00041500 === 'undefined' ||
        typeof dirSeq[i].x00041500.value === 'undefined') {
        continue;
      }
      var refFileIds = dirSeq[i].x00041500.value;
      // clean and join ids
      var refFileId = '';
      for (var j = 0; j < refFileIds.length; ++j) {
        if (j !== 0) {
          refFileId += '/';
        }
        refFileId += dwv.dicom.cleanString(refFileIds[j]);
      }
      series.push(refFileId);
    }
  }
  return records;
};