tests_dicom_dicomWriter.test.js
import {DicomParser} from '../../src/dicom/dicomParser';
import {
DicomWriter,
getElementsFromJSONTags,
getUID
} from '../../src/dicom/dicomWriter';
import {
getTagFromDictionary,
getPixelDataTag
} from '../../src/dicom/dicomTag';
import {dictionary} from '../../src/dicom/dictionary';
import {b64urlToArrayBuffer} from './utils';
import multiframeTest from '/tests/data/multiframe-test1.dcm';
import dwvTestAnonymise from '/tests/data/dwv-test-anonymise.dcm';
import syntheticDataExplicit from '/tests/dicom/synthetic-data_explicit.json';
import syntheticDataImplicit from '/tests/dicom/synthetic-data_implicit.json';
import syntheticDataExplicitBE from
'/tests/dicom/synthetic-data_explicit_big-endian.json';
/**
* Tests for the 'dicom/dicomWriter.js' file.
*/
// Do not warn if these variables were not defined before.
/* global QUnit */
/**
* Tests getUID.
*
* @function module:tests/dicom~getUID
*/
QUnit.test('Test getUID', function (assert) {
// check size
const uid00 = getUID('mytag');
assert.ok(uid00.length <= 64, 'uid length #0');
const uid01 = getUID('mysuperlongtagthatneverfinishes');
assert.ok(uid01.length <= 64, 'uid length #1');
// consecutive for same tag are different
const uid10 = getUID('mytag');
const uid11 = getUID('mytag');
assert.notEqual(uid10, uid11, 'Consecutive getUID');
// groups do not start with 0
const parts = uid10.split('.');
let count = 0;
for (let i = 0; i < parts.length; ++i) {
const part = parts[i];
if (part[0] === '0' && part.length !== 1) {
++count;
}
}
assert.ok(count === 0, 'Zero at start of part');
});
/**
* Tests for {@link DicomWriter} using simple DICOM data.
* Using remote file for CI integration.
*
* @function module:tests/dicom~dicomWriterSimpleDicom
*/
QUnit.test('Test multiframe writer support.', function (assert) {
// parse DICOM
let dicomParser = new DicomParser();
dicomParser.parse(b64urlToArrayBuffer(multiframeTest));
const numCols = 256;
const numRows = 256;
const numFrames = 16;
const bufferSize = numCols * numRows * numFrames;
// raw tags
let rawTags = dicomParser.getDicomElements();
// check values
assert.equal(rawTags['00280008'].value[0], numFrames, 'Number of frames');
assert.equal(rawTags['00280011'].value[0], numCols, 'Number of columns');
assert.equal(rawTags['00280010'].value[0], numRows, 'Number of rows');
// length of value array for pixel data
assert.equal(
rawTags['7FE00010'].value[0].length,
bufferSize,
'Length of value array for pixel data');
const dicomWriter = new DicomWriter();
const buffer = dicomWriter.getBuffer(rawTags);
dicomParser = new DicomParser();
dicomParser.parse(buffer);
rawTags = dicomParser.getDicomElements();
// check values
assert.equal(rawTags['00280008'].value[0], numFrames, 'Number of frames');
assert.equal(rawTags['00280011'].value[0], numCols, 'Number of columns');
assert.equal(rawTags['00280010'].value[0], numRows, 'Number of rows');
// length of value array for pixel data
assert.equal(
rawTags['7FE00010'].value[0].length,
bufferSize,
'Length of value array for pixel data');
});
/**
* Tests for {@link DicomWriter} anomnymisation.
* Using remote file for CI integration.
*
* @function module:tests/dicom~dicomWriterAnonymise
*/
QUnit.test('Test patient anonymisation', function (assert) {
// parse DICOM
let dicomParser = new DicomParser();
dicomParser.parse(b64urlToArrayBuffer(dwvTestAnonymise));
const patientsNameAnonymised = 'anonymise-name';
const patientsIdAnonymised = 'anonymise-id';
// rules with different levels: full tag, tag name and group name
const rules = {
default: {
action: 'copy', value: null
},
'00100010': {
action: 'replace', value: patientsNameAnonymised
},
PatientID: {
action: 'replace', value: patientsIdAnonymised
},
Patient: {
action: 'remove', value: null
}
};
const patientsName = 'dwv^PatientName';
const patientID = 'dwv-patient-id123';
const patientsBirthDate = '19830101';
const patientsSex = 'M';
// raw tags
let rawTags = dicomParser.getDicomElements();
// check values
assert.equal(
rawTags['00100010'].value[0].trim(),
patientsName,
'patientsName');
assert.equal(
rawTags['00100020'].value[0].trim(),
patientID,
'patientID');
assert.equal(
rawTags['00100030'].value[0].trim(),
patientsBirthDate,
'patientsBirthDate');
assert.equal(
rawTags['00100040'].value[0].trim(),
patientsSex,
'patientsSex');
const dicomWriter = new DicomWriter();
dicomWriter.setRules(rules);
const buffer = dicomWriter.getBuffer(rawTags);
dicomParser = new DicomParser();
dicomParser.parse(buffer);
rawTags = dicomParser.getDicomElements();
// check values
assert.equal(
rawTags['00100010'].value[0],
patientsNameAnonymised,
'patientName');
assert.equal(
rawTags['00100020'].value[0],
patientsIdAnonymised,
'patientID');
assert.notOk(rawTags['00100030'], 'patientsBirthDate');
assert.notOk(rawTags['00100040'], 'patientsSex');
});
/**
* Compare JSON tags and DICOM elements
*
* @param {object} jsonTags The JSON tags.
* @param {object} dicomElements The DICOM elements
* @param {string} name The name of the test.
* @param {object} comparator An object with an equal function (such as
* Qunit assert).
*/
function compare(jsonTags, dicomElements, name, comparator) {
// check content
if (jsonTags === null || jsonTags === 0) {
return;
}
const keys = Object.keys(jsonTags);
for (let k = 0; k < keys.length; ++k) {
const tagName = keys[k];
const tag = getTagFromDictionary(tagName);
const tagKey = tag.getKey();
const element = dicomElements[tagKey];
const value = element.value;
if (element.vr !== 'SQ') {
let jsonTag = jsonTags[tagName];
// stringify possible array
if (Array.isArray(jsonTag)) {
jsonTag = jsonTag.join();
}
comparator.equal(
value.join(),
jsonTag,
name + ' - ' + tagName);
} else {
// check content
if (jsonTags[tagName] === null || jsonTags[tagName] === 0) {
continue;
}
const sqValue = jsonTags[tagName].value;
if (typeof sqValue === 'undefined' ||
sqValue === null) {
continue;
}
// supposing same order of subkeys and indices...
for (let i = 0; i < sqValue.length; ++i) {
if (sqValue[i] !== 'undefinedLength') {
compare(
sqValue[i], value[i], name, comparator);
}
}
}
}
}
/**
* Simple GradSquarePixGenerator
*
* @param {object} tags The input tags.
* @returns {object} The pixel buffer
*/
function generateGradSquare(tags) {
const numberOfColumns = tags.Columns;
const numberOfRows = tags.Rows;
const isRGB = tags.PhotometricInterpretation.trim() === 'RGB';
const samplesPerPixel = tags.SamplesPerPixel;
let numberOfSamples = 1;
let numberOfColourPlanes = 1;
if (samplesPerPixel === 3) {
const planarConfiguration = tags.PlanarConfiguration;
if (planarConfiguration === 0) {
numberOfSamples = 3;
} else {
numberOfColourPlanes = 3;
}
}
const dataLength = numberOfRows * numberOfColumns * samplesPerPixel;
const bitsAllocated = tags.BitsAllocated;
const pixelRepresentation = tags.PixelRepresentation;
let pixelBuffer;
if (bitsAllocated === 8) {
if (pixelRepresentation === 0) {
pixelBuffer = new Uint8Array(dataLength);
} else {
pixelBuffer = new Int8Array(dataLength);
}
} else if (bitsAllocated === 16) {
if (pixelRepresentation === 0) {
pixelBuffer = new Uint16Array(dataLength);
} else {
pixelBuffer = new Int16Array(dataLength);
}
}
const halfCols = numberOfRows * 0.5;
const halfRows = numberOfColumns * 0.5;
const background = 0;
const maxNoBounds = (halfCols + halfCols / 2) * (halfRows + halfRows / 2);
const max = 100;
let getFunc;
if (isRGB) {
getFunc = function (i, j) {
let value = background;
const jc = Math.abs(j - halfRows);
const ic = Math.abs(i - halfCols);
if (jc < halfRows / 2 && ic < halfCols / 2) {
value += (i * j) * (max / maxNoBounds);
}
if (value > 255) {
value = 200;
}
return [0, value, value];
};
} else {
getFunc = function (i, j) {
let value = 0;
const jc = Math.abs(j - halfRows);
const ic = Math.abs(i - halfCols);
if (jc < halfRows / 2 && ic < halfCols / 2) {
value += (i * j) * (max / maxNoBounds);
}
return [value];
};
}
// main loop
let offset = 0;
for (let c = 0; c < numberOfColourPlanes; ++c) {
for (let j = 0; j < numberOfRows; ++j) {
for (let i = 0; i < numberOfColumns; ++i) {
for (let s = 0; s < numberOfSamples; ++s) {
if (numberOfColourPlanes !== 1) {
pixelBuffer[offset] = getFunc(i, j)[c];
} else {
pixelBuffer[offset] = getFunc(i, j)[s];
}
++offset;
}
}
}
}
const pixVL = pixelBuffer.BYTES_PER_ELEMENT * dataLength;
return {
tag: getPixelDataTag(),
vr: bitsAllocated === 8 ? 'OB' : 'OW',
vl: pixVL,
value: pixelBuffer
};
}
/**
* Test a JSON config: write a DICOM file and read it back.
*
* @param {object} config A JSON config representing DICOM tags.
* @param {object} assert A Qunit assert.
*/
function testWriteReadDataFromConfig(config, assert) {
// add private tags to dict if present
let useUnVrForPrivateSq = false;
if (typeof config.privateDictionary !== 'undefined') {
const keys = Object.keys(config.privateDictionary);
for (let i = 0; i < keys.length; ++i) {
const group = keys[i];
const tags = config.privateDictionary[group];
dictionary[group] = tags;
}
if (typeof config.useUnVrForPrivateSq !== 'undefined') {
useUnVrForPrivateSq = config.useUnVrForPrivateSq;
}
}
// pass tags clone to avoid modifications (for ex by padElement)
// TODO: find another way
const jsonTags = JSON.parse(JSON.stringify(config.tags));
// convert JSON to DICOM element object
const dicomElements = getElementsFromJSONTags(jsonTags);
// pixels (if possible): small gradient square
if (config.tags.Modality !== 'KO') {
dicomElements['7FE00010'] = generateGradSquare(config.tags);
}
// create DICOM buffer
const writer = new DicomWriter();
writer.setUseUnVrForPrivateSq(useUnVrForPrivateSq);
let dicomBuffer = null;
try {
dicomBuffer = writer.getBuffer(dicomElements);
} catch (error) {
assert.ok(false, 'Caught error: ' + error);
return;
}
// parse the buffer
const dicomParser = new DicomParser();
dicomParser.parse(dicomBuffer);
const elements = dicomParser.getDicomElements();
// compare contents
compare(config.tags, elements, config.name, assert);
}
/**
* Tests write/read DICOM data from config file: explicit encoding.
* Using remote file for CI integration.
*
* @function module:tests/dicom~dicomExplicitWriteReadFromConfig
*/
QUnit.test('Test synthetic dicom explicit', function (assert) {
const configs = JSON.parse(syntheticDataExplicit);
for (let i = 0; i < configs.length; ++i) {
testWriteReadDataFromConfig(configs[i], assert);
}
});
/**
* Tests write/read DICOM data from config file: implicit encoding.
* Using remote file for CI integration.
*
* @function module:tests/dicom~dicomImplicitWriteReadFromConfig
*/
QUnit.test('Test synthetic dicom implicit', function (assert) {
const configs = JSON.parse(syntheticDataImplicit);
for (let i = 0; i < configs.length; ++i) {
testWriteReadDataFromConfig(configs[i], assert);
}
});
/**
* Tests write/read DICOM data from config file: explicit big endian encoding.
* Using remote file for CI integration.
*
* @function module:tests/dicom~dicomExplicitBigEndianWriteReadFromConfig
*/
QUnit.test('Test synthetic dicom explicit big endian', function (assert) {
const configs = JSON.parse(syntheticDataExplicitBE);
for (let i = 0; i < configs.length; ++i) {
testWriteReadDataFromConfig(configs[i], assert);
}
});