Skip to content

Commit

Permalink
Feat/parse raw dicom (#194)
Browse files Browse the repository at this point in the history
* v1.8.6

* feat(dicom-parser):Parse non-Part 10 binary DICOM (raw) format

* dropping back to older node to allow webpack build

* Code inspection comments

* Documented the raw dicom reading capability

* Fixing the parser to handle raw dimse and raw  file

* Removed unneeded dimseTransfer syntax setup - was using wrong tsuid
  • Loading branch information
wayfarer3130 authored Feb 7, 2022
1 parent d982570 commit 305e78b
Show file tree
Hide file tree
Showing 14 changed files with 1,389 additions and 1,182 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: 2
defaults: &defaults
working_directory: ~/repo
docker:
- image: circleci/node:latest
- image: circleci/node:16.13

jobs:
test:
Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
dicomParser
===========

dicomParser is a lightweight library for parsing DICOM P10 byte streams in modern HTML5 based web browsers (IE10+),
dicomParser is a lightweight library for parsing DICOM P10 byte streams, as well as raw (not encapsulated in part 10) byte streams, in modern HTML5 based web browsers (IE10+),
Node.js and Meteor. dicomParser is fast, easy to use and has no required external dependencies.

Live Examples
Expand Down Expand Up @@ -52,8 +52,10 @@ var byteArray = new Uint8Array(arrayBuffer);
try
{
// Parse the byte array to get a DataSet object that has the parsed contents
var dataSet = dicomParser.parseDicom(byteArray/*, options */);
// Allow raw files
const options = { TransferSyntaxUID: '1.2.840.10008.1.2' };
// Parse the byte array to get a DataSet object that has the parsed contents
var dataSet = dicomParser.parseDicom(byteArray, options);
// access a string element
var studyInstanceUid = dataSet.string('x0020000d');
Expand Down Expand Up @@ -86,6 +88,10 @@ Options

```dicomParser.parseDicom``` accepts an optional second argument that is an options object. The accepted properties are:

#### TransferSyntaxUID
A string value used as the default transfer syntax uid for parsing raw DICOM (not encapsualted in Part 10).
For raw DICOM files, this value should be the LEI UID value.

#### untilTag

A tag in the form xggggeeee (where gggg is the hexadecimal group number and eeee is the hexadecimal element number,
Expand Down
2,420 changes: 1,272 additions & 1,148 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dicom-parser",
"version": "1.8.5",
"version": "1.8.6",
"description": "Javascript parser for DICOM Part 10 data",
"main": "dist/dicomParser.min.js",
"module": "dist/dicomParser.min.js",
Expand Down Expand Up @@ -50,9 +50,9 @@
"webpack:watch": "webpack --progress --watch --config ./config/webpack"
},
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/eslint-parser": "^7.15.7",
"@babel/preset-env": "^7.15.6",
"@babel/core": "^7.17.0",
"@babel/eslint-parser": "^7.17.0",
"@babel/preset-env": "^7.16.11",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2",
"chai": "^4.3.4",
Expand Down
2 changes: 1 addition & 1 deletion src/byteArrayParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/**
* 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. Trailing and leading
* but will not affect advancement of the position. Trailing and leading
* spaces are preserved (not trimmed)
* @param byteArray the byteArray to read from
* @param position the position in the byte array to read from
Expand Down
4 changes: 4 additions & 0 deletions src/byteStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ export default class ByteStream {
return new ByteStream(this.byteArrayParser, byteArrayView);
}

getSize() {
return this.byteArray.length;
}

/**
*
* Parses an unsigned int 16 from a byte array and advances
Expand Down
2 changes: 2 additions & 0 deletions src/dataSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ export default class DataSet {
string (tag, index) {
var element = this.elements[tag];

if( element && element.Value ) return element.Value;

if (element && element.length > 0) {
var fixedString = readFixedString(this.byteArray, element.dataOffset, element.length);

Expand Down
2 changes: 2 additions & 0 deletions src/findEndOfEncapsulatedPixelData.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export default function findEndOfEncapsulatedElement (byteStream, element, warni
const basicOffsetTableItemlength = byteStream.readUint32();
const numFragments = basicOffsetTableItemlength / 4;

// Bad idea to not include the basic offset table, as it means writing the data out is inconsistent with reading it
// but leave this for now. To fix later.
for (let i = 0; i < numFragments; i++) {
const offset = byteStream.readUint32();

Expand Down
8 changes: 5 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import findAndSetUNElementLength from './findAndSetUNElementLength.js';
import findEndOfEncapsulatedElement from './findEndOfEncapsulatedPixelData.js';
import findItemDelimitationItemAndSetElementLength from './findItemDelimitationItem.js';
import littleEndianByteArrayParser from './littleEndianByteArrayParser.js';
import parseDicom from './parseDicom.js';
import parseDicom, { LEI, LEE } from './parseDicom.js';
import readDicomElementExplicit from './readDicomElementExplicit.js';
import readDicomElementImplicit from './readDicomElementImplicit.js';
import readEncapsulatedImageFrame from './readEncapsulatedImageFrame.js';
Expand Down Expand Up @@ -66,7 +66,9 @@ const dicomParser = {
readSequenceItemsExplicit,
readSequenceItemsImplicit,
readSequenceItem,
readTag
readTag,
LEI,
LEE,
};

export {
Expand Down Expand Up @@ -104,4 +106,4 @@ export {
readTag
};

export default dicomParser;
export default dicomParser;
37 changes: 25 additions & 12 deletions src/parseDicom.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ import sharedCopy from './sharedCopy.js';
import * as byteArrayParser from './byteArrayParser.js';
import * as parseDicomDataSet from './parseDicomDataSet.js';

// LEE (Little Endian Explicit) is the transfer syntax used in dimse operations when there is a split
// between the header and data.
const LEE = '1.2.840.10008.1.2.1';

// LEI (Little Endian Implicit) is the transfer syntax in raw files
const LEI = '1.2.840.10008.1.2';

// BEI (Big Endian Implicit) is deprecated, but needs special parse handling
const BEI = '1.2.840.10008.1.2.2';

/**
* Parses a DICOM P10 byte array and returns a DataSet object with the parsed elements.
* If the options argument is supplied and it contains the untilTag property, parsing
Expand All @@ -20,22 +30,23 @@ import * as parseDicomDataSet from './parseDicomDataSet.js';
* property dataSet with the elements successfully parsed before the error.
*/

export default function parseDicom (byteArray, options) {
export default function parseDicom(byteArray, options = {}) {
if (byteArray === undefined) {
throw 'dicomParser.parseDicom: missing required parameter \'byteArray\'';
throw new Error('dicomParser.parseDicom: missing required parameter \'byteArray\'');
}

function readTransferSyntax (metaHeaderDataSet) {

const readTransferSyntax = (metaHeaderDataSet) => {
if (metaHeaderDataSet.elements.x00020010 === undefined) {
throw 'dicomParser.parseDicom: missing required meta header attribute 0002,0010';
throw new Error('dicomParser.parseDicom: missing required meta header attribute 0002,0010');
}

const transferSyntaxElement = metaHeaderDataSet.elements.x00020010;

return byteArrayParser.readFixedString(byteArray, transferSyntaxElement.dataOffset, transferSyntaxElement.length);
return transferSyntaxElement && transferSyntaxElement.Value ||
byteArrayParser.readFixedString(byteArray, transferSyntaxElement.dataOffset, transferSyntaxElement.length);
}

function isExplicit (transferSyntax) {
function isExplicit(transferSyntax) {
// implicit little endian
if (transferSyntax === '1.2.840.10008.1.2') {
return false;
Expand All @@ -45,7 +56,7 @@ export default function parseDicom (byteArray, options) {
return true;
}

function getDataSetByteStream (transferSyntax, position) {
function getDataSetByteStream(transferSyntax, position) {
// Detect whether we are inside a browser or Node.js
const isNode = (Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]');

Expand Down Expand Up @@ -93,7 +104,7 @@ export default function parseDicom (byteArray, options) {
}

// explicit big endian
if (transferSyntax === '1.2.840.10008.1.2.2') {
if (transferSyntax === BEI) {
return new ByteStream(bigEndianByteArrayParser, byteArray, position);
}

Expand All @@ -102,7 +113,7 @@ export default function parseDicom (byteArray, options) {
return new ByteStream(littleEndianByteArrayParser, byteArray, position);
}

function mergeDataSets (metaHeaderDataSet, instanceDataSet) {
function mergeDataSets(metaHeaderDataSet, instanceDataSet) {
for (const propertyName in metaHeaderDataSet.elements) {
if (metaHeaderDataSet.elements.hasOwnProperty(propertyName)) {
instanceDataSet.elements[propertyName] = metaHeaderDataSet.elements[propertyName];
Expand All @@ -116,7 +127,7 @@ export default function parseDicom (byteArray, options) {
return instanceDataSet;
}

function readDataSet (metaHeaderDataSet) {
function readDataSet(metaHeaderDataSet) {
const transferSyntax = readTransferSyntax(metaHeaderDataSet);
const explicit = isExplicit(transferSyntax);
const dataSetByteStream = getDataSetByteStream(transferSyntax, metaHeaderDataSet.position);
Expand Down Expand Up @@ -145,7 +156,7 @@ export default function parseDicom (byteArray, options) {
}

// main function here
function parseTheByteStream () {
function parseTheByteStream() {
const metaHeaderDataSet = readPart10Header(byteArray, options);
const dataSet = readDataSet(metaHeaderDataSet);

Expand All @@ -155,3 +166,5 @@ export default function parseDicom (byteArray, options) {
// This is where we actually start parsing
return parseTheByteStream();
}

export { LEI, LEE, BEI };
34 changes: 28 additions & 6 deletions src/readPart10Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,59 @@ import readDicomElementExplicit from './readDicomElementExplicit.js';
* tag is encoutered. This can be used to parse partial byte streams.
*
* @param byteArray the byte array
* @param options object to control parsing behavior (optional)
* @param options Optional options values
* TransferSyntaxUID: String to specify a default raw transfer syntax UID.
* Use the LEI transfer syntax for raw files, or the provided one for SCP transfers.
* @returns {DataSet}
* @throws error if an error occurs while parsing. The exception object will contain a property dataSet with the
* elements successfully parsed before the error.
*/

export default function readPart10Header (byteArray, options) {
export default function readPart10Header (byteArray, options = {}) {
if (byteArray === undefined) {
throw 'dicomParser.readPart10Header: missing required parameter \'byteArray\'';
}

const { TransferSyntaxUID } = options;
const littleEndianByteStream = new ByteStream(littleEndianByteArrayParser, byteArray);

function readPrefix () {
function readPrefix() {
if (littleEndianByteStream.getSize() <= 132 && TransferSyntaxUID) {
return false;
}
littleEndianByteStream.seek(128);
const prefix = littleEndianByteStream.readFixedString(4);

if (prefix !== 'DICM') {
throw 'dicomParser.readPart10Header: DICM prefix not found at location 132 - this is not a valid DICOM P10 file.';
const { TransferSyntaxUID } = options || {};
if (!TransferSyntaxUID) {
throw 'dicomParser.readPart10Header: DICM prefix not found at location 132 - this is not a valid DICOM P10 file.';
}
littleEndianByteStream.seek(0);
return false;
}
return true;
}

// main function here
function readTheHeader () {
function readTheHeader() {
// Per the DICOM standard, the header is always encoded in Explicit VR Little Endian (see PS3.10, section 7.1)
// so use littleEndianByteStream throughout this method regardless of the transfer syntax
readPrefix();
const isPart10 = readPrefix();

const warnings = [];
const elements = {};

if (!isPart10) {
littleEndianByteStream.position = 0;
const metaHeaderDataSet = {
elements: { x00020010: { tag: 'x00020010', vr: 'UI', Value: TransferSyntaxUID } },
warnings,
};
// console.log('Returning metaHeaderDataSet', metaHeaderDataSet);
return metaHeaderDataSet;
}

while (littleEndianByteStream.position < littleEndianByteStream.byteArray.length) {
const position = littleEndianByteStream.position;
const element = readDicomElementExplicit(littleEndianByteStream, warnings);
Expand Down
2 changes: 1 addition & 1 deletion src/util/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ export {
explicitElementToString,
explicitDataSetToJS,
createJPEGBasicOffsetTable
};
};
2 changes: 1 addition & 1 deletion src/version.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export default '1.8.5';
export default '1.8.6';
36 changes: 34 additions & 2 deletions test/parseDicom_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,26 @@ describe('parseDicom', () => {

return convertToByteArray(rawData);
}


/**
*
* @returns a DICOM file consisting of just the raw data
*/
function makeRawData() {
const rawData = [
// Transfer Syntax UID
// x00020010 UI 20 1.2.840.10008.1.2.1 (Explicit VR Little Endian)
// Slice Location
// x00201041 DS 4 '-43'
0x20,0x00,0x41,0x10, 0x44,0x53, 0x04,0x00, 0x2D,0x34,0x33,0x00,
// Rows
// x00280010 US 2 512
0x28,0x00,0x10,0x00, 0x55,0x53, 0x02,0x00, 0x00,0x02
];

return convertToByteArray(rawData);
}

function assertMetaHeaderElements(dataSet, transferSyntax, groupLength) {
expect(dataSet.uint32('x00020000')).to.equal(groupLength); // '(0002,0000) was not read correctly');
expect(dataSet.uint16('x00020001')).to.equal(256); //'(0002,0001) was not read correctly');
Expand Down Expand Up @@ -174,7 +193,20 @@ describe('parseDicom', () => {
expect(dataSet.uint16('x00280010')).to.equal(512);
expect(dataSet.string('x00201041')).to.equal('-43');
});


it('should parse the dataset correctly (raw LEE)', () => {
// Arrange
const byteArray = makeRawData();

// Act
const dataSet = parseDicom(byteArray, {TransferSyntaxUID: '1.2.840.10008.1.2.1'});

// Assert
// assertMetaHeaderElements(dataSet, '1.2.840.10008.1.2.1', 159);
expect(dataSet.uint16('x00280010')).to.equal(512);
expect(dataSet.string('x00201041')).to.equal('-43');
});

it('should parse the dataset correctly (implicitLittleEndian)', () => {
// Arrange
const byteArray = makeImplicitLittleEndianTestData();
Expand Down

0 comments on commit 305e78b

Please sign in to comment.