Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/parse raw dicom #194

Merged
merged 7 commits into from
Feb 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
wayfarer3130 marked this conversation as resolved.
Show resolved Hide resolved

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
wayfarer3130 marked this conversation as resolved.
Show resolved Hide resolved
* but will not affect advancement of the position. Trailing and leading
wayfarer3130 marked this conversation as resolved.
Show resolved Hide resolved
* 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() {
wayfarer3130 marked this conversation as resolved.
Show resolved Hide resolved
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