From 6eafaa852bb7ea653b5d721477f9ca790e6cfc15 Mon Sep 17 00:00:00 2001 From: Chris Hafey Date: Sat, 29 Mar 2014 22:08:26 -0500 Subject: [PATCH] First commit --- .gitignore | 3 + LICENSE | 2 +- README.md | 59 ++- bower.json | 22 + dist/dicomParser.js | 696 +++++++++++++++++++++++++++ examples/dragAndDropParse/index.html | 125 +++++ gruntfile.js | 48 ++ package.json | 26 + src/byteArrayParser.js | 99 ++++ src/dataSet.js | 218 +++++++++ src/littleEndianByteStream.js | 126 +++++ src/parseDicom.js | 112 +++++ src/parseDicomDataSet.js | 54 +++ src/parseDicomElement.js | 87 ++++ test/byteArrayParserTest.js | 116 +++++ test/dataSetTest.js | 222 +++++++++ test/index.html | 33 ++ test/littleEndianByteStreamTest.js | 362 ++++++++++++++ test/parseDicomDataSetTest.js | 63 +++ test/parseDicomElementTest.js | 119 +++++ 20 files changed, 2590 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 bower.json create mode 100644 dist/dicomParser.js create mode 100644 examples/dragAndDropParse/index.html create mode 100644 gruntfile.js create mode 100644 package.json create mode 100644 src/byteArrayParser.js create mode 100644 src/dataSet.js create mode 100644 src/littleEndianByteStream.js create mode 100644 src/parseDicom.js create mode 100644 src/parseDicomDataSet.js create mode 100644 src/parseDicomElement.js create mode 100644 test/byteArrayParserTest.js create mode 100644 test/dataSetTest.js create mode 100644 test/index.html create mode 100644 test/littleEndianByteStreamTest.js create mode 100644 test/parseDicomDataSetTest.js create mode 100644 test/parseDicomElementTest.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..78dbd62 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +bower_components +.idea \ No newline at end of file diff --git a/LICENSE b/LICENSE index 63c3fd3..dd77003 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 chafey +Copyright (c) 2014 Chris Hafey (chafey@gmail.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 631ca0c..7005062 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,61 @@ dicomParser =========== -Javascript DICOM Parser +Javascript parser for DICOM Part 10 byte streams like you might get from WADO. Target environment +is the browser, not Node.js + +[Click here for an live example of the library in action!](https://rawgithub.com/chafey/dicomParser/master/example/dragAndDropParse/index.html) + + +Features +======== +* Alpha - not released + +Features +======== + +* Parses DICOM Part 10 byte streams + +Backlog +======== + +* Add support for sequences +* Add support for elements with undefined lengths +* Add conversion functions for the VR's that don't have them yet +* Figure out how to automatically generate documentation from the source (jsdoc) +* Create bower package +* Add support for AMD loaders +* Create more examples + +Build System +============ + +This project uses grunt to build the software. + +Pre-requisites: +--------------- + +NodeJs - [click to visit web site for installation instructions](http://nodejs.org). + +grunt-cli + +> npm install -g grunt-cli + +Common Tasks +------------ + +Update dependencies (after each pull): +> npm install + +> bower install + +Running the build: +> grunt + +Automatically running the build and unit tests after each source change: +> grunt watch + + +Copyright +------------ +Copyright 2014 Chris Hafey \ No newline at end of file diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..75dcaee --- /dev/null +++ b/bower.json @@ -0,0 +1,22 @@ +{ + "name": "DICOM Parser", + "description": "Javascript parser for DICOM Part 10 data", + "version": "0.0.1", + "license": "MIT", + "authors" : ["Chris Hafey"], + "homepage": "https://github.com/chafey/dicomParser", + "keywords": ["DICOM", "medical", "imaging"], + "repository": { + "type": "git", + "url": "https://github.com/chafey/dicomParser.git" + }, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test" + ], + "devDependencies": { + "qunit": "~1.14.0" + } +} diff --git a/dist/dicomParser.js b/dist/dicomParser.js new file mode 100644 index 0000000..476cf83 --- /dev/null +++ b/dist/dicomParser.js @@ -0,0 +1,696 @@ +/** + * Internal helper functions for parsing different types from a byte array + */ + +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + /** + * + * Parses an unsigned int 16 from a little endian byte stream and advances + * the position by 2 bytes + * + * @param byteArray the byteArray to read from + * @param position the position in the byte array to read from + * @returns {*} the parsed unsigned int 16 + * @throws error if buffer overread would occur + * @access private + */ + dicomParser.readUint16 = function(byteArray, position) + { + if(position < 0) { + throw 'uint16 - position cannot be less than 0'; + } + if(position + 2 > byteArray.length) { + throw 'uint16 - buffer overread'; + } + return byteArray[position] + (byteArray[position + 1] << 8); + }; + + /** + * Parses an unsigned int 32 from a little endian byte stream and advances + * the position by 2 bytes + * + * @param byteArray the byteArray to read from + * @param position the position in the byte array to read from + * @returns {*} the parse unsigned int 32 + * @throws error if buffer overread would occur + * @access private + */ + dicomParser.readUint32 = function(byteArray, position) { + if(position < 0) + { + throw 'readFixedString - length cannot be less than 0'; + } + + if(position + 4 > byteArray.length) { + throw 'uint32 - buffer overread'; + } + + return (byteArray[position] + + (byteArray[position + 1] << 8) + + (byteArray[position + 2] << 16) + + (byteArray[position + 3] << 24)); + }; + + /** + * Reads a string of 8 bit characters from an array of bytes and advances + * the position by length bytes. A null terminator will end the string + * but will not effect advancement of the position. + * @param byteArray the byteArray to read from + * @param position the position in the byte array to read from + * @param length the maximum number of bytes to parse + * @returns {string} the parsed string + * @throws error if buffer overread would occur + * @access private + */ + dicomParser.readFixedString = function(byteArray, position, length) + { + if(length < 0) + { + throw 'readFixedString - length cannot be less than 0'; + } + + if(position + length > byteArray.length) { + throw 'readFixedString - buffer overread'; + } + + var result = ""; + for(var i=0; i < length; i++) + { + var byte = byteArray[position + i]; + if(byte === 0) { + position += length; + return result; + } + result += String.fromCharCode(byte); + } + return result; + }; + + + return dicomParser; +}(dicomParser)); +/** + * + * The DataSet class encapsulates a collection of DICOM Elements and provides various functions + * to access the data in those elements + * + */ +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + /** + * Constructs a new DataSet given byteArray and collection of elements + * @param byteArray + * @param elements + * @constructor + */ + dicomParser.DataSet = function(byteArray, elements) + { + this.byteArray = byteArray; + this.elements = elements; + }; + + /** + * Finds the element for tag and returns an unsigned int 16 if it exists and has data + * @param tag The DICOM tag in the format xGGGGEEEE + * @returns {*} unsigned int 16 or undefined if the attribute is not present or doesn't have data of length 2 + */ + dicomParser.DataSet.prototype.uint16 = function(tag) + { + var element = this.elements[tag]; + if(element && element.length === 2) + { + return dicomParser.readUint16(this.byteArray, element.dataOffset); + } + return undefined; + }; + + /** + * Finds the element for tag and returns an unsigned int 32 if it exists and has data + * @param tag The DICOM tag in the format xGGGGEEEE + * @returns {*} unsigned int 32 or undefined if the attribute is not present or doesn't have data of length 4 + */ + dicomParser.DataSet.prototype.uint32 = function(tag) + { + var element = this.elements[tag]; + if(element && element.length === 4) + { + return dicomParser.readUint32(this.byteArray, element.dataOffset); + } + return undefined; + }; + + /** + * Returns the number of string values for the element + * @param tag The DICOM tag in the format xGGGGEEEE + * @returns {*} the number of string values or undefined if the attribute is not present or has zero length data + */ + dicomParser.DataSet.prototype.numStringValues = function(tag) + { + var element = this.elements[tag]; + if(element && element.length > 0) + { + var fixedString = dicomParser.readFixedString(this.byteArray, element.dataOffset, element.length); + var numMatching = fixedString.match(/\\/g); + if(numMatching === null) + { + return 1; + } + return numMatching.length + 1; + } + return undefined; + }; + + /** + * Returns the full string for the element index is not specified or if specified, + * the value at the specified index for a multi-valued string + * @param tag The DICOM tag in the format xGGGGEEEE + * @param index the index of the desired value in a multi valued string or undefined for the entire string + * @returns {*} + */ + dicomParser.DataSet.prototype.string = function(tag, index) + { + var element = this.elements[tag]; + if(element && element.length > 0) + { + var fixedString = dicomParser.readFixedString(this.byteArray, element.dataOffset, element.length); + if(index >= 0) + { + var values = fixedString.split('\\'); + return values[index]; + } + else + { + return fixedString; + } + } + return undefined; + }; + + /** + * Parses a string to a float for the specified index in a multi-valued element. If index is not specified, + * the first value in a multi-valued VR will be parsed if present. + * @param tag The DICOM tag in the format xGGGGEEEE + * @param index the index of the desired value in a multi valued string or undefined for the first value + * @returns {*} a floating point number or undefined if not present or data not long enough + */ + dicomParser.DataSet.prototype.floatString = function(tag, index) + { + var element = this.elements[tag]; + if(element && element.length > 0) + { + index |= 0; + var value = this.string(tag, index); + if(value) { + return parseFloat(value); + } + } + return undefined; + }; + + /** + * Parses a string to an integer for the specified index in a multi-valued element. If index is not specified, + * the first value in a multi-valued VR will be parsed if present. + * @param tag The DICOM tag in the format xGGGGEEEE + * @param index the index of the desired value in a multi valued string or undefined for the first value + * @returns {*} an integer or undefined if not present or data not long enough + */ + dicomParser.DataSet.prototype.intString = function(tag, index) + { + var element = this.elements[tag]; + if(element && element.length > 0) { + index |= 0; + var value = this.string(tag, index); + return parseInt(value); + } + return undefined; + }; + + /** + * Parses a DA formatted string into a Javascript Date object + * @param tag The DICOM tag in the format xGGGGEEEE + * @param index + * @returns {*} Javascript Date object or undefined if not present or not 8 bytes long + */ + dicomParser.DataSet.prototype.date = function(tag, index) + { + var value = this.string(tag, index); + if(value && value.length === 8) + { + var yyyy = parseInt(value.substring(0, 4), 10); + var mm = parseInt(value.substring(4, 6), 10); + var dd = parseInt(value.substring(6, 8), 10); + + return new Date(yyyy, mm - 1, dd); + } + return undefined; + }; + + /** + * Parses a TM formatted string into a javascript object with properties for hours, minutes, seconds and fractionalSeconds + * @param tag The DICOM tag in the format xGGGGEEEE + * @param index + * @returns {*} javascript object with properties for hours, minutes, seconds and fractionalSeconds or undefined if no element or data + */ + dicomParser.DataSet.prototype.time = function(tag, index) + { + var value = this.string(tag, index); + if(value && value.length >= 2) // must at least have HH + { + // 0123456789 + // HHMMSS.FFFFFF + var hh = parseInt(value.substring(0, 2), 10); + var mm = value.length >= 4 ? parseInt(value.substring(2, 4), 10) : 0; + var ss = value.length >= 6 ? parseInt(value.substring(4, 6), 10) : 0; + var fff = value.length >= 7 ? parseInt(value.substring(7, 13), 10) : 0; /// note - javascript date object is only precise to milliseconds + + return { + hours: hh, + minutes: mm, + seconds: ss, + fractionalSeconds: fff + }; + } + return undefined; + }; + + /** + * Parses a PN formatted string into a javascript object with properties for givenName, familyName, middleName, prefix and suffix + * @param tag The DICOM tag in the format xGGGGEEEE + * @param index + * @returns {*} javascript object with properties for givenName, familyName, middleName, prefix and suffix or undefined if no element or data + */ + dicomParser.DataSet.prototype.personName = function(tag, index) + { + var stringValue = this.string(tag, index); + if(stringValue) + { + var stringValues = stringValue.split('^'); + return { + familyName: stringValues[0], + givenName: stringValues[1], + middleName: stringValues[2], + prefix: stringValues[3], + suffix: stringValues[4] + }; + } + return undefined; + }; + + //dicomParser.DataSet = DataSet; + + return dicomParser; +}(dicomParser)); +/** + * + * Interal helper class to assist with parsing class supports reading from a little endian byte + * stream contained in an Uint18Array. Example usage: + * + * var byteArray = new Uint8Array(32); + * var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + * + * */ +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + /** + * Constructor for LittleEndianByteStream objects. + * @param byteArray a Uint8Array containing the byte stream + * @param position (optional) the position to start reading from. 0 if not specified + * @constructor + * @throws will throw an error the byteArray parameter is not present or invalid + * @throws will throw an error the position parameter is not inside the byteArray + */ + dicomParser.LittleEndianByteStream = function(byteArray, position) { + this.byteArray = byteArray; + this.position = position ? position : 0; + + if(!byteArray) + { + throw "missing required parameter 'byteArray'"; + } + if((byteArray instanceof Uint8Array) === false) { + throw 'parameter byteArray is not of type Uint8Array'; + } + if(this.position < 0) + { + throw "parameter 'position' cannot be less than 0"; + } + if(this.position >= byteArray.length) + { + throw "parameter 'position' cannot be larger than 'byteArray' length"; + } + }; + + /** + * Safely seeks through the byte stream. Will throw an exception if an attempt + * is made to seek outside of the byte array. + * @param offset the number of bytes to add to the position + * @throws error if seek would cause position to be outside of the byteArray + */ + dicomParser.LittleEndianByteStream.prototype.seek = function(offset) + { + if(this.position + offset < 0) + { + throw "cannot seek to position < 0"; + } + this.position += offset; + }; + + + /** + * Returns a new LittleEndianByteStream object from the current position and of the requested number of bytes + * @param numBytes the length of the byteArray for the LittleEndianByteStream to contain + * @returns {dicomParser.LittleEndianByteStream} + * @throws error if buffer overread would occur + */ + dicomParser.LittleEndianByteStream.prototype.readByteStream = function(numBytes) + { + if(this.position + numBytes > this.byteArray.length) { + throw 'readByteStream - buffer overread'; + } + var byteArrayView = new Uint8Array(this.byteArray.buffer, this.position, numBytes); + this.position += numBytes; + return new dicomParser.LittleEndianByteStream(byteArrayView); + }; + + /** + * + * Parses an unsigned int 16 from a little endian byte stream and advances + * the position by 2 bytes + * + * @returns {*} the parsed unsigned int 16 + * @throws error if buffer overread would occur + */ + dicomParser.LittleEndianByteStream.prototype.readUint16 = function() + { + var result = dicomParser.readUint16(this.byteArray, this.position); + this.position += 2; + return result; + }; + + /** + * Parses an unsigned int 32 from a little endian byte stream and advances + * the position by 2 bytes + * + * @returns {*} the parse unsigned int 32 + * @throws error if buffer overread would occur + */ + dicomParser.LittleEndianByteStream.prototype.readUint32 = function() + { + var result = dicomParser.readUint32(this.byteArray, this.position); + this.position += 4; + return result; + }; + + /** + * Reads a string of 8 bit characters from an array of bytes and advances + * the position by length bytes. A null terminator will end the string + * but will not effect advancement of the position. + * @param length the maximum number of bytes to parse + * @returns {string} the parsed string + * @throws error if buffer overread would occur + */ + dicomParser.LittleEndianByteStream.prototype.readFixedString = function(length) + { + var result = dicomParser.readFixedString(this.byteArray, this.position, length); + this.position += length; + return result; + + }; + + return dicomParser; +}(dicomParser)); +/** + * This module contains the entry point for parsing a DICOM P10 byte stream + * + */ + +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + /** + * Parses a DICOM P10 byte array and returns a DataSet object + * @type {Function} + * @param byteArray the byte array + * @returns {DataSet} + */ + dicomParser.parseDicom = function(byteArray) { + + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + if(!byteArray) + { + throw "missing required parameter 'byteStream'"; + } + + function readPrefix() + { + byteStream.seek(128); + var prefix = byteStream.readFixedString(4); + if(prefix !== "DICM") + { + throw "DICM prefix not found at location 132 in this byteStream"; + } + } + + function readPart10Header() + { + readPrefix(); + + // Read the group length element so we know how many bytes needed + // to read the entire meta header + var groupLengthElement = dicomParser.parseDicomElementExplicit(byteStream); + var metaHeaderLength = dicomParser.readUint32(byteStream.byteArray, groupLengthElement.dataOffset); + var positionAfterMetaHeader = byteStream.position + metaHeaderLength; + + var metaHeaderDataSet = dicomParser.parseDicomDataSetExplicit(byteStream, positionAfterMetaHeader); + metaHeaderDataSet[groupLengthElement.tag] = groupLengthElement; + return metaHeaderDataSet; + } + + function isExplicit(metaHeaderDataSet) { + if(metaHeaderDataSet.elements.x00020010 === undefined) { + throw 'missing required meta header attribute 0002,0010'; + } + var transferSyntaxElement = metaHeaderDataSet.elements.x00020010; + var transferSyntax = dicomParser.readFixedString(byteStream.byteArray, transferSyntaxElement.dataOffset, transferSyntaxElement.length); + if(transferSyntax === '1.2.840.10008.1.2') // implicit little endian + { + return false; + } + else if(transferSyntax === '1.2.840.10008.1.2.2') + { + throw 'explicit big endian transfer syntax not supported'; + } + // all other transfer syntaxes should be explicit + return true; + } + + function mergeDataSets(metaHeaderDataSet, instanceDataSet) + { + for (var propertyName in metaHeaderDataSet.elements) + { + if(metaHeaderDataSet.elements.hasOwnProperty(propertyName)) + { + instanceDataSet.elements[propertyName] = metaHeaderDataSet.elements[propertyName]; + } + } + return instanceDataSet; + } + + function readDataSet(metaHeaderDataSet) + { + var explicit = isExplicit(metaHeaderDataSet); + + if(explicit) { + return dicomParser.parseDicomDataSetExplicit(byteStream); + } + else + { + return dicomParser.parseDicomDataSetImplicit(byteStream); + } + } + + // main function here + function parseTheByteStream() { + var metaHeaderDataSet = readPart10Header(); + + var dataSet = readDataSet(metaHeaderDataSet); + + return mergeDataSets(metaHeaderDataSet, dataSet); + } + + // This is where we actually start parsing + return parseTheByteStream(); + }; + + return dicomParser; +}(dicomParser)); +/** + * Internal helper functions for parsing implicit and explicit DICOM data sets + */ + +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + function parseDicomDataSetExplicit(byteStream, maxPosition) { + if(!byteStream) + { + throw "missing required parameter 'byteStream'"; + } + + maxPosition = maxPosition ||byteStream.byteArray.length; + + var elements = {}; + + while(byteStream.position < maxPosition) + { + var element = dicomParser.parseDicomElementExplicit(byteStream); + elements[element.tag] = element; + } + return new dicomParser.DataSet(byteStream.byteArray, elements); + } + + function parseDicomDataSetImplicit(byteStream, maxPosition) { + if(!byteStream) + { + throw "missing required parameter 'byteStream'"; + } + + var elements = {}; + + maxPosition = maxPosition ? maxPosition : byteStream.byteArray.length; + + while(byteStream.position < maxPosition) + { + var element = dicomParser.parseDicomElementImplicit(byteStream); + elements[element.tag] = element; + } + return new dicomParser.DataSet(byteStream.byteArray, elements); + } + + dicomParser.parseDicomDataSetExplicit = parseDicomDataSetExplicit; + dicomParser.parseDicomDataSetImplicit = parseDicomDataSetImplicit; + + return dicomParser; +}(dicomParser)); +/** + * Internal helper functions for for parsing DICOM elements + */ + +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + function getDataLengthSizeInBytesForVR(vr) + { + if( vr === 'OB' || + vr === 'OW' || + vr === 'SQ' || + vr === 'OF' || + vr === 'UT' || + vr === 'UN') + { + return 4; + } + else + { + return 2; + } + } + + function readTag(byteStream) + { + var groupNumber = byteStream.readUint16(); + var elementNumber = byteStream.readUint16(); + return "x" + ('00000000' + ((groupNumber << 16) + elementNumber).toString(16)).substr(-8); + } + + dicomParser.parseDicomElementImplicit = function(byteStream) + { + if(!byteStream) + { + throw "missing required parameter 'byteStream'"; + } + + var element = { + tag : readTag(byteStream), + length : byteStream.readUint32(), + dataOffset : byteStream.position + }; + + byteStream.seek(element.length); + return element; + }; + + dicomParser.parseDicomElementExplicit = function(byteStream) + { + if(!byteStream) + { + throw "missing required parameter 'byteStream'"; + } + + var element = { + tag : readTag(byteStream), + vr : byteStream.readFixedString(2) + // length set below based on VR + // dataOffset set below based on VR and size of length + }; + + var dataLengthSizeBytes = getDataLengthSizeInBytesForVR(element.vr); + if(dataLengthSizeBytes === 2) + { + element.length = byteStream.readUint16(); + element.dataOffset = byteStream.position; + } + else + { + byteStream.seek(2); + element.length = byteStream.readUint32(); + element.dataOffset = byteStream.position; + } + + byteStream.seek(element.length); + return element; + }; + + return dicomParser; +}(dicomParser)); \ No newline at end of file diff --git a/examples/dragAndDropParse/index.html b/examples/dragAndDropParse/index.html new file mode 100644 index 0000000..33654df --- /dev/null +++ b/examples/dragAndDropParse/index.html @@ -0,0 +1,125 @@ + + + + + + + + + + + + +
+ + + +
+
+
+
+
+
+ +
+ + + + + + diff --git a/gruntfile.js b/gruntfile.js new file mode 100644 index 0000000..35f6c0b --- /dev/null +++ b/gruntfile.js @@ -0,0 +1,48 @@ +module.exports = function(grunt) { + + // Project configuration. + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + clean: { + default: { + src: [ + 'dist', + 'docs' + ] + } + }, + concat: { + dist: { + src : ['src/*.js'], + dest: 'dist/dicomParser.js' + } + }, + jshint: { + files: [ + 'src/*.js' + ] + }, + qunit: { + all: ['test/*.html'] + }, + jsdoc : { + dist : { + src: ['src/*.js', 'test/*.js'], + options: { + destination: 'docs' + } + } + }, + watch: { + scripts: { + files: ['src/*.js', 'test/*.js'], + tasks: ['concat', 'jshint', 'qunit'] + } + } + }); + + require('load-grunt-tasks')(grunt); + + grunt.registerTask('buildAll', ['clean','concat', 'jshint', 'qunit']); + grunt.registerTask('default', ['buildAll']); +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..b94ebc2 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "dicomParser", + "version": "0.0.1", + "description": "Javascript parser for DICOM Part 10 data", + "keywords": ["DICOM", "medical", "imaging"], + "author" : "Chris Hafey", + "homepage": "https://github.com/chafey/dicomParser", + "licnense" : "MIT", + "repository": { + "type": "git", + "url": "https://github.com/chafey/dicomParser.git" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "devDependencies": { + "grunt-contrib-copy": "0.4.x", + "grunt-contrib-qunit": "^0.4.0", + "grunt-contrib-concat": "^0.3.0", + "grunt-contrib-watch": "^0.6.1", + "grunt-contrib-clean": "^0.5.0", + "grunt-contrib-jshint": "^0.8.0", + "load-grunt-tasks": "^0.2.1", + "grunt-jsdoc": "^0.5.4" + } +} diff --git a/src/byteArrayParser.js b/src/byteArrayParser.js new file mode 100644 index 0000000..13c0b4a --- /dev/null +++ b/src/byteArrayParser.js @@ -0,0 +1,99 @@ +/** + * Internal helper functions for parsing different types from a byte array + */ + +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + /** + * + * Parses an unsigned int 16 from a little endian byte stream and advances + * the position by 2 bytes + * + * @param byteArray the byteArray to read from + * @param position the position in the byte array to read from + * @returns {*} the parsed unsigned int 16 + * @throws error if buffer overread would occur + * @access private + */ + dicomParser.readUint16 = function(byteArray, position) + { + if(position < 0) { + throw 'uint16 - position cannot be less than 0'; + } + if(position + 2 > byteArray.length) { + throw 'uint16 - buffer overread'; + } + return byteArray[position] + (byteArray[position + 1] << 8); + }; + + /** + * Parses an unsigned int 32 from a little endian byte stream and advances + * the position by 2 bytes + * + * @param byteArray the byteArray to read from + * @param position the position in the byte array to read from + * @returns {*} the parse unsigned int 32 + * @throws error if buffer overread would occur + * @access private + */ + dicomParser.readUint32 = function(byteArray, position) { + if(position < 0) + { + throw 'readFixedString - length cannot be less than 0'; + } + + if(position + 4 > byteArray.length) { + throw 'uint32 - buffer overread'; + } + + return (byteArray[position] + + (byteArray[position + 1] << 8) + + (byteArray[position + 2] << 16) + + (byteArray[position + 3] << 24)); + }; + + /** + * Reads a string of 8 bit characters from an array of bytes and advances + * the position by length bytes. A null terminator will end the string + * but will not effect advancement of the position. + * @param byteArray the byteArray to read from + * @param position the position in the byte array to read from + * @param length the maximum number of bytes to parse + * @returns {string} the parsed string + * @throws error if buffer overread would occur + * @access private + */ + dicomParser.readFixedString = function(byteArray, position, length) + { + if(length < 0) + { + throw 'readFixedString - length cannot be less than 0'; + } + + if(position + length > byteArray.length) { + throw 'readFixedString - buffer overread'; + } + + var result = ""; + for(var i=0; i < length; i++) + { + var byte = byteArray[position + i]; + if(byte === 0) { + position += length; + return result; + } + result += String.fromCharCode(byte); + } + return result; + }; + + + return dicomParser; +}(dicomParser)); \ No newline at end of file diff --git a/src/dataSet.js b/src/dataSet.js new file mode 100644 index 0000000..607923a --- /dev/null +++ b/src/dataSet.js @@ -0,0 +1,218 @@ +/** + * + * The DataSet class encapsulates a collection of DICOM Elements and provides various functions + * to access the data in those elements + * + */ +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + /** + * Constructs a new DataSet given byteArray and collection of elements + * @param byteArray + * @param elements + * @constructor + */ + dicomParser.DataSet = function(byteArray, elements) + { + this.byteArray = byteArray; + this.elements = elements; + }; + + /** + * Finds the element for tag and returns an unsigned int 16 if it exists and has data + * @param tag The DICOM tag in the format xGGGGEEEE + * @returns {*} unsigned int 16 or undefined if the attribute is not present or doesn't have data of length 2 + */ + dicomParser.DataSet.prototype.uint16 = function(tag) + { + var element = this.elements[tag]; + if(element && element.length === 2) + { + return dicomParser.readUint16(this.byteArray, element.dataOffset); + } + return undefined; + }; + + /** + * Finds the element for tag and returns an unsigned int 32 if it exists and has data + * @param tag The DICOM tag in the format xGGGGEEEE + * @returns {*} unsigned int 32 or undefined if the attribute is not present or doesn't have data of length 4 + */ + dicomParser.DataSet.prototype.uint32 = function(tag) + { + var element = this.elements[tag]; + if(element && element.length === 4) + { + return dicomParser.readUint32(this.byteArray, element.dataOffset); + } + return undefined; + }; + + /** + * Returns the number of string values for the element + * @param tag The DICOM tag in the format xGGGGEEEE + * @returns {*} the number of string values or undefined if the attribute is not present or has zero length data + */ + dicomParser.DataSet.prototype.numStringValues = function(tag) + { + var element = this.elements[tag]; + if(element && element.length > 0) + { + var fixedString = dicomParser.readFixedString(this.byteArray, element.dataOffset, element.length); + var numMatching = fixedString.match(/\\/g); + if(numMatching === null) + { + return 1; + } + return numMatching.length + 1; + } + return undefined; + }; + + /** + * Returns the full string for the element index is not specified or if specified, + * the value at the specified index for a multi-valued string + * @param tag The DICOM tag in the format xGGGGEEEE + * @param index the index of the desired value in a multi valued string or undefined for the entire string + * @returns {*} + */ + dicomParser.DataSet.prototype.string = function(tag, index) + { + var element = this.elements[tag]; + if(element && element.length > 0) + { + var fixedString = dicomParser.readFixedString(this.byteArray, element.dataOffset, element.length); + if(index >= 0) + { + var values = fixedString.split('\\'); + return values[index]; + } + else + { + return fixedString; + } + } + return undefined; + }; + + /** + * Parses a string to a float for the specified index in a multi-valued element. If index is not specified, + * the first value in a multi-valued VR will be parsed if present. + * @param tag The DICOM tag in the format xGGGGEEEE + * @param index the index of the desired value in a multi valued string or undefined for the first value + * @returns {*} a floating point number or undefined if not present or data not long enough + */ + dicomParser.DataSet.prototype.floatString = function(tag, index) + { + var element = this.elements[tag]; + if(element && element.length > 0) + { + index |= 0; + var value = this.string(tag, index); + if(value) { + return parseFloat(value); + } + } + return undefined; + }; + + /** + * Parses a string to an integer for the specified index in a multi-valued element. If index is not specified, + * the first value in a multi-valued VR will be parsed if present. + * @param tag The DICOM tag in the format xGGGGEEEE + * @param index the index of the desired value in a multi valued string or undefined for the first value + * @returns {*} an integer or undefined if not present or data not long enough + */ + dicomParser.DataSet.prototype.intString = function(tag, index) + { + var element = this.elements[tag]; + if(element && element.length > 0) { + index |= 0; + var value = this.string(tag, index); + return parseInt(value); + } + return undefined; + }; + + /** + * Parses a DA formatted string into a Javascript Date object + * @param tag The DICOM tag in the format xGGGGEEEE + * @param index + * @returns {*} Javascript Date object or undefined if not present or not 8 bytes long + */ + dicomParser.DataSet.prototype.date = function(tag, index) + { + var value = this.string(tag, index); + if(value && value.length === 8) + { + var yyyy = parseInt(value.substring(0, 4), 10); + var mm = parseInt(value.substring(4, 6), 10); + var dd = parseInt(value.substring(6, 8), 10); + + return new Date(yyyy, mm - 1, dd); + } + return undefined; + }; + + /** + * Parses a TM formatted string into a javascript object with properties for hours, minutes, seconds and fractionalSeconds + * @param tag The DICOM tag in the format xGGGGEEEE + * @param index + * @returns {*} javascript object with properties for hours, minutes, seconds and fractionalSeconds or undefined if no element or data + */ + dicomParser.DataSet.prototype.time = function(tag, index) + { + var value = this.string(tag, index); + if(value && value.length >= 2) // must at least have HH + { + // 0123456789 + // HHMMSS.FFFFFF + var hh = parseInt(value.substring(0, 2), 10); + var mm = value.length >= 4 ? parseInt(value.substring(2, 4), 10) : 0; + var ss = value.length >= 6 ? parseInt(value.substring(4, 6), 10) : 0; + var fff = value.length >= 7 ? parseInt(value.substring(7, 13), 10) : 0; /// note - javascript date object is only precise to milliseconds + + return { + hours: hh, + minutes: mm, + seconds: ss, + fractionalSeconds: fff + }; + } + return undefined; + }; + + /** + * Parses a PN formatted string into a javascript object with properties for givenName, familyName, middleName, prefix and suffix + * @param tag The DICOM tag in the format xGGGGEEEE + * @param index + * @returns {*} javascript object with properties for givenName, familyName, middleName, prefix and suffix or undefined if no element or data + */ + dicomParser.DataSet.prototype.personName = function(tag, index) + { + var stringValue = this.string(tag, index); + if(stringValue) + { + var stringValues = stringValue.split('^'); + return { + familyName: stringValues[0], + givenName: stringValues[1], + middleName: stringValues[2], + prefix: stringValues[3], + suffix: stringValues[4] + }; + } + return undefined; + }; + + //dicomParser.DataSet = DataSet; + + return dicomParser; +}(dicomParser)); \ No newline at end of file diff --git a/src/littleEndianByteStream.js b/src/littleEndianByteStream.js new file mode 100644 index 0000000..c31adf3 --- /dev/null +++ b/src/littleEndianByteStream.js @@ -0,0 +1,126 @@ +/** + * + * Interal helper class to assist with parsing class supports reading from a little endian byte + * stream contained in an Uint18Array. Example usage: + * + * var byteArray = new Uint8Array(32); + * var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + * + * */ +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + /** + * Constructor for LittleEndianByteStream objects. + * @param byteArray a Uint8Array containing the byte stream + * @param position (optional) the position to start reading from. 0 if not specified + * @constructor + * @throws will throw an error the byteArray parameter is not present or invalid + * @throws will throw an error the position parameter is not inside the byteArray + */ + dicomParser.LittleEndianByteStream = function(byteArray, position) { + this.byteArray = byteArray; + this.position = position ? position : 0; + + if(!byteArray) + { + throw "missing required parameter 'byteArray'"; + } + if((byteArray instanceof Uint8Array) === false) { + throw 'parameter byteArray is not of type Uint8Array'; + } + if(this.position < 0) + { + throw "parameter 'position' cannot be less than 0"; + } + if(this.position >= byteArray.length) + { + throw "parameter 'position' cannot be larger than 'byteArray' length"; + } + }; + + /** + * Safely seeks through the byte stream. Will throw an exception if an attempt + * is made to seek outside of the byte array. + * @param offset the number of bytes to add to the position + * @throws error if seek would cause position to be outside of the byteArray + */ + dicomParser.LittleEndianByteStream.prototype.seek = function(offset) + { + if(this.position + offset < 0) + { + throw "cannot seek to position < 0"; + } + this.position += offset; + }; + + + /** + * Returns a new LittleEndianByteStream object from the current position and of the requested number of bytes + * @param numBytes the length of the byteArray for the LittleEndianByteStream to contain + * @returns {dicomParser.LittleEndianByteStream} + * @throws error if buffer overread would occur + */ + dicomParser.LittleEndianByteStream.prototype.readByteStream = function(numBytes) + { + if(this.position + numBytes > this.byteArray.length) { + throw 'readByteStream - buffer overread'; + } + var byteArrayView = new Uint8Array(this.byteArray.buffer, this.position, numBytes); + this.position += numBytes; + return new dicomParser.LittleEndianByteStream(byteArrayView); + }; + + /** + * + * Parses an unsigned int 16 from a little endian byte stream and advances + * the position by 2 bytes + * + * @returns {*} the parsed unsigned int 16 + * @throws error if buffer overread would occur + */ + dicomParser.LittleEndianByteStream.prototype.readUint16 = function() + { + var result = dicomParser.readUint16(this.byteArray, this.position); + this.position += 2; + return result; + }; + + /** + * Parses an unsigned int 32 from a little endian byte stream and advances + * the position by 2 bytes + * + * @returns {*} the parse unsigned int 32 + * @throws error if buffer overread would occur + */ + dicomParser.LittleEndianByteStream.prototype.readUint32 = function() + { + var result = dicomParser.readUint32(this.byteArray, this.position); + this.position += 4; + return result; + }; + + /** + * Reads a string of 8 bit characters from an array of bytes and advances + * the position by length bytes. A null terminator will end the string + * but will not effect advancement of the position. + * @param length the maximum number of bytes to parse + * @returns {string} the parsed string + * @throws error if buffer overread would occur + */ + dicomParser.LittleEndianByteStream.prototype.readFixedString = function(length) + { + var result = dicomParser.readFixedString(this.byteArray, this.position, length); + this.position += length; + return result; + + }; + + return dicomParser; +}(dicomParser)); \ No newline at end of file diff --git a/src/parseDicom.js b/src/parseDicom.js new file mode 100644 index 0000000..a01bee9 --- /dev/null +++ b/src/parseDicom.js @@ -0,0 +1,112 @@ +/** + * This module contains the entry point for parsing a DICOM P10 byte stream + * + */ + +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + /** + * Parses a DICOM P10 byte array and returns a DataSet object + * @type {Function} + * @param byteArray the byte array + * @returns {DataSet} + */ + dicomParser.parseDicom = function(byteArray) { + + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + if(!byteArray) + { + throw "missing required parameter 'byteStream'"; + } + + function readPrefix() + { + byteStream.seek(128); + var prefix = byteStream.readFixedString(4); + if(prefix !== "DICM") + { + throw "DICM prefix not found at location 132 in this byteStream"; + } + } + + function readPart10Header() + { + readPrefix(); + + // Read the group length element so we know how many bytes needed + // to read the entire meta header + var groupLengthElement = dicomParser.parseDicomElementExplicit(byteStream); + var metaHeaderLength = dicomParser.readUint32(byteStream.byteArray, groupLengthElement.dataOffset); + var positionAfterMetaHeader = byteStream.position + metaHeaderLength; + + var metaHeaderDataSet = dicomParser.parseDicomDataSetExplicit(byteStream, positionAfterMetaHeader); + metaHeaderDataSet[groupLengthElement.tag] = groupLengthElement; + return metaHeaderDataSet; + } + + function isExplicit(metaHeaderDataSet) { + if(metaHeaderDataSet.elements.x00020010 === undefined) { + throw 'missing required meta header attribute 0002,0010'; + } + var transferSyntaxElement = metaHeaderDataSet.elements.x00020010; + var transferSyntax = dicomParser.readFixedString(byteStream.byteArray, transferSyntaxElement.dataOffset, transferSyntaxElement.length); + if(transferSyntax === '1.2.840.10008.1.2') // implicit little endian + { + return false; + } + else if(transferSyntax === '1.2.840.10008.1.2.2') + { + throw 'explicit big endian transfer syntax not supported'; + } + // all other transfer syntaxes should be explicit + return true; + } + + function mergeDataSets(metaHeaderDataSet, instanceDataSet) + { + for (var propertyName in metaHeaderDataSet.elements) + { + if(metaHeaderDataSet.elements.hasOwnProperty(propertyName)) + { + instanceDataSet.elements[propertyName] = metaHeaderDataSet.elements[propertyName]; + } + } + return instanceDataSet; + } + + function readDataSet(metaHeaderDataSet) + { + var explicit = isExplicit(metaHeaderDataSet); + + if(explicit) { + return dicomParser.parseDicomDataSetExplicit(byteStream); + } + else + { + return dicomParser.parseDicomDataSetImplicit(byteStream); + } + } + + // main function here + function parseTheByteStream() { + var metaHeaderDataSet = readPart10Header(); + + var dataSet = readDataSet(metaHeaderDataSet); + + return mergeDataSets(metaHeaderDataSet, dataSet); + } + + // This is where we actually start parsing + return parseTheByteStream(); + }; + + return dicomParser; +}(dicomParser)); \ No newline at end of file diff --git a/src/parseDicomDataSet.js b/src/parseDicomDataSet.js new file mode 100644 index 0000000..c376d7b --- /dev/null +++ b/src/parseDicomDataSet.js @@ -0,0 +1,54 @@ +/** + * Internal helper functions for parsing implicit and explicit DICOM data sets + */ + +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + function parseDicomDataSetExplicit(byteStream, maxPosition) { + if(!byteStream) + { + throw "missing required parameter 'byteStream'"; + } + + maxPosition = maxPosition ||byteStream.byteArray.length; + + var elements = {}; + + while(byteStream.position < maxPosition) + { + var element = dicomParser.parseDicomElementExplicit(byteStream); + elements[element.tag] = element; + } + return new dicomParser.DataSet(byteStream.byteArray, elements); + } + + function parseDicomDataSetImplicit(byteStream, maxPosition) { + if(!byteStream) + { + throw "missing required parameter 'byteStream'"; + } + + var elements = {}; + + maxPosition = maxPosition ? maxPosition : byteStream.byteArray.length; + + while(byteStream.position < maxPosition) + { + var element = dicomParser.parseDicomElementImplicit(byteStream); + elements[element.tag] = element; + } + return new dicomParser.DataSet(byteStream.byteArray, elements); + } + + dicomParser.parseDicomDataSetExplicit = parseDicomDataSetExplicit; + dicomParser.parseDicomDataSetImplicit = parseDicomDataSetImplicit; + + return dicomParser; +}(dicomParser)); \ No newline at end of file diff --git a/src/parseDicomElement.js b/src/parseDicomElement.js new file mode 100644 index 0000000..b162a7e --- /dev/null +++ b/src/parseDicomElement.js @@ -0,0 +1,87 @@ +/** + * Internal helper functions for for parsing DICOM elements + */ + +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + function getDataLengthSizeInBytesForVR(vr) + { + if( vr === 'OB' || + vr === 'OW' || + vr === 'SQ' || + vr === 'OF' || + vr === 'UT' || + vr === 'UN') + { + return 4; + } + else + { + return 2; + } + } + + function readTag(byteStream) + { + var groupNumber = byteStream.readUint16(); + var elementNumber = byteStream.readUint16(); + return "x" + ('00000000' + ((groupNumber << 16) + elementNumber).toString(16)).substr(-8); + } + + dicomParser.parseDicomElementImplicit = function(byteStream) + { + if(!byteStream) + { + throw "missing required parameter 'byteStream'"; + } + + var element = { + tag : readTag(byteStream), + length : byteStream.readUint32(), + dataOffset : byteStream.position + }; + + byteStream.seek(element.length); + return element; + }; + + dicomParser.parseDicomElementExplicit = function(byteStream) + { + if(!byteStream) + { + throw "missing required parameter 'byteStream'"; + } + + var element = { + tag : readTag(byteStream), + vr : byteStream.readFixedString(2) + // length set below based on VR + // dataOffset set below based on VR and size of length + }; + + var dataLengthSizeBytes = getDataLengthSizeInBytesForVR(element.vr); + if(dataLengthSizeBytes === 2) + { + element.length = byteStream.readUint16(); + element.dataOffset = byteStream.position; + } + else + { + byteStream.seek(2); + element.length = byteStream.readUint32(); + element.dataOffset = byteStream.position; + } + + byteStream.seek(element.length); + return element; + }; + + return dicomParser; +}(dicomParser)); \ No newline at end of file diff --git a/test/byteArrayParserTest.js b/test/byteArrayParserTest.js new file mode 100644 index 0000000..6ce65f1 --- /dev/null +++ b/test/byteArrayParserTest.js @@ -0,0 +1,116 @@ + +(function(dicomParser) { + module("dicomParser.byteArrayParser"); + + test("readUint16", function() { + // Arrange + var byteArray = new Uint8Array(32); + byteArray[0] = 0xff; + byteArray[1] = 0x80; + + // Act + var uint16 = dicomParser.readUint16(byteArray, 0); + + // Assert + equal(uint16, 0x80ff, "readUint16 did not return expected value"); + }); + + test("readUint16 throws on buffer overread", function() { + // Arrange + var byteArray = new Uint8Array(32); + + // Act + throws( + function() { + var uint16 = dicomParser.readUint16(byteArray, 31); + }, + "readUint16 did not throw on buffer overread" + ) + }); + + test("readUint16 throws on position < 0", function() { + // Arrange + var byteArray = new Uint8Array(32); + + // Act + throws( + function() { + var uint16 = dicomParser.readUint16(byteArray, -1); + }, + "readUint16 did not throw on buffer overread" + ) + }); + + test("readUint32", function() { + // Arrange + var byteArray = new Uint8Array(32); + byteArray[0] = 0x11; + byteArray[1] = 0x22; + byteArray[2] = 0x33; + byteArray[3] = 0x44; + + // Act + var uint32 = dicomParser.readUint32(byteArray, 0); + + // Assert + equal(uint32, 0x44332211, "readUint32 did not return expected value"); + }); + + test("readUint32 throws on buffer overread", function() { + // Arrange + var byteArray = new Uint8Array(32); + + // Act + throws( + function() { + var uint32 = dicomParser.readUint32(byteArray, 30); + }, + "readUint32 did not throw on buffer overread" + ) + }); + + test("readUint32 throws on position < 0", function() { + // Arrange + var byteArray = new Uint8Array(32); + + // Act + throws( + function() { + var uint16 = dicomParser.readUint32(byteArray, -1); + }, + "readUint32 did not throw on buffer overread" + ) + }); + + test("readFixedString can read at end of buffer", function() { + // Arrange + var byteArray = new Uint8Array(5); + var str = "Hello"; + for(var i=0; i < str.length; i++) { + byteArray[i] = str.charCodeAt(i); + } + + // Act + var fixedString = dicomParser.readFixedString(byteArray, 0, 5); + + // Assert + equal(fixedString, "Hello", "readFixedString did not return expected value"); + }); + + test("readFixedString can read null terminated string", function() { + // Arrange + var byteArray = new Uint8Array(6); + var str = "Hello"; + for(var i=0; i < str.length; i++) { + byteArray[i] = str.charCodeAt(i); + } + + // Act + var fixedString = dicomParser.readFixedString(byteArray, 0, 6); + + // Assert + equal(fixedString, "Hello", "readFixedString did not return expected value"); + }); + + +})(dicomParser); \ No newline at end of file diff --git a/test/dataSetTest.js b/test/dataSetTest.js new file mode 100644 index 0000000..3a18f15 --- /dev/null +++ b/test/dataSetTest.js @@ -0,0 +1,222 @@ + +(function(dicomParser) { + module("dicomParser.dataSet"); + + function makeTestData() + { + var elements = [ + // x22114433 US 2 0xadde + [0x11,0x22,0x33,0x44, 0x55,0x53, 0x02,0x00, 0xde,0xad], + // x22114434 OB 4 "O\B" + [0x11,0x22,0x34,0x44, 0x4F,0x42, 0x00,0x00, 0x04,0x00,0x00,0x00, 0x4F, 0x5C, 0x42,0x00], + // x22114435 DS 8 "1.2\2.3" + [0x11,0x22,0x35,0x44, 0x4F,0x42, 0x00,0x00, 0x08,0x00,0x00,0x00, 0x31,0x2E,0x32, 0x5C, 0x32,0x2E,0x33,0x00], + // x22114436 IS 2 "1.2\2.3" + [0x11,0x22,0x36,0x44, 0x49,0x53, 0x04,0x00, 0x31,0x32,0x33,0x34], + // x22114437 DA 8 "20140329" + [0x11,0x22,0x37,0x44, 0x49,0x53, 0x08,0x00, 0x32,0x30,0x31,0x34,0x30,0x33,0x32,0x39], + // x22114438 TM 14 "081236.531000" + [0x11,0x22,0x38,0x44, 0x49,0x53, 0x0E,0x00, 0x30,0x38,0x31,0x32,0x33,0x36, 0x2E, 0x35,0x33,0x31,0x30,0x30,0x30, 0x00], + // x22114439 PN 10 "F^G^M^P^S" + [0x11,0x22,0x39,0x44, 0x50,0x4E, 0x0A,0x00, 0x46,0x5E,0x47,0x5E,0x4D,0x5E,0x50,0x5E,0x53,0x00] + ]; + + + var arrayLength = 0; + elements.forEach(function(element) { + arrayLength += element.length; + }); + + var byteArray = new Uint8Array(arrayLength); + var index = 0; + elements.forEach(function(element) { + for(var i=0; i < element.length; i++) + { + byteArray[index++] = element[i]; + } + }); + + return byteArray; + } + + + test("DataSet uint16", function() { + // Arrange + var byteArray = makeTestData(); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + var dataSet = dicomParser.parseDicomDataSetExplicit(byteStream); + + // Act + var uint16 = dataSet.uint16('x22114433'); + + // Assert + equal(uint16, 0xadde, "uint16 returned wrong value"); + }); + + test("DataSet uint32", function() { + // Arrange + var byteArray = makeTestData(); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + var dataSet = dicomParser.parseDicomDataSetExplicit(byteStream); + + // Act + var uint32 = dataSet.uint32('x22114434'); + + + // Assert + equal(uint32, 0x00425C4F, "uint32 returned wrong value"); + }); + + test("DataSet numStringValues", function() { + // Arrange + var byteArray = makeTestData(); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + var dataSet = dicomParser.parseDicomDataSetExplicit(byteStream); + + // Act + var numStringValues = dataSet.numStringValues('x22114434'); + + // Assert + equal(numStringValues, 2, "numStringValues returned wrong value"); + }); + + test("DataSet string", function() { + // Arrange + var byteArray = makeTestData(); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + var dataSet = dicomParser.parseDicomDataSetExplicit(byteStream); + + // Act + var str = dataSet.string('x22114434'); + + // Assert + equal(str, 'O\\B', "string returned wrong value"); + }); + + test("DataSet string with index", function() { + // Arrange + var byteArray = makeTestData(); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + var dataSet = dicomParser.parseDicomDataSetExplicit(byteStream); + + // Act + var str = dataSet.string('x22114434', 1); + + // Assert + equal(str, 'B', "string returned wrong value"); + }); + + test("DataSet floatString", function() { + // Arrange + var byteArray = makeTestData(); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + var dataSet = dicomParser.parseDicomDataSetExplicit(byteStream); + + // Act + var float = dataSet.floatString('x22114435', 0); + + // Assert + equal(float, 1.2, "floatString returned wrong value"); + }); + + test("DataSet floatString no index", function() { + // Arrange + var byteArray = makeTestData(); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + var dataSet = dicomParser.parseDicomDataSetExplicit(byteStream); + + // Act + var float = dataSet.floatString('x22114435'); + + // Assert + equal(float, 1.2, "floatString returned wrong value"); + }); + + test("DataSet floatString", function() { + // Arrange + var byteArray = makeTestData(); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + var dataSet = dicomParser.parseDicomDataSetExplicit(byteStream); + + // Act + var float = dataSet.floatString('x22114435', 0); + + // Assert + equal(float, 1.2, "floatString returned wrong value"); + }); + test("DataSet floatString", function() { + // Arrange + var byteArray = makeTestData(); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + var dataSet = dicomParser.parseDicomDataSetExplicit(byteStream); + + // Act + var float = dataSet.floatString('x22114435', 1); + + // Assert + equal(float, 2.3, "floatString returned wrong value"); + }); + test("DataSet intString", function() { + // Arrange + var byteArray = makeTestData(); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + var dataSet = dicomParser.parseDicomDataSetExplicit(byteStream); + + // Act + var val = dataSet.intString('x22114436'); + + // Assert + equal(val, 1234, "intString returned wrong value"); + }); + + test("DataSet date", function() { + // Arrange + var byteArray = makeTestData(); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + var dataSet = dicomParser.parseDicomDataSetExplicit(byteStream); + + // Act + var val = dataSet.date('x22114437'); + + // Assert + equal(val.getFullYear(), 2014, "date returned wrong value"); + equal(val.getMonth(), 2, "date returned wrong value"); + equal(val.getDate(), 29, "date returned wrong value"); + }); + + + test("DataSet time", function() { + // Arrange + var byteArray = makeTestData(); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + var dataSet = dicomParser.parseDicomDataSetExplicit(byteStream); + + // Act + var val = dataSet.time('x22114438'); + + // Assert + equal(val.hours, 8, "time returned wrong value for hours"); + equal(val.minutes, 12, "time returned wrong value for minutes"); + equal(val.seconds, 36, "time returned wrong value for seconds"); + equal(val.fractionalSeconds, 531000, "time returned wrong value for fractionalSeconds"); + }); + + test("DataSet personName", function() { + // Arrange + var byteArray = makeTestData(); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + var dataSet = dicomParser.parseDicomDataSetExplicit(byteStream); + + // Act + var val = dataSet.personName('x22114439'); + + // Assert + equal(val.familyName, 'F', "personName returned wrong value for familyName"); + equal(val.givenName, 'G', "personName returned wrong value for givenName"); + equal(val.middleName, 'M', "personName returned wrong value for middleName"); + equal(val.prefix, 'P', "personName returned wrong value for prefix"); + equal(val.suffix, 'S', "personName returned wrong value for suffix"); + }); + + +})(dicomParser); \ No newline at end of file diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..c91685f --- /dev/null +++ b/test/index.html @@ -0,0 +1,33 @@ + + + + + +   + DICOM Parser Test Runner +   + + + + + + + + + + + + + + + + +   + + +
+    
+      +
+   + \ No newline at end of file diff --git a/test/littleEndianByteStreamTest.js b/test/littleEndianByteStreamTest.js new file mode 100644 index 0000000..9e9d3a0 --- /dev/null +++ b/test/littleEndianByteStreamTest.js @@ -0,0 +1,362 @@ + +(function(dicomParser) { + module("dicomParser.LittleEndianByteStream"); + + test("construction", function() { + // Arrange + var byteArray = new Uint8Array(32); + + // Act + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Assert + ok(byteStream, "construction did not return object"); + }); + + test("position 0 on creation", function() { + // Arrange + var byteArray = new Uint8Array(32); + + // Act + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Assert + equal(byteStream.position, 0, "position 0"); + }); + + test("position 10 on creation", function() { + // Arrange + var byteArray = new Uint8Array(32); + + // Act + var byteStream = new dicomParser.LittleEndianByteStream(byteArray, 10); + + // Assert + equal(byteStream.position, 10, "position 10"); + }); + + test("missing constructor parameter throws", function() { + // Arrange + + // Act + throws( + function() { + var byteStream = new dicomParser.LittleEndianByteStream(); + }, + "construction without byteArray parameter throws" + ) + }); + + test("constructor throws if byteArray parameter is not Uint8Array", function() { + // Arrange + // Arrange + var uint16Array = new Uint16Array(32); + + + // Act + throws( + function() { + var byteStream = new dicomParser.LittleEndianByteStream(uint16Array); + }, + "construction did not throw on invalid type for byteArray parameter" + ) + }); + + + test("position cannot be < 0", function() { + // Arrange + var byteArray = new Uint8Array(32); + + // Act + throws( + function() { + var byteStream = new dicomParser.LittleEndianByteStream(byteArray, -1); + }, + "position cannot be < 0" + ) + }); + + test("position cannot equal or exceed array length", function() { + // Arrange + var byteArray = new Uint8Array(32); + + // Act + throws( + function() { + var byteStream = new dicomParser.LittleEndianByteStream(byteArray, 32); + }, + "position cannot exceed array length" + ) + }); + + test("seek succeeds", function() { + // Arrange + var byteArray = new Uint8Array(32); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + byteStream.seek(10); + + // Assert + equal(byteStream.position, 10, "position 10"); + }); + + test("seek to negative position throws", function() { + // Arrange + var byteArray = new Uint8Array(32); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + throws( + function() { + byteStream.seek(-1); + }, + "seek to negative position not throw" + ) + }); + + test("readByteStream returns object", function() { + // Arrange + var byteArray = new Uint8Array(32); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var subByteStream = byteStream.readByteStream(5); + + // Assert + ok(subByteStream, "readByteStream did not return an object"); + }); + + test("readByteStream returns array with size matching numBytes parameter", function() { + // Arrange + var byteArray = new Uint8Array(32); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var subByteStream = byteStream.readByteStream(5); + + // Assert + equal(subByteStream.byteArray.length, 5, "readByteStream returned object with byteArray of wrong length"); + }); + + test("readByteStream returns object with position 0", function() { + // Arrange + var byteArray = new Uint8Array(32); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var subByteStream = byteStream.readByteStream(5); + + // Assert + equal(subByteStream.position, 0, "readByteStream returned object with position not 0"); + }); + + test("readByteStream can read all remaining bytes", function() { + // Arrange + var byteArray = new Uint8Array(32); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var subByteStream = byteStream.readByteStream(32); + + // Assert + ok(subByteStream, "readByteStream did not return an object"); + }); + + test("readByteStream throws on buffer overread", function() { + // Arrange + var byteArray = new Uint8Array(32); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + throws( + function() { + var subByteStream = byteStream.readByteStream(40); + }, + "readByteStream did not throw on buffer overread" + ) + }); + + test("readUint16 works", function() { + // Arrange + var byteArray = new Uint8Array(32); + byteArray[0] = 0xff; + byteArray[1] = 0x80; + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var uint16 = byteStream.readUint16(); + + // Assert + equal(uint16, 0x80ff, "readUint16 did not return expected value"); + }); + + test("readUint16 can read at end of buffer", function() { + // Arrange + var byteArray = new Uint8Array(2); + byteArray[0] = 0xff; + byteArray[1] = 0x80; + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var uint16 = byteStream.readUint16(); + + // Assert + equal(uint16, 0x80ff, "readUint16 did not return expected value"); + }); + + + test("readUint16 throws on buffer overread", function() { + // Arrange + var byteArray = new Uint8Array(32); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + byteStream.seek(31); + // Act + throws( + function() { + var uint16 = byteStream.readUint16(); + }, + "readUint16 did not throw on buffer overread" + ) + }); + + test("readUint32 works", function() { + // Arrange + var byteArray = new Uint8Array(32); + byteArray[0] = 0x11; + byteArray[1] = 0x22; + byteArray[2] = 0x33; + byteArray[3] = 0x44; + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var uint32 = byteStream.readUint32(); + + // Assert + equal(uint32, 0x44332211, "readUint32 did not return expected value"); + }); + + test("readUint32 can read at end of buffer", function() { + // Arrange + var byteArray = new Uint8Array(4); + byteArray[0] = 0x11; + byteArray[1] = 0x22; + byteArray[2] = 0x33; + byteArray[3] = 0x44; + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var uint32 = byteStream.readUint32(); + + // Assert + equal(uint32, 0x44332211, "readUint32 did not return expected value"); + }); + + + test("readUint32 throws on buffer overread", function() { + // Arrange + var byteArray = new Uint8Array(32); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + byteStream.seek(31); + // Act + throws( + function() { + var uint16 = byteStream.readUint32(); + }, + "readUint32 did not throw on buffer overread" + ) + }); + + test("readFixedString works", function() { + // Arrange + var byteArray = new Uint8Array(32); + var str = "Hello"; + for(var i=0; i < str.length; i++) { + byteArray[i] = str.charCodeAt(i); + } + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var fixedString = byteStream.readFixedString(5); + + // Assert + equal(fixedString, 'Hello', "readFixedString did not return expected value"); + }); + + test("readUint32 throws on buffer overread", function() { + // Arrange + var byteArray = new Uint8Array(32); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + throws( + function() { + var str = byteStream.readFixedString(33); + }, + "readFixedString did not throw on buffer overread" + ) + }); + + test("readUint32 throws on negative length", function() { + // Arrange + var byteArray = new Uint8Array(32); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + throws( + function() { + var str = byteStream.readFixedString(-1); + }, + "readFixedString did not throw on negative length" + ) + }); + + test("readFixedString can read at end of buffer", function() { + // Arrange + var byteArray = new Uint8Array(5); + var str = "Hello"; + for(var i=0; i < str.length; i++) { + byteArray[i] = str.charCodeAt(i); + } + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var fixedString = byteStream.readFixedString(5); + + // Assert + equal(fixedString, "Hello", "readFixedString did not return expected value"); + }); + + test("readFixedString can read null terminated string", function() { + // Arrange + var byteArray = new Uint8Array(6); + var str = "Hello"; + for(var i=0; i < str.length; i++) { + byteArray[i] = str.charCodeAt(i); + } + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var fixedString = byteStream.readFixedString(6); + + // Assert + equal(fixedString, "Hello", "readFixedString did not return expected value"); + }); + + test("readFixedString sets position properly after reading null terminated string", function() { + // Arrange + var byteArray = new Uint8Array(6); + var str = "Hello"; + for(var i=0; i < str.length; i++) { + byteArray[i] = str.charCodeAt(i); + } + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var fixedString = byteStream.readFixedString(6); + + // Assert + equal(byteStream.position, 6, "readFixedString did not set position propery after reading null terminated string"); + }); + +})(dicomParser); \ No newline at end of file diff --git a/test/parseDicomDataSetTest.js b/test/parseDicomDataSetTest.js new file mode 100644 index 0000000..6dd1af7 --- /dev/null +++ b/test/parseDicomDataSetTest.js @@ -0,0 +1,63 @@ + +(function(dicomParser) { + module("dicomParser.parseDicomDataSetExplicit"); + + function makeTestData() + { + var byteArray = new Uint8Array(26); + byteArray[0] = 0x11; + byteArray[1] = 0x22; + byteArray[2] = 0x33; + byteArray[3] = 0x44; + byteArray[4] = 0x4F; // OB + byteArray[5] = 0x42; + byteArray[6] = 0x00; + byteArray[7] = 0x00; + byteArray[8] = 0x00; // length = 0 + byteArray[9] = 0x00; + byteArray[10] = 0x00; + byteArray[11] = 0x00; + byteArray[12] = 0x10; + byteArray[13] = 0x22; + byteArray[14] = 0x33; + byteArray[15] = 0x44; + byteArray[16] = 0x4F; // OB + byteArray[17] = 0x42; + byteArray[18] = 0x00; // OB + byteArray[19] = 0x00; + byteArray[20] = 0x02; // length = 2 + byteArray[21] = 0x00; + byteArray[22] = 0x00; + byteArray[23] = 0x00; + byteArray[24] = 0x00; + byteArray[25] = 0x00; + return byteArray; + } + + + test("parse returns DataSet", function() { + // Arrange + var byteArray = makeTestData(); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var dataSet = dicomParser.parseDicomDataSetExplicit(byteStream); + + // Assert + ok(dataSet, "dataSet created"); + }); + + test("DataSet has two elements", function() { + // Arrange + var byteArray = makeTestData(); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var dataSet = dicomParser.parseDicomDataSetExplicit(byteStream); + + // Assert + ok(dataSet.elements.x22104433, "DataSet does not contain element with tag x22104433"); + ok(dataSet.elements.x22114433, "DataSet does not contain element with tag x22114433"); + }); + +})(dicomParser); \ No newline at end of file diff --git a/test/parseDicomElementTest.js b/test/parseDicomElementTest.js new file mode 100644 index 0000000..5cebdea --- /dev/null +++ b/test/parseDicomElementTest.js @@ -0,0 +1,119 @@ + +(function(dicomParser) { + module("dicomParser.parseDicomElementExplicit"); + + test("returns element", function() { + // Arrange + var byteArray = new Uint8Array(32); + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var element = dicomParser.parseDicomElementExplicit(byteStream); + + // Assert + ok(element, "no element returned"); + }); + + test("parsed tag is correct", function() { + // Arrange + var byteArray = new Uint8Array(32); + byteArray[0] = 0x11; + byteArray[1] = 0x22; + byteArray[2] = 0x33; + byteArray[3] = 0x44; + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var element = dicomParser.parseDicomElementExplicit(byteStream); + + // Assert + equal(element.tag, "x22114433", "tag not correct"); + }); + + test("parsed vr is correct", function() { + // Arrange + var byteArray = new Uint8Array(32); + byteArray[0] = 0x11; + byteArray[1] = 0x22; + byteArray[2] = 0x33; + byteArray[3] = 0x44; + byteArray[4] = 0x53; // ST + byteArray[5] = 0x54; + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var element = dicomParser.parseDicomElementExplicit(byteStream); + + // Assert + equal(element.vr, "ST", "tag not correct"); + }); + + test("parse element for 2 byte length is correct", function() { + // Arrange + var byteArray = new Uint8Array(1024); + byteArray[0] = 0x11; + byteArray[1] = 0x22; + byteArray[2] = 0x33; + byteArray[3] = 0x44; + byteArray[4] = 0x53; // ST + byteArray[5] = 0x54; + byteArray[6] = 0x01; // length of 513 + byteArray[7] = 0x02; + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var element = dicomParser.parseDicomElementExplicit(byteStream); + + // Assert + equal(element.length, 513, "length is not correct"); + }); + + test("parse element for 4 byte length is correct", function() { + // Arrange + var byteArray = new Uint8Array(16909060 + 12); + byteArray[0] = 0x11; + byteArray[1] = 0x22; + byteArray[2] = 0x33; + byteArray[3] = 0x44; + byteArray[4] = 0x4F; // OB + byteArray[5] = 0x42; + byteArray[6] = 0x00; + byteArray[7] = 0x00; + byteArray[8] = 0x04; // 4 overall length = 16909060 = (16777216 + 131072 + 768 + 4) + byteArray[9] = 0x03; // 768 + byteArray[10] = 0x02; // 131072 + byteArray[11] = 0x01; // 16777216 + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var element = dicomParser.parseDicomElementExplicit(byteStream); + + // Assert + equal(element.length, 16909060, "length is not correct"); + }); + + test("parse element has correct data offset", function() { + // Arrange + var byteArray = new Uint8Array(16909060 + 12); + byteArray[0] = 0x11; + byteArray[1] = 0x22; + byteArray[2] = 0x33; + byteArray[3] = 0x44; + byteArray[4] = 0x4F; // OB + byteArray[5] = 0x42; + byteArray[6] = 0x00; + byteArray[7] = 0x00; + byteArray[8] = 0x04; // 4 overall length = 16909060 = (16777216 + 131072 + 768 + 4) + byteArray[9] = 0x03; // 768 + byteArray[10] = 0x02; // 131072 + byteArray[11] = 0x01; // 16777216 + var byteStream = new dicomParser.LittleEndianByteStream(byteArray); + + // Act + var element = dicomParser.parseDicomElementExplicit(byteStream); + + // Assert + equal(element.dataOffset, 12, "dataOffset is not correct"); + }); + +})(dicomParser); \ No newline at end of file