/** * DICOM module. * @module dicom */ var dwv = dwv || {}; dwv.dicom = dwv.dicom || {}; /** * Data reader. * @class DataReader * @namespace dwv.dicom * @constructor * @param {Array} buffer The input array buffer. * @param {Boolean} isLittleEndian Flag to tell if the data is little or big endian. */ dwv.dicom.DataReader = function(buffer, isLittleEndian) { /** * The main data view. * @property view * @private * @type DataView */ var view = new DataView(buffer); // Set endian flag if not defined. if(typeof(isLittleEndian)==='undefined') { isLittleEndian = true; } /** * Read Uint8 (1 byte) data. * @method readUint8 * @param {Number} byteOffset The offset to start reading from. * @return {Number} The read data. */ this.readUint8 = function(byteOffset) { return view.getUint8(byteOffset, isLittleEndian); }; /** * Read Uint16 (2 bytes) data. * @method readUint16 * @param {Number} byteOffset The offset to start reading from. * @return {Number} The read data. */ this.readUint16 = function(byteOffset) { return view.getUint16(byteOffset, isLittleEndian); }; /** * Read Uint32 (4 bytes) data. * @method readUint32 * @param {Number} byteOffset The offset to start reading from. * @return {Number} The read data. */ this.readUint32 = function(byteOffset) { return view.getUint32(byteOffset, isLittleEndian); }; /** * Read Float32 (8 bytes) data. * @method readFloat32 * @param {Number} byteOffset The offset to start reading from. * @return {Number} The read data. */ this.readFloat32 = function(byteOffset) { return view.getFloat32(byteOffset, isLittleEndian); }; /** * Read Uint data of nBytes size. * @method readNumber * @param {Number} byteOffset The offset to start reading from. * @param {Number} nBytes The number of bytes to read. * @return {Number} The read data. */ this.readNumber = function(byteOffset, nBytes) { if( nBytes === 1 ) { return this.readUint8(byteOffset, isLittleEndian); } else if( nBytes === 2 ) { return this.readUint16(byteOffset, isLittleEndian); } else if( nBytes === 4 ) { return this.readUint32(byteOffset, isLittleEndian); } else if( nBytes === 8 ) { return this.readFloat32(byteOffset, isLittleEndian); } else { console.log("Non number: '"+this.readString(byteOffset, nBytes)+"'"); throw new Error("Unsupported number size."); } }; /** * Read Uint8 array. * @method readUint8Array * @param {Number} byteOffset The offset to start reading from. * @param {Number} size The size of the array. * @return {Array} The read data. */ this.readUint8Array = function(byteOffset, size) { var data = new Uint8Array(size); var index = 0; for(var i=byteOffset; i 132: magic word offset = 128; var magicword = metaReader.readString( offset, 4 ); if(magicword !== "DICM") { throw new Error("Not a valid DICOM file (no magic DICM word found)"); } offset += 4; // 0x0002, 0x0000: MetaElementGroupLength var dataElement = this.readDataElement(metaReader, offset); var metaLength = parseInt(dataElement.data, 10); offset += dataElement.offset; // meta elements var metaStart = offset; var metaEnd = metaStart + metaLength; var i = metaStart; while( i < metaEnd ) { // get the data element dataElement = this.readDataElement(metaReader, i, false); // check the transfer syntax if( dataElement.tag.name === "TransferSyntaxUID" ) { var syntax = dwv.utils.cleanString(dataElement.data[0]); // Implicit VR - Little Endian if( syntax === "1.2.840.10008.1.2" ) { implicit = true; } // Explicit VR - Little Endian (default): 1.2.840.10008.1.2.1 // Deflated Explicit VR - Little Endian else if( syntax === "1.2.840.10008.1.2.1.99" ) { throw new Error("Unsupported DICOM transfer syntax (Deflated Explicit VR): "+syntax); } // Explicit VR - Big Endian else if( syntax === "1.2.840.10008.1.2.2" ) { dataReader = new dwv.dicom.DataReader(buffer,false); } // JPEG else if( dwv.dicom.isJpegTransferSyntax(syntax) ) { jpeg = true; //console.log("JPEG compressed DICOM data: " + syntax); throw new Error("Unsupported DICOM transfer syntax (JPEG): "+syntax); } // JPEG-LS else if( dwv.dicom.isJpeglsTransferSyntax(syntax) ) { //console.log("JPEG-LS compressed DICOM data: " + syntax); throw new Error("Unsupported DICOM transfer syntax (JPEG-LS): "+syntax); } // JPEG 2000 else if( dwv.dicom.isJpeg2000TransferSyntax(syntax) ) { console.log("JPEG 2000 compressed DICOM data: " + syntax); jpeg2000 = true; } // MPEG2 Image Compression else if( syntax === "1.2.840.10008.1.2.4.100" ) { throw new Error("Unsupported DICOM transfer syntax (MPEG2): "+syntax); } // RLE (lossless) else if( syntax === "1.2.840.10008.1.2.4.5" ) { throw new Error("Unsupported DICOM transfer syntax (RLE): "+syntax); } } // store the data element this.appendDicomElement( { 'name': dataElement.tag.name, 'group': dataElement.tag.group, 'vr' : dataElement.vr, 'vl' : dataElement.vl, 'element': dataElement.tag.element, 'value': dataElement.data }); // increment index i += dataElement.offset; } var startedPixelItems = false; var tagName = ""; // DICOM data elements while( i < buffer.byteLength ) { // get the data element try { dataElement = this.readDataElement(dataReader, i, implicit); } catch(err) { console.warn("Problem reading at " + i + " / " + buffer.byteLength + ", after " + tagName + ".\n" + err); } tagName = dataElement.tag.name; // store pixel data from multiple items if( startedPixelItems ) { if( tagName === "Item" ) { if( dataElement.data.length === 4 ) { console.log("Skipping Basic Offset Table."); } else if( dataElement.data.length !== 0 ) { console.log("Concatenating multiple pixel data items, length: "+dataElement.data.length); // concat does not work on typed arrays //this.pixelBuffer = this.pixelBuffer.concat( dataElement.data ); // manual concat... var size = dataElement.data.length + this.pixelBuffer.length; var newBuffer = new Uint16Array(size); newBuffer.set( this.pixelBuffer, 0 ); newBuffer.set( dataElement.data, this.pixelBuffer.length ); this.pixelBuffer = newBuffer; } } else if( tagName === "SequenceDelimitationItem" ) { startedPixelItems = false; } else { throw new Error("Unexpected tag in encapsulated pixel data: "+dataElement.tag.name); } } // check the pixel data tag if( tagName === "PixelData") { if( dataElement.data.length !== 0 ) { this.pixelBuffer = dataElement.data; } else { startedPixelItems = true; } } // store the data element this.appendDicomElement( { 'name': tagName, 'group' : dataElement.tag.group, 'vr' : dataElement.vr, 'vl' : dataElement.vl, 'element': dataElement.tag.element, 'value': dataElement.data }); // increment index i += dataElement.offset; } // uncompress data if( jpeg ) { // using jpgjs from https://github.com/notmasteryet/jpgjs // -> error with ffc3 and ffc1 jpeg jfif marker /*var j = new JpegImage(); j.parse(this.pixelBuffer); var d = 0; j.copyToImageData(d); this.pixelBuffer = d.data;*/ } else if( jpeg2000 ) { // decompress pixel buffer into Uint8 image var uint8Image = null; try { uint8Image = openjpeg(this.pixelBuffer, "j2k"); } catch(error) { throw new Error("Cannot decode JPEG 2000 ([" +error.name + "] " + error.message + ")"); } this.pixelBuffer = uint8Image.data; } }; /** * Get an Image object from the read DICOM file. * @method createImage * @returns {View} A new Image. */ dwv.dicom.DicomParser.prototype.createImage = function() { // size if( !this.dicomElements.Columns ) { throw new Error("Missing DICOM image number of columns"); } if( !this.dicomElements.Rows ) { throw new Error("Missing DICOM image number of rows"); } var size = new dwv.image.Size( this.dicomElements.Columns.value[0], this.dicomElements.Rows.value[0] ); // spacing var rowSpacing = 1; var columnSpacing = 1; if( this.dicomElements.PixelSpacing ) { rowSpacing = parseFloat(this.dicomElements.PixelSpacing.value[0]); columnSpacing = parseFloat(this.dicomElements.PixelSpacing.value[1]); } else if( this.dicomElements.ImagerPixelSpacing ) { rowSpacing = parseFloat(this.dicomElements.ImagerPixelSpacing.value[0]); columnSpacing = parseFloat(this.dicomElements.ImagerPixelSpacing.value[1]); } var spacing = new dwv.image.Spacing( columnSpacing, rowSpacing); // special jpeg 2000 case: openjpeg returns a Uint8 planar MONO or RGB image var syntax = dwv.utils.cleanString( this.dicomElements.TransferSyntaxUID.value[0] ); var jpeg2000 = dwv.dicom.isJpeg2000TransferSyntax( syntax ); // buffer data var buffer = null; // convert to 16bit if needed if( jpeg2000 && this.dicomElements.BitsAllocated.value[0] === 16 ) { var sliceSize = size.getSliceSize(); buffer = new Int16Array( sliceSize ); var k = 0; for( var p = 0; p < sliceSize; ++p ) { buffer[p] = 256 * this.pixelBuffer[k] + this.pixelBuffer[k+1]; k += 2; } } else { buffer = new Int16Array(this.pixelBuffer.length); // unsigned to signed data if needed var shift = false; if( this.dicomElements.PixelRepresentation && this.dicomElements.PixelRepresentation.value[0] == 1) { shift = true; } // copy for( var i=0; i= Math.pow(2, 15) ) { buffer[i] -= Math.pow(2, 16); } } } // slice position var slicePosition = new Array(0,0,0); if( this.dicomElements.ImagePositionPatient ) { slicePosition = [ parseFloat(this.dicomElements.ImagePositionPatient.value[0]), parseFloat(this.dicomElements.ImagePositionPatient.value[1]), parseFloat(this.dicomElements.ImagePositionPatient.value[2]) ]; } // image var image = new dwv.image.Image( size, spacing, buffer, [slicePosition] ); // photometricInterpretation if( this.dicomElements.PhotometricInterpretation ) { var photo = dwv.utils.cleanString( this.dicomElements.PhotometricInterpretation.value[0]).toUpperCase(); if( jpeg2000 && photo.match(/YBR/) ) { photo = "RGB"; } image.setPhotometricInterpretation( photo ); } // planarConfiguration if( this.dicomElements.PlanarConfiguration ) { var planar = this.dicomElements.PlanarConfiguration.value[0]; if( jpeg2000 ) { planar = 1; } image.setPlanarConfiguration( planar ); } // rescale slope if( this.dicomElements.RescaleSlope ) { image.setRescaleSlope( parseFloat(this.dicomElements.RescaleSlope.value[0]) ); } // rescale intercept if( this.dicomElements.RescaleIntercept ) { image.setRescaleIntercept( parseFloat(this.dicomElements.RescaleIntercept.value[0]) ); } // meta information var meta = {}; if( this.dicomElements.Modality ) { meta.Modality = this.dicomElements.Modality.value[0]; } if( this.dicomElements.StudyInstanceUID ) { meta.StudyInstanceUID = this.dicomElements.StudyInstanceUID.value[0]; } if( this.dicomElements.SeriesInstanceUID ) { meta.SeriesInstanceUID = this.dicomElements.SeriesInstanceUID.value[0]; } if( this.dicomElements.BitsStored ) { meta.BitsStored = parseInt(this.dicomElements.BitsStored.value[0], 10); } image.setMeta(meta); // pixel representation var isSigned = 0; if( this.dicomElements.PixelRepresentation ) { isSigned = this.dicomElements.PixelRepresentation.value[0]; } // view var view = new dwv.image.View(image, isSigned); // window center and width var windowPresets = []; if( this.dicomElements.WindowCenter && this.dicomElements.WindowWidth ) { var name; for( var j = 0; j < this.dicomElements.WindowCenter.value.length; ++j) { var width = parseFloat( this.dicomElements.WindowWidth.value[j], 10 ); if( width !== 0 ) { if( this.dicomElements.WindowCenterWidthExplanation ) { name = this.dicomElements.WindowCenterWidthExplanation.value[j]; } else { name = "Default"+j; } windowPresets.push({ "center": parseFloat( this.dicomElements.WindowCenter.value[j], 10 ), "width": width, "name": name }); } } } if( windowPresets.length !== 0 ) { view.setWindowPresets( windowPresets ); } else { view.setWindowLevelMinMax(); } return view; };