src_dicom_dicomTag.js

import {
  dictionary,
  tagGroups
} from './dictionary.js';

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

/**
 * Immutable tag.
 */
export class Tag {

  /**
   * The tag group.
   *
   * @type {string}
   */
  #group;

  /**
   * The tag element.
   *
   * @type {string}
   */
  #element;

  /**
   * @param {string} group The tag group as '####'.
   * @param {string} element The tag element as '####'.
   */
  constructor(group, element) {
    if (!group || typeof group === 'undefined') {
      throw new Error('Cannot create tag with no group.');
    }
    if (group.length !== 4) {
      throw new Error('Cannot create tag with badly sized group: ' + group);
    }
    if (!element || typeof element === 'undefined') {
      throw new Error('Cannot create tag with no element.');
    }
    if (element.length !== 4) {
      throw new Error('Cannot create tag with badly sized element: ' + element);
    }
    this.#group = group;
    this.#element = element;
  }

  /**
   * Get the tag group.
   *
   * @returns {string} The tag group.
   */
  getGroup() {
    return this.#group;
  }

  /**
   * Get the tag element.
   *
   * @returns {string} The tag element.
   */
  getElement() {
    return this.#element;
  }

  /**
   * Get as string representation of the tag: 'key: name'.
   *
   * @returns {string} A string representing the tag.
   */
  toString() {
    return this.getKey() + ': ' + this.getNameFromDictionary();
  }

  /**
   * Check for Tag equality.
   *
   * @param {Tag} rhs The other tag to compare to.
   * @returns {boolean} True if both tags are equal.
   */
  equals(rhs) {
    return rhs !== null &&
      typeof rhs !== 'undefined' &&
      this.#group === rhs.getGroup() &&
      this.#element === rhs.getElement();
  }

  /**
   * Get the group-element key used to store DICOM elements.
   *
   * @returns {string} The key as '########'.
   */
  getKey() {
    return this.#group + this.#element;
  }

  /**
   * Get the group name as defined in TagGroups.
   *
   * @returns {string} The name.
   */
  getGroupName() {
    return tagGroups[this.#group];
  }

  /**
   * Does this tag have a VR.
   * Basically not the Item, ItemDelimitationItem nor
   *  SequenceDelimitationItem tags.
   *
   * @returns {boolean} True if this tag has a VR.
   */
  isWithVR() {
    return !(this.#group === 'FFFE' &&
      (this.#element === 'E000' ||
      this.#element === 'E00D' ||
      this.#element === 'E0DD')
    );
  }

  /**
   * Is the tag group a private tag group ?
   *
   * See: {@link http://dicom.nema.org/medical/dicom/2022a/output/html/part05.html#sect_7.8}.
   *
   * @returns {boolean} True if the tag group is private,
   *   ie if its group is an odd number.
   */
  isPrivate() {
    return parseInt(this.#group, 16) % 2 === 1;
  }

  /**
   * Get the tag info from the dicom dictionary.
   *
   * @returns {string[]|undefined} The info as [vr, multiplicity, name].
   */
  #getInfoFromDictionary() {
    let info;
    if (typeof dictionary[this.#group] !== 'undefined' &&
      typeof dictionary[this.#group][this.#element] !==
        'undefined') {
      info = dictionary[this.#group][this.#element];
    }
    return info;
  }

  /**
   * Get the tag Value Representation (VR) from the dicom dictionary.
   *
   * @returns {string|undefined} The VR.
   */
  getVrFromDictionary() {
    let vr;
    const info = this.#getInfoFromDictionary();
    if (typeof info !== 'undefined') {
      vr = info[0];
    }
    return vr;
  }

  /**
   * Get the multiplicity from the dicom dictionary.
   *
   * @returns {number|undefined} The multiplicity.
   */
  getMultiplicityFromDictionary() {
    let multiplicity;
    const info = this.#getInfoFromDictionary();
    if (typeof info !== 'undefined') {
      multiplicity = parseInt(info[1], 10);
    }
    return multiplicity;
  }

  /**
   * Get the tag name from the dicom dictionary.
   *
   * @returns {string|undefined} The VR.
   */
  getNameFromDictionary() {
    let name;
    const info = this.#getInfoFromDictionary();
    if (typeof info !== 'undefined') {
      name = info[2];
    }
    return name;
  }

} // Tag class

/**
 * Tag compare function.
 *
 * @param {Tag} a The first tag.
 * @param {Tag} b The second tag.
 * @returns {number} The result of the tag comparison,
 *   positive for b before a, negative for a before b and
 *   zero to keep same order.
 */
export function tagCompareFunction(a, b) {
  // first by group
  let res = parseInt(a.getGroup(), 16) - parseInt(b.getGroup(), 16);
  if (res === 0) {
    // by element if same group
    res = parseInt(a.getElement(), 16) - parseInt(b.getElement(), 16);
  }
  return res;
}

/**
 * Split a group-element key used to store DICOM elements.
 *
 * @param {string} key The key in form "00280102" as generated by tag::getKey.
 * @returns {Tag} The DICOM tag.
 */
export function getTagFromKey(key) {
  if (!key || typeof key === 'undefined') {
    throw new Error('Cannot create tag with no key.');
  }
  if (key.length !== 8) {
    throw new Error('Cannot create tag with badly sized key: ' + key);
  }
  return new Tag(key.substring(0, 4), key.substring(4, 8));
}

/**
 * Get the TransferSyntaxUID Tag.
 *
 * @returns {Tag} The tag.
 */
export function getTransferSyntaxUIDTag() {
  return new Tag('0002', '0010');
}

/**
 * Get the FileMetaInformationGroupLength Tag.
 *
 * @returns {Tag} The tag.
 */
export function getFileMetaInformationGroupLengthTag() {
  return new Tag('0002', '0000');
}

/**
 * Is the input tag the FileMetaInformationGroupLength Tag.
 *
 * @param {Tag} tag The tag to test.
 * @returns {boolean} True if the asked tag.
 */
export function isFileMetaInformationGroupLengthTag(tag) {
  return tag.equals(getFileMetaInformationGroupLengthTag());
}

/**
 * Get the Item Tag.
 *
 * @returns {Tag} The tag.
 */
export function getItemTag() {
  return new Tag('FFFE', 'E000');
}

/**
 * Is the input tag the Item Tag.
 *
 * @param {Tag} tag The tag to test.
 * @returns {boolean} True if the asked tag.
 */
export function isItemTag(tag) {
  // faster than tag.equals(getItemTag());
  return tag.getKey() === 'FFFEE000';
}

/**
 * Get the ItemDelimitationItem Tag.
 *
 * @returns {Tag} The tag.
 */
export function getItemDelimitationItemTag() {
  return new Tag('FFFE', 'E00D');
}

/**
 * Is the input tag the ItemDelimitationItem Tag.
 *
 * @param {Tag} tag The tag to test.
 * @returns {boolean} True if the asked tag.
 */
export function isItemDelimitationItemTag(tag) {
  // faster than tag.equals(getItemDelimitationItemTag());
  return tag.getKey() === 'FFFEE00D';
}

/**
 * Get the SequenceDelimitationItem Tag.
 *
 * @returns {Tag} The tag.
 */
export function getSequenceDelimitationItemTag() {
  return new Tag('FFFE', 'E0DD');
}

/**
 * Is the input tag the SequenceDelimitationItem Tag.
 *
 * @param {Tag} tag The tag to test.
 * @returns {boolean} True if the asked tag.
 */
export function isSequenceDelimitationItemTag(tag) {
  // faster than tag.equals(getSequenceDelimitationItemTag());
  return tag.getKey() === 'FFFEE0DD';
}

/**
 * Get the PixelData Tag.
 *
 * @returns {Tag} The tag.
 */
export function getPixelDataTag() {
  return new Tag('7FE0', '0010');
}

/**
 * Get the list of pixel data DICOM tag keys: PixelData,
 *   FloatPixelData and DoubleFloatPixelData.
 *
 * @returns {string[]} The key list.
 */
function getAllPixelDataTagKeys() {
  return ['7FE00010', '7FE00009', '7FE00008'];
}

/**
 * Is the input tag one of the pixel data tag.
 *
 * @param {Tag} tag The tag to test.
 * @returns {boolean} True if input is a pixel data tag.
 */
export function isAnyPixelDataTag(tag) {
  const keys = getAllPixelDataTagKeys();
  return keys.includes(tag.getKey());
}

/**
 * Check if an input data elements contains a pixel data element.
 *
 * @param {Object<string, DataElement>} elements Data elements.
 * @returns {boolean} True if the elements contain one of the
 *   pixel data tags.
 */
export function hasAnyPixelDataElement(elements) {
  const keys = getAllPixelDataTagKeys();
  let found = false;
  for (const key of keys) {
    if (typeof elements[key] !== 'undefined') {
      found = true;
      break;
    }
  }
  return found;
}

/**
 * Get a tag from the dictionary using a tag string name.
 *
 * @param {string} tagName The tag string name.
 * @returns {Tag|undefined} The tag object or null if not found.
 */
export function getTagFromDictionary(tagName) {
  if (typeof tagName === 'undefined' || tagName === null) {
    return null;
  }
  let group = null;
  let element = null;
  const dict = dictionary;
  const keys0 = Object.keys(dict);
  let keys1 = null;
  let foundTag = false;
  // search through dictionary
  for (let k0 = 0, lenK0 = keys0.length; k0 < lenK0; ++k0) {
    group = keys0[k0];
    keys1 = Object.keys(dict[group]);
    for (let k1 = 0, lenK1 = keys1.length; k1 < lenK1; ++k1) {
      element = keys1[k1];
      if (dict[group][element][2] === tagName) {
        foundTag = true;
        break;
      }
    }
    if (foundTag) {
      break;
    }
  }
  let tag;
  if (foundTag) {
    tag = new Tag(group, element);
  }
  return tag;
}

/**
 * Get an array reducer to reduce an array of tag keys taken from
 *   the input dataElements and return as simple elements.
 *
 * @param {Object<string, DataElement>} dataElements The meta data
 *   index by tag keys.
 * @returns {any} An array reducer callbackFn.
 */
function getSimpleElementReducer(dataElements) {
  return function (accumulator, currentValue) {
    // get the tag name
    const tag = getTagFromKey(currentValue);
    let tagName = tag.getNameFromDictionary();
    if (typeof tagName === 'undefined') {
      // add 'x' to list private at end
      tagName = 'x' + tag.getKey();
    }
    const currentMeta = dataElements[currentValue];
    // remove undefined properties
    for (const property in currentMeta) {
      if (typeof currentMeta[property] === 'undefined') {
        delete currentMeta[property];
      }
    }
    let tagValue;
    // recurse for sequences
    if (currentMeta.vr === 'SQ') {
      tagValue = {value: []};
      // valid for 1D array, not for merged data elements
      for (let i = 0; i < currentMeta.value.length; ++i) {
        const item = currentMeta.value[i];
        tagValue.value.push(Object.keys(item).reduce(
          getSimpleElementReducer(item), {}));
      }
    } else {
      if (currentMeta.value.length === 1) {
        tagValue = currentMeta.value[0];
      } else {
        tagValue = currentMeta.value;
      }
    }
    accumulator[tagName] = tagValue;
    return accumulator;
  };
}

/**
 * Get the meta data as simple elements:
 * - indexed by tag names instead of tag keys,
 * - no element object, just value if not sequence nor merged item.
 *
 * @param {Object<string, DataElement>} metaData The meta data
 *   index by tag keys.
 * @returns {Object<string, any>} The simple elements.
 */
export function getAsSimpleElements(metaData) {
  const meta = structuredClone(metaData);
  return Object.keys(meta).reduce(getSimpleElementReducer(meta), {});
}