tests_dicom_dicomParser.test.js

import {safeGet} from '../../src/dicom/dataElement.js';
import {
  getDwvVersion,
  getDwvVersionUI,
  getDwvUIDPrefix,
  getImplementationClassUID,
  getDwvVersionFromImplementationClassUID,
  compareVersions,
  isVersionInBounds,
  cleanString,
  hasDicomPrefix,
  DicomParser
} from '../../src/dicom/dicomParser.js';
import {
  Tag,
  getPixelDataTag
} from '../../src/dicom/dicomTag.js';
import {getFileListFromDicomDir} from '../../src/dicom/dicomDir.js';
import {b64urlToArrayBuffer} from './utils.js';

import dwvTestSimple from '../data/dwv-test-simple.dcm';
import dwvTestSequence from '../data/dwv-test-sequence.dcm';
import dwvDicomDir from '../data/DICOMDIR';

/**
 * Tests for the 'dicom/dicomParser.js' file.
 */

// Do not warn if these variables were not defined before.
/* global QUnit */
QUnit.module('dicom');

/**
 * Tests for {@link DicomParser} Implementation class.
 *
 * @function module:tests/dicom~implementaion-class
 */
QUnit.test('DICOM parsing - Implementation class and name', function (assert) {
  // check UI
  const versionUI = getDwvVersionUI();
  const regex = /[a-zA-Z]/g;
  assert.equal(versionUI.search(regex), -1, 'No letters in vr=UI');

  // test #0: get and parse
  const version00 = getDwvVersion();
  // dot and possible '-' for beta
  const classUID0 = getImplementationClassUID();
  const version01 = getDwvVersionFromImplementationClassUID(classUID0);
  assert.deepEqual(version00, version01, 'Implementation class test #0');

  // test #1: basic
  const version10 = '1.2.3';
  const classUID10 = getDwvUIDPrefix() + '.' + version10;
  const version11 = getDwvVersionFromImplementationClassUID(classUID10);
  assert.deepEqual(version10, version11, 'Implementation class test #1');

  // test #2: with beta
  const version20 = '4.5.6-beta.19';
  const classUID20 = getDwvUIDPrefix() + '.' + '4.5.6.99.19';
  const version21 = getDwvVersionFromImplementationClassUID(classUID20);
  assert.deepEqual(version20, version21, 'Implementation class test #2');
});

/**
 * Tests for {@link DicomParser} compare version.
 *
 * @function module:tests/dicom~compare-version
 */
QUnit.test('DICOM parsing - compare version', function (assert) {
  const versions00 = '0.0.0';
  assert.equal(
    compareVersions(versions00, versions00), 0, 'compare version #00');

  const testVersions = function (version0, versions, id) {
    for (let i = 0; i < versions.length; ++i) {
      assert.equal(compareVersions(version0, versions[i]), -1,
        'compare version #' + id + '-' + i + '0');
      assert.equal(compareVersions(versions[i], version0), 1,
        'compare version #' + id + '-' + i + '1');
    }
  };
  const versions01 = [
    '0.0.1',
    '0.1.0',
    '1.0.0',
    '0.0.1-beta.1',
    '0.1.0-beta.1',
    '1.0.0-beta.1'
  ];
  testVersions('0.0.0', versions01, '01');
  testVersions('0.0.0-beta.2', versions01, '02');

  // with sort
  const versions10 = [
    '0.1.5',
    '1.5.0',
    '0.1.5-beta.3',
    '1.5.0-beta.2',
    '0.1.5-beta.4',
    '0.0.6',
    '0.0.6-beta.3'
  ];
  const theoVersions10 = [
    '0.0.6-beta.3',
    '0.0.6',
    '0.1.5-beta.3',
    '0.1.5-beta.4',
    '0.1.5',
    '1.5.0-beta.2',
    '1.5.0'
  ];
  assert.deepEqual(
    versions10.sort(compareVersions), theoVersions10, 'compare version #10');
});

/**
 * Tests for {@link DicomParser} isVersionInBounds.
 *
 * @function module:tests/dicom~version-bounds
 */
QUnit.test('DICOM parsing - isVersionInBounds', function (assert) {
  assert.ok(
    isVersionInBounds('0.1.1', '0.1.0', '0.1.2'),
    'isVersionInBounds #00');
  // inclusive bounds
  assert.ok(
    isVersionInBounds('0.1.1', '0.1.1', '0.1.1'),
    'isVersionInBounds #01');
  // with beta
  assert.ok(
    isVersionInBounds('0.1.0', '0.0.5-beta.3', '0.2.0'),
    'isVersionInBounds #02');
});


/**
 * Tests for {@link DicomParser} using simple DICOM data.
 * Using remote file for CI integration.
 *
 * @function module:tests/dicom~simple-dicom-parsing
 */
QUnit.test('Simple DICOM parsing - #DWV-REQ-IO-01-001 Load DICOM file(s)',
  function (assert) {
    const buffer = b64urlToArrayBuffer(dwvTestSimple);

    assert.ok(hasDicomPrefix(buffer), 'Response has DICOM prefix.');

    // parse DICOM
    const dicomParser = new DicomParser();
    dicomParser.parse(buffer);

    const numRows = 32;
    const numCols = 32;

    // raw tags
    const rawTags = dicomParser.getDicomElements();
    // check values
    assert.equal(rawTags['00280010'].value[0], numRows, 'Number of rows (raw)');
    assert.equal(
      rawTags['00280011'].value[0], numCols, 'Number of columns (raw)');
    // ReferencedImageSequence - ReferencedSOPInstanceUID
    assert.equal(rawTags['00081140'].value[0]['00081155'].value[0],
      '1.3.12.2.1107.5.2.32.35162.2012021515511672669154094',
      'ReferencedImageSequence SQ (raw)');

    // wrapped tags
    const tags = dicomParser.getDicomElements();
    // wrong key
    assert.ok(typeof tags['12345678'] === 'undefined',
      'Wrong key fails if test');
    // empty key
    assert.ok(typeof tags[''] === 'undefined',
      'Empty key fails if test');
    // good key
    assert.ok(typeof tags['00280010'] !== 'undefined',
      'Good key passes if test');

    // zero value (passes test since it is a string)
    assert.equal(tags['00181318'].value[0], 0, 'Good key, zero value');
    // check values
    assert.equal(tags['00280010'].value[0], numRows, 'Number of rows');
    assert.equal(tags['00280011'].value[0], numCols, 'Number of columns');
    // ReferencedImageSequence - ReferencedSOPInstanceUID
    // only one item value -> returns the object directly
    // (no need for tags["ReferencedImageSequence")[0])
    assert.equal(tags['00081140'].value[0]['00081155'].value[0],
      '1.3.12.2.1107.5.2.32.35162.2012021515511672669154094',
      'ReferencedImageSequence SQ');
  }
);

/**
 * Tests for {@link DicomParser} using simple DICOM data.
 * Using remote file for CI integration.
 *
 * @function module:tests/dicom~simple-dicom-parsing
 */
QUnit.test('Simple DICOM parsing - until tag',
  function (assert) {
    const buffer = b64urlToArrayBuffer(dwvTestSimple);

    assert.ok(hasDicomPrefix(buffer), 'Response has DICOM prefix.');

    const getRefUID = function (tags) {
      return safeGet(safeGet(tags, '00081140'), '00081155');
    };

    const patientNameKey = '00100010';
    const patientIdKey = '00100020';
    const rowsKey = '00280010';
    const colsKey = '00280011';

    const patientName = 'dwv^PatientName';
    const patientId = 'dwv-patient-id123';
    const numRows = 32;
    const numCols = 32;
    const refUID = '1.3.12.2.1107.5.2.32.35162.2012021515511672669154094';

    // test #0: parse until patient id
    const dicomParser0 = new DicomParser();
    const untilTag0 = new Tag('0010', '0020'); // patient id
    dicomParser0.parse(buffer, untilTag0);
    const tags0 = dicomParser0.getDicomElements();
    // check values
    assert.equal(
      dicomParser0.safeGet(patientNameKey), patientName, '#0 Patient name');
    assert.ok(
      typeof tags0[untilTag0.getKey()] === 'undefined',
      '#0 Until tag is not defined');
    assert.ok(
      typeof tags0[rowsKey] === 'undefined', '#0 Number of rows');
    assert.ok(
      typeof tags0[colsKey] === 'undefined', '#0 Number of columns');

    // test #1: parse until pixel data
    const dicomParser1 = new DicomParser();
    const untilTag1 = getPixelDataTag();
    dicomParser1.parse(buffer, untilTag1);
    const tags1 = dicomParser1.getDicomElements();
    // check values
    assert.equal(
      dicomParser1.safeGet(patientNameKey), patientName, '#1 Patient name');
    assert.equal(
      dicomParser1.safeGet(patientIdKey), patientId, '#1 Patient id');
    assert.equal(
      dicomParser1.safeGet(rowsKey), numRows, '#1 Number of rows');
    assert.equal(
      dicomParser1.safeGet(colsKey), numCols, '#1 Number of columns');
    assert.equal(
      getRefUID(tags1), refUID, '#1 ReferencedImageSequence SQ');
    assert.ok(
      typeof tags1[untilTag1.getKey()] === 'undefined',
      '#1 Until tag is not defined');
  }
);

/**
 * Tests for {@link DicomParser} using sequence test DICOM data.
 * Using remote file for CI integration.
 *
 * @function module:tests/dicom~dicom-sequence-parsing
 */
QUnit.test('DICOM sequence parsing - #DWV-REQ-IO-01-001 Load DICOM file(s)',
  function (assert) {
    const buffer = b64urlToArrayBuffer(dwvTestSequence);

    assert.ok(hasDicomPrefix(buffer), 'Response has DICOM prefix.');

    // parse DICOM
    const dicomParser = new DicomParser();
    dicomParser.parse(buffer);
    // raw tags
    const tags = dicomParser.getDicomElements();
    assert.ok((Object.keys(tags).length !== 0), 'Got raw tags.');

    // ReferencedImageSequence: explicit sequence
    const seq00 = tags['00081140'].value;
    assert.equal(seq00.length, 3, 'ReferencedImageSequence length');
    assert.equal(seq00[0]['00081155'].value[0],
      '1.3.12.2.1107.5.2.32.35162.2012021515511672669154094',
      'ReferencedImageSequence - item0 - ReferencedSOPInstanceUID');
    assert.equal(seq00[1]['00081155'].value[0],
      '1.3.12.2.1107.5.2.32.35162.2012021515511286933854090',
      'ReferencedImageSequence - item1 - ReferencedSOPInstanceUID');

    // SourceImageSequence: implicit sequence
    const seq01 = tags['00082112'].value;
    assert.equal(seq01.length, 3, 'SourceImageSequence length');
    assert.equal(seq01[0]['00081155'].value[0],
      '1.3.12.2.1107.5.2.32.35162.2012021515511672669154094',
      'SourceImageSequence - item0 - ReferencedSOPInstanceUID');
    assert.equal(seq01[1]['00081155'].value[0],
      '1.3.12.2.1107.5.2.32.35162.2012021515511286933854090',
      'SourceImageSequence - item1 - ReferencedSOPInstanceUID');

    // ReferencedPatientSequence: explicit empty sequence
    const seq10 = tags['00081120'].value;
    assert.equal(seq10.length, 0, 'ReferencedPatientSequence length');

    // ReferencedOverlaySequence: implicit empty sequence
    const seq11 = tags['00081130'].value;
    assert.equal(seq11.length, 0, 'ReferencedOverlaySequence length');

    // ReferringPhysicianIdentificationSequence: explicit empty item
    const seq12 = tags['00080096'].value;
    assert.equal(seq12[0]['FFFEE000'].value.length, 0,
      'ReferringPhysicianIdentificationSequence item length');

    // ConsultingPhysicianIdentificationSequence: implicit empty item
    const seq13 = tags['0008009D'].value;
    assert.equal(seq13.length, 0,
      'ConsultingPhysicianIdentificationSequence item length');

    // ReferencedStudySequence: explicit sequence of sequence
    const seq20 = tags['00081110'].value;
    // just one element
    //assert.equal(seq20.length, 2, "ReferencedStudySequence length");
    assert.equal(seq20[0]['0040A170'].value[0]['00080100'].value[0],
      '123456',
      'ReferencedStudySequence - seq - item0 - CodeValue');

    // ReferencedSeriesSequence: implicit sequence of sequence
    const seq21 = tags['00081115'].value;
    // just one element
    //assert.equal(seq21.length, 2, "ReferencedSeriesSequence length");
    assert.equal(seq21[0]['0040A170'].value[0]['00080100'].value[0],
      '789101',
      'ReferencedSeriesSequence - seq - item0 - CodeValue');

    // ReferencedInstanceSequence: explicit empty sequence of sequence
    const seq30 = tags['0008114A'].value;
    assert.equal(seq30[0]['0040A170'].value.length, 0,
      'ReferencedInstanceSequence - seq - length');

    // ReferencedVisitSequence: implicit empty sequence of sequence
    const seq31 = tags['00081125'].value;
    assert.equal(seq31[0]['0040A170'].value.length, 0,
      'ReferencedVisitSequence - seq - length');
  }
);

/**
 * Tests for {@link cleanString}.
 *
 * @function module:tests/dicom~cleanstring
 */
QUnit.test('cleanString', function (assert) {
  // undefined
  assert.throws(function () {
    cleanString();
  },
  new TypeError('Cannot read properties of undefined (reading \'length\')'),
  'cleanstring undefined throws.');
  // null
  assert.throws(function () {
    cleanString(null);
  },
  new TypeError('Cannot read properties of null (reading \'length\')'),
  'cleanstring null throws.');
  // number
  assert.throws(function () {
    cleanString(3);
  },
  new TypeError('res.trim is not a function'),
  'cleanstring number throws.');
  // empty
  assert.equal(cleanString(''), '', 'Clean empty');
  // short
  assert.equal(cleanString('a'), 'a', 'Clean short');
  // special
  const special = String.fromCharCode('u200B');
  assert.equal(cleanString(special), '', 'Clean just special');
  // regular
  let str = ' El cielo azul ';
  let refStr = 'El cielo azul';
  assert.equal(cleanString(str), refStr, 'Clean regular');
  // regular with special
  str = ' El cielo azul' + special;
  refStr = 'El cielo azul';
  assert.equal(
    cleanString(str), refStr, 'Clean regular with special');
  // regular with special and ending space (not trimmed)
  str = ' El cielo azul ' + special;
  refStr = 'El cielo azul';
  assert.equal(
    cleanString(str), refStr, 'Clean regular with special 2');
});

/**
 * Tests for {@link DicomParser} using DICOMDIR data.
 * Using remote file for CI integration.
 *
 * @function module:tests/dicom~dicomdir-parsing
 */
QUnit.test('DICOMDIR parsing - #DWV-REQ-IO-02-004 Load DICOMDIR URL',
  function (assert) {
    // get the file list
    const list = getFileListFromDicomDir(
      b64urlToArrayBuffer(dwvDicomDir)
    );

    // check file list
    const nFilesSeries0Study0 = 23;
    const nFilesSeries1Study0 = 20;
    const nFilesSeries0Study1 = 1;
    assert.equal(list.length, 2, 'Number of study');
    assert.equal(list[0].length, 2, 'Number of series in first study');
    assert.equal(list[1].length, 1, 'Number of series in second study');
    assert.equal(
      list[0][0].length,
      nFilesSeries0Study0,
      'Study#0:Series#0 number of files');
    assert.equal(
      list[0][1].length,
      nFilesSeries1Study0,
      'Study#0:Series#1 number of files');
    assert.equal(
      list[1][0].length,
      nFilesSeries0Study1,
      'Study#1:Series#0 number of files'
    );

    // files
    const files00 = [];
    let iStart = 0;
    let iEnd = nFilesSeries0Study0;
    for (let i = iStart; i < iEnd; ++i) {
      files00.push('IMAGES/IM' + i);
    }
    assert.deepEqual(list[0][0], files00, 'Study#0:Series#0 file names');
    const files01 = [];
    iStart = iEnd;
    iEnd += nFilesSeries1Study0;
    for (let i = iStart; i < iEnd; ++i) {
      files01.push('IMAGES/IM' + i);
    }
    assert.deepEqual(list[0][0], files00, 'Study#0:Series#1 file names');
    const files10 = [];
    iStart = iEnd;
    iEnd += nFilesSeries0Study1;
    for (let i = iStart; i < iEnd; ++i) {
      files10.push('IMAGES/IM' + i);
    }
    assert.deepEqual(list[0][0], files00, 'Study#1:Series#0 file names');
  }
);