// namespaces var dwv = dwv || {}; /** @namespace */ dwv.dicom = dwv.dicom || {}; /** * Get the version of the library. * @return {String} The version of the library. */ dwv.getVersion = function () { return "0.23.0-beta"; }; /** * Clean string: trim and remove ending. * @param {String} inputStr The string to clean. * @return {String} The cleaned string. */ dwv.dicom.cleanString = function (inputStr) { var res = inputStr; if ( inputStr ) { // trim spaces res = inputStr.trim(); // get rid of ending zero-width space (u200B) if ( res[res.length-1] === String.fromCharCode("u200B") ) { res = res.substring(0, res.length-1); } } return res; }; /** * Is the Native endianness Little Endian. * @type Boolean */ dwv.dicom.isNativeLittleEndian = function () { return new Int8Array(new Int16Array([1]).buffer)[0] > 0; }; /** * Get the utfLabel (used by the TextDecoder) from a character set term * References: * - DICOM [Value Encoding]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_6.html} * - DICOM [Specific Character Set]{@link http://dicom.nema.org/dicom/2013/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2} * - [TextDecoder#Parameters]{@link https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/TextDecoder#Parameters} */ dwv.dicom.getUtfLabel = function (charSetTerm) { var label = "utf-8"; if (charSetTerm === "ISO_IR 100" ) { label = "iso-8859-1"; } else if (charSetTerm === "ISO_IR 101" ) { label = "iso-8859-2"; } else if (charSetTerm === "ISO_IR 109" ) { label = "iso-8859-3"; } else if (charSetTerm === "ISO_IR 110" ) { label = "iso-8859-4"; } else if (charSetTerm === "ISO_IR 144" ) { label = "iso-8859-5"; } else if (charSetTerm === "ISO_IR 127" ) { label = "iso-8859-6"; } else if (charSetTerm === "ISO_IR 126" ) { label = "iso-8859-7"; } else if (charSetTerm === "ISO_IR 138" ) { label = "iso-8859-8"; } else if (charSetTerm === "ISO_IR 148" ) { label = "iso-8859-9"; } else if (charSetTerm === "ISO_IR 13" ) { label = "shift-jis"; } else if (charSetTerm === "ISO_IR 166" ) { label = "iso-8859-11"; } else if (charSetTerm === "ISO 2022 IR 87" ) { label = "iso-2022-jp"; } else if (charSetTerm === "ISO 2022 IR 149" ) { // not supported by TextDecoder when it says it should... //label = "iso-2022-kr"; } else if (charSetTerm === "ISO 2022 IR 58") { // not supported by TextDecoder... //label = "iso-2022-cn"; } else if (charSetTerm === "ISO_IR 192" ) { label = "utf-8"; } else if (charSetTerm === "GB18030" ) { label = "gb18030"; } else if (charSetTerm === "GB2312" ) { label = "gb2312"; } else if (charSetTerm === "GBK" ) { label = "chinese"; } return label; }; /** * Data reader. * @constructor * @param {Array} buffer The input array buffer. * @param {Boolean} isLittleEndian Flag to tell if the data is little or big endian. */ dwv.dicom.DataReader = function (buffer, isLittleEndian) { // Set endian flag if not defined. if ( typeof isLittleEndian === 'undefined' ) { isLittleEndian = true; } // Default text decoder var defaultTextDecoder = {}; defaultTextDecoder.decode = function (buffer) { var result = ""; for ( var i = 0, leni = buffer.length; i < leni; ++i ) { result += String.fromCharCode( buffer[ i ] ); } return result; }; // Text decoder var textDecoder = defaultTextDecoder; if (typeof window.TextDecoder !== "undefined") { textDecoder = new TextDecoder("iso-8859-1"); } /** * Set the utfLabel used to construct the TextDecoder. * @param {String} label The encoding label. */ this.setUtfLabel = function (label) { if (typeof window.TextDecoder !== "undefined") { textDecoder = new TextDecoder(label); } }; /** * Is the Native endianness Little Endian. * @private * @type Boolean */ var isNativeLittleEndian = dwv.dicom.isNativeLittleEndian(); /** * Flag to know if the TypedArray data needs flipping. * @private * @type Boolean */ var needFlip = (isLittleEndian !== isNativeLittleEndian); /** * The main data view. * @private * @type DataView */ var view = new DataView(buffer); /** * Flip an array's endianness. * Inspired from [DataStream.js]{@link https://github.com/kig/DataStream.js}. * @param {Object} array The array to flip (modified). */ this.flipArrayEndianness = function (array) { var blen = array.byteLength; var u8 = new Uint8Array(array.buffer, array.byteOffset, blen); var bpel = array.BYTES_PER_ELEMENT; var tmp; for ( var i = 0; i < blen; i += bpel ) { for ( var j = i + bpel - 1, k = i; j > k; j--, k++ ) { tmp = u8[k]; u8[k] = u8[j]; u8[j] = tmp; } } }; /** * Read Uint16 (2 bytes) data. * @param {Number} byteOffset The offset to start reading from. * @return {Number} The read data. */ this.readUint16 = function (byteOffset) { return view.getUint16(byteOffset, isLittleEndian); }; /** * Read Uint32 (4 bytes) data. * @param {Number} byteOffset The offset to start reading from. * @return {Number} The read data. */ this.readUint32 = function (byteOffset) { return view.getUint32(byteOffset, isLittleEndian); }; /** * Read Int32 (4 bytes) data. * @param {Number} byteOffset The offset to start reading from. * @return {Number} The read data. */ this.readInt32 = function (byteOffset) { return view.getInt32(byteOffset, isLittleEndian); }; /** * Read Uint8 array. * @param {Number} byteOffset The offset to start reading from. * @param {Number} size The size of the array. * @return {Array} The read data. */ this.readUint8Array = function (byteOffset, size) { return new Uint8Array(buffer, byteOffset, size); }; /** * Read Int8 array. * @param {Number} byteOffset The offset to start reading from. * @param {Number} size The size of the array. * @return {Array} The read data. */ this.readInt8Array = function (byteOffset, size) { return new Int8Array(buffer, byteOffset, size); }; /** * Read Uint16 array. * @param {Number} byteOffset The offset to start reading from. * @param {Number} size The size of the array. * @return {Array} The read data. */ this.readUint16Array = function (byteOffset, size) { var arraySize = size / Uint16Array.BYTES_PER_ELEMENT; var data = null; // byteOffset should be a multiple of Uint16Array.BYTES_PER_ELEMENT (=2) if ( (byteOffset % Uint16Array.BYTES_PER_ELEMENT) === 0 ) { data = new Uint16Array(buffer, byteOffset, arraySize); if ( needFlip ) { this.flipArrayEndianness(data); } } else { data = new Uint16Array(arraySize); for ( var i = 0; i < arraySize; ++i ) { data[i] = view.getInt16( (byteOffset + Uint16Array.BYTES_PER_ELEMENT * i), isLittleEndian); } } return data; }; /** * Read Int16 array. * @param {Number} byteOffset The offset to start reading from. * @param {Number} size The size of the array. * @return {Array} The read data. */ this.readInt16Array = function (byteOffset, size) { var arraySize = size / Int16Array.BYTES_PER_ELEMENT; var data = null; // byteOffset should be a multiple of Int16Array.BYTES_PER_ELEMENT (=2) if ( (byteOffset % Int16Array.BYTES_PER_ELEMENT) === 0 ) { data = new Int16Array(buffer, byteOffset, arraySize); if ( needFlip ) { this.flipArrayEndianness(data); } } else { data = new Int16Array(arraySize); for ( var i = 0; i < arraySize; ++i ) { data[i] = view.getInt16( (byteOffset + Int16Array.BYTES_PER_ELEMENT * i), isLittleEndian); } } return data; }; /** * Read Uint32 array. * @param {Number} byteOffset The offset to start reading from. * @param {Number} size The size of the array. * @return {Array} The read data. */ this.readUint32Array = function (byteOffset, size) { var arraySize = size / Uint32Array.BYTES_PER_ELEMENT; var data = null; // byteOffset should be a multiple of Uint32Array.BYTES_PER_ELEMENT (=4) if ( (byteOffset % Uint32Array.BYTES_PER_ELEMENT) === 0 ) { data = new Uint32Array(buffer, byteOffset, arraySize); if ( needFlip ) { this.flipArrayEndianness(data); } } else { data = new Uint32Array(arraySize); for ( var i = 0; i < arraySize; ++i ) { data[i] = view.getUint32( (byteOffset + Uint32Array.BYTES_PER_ELEMENT * i), isLittleEndian); } } return data; }; /** * Read Int32 array. * @param {Number} byteOffset The offset to start reading from. * @param {Number} size The size of the array. * @return {Array} The read data. */ this.readInt32Array = function (byteOffset, size) { var arraySize = size / Int32Array.BYTES_PER_ELEMENT; var data = null; // byteOffset should be a multiple of Int32Array.BYTES_PER_ELEMENT (=4) if ( (byteOffset % Int32Array.BYTES_PER_ELEMENT) === 0 ) { data = new Int32Array(buffer, byteOffset, arraySize); if ( needFlip ) { this.flipArrayEndianness(data); } } else { data = new Int32Array(arraySize); for ( var i = 0; i < arraySize; ++i ) { data[i] = view.getInt32( (byteOffset + Int32Array.BYTES_PER_ELEMENT * i), isLittleEndian); } } return data; }; /** * Read Float32 array. * @param {Number} byteOffset The offset to start reading from. * @param {Number} size The size of the array. * @return {Array} The read data. */ this.readFloat32Array = function (byteOffset, size) { var arraySize = size / Float32Array.BYTES_PER_ELEMENT; var data = null; // byteOffset should be a multiple of Float32Array.BYTES_PER_ELEMENT (=4) if ( (byteOffset % Float32Array.BYTES_PER_ELEMENT) === 0 ) { data = new Float32Array(buffer, byteOffset, arraySize); if ( needFlip ) { this.flipArrayEndianness(data); } } else { data = new Float32Array(arraySize); for ( var i = 0; i < arraySize; ++i ) { data[i] = view.getFloat32( (byteOffset + Float32Array.BYTES_PER_ELEMENT * i), isLittleEndian); } } return data; }; /** * Read Float64 array. * @param {Number} byteOffset The offset to start reading from. * @param {Number} size The size of the array. * @return {Array} The read data. */ this.readFloat64Array = function (byteOffset, size) { var arraySize = size / Float64Array.BYTES_PER_ELEMENT; var data = null; // byteOffset should be a multiple of Float64Array.BYTES_PER_ELEMENT (=8) if ( (byteOffset % Float64Array.BYTES_PER_ELEMENT) === 0 ) { data = new Float64Array(buffer, byteOffset, arraySize); if ( needFlip ) { this.flipArrayEndianness(data); } } else { data = new Float64Array(arraySize); for ( var i = 0; i < arraySize; ++i ) { data[i] = view.getFloat64( (byteOffset + Float64Array.BYTES_PER_ELEMENT*i), isLittleEndian); } } return data; }; /** * Read data as an hexadecimal string. * @param {Number} byteOffset The offset to start reading from. * @return {Array} The read data. */ this.readHex = function (byteOffset) { // read and convert to hex string var str = this.readUint16(byteOffset).toString(16); // return padded return "0x0000".substr(0, 6 - str.length) + str.toUpperCase(); }; /** * Read data as a string. * @param {Number} byteOffset The offset to start reading from. * @param {Number} nChars The number of characters to read. * @return {String} The read data. */ this.readString = function (byteOffset, nChars) { var data = this.readUint8Array(byteOffset, nChars); return defaultTextDecoder.decode(data); }; /** * Read data as a 'special' string, decoding it if the TextDecoder is available. * @param {Number} byteOffset The offset to start reading from. * @param {Number} nChars The number of characters to read. * @return {String} The read data. */ this.readSpecialString = function (byteOffset, nChars) { var data = this.readUint8Array(byteOffset, nChars); return textDecoder.decode(data); }; }; /** * Get the group-element pair from a tag string name. * @param {String} tagName The tag string name. * @return {Object} group-element pair. */ dwv.dicom.getGroupElementFromName = function (tagName) { var group = null; var element = null; var dict = dwv.dicom.dictionary; var keys0 = Object.keys(dict); var keys1 = null; // label for nested loop break outLabel: // search through dictionary for ( var k0 = 0, lenK0 = keys0.length; k0 < lenK0; ++k0 ) { group = keys0[k0]; keys1 = Object.keys( dict[group] ); for ( var k1 = 0, lenK1 = keys1.length; k1 < lenK1; ++k1 ) { element = keys1[k1]; if ( dict[group][element][2] === tagName ) { break outLabel; } } } return { 'group': group, 'element': element }; }; /** * Immutable tag. * @constructor * @param {String} group The tag group. * @param {String} element The tag element. */ dwv.dicom.Tag = function (group, element) { /** * Get the tag group. * @return {String} The tag group. */ this.getGroup = function () { return group; }; /** * Get the tag element. * @return {String} The tag element. */ this.getElement = function () { return element; }; }; // Tag class /** * Check for Tag equality. * @param {Object} rhs The other tag to compare to. * @return {Boolean} True if both tags are equal. */ dwv.dicom.Tag.prototype.equals = function (rhs) { return rhs !== null && this.getGroup() === rhs.getGroup() && this.getElement() === rhs.getElement(); }; /** * Check for Tag equality. * @param {Object} rhs The other tag to compare to provided as a simple object. * @return {Boolean} True if both tags are equal. */ dwv.dicom.Tag.prototype.equals2 = function (rhs) { if (rhs === null || typeof rhs.group === "undefined" || typeof rhs.element === "undefined" ) { return false; } return this.equals(new dwv.dicom.Tag(rhs.group, rhs.element)); }; // Get the FileMetaInformationGroupLength Tag. dwv.dicom.getFileMetaInformationGroupLengthTag = function () { return new dwv.dicom.Tag("0x0002", "0x0000"); }; // Get the Item Tag. dwv.dicom.getItemTag = function () { return new dwv.dicom.Tag("0xFFFE", "0xE000"); }; // Get the ItemDelimitationItem Tag. dwv.dicom.getItemDelimitationItemTag = function () { return new dwv.dicom.Tag("0xFFFE", "0xE00D"); }; // Get the SequenceDelimitationItem Tag. dwv.dicom.getSequenceDelimitationItemTag = function () { return new dwv.dicom.Tag("0xFFFE", "0xE0DD"); }; // Get the PixelData Tag. dwv.dicom.getPixelDataTag = function () { return new dwv.dicom.Tag("0x7FE0", "0x0010"); }; /** * Get the group-element key used to store DICOM elements. * @param {Number} group The DICOM group. * @param {Number} element The DICOM element. * @return {String} The key. */ dwv.dicom.getGroupElementKey = function (group, element) { return 'x' + group.substr(2,6) + element.substr(2,6); }; /** * Split a group-element key used to store DICOM elements. * @param {String} key The key in form "x00280102. * @return {Object} The DICOM group and element. */ dwv.dicom.splitGroupElementKey = function (key) { return {'group': key.substr(1,4), 'element': key.substr(5,8) }; }; /** * Get patient orientation label in the reverse direction. * @param {String} ori Patient Orientation value. * @return {String} Reverse Orientation Label. */ dwv.dicom.getReverseOrientation = function (ori) { if (!ori) { return null; } // reverse labels var rlabels = { "L": "R", "R": "L", "A": "P", "P": "A", "H": "F", "F": "H" }; var rori = ""; for (var n=0; n regular explicit: 8 + X * | 4 | 2+2 | 4 | X | -> 32bit VL: 12 + X * * | Tag | VL | Value | * | 4 | 4 | X | -> implicit (32bit VL): 8 + X * * | Tag | Len | Value | * | 4 | 4 | X | -> item: 8 + X */ dwv.dicom.getDataElementPrefixByteSize = function (vr, isImplicit) { return isImplicit ? 8 : dwv.dicom.is32bitVLVR(vr) ? 12 : 8; }; /** * DicomParser class. * @constructor */ dwv.dicom.DicomParser = function () { /** * The list of DICOM elements. * @type Array */ this.dicomElements = {}; /** * Default character set (optional). * @private * @type String */ var defaultCharacterSet; /** * Get the default character set. * @return {String} The default character set. */ this.getDefaultCharacterSet = function () { return defaultCharacterSet; }; /** * Set the default character set. * param {String} The character set. */ this.setDefaultCharacterSet = function (characterSet) { defaultCharacterSet = characterSet; }; }; /** * Get the raw DICOM data elements. * @return {Object} The raw DICOM elements. */ dwv.dicom.DicomParser.prototype.getRawDicomElements = function () { return this.dicomElements; }; /** * Get the DICOM data elements. * @return {Object} The DICOM elements. */ dwv.dicom.DicomParser.prototype.getDicomElements = function () { return new dwv.dicom.DicomElementsWrapper(this.dicomElements); }; /** * Read a DICOM tag. * @param reader The raw data reader. * @param offset The offset where to start to read. * @return An object containing the tags 'group', 'element' and 'name'. */ dwv.dicom.DicomParser.prototype.readTag = function (reader, offset) { // group var group = reader.readHex(offset); offset += Uint16Array.BYTES_PER_ELEMENT; // element var element = reader.readHex(offset); offset += Uint16Array.BYTES_PER_ELEMENT; // name var name = dwv.dicom.getGroupElementKey(group, element); // return return { 'group': group, 'element': element, 'name': name, 'endOffset': offset }; }; /** * Read an item data element. * @param {Object} reader The raw data reader. * @param {Number} offset The offset where to start to read. * @param {Boolean} implicit Is the DICOM VR implicit? * @returns {Object} The item data as a list of data elements. */ dwv.dicom.DicomParser.prototype.readItemDataElement = function (reader, offset, implicit) { var itemData = {}; // read the first item var item = this.readDataElement(reader, offset, implicit); offset = item.endOffset; // exit if it is a sequence delimitation item var isSeqDelim = ( item.tag.name === "xFFFEE0DD" ); if (isSeqDelim) { return { data: itemData, endOffset: item.endOffset, isSeqDelim: isSeqDelim }; } // store it itemData[item.tag.name] = item; // explicit VR items if (item.vl !== "u/l") { // not empty if (item.vl !== 0) { // read until the end offset var endOffset = offset; offset -= item.vl; while (offset < endOffset) { item = this.readDataElement(reader, offset, implicit); offset = item.endOffset; itemData[item.tag.name] = item; } } } // implicit VR items else { // read until the item delimitation item var isItemDelim = false; while (!isItemDelim) { item = this.readDataElement(reader, offset, implicit); offset = item.endOffset; isItemDelim = ( item.tag.name === "xFFFEE00D" ); if (!isItemDelim) { itemData[item.tag.name] = item; } } } return { 'data': itemData, 'endOffset': offset, 'isSeqDelim': false }; }; /** * Read the pixel item data element. * Ref: [Single frame fragments]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_A.4.html#table_A.4-1}. * @param {Object} reader The raw data reader. * @param {Number} offset The offset where to start to read. * @param {Boolean} implicit Is the DICOM VR implicit? * @returns {Array} The item data as an array of data elements. */ dwv.dicom.DicomParser.prototype.readPixelItemDataElement = function (reader, offset, implicit) { var itemData = []; // first item: basic offset table var item = this.readDataElement(reader, offset, implicit); var offsetTableVl = item.vl; offset = item.endOffset; // read until the sequence delimitation item var isSeqDelim = false; while (!isSeqDelim) { item = this.readDataElement(reader, offset, implicit); offset = item.endOffset; isSeqDelim = ( item.tag.name === "xFFFEE0DD" ); if (!isSeqDelim) { itemData.push(item.value); } } return { 'data': itemData, 'endOffset': offset, 'offsetTableVl': offsetTableVl }; }; /** * Read a DICOM data element. * Reference: [DICOM VRs]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html#table_6.2-1}. * @param {Object} reader The raw data reader. * @param {Number} offset The offset where to start to read. * @param {Boolean} implicit Is the DICOM VR implicit? * @return {Object} An object containing the element 'tag', 'vl', 'vr', 'data' and 'endOffset'. */ dwv.dicom.DicomParser.prototype.readDataElement = function (reader, offset, implicit) { // Tag: group, element var tag = this.readTag(reader, offset); offset = tag.endOffset; // Value Representation (VR) var vr = null; var is32bitVLVR = false; if (dwv.dicom.isTagWithVR(tag.group, tag.element)) { // implicit VR if (implicit) { vr = "UN"; var dict = dwv.dicom.dictionary; if ( typeof dict[tag.group] !== "undefined" && typeof dict[tag.group][tag.element] !== "undefined" ) { vr = dwv.dicom.dictionary[tag.group][tag.element][0]; } is32bitVLVR = true; } else { vr = reader.readString( offset, 2 ); offset += 2 * Uint8Array.BYTES_PER_ELEMENT; is32bitVLVR = dwv.dicom.is32bitVLVR(vr); // reserved 2 bytes if ( is32bitVLVR ) { offset += 2 * Uint8Array.BYTES_PER_ELEMENT; } } } else { vr = "UN"; is32bitVLVR = true; } // Value Length (VL) var vl = 0; if ( is32bitVLVR ) { vl = reader.readUint32( offset ); offset += Uint32Array.BYTES_PER_ELEMENT; } else { vl = reader.readUint16( offset ); offset += Uint16Array.BYTES_PER_ELEMENT; } // check the value of VL var vlString = vl; if( vl === 0xffffffff ) { vlString = "u/l"; vl = 0; } var startOffset = offset; // data var data = null; var isPixelData = (tag.name === "x7FE00010"); // pixel data sequence (implicit) if (isPixelData && vlString === "u/l") { var pixItemData = this.readPixelItemDataElement(reader, offset, implicit); offset = pixItemData.endOffset; startOffset += pixItemData.offsetTableVl; data = pixItemData.data; } else if (isPixelData && (vr === "OB" || vr === "OW" || vr === "OF" || vr === "ox")) { // BitsAllocated var bitsAllocated = 16; if ( typeof this.dicomElements.x00280100 !== 'undefined' ) { bitsAllocated = this.dicomElements.x00280100.value[0]; } else { console.warn("Reading DICOM pixel data with default bitsAllocated."); } if (bitsAllocated === 8 && vr === "OW") { console.warn("Reading DICOM pixel data with vr=OW and bitsAllocated=8 (should be 16)."); } if (bitsAllocated === 16 && vr === "OB") { console.warn("Reading DICOM pixel data with vr=OB and bitsAllocated=16 (should be 8)."); } // PixelRepresentation 0->unsigned, 1->signed var pixelRepresentation = 0; if ( typeof this.dicomElements.x00280103 !== 'undefined' ) { pixelRepresentation = this.dicomElements.x00280103.value[0]; } // read if ( bitsAllocated === 8 ) { if (pixelRepresentation === 0) { data = reader.readUint8Array( offset, vl ); } else { data = reader.readInt8Array( offset, vl ); } } else if ( bitsAllocated === 16 ) { if (pixelRepresentation === 0) { data = reader.readUint16Array( offset, vl ); } else { data = reader.readInt16Array( offset, vl ); } } else if ( bitsAllocated === 32 ) { if (pixelRepresentation === 0) { data = reader.readUint32Array( offset, vl ); } else { data = reader.readInt32Array( offset, vl ); } } else if ( bitsAllocated === 64 ) { if (pixelRepresentation === 0) { data = reader.readUint64Array( offset, vl ); } else { data = reader.readInt64Array( offset, vl ); } } offset += vl; } // others else if ( vr === "OB" ) { data = reader.readInt8Array( offset, vl ); offset += vl; } else if ( vr === "OW" ) { data = reader.readInt16Array( offset, vl ); offset += vl; } else if ( vr === "OF" ) { data = reader.readInt32Array( offset, vl ); offset += vl; } else if ( vr === "OD" ) { data = reader.readInt64Array( offset, vl ); offset += vl; } // numbers else if( vr === "US") { data = reader.readUint16Array( offset, vl ); offset += vl; } else if( vr === "UL") { data = reader.readUint32Array( offset, vl ); offset += vl; } else if( vr === "SS") { data = reader.readInt16Array( offset, vl ); offset += vl; } else if( vr === "SL") { data = reader.readInt32Array( offset, vl ); offset += vl; } else if( vr === "FL") { data = reader.readFloat32Array( offset, vl ); offset += vl; } else if( vr === "FD") { data = reader.readFloat64Array( offset, vl ); offset += vl; } // attribute else if( vr === "AT") { var raw = reader.readUint16Array( offset, vl ); offset += vl; data = []; for ( var i = 0, leni = raw.length; i < leni; i+=2 ) { var stri = raw[i].toString(16); var stri1 = raw[i+1].toString(16); var str = "("; str += "0000".substr(0, 4 - stri.length) + stri.toUpperCase(); str += ","; str += "0000".substr(0, 4 - stri1.length) + stri1.toUpperCase(); str += ")"; data.push(str); } } // not available else if( vr === "UN") { data = reader.readUint8Array( offset, vl ); offset += vl; } // sequence else if (vr === "SQ") { data = []; var itemData; // explicit VR sequence if (vlString !== "u/l") { // not empty if (vl !== 0) { var sqEndOffset = offset + vl; while (offset < sqEndOffset) { itemData = this.readItemDataElement(reader, offset, implicit); data.push( itemData.data ); offset = itemData.endOffset; } } } // implicit VR sequence else { // read until the sequence delimitation item var isSeqDelim = false; while (!isSeqDelim) { itemData = this.readItemDataElement(reader, offset, implicit); isSeqDelim = itemData.isSeqDelim; offset = itemData.endOffset; // do not store the delimitation item if (!isSeqDelim) { data.push( itemData.data ); } } } } // raw else { if ( vr === "SH" || vr === "LO" || vr === "ST" || vr === "PN" || vr === "LT" || vr === "UT" ) { data = reader.readSpecialString( offset, vl ); } else { data = reader.readString( offset, vl ); } offset += vl; data = data.split("\\"); } // return return { 'tag': tag, 'vr': vr, 'vl': vlString, 'value': data, 'startOffset': startOffset, 'endOffset': offset }; }; /** * Parse the complete DICOM file (given as input to the class). * Fills in the member object 'dicomElements'. * @param buffer The input array buffer. */ dwv.dicom.DicomParser.prototype.parse = function (buffer) { var offset = 0; var implicit = false; // default readers var metaReader = new dwv.dicom.DataReader(buffer); var dataReader = new dwv.dicom.DataReader(buffer); // 128 -> 132: magic word offset = 128; var magicword = metaReader.readString( offset, 4 ); offset += 4 * Uint8Array.BYTES_PER_ELEMENT; if(magicword !== "DICM") { throw new Error("Not a valid DICOM file (no magic DICM word found)"); } // 0x0002, 0x0000: FileMetaInformationGroupLength var dataElement = this.readDataElement(metaReader, offset, false); offset = dataElement.endOffset; // store the data element this.dicomElements[dataElement.tag.name] = dataElement; // get meta length var metaLength = parseInt(dataElement.value[0], 10); // meta elements var metaEnd = offset + metaLength; while( offset < metaEnd ) { // get the data element dataElement = this.readDataElement(metaReader, offset, false); offset = dataElement.endOffset; // store the data element this.dicomElements[dataElement.tag.name] = dataElement; } // check the TransferSyntaxUID (has to be there!) if (typeof this.dicomElements.x00020010 === "undefined") { throw new Error("Not a valid DICOM file (no TransferSyntaxUID found)"); } var syntax = dwv.dicom.cleanString(this.dicomElements.x00020010.value[0]); // check support if (!dwv.dicom.isReadSupportedTransferSyntax(syntax)) { throw new Error("Unsupported DICOM transfer syntax: '"+syntax+ "' ("+dwv.dicom.getTransferSyntaxName(syntax)+")"); } // Implicit VR if (dwv.dicom.isImplicitTransferSyntax(syntax)) { implicit = true; } // Big Endian if (dwv.dicom.isBigEndianTransferSyntax(syntax)) { dataReader = new dwv.dicom.DataReader(buffer,false); } // default character set if (typeof this.getDefaultCharacterSet() !== "undefined") { dataReader.setUtfLabel(this.getDefaultCharacterSet()); } // DICOM data elements while ( offset < buffer.byteLength ) { // get the data element dataElement = this.readDataElement(dataReader, offset, implicit); // check character set if (dataElement.tag.name === "x00080005") { var charSetTerm; if (dataElement.value.length === 1) { charSetTerm = dwv.dicom.cleanString(dataElement.value[0]); } else { charSetTerm = dwv.dicom.cleanString(dataElement.value[1]); console.warn("Unsupported character set with code extensions: '"+charSetTerm+"'."); } dataReader.setUtfLabel(dwv.dicom.getUtfLabel(charSetTerm)); } // increment offset offset = dataElement.endOffset; // store the data element this.dicomElements[dataElement.tag.name] = dataElement; } // safety check... if (buffer.byteLength !== offset) { console.warn("Did not reach the end of the buffer: "+ offset+" != "+buffer.byteLength); } // pixel buffer if (typeof this.dicomElements.x7FE00010 !== "undefined") { var numberOfFrames = 1; if (typeof this.dicomElements.x00280008 !== "undefined") { numberOfFrames = this.dicomElements.x00280008.value[0]; } if (this.dicomElements.x7FE00010.vl !== "u/l") { // compressed should be encapsulated... if (dwv.dicom.isJpeg2000TransferSyntax( syntax ) || dwv.dicom.isJpegBaselineTransferSyntax( syntax ) || dwv.dicom.isJpegLosslessTransferSyntax( syntax ) ) { console.warn("Compressed but no items..."); } // calculate the slice size var pixData = this.dicomElements.x7FE00010.value; var columns = this.dicomElements.x00280011.value[0]; var rows = this.dicomElements.x00280010.value[0]; var samplesPerPixel = this.dicomElements.x00280002.value[0]; var sliceSize = columns * rows * samplesPerPixel; // slice data in an array of frames var newPixData = []; var frameOffset = 0; for (var g = 0; g < numberOfFrames; ++g) { newPixData[g] = pixData.slice(frameOffset, frameOffset+sliceSize); frameOffset += sliceSize; } // store as pixel data this.dicomElements.x7FE00010.value = newPixData; } else { // handle fragmented pixel buffer // Reference: http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_8.2.html // (third note, "Depending on the transfer syntax...") var pixItems = this.dicomElements.x7FE00010.value; if (pixItems.length > 1 && pixItems.length > numberOfFrames ) { // concatenate pixel data items // concat does not work on typed arrays //this.pixelBuffer = this.pixelBuffer.concat( dataElement.data ); // manual concat... var nItemPerFrame = pixItems.length / numberOfFrames; var newPixItems = []; var index = 0; for (var f = 0; f < numberOfFrames; ++f) { index = f * nItemPerFrame; // calculate the size of a frame var size = 0; for (var i = 0; i < nItemPerFrame; ++i) { size += pixItems[index + i].length; } // create new buffer var newBuffer = new pixItems[0].constructor(size); // fill new buffer var fragOffset = 0; for (var j = 0; j < nItemPerFrame; ++j) { newBuffer.set( pixItems[index + j], fragOffset ); fragOffset += pixItems[index + j].length; } newPixItems[f] = newBuffer; } // store as pixel data this.dicomElements.x7FE00010.value = newPixItems; } } } }; /** * DicomElements wrapper. * @constructor * @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. * @return {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. * @return {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 array. * @return {Array} */ this.dumpToTable = function () { var keys = Object.keys(dicomElements); var dict = dwv.dicom.dictionary; var table = []; var dicomElement = null; var dictElement = null; var row = null; for ( var i = 0, leni = keys.length; i < leni; ++i ) { dicomElement = dicomElements[keys[i]]; row = {}; // dictionnary entry (to get name) dictElement = null; if ( typeof dict[dicomElement.tag.group] !== "undefined" && typeof dict[dicomElement.tag.group][dicomElement.tag.element] !== "undefined") { dictElement = dict[dicomElement.tag.group][dicomElement.tag.element]; } // name if ( dictElement !== null ) { row.name = dictElement[2]; } else { row.name = "Unknown Tag & Data"; } // value row.value = this.getElementValueAsString(dicomElement); // others row.group = dicomElement.tag.group; row.element = dicomElement.tag.element; row.vr = dicomElement.vr; row.vl = dicomElement.vl; table.push( row ); } return table; }; /** * Dump the DICOM tags to a string. * @return {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. * @return {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]; var daYear = parseInt( daValue.substr(0,4), 10 ); var daMonth = parseInt( daValue.substr(4,2), 10 ) - 1; // 0-11 var daDay = parseInt( daValue.substr(6,2), 10 ); var da = new Date(daYear, daMonth, daDay); 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 = ( 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. */ 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. */ dwv.dicom.DicomElementsWrapper.prototype.getElementAsString = function ( dicomElement, prefix ) { // default prefix prefix = prefix || ""; // get element from dictionary var dict = dwv.dicom.dictionary; var dictElement = null; if ( typeof dict[dicomElement.tag.group] !== "undefined" && typeof dict[dicomElement.tag.group][dicomElement.tag.element] !== "undefined") { dictElement = dict[dicomElement.tag.group][dicomElement.tag.element]; } var deSize = dicomElement.value.length; var 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]; } // pixel sequence else if ( isPixSequence ) { 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 += ")"; } // 'O'ther array, limited display length 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" ) { line += " "; line += this.getElementValueAsString(dicomElement, false); } // default else { 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 ( dictElement !== null ) { line += dictElement[2]; } 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); } // pixel sequence else if ( isPixSequence ) { 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. * @return {Object} The DICOM element value. */ dwv.dicom.DicomElementsWrapper.prototype.getFromGroupElement = function ( group, element ) { return this.getFromKey( dwv.dicom.getGroupElementKey(group, element) ); }; /** * Get a DICOM Element value from a tag name. * Uses the DICOM dictionary. * @param {String} name The tag name. * @return {Object} The DICOM element value. */ dwv.dicom.DicomElementsWrapper.prototype.getFromName = function ( name ) { var value = null; var tagGE = dwv.dicom.getGroupElementFromName(name); // check that we are not at the end of the dictionary if ( tagGE.group !== null && tagGE.element !== null ) { value = this.getFromKey(dwv.dicom.getGroupElementKey(tagGE.group, tagGE.element)); } return value; };