// namespaces
var dwv = dwv || {};
dwv.dicom = dwv.dicom || {};
/**
* Get the dwv UID prefix.
* Issued by Medical Connections Ltd (www.medicalconnections.co.uk)
* on 25/10/2017.
*
* @returns {string} The dwv UID prefix.
*/
dwv.dicom.getDwvUIDPrefix = function () {
return '1.2.826.0.1.3680043.9.7278.1';
};
// local generated uid counter
var _uidCount = 0;
/**
* Get a UID for a DICOM tag.
* Note: Use https://github.com/uuidjs/uuid?
*
* @see http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_9.html
* @see http://dicomiseasy.blogspot.com/2011/12/chapter-4-dicom-objects-in-chapter-3.html
* @see https://stackoverflow.com/questions/46304306/how-to-generate-unique-dicom-uid
* @param {string} tagName The input tag.
* @returns {string} The corresponding UID.
*/
dwv.dicom.getUID = function (tagName) {
var prefix = dwv.dicom.getDwvUIDPrefix() + '.';
var uid = '';
if (tagName === 'ImplementationClassUID') {
uid = prefix + dwv.getVersion();
} else {
// date (only numbers), do not keep milliseconds
var date = (new Date()).toISOString().replace(/\D/g, '');
var datePart = '.' + date.substring(0, 14);
// count
_uidCount += 1;
var countPart = '.' + _uidCount;
// uid = prefix . tag . date . count
uid = prefix;
// limit tag part to not exceed 64 length
var nonTagLength = prefix.length + countPart.length + datePart.length;
var leni = Math.min(tagName.length, 64 - nonTagLength);
if (leni > 1) {
var tagNumber = '';
for (var i = 0; i < leni; ++i) {
tagNumber += tagName.charCodeAt(i);
}
uid += tagNumber.substring(0, leni);
}
// finish
uid += datePart + countPart;
}
return uid;
};
/**
* Return true if the input number is even.
*
* @param {number} number The number to check.
* @returns {boolean} True is the number is even.
*/
dwv.dicom.isEven = function (number) {
return number % 2 === 0;
};
/**
* Is the input VR a VR that stores data in a typed array.
* TODO: include ox and xs?
*
* @param {string} vr The element VR.
* @returns {boolean} True if the VR is a typed array one.
*/
dwv.dicom.isTypedArrayVr = function (vr) {
var vrType = dwv.dicom.vrTypes[vr];
return typeof vrType !== 'undefined' &&
vrType !== 'string';
};
/**
* Is the input VR a string VR.
*
* @param {string} vr The element VR.
* @returns {boolean} True if the VR is a string one.
*/
dwv.dicom.isStringVr = function (vr) {
var vrType = dwv.dicom.vrTypes[vr];
return typeof vrType !== 'undefined' &&
vrType === 'string';
};
/**
* Is the input VR a VR that could need padding.
* see http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html
*
* @param {string} vr The element VR.
* @returns {boolean} True if the VR needs padding.
*/
dwv.dicom.isVrToPad = function (vr) {
return dwv.dicom.isStringVr(vr) || vr === 'OB';
};
/**
* Get the VR specific padding value.
*
* @param {string} vr The element VR.
* @returns {boolean} The value used to pad.
*/
dwv.dicom.getVrPad = function (vr) {
var pad = 0;
if (dwv.dicom.isStringVr(vr)) {
if (vr === 'UI') {
pad = '\0';
} else {
pad = ' ';
}
}
return pad;
};
/**
* Push a value at the end of an input Uint8Array.
*
* @param {Uint8Array} arr The input array.
* @param {number} value The value to push.
* @returns {Uint8Array} The new array.
*/
dwv.dicom.uint8ArrayPush = function (arr, value) {
var newArr = new Uint8Array(arr.length + 1);
newArr.set(arr);
newArr.set(value, arr.length);
return newArr;
};
/**
* Pad an input OB value.
*
* @param {Array|Uint8Array} value The input value.
* @returns {Array|Uint8Array} The padded input.
*/
dwv.dicom.padOBValue = function (value) {
if (value !== null &&
typeof value !== 'undefined' &&
typeof value.length !== 'undefined') {
// calculate size and pad if needed
if (value.length !== 0 &&
typeof value[0].length !== 'undefined') {
// handle array of array
var size = 0;
for (var i = 0; i < value.length; ++i) {
size += value[i].length;
}
if (!dwv.dicom.isEven(size)) {
value[value.length - 1] = dwv.dicom.uint8ArrayPush(
value[value.length - 1], 0);
}
} else {
if (!dwv.dicom.isEven(value.length)) {
value = dwv.dicom.uint8ArrayPush(value, 0);
}
}
} else {
throw new Error('Cannot pad undefined or null OB value.');
}
// uint8ArrayPush may create a new array so we
// need to return it
return value;
};
/**
* Helper method to flatten an array of typed arrays to 2D typed array
*
* @param {Array} initialArray array of typed arrays
* @returns {object} a typed array containing all values
*/
dwv.dicom.flattenArrayOfTypedArrays = function (initialArray) {
var initialArrayLength = initialArray.length;
var arrayLength = initialArray[0].length;
// If this is not a array of arrays, just return the initial one:
if (typeof arrayLength === 'undefined') {
return initialArray;
}
var flattenendArrayLength = initialArrayLength * arrayLength;
var flattenedArray = new initialArray[0].constructor(flattenendArrayLength);
for (var i = 0; i < initialArrayLength; i++) {
var indexFlattenedArray = i * arrayLength;
flattenedArray.set(initialArray[i], indexFlattenedArray);
}
return flattenedArray;
};
/**
* Default text encoder.
*/
dwv.dicom.DefaultTextEncoder = function () {
/**
* Encode an input string.
*
* @param {string} str The string to encode.
* @returns {Uint8Array} The encoded string.
*/
this.encode = function (str) {
var result = new Uint8Array(str.length);
for (var i = 0, leni = str.length; i < leni; ++i) {
result[i] = str.charCodeAt(i);
}
return result;
};
};
/**
* DICOM writer.
*
* Example usage:
* var parser = new dwv.dicom.DicomParser();
* parser.parse(this.response);
*
* var writer = new dwv.dicom.DicomWriter(parser.getRawDicomElements());
* var blob = new Blob([writer.getBuffer()], {type: 'application/dicom'});
*
* var element = document.getElementById("download");
* element.href = URL.createObjectURL(blob);
* element.download = "anonym.dcm";
*
* @class
*/
dwv.dicom.DicomWriter = function () {
// flag to use VR=UN for private sequences, default to false
// (mainly used in tests)
this.useUnVrForPrivateSq = false;
// possible tag actions
var actions = {
copy: function (item) {
return item;
},
remove: function () {
return null;
},
clear: function (item) {
item.value = [];
return item;
},
replace: function (item, value) {
item.value = [value];
return item;
}
};
// default rules: just copy
var defaultRules = {
default: {action: 'copy', value: null}
};
/**
* Public (modifiable) rules.
* Set of objects as:
* name : { action: 'actionName', value: 'optionalValue }
* The names are either 'default', tagName or groupName.
* Each DICOM element will be checked to see if a rule is applicable.
* First checked by tagName and then by groupName,
* if nothing is found the default rule is applied.
*/
this.rules = defaultRules;
/**
* Default text encoder.
*
* @private
* @type {dwv.dicom.DefaultTextEncoder}
*/
var defaultTextEncoder = new dwv.dicom.DefaultTextEncoder();
/**
* Special text encoder.
*
* @private
* @type {dwv.dicom.DefaultTextEncoder|TextEncoder}
*/
var textEncoder = defaultTextEncoder;
/**
* Encode string data.
*
* @param {number} str The string to encode.
* @returns {Uint8Array} The encoded string.
*/
this.encodeString = function (str) {
return defaultTextEncoder.encode(str);
};
/**
* Encode data as a UTF-8.
*
* @param {number} str The string to write.
* @returns {Uint8Array} The encoded string.
*/
this.encodeSpecialString = function (str) {
return textEncoder.encode(str);
};
/**
* Use a TextEncoder instead of the default text decoder.
*/
this.useSpecialTextEncoder = function () {
/**
* The text encoder.
*
* @external TextEncoder
* @see https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
*/
textEncoder = new TextEncoder();
};
/**
* Example anonymisation rules.
*/
this.anonymisationRules = {
default: {action: 'remove', value: null},
PatientName: {action: 'replace', value: 'Anonymized'}, // tag
'Meta Element': {action: 'copy', value: null}, // group 'x0002'
Acquisition: {action: 'copy', value: null}, // group 'x0018'
'Image Presentation': {action: 'copy', value: null}, // group 'x0028'
Procedure: {action: 'copy', value: null}, // group 'x0040'
'Pixel Data': {action: 'copy', value: null} // group 'x7fe0'
};
/**
* Get the element to write according to the class rules.
* Priority order: tagName, groupName, default.
*
* @param {object} element The element to check
* @returns {object} The element to write, can be null.
*/
this.getElementToWrite = function (element) {
// get group and tag string name
var groupName = element.tag.getGroupName();
var tagName = element.tag.getNameFromDictionary();
// apply rules:
var rule;
if (typeof this.rules[element.tag.getKey()] !== 'undefined') {
// 1. tag itself
rule = this.rules[element.tag.getKey()];
} else if (tagName !== null && typeof this.rules[tagName] !== 'undefined') {
// 2. tag name
rule = this.rules[tagName];
} else if (typeof this.rules[groupName] !== 'undefined') {
// 3. group name
rule = this.rules[groupName];
} else {
// 4. default
rule = this.rules['default'];
}
// apply action on element and return
return actions[rule.action](element, rule.value);
};
};
/**
* Write a list of items.
*
* @param {dwv.dicom.DataWriter} writer The raw data writer.
* @param {number} byteOffset The offset to start writing from.
* @param {Array} items The list of items to write.
* @param {boolean} isImplicit Is the DICOM VR implicit?
* @returns {number} The new offset position.
*/
dwv.dicom.DicomWriter.prototype.writeDataElementItems = function (
writer, byteOffset, items, isImplicit) {
var item = null;
for (var i = 0; i < items.length; ++i) {
item = items[i];
var itemKeys = Object.keys(item);
if (itemKeys.length === 0) {
continue;
}
// item element (create new to not modify original)
var undefinedLength = false;
if (typeof item.xFFFEE000.undefinedLength !== 'undefined') {
undefinedLength = item.xFFFEE000.undefinedLength;
}
var itemElement = {
tag: dwv.dicom.getItemTag(),
vr: 'NONE',
vl: undefinedLength ? 0xffffffff : item.xFFFEE000.vl,
value: []
};
byteOffset = this.writeDataElement(
writer, itemElement, byteOffset, isImplicit);
// write rest
for (var m = 0; m < itemKeys.length; ++m) {
if (itemKeys[m] !== 'xFFFEE000' && itemKeys[m] !== 'xFFFEE00D') {
byteOffset = this.writeDataElement(
writer, item[itemKeys[m]], byteOffset, isImplicit);
}
}
// item delimitation
if (undefinedLength) {
var itemDelimElement = {
tag: dwv.dicom.getItemDelimitationItemTag(),
vr: 'NONE',
vl: 0,
value: []
};
byteOffset = this.writeDataElement(
writer, itemDelimElement, byteOffset, isImplicit);
}
}
// return new offset
return byteOffset;
};
/**
* Write data with a specific Value Representation (VR).
*
* @param {dwv.dicom.DataWriter} writer The raw data writer.
* @param {object} element The element to write.
* @param {number} byteOffset The offset to start writing from.
* @param {Array} value The array to write.
* @param {boolean} isImplicit Is the DICOM VR implicit?
* @returns {number} The new offset position.
*/
dwv.dicom.DicomWriter.prototype.writeDataElementValue = function (
writer, element, byteOffset, value, isImplicit) {
var startOffset = byteOffset;
if (element.vr === 'NONE') {
// nothing to do!
} else if (value instanceof Uint8Array) {
// binary data has been expanded 8 times at read
if (value.length === 8 * element.vl) {
byteOffset = writer.writeBinaryArray(byteOffset, value);
} else {
byteOffset = writer.writeUint8Array(byteOffset, value);
}
} else if (value instanceof Int8Array) {
byteOffset = writer.writeInt8Array(byteOffset, value);
} else if (value instanceof Uint16Array) {
byteOffset = writer.writeUint16Array(byteOffset, value);
} else if (value instanceof Int16Array) {
byteOffset = writer.writeInt16Array(byteOffset, value);
} else if (value instanceof Uint32Array) {
byteOffset = writer.writeUint32Array(byteOffset, value);
} else if (value instanceof Int32Array) {
byteOffset = writer.writeInt32Array(byteOffset, value);
} else if (value instanceof BigUint64Array) {
byteOffset = writer.writeUint64Array(byteOffset, value);
} else if (value instanceof BigInt64Array) {
byteOffset = writer.writeInt64Array(byteOffset, value);
} else {
// switch according to VR if input type is undefined
var vrType = dwv.dicom.vrTypes[element.vr];
if (typeof vrType !== 'undefined') {
if (vrType === 'Uint8') {
byteOffset = writer.writeUint8Array(byteOffset, value);
} else if (vrType === 'Uint16') {
byteOffset = writer.writeUint16Array(byteOffset, value);
} else if (vrType === 'Int16') {
byteOffset = writer.writeInt16Array(byteOffset, value);
} else if (vrType === 'Uint32') {
byteOffset = writer.writeUint32Array(byteOffset, value);
} else if (vrType === 'Int32') {
byteOffset = writer.writeInt32Array(byteOffset, value);
} else if (vrType === 'Uint64') {
byteOffset = writer.writeUint64Array(byteOffset, value);
} else if (vrType === 'Int64') {
byteOffset = writer.writeInt64Array(byteOffset, value);
} else if (vrType === 'Float32') {
byteOffset = writer.writeFloat32Array(byteOffset, value);
} else if (vrType === 'Float64') {
byteOffset = writer.writeFloat64Array(byteOffset, value);
} else if (vrType === 'string') {
byteOffset = writer.writeUint8Array(byteOffset, value);
} else {
throw Error('Unknown VR type: ' + vrType);
}
} else if (element.vr === 'SQ') {
byteOffset = this.writeDataElementItems(
writer, byteOffset, value, isImplicit);
} else if (element.vr === 'AT') {
for (var i = 0; i < value.length; ++i) {
var hexString = value[i] + '';
var hexString1 = hexString.substring(1, 5);
var hexString2 = hexString.substring(6, 10);
var dec1 = parseInt(hexString1, 16);
var dec2 = parseInt(hexString2, 16);
var atValue = new Uint16Array([dec1, dec2]);
byteOffset = writer.writeUint16Array(byteOffset, atValue);
}
} else {
dwv.logger.warn('Unknown VR: ' + element.vr);
}
}
if (element.vr !== 'SQ' && element.vr !== 'NONE') {
var diff = byteOffset - startOffset;
if (diff !== element.vl) {
dwv.logger.warn('Offset difference and VL are not equal: ' +
diff + ' != ' + element.vl + ', vr:' + element.vr);
}
}
// return new offset
return byteOffset;
};
/**
* Write a pixel data element.
*
* @param {dwv.dicom.DataWriter} writer The raw data writer.
* @param {object} element The element to write.
* @param {number} byteOffset The offset to start writing from.
* @param {Array} value The array to write.
* @param {boolean} isImplicit Is the DICOM VR implicit?
* @returns {number} The new offset position.
*/
dwv.dicom.DicomWriter.prototype.writePixelDataElementValue = function (
writer, element, byteOffset, value, isImplicit) {
// undefined length flag
var undefinedLength = false;
if (typeof element.undefinedLength !== 'undefined') {
undefinedLength = element.undefinedLength;
}
// explicit length
if (!undefinedLength) {
var finalValue = value[0];
// flatten multi frame
if (value.length > 1) {
finalValue = dwv.dicom.flattenArrayOfTypedArrays(value);
}
// write
byteOffset = this.writeDataElementValue(
writer, element, byteOffset, finalValue, isImplicit);
} else {
// pixel data as sequence
var item = {};
// first item: basic offset table
item.xFFFEE000 = {
tag: dwv.dicom.getItemTag(),
vr: 'NONE',
vl: 0,
value: []
};
// data
for (var i = 0; i < value.length; ++i) {
item[i] = {
tag: dwv.dicom.getItemTag(),
vr: element.vr,
vl: value[i].length,
value: value[i]
};
}
// write
byteOffset = this.writeDataElementItems(
writer, byteOffset, [item], isImplicit);
}
// return new offset
return byteOffset;
};
/**
* Write a data element.
*
* @param {dwv.dicom.DataWriter} writer The raw data writer.
* @param {object} element The DICOM data element to write.
* @param {number} byteOffset The offset to start writing from.
* @param {boolean} isImplicit Is the DICOM VR implicit?
* @returns {number} The new offset position.
*/
dwv.dicom.DicomWriter.prototype.writeDataElement = function (
writer, element, byteOffset, isImplicit) {
var isTagWithVR = element.tag.isWithVR();
var is32bitVLVR = (isImplicit || !isTagWithVR)
? true : dwv.dicom.is32bitVLVR(element.vr);
// group
byteOffset = writer.writeHex(byteOffset, element.tag.getGroup());
// element
byteOffset = writer.writeHex(byteOffset, element.tag.getElement());
// VR
var vr = element.vr;
// use VR=UN for private sequence
if (this.useUnVrForPrivateSq &&
element.tag.isPrivate() &&
vr === 'SQ') {
dwv.logger.warn('Write element using VR=UN for private sequence.');
vr = 'UN';
}
if (isTagWithVR && !isImplicit) {
byteOffset = writer.writeUint8Array(byteOffset, this.encodeString(vr));
// reserved 2 bytes for 32bit VL
if (is32bitVLVR) {
byteOffset += 2;
}
}
var undefinedLengthSequence = false;
if (element.vr === 'SQ' ||
dwv.dicom.isPixelDataTag(element.tag)) {
if (typeof element.undefinedLength !== 'undefined') {
undefinedLengthSequence = element.undefinedLength;
}
}
var undefinedLengthItem = false;
if (dwv.dicom.isItemTag(element.tag)) {
if (typeof element.undefinedLength !== 'undefined') {
undefinedLengthItem = element.undefinedLength;
}
}
// update vl for sequence or item with undefined length
var vl = element.vl;
if (undefinedLengthSequence || undefinedLengthItem) {
vl = 0xffffffff;
}
// VL
if (is32bitVLVR) {
byteOffset = writer.writeUint32(byteOffset, vl);
} else {
byteOffset = writer.writeUint16(byteOffset, vl);
}
// value
var value = element.value;
// check value
if (typeof value === 'undefined') {
value = [];
}
// write
if (dwv.dicom.isPixelDataTag(element.tag)) {
byteOffset = this.writePixelDataElementValue(
writer, element, byteOffset, value, isImplicit);
} else {
byteOffset = this.writeDataElementValue(
writer, element, byteOffset, value, isImplicit);
}
// sequence delimitation item for sequence with undefined length
if (undefinedLengthSequence) {
var seqDelimElement = {
tag: dwv.dicom.getSequenceDelimitationItemTag(),
vr: 'NONE',
vl: 0,
value: []
};
byteOffset = this.writeDataElement(
writer, seqDelimElement, byteOffset, isImplicit);
}
// return new offset
return byteOffset;
};
/**
* Get the ArrayBuffer corresponding to input DICOM elements.
*
* @param {Array} dicomElements The wrapped elements to write.
* @returns {ArrayBuffer} The elements as a buffer.
*/
dwv.dicom.DicomWriter.prototype.getBuffer = function (dicomElements) {
// Transfer Syntax
var syntax = dwv.dicom.cleanString(dicomElements.x00020010.value[0]);
var isImplicit = dwv.dicom.isImplicitTransferSyntax(syntax);
var isBigEndian = dwv.dicom.isBigEndianTransferSyntax(syntax);
// Specific CharacterSet
if (typeof dicomElements.x00080005 !== 'undefined') {
var oldscs = dwv.dicom.cleanString(dicomElements.x00080005.value[0]);
// force UTF-8 if not default character set
if (typeof oldscs !== 'undefined' && oldscs !== 'ISO-IR 6') {
dwv.logger.debug('Change charset to UTF, was: ' + oldscs);
this.useSpecialTextEncoder();
dicomElements.x00080005.value = ['ISO_IR 192'];
}
}
// Bits Allocated (for image data)
var bitsAllocated;
if (typeof dicomElements.x00280100 !== 'undefined') {
bitsAllocated = dicomElements.x00280100.value[0];
}
// calculate buffer size and split elements (meta and non meta)
var totalSize = 128 + 4; // DICM
var localSize = 0;
var metaElements = [];
var rawElements = [];
var element;
var groupName;
var metaLength = 0;
// FileMetaInformationGroupLength
var fmiglTag = dwv.dicom.getFileMetaInformationGroupLengthTag();
// FileMetaInformationVersion
var fmivTag = new dwv.dicom.Tag('0x0002', '0x0001');
// ImplementationClassUID
var icUIDTag = new dwv.dicom.Tag('0x0002', '0x0012');
// ImplementationVersionName
var ivnTag = new dwv.dicom.Tag('0x0002', '0x0013');
// loop through elements to get the buffer size
var keys = Object.keys(dicomElements);
for (var i = 0, leni = keys.length; i < leni; ++i) {
element = this.getElementToWrite(dicomElements[keys[i]]);
if (element !== null &&
!fmiglTag.equals(element.tag) &&
!fmivTag.equals(element.tag) &&
!icUIDTag.equals(element.tag) &&
!ivnTag.equals(element.tag)) {
localSize = 0;
// XB7 2020-04-17
// Check if UN can be converted to correct VR.
// This check must be done BEFORE calculating totalSize,
// otherwise there may be extra null bytes at the end of the file
// (dcmdump may crash because of these bytes)
dwv.dicom.checkUnknownVR(element);
// update value and vl
this.setElementValue(
element, element.value, isImplicit, bitsAllocated);
// tag group name
groupName = element.tag.getGroupName();
// prefix
if (groupName === 'Meta Element') {
localSize += dwv.dicom.getDataElementPrefixByteSize(element.vr, false);
} else {
localSize += dwv.dicom.getDataElementPrefixByteSize(
element.vr, isImplicit);
}
// value
localSize += element.vl;
// sort elements
if (groupName === 'Meta Element') {
metaElements.push(element);
metaLength += localSize;
} else {
rawElements.push(element);
}
// add to total size
totalSize += localSize;
}
}
// FileMetaInformationVersion
var fmiv = dwv.dicom.getDicomElement('FileMetaInformationVersion');
var fmivSize = dwv.dicom.getDataElementPrefixByteSize(fmiv.vr, false);
fmivSize += this.setElementValue(fmiv, [0, 1], false);
metaElements.push(fmiv);
metaLength += fmivSize;
totalSize += fmivSize;
// ImplementationClassUID
var icUID = dwv.dicom.getDicomElement('ImplementationClassUID');
var icUIDSize = dwv.dicom.getDataElementPrefixByteSize(icUID.vr, false);
icUIDSize += this.setElementValue(
icUID, [dwv.dicom.getUID('ImplementationClassUID')], false);
metaElements.push(icUID);
metaLength += icUIDSize;
totalSize += icUIDSize;
// ImplementationVersionName
var ivn = dwv.dicom.getDicomElement('ImplementationVersionName');
var ivnSize = dwv.dicom.getDataElementPrefixByteSize(ivn.vr, false);
var ivnValue = 'DWV_' + dwv.getVersion();
ivnSize += this.setElementValue(ivn, [ivnValue], false);
metaElements.push(ivn);
metaLength += ivnSize;
totalSize += ivnSize;
// sort elements
var elemSortFunc = function (a, b) {
return dwv.dicom.tagCompareFunction(a.tag, b.tag);
};
metaElements.sort(elemSortFunc);
rawElements.sort(elemSortFunc);
// create the FileMetaInformationGroupLength element
var fmigl = dwv.dicom.getDicomElement('FileMetaInformationGroupLength');
var fmiglSize = dwv.dicom.getDataElementPrefixByteSize(fmigl.vr, false);
fmiglSize += this.setElementValue(
fmigl, new Uint32Array([metaLength]), false);
totalSize += fmiglSize;
// create buffer
var buffer = new ArrayBuffer(totalSize);
var metaWriter = new dwv.dicom.DataWriter(buffer);
var dataWriter = new dwv.dicom.DataWriter(buffer, !isBigEndian);
var offset = 128;
// DICM
offset = metaWriter.writeUint8Array(offset, this.encodeString('DICM'));
// FileMetaInformationGroupLength
offset = this.writeDataElement(metaWriter, fmigl, offset, false);
// write meta
for (var j = 0, lenj = metaElements.length; j < lenj; ++j) {
offset = this.writeDataElement(metaWriter, metaElements[j], offset, false);
}
// check meta position
var preambleSize = 128 + 4;
var metaOffset = preambleSize + fmiglSize + metaLength;
if (offset !== metaOffset) {
dwv.logger.warn('Bad size calculation... meta offset: ' + offset +
', calculated size:' + metaOffset +
' (diff:' + (offset - metaOffset) + ')');
}
// pass flag to writer
dataWriter.useUnVrForPrivateSq = this.useUnVrForPrivateSq;
// write non meta
for (var k = 0, lenk = rawElements.length; k < lenk; ++k) {
offset = this.writeDataElement(
dataWriter, rawElements[k], offset, isImplicit);
}
// check final position
if (offset !== totalSize) {
dwv.logger.warn('Bad size calculation... final offset: ' + offset +
', calculated size:' + totalSize +
' (diff:' + (offset - totalSize) + ')');
}
// return
return buffer;
};
/**
* Fix for broken DICOM elements: Replace "UN" with correct VR if the
* element exists in dictionary
*
* @param {object} element The DICOM element.
*/
dwv.dicom.checkUnknownVR = function (element) {
if (element.vr === 'UN') {
var dictVr = element.tag.getVrFromDictionary();
if (dictVr !== null && element.vr !== dictVr) {
element.vr = dictVr;
dwv.logger.info('Element ' + element.tag.getGroup() +
' ' + element.tag.getElement() +
' VR changed from UN to ' + element.vr);
}
}
};
/**
* Get a DICOM element from its tag name (value set separatly).
*
* @param {string} tagName The string tag name.
* @returns {object} The DICOM element.
*/
dwv.dicom.getDicomElement = function (tagName) {
var tag = dwv.dicom.getTagFromDictionary(tagName);
return {
tag: tag,
vr: tag.getVrFromDictionary()
};
};
/**
* Get the number of bytes per element for a given VR type.
*
* @param {string} vrType The VR type as defined in the dictionary.
* @returns {number} The bytes per element.
*/
dwv.dicom.getBpeForVrType = function (vrType) {
var bpe;
if (vrType === 'Uint8') {
bpe = Uint8Array.BYTES_PER_ELEMENT;
} else if (vrType === 'Uint16') {
bpe = Uint16Array.BYTES_PER_ELEMENT;
} else if (vrType === 'Int16') {
bpe = Int16Array.BYTES_PER_ELEMENT;
} else if (vrType === 'Uint32') {
bpe = Uint32Array.BYTES_PER_ELEMENT;
} else if (vrType === 'Int32') {
bpe = Int32Array.BYTES_PER_ELEMENT;
} else if (vrType === 'Float32') {
bpe = Float32Array.BYTES_PER_ELEMENT;
} else if (vrType === 'Float64') {
bpe = Float64Array.BYTES_PER_ELEMENT;
} else if (vrType === 'Uint64') {
bpe = BigUint64Array.BYTES_PER_ELEMENT;
} else if (vrType === 'Int64') {
bpe = BigInt64Array.BYTES_PER_ELEMENT;
}
return bpe;
};
/**
* Set a DICOM element value according to its VR (Value Representation).
*
* @param {object} element The DICOM element to set the value.
* @param {object} value The value to set.
* @param {boolean} isImplicit Does the data use implicit VR?
* @param {number} bitsAllocated Bits allocated used for pixel data.
* @returns {number} The total element size.
*/
dwv.dicom.DicomWriter.prototype.setElementValue = function (
element, value, isImplicit, bitsAllocated) {
// byte size of the element
var size = 0;
// special sequence case
if (element.vr === 'SQ') {
if (value !== null && value !== 0) {
var newItems = [];
var name;
// explicit or undefined length sequence
var undefinedLength = false;
if (typeof element.undefinedLength !== 'undefined') {
undefinedLength = element.undefinedLength;
delete element.undefinedLength;
}
// items
for (var i = 0; i < value.length; ++i) {
var oldItemElements = value[i];
var newItemElements = {};
var subSize = 0;
// check data
if (oldItemElements === null || oldItemElements === 0) {
continue;
}
// elements
var itemKeys = Object.keys(oldItemElements);
for (var j = 0, lenj = itemKeys.length; j < lenj; ++j) {
var itemKey = itemKeys[j];
var subElement = oldItemElements[itemKey];
if (dwv.dicom.isItemTag(subElement.tag)) {
continue;
}
// set item value
subSize += this.setElementValue(
subElement, subElement.value, isImplicit, bitsAllocated);
newItemElements[itemKey] = subElement;
// add prefix size
subSize += dwv.dicom.getDataElementPrefixByteSize(
subElement.vr, isImplicit);
}
// add item element (used to store its size)
var itemElement = {
tag: dwv.dicom.getItemTag(),
vr: 'NONE',
vl: subSize,
value: []
};
if (undefinedLength) {
itemElement.undefinedLength = undefinedLength;
}
name = itemElement.tag.getKey();
newItemElements[name] = itemElement;
subSize += dwv.dicom.getDataElementPrefixByteSize(
itemElement.vr, isImplicit);
// add item delimitation size
if (undefinedLength) {
subSize += dwv.dicom.getDataElementPrefixByteSize(
'NONE', isImplicit);
}
size += subSize;
newItems.push(newItemElements);
}
// add sequence delimitation size
if (undefinedLength) {
size += dwv.dicom.getDataElementPrefixByteSize('NONE', isImplicit);
}
// update sequence element
element.value = newItems;
element.vl = size;
if (undefinedLength) {
element.undefinedLength = undefinedLength;
}
}
} else {
// pad if necessary
if (dwv.dicom.isVrToPad(element.vr)) {
var pad = dwv.dicom.getVrPad(element.vr);
// encode string
// TODO: not sure for UN...
if (dwv.dicom.isStringVr(element.vr)) {
if (dwv.dicom.charSetString.includes(element.vr)) {
value = this.encodeSpecialString(value.join('\\'));
pad = this.encodeSpecialString(pad);
} else {
value = this.encodeString(value.join('\\'));
pad = this.encodeString(pad);
}
if (!dwv.dicom.isEven(value.length)) {
value = dwv.dicom.uint8ArrayPush(value, pad);
}
} else if (element.vr === 'OB') {
value = dwv.dicom.padOBValue(value);
}
}
// calculate byte size
size = 0;
if (element.vr === 'AT') {
size = 4 * value.length;
} else if (element.vr === 'xs') {
size = value.length * Uint16Array.BYTES_PER_ELEMENT;
} else if (dwv.dicom.isTypedArrayVr(element.vr) || element.vr === 'ox') {
if (dwv.dicom.isPixelDataTag(element.tag) &&
Array.isArray(value)) {
size = 0;
for (var b = 0; b < value.length; ++b) {
size += value[b].length;
}
} else {
size = value.length;
}
// convert size to bytes
var vrType = dwv.dicom.vrTypes[element.vr];
if (dwv.dicom.isPixelDataTag(element.tag) || element.vr === 'ox') {
if (element.undefinedLength) {
var itemPrefixSize =
dwv.dicom.getDataElementPrefixByteSize('NONE', isImplicit);
// offset table
size += itemPrefixSize;
// pixel items
size += itemPrefixSize * value.length;
// add sequence delimitation size
size += itemPrefixSize;
} else {
// use bitsAllocated for pixel data
// no need to multiply for 8 bits
if (typeof bitsAllocated !== 'undefined') {
if (bitsAllocated === 1) {
// binary data
size /= 8;
} else if (bitsAllocated === 16) {
size *= Uint16Array.BYTES_PER_ELEMENT;
}
}
}
} else if (typeof vrType !== 'undefined') {
var bpe = dwv.dicom.getBpeForVrType(vrType);
if (typeof bpe !== 'undefined') {
size *= bpe;
} else {
throw Error('Unknown bytes per element for VR type: ' + vrType);
}
} else {
throw Error('Unsupported element: ' + element.vr);
}
} else {
size = value.length;
}
element.value = value;
element.vl = size;
}
// return the size of that data
return size;
};
/**
* Get the DICOM elements from a DICOM json tags object.
* The json is a simplified version of the oficial DICOM json with
* tag names instead of keys and direct values (no value property) for
* simple tags.
*
* @param {object} jsonTags The DICOM json tags object.
* @returns {object} The DICOM elements.
*/
dwv.dicom.getElementsFromJSONTags = function (jsonTags) {
var keys = Object.keys(jsonTags);
var dicomElements = {};
for (var k = 0, len = keys.length; k < len; ++k) {
// get the DICOM element definition from its name
var tag = dwv.dicom.getTagFromDictionary(keys[k]);
if (!tag) {
continue;
}
var vr = tag.getVrFromDictionary();
// tag value
var value;
var undefinedLength = false;
var jsonTag = jsonTags[keys[k]];
if (vr === 'SQ') {
var items = [];
if (typeof jsonTag.undefinedLength !== 'undefined') {
undefinedLength = jsonTag.undefinedLength;
}
if (typeof jsonTag.value !== 'undefined' &&
jsonTag.value !== null) {
for (var i = 0; i < jsonTag.value.length; ++i) {
items.push(dwv.dicom.getElementsFromJSONTags(jsonTag.value[i]));
}
} else {
dwv.logger.trace('Undefined or null jsonTag SQ value.');
}
value = items;
} else {
if (Array.isArray(jsonTag)) {
value = jsonTag;
} else {
value = [jsonTag];
}
}
// create element
var dicomElement = {
tag: tag,
vr: vr,
value: value
};
if (undefinedLength) {
dicomElement.undefinedLength = undefinedLength;
}
// store
dicomElements[tag.getKey()] = dicomElement;
}
// return
return dicomElements;
};