diff --git a/dist/dwv.js b/dist/dwv.js index a1fede6a3b..53007d535d 100644 --- a/dist/dwv.js +++ b/dist/dwv.js @@ -1,3 +1,4 @@ +/*! dwv 0.24.0 2018-08-24 22:42:55 */ // Inspired from umdjs // See https://github.com/umdjs/umd/blob/master/templates/returnExports.js (function (root, factory) { @@ -19,15 +20,15 @@ // like Node. // i18next-xhr-backend: requires XMlHttpRequest - // Konva (requires 'canvas') and MagicWand are gui specific - // -> deactivated for now... + // Konva: requires 'canvas' + // MagicWand: no module, deactivated for now... module.exports = factory( require('i18next'), require('i18next-xhr-backend'), require('i18next-browser-languagedetector'), require('jszip'), - null, + require('konva'), null ); } else { @@ -106,6 +107,7 @@ dwv.App = function () var infoController = null; // Dicom tags gui + var tags = null; var tagsGui = null; // Drawing list gui @@ -256,7 +258,7 @@ dwv.App = function () for ( var t = 0; t < config.tools.length; ++t ) { var toolName = config.tools[t]; if ( toolName === "Draw" ) { - if ( config.shapes !== 0 ) { + if ( typeof config.shapes !== "undefined" && config.shapes.length !== 0 ) { // setup the shape list var shapeList = {}; for ( var s = 0; s < config.shapes.length; ++s ) { @@ -274,10 +276,12 @@ dwv.App = function () toolList.Draw.addEventListener("draw-change", fireEvent); toolList.Draw.addEventListener("draw-move", fireEvent); toolList.Draw.addEventListener("draw-delete", fireEvent); + } else { + console.warn("Please provide a list of shapes in the application configuration to activate the Draw tool."); } } else if ( toolName === "Filter" ) { - if ( config.filters.length !== 0 ) { + if ( typeof config.filters !== "undefined" && config.filters.length !== 0 ) { // setup the filter list var filterList = {}; for ( var f = 0; f < config.filters.length; ++f ) { @@ -292,6 +296,8 @@ dwv.App = function () toolList.Filter = new dwv.tool.Filter(filterList, this); toolList.Filter.addEventListener("filter-run", fireEvent); toolList.Filter.addEventListener("filter-undo", fireEvent); + } else { + console.warn("Please provide a list of filters in the application configuration to activate the Filter tool."); } } else { @@ -662,7 +668,8 @@ dwv.App = function () // flag used by scroll to decide wether to activate or not // TODO: supposing multi-slice for zip files, could not be... isMonoSliceData = (data.length === 1 && - firstName.split('.').pop().toLowerCase() !== "zip"); + firstName.split('.').pop().toLowerCase() !== "zip" && + !dwv.utils.endsWith(firstName, "DICOMDIR")); // set IO loader.setDefaultCharacterSet(defaultCharacterSet); loader.onload = function (data) { @@ -862,6 +869,15 @@ dwv.App = function () return drawController.getDrawDisplayDetails(); }; + /** + * Get the data tags. + * @return {Object} The list of DICOM tags. + */ + this.getTags = function () + { + return tags; + }; + /** * Get a list of drawing store details. * @return {Object} A list of draw details including id, text, quant... @@ -1081,7 +1097,8 @@ dwv.App = function () var state = new dwv.State(); // add href to link (html5) var element = self.getElement("download-state"); - element.href = "data:application/json," + state.toJSON(self); + var blob = new Blob([state.toJSON(self)], {type: 'application/json'}); + element.href = window.URL.createObjectURL(blob); }; /** @@ -1428,6 +1445,7 @@ dwv.App = function () viewController = new dwv.ViewController(view); // append the DICOM tags table + tags = data.info; if ( tagsGui ) { tagsGui.update(data.info); } @@ -1853,8 +1871,8 @@ dwv.DrawController = function (drawDiv) } // get details (non konva vars) drawingsDetails[ group.id() ] = { - "textExpr": encodeURIComponent(texts[0].textExpr), - "longText": encodeURIComponent(texts[0].longText), + "textExpr": texts[0].textExpr, + "longText": texts[0].longText, "quant": texts[0].quant }; } @@ -1893,8 +1911,8 @@ dwv.DrawController = function (drawDiv) var statePosKids = statePosGroup.getChildren(); for ( var j = 0, lenj = statePosKids.length; j < lenj; ++j ) { - // shape group - var stateGroup = statePosKids[j]; + // shape group (use first one since it will be removed from the group when we change it) + var stateGroup = statePosKids[0]; // add group to posGroup (switches its parent) posGroup.add( stateGroup ); // shape @@ -2946,6 +2964,8 @@ dwv.ViewController = function ( view ) if ( playerID === null ) { var nSlices = view.getImage().getGeometry().getSize().getNumberOfSlices(); var nFrames = view.getImage().getNumberOfFrames(); + var recommendedDisplayFrameRate = view.getImage().getMeta().RecommendedDisplayFrameRate; + var milliseconds = view.getPlaybackMilliseconds(recommendedDisplayFrameRate); playerID = setInterval( function () { if ( nSlices !== 1 ) { @@ -2958,7 +2978,7 @@ dwv.ViewController = function ( view ) } } - }, 300); + }, milliseconds); } else { this.stop(); } @@ -3033,1902 +3053,1970 @@ dwv.ViewController = function ( view ) // namespaces var dwv = dwv || {}; -/** @namespace */ dwv.dicom = dwv.dicom || {}; /** - * Get the version of the library. - * @return {String} The version of the library. + * DicomElements wrapper. + * @constructor + * @param {Array} dicomElements The elements to wrap. */ -dwv.getVersion = function () { return "0.23.3"; }; +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; + }; + +}; /** - * Clean string: trim and remove ending. - * @param {String} inputStr The string to clean. - * @return {String} The cleaned string. + * 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.cleanString = function (inputStr) +dwv.dicom.DicomElementsWrapper.prototype.getElementValueAsString = function ( dicomElement, pretty ) { - 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); + 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 res; + return str; }; /** - * Is the Native endianness Little Endian. - * @type Boolean + * Get a data element value as a string. + * @param {String} groupElementKey The key to retrieve. */ -dwv.dicom.isNativeLittleEndian = function () +dwv.dicom.DicomElementsWrapper.prototype.getElementValueAsStringFromKey = function ( groupElementKey ) { - return new Int8Array(new Int16Array([1]).buffer)[0] > 0; + return this.getElementValueAsString( this.getDEFromKey(groupElementKey) ); }; /** - * 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} + * Get a data element as a string. + * @param {Object} dicomElement The DICOM element. + * @param {String} prefix A string to prepend this one. */ -dwv.dicom.getUtfLabel = function (charSetTerm) +dwv.dicom.DicomElementsWrapper.prototype.getElementAsString = function ( dicomElement, prefix ) { - 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"; + // 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]; } - else if (charSetTerm === "ISO_IR 13" ) { - label = "shift-jis"; + + 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 (charSetTerm === "ISO_IR 166" ) { - label = "iso-8859-11"; + else if ( isOtherVR ) { + deSize = 1; } - else if (charSetTerm === "ISO 2022 IR 87" ) { - label = "iso-2022-jp"; + + 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 if (charSetTerm === "ISO 2022 IR 149" ) { - // not supported by TextDecoder when it says it should... - //label = "iso-2022-kr"; + 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 += "]"; + } } - else if (charSetTerm === "ISO 2022 IR 58") { - // not supported by TextDecoder... - //label = "iso-2022-cn"; + + // align # + var nSpaces = 55 - line.length; + if ( nSpaces > 0 ) { + for ( var s = 0; s < nSpaces; ++s ) { + line += " "; + } } - else if (charSetTerm === "ISO_IR 192" ) { - label = "utf-8"; + line += " # "; + if ( dicomElement.vl < 100 ) { + line += " "; } - else if (charSetTerm === "GB18030" ) { - label = "gb18030"; + if ( dicomElement.vl < 10 ) { + line += " "; } - else if (charSetTerm === "GB2312" ) { - label = "gb2312"; + line += dicomElement.vl; + line += ", "; + line += deSize; //dictElement[1]; + line += " "; + if ( dictElement !== null ) { + line += dictElement[2]; } - else if (charSetTerm === "GBK" ) { - label = "chinese"; + else { + line += "Unknown Tag & Data"; } - 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; - } + var message = null; - // 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"); - } + // 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; + } - /** - * 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); - } - }; + // 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"; - /** - * Is the Native endianness Little Endian. - * @private - * @type Boolean - */ - var isNativeLittleEndian = dwv.dicom.isNativeLittleEndian(); + line += "\n"; + line += this.getElementAsString(itemElement, prefix + " "); - /** - * Flag to know if the TypedArray data needs flipping. - * @private - * @type Boolean - */ - var needFlip = (isLittleEndian !== isNativeLittleEndian); + for ( var m = 0, lenm = itemKeys.length; m < lenm; ++m ) { + if ( itemKeys[m] !== "xFFFEE000" ) { + line += "\n"; + line += this.getElementAsString(item[itemKeys[m]], prefix + " "); + } + } - /** - * The main data view. - * @private - * @type DataView - */ - var view = new DataView(buffer); + 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 + " "); - /** - * 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); - } + message = "(SequenceDelimitationItem"; + if ( dicomElement.vl !== "u/l" ) { + message += " for re-encod."; } - else { - data = new Float64Array(arraySize); - for ( var i = 0; i < arraySize; ++i ) { - data[i] = view.getFloat64( (byteOffset + - Float64Array.BYTES_PER_ELEMENT*i), - isLittleEndian); - } + 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 + " "); } - 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); - }; + var pixDelimElement = { + "tag": { "group": "0xFFFE", "element": "0xE0DD" }, + "vr": "na", + "vl": "0", + "value": ["(SequenceDelimitationItem)"] + }; + line += "\n"; + line += this.getElementAsString(pixDelimElement, prefix); + } + return prefix + line; }; /** - * Get the group-element pair from a tag string name. - * @param {String} tagName The tag string name. - * @return {Object} group-element pair. + * 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.getGroupElementFromName = function (tagName) +dwv.dicom.DicomElementsWrapper.prototype.getFromGroupElement = function ( + group, element ) { - 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 }; + return this.getFromKey( + dwv.dicom.getGroupElementKey(group, element) ); }; /** - * Immutable tag. - * @constructor - * @param {String} group The tag group. - * @param {String} element The tag 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.Tag = function (group, element) +dwv.dicom.DicomElementsWrapper.prototype.getFromName = function ( name ) { - /** - * 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(); + 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; }; /** - * 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. + * Get the file list from a DICOMDIR + * @param {Object} data The buffer data of the DICOMDIR + * @return {Array} The file list as an array ordered by STUDY > SERIES > IMAGES. */ -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) +dwv.dicom.getFileListFromDicomDir = function (data) { - return 'x' + group.substr(2,6) + element.substr(2,6); -}; + // parse file + var parser = new dwv.dicom.DicomParser(); + parser.parse(data); + var elements = parser.getRawDicomElements(); -/** - * 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) }; -}; + // Directory Record Sequence + if ( typeof elements.x00041220 === "undefined" || + typeof elements.x00041220.value === "undefined" ) { + console.warn("No Directory Record Sequence found in DICOMDIR."); + return; + } + var dirSeq = elements.x00041220.value; -/** - * 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; + if ( dirSeq.length === 0 ) { + console.warn("The Directory Record Sequence of the DICOMDIR is empty."); + return; } - // reverse labels - var rlabels = { - "L": "R", - "R": "L", - "A": "P", - "P": "A", - "H": "F", - "F": "H" - }; - var rori = ""; - for (var n=0; n 0; }; /** - * Tell if a given syntax needs decompression. - * @param {String} syntax The transfer syntax to test. - * @return {String} The name of the decompression algorithm. + * 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.getSyntaxDecompressionName = function (syntax) +dwv.dicom.getUtfLabel = function (charSetTerm) { - var algo = null; - if ( dwv.dicom.isJpeg2000TransferSyntax(syntax) ) { - algo = "jpeg2000"; + var label = "utf-8"; + if (charSetTerm === "ISO_IR 100" ) { + label = "iso-8859-1"; } - else if ( dwv.dicom.isJpegBaselineTransferSyntax(syntax) ) { - algo = "jpeg-baseline"; + else if (charSetTerm === "ISO_IR 101" ) { + label = "iso-8859-2"; } - else if ( dwv.dicom.isJpegLosslessTransferSyntax(syntax) ) { - algo = "jpeg-lossless"; + else if (charSetTerm === "ISO_IR 109" ) { + label = "iso-8859-3"; } - return algo; -}; - -/** - * Tell if a given syntax is supported for reading. - * @param {String} syntax The transfer syntax to test. - * @return {Boolean} True if a supported syntax. - */ -dwv.dicom.isReadSupportedTransferSyntax = function (syntax) { - - // Unsupported: - // "1.2.840.10008.1.2.1.99": Deflated Explicit VR - Little Endian - // "1.2.840.10008.1.2.4.100": MPEG2 Image Compression - // dwv.dicom.isJpegRetiredTransferSyntax(syntax): non supported JPEG - // dwv.dicom.isJpeglsTransferSyntax(syntax): JPEG-LS - // "1.2.840.10008.1.2.5": RLE (lossless) - - return( syntax === "1.2.840.10008.1.2" || // Implicit VR - Little Endian - syntax === "1.2.840.10008.1.2.1" || // Explicit VR - Little Endian - syntax === "1.2.840.10008.1.2.2" || // Explicit VR - Big Endian - dwv.dicom.isJpegBaselineTransferSyntax(syntax) || // JPEG baseline - dwv.dicom.isJpegLosslessTransferSyntax(syntax) || // JPEG Lossless - dwv.dicom.isJpeg2000TransferSyntax(syntax) ); // JPEG 2000 + 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; }; /** - * Get the transfer syntax name. - * Reference: [UID Values]{@link http://dicom.nema.org/dicom/2013/output/chtml/part06/chapter_A.html}. - * @param {String} syntax The transfer syntax. - * @return {String} The name of the transfer syntax. + * 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.getTransferSyntaxName = function (syntax) +dwv.dicom.DataReader = function (buffer, isLittleEndian) { - var name = "Unknown"; - // Implicit VR - Little Endian - if( syntax === "1.2.840.10008.1.2" ) { - name = "Little Endian Implicit"; - } - // Explicit VR - Little Endian - else if( syntax === "1.2.840.10008.1.2.1" ) { - name = "Little Endian Explicit"; - } - // Deflated Explicit VR - Little Endian - else if( syntax === "1.2.840.10008.1.2.1.99" ) { - name = "Little Endian Deflated Explicit"; + // Set endian flag if not defined. + if ( typeof isLittleEndian === 'undefined' ) { + isLittleEndian = true; } - // Explicit VR - Big Endian - else if( syntax === "1.2.840.10008.1.2.2" ) { - name = "Big Endian Explicit"; + + // 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"); } - // JPEG baseline - else if( dwv.dicom.isJpegBaselineTransferSyntax(syntax) ) { - if ( syntax === "1.2.840.10008.1.2.4.50" ) { - name = "JPEG Baseline"; + + /** + * 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 { // *.51 - name = "JPEG Extended, Process 2+4"; + else { + data = new Uint16Array(arraySize); + for ( var i = 0; i < arraySize; ++i ) { + data[i] = view.getInt16( (byteOffset + + Uint16Array.BYTES_PER_ELEMENT * i), + isLittleEndian); + } } - } - // JPEG Lossless - else if( dwv.dicom.isJpegLosslessTransferSyntax(syntax) ) { - if ( syntax === "1.2.840.10008.1.2.4.57" ) { - name = "JPEG Lossless, Nonhierarchical (Processes 14)"; + 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 { // *.70 - name = "JPEG Lossless, Non-hierarchical, 1st Order Prediction"; + else { + data = new Int16Array(arraySize); + for ( var i = 0; i < arraySize; ++i ) { + data[i] = view.getInt16( (byteOffset + + Int16Array.BYTES_PER_ELEMENT * i), + isLittleEndian); + } } - } - // Retired JPEG - else if( dwv.dicom.isJpegRetiredTransferSyntax(syntax) ) { - name = "Retired JPEG"; - } - // JPEG-LS - else if( dwv.dicom.isJpeglsTransferSyntax(syntax) ) { - name = "JPEG-LS"; - } - // JPEG 2000 - else if( dwv.dicom.isJpeg2000TransferSyntax(syntax) ) { - if ( syntax === "1.2.840.10008.1.2.4.91" ) { - name = "JPEG 2000 (Lossless or Lossy)"; + 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 { // *.90 - name = "JPEG 2000 (Lossless only)"; + else { + data = new Uint32Array(arraySize); + for ( var i = 0; i < arraySize; ++i ) { + data[i] = view.getUint32( (byteOffset + + Uint32Array.BYTES_PER_ELEMENT * i), + isLittleEndian); + } } - } - // MPEG2 Image Compression - else if( syntax === "1.2.840.10008.1.2.4.100" ) { - name = "MPEG2"; - } - // RLE (lossless) - else if( syntax === "1.2.840.10008.1.2.5" ) { - name = "RLE"; - } - // return - return name; -}; - -/** - * Get the appropriate TypedArray in function of arguments. - * @param {Number} bitsAllocated The number of bites used to store the data: [8, 16, 32]. - * @param {Number} pixelRepresentation The pixel representation, 0:unsigned;1:signed. - * @param {Size} size The size of the new array. - * @return The good typed array. - */ -dwv.dicom.getTypedArray = function (bitsAllocated, pixelRepresentation, size) -{ - var res = null; - if (bitsAllocated === 8) { - if (pixelRepresentation === 0) { - res = new Uint8Array(size); + 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 { - res = new Int8Array(size); + data = new Int32Array(arraySize); + for ( var i = 0; i < arraySize; ++i ) { + data[i] = view.getInt32( (byteOffset + + Int32Array.BYTES_PER_ELEMENT * i), + isLittleEndian); + } } - } - else if (bitsAllocated === 16) { - if (pixelRepresentation === 0) { - res = new Uint16Array(size); + 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 { - res = new Int16Array(size); + data = new Float32Array(arraySize); + for ( var i = 0; i < arraySize; ++i ) { + data[i] = view.getFloat32( (byteOffset + + Float32Array.BYTES_PER_ELEMENT * i), + isLittleEndian); + } } - } - else if (bitsAllocated === 32) { - if (pixelRepresentation === 0) { - res = new Uint32Array(size); + 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 { - res = new Int32Array(size); + data = new Float64Array(arraySize); + for ( var i = 0; i < arraySize; ++i ) { + data[i] = view.getFloat64( (byteOffset + + Float64Array.BYTES_PER_ELEMENT*i), + isLittleEndian); + } } - } - return res; + 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); + }; + }; /** - * Does this Value Representation (VR) have a 32bit Value Length (VL). - * Ref: [Data Element explicit]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_7.html#table_7.1-1}. - * @param {String} vr The data Value Representation (VR). - * @returns {Boolean} True if this VR has a 32-bit VL. + * Get the group-element pair from a tag string name. + * @param {String} tagName The tag string name. + * @return {Object} group-element pair. */ -dwv.dicom.is32bitVLVR = function (vr) +dwv.dicom.getGroupElementFromName = function (tagName) { - // added locally used 'ox' - return ( vr === "OB" || vr === "OW" || vr === "OF" || vr === "ox" || vr === "UT" || - vr === "SQ" || vr === "UN" ); + 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 }; }; /** - * Does this tag have a VR. - * Basically the Item, ItemDelimitationItem and SequenceDelimitationItem tags. + * Immutable tag. + * @constructor * @param {String} group The tag group. * @param {String} element The tag element. - * @returns {Boolean} True if this tar has a VR. */ -dwv.dicom.isTagWithVR = function (group, element) { - return !(group === "0xFFFE" && - (element === "0xE000" || element === "0xE00D" || element === "0xE0DD" )); -}; - +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 /** - * Get the number of bytes occupied by a data element prefix, i.e. without its value. - * @param {String} vr The Value Representation of the element. - * @param {Boolean} isImplicit Does the data use implicit VR? - * WARNING: this is valid for tags with a VR, if not sure use the 'isTagWithVR' function first. - * Reference: - * - [Data Element explicit]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_7.html#table_7.1-1}, - * - [Data Element implicit]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_7.5.html#table_7.5-1}. - * - * | Tag | VR | VL | Value | - * | 4 | 2 | 2 | X | -> 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 + * Check for Tag equality. + * @param {Object} rhs The other tag to compare to. + * @return {Boolean} True if both tags are equal. */ -dwv.dicom.getDataElementPrefixByteSize = function (vr, isImplicit) { - return isImplicit ? 8 : dwv.dicom.is32bitVLVR(vr) ? 12 : 8; +dwv.dicom.Tag.prototype.equals = function (rhs) { + return rhs !== null && + this.getGroup() === rhs.getGroup() && + this.getElement() === rhs.getElement(); }; /** - * DicomParser class. - * @constructor + * 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.DicomParser = function () -{ - /** - * The list of DICOM elements. - * @type Array - */ - this.dicomElements = {}; +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)); +}; - /** - * 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 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 raw DICOM data elements. - * @return {Object} The raw DICOM elements. + * 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.DicomParser.prototype.getRawDicomElements = function () +dwv.dicom.getGroupElementKey = function (group, element) { - return this.dicomElements; + return 'x' + group.substr(2,6) + element.substr(2,6); }; /** - * Get the DICOM data elements. - * @return {Object} The DICOM elements. + * 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.DicomParser.prototype.getDicomElements = function () +dwv.dicom.splitGroupElementKey = function (key) { - return new dwv.dicom.DicomElementsWrapper(this.dicomElements); + return {'group': key.substr(1,4), 'element': key.substr(5,8) }; }; /** - * 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'. + * Get patient orientation label in the reverse direction. + * @param {String} ori Patient Orientation value. + * @return {String} Reverse Orientation Label. */ -dwv.dicom.DicomParser.prototype.readTag = function (reader, offset) +dwv.dicom.getReverseOrientation = function (ori) { - // 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); + 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; nunsigned, 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; + return( syntax === "1.2.840.10008.1.2" || // Implicit VR - Little Endian + syntax === "1.2.840.10008.1.2.1" || // Explicit VR - Little Endian + syntax === "1.2.840.10008.1.2.2" || // Explicit VR - Big Endian + dwv.dicom.isJpegBaselineTransferSyntax(syntax) || // JPEG baseline + dwv.dicom.isJpegLosslessTransferSyntax(syntax) || // JPEG Lossless + dwv.dicom.isJpeg2000TransferSyntax(syntax) ); // JPEG 2000 +}; + +/** + * Get the transfer syntax name. + * Reference: [UID Values]{@link http://dicom.nema.org/dicom/2013/output/chtml/part06/chapter_A.html}. + * @param {String} syntax The transfer syntax. + * @return {String} The name of the transfer syntax. + */ +dwv.dicom.getTransferSyntaxName = function (syntax) +{ + var name = "Unknown"; + // Implicit VR - Little Endian + if( syntax === "1.2.840.10008.1.2" ) { + name = "Little Endian Implicit"; } - // others - else if ( vr === "OB" ) - { - data = reader.readInt8Array( offset, vl ); - offset += vl; + // Explicit VR - Little Endian + else if( syntax === "1.2.840.10008.1.2.1" ) { + name = "Little Endian Explicit"; } - else if ( vr === "OW" ) - { - data = reader.readInt16Array( offset, vl ); - offset += vl; + // Deflated Explicit VR - Little Endian + else if( syntax === "1.2.840.10008.1.2.1.99" ) { + name = "Little Endian Deflated Explicit"; } - else if ( vr === "OF" ) - { - data = reader.readInt32Array( offset, vl ); - offset += vl; + // Explicit VR - Big Endian + else if( syntax === "1.2.840.10008.1.2.2" ) { + name = "Big Endian Explicit"; } - else if ( vr === "OD" ) - { - data = reader.readInt64Array( offset, vl ); - offset += vl; + // JPEG baseline + else if( dwv.dicom.isJpegBaselineTransferSyntax(syntax) ) { + if ( syntax === "1.2.840.10008.1.2.4.50" ) { + name = "JPEG Baseline"; + } + else { // *.51 + name = "JPEG Extended, Process 2+4"; + } } - // numbers - else if( vr === "US") - { - data = reader.readUint16Array( offset, vl ); - offset += vl; + // JPEG Lossless + else if( dwv.dicom.isJpegLosslessTransferSyntax(syntax) ) { + if ( syntax === "1.2.840.10008.1.2.4.57" ) { + name = "JPEG Lossless, Nonhierarchical (Processes 14)"; + } + else { // *.70 + name = "JPEG Lossless, Non-hierarchical, 1st Order Prediction"; + } } - else if( vr === "UL") - { - data = reader.readUint32Array( offset, vl ); - offset += vl; + // Retired JPEG + else if( dwv.dicom.isJpegRetiredTransferSyntax(syntax) ) { + name = "Retired JPEG"; } - else if( vr === "SS") - { - data = reader.readInt16Array( offset, vl ); - offset += vl; + // JPEG-LS + else if( dwv.dicom.isJpeglsTransferSyntax(syntax) ) { + name = "JPEG-LS"; } - else if( vr === "SL") - { - data = reader.readInt32Array( offset, vl ); - offset += vl; + // JPEG 2000 + else if( dwv.dicom.isJpeg2000TransferSyntax(syntax) ) { + if ( syntax === "1.2.840.10008.1.2.4.91" ) { + name = "JPEG 2000 (Lossless or Lossy)"; + } + else { // *.90 + name = "JPEG 2000 (Lossless only)"; + } } - else if( vr === "FL") - { - data = reader.readFloat32Array( offset, vl ); - offset += vl; + // MPEG2 Image Compression + else if( syntax === "1.2.840.10008.1.2.4.100" ) { + name = "MPEG2"; } - else if( vr === "FD") - { - data = reader.readFloat64Array( offset, vl ); - offset += vl; + // RLE (lossless) + else if( syntax === "1.2.840.10008.1.2.5" ) { + name = "RLE"; } - // 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); + // return + return name; +}; + +/** + * Get the appropriate TypedArray in function of arguments. + * @param {Number} bitsAllocated The number of bites used to store the data: [8, 16, 32]. + * @param {Number} pixelRepresentation The pixel representation, 0:unsigned;1:signed. + * @param {Size} size The size of the new array. + * @return The good typed array. + */ +dwv.dicom.getTypedArray = function (bitsAllocated, pixelRepresentation, size) +{ + var res = null; + if (bitsAllocated === 8) { + if (pixelRepresentation === 0) { + res = new Uint8Array(size); + } + else { + res = new Int8Array(size); } } - // 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; - } - } + else if (bitsAllocated === 16) { + if (pixelRepresentation === 0) { + res = new Uint16Array(size); } - // 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 ); - } - } + res = new Int16Array(size); } } - // 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 ); + else if (bitsAllocated === 32) { + if (pixelRepresentation === 0) { + res = new Uint32Array(size); + } + else { + res = new Int32Array(size); } - offset += vl; - data = data.split("\\"); } + return res; +}; + +/** + * Does this Value Representation (VR) have a 32bit Value Length (VL). + * Ref: [Data Element explicit]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_7.html#table_7.1-1}. + * @param {String} vr The data Value Representation (VR). + * @returns {Boolean} True if this VR has a 32-bit VL. + */ +dwv.dicom.is32bitVLVR = function (vr) +{ + // added locally used 'ox' + return ( vr === "OB" || vr === "OW" || vr === "OF" || vr === "ox" || vr === "UT" || + vr === "SQ" || vr === "UN" ); +}; + +/** + * Does this tag have a VR. + * Basically the Item, ItemDelimitationItem and SequenceDelimitationItem tags. + * @param {String} group The tag group. + * @param {String} element The tag element. + * @returns {Boolean} True if this tar has a VR. + */ +dwv.dicom.isTagWithVR = function (group, element) { + return !(group === "0xFFFE" && + (element === "0xE000" || element === "0xE00D" || element === "0xE0DD" )); +}; + + +/** + * Get the number of bytes occupied by a data element prefix, i.e. without its value. + * @param {String} vr The Value Representation of the element. + * @param {Boolean} isImplicit Does the data use implicit VR? + * WARNING: this is valid for tags with a VR, if not sure use the 'isTagWithVR' function first. + * Reference: + * - [Data Element explicit]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_7.html#table_7.1-1}, + * - [Data Element implicit]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_7.5.html#table_7.5-1}. + * + * | Tag | VR | VL | Value | + * | 4 | 2 | 2 | X | -> 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 { - 'tag': tag, - 'vr': vr, - 'vl': vlString, - 'value': data, - 'startOffset': startOffset, - 'endOffset': offset - }; + 'group': group, + 'element': element, + 'name': name, + '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. + * 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.parse = function (buffer) +dwv.dicom.DicomParser.prototype.readItemDataElement = function (reader, offset, implicit) { - 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)+")"); - } + var itemData = {}; - // Implicit VR - if (dwv.dicom.isImplicitTransferSyntax(syntax)) { - implicit = true; - } + // read the first item + var item = this.readDataElement(reader, offset, implicit); + offset = item.endOffset; - // Big Endian - if (dwv.dicom.isBigEndianTransferSyntax(syntax)) { - dataReader = new dwv.dicom.DataReader(buffer,false); + // exit if it is a sequence delimitation item + var isSeqDelim = ( item.tag.name === "xFFFEE0DD" ); + if (isSeqDelim) { + return { + data: itemData, + endOffset: item.endOffset, + isSeqDelim: isSeqDelim }; } - // default character set - if (typeof this.getDefaultCharacterSet() !== "undefined") { - dataReader.setUtfLabel(this.getDefaultCharacterSet()); - } + // store it + itemData[item.tag.name] = item; - // 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]); + // 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; } - else { - charSetTerm = dwv.dicom.cleanString(dataElement.value[1]); - console.warn("Unsupported character set with code extensions: '"+charSetTerm+"'."); + } + } + // 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; } - 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); - } + return { + 'data': itemData, + 'endOffset': offset, + 'isSeqDelim': false }; +}; - // pixel buffer - if (typeof this.dicomElements.x7FE00010 !== "undefined") { +/** + * 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 = []; - var numberOfFrames = 1; - if (typeof this.dicomElements.x00280008 !== "undefined") { - numberOfFrames = this.dicomElements.x00280008.value[0]; + // 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); } + } - 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..."); - } + return { + 'data': itemData, + 'endOffset': offset, + 'offsetTableVl': offsetTableVl }; +}; - // 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 ) { +/** + * 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; - // 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; + // 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; + } -/** - * DicomElements wrapper. - * @constructor - * @param {Array} dicomElements The elements to wrap. - */ -dwv.dicom.DicomElementsWrapper = function (dicomElements) { + // 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; + } - /** - * 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]; - }; + // check the value of VL + var vlString = vl; + if( vl === 0xffffffff ) { + vlString = "u/l"; + vl = 0; + } - /** - * 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 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."); } - 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]; + 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 { - value = dElement.value; + data = reader.readInt8Array( offset, vl ); } } - 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 if ( bitsAllocated === 16 ) { + if (pixelRepresentation === 0) { + data = reader.readUint16Array( offset, vl ); } else { - row.name = "Unknown Tag & Data"; + data = reader.readInt16Array( offset, vl ); } - // 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"; + else if ( bitsAllocated === 32 ) { + if (pixelRepresentation === 0) { + data = reader.readUint32Array( offset, vl ); + } + else { + data = reader.readInt32Array( offset, vl ); + } } - 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; + else if ( bitsAllocated === 64 ) { + if (pixelRepresentation === 0) { + data = reader.readUint64Array( offset, vl ); } - result += this.getElementAsString(dicomElement) + "\n"; + 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); } - 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; + // not available + else if( vr === "UN") + { + data = reader.readUint8Array( offset, vl ); + offset += vl; } - - // 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; + // 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; } - 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; + } + // 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 ); + } } } } - return str; -}; + // 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("\\"); + } -/** - * 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) ); + // return + return { + 'tag': tag, + 'vr': vr, + 'vl': vlString, + 'value': data, + 'startOffset': startOffset, + 'endOffset': offset + }; }; /** - * Get a data element as a string. - * @param {Object} dicomElement The DICOM element. - * @param {String} prefix A string to prepend this one. + * 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.DicomElementsWrapper.prototype.getElementAsString = function ( dicomElement, prefix ) +dwv.dicom.DicomParser.prototype.parse = function (buffer) { - // 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; + var offset = 0; + var implicit = false; + // default readers + var metaReader = new dwv.dicom.DataReader(buffer); + var dataReader = new dwv.dicom.DataReader(buffer); - // (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 += "]"; - } + // 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)"); } - // align # - var nSpaces = 55 - line.length; - if ( nSpaces > 0 ) { - for ( var s = 0; s < nSpaces; ++s ) { - line += " "; - } - } - line += " # "; - if ( dicomElement.vl < 100 ) { - line += " "; + // 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; } - if ( dicomElement.vl < 10 ) { - line += " "; + + // check the TransferSyntaxUID (has to be there!) + if (typeof this.dicomElements.x00020010 === "undefined") + { + throw new Error("Not a valid DICOM file (no TransferSyntaxUID found)"); } - line += dicomElement.vl; - line += ", "; - line += deSize; //dictElement[1]; - line += " "; - if ( dictElement !== null ) { - line += dictElement[2]; + 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)+")"); } - else { - line += "Unknown Tag & Data"; + + // Implicit VR + if (dwv.dicom.isImplicitTransferSyntax(syntax)) { + implicit = true; } - var message = null; + // Big Endian + if (dwv.dicom.isBigEndianTransferSyntax(syntax)) { + dataReader = new dwv.dicom.DataReader(buffer,false); + } - // 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; - } + // default character set + if (typeof this.getDefaultCharacterSet() !== "undefined") { + dataReader.setUtfLabel(this.getDefaultCharacterSet()); + } - // get the item element - var itemElement = item.xFFFEE000; - message = "(Item with"; - if ( itemElement.vl === "u/l" ) { - message += " undefined"; + // 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 { - message += " explicit"; + charSetTerm = dwv.dicom.cleanString(dataElement.value[1]); + console.warn("Unsupported character set with code extensions: '"+charSetTerm+"'."); } - message += " length #="+(itemKeys.length - 1)+")"; - itemElement.value = [message]; - itemElement.vr = "na"; + dataReader.setUtfLabel(dwv.dicom.getUtfLabel(charSetTerm)); + } + // increment offset + offset = dataElement.endOffset; + // store the data element + this.dicomElements[dataElement.tag.name] = dataElement; + } - line += "\n"; - line += this.getElementAsString(itemElement, prefix + " "); + // safety check... + if (buffer.byteLength !== offset) { + console.warn("Did not reach the end of the buffer: "+ + offset+" != "+buffer.byteLength); + } - for ( var m = 0, lenm = itemKeys.length; m < lenm; ++m ) { - if ( itemKeys[m] !== "xFFFEE000" ) { - line += "\n"; - line += this.getElementAsString(item[itemKeys[m]], prefix + " "); - } - } + // pixel buffer + if (typeof this.dicomElements.x7FE00010 !== "undefined") { - message = "(ItemDelimitationItem"; - if ( itemElement.vl !== "u/l" ) { - message += " for re-encoding"; + 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..."); } - message += ")"; - var itemDelimElement = { - "tag": { "group": "0xFFFE", "element": "0xE00D" }, - "vr": "na", - "vl": "0", - "value": [message] - }; - line += "\n"; - line += this.getElementAsString(itemDelimElement, prefix + " "); + // 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 ) { - 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 + " "); + // 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; + } } - - 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; }; // namespaces @@ -5475,6 +5563,11 @@ dwv.dicom.isImplicitLengthPixels = function (element) { 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); @@ -14445,6 +14538,16 @@ dwv.image.Geometry = function ( origin, size, spacing, orientation ) }; +/** + * Get a string representation of the Vector3D. + * @return {String} The vector as a string. + */ +dwv.image.Geometry.prototype.toString = function () { + return "Origin: " + this.getOrigin() + + ", Size: " + this.getSize() + + ", Spacing: " + this.getSpacing(); +}; + /** * Check for equality. * @param {Geometry} rhs The object to compare to. @@ -14452,9 +14555,9 @@ dwv.image.Geometry = function ( origin, size, spacing, orientation ) */ dwv.image.Geometry.prototype.equals = function (rhs) { return rhs !== null && - this.getOrigin() === rhs.getOrigin() && - this.getSize() === rhs.getSize() && - this.getSpacing() === rhs.getSpacing(); + this.getOrigin().equals( rhs.getOrigin() ) && + this.getSize().equals( rhs.getSize() ) && + this.getSpacing().equals( rhs.getSpacing() ); }; /** @@ -14835,6 +14938,9 @@ dwv.image.Image = function(geometry, buffer, numberOfFrames) if( size.getNumberOfRows() !== rhsSize.getNumberOfRows() ) { throw new Error("Cannot append a slice with different number of rows"); } + if( !geometry.getOrientation().equals( rhs.getGeometry().getOrientation() ) ) { + throw new Error("Cannot append a slice with different orientation"); + } if( photometricInterpretation !== rhs.getPhotometricInterpretation() ) { throw new Error("Cannot append a slice with different photometric interpretation"); } @@ -15328,10 +15434,10 @@ dwv.image.Image.prototype.quantifyRect = function(rect) } } var quantif = dwv.math.getStats( subBuffer ); - quant.min = {"value": quantif.min, "unit": ""}; - quant.max = {"value": quantif.max, "unit": ""}; - quant.mean = {"value": quantif.mean, "unit": ""}; - quant.stdDev = {"value": quantif.stdDev, "unit": ""}; + quant.min = {"value": quantif.getMin(), "unit": ""}; + quant.max = {"value": quantif.getMax(), "unit": ""}; + quant.mean = {"value": quantif.getMean(), "unit": ""}; + quant.stdDev = {"value": quantif.getStdDev(), "unit": ""}; // return return quant; }; @@ -15501,6 +15607,13 @@ dwv.image.ImageFactory.prototype.create = function (dicomElements, pixelBuffer) if ( pixelRepresentation ) { meta.IsSigned = (pixelRepresentation === 1); } + + // RecommendedDisplayFrameRate + var recommendedDisplayFrameRate = dicomElements.getFromKey("x00082144"); + if ( recommendedDisplayFrameRate ) { + meta.RecommendedDisplayFrameRate = parseInt(recommendedDisplayFrameRate, 10); + } + image.setMeta(meta); // overlay @@ -15517,6 +15630,7 @@ dwv.image.lut = dwv.image.lut || {}; /** * Rescale LUT class. + * Typically converts from integer to float. * @constructor * @param {Object} rsi The rescale slope and intercept. * @param {Number} bitsStored The number of bits used to store the data. @@ -15526,7 +15640,7 @@ dwv.image.lut.Rescale = function (rsi, bitsStored) /** * The internal array. * @private - * @type Array + * @type Float32Array */ var lut = null; @@ -15546,7 +15660,7 @@ dwv.image.lut.Rescale = function (rsi, bitsStored) /** * Get the Rescale Slope and Intercept (RSI). - * @return {Object} The rescale slope and intercept. + * @return {Object} The rescale slope and intercept object. */ this.getRSI = function () { return rsi; }; @@ -15583,7 +15697,8 @@ dwv.image.lut.Rescale = function (rsi, bitsStored) /** * Get the value of the LUT at the given offset. - * @return {Number} The value of the LUT at the given offset. + * @param {Number} offset The input offset in [0,2^bitsStored] range. + * @return {Number} The float32 value of the LUT at the given offset. */ this.getValue = function (offset) { @@ -15593,6 +15708,7 @@ dwv.image.lut.Rescale = function (rsi, bitsStored) /** * Window LUT class. + * Typically converts from float to integer. * @constructor * @param {Number} rescaleLut The associated rescale LUT. * @param {Boolean} isSigned Flag to know if the data is signed or not. @@ -15602,7 +15718,7 @@ dwv.image.lut.Window = function (rescaleLut, isSigned) /** * The internal array: Uint8ClampedArray clamps between 0 and 255. * @private - * @type Array + * @type Uint8ClampedArray */ var lut = null; @@ -15709,7 +15825,8 @@ dwv.image.lut.Window = function (rescaleLut, isSigned) /** * Get the value of the LUT at the given offset. - * @return {Number} The value of the LUT at the given offset. + * @param {Number} offset The input offset in [0,2^bitsStored] range. + * @return {Number} The integer value (default [0,255]) of the LUT at the given offset. */ this.getValue = function (offset) { @@ -15998,7 +16115,7 @@ dwv.image.WindowLevel = function (center, width) /** * Apply the window level on an input value. - * @param {Number} The value to rescale as an integer. + * @param {Number} value The value to rescale as an integer. * @return {Number} The leveled value, in the * [ymin, ymax] range (default [0,255]). */ @@ -16095,6 +16212,20 @@ dwv.image.View = function (image) */ this.setImage = function(inImage) { image = inImage; }; + /** + * Get the milliseconds per frame from frame rate. + * @return {Number} The milliseconds per frame. + */ + + this.getPlaybackMilliseconds = function(recommendedDisplayFrameRate) { + if ( !recommendedDisplayFrameRate ){ + // Default to 10 FPS if none is found in the meta + recommendedDisplayFrameRate = 10; + } + // round milliseconds per frame to nearest whole number + return Math.round(1000 / recommendedDisplayFrameRate); + }; + /** * Get the window LUT of the image. * Warning: can be undefined in no window/level was set. @@ -16514,8 +16645,7 @@ dwv.image.View.prototype.generateImageData = function( array ) var iMax = sliceOffset + sliceSize; for(var i=sliceOffset; i < iMax; ++i) { - pxValue = parseInt( windowLut.getValue( - image.getValueAtOffset(i, frame) ), 10 ); + pxValue = windowLut.getValue(image.getValueAtOffset(i, frame) ); array.data[index] = colourMap.red[pxValue]; array.data[index+1] = colourMap.green[pxValue]; array.data[index+2] = colourMap.blue[pxValue]; @@ -16547,12 +16677,9 @@ dwv.image.View.prototype.generateImageData = function( array ) for(var j=0; j < sliceSize; ++j) { - array.data[index] = parseInt( windowLut.getValue( - image.getValueAtOffset(posR, frame) ), 10 ); - array.data[index+1] = parseInt( windowLut.getValue( - image.getValueAtOffset(posG, frame) ), 10 ); - array.data[index+2] = parseInt( windowLut.getValue( - image.getValueAtOffset(posB, frame) ), 10 ); + array.data[index] = windowLut.getValue(image.getValueAtOffset(posR, frame) ); + array.data[index+1] = windowLut.getValue(image.getValueAtOffset(posG, frame) ); + array.data[index+2] = windowLut.getValue(image.getValueAtOffset(posB, frame) ); array.data[index+3] = 0xff; index += 4; @@ -16600,9 +16727,9 @@ dwv.image.View.prototype.generateImageData = function( array ) g = y - 0.34414 * (cb - 128) - 0.71414 * (cr - 128); b = y + 1.772 * (cb - 128); - array.data[index] = parseInt( windowLut.getValue(r), 10 ); - array.data[index+1] = parseInt( windowLut.getValue(g), 10 ); - array.data[index+2] = parseInt( windowLut.getValue(b), 10 ); + array.data[index] = windowLut.getValue(r); + array.data[index+1] = windowLut.getValue(g); + array.data[index+2] = windowLut.getValue(b); array.data[index+3] = 0xff; index += 4; @@ -16745,6 +16872,7 @@ dwv.io = dwv.io || {}; /** * DICOM data loader. + * @constructor */ dwv.io.DicomDataLoader = function () { @@ -16883,6 +17011,9 @@ dwv.io.DicomDataLoader.prototype.canLoadFile = function (file) { /** * Check if the loader can load the provided url. + * True if: + * - the url has a 'contentType' and it is 'application/dicom' (as in wado urls) + * - the url has no 'contentType' and no extension or the extension is 'dcm' * @param {String} url The url to check. * @return True if the url can be loaded. */ @@ -16894,9 +17025,10 @@ dwv.io.DicomDataLoader.prototype.canLoadUrl = function (url) { } var hasExt = (ext.length !== 0) && (ext.length < 5); // wado url - var isDicomContentType = (url.indexOf("contentType=application/dicom") !== -1); + var hasContentType = (url.indexOf("&contentType") !== -1); + var isDicomContentType = (url.indexOf("&contentType=application/dicom") !== -1); - return isDicomContentType || (ext === "dcm") || !hasExt; + return hasContentType ? isDicomContentType : !hasExt || (ext === "dcm"); }; /** @@ -17239,6 +17371,7 @@ dwv.io = dwv.io || {}; /** * JSON text loader. + * @constructor */ dwv.io.JSONTextLoader = function () { @@ -17618,6 +17751,7 @@ dwv.io = dwv.io || {}; /** * Raw image loader. + * @constructor */ dwv.io.RawImageLoader = function () { @@ -17646,20 +17780,14 @@ dwv.io.RawImageLoader = function () * @param {String} dataType The data type. */ function createDataUri(response, dataType) { - // image data as string - var bytes = new Uint8Array(response); - var imageDataStr = ''; - for( var i = 0; i < bytes.byteLength; ++i ) { - imageDataStr += String.fromCharCode(bytes[i]); - } // image type var imageType = dataType; - if (imageType === "jpg") { + if (!imageType || imageType === "jpg") { imageType = "jpeg"; } // create uri - var uri = "data:image/" + imageType + ";base64," + window.btoa(imageDataStr); - return uri; + var file = new Blob([response], {type: 'image/' + imageType}); + return window.URL.createObjectURL(file); } /** @@ -17671,10 +17799,6 @@ dwv.io.RawImageLoader = function () this.load = function ( dataUri, origin, index ) { // create a DOM image var image = new Image(); - image.src = dataUri; - // storing values to pass them on - image.origin = origin; - image.index = index; // triggered by ctx.drawImage image.onload = function (/*event*/) { try { @@ -17686,6 +17810,10 @@ dwv.io.RawImageLoader = function () self.onprogress({'type': 'read-progress', 'lengthComputable': true, 'loaded': 100, 'total': 100, 'index': index}); }; + // storing values to pass them on + image.origin = origin; + image.index = index; + image.src = dataUri; }; /** @@ -17821,6 +17949,7 @@ dwv.io = dwv.io || {}; * Raw video loader. * url example (cors enabled): * https://raw.githubusercontent.com/clappr/clappr/master/test/fixtures/SampleVideo_360x240_1mb.mp4 + * @constructor */ dwv.io.RawVideoLoader = function () { @@ -18188,38 +18317,6 @@ dwv.io.UrlsLoader.prototype.onabort = function (/*event*/) {}; */ dwv.io.UrlsLoader.prototype.load = function (ioArray, options) { - // clear storage - this.clearStoredRequests(); - this.clearStoredLoader(); - - // closure to self for handlers - var self = this; - // set the number of data to load - this.setNToLoad( ioArray.length ); - - var mproghandler = new dwv.utils.MultiProgressHandler(self.onprogress); - mproghandler.setNToLoad( ioArray.length ); - - // get loaders - var loaders = []; - for (var m = 0; m < dwv.io.loaderList.length; ++m) { - loaders.push( new dwv.io[dwv.io.loaderList[m]]() ); - } - - // set loaders callbacks - var loader = null; - for (var k = 0; k < loaders.length; ++k) { - loader = loaders[k]; - loader.onload = self.onload; - loader.onloadend = self.addLoaded; - loader.onerror = self.onerror; - loader.onabort = self.onabort; - loader.setOptions({ - 'defaultCharacterSet': this.getDefaultCharacterSet() - }); - loader.onprogress = mproghandler.getUndefinedMonoProgressHandler(1); - } - // request onerror handler var getRequestOnError = function (origin) { return function (/*event*/) { @@ -18239,57 +18336,125 @@ dwv.io.UrlsLoader.prototype.load = function (ioArray, options) }; }; - // loop on I/O elements - for (var i = 0; i < ioArray.length; ++i) - { - var url = ioArray[i]; - var request = new XMLHttpRequest(); - request.open('GET', url, true); + // clear storage + this.clearStoredRequests(); + this.clearStoredLoader(); + + // closure to self for handlers + var self = this; - // store request - this.storeRequest(request); + // raw url load + var internalUrlsLoad = function (urlsArray) { + // set the number of data to load + self.setNToLoad( urlsArray.length ); - // optional request headers - if ( typeof options.requestHeaders !== "undefined" ) { - var requestHeaders = options.requestHeaders; - for (var j = 0; j < requestHeaders.length; ++j) { - if ( typeof requestHeaders[j].name !== "undefined" && - typeof requestHeaders[j].value !== "undefined" ) { - request.setRequestHeader(requestHeaders[j].name, requestHeaders[j].value); - } - } + var mproghandler = new dwv.utils.MultiProgressHandler(self.onprogress); + mproghandler.setNToLoad( urlsArray.length ); + + // get loaders + var loaders = []; + for (var m = 0; m < dwv.io.loaderList.length; ++m) { + loaders.push( new dwv.io[dwv.io.loaderList[m]]() ); } - // bind reader progress - request.onprogress = mproghandler.getMonoProgressHandler(i, 0); - request.onloadend = mproghandler.getMonoOnLoadEndHandler(i, 0); + // set loaders callbacks + var loader = null; + for (var k = 0; k < loaders.length; ++k) { + loader = loaders[k]; + loader.onload = self.onload; + loader.onloadend = self.addLoaded; + loader.onerror = self.onerror; + loader.onabort = self.onabort; + loader.setOptions({ + 'defaultCharacterSet': self.getDefaultCharacterSet() + }); + loader.onprogress = mproghandler.getUndefinedMonoProgressHandler(1); + } - // find a loader - var foundLoader = false; - for (var l = 0; l < loaders.length; ++l) { - loader = loaders[l]; - if (loader.canLoadUrl(url)) { - foundLoader = true; - // store loader - this.storeLoader(loader); - // set reader callbacks - request.onload = loader.getUrlLoadHandler(url, i); - request.onerror = getRequestOnError(url); - request.onabort = getRequestOnAbort(url); - // response type (default is 'text') - if (loader.loadUrlAs() === dwv.io.urlContentTypes.ArrayBuffer) { - request.responseType = "arraybuffer"; + // loop on I/O elements + for (var i = 0; i < urlsArray.length; ++i) + { + var url = urlsArray[i]; + var request = new XMLHttpRequest(); + request.open('GET', url, true); + + // store request + self.storeRequest(request); + + // optional request headers + if ( typeof options.requestHeaders !== "undefined" ) { + var requestHeaders = options.requestHeaders; + for (var j = 0; j < requestHeaders.length; ++j) { + if ( typeof requestHeaders[j].name !== "undefined" && + typeof requestHeaders[j].value !== "undefined" ) { + request.setRequestHeader(requestHeaders[j].name, requestHeaders[j].value); + } } - // read - request.send(null); - // next file - break; + } + + // bind reader progress + request.onprogress = mproghandler.getMonoProgressHandler(i, 0); + request.onloadend = mproghandler.getMonoOnLoadEndHandler(i, 0); + + // find a loader + var foundLoader = false; + for (var l = 0; l < loaders.length; ++l) { + loader = loaders[l]; + if (loader.canLoadUrl(url)) { + foundLoader = true; + // store loader + self.storeLoader(loader); + // set reader callbacks + request.onload = loader.getUrlLoadHandler(url, i); + request.onerror = getRequestOnError(url); + request.onabort = getRequestOnAbort(url); + // response type (default is 'text') + if (loader.loadUrlAs() === dwv.io.urlContentTypes.ArrayBuffer) { + request.responseType = "arraybuffer"; + } + // read + request.send(null); + // next file + break; + } + } + // TODO: throw? + if (!foundLoader) { + throw new Error("No loader found for url: "+url); } } - // TODO: throw? - if (!foundLoader) { - throw new Error("No loader found for url: "+url); - } + }; + + // DICOMDIR load + var internalDicomDirLoad = function (dicomDirUrl) { + console.log("UrlsLoader: using a DICOMDIR."); + // read DICOMDIR + var dirRequest = new XMLHttpRequest(); + dirRequest.open('GET', dicomDirUrl, true); + dirRequest.responseType = "arraybuffer"; + dirRequest.onload = function (/*event*/) { + // get the file list + var list = dwv.dicom.getFileListFromDicomDir( this.response ); + // use the first list + var urls = list[0][0]; + // append root url + var rootUrl = dicomDirUrl.substr( 0, (dicomDirUrl.length - "DICOMDIR".length - 1 ) ); + var fullUrls = []; + for ( var i = 0; i < urls.length; ++i ) { + fullUrls.push( rootUrl + "/" + urls[i]); + } + internalUrlsLoad(fullUrls); + }; + dirRequest.onerror = getRequestOnError(dicomDirUrl); + dirRequest.onabort = getRequestOnAbort(dicomDirUrl); + dirRequest.send(null); + }; + + // check if DICOMDIR case + if ( ioArray.length === 1 && dwv.utils.endsWith(ioArray[0], "DICOMDIR") ) { + internalDicomDirLoad(ioArray[0]); + } else { + internalUrlsLoad(ioArray); } }; @@ -18301,6 +18466,7 @@ var JSZip = JSZip || {}; /** * ZIP data loader. + * @constructor */ dwv.io.ZipLoader = function () { @@ -20009,6 +20175,70 @@ dwv.math.Path.prototype.appenPath = function(other) { var dwv = dwv || {}; dwv.math = dwv.math || {}; +/** + * Basic statistics + * @constructor + * @param {Number} min The minimum value. + * @param {Number} max The maximum value. + * @param {Number} mean The mean value. + * @param {Number} stdDev The standard deviation. + */ +dwv.math.Stats = function ( min, max, mean, stdDev ) { + /** + * Get the minimum value. + * @return {Number} The minimum value. + */ + this.getMin = function () { + return min; + }; + /** + * Get the maximum value. + * @return {Number} The maximum value. + */ + this.getMax = function () { + return max; + }; + /** + * Get the mean value. + * @return {Number} The mean value. + */ + this.getMean = function () { + return mean; + }; + /** + * Get the standard deviation. + * @return {Number} The standard deviation. + */ + this.getStdDev = function () { + return stdDev; + }; +}; + +/** + * Check for Stats equality. + * @param {Object} rhs The other Stats object to compare to. + * @return {Boolean} True if both Stats object are equal. + */ +dwv.math.Stats.prototype.equals = function (rhs) { + return rhs !== null && + this.getMin() === rhs.getMin() && + this.getMax() === rhs.getMax() && + this.getMean() === rhs.getMean() && + this.getStdDev() === rhs.getStdDev(); +}; + +/** + * Get the stats as an object + * @return {Object} An object representation of the stats. + */ +dwv.math.Stats.prototype.asObject = function () { + return { 'min': this.getMin(), + 'max': this.getMax(), + 'mean': this.getMean(), + 'stdDev': this.getStdDev() + }; +}; + /** * Get the minimum, maximum, mean and standard deviation * of an array of values. @@ -20042,7 +20272,7 @@ dwv.math.getStats = function (array) variance = sumSqr / array.length - mean * mean; stdDev = Math.sqrt(variance); - return { 'min': min, 'max': max, 'mean': mean, 'stdDev': stdDev }; + return new dwv.math.Stats( min, max, mean, stdDev ); }; /** @@ -20202,7 +20432,7 @@ dwv.tool.ArrowFactory.prototype.create = function (points, style/*, image*/) var verticalLine = new dwv.math.Line(line.getBegin(), beginTy); var angle = dwv.math.getAngle(line, verticalLine); var angleRad = angle * Math.PI / 180; - var radius = 5; + var radius = 5 * style.getScaledStrokeWidth(); var kpoly = new Konva.RegularPolygon({ x: line.getBegin().getX() + radius * Math.sin(angleRad), y: line.getBegin().getY() + radius * Math.cos(angleRad), @@ -20386,6 +20616,13 @@ dwv.tool.Draw = function (app, shapeFactoryList) * @type Object */ this.shapeFactoryList = shapeFactoryList; + + /** + * Current shape factory. + * @type Object + */ + var currentFactory = null; + /** * Draw command. * @private @@ -20447,6 +20684,8 @@ dwv.tool.Draw = function (app, shapeFactoryList) points: [10, -10, -10, 10 ], stroke: 'red' }); + trash.width(20); + trash.height(20); trash.add(trashLine1); trash.add(trashLine2); @@ -20474,6 +20713,11 @@ dwv.tool.Draw = function (app, shapeFactoryList) * @param {Object} event The mouse down event. */ this.mousedown = function(event){ + // exit if a draw was started (handle at mouse move or up) + if ( started ) { + return; + } + // determine if the click happened in an existing shape var stage = app.getDrawStage(); var kshape = stage.getIntersection({ @@ -20500,6 +20744,8 @@ dwv.tool.Draw = function (app, shapeFactoryList) shapeEditor.setImage(null); // start storing points started = true; + // set factory + currentFactory = new self.shapeFactoryList[self.shapeName](); // clear array points = []; // store point @@ -20513,42 +20759,29 @@ dwv.tool.Draw = function (app, shapeFactoryList) * @param {Object} event The mouse move event. */ this.mousemove = function(event){ - if (!started) - { + // exit if not started draw + if ( !started ) { return; } + + // draw line to current pos if ( Math.abs( event._x - lastPoint.getX() ) > 0 || Math.abs( event._y - lastPoint.getY() ) > 0 ) { - // current point - lastPoint = new dwv.math.Point2D(event._x, event._y); // clear last added point from the list (but not the first one) - if ( points.length != 1 ) { - points.pop(); + // if it was marked as temporary + if ( points.length != 1 && + typeof points[points.length-1].tmp !== "undefined" ) { + points.pop(); } - // add current one to the list + // current point + lastPoint = new dwv.math.Point2D(event._x, event._y); + // mark it as temporary + lastPoint.tmp = true; + // add it to the list points.push( lastPoint ); - // allow for anchor points - var factory = new self.shapeFactoryList[self.shapeName](); - if( points.length < factory.getNPoints() ) { - clearTimeout(this.timer); - this.timer = setTimeout( function () { - points.push( lastPoint ); - }, factory.getTimeout() ); - } - // remove previous draw - if ( tmpShapeGroup ) { - tmpShapeGroup.destroy(); - } - // create shape group - tmpShapeGroup = factory.create(points, self.style, app.getImage()); - // do not listen during creation - var shape = tmpShapeGroup.getChildren( dwv.draw.isNodeNameShape )[0]; - shape.listening(false); - drawLayer.hitGraphEnabled(false); - // draw shape - drawLayer.add(tmpShapeGroup); - drawLayer.draw(); + // update points + onNewPoints(points); } }; @@ -20556,37 +20789,48 @@ dwv.tool.Draw = function (app, shapeFactoryList) * Handle mouse up event. * @param {Object} event The mouse up event. */ - this.mouseup = function (/*event*/){ - if (started && points.length > 1 ) - { - // reset shape group - if ( tmpShapeGroup ) { - tmpShapeGroup.destroy(); - } - // create final shape - var factory = new self.shapeFactoryList[self.shapeName](); - var finalShapeGroup = factory.create(points, self.style, app.getImage()); - finalShapeGroup.id( dwv.math.guid() ); - - // get the position group - var posGroup = app.getDrawController().getCurrentPosGroup(); - // add shape group to position group - posGroup.add(finalShapeGroup); + this.mouseup = function (/*event*/) { + // exit if not started draw + if ( !started ) { + return; + } + // exit if no points + if ( points.length === 0 ) { + console.warn("Draw mouseup but no points..."); + return; + } - // re-activate layer - drawLayer.hitGraphEnabled(true); - // draw shape command - command = new dwv.tool.DrawGroupCommand(finalShapeGroup, self.shapeName, drawLayer); - command.onExecute = fireEvent; - command.onUndo = fireEvent; - // execute it - command.execute(); - // save it in undo stack - app.addToUndoStack(command); + // do we have all the needed points + if ( points.length === currentFactory.getNPoints() ) { + // store points + onFinalPoints(points); + // reset flag + started = false; + } else { + // remove temporary flag + if ( typeof points[points.length-1].tmp !== "undefined" ) { + delete points[points.length-1].tmp; + } + } + }; - // activate shape listeners - self.setShapeOn( finalShapeGroup ); + /** + * Handle mouse up event. + * @param {Object} event The mouse up event. + */ + this.dblclick = function (/*event*/){ + // exit if not started draw + if ( !started ) { + return; } + // exit if no points + if ( points.length === 0 ) { + console.warn("Draw dblclick but no points..."); + return; + } + + // store points + onFinalPoints(points); // reset flag started = false; }; @@ -20612,7 +20856,32 @@ dwv.tool.Draw = function (app, shapeFactoryList) * @param {Object} event The touch move event. */ this.touchmove = function(event){ - self.mousemove(event); + // exit if not started draw + if ( !started ) { + return; + } + + if ( Math.abs( event._x - lastPoint.getX() ) > 0 || + Math.abs( event._y - lastPoint.getY() ) > 0 ) + { + // clear last added point from the list (but not the first one) + if ( points.length != 1 ) { + points.pop(); + } + // current point + lastPoint = new dwv.math.Point2D(event._x, event._y); + // add current one to the list + points.push( lastPoint ); + // allow for anchor points + if( points.length < currentFactory.getNPoints() ) { + clearTimeout(this.timer); + this.timer = setTimeout( function () { + points.push( lastPoint ); + }, currentFactory.getTimeout() ); + } + // update points + onNewPoints(points); + } }; /** @@ -20620,7 +20889,7 @@ dwv.tool.Draw = function (app, shapeFactoryList) * @param {Object} event The touch end event. */ this.touchend = function(event){ - self.mouseup(event); + self.dblclick(event); }; /** @@ -20629,8 +20898,78 @@ dwv.tool.Draw = function (app, shapeFactoryList) */ this.keydown = function(event){ app.onKeydown(event); + + // press delete key + if (event.keyCode === 46 && shapeEditor.isActive()) { + // get shape + var shapeGroup = shapeEditor.getShape().getParent(); + var shapeDisplayName = dwv.tool.GetShapeDisplayName( + shapeGroup.getChildren( dwv.draw.isNodeNameShape )[0]); + // delete command + var delcmd = new dwv.tool.DeleteGroupCommand(shapeGroup, + shapeDisplayName, drawLayer); + delcmd.onExecute = fireEvent; + delcmd.onUndo = fireEvent; + delcmd.execute(); + app.addToUndoStack(delcmd); + } }; + /** + * Update the current draw with new points. + * @param {Array} tmpPoints The array of new points. + */ + function onNewPoints( tmpPoints ) + { + // remove temporary shape draw + if ( tmpShapeGroup ) { + tmpShapeGroup.destroy(); + } + // create shape group + tmpShapeGroup = currentFactory.create(tmpPoints, self.style, app.getImage()); + // do not listen during creation + var shape = tmpShapeGroup.getChildren( dwv.draw.isNodeNameShape )[0]; + shape.listening(false); + drawLayer.hitGraphEnabled(false); + // draw shape + drawLayer.add(tmpShapeGroup); + drawLayer.draw(); + } + + /** + * Create the final shape from a point list. + * @param {Array} finalPoints The array of points. + */ + function onFinalPoints( finalPoints ) + { + // reset temporary shape group + if ( tmpShapeGroup ) { + tmpShapeGroup.destroy(); + } + // create final shape + var finalShapeGroup = currentFactory.create(finalPoints, self.style, app.getImage()); + finalShapeGroup.id( dwv.math.guid() ); + + // get the position group + var posGroup = app.getDrawController().getCurrentPosGroup(); + // add shape group to position group + posGroup.add(finalShapeGroup); + + // re-activate layer + drawLayer.hitGraphEnabled(true); + // draw shape command + command = new dwv.tool.DrawGroupCommand(finalShapeGroup, self.shapeName, drawLayer); + command.onExecute = fireEvent; + command.onUndo = fireEvent; + // execute it + command.execute(); + // save it in undo stack + app.addToUndoStack(command); + + // activate shape listeners + self.setShapeOn( finalShapeGroup ); + } + /** * Setup the tool GUI. */ @@ -20764,8 +21103,8 @@ dwv.tool.Draw = function (app, shapeFactoryList) var stage = app.getDrawStage(); var scale = stage.scale(); var invscale = {'x': 1/scale.x, 'y': 1/scale.y}; - trash.x( stage.offset().x + ( 256 / scale.x ) ); - trash.y( stage.offset().y + ( 20 / scale.y ) ); + trash.x( stage.offset().x + ( stage.width() / (2 * scale.x) ) ); + trash.y( stage.offset().y + ( stage.height() / (15 * scale.y) ) ); trash.scale( invscale ); drawLayer.add( trash ); // deactivate anchors to avoid events on null shape @@ -20778,8 +21117,10 @@ dwv.tool.Draw = function (app, shapeFactoryList) // highlight trash when on it var offset = dwv.html.getEventOffset( event.evt )[0]; var eventPos = getRealPosition( offset ); - if ( Math.abs( eventPos.x - trash.x() ) < 10 && - Math.abs( eventPos.y - trash.y() ) < 10 ) { + var trashHalfWidth = trash.width() * trash.scaleX() / 2; + var trashHalfHeight = trash.height() * trash.scaleY() / 2; + if ( Math.abs( eventPos.x - trash.x() ) < trashHalfWidth && + Math.abs( eventPos.y - trash.y() ) < trashHalfHeight ) { trash.getChildren().each( function (tshape){ tshape.stroke('orange'); }); // change the group shapes colour shapeGroup.getChildren( dwv.draw.canNodeChangeColour ).forEach( @@ -20802,8 +21143,10 @@ dwv.tool.Draw = function (app, shapeFactoryList) // delete case var offset = dwv.html.getEventOffset( event.evt )[0]; var eventPos = getRealPosition( offset ); - if ( Math.abs( eventPos.x - trash.x() ) < 10 && - Math.abs( eventPos.y - trash.y() ) < 10 ) { + var trashHalfWidth = trash.width() * trash.scaleX() / 2; + var trashHalfHeight = trash.height() * trash.scaleY() / 2; + if ( Math.abs( eventPos.x - trash.x() ) < trashHalfWidth && + Math.abs( eventPos.y - trash.y() ) < trashHalfHeight ) { // compensate for the drag translation this.x( dragStartPos.x ); this.y( dragStartPos.y ); @@ -21466,39 +21809,44 @@ dwv.tool.ShapeEditor = function (app) // add shape specific anchors to the shape group if ( shape instanceof Konva.Line ) { var points = shape.points(); - if ( points.length === 4 || points.length === 6) { - // add shape offset - var p0x = points[0] + shape.x(); - var p0y = points[1] + shape.y(); - var p1x = points[2] + shape.x(); - var p1y = points[3] + shape.y(); - addAnchor(group, p0x, p0y, 'begin'); - if ( points.length === 4 ) { - var shapekids = group.getChildren( dwv.draw.isNodeNameShapeExtra ); - if (shapekids.length === 2) { - updateFunction = dwv.tool.UpdateRuler; - } else { - updateFunction = dwv.tool.UpdateArrow; + + var needsBeginEnd = group.name() === "line-group" || + group.name() === "ruler-group" || + group.name() === "protractor-group"; + var needsMid = group.name() === "protractor-group"; + + var px = 0; + var py = 0; + var name = ""; + for ( var i = 0; i < points.length; i=i+2 ) { + px = points[i] + shape.x(); + py = points[i+1] + shape.y(); + name = i; + if ( needsBeginEnd ) { + if ( i === 0 ) { + name = "begin"; + } else if ( i === points.length - 2 ) { + name = "end"; } - addAnchor(group, p1x, p1y, 'end'); } - else { - updateFunction = dwv.tool.UpdateProtractor; - addAnchor(group, p1x, p1y, 'mid'); - var p2x = points[4] + shape.x(); - var p2y = points[5] + shape.y(); - addAnchor(group, p2x, p2y, 'end'); + if ( needsMid && i === 2 ) { + name = "mid"; } + addAnchor(group, px, py, name); } - else { + + if ( group.name() === "line-group" ) { + updateFunction = dwv.tool.UpdateArrow; + } else if ( group.name() === "ruler-group" ) { + updateFunction = dwv.tool.UpdateRuler; + } else if ( group.name() === "protractor-group" ) { + updateFunction = dwv.tool.UpdateProtractor; + } else if ( group.name() === "roi-group" ) { updateFunction = dwv.tool.UpdateRoi; - var px = 0; - var py = 0; - for ( var i = 0; i < points.length; i=i+2 ) { - px = points[i] + shape.x(); - py = points[i+1] + shape.y(); - addAnchor(group, px, py, i); - } + } else if ( group.name() === "freeHand-group" ) { + updateFunction = dwv.tool.UpdateFreeHand; + } else { + console.warn("Cannot update unknown line shape."); } } else if ( shape instanceof Konva.Rect ) { @@ -21600,13 +21948,16 @@ dwv.tool.ShapeEditor = function (app) }); // drag move listener anchor.on('dragmove.edit', function (evt) { + // update shape if ( updateFunction ) { updateFunction(this, image); + } else { + console.warn("No update function!"); } + // redraw if ( this.getLayer() ) { this.getLayer().draw(); - } - else { + } else { console.warn("No layer to draw the anchor!"); } // prevent bubbling upwards @@ -22962,7 +23313,8 @@ dwv.tool.UpdateFreeHand = function (anchor /*, image*/) var points = kline.points(); points[anchor.id()] = anchor.x() - kline.x(); points[anchor.id()+1] = anchor.y() - kline.y(); - kline.points( points ); + // concat to make Konva think it is a new array + kline.points( points.concat() ); // update text var ktext = klabel.getText(); @@ -23911,8 +24263,10 @@ dwv.tool.RulerFactory.prototype.create = function (points, style, image) name: "shape" }); + var tickLen = 10 * style.getScaledStrokeWidth(); + // tick begin - var linePerp0 = dwv.math.getPerpendicularLine( line, points[0], 10 ); + var linePerp0 = dwv.math.getPerpendicularLine( line, points[0], tickLen ); var ktick0 = new Konva.Line({ points: [linePerp0.getBegin().getX(), linePerp0.getBegin().getY(), linePerp0.getEnd().getX(), linePerp0.getEnd().getY() ], @@ -23922,7 +24276,7 @@ dwv.tool.RulerFactory.prototype.create = function (points, style, image) }); // tick end - var linePerp1 = dwv.math.getPerpendicularLine( line, points[1], 10 ); + var linePerp1 = dwv.math.getPerpendicularLine( line, points[1], tickLen ); var ktick1 = new Konva.Line({ points: [linePerp1.getBegin().getX(), linePerp1.getBegin().getY(), linePerp1.getEnd().getX(), linePerp1.getEnd().getY() ], @@ -26244,6 +26598,20 @@ dwv.utils.capitaliseFirstLetter = function (string) return res; }; +/** + * Check if a string ends with the input element. + * @param {String} str The input string. + * @param {String} end The searched ending. + * @return {Boolean} True if the input string ends with the seached ending. + */ +dwv.utils.endsWith = function (str, end) { + if ( typeof str === "undefined" || str === null || + typeof end === "undefined" || end === null ) { + return false; + } + return str.substr( str.length - end.length ) === end; +}; + /** * Split key/value string: * key0=val00&key0=val01&key1=val10 returns diff --git a/dist/dwv.min.js b/dist/dwv.min.js index 67477b10e1..85569b4a5f 100644 --- a/dist/dwv.min.js +++ b/dist/dwv.min.js @@ -1,3 +1,3 @@ -/*! dwv 0.23.3 25-03-2018 */ +/*! dwv 0.24.0 2018-08-24 22:42:56 */ -!function(e,t){"function"==typeof define&&define.amd?define(["i18next","i18next-xhr-backend","i18next-browser-languagedetector","jszip","konva"],t):"object"==typeof module&&module.exports?module.exports=t(require("i18next"),require("i18next-xhr-backend"),require("i18next-browser-languagedetector"),require("jszip"),null,null):e.dwv=t(e.i18next,e.i18nextXHRBackend,e.i18nextBrowserLanguageDetector,e.JSZip,e.Konva,e.MagicWand)}(this,function(a,s,l,t,T,f){var W=void 0!==W?W:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};(H=H||{}).App=function(){var v,o=this,a=null,s=null,l=null,u=0,c=0,d=0,T=null,r=1,L=!1,S=1,i={x:0,y:0},x={x:0,y:0},g=null,m=null,h=null,I=null,P=null,p=null,f=null,C=new H.html.Style,F=null,A=null,y=null,E=null,D={},w="./";function O(e,t){R([e],new H.io.UrlsLoader,{requestHeaders:t})}function b(e,t,n){y=t;var i=W.onkeydown;W.onkeydown=function(e){e.ctrlKey&&88===e.keyCode&&(console.log("crtl-x pressed!"),o.abortLoad())},o.reset();var r="";r=void 0!==e[0].name?e[0].name:e[0],d=1===e.length&&"zip"!==r.split(".").pop().toLowerCase(),t.setDefaultCharacterSet(v),t.onload=function(e){a&&g.append(e.view),function(e){if(g)return;g=e.view,m=new H.ViewController(g),I&&I.update(e.info);s=g.getImage();var t=(a=s).getGeometry().getSize();u=t.getNumberOfColumns(),c=t.getNumberOfRows(),function(e,t){var n=o.getElement("imageLayer");(p=new H.html.Layer(n)).initialise(e,t),p.fillContext(),p.setStyleDisplay(!0);var i=o.getElement("drawDiv");i&&(f=new H.DrawController(i)).create(e,t),L?o.fitToSize(H.gui.getWindowSize()):o.fitToSize({width:o.getElement("layerContainer").offsetWidth,height:o.getElement("layerContainer").offsetHeight}),o.resetLayout()}(u,c),l=p.getContext().createImageData(u,c),g.addEventListener("wl-width-change",o.onWLChange),g.addEventListener("wl-center-change",o.onWLChange),g.addEventListener("colour-change",o.onColourChange),g.addEventListener("slice-change",o.onSliceChange),g.addEventListener("frame-change",o.onFrameChange),g.addEventListener("wl-width-change",q),g.addEventListener("wl-center-change",q),g.addEventListener("colour-change",q),g.addEventListener("position-change",q),g.addEventListener("slice-change",q),g.addEventListener("frame-change",q);F&&F.initAndDisplay(p);var n=o.getElement("dropBox");if(n){n.removeEventListener("dragover",Q),n.removeEventListener("dragleave",B),n.removeEventListener("drop",V),H.html.removeNode(n);var i=o.getElement("layerContainer");i.addEventListener("dragover",Q),i.addEventListener("dragleave",B),i.addEventListener("drop",V)}o.getElement("infoLayer")&&((h=new H.InfoController(T)).create(o),h.toggleListeners(o,g));o.initWLDisplay(),U()}(e)},t.onerror=function(e){G(e)},t.onabort=function(e){!function(e){e.message?console.warn(e.message):console.warn("Abort called.");H.gui.displayProgress(100)}(e)},t.onloadend=function(){W.onkeydown=i,f&&f.activateDrawLayer(m),q({type:"load-progress",lengthComputable:!0,loaded:100,total:100}),q({type:"load-end"}),y=null},t.onprogress=k,q({type:"load-start"}),t.load(e,n)}function R(e,t,n){t.onload=function(e){var t=new H.State;t.apply(o,t.fromJSON(e))},t.onerror=function(e){G(e)},t.load(e,n)}function q(e){if(void 0!==D[e.type])for(var t=0;tc){for(var p=h.length/c,f=[],C=0,y=0;y";for(var c=1;c]+>/g,"").toLowerCase().indexOf(n[o])<0?i="none":n[o].length&&H.html.highlight(n[o],t.rows[r]),t.rows[r].style.display=i}},H.html.dehighlight=function(e){for(var t=0;t]+>/g,"")),n.parentNode);3!==n.nodeType&&H.html.dehighlight(n)}},H.html.highlight=function(e,t){for(var n=0;n=t){var l=s[t].firstChild.data;s[t].firstChild.data=H.i18n(r+l+o)}}},H.html.makeCellEditable=function(e,t,n){if(void 0!==e){var i=document.createElement("input");t?i.onchange=t:i.disabled=!0,i.value=e.firstChild.data,void 0===n||"color"===n&&!H.browser.hasInputColor()?i.type="text":i.type=n,H.html.cleanNode(e);var r=document.createElement("form");r.onsubmit=function(e){e.preventDefault()},r.appendChild(i),e.appendChild(r)}else console.warn("Cannot create input for non existing cell.")},H.html.setCursorToPointer=function(){document.body.style.cursor="pointer"},H.html.setCursorToDefault=function(){document.body.style.cursor="default"},H.html.createHtmlSelect=function(e,t,n,i){var r=document.createElement("select");r.className=e;var o,a=void 0===n?"":n+".",s=void 0!==i,l=function(e){var t=a+e+".name";return s?H.i18nExists(t)?H.i18n(t):e:H.i18n(t)};if(t instanceof Array)for(var u in t)t.hasOwnProperty(u)&&((o=document.createElement("option")).value=t[u],o.appendChild(document.createTextNode(l(t[u]))),r.appendChild(o));else{if("object"!=typeof t)throw new Error("Unsupported input list type.");for(var c in t)(o=document.createElement("option")).value=c,o.appendChild(document.createTextNode(l(c))),r.appendChild(o)}return r},H.html.displayElement=function(e,t){e.style.display=t?"":"none"},H.html.toggleDisplay=function(e){"none"===e.style.display?e.style.display="":e.style.display="none"},H.html.appendElement=function(e,t){e.appendChild(t),H.gui.refreshElement(e)},H.html.createHiddenElement=function(e,t){var n=document.createElement(e);return n.className=t,n.style.display="none",n},(H=H||{}).gui=H.gui||{},H.gui.base=H.gui.base||{},H.gui.info=H.gui.info||{},H.gui.base.plot=function(){},H.gui.info.MiniColourMap=function(h,p){this.create=function(){var e=h.getElementsByClassName("colour-map-info");0!==e.length&&H.html.removeNodes(e);var t=document.createElement("canvas");t.className="colour-map-info",t.width=98,t.height=10,h.appendChild(t)},this.update=function(e){for(var t,n=e.wc,i=e.ww,r=h.getElementsByClassName("colour-map-info")[0],o=r.getContext("2d"),a=p.getViewController().getColourMap(),s=o.getImageData(0,0,r.width,r.height),l=0,u=p.getImage().getRescaledDataRange().min,c=(p.getImage().getRescaledDataRange().max-u)/r.width,d=0,S=n-.5-(i-1)/2,x=n-.5+(i-1)/2,g=0;gn.getMax()?t:e})},H.image.filter.Sharpen=function(){this.getName=function(){return"Sharpen"};var t=null;this.setOriginalImage=function(e){t=e},this.getOriginalImage=function(){return t}},H.image.filter.Sharpen.prototype.update=function(){return this.getOriginalImage().convolute2D([0,-1,0,-1,5,-1,0,-1,0])},H.image.filter.Sobel=function(){this.getName=function(){return"Sobel"};var t=null;this.setOriginalImage=function(e){t=e},this.getOriginalImage=function(){return t}},H.image.filter.Sobel.prototype.update=function(){var e=this.getOriginalImage(),t=e.convolute2D([1,0,-1,2,0,-2,1,0,-1]),n=e.convolute2D([1,2,1,0,0,0,-1,-2,-1]);return t.compose(n,function(e,t){return Math.sqrt(e*e+t*t)})},(H=H||{}).image=H.image||{},H.image.Size=function(e,t,n){this.getNumberOfColumns=function(){return e},this.getNumberOfRows=function(){return t},this.getNumberOfSlices=function(){return n||1}},H.image.Size.prototype.getSliceSize=function(){return this.getNumberOfColumns()*this.getNumberOfRows()},H.image.Size.prototype.getTotalSize=function(){return this.getSliceSize()*this.getNumberOfSlices()},H.image.Size.prototype.equals=function(e){return null!==e&&this.getNumberOfColumns()===e.getNumberOfColumns()&&this.getNumberOfRows()===e.getNumberOfRows()&&this.getNumberOfSlices()===e.getNumberOfSlices()},H.image.Size.prototype.isInBounds=function(e,t,n){return!(e<0||e>this.getNumberOfColumns()-1||t<0||t>this.getNumberOfRows()-1||n<0||n>this.getNumberOfSlices()-1)},H.image.Size.prototype.toString=function(){return"("+this.getNumberOfColumns()+", "+this.getNumberOfRows()+", "+this.getNumberOfSlices()+")"},H.image.Spacing=function(e,t,n){this.getColumnSpacing=function(){return e},this.getRowSpacing=function(){return t},this.getSliceSpacing=function(){return n||1}},H.image.Spacing.prototype.equals=function(e){return null!==e&&this.getColumnSpacing()===e.getColumnSpacing()&&this.getRowSpacing()===e.getRowSpacing()&&this.getSliceSpacing()===e.getSliceSpacing()},H.image.Spacing.prototype.toString=function(){return"("+this.getColumnSpacing()+", "+this.getRowSpacing()+", "+this.getSliceSpacing()+")"},H.image.Geometry=function(e,n,t,o){void 0===e&&(e=new H.math.Point3D(0,0,0));var a=[e];void 0===o&&(o=new H.math.getIdentityMat33),this.getOrigin=function(){return e},this.getOrigins=function(){return a},this.getSize=function(){return n},this.getSpacing=function(){return t},this.getOrientation=function(){return o},this.getSliceIndex=function(e){for(var t=0,n=e.getDistance(a[0]),i=0,r=0;r=2*H.image.lut.range_max/3?H.image.lut.range_max-1:0},H.image.lut.toMaxFirstThird=function(e){var t=3*e;return t>H.image.lut.range_max-1?H.image.lut.range_max-1:t},H.image.lut.toMaxSecondThird=function(e){var t=H.image.lut.range_max/3,n=0;return t<=e&&(n=3*(e-t))>H.image.lut.range_max-1?H.image.lut.range_max-1:n},H.image.lut.toMaxThirdThird=function(e){var t=H.image.lut.range_max/3,n=0;return 2*t<=e&&(n=3*(e-2*t))>H.image.lut.range_max-1?H.image.lut.range_max-1:n},H.image.lut.zero=function(){return 0},H.image.lut.id=function(e){return e},H.image.lut.invId=function(e){return H.image.lut.range_max-1-e},H.image.lut.plain={red:H.image.lut.buildLut(H.image.lut.id),green:H.image.lut.buildLut(H.image.lut.id),blue:H.image.lut.buildLut(H.image.lut.id)},H.image.lut.invPlain={red:H.image.lut.buildLut(H.image.lut.invId),green:H.image.lut.buildLut(H.image.lut.invId),blue:H.image.lut.buildLut(H.image.lut.invId)},H.image.lut.rainbow={blue:[0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,255,247,239,231,223,215,207,199,191,183,175,167,159,151,143,135,127,119,111,103,95,87,79,71,63,55,47,39,31,23,15,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128,136,144,152,160,168,176,184,192,200,208,216,224,232,240,248,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,253,251,249,247,245,243,241,239,237,235,233,231,229,227,225,223,221,219,217,215,213,211,209,207,205,203,201,199,197,195,193,192,189,186,183,180,177,174,171,168,165,162,159,156,153,150,147,144,141,138,135,132,129,126,123,120,117,114,111,108,105,102,99,96,93,90,87,84,81,78,75,72,69,66,63,60,57,54,51,48,45,42,39,36,33,30,27,24,21,18,15,12,9,6,3],red:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,62,60,58,56,54,52,50,48,46,44,42,40,38,36,34,32,30,28,26,24,22,20,18,16,14,12,10,8,6,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255]},H.image.lut.hot={red:H.image.lut.buildLut(H.image.lut.toMaxFirstThird),green:H.image.lut.buildLut(H.image.lut.toMaxSecondThird),blue:H.image.lut.buildLut(H.image.lut.toMaxThirdThird)},H.image.lut.hot_iron={red:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,255],blue:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,255]},H.image.lut.pet={red:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,85,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,171,173,175,177,179,181,183,185,187,189,191,193,195,197,199,201,203,205,207,209,211,213,215,217,219,221,223,225,227,229,231,233,235,237,239,241,243,245,247,249,251,253,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95,97,99,101,103,105,107,109,111,113,115,117,119,121,123,125,128,126,124,122,120,118,116,114,112,110,108,106,104,102,100,98,96,94,92,90,88,86,84,82,80,78,76,74,72,70,68,66,64,63,61,59,57,55,53,51,49,47,45,43,41,39,37,35,33,31,29,27,25,23,21,19,17,15,13,11,9,7,5,3,1,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,255],blue:[0,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95,97,99,101,103,105,107,109,111,113,115,117,119,121,123,125,127,129,131,133,135,137,139,141,143,145,147,149,151,153,155,157,159,161,163,165,167,169,171,173,175,177,179,181,183,185,187,189,191,193,195,197,199,201,203,205,207,209,211,213,215,217,219,221,223,225,227,229,231,233,235,237,239,241,243,245,247,249,251,253,255,252,248,244,240,236,232,228,224,220,216,212,208,204,200,196,192,188,184,180,176,172,168,164,160,156,152,148,144,140,136,132,128,124,120,116,112,108,104,100,96,92,88,84,80,76,72,68,64,60,56,52,48,44,40,36,32,28,24,20,16,12,8,4,0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,85,89,93,97,101,105,109,113,117,121,125,129,133,137,141,145,149,153,157,161,165,170,174,178,182,186,190,194,198,202,206,210,214,218,222,226,230,234,238,242,246,250,255]},H.image.lut.hot_metal_blue={red:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,6,9,12,15,18,21,24,26,29,32,35,38,41,44,47,50,52,55,57,59,62,64,66,69,71,74,76,78,81,83,85,88,90,93,96,99,102,105,108,111,114,116,119,122,125,128,131,134,137,140,143,146,149,152,155,158,161,164,166,169,172,175,178,181,184,187,190,194,198,201,205,209,213,217,221,224,228,232,236,240,244,247,251,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,4,6,8,9,11,13,15,17,19,21,23,24,26,28,30,32,34,36,38,40,41,43,45,47,49,51,53,55,56,58,60,62,64,66,68,70,72,73,75,77,79,81,83,85,87,88,90,92,94,96,98,100,102,104,105,107,109,111,113,115,117,119,120,122,124,126,128,130,132,134,136,137,139,141,143,145,147,149,151,152,154,156,158,160,162,164,166,168,169,171,173,175,177,179,181,183,184,186,188,190,192,194,196,198,200,201,203,205,207,209,211,213,215,216,218,220,222,224,226,228,229,231,233,235,237,239,240,242,244,246,248,250,251,253,255],blue:[0,2,4,6,8,10,12,14,16,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,117,119,121,123,125,127,129,131,133,135,137,139,141,143,145,147,149,151,153,155,157,159,161,163,165,167,169,171,173,175,177,179,181,183,184,186,188,190,192,194,196,198,200,197,194,191,188,185,182,179,176,174,171,168,165,162,159,156,153,150,144,138,132,126,121,115,109,103,97,91,85,79,74,68,62,56,50,47,44,41,38,35,32,29,26,24,21,18,15,12,9,6,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,6,9,12,15,18,21,24,26,29,32,35,38,41,44,47,50,53,56,59,62,65,68,71,74,76,79,82,85,88,91,94,97,100,103,106,109,112,115,118,121,124,126,129,132,135,138,141,144,147,150,153,156,159,162,165,168,171,174,176,179,182,185,188,191,194,197,200,203,206,210,213,216,219,223,226,229,232,236,239,242,245,249,252,255]},H.image.lut.pet_20step={red:[0,0,0,0,0,0,0,0,0,0,0,0,0,96,96,96,96,96,96,96,96,96,96,96,96,96,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,96,96,96,96,96,96,96,96,96,96,96,96,96,112,112,112,112,112,112,112,112,112,112,112,112,112,128,128,128,128,128,128,128,128,128,128,128,128,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,64,64,64,64,64,64,64,64,64,64,64,64,224,224,224,224,224,224,224,224,224,224,224,224,224,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,192,192,192,192,192,192,192,192,192,192,192,192,192,176,176,176,176,176,176,176,176,176,176,176,176,176,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,96,96,96,96,96,96,96,96,96,96,96,96,96,112,112,112,112,112,112,112,112,112,112,112,112,112,128,128,128,128,128,128,128,128,128,128,128,128,96,96,96,96,96,96,96,96,96,96,96,96,96,144,144,144,144,144,144,144,144,144,144,144,144,144,192,192,192,192,192,192,192,192,192,192,192,192,192,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,208,208,208,208,208,208,208,208,208,208,208,208,208,176,176,176,176,176,176,176,176,176,176,176,176,176,144,144,144,144,144,144,144,144,144,144,144,144,96,96,96,96,96,96,96,96,96,96,96,96,96,48,48,48,48,48,48,48,48,48,48,48,48,48,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255],blue:[0,0,0,0,0,0,0,0,0,0,0,0,0,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,112,112,112,112,112,112,112,112,112,112,112,112,128,128,128,128,128,128,128,128,128,128,128,128,128,176,176,176,176,176,176,176,176,176,176,176,176,176,192,192,192,192,192,192,192,192,192,192,192,192,192,224,224,224,224,224,224,224,224,224,224,224,224,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,64,64,64,64,64,64,64,64,64,64,64,64,80,80,80,80,80,80,80,80,80,80,80,80,80,96,96,96,96,96,96,96,96,96,96,96,96,96,64,64,64,64,64,64,64,64,64,64,64,64,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255]},H.image.lut.test={red:H.image.lut.buildLut(H.image.lut.id),green:H.image.lut.buildLut(H.image.lut.zero),blue:H.image.lut.buildLut(H.image.lut.zero)},(H=H||{}).image=H.image||{},H.image.WindowLevel=function(t,n){if(0===n)throw new Error("A window level with a width of zero is not possible.");var i=0,r=0,o=255,a=null,s=null,l=null,u=null;function c(){var e=t+i;a=e-.5-(n-1)/2,s=e-.5+(n-1)/2,l=(o-r)/(n-1),u=(-(e-.5)/(n-1)+.5)*(o-r)+r}c(),this.getCenter=function(){return t},this.getWidth=function(){return n},this.setRange=function(e,t){r=parseInt(e,10),o=parseInt(t,10),c()},this.setSignedOffset=function(e){i=e,c()},this.apply=function(e){return e<=a?r:s=d.getNumberOfFrames())&&(n!==(n=e)&&1!==d.getNumberOfFrames()&&(this.fireEvent({type:"frame-change",frame:n}),this.setCurrentPosition(this.getCurrentPosition(),!0)),!0)},this.append=function(e){var t=this.getImage().appendSlice(e.getImage());t<=this.getCurrentPosition().k&&this.setCurrentPosition({i:this.getCurrentPosition().i,j:this.getCurrentPosition().j,k:this.getCurrentPosition().k+1},!0),this.addWindowPresets(e.getWindowPresets(),t)},this.appendFrameBuffer=function(e){this.getImage().appendFrameBuffer(e)},this.setWindowLevel=function(e,t,n){if(1<=t){var i=this.getCurrentPosition().k,r=null,o=d.getRescaleSlopeAndIntercept(i);if(o&&void 0!==o){var a=S[o.toString()];a&&void 0!==a&&(r=a.getWindowLevel())}void 0===n&&(n="manual"),x=n;var s=new H.image.WindowLevel(e,t);if(0===Object.keys(S).length){var l=new H.image.lut.Rescale(d.getRescaleSlopeAndIntercept(0),d.getMeta().BitsStored),u=new H.image.lut.Window(l,d.getMeta().IsSigned);this.addWindowLut(u)}for(var c in S)S[c].setWindowLevel(s);r&&void 0!==r?(r.getWidth()!==t&&this.fireEvent({type:"wl-width-change",wc:e,ww:t}),r.getCenter()!==e&&this.fireEvent({type:"wl-center-change",wc:e,ww:t})):(this.fireEvent({type:"wl-width-change",wc:e,ww:t}),this.fireEvent({type:"wl-center-change",wc:e,ww:t}))}},this.setWindowLevelPreset=function(e){var t=this.getWindowPresets()[e];if(void 0===t)throw new Error("Unknown window level preset: '"+e+"'");"minmax"===e&&void 0===t.wl&&(t.wl=this.getWindowLevelMinMax()),void 0!==t.perslice&&!0===t.perslice&&(t={wl:t.wl[this.getCurrentPosition().k]}),this.setWindowLevel(t.wl.getCenter(),t.wl.getWidth(),e)},this.setWindowLevelPresetById=function(e){var t=Object.keys(this.getWindowPresets());this.setWindowLevelPreset(t[e])},this.clone=function(){var e=new H.image.View(this.getImage());for(var t in S)e.addWindowLut(S[t]);return e.setListeners(this.getListeners()),e};var r={};this.getListeners=function(){return r},this.setListeners=function(e){r=e}},H.image.View.prototype.getWindowLevelMinMax=function(){var e=this.getImage().getRescaledDataRange(),t=e.min,n=e.max-t,i=t+n/2;return new H.image.WindowLevel(i,n)},H.image.View.prototype.setWindowLevelMinMax=function(){var e=this.getWindowLevelMinMax();this.setWindowLevel(e.getCenter(),e.getWidth(),"minmax")},H.image.View.prototype.generateImageData=function(e){var t=this.getCurrentWindowLut(),n=this.getImage(),i=n.getGeometry().getSize().getSliceSize(),r=i*this.getCurrentPosition().k,o=this.getCurrentFrame()?this.getCurrentFrame():0,a=0,s=0,l=0,u=n.getPhotometricInterpretation();switch(u){case"MONOCHROME1":case"MONOCHROME2":for(var c=this.getColourMap(),d=r+i,S=r;Sn.getEnd().getX()?0:-1,x=n.getBegin().getY()>n.getEnd().getY()?-1:.5,g=new T.Label({x:n.getEnd().getX()+25*S,y:n.getEnd().getY()+15*x,name:"label"});g.add(d),g.add(new T.Tag);var m=new T.Group;return m.name("line-group"),m.add(i),m.add(c),m.add(g),m.visible(!0),m},H.tool.UpdateArrow=function(e){var t=e.getParent(),n=t.getChildren(function(e){return"shape"===e.name()})[0],i=t.getChildren(function(e){return"shape-triangle"===e.name()})[0],r=t.getChildren(function(e){return"label"===e.name()})[0],o=t.getChildren(function(e){return"begin"===e.id()})[0],a=t.getChildren(function(e){return"end"===e.id()})[0];switch(e.id()){case"begin":o.x(e.x()),o.y(e.y());break;case"end":a.x(e.x()),a.y(e.y())}var s=o.x()-n.x(),l=o.y()-n.y(),u=a.x()-n.x(),c=a.y()-n.y();n.points([s,l,u,c]);var d=new H.math.Point2D(o.x(),o.y()),S=new H.math.Point2D(a.x(),a.y()),x=new H.math.Line(d,S),g=new H.math.Point2D(s,l),m=new H.math.Point2D(u,c),h=H.math.getPerpendicularLine(x,g,10),p=H.math.getPerpendicularLine(x,m,10);n.hitFunc(function(e){e.beginPath(),e.moveTo(h.getBegin().getX(),h.getBegin().getY()),e.lineTo(h.getEnd().getX(),h.getEnd().getY()),e.lineTo(p.getEnd().getX(),p.getEnd().getY()),e.lineTo(p.getBegin().getX(),p.getBegin().getY()),e.closePath(),e.fillStrokeShape(this)});var f=new H.math.Point2D(x.getBegin().getX(),x.getBegin().getY()-10),C=new H.math.Line(x.getBegin(),f),y=H.math.getAngle(x,C),D=y*Math.PI/180;i.x(x.getBegin().getX()+i.radius()*Math.sin(D)),i.y(x.getBegin().getY()+i.radius()*Math.cos(D)),i.rotation(-y);var v=r.getText();v.quant=null,v.setText(v.textExpr);var T=x.getBegin().getX()>x.getEnd().getX()?0:-1,L=x.getBegin().getY()>x.getEnd().getY()?-1:.5,I={x:x.getEnd().getX()+25*T,y:x.getEnd().getY()+15*L};r.position(I)},(H=H||{}).tool=H.tool||{};T=T||{};H.tool.Draw=function(d,e){var n=this,i=null,r=!1;this.shapeFactoryList=e;var t=null,o=null;this.shapeName=0;var a=[],s=null,S=new H.tool.ShapeEditor(d);S.setDrawEventCallback(f);var x=new T.Group,l=new T.Line({points:[-10,-10,10,10],stroke:"red"}),u=new T.Line({points:[10,-10,-10,10],stroke:"red"});x.add(l),x.add(u),this.style=new H.html.Style;var c={},g=null;function m(){h(!0)}function h(e){g.listening(e),g.hitGraphEnabled(e);var t=d.getDrawController().getCurrentPosGroup().getChildren();e?(d.addToolCanvasListeners(d.getDrawStage().getContent()),t.forEach(function(e){n.setShapeOn(e)})):(d.removeToolCanvasListeners(d.getDrawStage().getContent()),t.forEach(function(e){var t;(t=e).off("mouseover"),t.off("mouseout"),t.draggable(!1),t.off("dragstart.draw"),t.off("dragmove.draw"),t.off("dragend.draw"),t.off("dblclick")})),g.draw()}function p(e){var t=d.getDrawStage();return{x:t.offset().x+e.x/t.scale().x,y:t.offset().y+e.y/t.scale().y}}function f(e){if(void 0!==c[e.type])for(var t=0;ti.getEnd().getX()?0:-1,S=i.getBegin().getY()>i.getEnd().getY()?-1:.5,x=new T.Label({x:i.getEnd().getX()+25*d,y:i.getEnd().getY()+15*S,name:"label"});x.add(c),x.add(new T.Tag);var g=new T.Group;return g.name("ruler-group"),g.add(r),g.add(a),g.add(l),g.add(x),g.visible(!0),g},H.tool.UpdateRuler=function(e,t){var n=e.getParent(),i=n.getChildren(function(e){return"shape"===e.name()})[0],r=n.getChildren(function(e){return"shape-tick0"===e.name()})[0],o=n.getChildren(function(e){return"shape-tick1"===e.name()})[0],a=n.getChildren(function(e){return"label"===e.name()})[0],s=n.getChildren(function(e){return"begin"===e.id()})[0],l=n.getChildren(function(e){return"end"===e.id()})[0];switch(e.id()){case"begin":s.x(e.x()),s.y(e.y());break;case"end":l.x(e.x()),l.y(e.y())}var u=s.x()-i.x(),c=s.y()-i.y(),d=l.x()-i.x(),S=l.y()-i.y();i.points([u,c,d,S]);var x=new H.math.Point2D(s.x(),s.y()),g=new H.math.Point2D(l.x(),l.y()),m=new H.math.Line(x,g),h=new H.math.Point2D(u,c),p=new H.math.Point2D(d,S),f=H.math.getPerpendicularLine(m,h,10);r.points([f.getBegin().getX(),f.getBegin().getY(),f.getEnd().getX(),f.getEnd().getY()]);var C=H.math.getPerpendicularLine(m,p,10);o.points([C.getBegin().getX(),C.getBegin().getY(),C.getEnd().getX(),C.getEnd().getY()]),i.hitFunc(function(e){e.beginPath(),e.moveTo(f.getBegin().getX(),f.getBegin().getY()),e.lineTo(f.getEnd().getX(),f.getEnd().getY()),e.lineTo(C.getEnd().getX(),C.getEnd().getY()),e.lineTo(C.getBegin().getX(),C.getBegin().getY()),e.closePath(),e.fillStrokeShape(this)});var y=t.quantifyLine(m),D=a.getText();D.quant=y,D.setText(H.utils.replaceFlags(D.textExpr,D.quant));var v=m.getBegin().getX()>m.getEnd().getX()?0:-1,T=m.getBegin().getY()>m.getEnd().getY()?-1:.5,L={x:m.getEnd().getX()+25*v,y:m.getEnd().getY()+15*T};a.position(L)},(H=H||{}).tool=H.tool||{},H.tool.Scroll=function(o){var a=this,t=null;this.started=!1;var n=null;function i(e){var t=1!==o.getImage().getGeometry().getSize().getNumberOfSlices(),n=1!==o.getImage().getNumberOfFrames();e?t?o.getViewController().incrementSliceNb():n&&o.getViewController().incrementFrameNb():t?o.getViewController().decrementSliceNb():n&&o.getViewController().decrementFrameNb()}this.mousedown=function(e){o.getViewController().isPlaying()&&o.getViewController().stop(),a.started=!0,a.x0=e._x,a.y0=e._y},this.mousemove=function(e){if(a.started){var t=e._y-a.y0,n=15c){for(var p=h.length/c,f=[],C=0,y=0;y";for(var c=1;c]+>/g,"").toLowerCase().indexOf(n[o])<0?i="none":n[o].length&&_.html.highlight(n[o],t.rows[r]),t.rows[r].style.display=i}},_.html.dehighlight=function(e){for(var t=0;t]+>/g,"")),n.parentNode);3!==n.nodeType&&_.html.dehighlight(n)}},_.html.highlight=function(e,t){for(var n=0;n=t){var l=s[t].firstChild.data;s[t].firstChild.data=_.i18n(r+l+o)}}},_.html.makeCellEditable=function(e,t,n){if(void 0!==e){var i=document.createElement("input");t?i.onchange=t:i.disabled=!0,i.value=e.firstChild.data,void 0===n||"color"===n&&!_.browser.hasInputColor()?i.type="text":i.type=n,_.html.cleanNode(e);var r=document.createElement("form");r.onsubmit=function(e){e.preventDefault()},r.appendChild(i),e.appendChild(r)}else console.warn("Cannot create input for non existing cell.")},_.html.setCursorToPointer=function(){document.body.style.cursor="pointer"},_.html.setCursorToDefault=function(){document.body.style.cursor="default"},_.html.createHtmlSelect=function(e,t,n,i){var r=document.createElement("select");r.className=e;var o,a=void 0===n?"":n+".",s=void 0!==i,l=function(e){var t=a+e+".name";return s?_.i18nExists(t)?_.i18n(t):e:_.i18n(t)};if(t instanceof Array)for(var u in t)t.hasOwnProperty(u)&&((o=document.createElement("option")).value=t[u],o.appendChild(document.createTextNode(l(t[u]))),r.appendChild(o));else{if("object"!=typeof t)throw new Error("Unsupported input list type.");for(var c in t)(o=document.createElement("option")).value=c,o.appendChild(document.createTextNode(l(c))),r.appendChild(o)}return r},_.html.displayElement=function(e,t){e.style.display=t?"":"none"},_.html.toggleDisplay=function(e){"none"===e.style.display?e.style.display="":e.style.display="none"},_.html.appendElement=function(e,t){e.appendChild(t),_.gui.refreshElement(e)},_.html.createHiddenElement=function(e,t){var n=document.createElement(e);return n.className=t,n.style.display="none",n},(_=_||{}).gui=_.gui||{},_.gui.base=_.gui.base||{},_.gui.info=_.gui.info||{},_.gui.base.plot=function(){},_.gui.info.MiniColourMap=function(h,p){this.create=function(){var e=h.getElementsByClassName("colour-map-info");0!==e.length&&_.html.removeNodes(e);var t=document.createElement("canvas");t.className="colour-map-info",t.width=98,t.height=10,h.appendChild(t)},this.update=function(e){for(var t,n=e.wc,i=e.ww,r=h.getElementsByClassName("colour-map-info")[0],o=r.getContext("2d"),a=p.getViewController().getColourMap(),s=o.getImageData(0,0,r.width,r.height),l=0,u=p.getImage().getRescaledDataRange().min,c=(p.getImage().getRescaledDataRange().max-u)/r.width,d=0,S=n-.5-(i-1)/2,x=n-.5+(i-1)/2,g=0;gn.getMax()?t:e})},_.image.filter.Sharpen=function(){this.getName=function(){return"Sharpen"};var t=null;this.setOriginalImage=function(e){t=e},this.getOriginalImage=function(){return t}},_.image.filter.Sharpen.prototype.update=function(){return this.getOriginalImage().convolute2D([0,-1,0,-1,5,-1,0,-1,0])},_.image.filter.Sobel=function(){this.getName=function(){return"Sobel"};var t=null;this.setOriginalImage=function(e){t=e},this.getOriginalImage=function(){return t}},_.image.filter.Sobel.prototype.update=function(){var e=this.getOriginalImage(),t=e.convolute2D([1,0,-1,2,0,-2,1,0,-1]),n=e.convolute2D([1,2,1,0,0,0,-1,-2,-1]);return t.compose(n,function(e,t){return Math.sqrt(e*e+t*t)})},(_=_||{}).image=_.image||{},_.image.Size=function(e,t,n){this.getNumberOfColumns=function(){return e},this.getNumberOfRows=function(){return t},this.getNumberOfSlices=function(){return n||1}},_.image.Size.prototype.getSliceSize=function(){return this.getNumberOfColumns()*this.getNumberOfRows()},_.image.Size.prototype.getTotalSize=function(){return this.getSliceSize()*this.getNumberOfSlices()},_.image.Size.prototype.equals=function(e){return null!==e&&this.getNumberOfColumns()===e.getNumberOfColumns()&&this.getNumberOfRows()===e.getNumberOfRows()&&this.getNumberOfSlices()===e.getNumberOfSlices()},_.image.Size.prototype.isInBounds=function(e,t,n){return!(e<0||e>this.getNumberOfColumns()-1||t<0||t>this.getNumberOfRows()-1||n<0||n>this.getNumberOfSlices()-1)},_.image.Size.prototype.toString=function(){return"("+this.getNumberOfColumns()+", "+this.getNumberOfRows()+", "+this.getNumberOfSlices()+")"},_.image.Spacing=function(e,t,n){this.getColumnSpacing=function(){return e},this.getRowSpacing=function(){return t},this.getSliceSpacing=function(){return n||1}},_.image.Spacing.prototype.equals=function(e){return null!==e&&this.getColumnSpacing()===e.getColumnSpacing()&&this.getRowSpacing()===e.getRowSpacing()&&this.getSliceSpacing()===e.getSliceSpacing()},_.image.Spacing.prototype.toString=function(){return"("+this.getColumnSpacing()+", "+this.getRowSpacing()+", "+this.getSliceSpacing()+")"},_.image.Geometry=function(e,n,t,o){void 0===e&&(e=new _.math.Point3D(0,0,0));var a=[e];void 0===o&&(o=new _.math.getIdentityMat33),this.getOrigin=function(){return e},this.getOrigins=function(){return a},this.getSize=function(){return n},this.getSpacing=function(){return t},this.getOrientation=function(){return o},this.getSliceIndex=function(e){for(var t=0,n=e.getDistance(a[0]),i=0,r=0;r=2*_.image.lut.range_max/3?_.image.lut.range_max-1:0},_.image.lut.toMaxFirstThird=function(e){var t=3*e;return t>_.image.lut.range_max-1?_.image.lut.range_max-1:t},_.image.lut.toMaxSecondThird=function(e){var t=_.image.lut.range_max/3,n=0;return t<=e&&(n=3*(e-t))>_.image.lut.range_max-1?_.image.lut.range_max-1:n},_.image.lut.toMaxThirdThird=function(e){var t=_.image.lut.range_max/3,n=0;return 2*t<=e&&(n=3*(e-2*t))>_.image.lut.range_max-1?_.image.lut.range_max-1:n},_.image.lut.zero=function(){return 0},_.image.lut.id=function(e){return e},_.image.lut.invId=function(e){return _.image.lut.range_max-1-e},_.image.lut.plain={red:_.image.lut.buildLut(_.image.lut.id),green:_.image.lut.buildLut(_.image.lut.id),blue:_.image.lut.buildLut(_.image.lut.id)},_.image.lut.invPlain={red:_.image.lut.buildLut(_.image.lut.invId),green:_.image.lut.buildLut(_.image.lut.invId),blue:_.image.lut.buildLut(_.image.lut.invId)},_.image.lut.rainbow={blue:[0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,255,247,239,231,223,215,207,199,191,183,175,167,159,151,143,135,127,119,111,103,95,87,79,71,63,55,47,39,31,23,15,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128,136,144,152,160,168,176,184,192,200,208,216,224,232,240,248,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,253,251,249,247,245,243,241,239,237,235,233,231,229,227,225,223,221,219,217,215,213,211,209,207,205,203,201,199,197,195,193,192,189,186,183,180,177,174,171,168,165,162,159,156,153,150,147,144,141,138,135,132,129,126,123,120,117,114,111,108,105,102,99,96,93,90,87,84,81,78,75,72,69,66,63,60,57,54,51,48,45,42,39,36,33,30,27,24,21,18,15,12,9,6,3],red:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,62,60,58,56,54,52,50,48,46,44,42,40,38,36,34,32,30,28,26,24,22,20,18,16,14,12,10,8,6,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255]},_.image.lut.hot={red:_.image.lut.buildLut(_.image.lut.toMaxFirstThird),green:_.image.lut.buildLut(_.image.lut.toMaxSecondThird),blue:_.image.lut.buildLut(_.image.lut.toMaxThirdThird)},_.image.lut.hot_iron={red:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,255],blue:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,255]},_.image.lut.pet={red:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,85,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,171,173,175,177,179,181,183,185,187,189,191,193,195,197,199,201,203,205,207,209,211,213,215,217,219,221,223,225,227,229,231,233,235,237,239,241,243,245,247,249,251,253,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95,97,99,101,103,105,107,109,111,113,115,117,119,121,123,125,128,126,124,122,120,118,116,114,112,110,108,106,104,102,100,98,96,94,92,90,88,86,84,82,80,78,76,74,72,70,68,66,64,63,61,59,57,55,53,51,49,47,45,43,41,39,37,35,33,31,29,27,25,23,21,19,17,15,13,11,9,7,5,3,1,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,255],blue:[0,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95,97,99,101,103,105,107,109,111,113,115,117,119,121,123,125,127,129,131,133,135,137,139,141,143,145,147,149,151,153,155,157,159,161,163,165,167,169,171,173,175,177,179,181,183,185,187,189,191,193,195,197,199,201,203,205,207,209,211,213,215,217,219,221,223,225,227,229,231,233,235,237,239,241,243,245,247,249,251,253,255,252,248,244,240,236,232,228,224,220,216,212,208,204,200,196,192,188,184,180,176,172,168,164,160,156,152,148,144,140,136,132,128,124,120,116,112,108,104,100,96,92,88,84,80,76,72,68,64,60,56,52,48,44,40,36,32,28,24,20,16,12,8,4,0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,85,89,93,97,101,105,109,113,117,121,125,129,133,137,141,145,149,153,157,161,165,170,174,178,182,186,190,194,198,202,206,210,214,218,222,226,230,234,238,242,246,250,255]},_.image.lut.hot_metal_blue={red:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,6,9,12,15,18,21,24,26,29,32,35,38,41,44,47,50,52,55,57,59,62,64,66,69,71,74,76,78,81,83,85,88,90,93,96,99,102,105,108,111,114,116,119,122,125,128,131,134,137,140,143,146,149,152,155,158,161,164,166,169,172,175,178,181,184,187,190,194,198,201,205,209,213,217,221,224,228,232,236,240,244,247,251,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,4,6,8,9,11,13,15,17,19,21,23,24,26,28,30,32,34,36,38,40,41,43,45,47,49,51,53,55,56,58,60,62,64,66,68,70,72,73,75,77,79,81,83,85,87,88,90,92,94,96,98,100,102,104,105,107,109,111,113,115,117,119,120,122,124,126,128,130,132,134,136,137,139,141,143,145,147,149,151,152,154,156,158,160,162,164,166,168,169,171,173,175,177,179,181,183,184,186,188,190,192,194,196,198,200,201,203,205,207,209,211,213,215,216,218,220,222,224,226,228,229,231,233,235,237,239,240,242,244,246,248,250,251,253,255],blue:[0,2,4,6,8,10,12,14,16,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,117,119,121,123,125,127,129,131,133,135,137,139,141,143,145,147,149,151,153,155,157,159,161,163,165,167,169,171,173,175,177,179,181,183,184,186,188,190,192,194,196,198,200,197,194,191,188,185,182,179,176,174,171,168,165,162,159,156,153,150,144,138,132,126,121,115,109,103,97,91,85,79,74,68,62,56,50,47,44,41,38,35,32,29,26,24,21,18,15,12,9,6,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,6,9,12,15,18,21,24,26,29,32,35,38,41,44,47,50,53,56,59,62,65,68,71,74,76,79,82,85,88,91,94,97,100,103,106,109,112,115,118,121,124,126,129,132,135,138,141,144,147,150,153,156,159,162,165,168,171,174,176,179,182,185,188,191,194,197,200,203,206,210,213,216,219,223,226,229,232,236,239,242,245,249,252,255]},_.image.lut.pet_20step={red:[0,0,0,0,0,0,0,0,0,0,0,0,0,96,96,96,96,96,96,96,96,96,96,96,96,96,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,96,96,96,96,96,96,96,96,96,96,96,96,96,112,112,112,112,112,112,112,112,112,112,112,112,112,128,128,128,128,128,128,128,128,128,128,128,128,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,64,64,64,64,64,64,64,64,64,64,64,64,224,224,224,224,224,224,224,224,224,224,224,224,224,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,192,192,192,192,192,192,192,192,192,192,192,192,192,176,176,176,176,176,176,176,176,176,176,176,176,176,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,96,96,96,96,96,96,96,96,96,96,96,96,96,112,112,112,112,112,112,112,112,112,112,112,112,112,128,128,128,128,128,128,128,128,128,128,128,128,96,96,96,96,96,96,96,96,96,96,96,96,96,144,144,144,144,144,144,144,144,144,144,144,144,144,192,192,192,192,192,192,192,192,192,192,192,192,192,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,208,208,208,208,208,208,208,208,208,208,208,208,208,176,176,176,176,176,176,176,176,176,176,176,176,176,144,144,144,144,144,144,144,144,144,144,144,144,96,96,96,96,96,96,96,96,96,96,96,96,96,48,48,48,48,48,48,48,48,48,48,48,48,48,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255],blue:[0,0,0,0,0,0,0,0,0,0,0,0,0,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,112,112,112,112,112,112,112,112,112,112,112,112,128,128,128,128,128,128,128,128,128,128,128,128,128,176,176,176,176,176,176,176,176,176,176,176,176,176,192,192,192,192,192,192,192,192,192,192,192,192,192,224,224,224,224,224,224,224,224,224,224,224,224,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,64,64,64,64,64,64,64,64,64,64,64,64,80,80,80,80,80,80,80,80,80,80,80,80,80,96,96,96,96,96,96,96,96,96,96,96,96,96,64,64,64,64,64,64,64,64,64,64,64,64,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255]},_.image.lut.test={red:_.image.lut.buildLut(_.image.lut.id),green:_.image.lut.buildLut(_.image.lut.zero),blue:_.image.lut.buildLut(_.image.lut.zero)},(_=_||{}).image=_.image||{},_.image.WindowLevel=function(t,n){if(0===n)throw new Error("A window level with a width of zero is not possible.");var i=0,r=0,o=255,a=null,s=null,l=null,u=null;function c(){var e=t+i;a=e-.5-(n-1)/2,s=e-.5+(n-1)/2,l=(o-r)/(n-1),u=(-(e-.5)/(n-1)+.5)*(o-r)+r}c(),this.getCenter=function(){return t},this.getWidth=function(){return n},this.setRange=function(e,t){r=parseInt(e,10),o=parseInt(t,10),c()},this.setSignedOffset=function(e){i=e,c()},this.apply=function(e){return e<=a?r:s=d.getNumberOfFrames())&&(n!==(n=e)&&1!==d.getNumberOfFrames()&&(this.fireEvent({type:"frame-change",frame:n}),this.setCurrentPosition(this.getCurrentPosition(),!0)),!0)},this.append=function(e){var t=this.getImage().appendSlice(e.getImage());t<=this.getCurrentPosition().k&&this.setCurrentPosition({i:this.getCurrentPosition().i,j:this.getCurrentPosition().j,k:this.getCurrentPosition().k+1},!0),this.addWindowPresets(e.getWindowPresets(),t)},this.appendFrameBuffer=function(e){this.getImage().appendFrameBuffer(e)},this.setWindowLevel=function(e,t,n){if(1<=t){var i=this.getCurrentPosition().k,r=null,o=d.getRescaleSlopeAndIntercept(i);if(o&&void 0!==o){var a=S[o.toString()];a&&void 0!==a&&(r=a.getWindowLevel())}void 0===n&&(n="manual"),x=n;var s=new _.image.WindowLevel(e,t);if(0===Object.keys(S).length){var l=new _.image.lut.Rescale(d.getRescaleSlopeAndIntercept(0),d.getMeta().BitsStored),u=new _.image.lut.Window(l,d.getMeta().IsSigned);this.addWindowLut(u)}for(var c in S)S[c].setWindowLevel(s);r&&void 0!==r?(r.getWidth()!==t&&this.fireEvent({type:"wl-width-change",wc:e,ww:t}),r.getCenter()!==e&&this.fireEvent({type:"wl-center-change",wc:e,ww:t})):(this.fireEvent({type:"wl-width-change",wc:e,ww:t}),this.fireEvent({type:"wl-center-change",wc:e,ww:t}))}},this.setWindowLevelPreset=function(e){var t=this.getWindowPresets()[e];if(void 0===t)throw new Error("Unknown window level preset: '"+e+"'");"minmax"===e&&void 0===t.wl&&(t.wl=this.getWindowLevelMinMax()),void 0!==t.perslice&&!0===t.perslice&&(t={wl:t.wl[this.getCurrentPosition().k]}),this.setWindowLevel(t.wl.getCenter(),t.wl.getWidth(),e)},this.setWindowLevelPresetById=function(e){var t=Object.keys(this.getWindowPresets());this.setWindowLevelPreset(t[e])},this.clone=function(){var e=new _.image.View(this.getImage());for(var t in S)e.addWindowLut(S[t]);return e.setListeners(this.getListeners()),e};var r={};this.getListeners=function(){return r},this.setListeners=function(e){r=e}},_.image.View.prototype.getWindowLevelMinMax=function(){var e=this.getImage().getRescaledDataRange(),t=e.min,n=e.max-t,i=t+n/2;return new _.image.WindowLevel(i,n)},_.image.View.prototype.setWindowLevelMinMax=function(){var e=this.getWindowLevelMinMax();this.setWindowLevel(e.getCenter(),e.getWidth(),"minmax")},_.image.View.prototype.generateImageData=function(e){var t=this.getCurrentWindowLut(),n=this.getImage(),i=n.getGeometry().getSize().getSliceSize(),r=i*this.getCurrentPosition().k,o=this.getCurrentFrame()?this.getCurrentFrame():0,a=0,s=0,l=0,u=n.getPhotometricInterpretation();switch(u){case"MONOCHROME1":case"MONOCHROME2":for(var c=this.getColourMap(),d=r+i,S=r;Sn.getEnd().getX()?0:-1,g=n.getBegin().getY()>n.getEnd().getY()?-1:.5,m=new T.Label({x:n.getEnd().getX()+25*x,y:n.getEnd().getY()+15*g,name:"label"});m.add(S),m.add(new T.Tag);var h=new T.Group;return h.name("line-group"),h.add(i),h.add(d),h.add(m),h.visible(!0),h},_.tool.UpdateArrow=function(e){var t=e.getParent(),n=t.getChildren(function(e){return"shape"===e.name()})[0],i=t.getChildren(function(e){return"shape-triangle"===e.name()})[0],r=t.getChildren(function(e){return"label"===e.name()})[0],o=t.getChildren(function(e){return"begin"===e.id()})[0],a=t.getChildren(function(e){return"end"===e.id()})[0];switch(e.id()){case"begin":o.x(e.x()),o.y(e.y());break;case"end":a.x(e.x()),a.y(e.y())}var s=o.x()-n.x(),l=o.y()-n.y(),u=a.x()-n.x(),c=a.y()-n.y();n.points([s,l,u,c]);var d=new _.math.Point2D(o.x(),o.y()),S=new _.math.Point2D(a.x(),a.y()),x=new _.math.Line(d,S),g=new _.math.Point2D(s,l),m=new _.math.Point2D(u,c),h=_.math.getPerpendicularLine(x,g,10),p=_.math.getPerpendicularLine(x,m,10);n.hitFunc(function(e){e.beginPath(),e.moveTo(h.getBegin().getX(),h.getBegin().getY()),e.lineTo(h.getEnd().getX(),h.getEnd().getY()),e.lineTo(p.getEnd().getX(),p.getEnd().getY()),e.lineTo(p.getBegin().getX(),p.getBegin().getY()),e.closePath(),e.fillStrokeShape(this)});var f=new _.math.Point2D(x.getBegin().getX(),x.getBegin().getY()-10),C=new _.math.Line(x.getBegin(),f),y=_.math.getAngle(x,C),D=y*Math.PI/180;i.x(x.getBegin().getX()+i.radius()*Math.sin(D)),i.y(x.getBegin().getY()+i.radius()*Math.cos(D)),i.rotation(-y);var v=r.getText();v.quant=null,v.setText(v.textExpr);var T=x.getBegin().getX()>x.getEnd().getX()?0:-1,L=x.getBegin().getY()>x.getEnd().getY()?-1:.5,I={x:x.getEnd().getX()+25*T,y:x.getEnd().getY()+15*L};r.position(I)},(_=_||{}).tool=_.tool||{};T=T||{};_.tool.Draw=function(x,e){var i=this,n=null,r=!1;this.shapeFactoryList=e;var o=null,a=null,s=null;this.shapeName=0;var l=[],u=null,g=new _.tool.ShapeEditor(x);g.setDrawEventCallback(D);var m=new T.Group,t=new T.Line({points:[-10,-10,10,10],stroke:"red"}),c=new T.Line({points:[10,-10,-10,10],stroke:"red"});m.width(20),m.height(20),m.add(t),m.add(c),this.style=new _.html.Style;var d={},h=null;function S(e){s&&s.destroy(),(s=o.create(e,i.style,x.getImage())).getChildren(_.draw.isNodeNameShape)[0].listening(!1),h.hitGraphEnabled(!1),h.add(s),h.draw()}function p(e){s&&s.destroy();var t=o.create(e,i.style,x.getImage());t.id(_.math.guid()),x.getDrawController().getCurrentPosGroup().add(t),h.hitGraphEnabled(!0),(a=new _.tool.DrawGroupCommand(t,i.shapeName,h)).onExecute=D,a.onUndo=D,a.execute(),x.addToUndoStack(a),i.setShapeOn(t)}function f(){C(!0)}function C(e){h.listening(e),h.hitGraphEnabled(e);var t=x.getDrawController().getCurrentPosGroup().getChildren();e?(x.addToolCanvasListeners(x.getDrawStage().getContent()),t.forEach(function(e){i.setShapeOn(e)})):(x.removeToolCanvasListeners(x.getDrawStage().getContent()),t.forEach(function(e){var t;(t=e).off("mouseover"),t.off("mouseout"),t.draggable(!1),t.off("dragstart.draw"),t.off("dragmove.draw"),t.off("dragend.draw"),t.off("dblclick")})),h.draw()}function y(e){var t=x.getDrawStage();return{x:t.offset().x+e.x/t.scale().x,y:t.offset().y+e.y/t.scale().y}}function D(e){if(void 0!==d[e.type])for(var t=0;ti.getEnd().getX()?0:-1,x=i.getBegin().getY()>i.getEnd().getY()?-1:.5,g=new T.Label({x:i.getEnd().getX()+25*S,y:i.getEnd().getY()+15*x,name:"label"});g.add(d),g.add(new T.Tag);var m=new T.Group;return m.name("ruler-group"),m.add(r),m.add(s),m.add(u),m.add(g),m.visible(!0),m},_.tool.UpdateRuler=function(e,t){var n=e.getParent(),i=n.getChildren(function(e){return"shape"===e.name()})[0],r=n.getChildren(function(e){return"shape-tick0"===e.name()})[0],o=n.getChildren(function(e){return"shape-tick1"===e.name()})[0],a=n.getChildren(function(e){return"label"===e.name()})[0],s=n.getChildren(function(e){return"begin"===e.id()})[0],l=n.getChildren(function(e){return"end"===e.id()})[0];switch(e.id()){case"begin":s.x(e.x()),s.y(e.y());break;case"end":l.x(e.x()),l.y(e.y())}var u=s.x()-i.x(),c=s.y()-i.y(),d=l.x()-i.x(),S=l.y()-i.y();i.points([u,c,d,S]);var x=new _.math.Point2D(s.x(),s.y()),g=new _.math.Point2D(l.x(),l.y()),m=new _.math.Line(x,g),h=new _.math.Point2D(u,c),p=new _.math.Point2D(d,S),f=_.math.getPerpendicularLine(m,h,10);r.points([f.getBegin().getX(),f.getBegin().getY(),f.getEnd().getX(),f.getEnd().getY()]);var C=_.math.getPerpendicularLine(m,p,10);o.points([C.getBegin().getX(),C.getBegin().getY(),C.getEnd().getX(),C.getEnd().getY()]),i.hitFunc(function(e){e.beginPath(),e.moveTo(f.getBegin().getX(),f.getBegin().getY()),e.lineTo(f.getEnd().getX(),f.getEnd().getY()),e.lineTo(C.getEnd().getX(),C.getEnd().getY()),e.lineTo(C.getBegin().getX(),C.getBegin().getY()),e.closePath(),e.fillStrokeShape(this)});var y=t.quantifyLine(m),D=a.getText();D.quant=y,D.setText(_.utils.replaceFlags(D.textExpr,D.quant));var v=m.getBegin().getX()>m.getEnd().getX()?0:-1,T=m.getBegin().getY()>m.getEnd().getY()?-1:.5,L={x:m.getEnd().getX()+25*v,y:m.getEnd().getY()+15*T};a.position(L)},(_=_||{}).tool=_.tool||{},_.tool.Scroll=function(o){var a=this,t=null;this.started=!1;var n=null;function i(e){var t=1!==o.getImage().getGeometry().getSize().getNumberOfSlices(),n=1!==o.getImage().getNumberOfFrames();e?t?o.getViewController().incrementSliceNb():n&&o.getViewController().incrementFrameNb():t?o.getViewController().decrementSliceNb():n&&o.getViewController().decrementFrameNb()}this.mousedown=function(e){o.getViewController().isPlaying()&&o.getViewController().stop(),a.started=!0,a.x0=e._x,a.y0=e._y},this.mousemove=function(e){if(a.started){var t=e._y-a.y0,n=15