in src/engine/loaders/LoaderDicom.js [264:1894]
createVolumeFromSlices(volSet, indexSelected, hashSelected) {
// check arguments
console.assert(volSet != null, 'Null volume');
console.assert(volSet instanceof VolumeSet, 'Should be volume set');
console.assert(typeof indexSelected === 'number', 'index should be number');
console.assert(typeof hashSelected === 'number', 'index should be number');
const is16bit = true;
let volDst = null;
if (indexSelected < volSet.getNumVolumes()) {
volDst = volSet.getVolume(indexSelected);
} else {
volDst = new Volume();
volSet.addVolume(volDst);
volDst = volSet.getVolume(indexSelected);
console.assert(volDst !== null);
}
const numSeries = this.m_slicesVolume.m_series.length;
// get serie with given hash
let indSerie = -1;
for (let s = 0; s < numSeries; s++) {
if (this.m_slicesVolume.m_series[s].m_hash === hashSelected) {
indSerie = s;
break;
}
}
console.assert(indSerie >= 0);
const serie = this.m_slicesVolume.m_series[indSerie];
const slice0 = serie.m_slices[0];
const numSlices = serie.m_slices.length;
this.m_xDim = slice0.m_xDim;
this.m_yDim = slice0.m_yDim;
this.m_zDim = serie.m_slices.length;
let xyDim = this.m_xDim * this.m_yDim;
const imagePosBox = {
x: this.m_imagePosMax.x - this.m_imagePosMin.x,
y: this.m_imagePosMax.y - this.m_imagePosMin.y,
z: this.m_imagePosMax.z - this.m_imagePosMin.z,
};
const TOO_MIN = 0.00001;
let zBox;
if (Math.abs(this.m_pixelSpacing.z) > TOO_MIN) {
zBox = this.m_pixelSpacing.z * this.m_zDim;
} else {
zBox = imagePosBox.z;
if (Math.abs(zBox) < TOO_MIN) {
zBox = imagePosBox.x;
if (Math.abs(zBox) < TOO_MIN) {
zBox = imagePosBox.y;
}
}
} // if pixel spacing 0
if (zBox < TOO_MIN) {
zBox = 1.0;
}
// check empty pixel spacing
if (this.m_pixelSpacing.x * this.m_pixelSpacing.y < TOO_MIN) {
this.m_pixelSpacing.x = 1.0;
this.m_pixelSpacing.y = 1.0;
}
this.m_pixelSpacing.z = zBox / this.m_zDim;
this.m_boxSize.z = this.m_zDim * this.m_pixelSpacing.z;
this.m_boxSize.x = this.m_xDim * this.m_pixelSpacing.x;
this.m_boxSize.y = this.m_yDim * this.m_pixelSpacing.y;
console.log(`createVolumeFromSlices. Volume local phys dim: ${this.m_boxSize.x} * ${this.m_boxSize.y} * ${this.m_boxSize.z}`);
let i;
let dataSize = 0;
let dataArray = null;
let dataArray16 = null;
// convert big endian in slices
if (!this.m_littleEndian) {
for (let ser = 0; ser < numSeries; ser++) {
const serie = this.m_slicesVolume.m_series[ser];
if (serie.m_hash !== hashSelected) {
continue;
}
for (let sl = 0; sl < numSlices; sl++) {
const slice = serie.m_slices[sl];
const sliceData16 = slice.m_image;
const xDim = slice.m_xDim;
const yDim = slice.m_yDim;
xyDim = xDim * yDim;
for (i = 0; i < xyDim; i++) {
const val16 = sliceData16[i];
sliceData16[i] = (val16 >> 8) | ((val16 << 8) & 0xffff);
} // for (i) all slice pixels
} // for sl
} // for ser
} // if big endian
// remove pad values or too much values
for (let ser = 0; ser < numSeries; ser++) {
const serie = this.m_slicesVolume.m_series[ser];
if (serie.m_hash !== hashSelected) {
continue;
}
for (let sl = 0; sl < numSlices; sl++) {
const slicePad = serie.m_slices[sl];
const xDim = slicePad.m_xDim;
const yDim = slicePad.m_yDim;
xyDim = xDim * yDim;
for (i = 0; i < xyDim; i++) {
let val16 = slicePad.m_image[i];
// if ((val16 === this.m_padValue) || ((val16 & 0x8000) !== 0)) {
if (val16 === this.m_padValue) {
val16 = 0;
}
slicePad.m_image[i] = val16;
} // for (i) all slice pixels
} // for sl
} // for ser
// dont apply rescale formula here, due to unsigned numbder can became signed, but store
// image in the unsigmed form
// get maximum value from slices (but only for given serie : hash)
let maxVal = -LARGE_NUMBER;
let minVal = +LARGE_NUMBER;
for (let ser = 0; ser < numSeries; ser++) {
const serie = this.m_slicesVolume.m_series[ser];
if (serie.m_hash !== hashSelected) {
continue;
}
for (let sl = 0; sl < numSlices; sl++) {
const slice = serie.m_slices[sl];
const sliceData16 = slice.m_image;
const xDim = slice.m_xDim;
const yDim = slice.m_yDim;
xyDim = xDim * yDim;
for (i = 0; i < xyDim; i++) {
const valData = sliceData16[i] * this.m_rescaleSlope + this.m_rescaleIntercept;
minVal = valData < minVal ? valData : minVal;
maxVal = valData > maxVal ? valData : maxVal;
} // for (i) all slice pixels
} // for sl
} // for ser
this.m_minVal = minVal;
this.m_maxVal = maxVal;
console.log(`Build Volume. min/max value=${minVal}/${maxVal}. Volume dim = ${this.m_xDim}*${this.m_yDim}*${this.m_zDim}`);
console.log(`Min slice number = ${serie.m_minSlice}`);
console.log(`Max slice number = ${serie.m_maxSlice}`);
maxVal = maxVal - minVal > 0 ? maxVal : maxVal + 1;
const BITS_ACCUR = 11;
const BITS_IN_BYTE = 8;
const scale = Math.floor((1 << (BITS_IN_BYTE + BITS_ACCUR)) / (maxVal - minVal));
const TOO_MIN_SCALE = 4;
if (scale <= TOO_MIN_SCALE) {
console.log('Bad scaling: image will be 0');
return LoadResult.ERROR_SCALING;
}
// get slices for selected serie
const srcSlices = serie.m_slices;
const numSlicesByTags = serie.m_maxSlice - serie.m_minSlice + 1;
if (numSlicesByTags !== numSlices) {
console.log(`Sort by location! N slices by tags = ${numSlicesByTags}, but N readed slices = ${numSlices}`);
}
// sort slices via slice location OR slice number
let minSliceNum = srcSlices[0].m_sliceNumber;
let maxSliceNum = srcSlices[0].m_sliceNumber;
for (let s = 0; s < numSlices; s++) {
const num = srcSlices[s].m_sliceNumber;
minSliceNum = num < minSliceNum ? num : minSliceNum;
maxSliceNum = num > maxSliceNum ? num : maxSliceNum;
}
const difSlceNum = maxSliceNum - minSliceNum;
if (difSlceNum > 0) {
// sort slices by slice number (read from dicom tag)
srcSlices.sort((a, b) => {
const zDif = a.m_sliceNumber - b.m_sliceNumber;
return zDif;
});
} else {
// sort slices by slice location (read from diocom tag)
srcSlices.sort((a, b) => {
const zDif = a.m_sliceLocation - b.m_sliceLocation;
return zDif;
});
}
// assign new slice numbers according accending location
let ind = 0;
for (let s = 0; s < numSlices; s++) {
srcSlices[s].m_sliceNumber = ind;
ind++;
}
this.m_zDim = numSlices;
// create out volume data array
// normal copy volume with transform 16 -> 8 bit
dataSize = this.m_xDim * this.m_yDim * this.m_zDim;
dataArray = new Uint8Array(dataSize);
if (is16bit) dataArray16 = new Uint16Array(dataSize);
if (dataArray === null) {
console.log('no memory');
return LoadResult.ERROR_NO_MEMORY;
}
for (i = 0; i < dataSize; i++) {
dataArray[i] = 0;
}
if (is16bit) {
for (i = 0; i < dataSize; i++) {
dataArray16[i] = 0;
}
}
// convert slices data into volume set data (16 bpp -> 8 bpp, etc)
const MAX_BYTE = 255;
for (let s = 0; s < numSlices; s++) {
const sliceSrc = srcSlices[s];
xyDim = sliceSrc.m_xDim * sliceSrc.m_yDim;
const dataSrc16 = sliceSrc.m_image;
// console.log(`Slice[${s}] sliceNumber = ${slice.m_sliceNumber} sliceLocation = ${slice.m_sliceLocation}`);
// const z = slice.m_sliceNumber - this.m_slicesVolume.m_minSlice;
let z = sliceSrc.m_sliceNumber;
if (z >= serie.m_slices.length) {
z = sliceSrc.m_sliceNumber - serie.m_minSlice;
if (z < 0 || z >= this.m_zDim) {
console.log('Invalid z slice reference');
return LoadResult.ERROR_INVALID_SLICE_INDEX;
} // if z invalid
} // if z more num slices
if (dataSrc16 !== null) {
const offZ = z * xyDim;
const BITS_16 = 16;
const max16 = 1 << BITS_16;
if (is16bit) {
for (i = 0; i < xyDim; i++) {
let val_16 = dataSrc16[i] * this.m_rescaleSlope + this.m_rescaleIntercept;
//let val = (val_16 - minVal) * max16 / (maxVal - minVal);
let val = val_16 - minVal;
val = Math.floor(val);
dataArray16[offZ + i] = val;
} // for i
}
if (this.m_windowCenter !== LARGE_NUMBER && this.m_windowWidth !== LARGE_NUMBER) {
const winMin = this.m_windowCenter - this.m_windowWidth * 0.5;
for (i = 0; i < xyDim; i++) {
const valScaled = dataSrc16[i] * this.m_rescaleSlope + this.m_rescaleIntercept;
let val = 0;
if (this.m_rescaleHounsfield) {
// rescale for hounsfield units
val = Math.floor(((valScaled - winMin) * 255) / this.m_windowWidth);
} else {
// usual (default) rescale
val = Math.floor(127 + ((valScaled - this.m_windowCenter) * 128) / (this.m_windowWidth / 2));
}
val = val >= 0 ? val : 0;
val = val < 255 ? val : 255;
dataArray[offZ + i] = val;
} // for i
} else {
// window center, width not specified
for (i = 0; i < xyDim; i++) {
const val16 = dataSrc16[i] * this.m_rescaleSlope + this.m_rescaleIntercept;
let val = ((val16 - minVal) * scale) >> BITS_ACCUR;
// let val = Math.floor(255 * val16 / maxVal);
val = val <= MAX_BYTE ? val : MAX_BYTE;
dataArray[offZ + i] = val;
} // for i
} // no defined window center, width
if (this.m_windowCenter !== LARGE_NUMBER && this.m_windowWidth !== LARGE_NUMBER && !this.m_rescaleHounsfield) {
this.m_winRight = (-this.m_minVal + this.m_windowCenter + this.m_windowWidth / 2) / (this.m_maxVal - this.m_minVal);
this.m_winLeft = (-this.m_minVal + this.m_windowCenter - this.m_windowWidth / 2) / (this.m_maxVal - this.m_minVal);
}
} // if has slice data
} // for(s) all slices
// destroy for what?
// this.m_slicesVolume.destroy();
// Scale down volume by slices
const numPixelsInVolume = this.m_xDim * this.m_yDim * this.m_zDim;
// eslint-disable-next-line
const MAX_VOLUME_PIXELS = 512 * 512 * 256;
if (this.m_needScaleDownTexture && numPixelsInVolume > MAX_VOLUME_PIXELS) {
const XY_MAX_DIM = 512;
const Z_LOW_DIM = 120;
const xDimDst = this.m_xDim <= XY_MAX_DIM ? this.m_xDim : XY_MAX_DIM;
const yDimDst = this.m_yDim <= XY_MAX_DIM ? this.m_yDim : XY_MAX_DIM;
const zDimDst = Z_LOW_DIM;
const dataNew = VolumeTools.scaleTextureDown(this, dataArray, xDimDst, yDimDst, zDimDst);
dataArray = dataNew;
xyDim = this.m_xDim * this.m_yDim;
console.log(`Volume scaled down to: ${xDimDst} * ${yDimDst} * ${zDimDst}.`);
}
// Scan for empty voxels on border sides
if (NEED_SCAN_EMPTY_BORDER) {
const xDim = this.m_xDim;
const yDim = this.m_yDim;
const zDim = this.m_zDim;
const minValBarrier = 16;
let x;
let y;
let z;
let isEmpty;
isEmpty = true;
for (x = 0; x < xDim / TWICE && isEmpty; x++) {
// check is empty plane
for (y = 0; y < yDim && isEmpty; y++) {
for (z = 0; z < zDim && isEmpty; z++) {
const off = z * xyDim + y * xDim + x;
if (dataArray[off] > minValBarrier) {
isEmpty = false;
}
} // for (z)
} // for (y()
} // for (x)
const xBorderMin = x;
isEmpty = true;
for (x = xDim - 1; x > xDim / TWICE && isEmpty; x--) {
// check is empty plane
for (y = 0; y < yDim && isEmpty; y++) {
for (z = 0; z < zDim && isEmpty; z++) {
const off = z * xyDim + y * xDim + x;
if (dataArray[off] > minValBarrier) {
isEmpty = false;
}
} // for (z)
} // for (y()
} // for (x)
const xBorderMax = x;
isEmpty = true;
for (y = 0; y < yDim / TWICE && isEmpty; y++) {
// check is empty plane
for (x = 0; x < xDim && isEmpty; x++) {
for (z = 0; z < zDim && isEmpty; z++) {
const off = z * xyDim + y * xDim + x;
if (dataArray[off] > minValBarrier) {
isEmpty = false;
}
} // for (z)
} // for (y()
} // for (x)
const yBorderMin = y;
isEmpty = true;
for (y = yDim - 1; y > yDim / TWICE && isEmpty; y--) {
// check is empty plane
for (x = 0; x < xDim && isEmpty; x++) {
for (z = 0; z < zDim && isEmpty; z++) {
const off = z * xyDim + y * xDim + x;
if (dataArray[off] > minValBarrier) {
isEmpty = false;
}
} // for (z)
} // for (y()
} // for (x)
const yBorderMax = y;
isEmpty = true;
for (z = 0; z < zDim / TWICE && isEmpty; z++) {
// check is empty plane
for (x = 0; x < xDim && isEmpty; x++) {
for (y = 0; y < yDim && isEmpty; y++) {
const off = z * xyDim + y * xDim + x;
if (dataArray[off] > minValBarrier) {
isEmpty = false;
}
} // for (z)
} // for (y()
} // for (x)
const zBorderMin = z;
isEmpty = true;
for (z = zDim - 1; z > zDim / TWICE && isEmpty; z--) {
// check is empty plane
for (x = 0; x < xDim && isEmpty; x++) {
for (y = 0; y < yDim && isEmpty; y++) {
const off = z * xyDim + y * xDim + x;
if (dataArray[off] > minValBarrier) {
isEmpty = false;
}
} // for (z)
} // for (y()
} // for (x)
const zBorderMax = z;
// console.log(`Border scan: xBorderMin = ${xBorderMin}, xBorderMax = ${xBorderMax}. xDim = ${xDim}`);
// console.log(`Border scan: yBorderMin = ${yBorderMin}, yBorderMax = ${yBorderMax}. yDim = ${yDim}`);
// console.log(`Border scan: zBorderMin = ${zBorderMin}, zBorderMax = ${zBorderMax}. zDim = ${zDim}`);
this.m_nonEmptyBoxMin.x = xBorderMin / xDim;
this.m_nonEmptyBoxMin.y = yBorderMin / yDim;
this.m_nonEmptyBoxMin.z = zBorderMin / zDim;
this.m_nonEmptyBoxMax.x = xBorderMax / xDim;
this.m_nonEmptyBoxMax.y = yBorderMax / yDim;
this.m_nonEmptyBoxMax.z = zBorderMax / zDim;
this.m_boxSize.z = (zBorderMax - zBorderMin) * this.m_pixelSpacing.z;
this.m_boxSize.x = (xBorderMax - xBorderMin) * this.m_pixelSpacing.x;
this.m_boxSize.y = (yBorderMax - yBorderMin) * this.m_pixelSpacing.y;
const neMin = this.m_nonEmptyBoxMin;
const neMax = this.m_nonEmptyBoxMax;
console.log(`Border scan min: ${neMin.x}, ${neMin.y}, ${neMin.z}`);
console.log(`Border scan max: ${neMax.x}, ${neMax.y}, ${neMax.z}`);
} // if need scan empty borders
// Special volume texture size fix (z dim should be even)
if (NEED_EVEN_TEXTURE_SIZE) {
const xDim = this.m_xDim;
const yDim = this.m_yDim;
const zDim = this.m_zDim;
if ((zDim & 1) !== 0) {
const volDataAlignedSize = VolumeTools.makeTextureSizeEven(dataArray, xDim, yDim, zDim);
// Align all dims to 4*x
const NUM3 = 3;
this.m_xDim = (xDim + NUM3) & ~NUM3;
this.m_yDim = (yDim + NUM3) & ~NUM3;
this.m_zDim = (zDim + NUM3) & ~NUM3;
dataArray = volDataAlignedSize;
}
}
// Apply Gauss smooth filter
if (NEED_APPLY_GAUSS_SMOOTHING) {
const volTools = new VolumeTools();
const GAUSS_RADIUS = 2;
const GAUSS_SIGMA = 1.4;
volTools.gaussSmooth(dataArray, this.m_xDim, this.m_yDim, this.m_zDim, GAUSS_RADIUS, GAUSS_SIGMA);
}
// Apply 0 to the edges of volume
const MIN_NUM_SLICES_FOR_VOL = 4;
if (this.m_zDim > MIN_NUM_SLICES_FOR_VOL) {
let x;
let y;
let z;
let yOff;
let zOff;
// z planes
z = 0;
const zOffMin = z * this.m_xDim * this.m_yDim;
z = this.m_zDim - 1;
const zOffMax = z * this.m_xDim * this.m_yDim;
for (y = 0; y < this.m_yDim; y++) {
yOff = y * this.m_xDim;
for (x = 0; x < this.m_xDim; x++) {
dataArray[zOffMin + yOff + x] = 0;
dataArray[zOffMax + yOff + x] = 0;
} // for x
} // for y
// x planes
x = 0;
const xOffMin = x;
x = this.m_xDim - 1;
const xOffMax = x;
for (z = 0; z < this.m_zDim; z++) {
zOff = z * this.m_xDim * this.m_yDim;
for (y = 0; y < this.m_yDim; y++) {
yOff = y * this.m_xDim;
dataArray[zOff + yOff + xOffMin] = 0;
dataArray[zOff + yOff + xOffMax] = 0;
}
}
// y planes
y = 0;
const yOffMin = y * this.m_xDim;
y = this.m_yDim - 1;
const yOffMax = y * this.m_xDim;
for (z = 0; z < this.m_zDim; z++) {
zOff = z * this.m_xDim * this.m_yDim;
for (x = 0; x < this.m_xDim; x++) {
dataArray[zOff + x + yOffMin] = 0;
dataArray[zOff + x + yOffMax] = 0;
} // for x
} // for z
} // if zDim more min possible for volume
const KTX_GL_RED = 0x1903;
const KTX_UNSIGNED_BYTE = 0x1401;
const header = {
m_pixelWidth: this.m_xDim,
m_pixelHeight: this.m_yDim,
m_pixelDepth: this.m_zDim,
m_glType: KTX_UNSIGNED_BYTE,
m_glTypeSize: 1,
m_glFormat: KTX_GL_RED,
m_glInternalFormat: KTX_GL_RED,
m_glBaseInternalFormat: KTX_GL_RED,
};
const callbackComplete = this.m_callbackComplete;
if (callbackComplete) {
callbackComplete(LoadResult.SUCCESS, header, dataSize, dataArray);
} // if callback ready
const ONE = 1;
volDst.m_xDim = this.m_xDim;
volDst.m_yDim = this.m_yDim;
volDst.m_zDim = this.m_zDim;
volDst.m_dataArray = dataArray;
volDst.m_dataArray16 = dataArray16;
volDst.m_dataSize = dataSize;
volDst.m_bytesPerVoxel = ONE;
volDst.m_boxSize.x = this.m_boxSize.x;
volDst.m_boxSize.y = this.m_boxSize.y;
volDst.m_boxSize.z = this.m_boxSize.z;
volDst.m_patientName = this.m_dicomInfo.m_patientName;
volDst.m_patientBirth = this.m_dicomInfo.m_patientDateOfBirth;
volDst.m_seriesDescr = this.m_dicomInfo.m_seriesDescr;
volDst.m_studyDescr = this.m_dicomInfo.m_studyDescr;
volDst.m_studyDate = this.m_dicomInfo.m_studyDate;
volDst.m_seriesTime = this.m_dicomInfo.m_seriesTime;
volDst.m_bodyPartExamined = this.m_dicomInfo.m_bodyPartExamined;
volDst.m_institutionName = this.m_dicomInfo.m_institutionName;
volDst.m_operatorsName = this.m_dicomInfo.m_operatorsName;
volDst.m_physicansName = this.m_dicomInfo.m_physicansName;
volDst.createIcon();
return LoadResult.SUCCESS;
} // end createVolumeFromSlices
static getVrsStringIndex(vr) {
const VRS = [
'AE',
'AS',
'AT',
'CS',
'DA',
'DS',
'DT',
'FL',
'FD',
'IS',
'LO',
'LT',
'OB',
'OD',
'OF',
'OW',
'PN',
'SH',
'SL',
'SS',
'ST',
'TM',
'UI',
'UL',
'UN',
'US',
'UT',
];
const numElems = VRS.length;
for (let i = 0; i < numElems; i++) {
if (VRS[i] === vr) {
return i;
}
}
return -1;
}
static getDataVrsStringIndex(vr) {
const DATA_VRS = ['OB', 'OW', 'OF', 'SQ', 'UT', 'UN'];
const numElems = DATA_VRS.length;
for (let i = 0; i < numElems; i++) {
if (DATA_VRS[i] === vr) {
return i;
}
}
return -1;
}
/**
* Convert DataView object into string
* @param {object} dataView - DataView object (created from ArrayBuffer)
* @param {number} offset - current offset in buffer, when string started
* @param {number} lengthBuf - number of bytes to convert to string
* @return {string} string presentation of DataView
*/
static getStringAt(dataView, offset, lengthBuf) {
let str = '';
for (let i = 0; i < lengthBuf; i++) {
const ch = dataView.getUint8(offset + i);
if (ch !== 0) {
str += String.fromCharCode(ch);
}
}
return str;
}
static getUtf8StringAt(dataView, offset, lengthBuf) {
let str = '';
let i = 0;
while (i < lengthBuf) {
let c = dataView.getUint8(offset + i);
i++;
if (c == 0x5e) {
c = 32;
}
switch (c >> 4) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
str += String.fromCharCode(c);
break;
case 12:
case 13:
const char2 = dataView.getUint8(offset + i);
i++;
str += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f));
break;
case 14:
const ch2 = dataView.getUint8(offset + i);
i++;
const ch3 = dataView.getUint8(offset + i);
i++;
str += String.fromCharCode(((c & 0x0f) << 12) | ((ch2 & 0x3f) << 6) | ((ch3 & 0x3f) << 0));
break;
} // switch
} // while not end string
return str;
}
static getAttrValueAsString(tag) {
if (tag.m_value === null) {
if (tag.m_vr === 'SQ') {
// sequence of items
return '(Sequence Data)';
}
return null;
}
const SIZE_SHORT = 2;
const SIZE_DWORD = 4;
const dvTag = new DataView(tag.m_value);
let tmp = null;
let res = null;
let date = null;
let readBytes = 0;
// to do: add AT, OB?, OD?, OF?, OW?, Unknown?
switch (tag.m_vr) {
case 'AE': // application entity
return LoaderDicom.getStringAt(dvTag, 0, tag.m_value.byteLength);
case 'AS': // age string
tmp = LoaderDicom.getStringAt(dvTag, 0, tag.m_value.byteLength);
// eslint-disable-next-line
res = Number(tmp.slice(0, 3)).toString();
switch (tmp[3]) {
case 'D':
return `${res} days`;
case 'W':
return `${res} weeks`;
case 'M':
return `${res} months`;
case 'Y':
return `${res} years`;
default:
return null;
}
case 'CS': // code string
return LoaderDicom.getStringAt(dvTag, 0, tag.m_value.byteLength);
case 'DA': // date
tmp = LoaderDicom.getStringAt(dvTag, 0, tag.m_value.byteLength);
// eslint-disable-next-line
date = new Date(`${tmp.slice(0, 4)}-${tmp.slice(4, 6)}-${tmp.slice(6, 8)}`);
return date.toLocaleDateString();
case 'DS': // decimal string
return LoaderDicom.getStringAt(dvTag, 0, tag.m_value.byteLength);
case 'DT': // date time
// to do: parse date-time as YYYYMMDDHHMMSS.FFFFFF&ZZXX
return LoaderDicom.getStringAt(dvTag, 0, tag.m_value.byteLength);
case 'FL': // floating point single
res = dvTag.getFloat32(0, tag.m_littleEndian).toString();
readBytes = SIZE_DWORD;
while (readBytes + SIZE_DWORD <= dvTag.byteLength) {
res = `${res} \\ ${dvTag.getFloat32(readBytes, tag.m_littleEndian).toString()}`;
readBytes += SIZE_DWORD;
}
return res;
case 'FD': // floating point double
res = dvTag.getFloat64(0, tag.m_littleEndian).toString();
readBytes = SIZE_DWORD + SIZE_DWORD;
while (readBytes + SIZE_DWORD + SIZE_DWORD <= dvTag.byteLength) {
res = `${res} \\ ${dvTag.getFloat64(readBytes, tag.m_littleEndian).toString()}`;
readBytes += SIZE_DWORD + SIZE_DWORD;
}
return res;
case 'IS': // integer string
return LoaderDicom.getStringAt(dvTag, 0, tag.m_value.byteLength);
case 'LO': // long string
return LoaderDicom.getStringAt(dvTag, 0, tag.m_value.byteLength);
case 'LT': // long text
// to do: check if it works for several paragraphs
return LoaderDicom.getStringAt(dvTag, 0, tag.m_value.byteLength);
case 'PN': // person name
return LoaderDicom.getStringAt(dvTag, 0, tag.m_value.byteLength);
case 'SH': // short string
return LoaderDicom.getStringAt(dvTag, 0, tag.m_value.byteLength);
case 'SL': // signed long
res = dvTag.getInt32(0, tag.m_littleEndian).toString();
readBytes = SIZE_DWORD;
while (readBytes + SIZE_SHORT <= dvTag.byteLength) {
res = `${res} \\ ${dvTag.getInt16(readBytes, tag.m_littleEndian).toString()}`;
readBytes += SIZE_DWORD;
}
return res;
case 'SS': // signed short
res = dvTag.getInt16(0, tag.m_littleEndian).toString();
readBytes = SIZE_SHORT;
while (readBytes + SIZE_SHORT <= dvTag.byteLength) {
res = `${res} \\ ${dvTag.getInt16(readBytes, tag.m_littleEndian).toString()}`;
readBytes += SIZE_SHORT;
}
return res;
case 'ST': // short text
return LoaderDicom.getStringAt(dvTag, 0, tag.m_value.byteLength);
case 'TM': // time
tmp = LoaderDicom.getStringAt(dvTag, 0, tag.m_value.byteLength);
if (tag.m_value.byteLength >= SIZE_SHORT) {
// eslint-disable-next-line
res = `${Number(tmp.slice(0, 2))}h`;
}
if (tag.m_value.byteLength >= SIZE_DWORD) {
// eslint-disable-next-line
res = `${res} ${Number(tmp.slice(2, 4))}m`;
}
if (tag.m_value.byteLength > SIZE_DWORD) {
// eslint-disable-next-line
res = `${res} ${parseFloat(tmp.slice(4, tag.m_value.byteLength))}s`;
}
return res;
case 'UI': // unique identifier
return LoaderDicom.getStringAt(dvTag, 0, tag.m_value.byteLength);
case 'UL': // unsigned long
res = dvTag.getUint32(0, tag.m_littleEndian).toString();
readBytes = SIZE_DWORD;
while (readBytes + SIZE_DWORD <= dvTag.byteLength) {
res = `${res} \\ ${dvTag.getUint32(readBytes, tag.m_littleEndian).toString()}`;
readBytes += SIZE_DWORD;
}
return res;
case 'US': // unsigned short
res = dvTag.getUint16(0, tag.m_littleEndian).toString();
readBytes = SIZE_SHORT;
while (readBytes + SIZE_SHORT <= dvTag.byteLength) {
res = `${res} \\ ${dvTag.getUint16(readBytes, tag.m_littleEndian).toString()}`;
readBytes += SIZE_SHORT;
}
return res;
case 'UT': // unlimited text
// to do: check if it works for several paragraphs
return LoaderDicom.getStringAt(dvTag, 0, tag.m_value.byteLength);
default:
return null;
}
}
/**
* Get next tag from stream
* @param {object} dataView - array
* @param {number} offset - current offset in buffer
* @return {object} if success or null, when fail. Fail reasons: invalid tag length read.
*/
getNextTag(dataView, offset) {
// check end of buffer
if (offset >= dataView.byteLengh) {
return null;
}
const SIZE_SHORT = 2;
const SIZE_DWORD = 4;
let little = true;
let group = 0;
let element = 0;
let lenData = 0;
const offsetStart = offset;
let vr = '';
const MAGIC_GROUP = 0x0002;
if (this.m_metaFinished) {
little = this.m_littleEndian;
group = dataView.getUint16(offset, little);
} else {
group = dataView.getUint16(offset, 1);
if ((this.m_metaFinishedOffset !== -1 && offset >= this.m_metaFinishedOffset) || group !== MAGIC_GROUP) {
this.m_metaFinished = 1;
little = this.m_littleEndian;
group = dataView.getUint16(offset, little);
} else {
little = true;
}
}
if (!this.m_metaFound && group === MAGIC_GROUP) {
this.m_metaFound = true;
}
offset += SIZE_SHORT; // skip group number
element = dataView.getUint16(offset, little);
offset += SIZE_SHORT; // skip element number
if (this.m_explicit || !this.m_metaFinished) {
vr = LoaderDicom.getStringAt(dataView, offset, SIZE_SHORT);
if (!this.m_metaFound && this.m_metaFinished && LoaderDicom.getVrsStringIndex(vr) === -1) {
vr = DicomDictionary.getVr(group, element);
lenData = dataView.getUint32(offset, little);
// assert for lenData < 1024 * 1024 * 32
offset += SIZE_DWORD;
this.m_explicit = false;
} else {
offset += SIZE_SHORT;
if (LoaderDicom.getDataVrsStringIndex(vr) !== -1) {
offset += SIZE_SHORT;
lenData = dataView.getUint32(offset, little);
// assert for lenData < 1024 * 1024 * 32
offset += SIZE_DWORD;
} else {
lenData = dataView.getUint16(offset, little);
// assert for lenData < 1024 * 1024 * 32
offset += SIZE_SHORT;
}
}
} else {
vr = this.m_dictionary.getVr(group, element);
lenData = dataView.getUint32(offset, little);
// assert for lenData < 1024 * 1024 * 32
if (lenData === UNDEFINED_LENGTH) {
vr = 'SQ';
}
offset += SIZE_DWORD;
}
const offsetValue = offset;
let dataValue = null;
if (vr === 'SQ') {
// see nema dicom Table 7.5-3
// for now just skip sequence items
if (lenData === UNDEFINED_LENGTH) {
let sqGroup = 0;
let sqElement = 0;
while (!(sqGroup === TAG_END_OF_SEQUENCE[0] && sqElement === TAG_END_OF_SEQUENCE[1])) {
sqGroup = dataView.getUint16(offset, little);
offset += SIZE_SHORT;
sqElement = dataView.getUint16(offset, little);
offset += SIZE_SHORT;
const sqLength = dataView.getUint32(offset, little);
offset += SIZE_DWORD;
if (sqLength === UNDEFINED_LENGTH) {
// item delim. tag (fffe, e00d)
while (
!(
(sqGroup === TAG_END_OF_ITEMS[0] && sqElement === TAG_END_OF_ITEMS[1]) ||
(sqGroup === TAG_END_OF_SEQUENCE[0] && sqElement === TAG_END_OF_SEQUENCE[1])
)
) {
const tagNew = this.getNextTag(dataView, offset);
offset = tagNew.m_offsetEnd;
sqGroup = dataView.getUint16(offset, little);
sqElement = dataView.getUint16(offset + SIZE_SHORT, little);
}
offset += SIZE_DWORD + SIZE_DWORD; // 4 for group and element + 4 for length field
} else {
// if sqLength is ffffffff
offset += sqLength;
}
} // while not end tag sequence
lenData = 0;
} // if length equal to ffffffff
} else if (lenData > 0) {
if (lenData === UNDEFINED_LENGTH) {
if (group === TAG_PIXEL_DATA[0] && element === TAG_PIXEL_DATA[1]) {
lenData = dataView.byteLength - offset;
}
}
// Get data from buffer, starting from offset
dataValue = dataView.buffer.slice(offset, offset + lenData);
}
const VAL_16 = 16;
if (DEBUG_PRINT_TAGS_INFO) {
const strDesc = this.m_dictionary.getTextDesc(group, element);
const strGr = group.toString(VAL_16);
const strEl = element.toString(VAL_16);
console.log(`Tag {${strGr},${strEl}}, VR='${vr}', Length=${lenData}, Desc=${strDesc}`);
}
const VAL_MIN_ONE = 0xffffffff;
if (lenData === VAL_MIN_ONE) {
return null;
}
offset += lenData;
const tag = new DicomTag(group, element, vr, dataValue, offsetStart, offsetValue, offset, this.m_littleEndian);
if (tag) {
this.m_newTagEvent.detail.group = group.toString(VAL_16);
this.m_newTagEvent.detail.element = element.toString(VAL_16);
this.m_newTagEvent.detail.desc = this.m_dictionary.getTextDesc(group, element);
this.m_newTagEvent.detail.value = LoaderDicom.getAttrValueAsString(tag);
if (group === TAG_IMAGE_INSTANCE_NUMBER[0] && element === TAG_IMAGE_INSTANCE_NUMBER[1]) {
this.m_newTagEvent.detail.imageNumber = parseInt(this.m_newTagEvent.detail.value, 10);
} else {
this.m_newTagEvent.detail.imageNumber = -1;
}
dispatchEvent(this.m_newTagEvent);
}
if (tag.isTransformSyntax()) {
const tagDataLen = tag.m_value.byteLength;
const dvTag = new DataView(tag.m_value);
const strTagVal = LoaderDicom.getStringAt(dvTag, 0, tagDataLen);
if (strTagVal === TRANSFER_SYNTAX_IMPLICIT_LITTLE) {
this.m_explicit = false;
this.m_littleEndian = true;
} else if (strTagVal === TRANSFER_SYNTAX_EXPLICIT_BIG) {
this.m_explicit = true;
this.m_littleEndian = false;
} else if (strTagVal === TRANSFER_SYNTAX_COMPRESSION_DEFLATE) {
this.m_needsDeflate = true;
this.m_explicit = true;
this.m_littleEndian = true;
} else if (strTagVal == TRANSFER_SYNTAX_COMPRESSION_JPEG) {
this.m_transformSyntax = TRANSFER_SYNTAX_COMPRESSION_JPEG;
} else if (strTagVal == TRANSFER_SYNTAX_COMPRESSION_JPEG_LOSSLESS_SEL1) {
this.m_transformSyntax = TRANSFER_SYNTAX_COMPRESSION_JPEG_LOSSLESS_SEL1;
} else if (strTagVal == TRANSFER_SYNTAX_COMPRESSION_JPEG_LOSSLESS) {
this.m_transformSyntax = TRANSFER_SYNTAX_COMPRESSION_JPEG_LOSSLESS;
} else if (strTagVal == TRANSFER_SYNTAX_COMPRESSION_JPEG_BASELINE_8BIT) {
this.m_transformSyntax = TRANSFER_SYNTAX_COMPRESSION_JPEG_BASELINE_8BIT;
} else if (strTagVal == TRANSFER_SYNTAX_COMPRESSION_JPEG_BASELINE_12BIT) {
this.m_transformSyntax = TRANSFER_SYNTAX_COMPRESSION_JPEG_BASELINE_12BIT;
} else if (strTagVal == TRANSFER_SYNTAX_COMPRESSION_JPEG_2000_LOSSLESS) {
this.m_transformSyntax = TRANSFER_SYNTAX_COMPRESSION_JPEG_2000_LOSSLESS;
} else if (strTagVal == TRANSFER_SYNTAX_COMPRESSION_JPEG_2000) {
this.m_transformSyntax = TRANSFER_SYNTAX_COMPRESSION_JPEG_2000;
} else if (strTagVal == TRANSFER_SYNTAX_COMPRESSION_RLE) {
this.m_transformSyntax = TRANSFER_SYNTAX_COMPRESSION_RLE;
} else {
this.m_explicit = true;
this.m_littleEndian = true;
}
} else if (tag.isMetaLength()) {
this.m_metaFinishedOffset = tag.m_value[0] + offset;
}
return tag;
} // getNextTag
static numberToHexString(val) {
const str = val.toString(16);
const len = str.length;
const numLeadZeros = 4 - len;
let strZeros = '';
for (let i = 0; i < numLeadZeros; i++) {
strZeros += '0';
}
const strRes = '0x' + strZeros + str;
return strRes;
}
readFromGoogleBuffer(i, fileName, ratioLoaded, arrBuf, callbackProgress, callbackComplete) {
const dataView = new DataView(arrBuf);
let fileSize = dataView.byteLength;
// fileSize = (fileSize) & (~1);
// console.log(`readFromGoogleBuffer. fileSize = ${fileSize}`);
// fileSize = (fileSize <= 512) ? fileSize : 512;
// const strHeader = LoaderDicom.getStringAt(dataView, 0, 512);
// console.log(`readFromGoogleBuffer. header = ${strHeader}`);
const OFF_CONTENT_TYPE = 64;
const LEN_CONTENT_TYPE = 32;
const strCtx = LoaderDicom.getStringAt(dataView, OFF_CONTENT_TYPE, LEN_CONTENT_TYPE);
const STR_CTX_MATCH = 'Content-Type: application/dicom;';
const isEqCtxStr = strCtx === STR_CTX_MATCH;
if (isEqCtxStr) {
const SIZE_GOOGLE_HEADER = 136;
const arrBufWoHead = arrBuf.slice(SIZE_GOOGLE_HEADER);
const dataViewWoGoogle = new DataView(arrBufWoHead);
const okRet = this.readFromBuffer(i, fileName, ratioLoaded, arrBufWoHead, callbackProgress, callbackComplete);
return okRet;
} else {
console.log(`readFromGoogleBuffer. bad content type = ${strCtx}`);
return LoadResult.BAD_HEADER;
}
}
/**
* Read from local file buffer
* @param {number} indexFile - index of slice loaded
* @param {string} fileName - Loaded file
* @param {number} ratioLoaded - ratio from 0 to 1.0.
* @param {object} arrBuf - source byte buffer, ArrayBuffer type
* @return LoadResult.XXX
*/
readFromBuffer(indexFile, fileName, ratioLoaded, arrBuf, callbackProgress, callbackComplete) {
if (typeof indexFile !== 'number') {
console.log('LoaderDicom.readFromBuffer: bad indexFile argument');
}
if (typeof fileName !== 'string') {
console.log('LoaderDicom.readFromBuffer: bad fileName argument');
}
if (typeof arrBuf !== 'object') {
console.log('LoaderDicom.readFromBuffer: bad arrBuf argument');
}
// const bufBytes = new Uint8Array(arrBuf);
// const isUint8Arr = bufBytes instanceof Uint8Array;
// if (!isUint8Arr) {
// console.log('LoaderDicom. readFromBuffer. Error read buffer');
// return false;
// }
// console.log(`LoaderDicom. readFromBuffer. file = ${fileName}, ratio = ${ratioLoaded}`);
// add info
const dicomInfo = this.m_dicomInfo;
const sliceInfo = new DicomSliceInfo();
const strSlice = 'Slice ' + indexFile.toString();
sliceInfo.m_sliceName = strSlice;
sliceInfo.m_fileName = fileName;
sliceInfo.m_tags = [];
dicomInfo.m_sliceInfo.push(sliceInfo);
const dataView = new DataView(arrBuf);
if (dataView === null) {
console.log('No memory');
return LoadResult.ERROR_NO_MEMORY;
}
const fileSize = dataView.byteLength;
// check dicom header
const SIZE_HEAD = 144;
if (fileSize < SIZE_HEAD) {
// this.m_errors[indexFile] = DICOM_ERROR_TOO_SMALL_FILE;
this.m_error = LoadResult.ERROR_TOO_SMALL_DATA_SIZE;
if (callbackComplete !== undefined) {
callbackComplete(LoadResult.ERROR_TOO_SMALL_DATA_SIZE);
}
return LoadResult.ERROR_TOO_SMALL_DATA_SIZE;
}
const OFF_MAGIC = 128;
const SIZE_DWORD = 4;
const SIZE_SHORT = 2;
for (let i = 0; i < SIZE_DWORD; i++) {
const v = dataView.getUint8(OFF_MAGIC + i);
if (v !== MAGIC_DICM[i]) {
this.m_errors[indexFile] = DICOM_ERROR_WRONG_HEADER;
console.log(`Dicom readFromBuffer. Wrong header in file: ${fileName}`);
if (callbackComplete !== undefined) {
callbackComplete(LoadResult.WRONG_HEADER_MAGIC);
}
return LoadResult.WRONG_HEADER_MAGIC;
}
}
let offset = OFF_MAGIC;
offset += SIZE_DWORD;
//
this.m_littleEndian = true;
this.m_explicit = true;
this.m_metaFound = false;
this.m_metaFinished = false;
this.m_metaFinishedOffset = -1;
this.m_needsDeflate = false;
this.m_imageNumber = -1;
this.m_xDim = -1;
this.m_yDim = -1;
this.m_bitsPerPixel = -1;
this.m_windowCenter = -1;
this.m_windowWidth = -1;
let pixelBitMask = 0;
let pixelPaddingValue = 0;
let pixelsTagReaded = false;
let pixelMinValue = -1;
let pixelMaxValue = -1;
// read tag by tag, until image tag
let tag;
for (tag = this.getNextTag(dataView, offset); tag !== null; ) {
if (tag.isPixelData()) {
pixelsTagReaded = true;
break;
}
offset = tag.m_offsetEnd;
tag = this.getNextTag(dataView, offset);
if (tag === null) {
break;
}
// add to tag info
const dicomInfo = this.m_dicomInfo;
const numlices = dicomInfo.m_sliceInfo.length;
const sliceInfo = dicomInfo.m_sliceInfo[numlices - 1];
const tagInfo = new DicomTagInfo();
tagInfo.m_tag = '(' + LoaderDicom.numberToHexString(tag.m_group) + ',' + LoaderDicom.numberToHexString(tag.m_element) + ')';
const strTagName = this.m_dictionary.getTextDesc(tag.m_group, tag.m_element);
tagInfo.m_attrName = strTagName.length > 1 ? strTagName : '';
let strVal = LoaderDicom.getAttrValueAsString(tag);
strVal = strVal !== null ? strVal : '';
tagInfo.m_attrValue = strVal;
sliceInfo.m_tags.push(tagInfo);
// console.log(`Add tag info. tag = ${tagInfo.m_tag} atNa = ${tagInfo.m_attrName} atVal = ${tagInfo.m_attrValue} `);
// get important info from tag: image number
if (tag.m_group === TAG_IMAGE_INSTANCE_NUMBER[0] && tag.m_element === TAG_IMAGE_INSTANCE_NUMBER[1]) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const strNum = LoaderDicom.getStringAt(dv, 0, dataLen);
this.m_imageNumber = parseInt(strNum, 10) - 1;
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`Str = ${strNum}, ImageNumber = ${this.m_imageNumber}`);
}
}
// get important tag: image rows
if (tag.m_group === TAG_IMAGE_ROWS[0] && tag.m_element === TAG_IMAGE_ROWS[1]) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const yDim = dataLen === SIZE_SHORT ? dv.getUint16(0, this.m_littleEndian) : dv.getUint32(0, this.m_littleEndian);
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`yDim = ${yDim}`);
}
if (this.m_yDim < 0) {
this.m_yDim = yDim;
} else if (this.m_yDim !== yDim) {
console.log('Bad image size y');
if (callbackComplete !== undefined) {
callbackComplete(LoadResult.WRONG_IMAGE_DIM_Y);
}
return LoadResult.WRONG_IMAGE_DIM_Y;
}
}
// get important tag: image cols
if (tag.m_group === TAG_IMAGE_COLS[0] && tag.m_element === TAG_IMAGE_COLS[1]) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const xDim = dataLen === SIZE_SHORT ? dv.getUint16(0, this.m_littleEndian) : dv.getUint32(0, this.m_littleEndian);
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`xDim = ${xDim}`);
}
if (this.m_xDim < 0) {
this.m_xDim = xDim;
} else if (this.m_xDim !== xDim) {
console.log('Bad image size x');
if (callbackComplete !== undefined) {
callbackComplete(LoadResult.WRONG_IMAGE_DIM_X);
}
return LoadResult.WRONG_IMAGE_DIM_X;
}
}
// get important tag: bits allocated
if (tag.m_group === TAG_BITS_ALLOCATED[0] && tag.m_element === TAG_BITS_ALLOCATED[1]) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
this.m_bitsPerPixel = dataLen === SIZE_SHORT ? dv.getUint16(0, this.m_littleEndian) : dv.getUint32(0, this.m_littleEndian);
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`bitsPerPixel = ${this.m_bitsPerPixel}`);
}
}
// window center
if (tag.m_group === TAG_WINDOW_CENTER[0] && tag.m_element === TAG_WINDOW_CENTER[1]) {
if (tag.m_value != null && tag.m_value.byteLength > 0) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const strNum = LoaderDicom.getStringAt(dv, 0, dataLen);
this.m_windowCenter = parseInt(strNum, 10);
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`Str = ${strNum}, WindowCenter = ${this.m_windowCenter}`);
}
} // if non zero data
} // window center
// window width
if (tag.m_group === TAG_WINDOW_WIDTH[0] && tag.m_element === TAG_WINDOW_WIDTH[1]) {
if (tag.m_value != null && tag.m_value.byteLength > 0) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const strNum = LoaderDicom.getStringAt(dv, 0, dataLen);
this.m_windowWidth = parseInt(strNum, 10);
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`Str = ${strNum}, WindowWidth = ${this.m_windowWidth}`);
}
} // if non zero data
} // window width
// rescale intercept
if (tag.m_group === TAG_RESCALE_INTERCEPT[0] && tag.m_element === TAG_RESCALE_INTERCEPT[1]) {
if (tag.m_value != null && tag.m_value.byteLength > 0) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const strNum = LoaderDicom.getStringAt(dv, 0, dataLen);
this.m_rescaleIntercept = parseInt(strNum, 10);
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`Str = ${strNum}, RescaleIntercept = ${this.m_rescaleIntercept}`);
}
} // if non zero data
} // rescale intercept
// rescale slope
if (tag.m_group === TAG_RESCALE_SLOPE[0] && tag.m_element === TAG_RESCALE_SLOPE[1]) {
if (tag.m_value != null && tag.m_value.byteLength > 0) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const strNum = LoaderDicom.getStringAt(dv, 0, dataLen);
this.m_rescaleSlope = parseInt(strNum, 10);
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`Str = ${strNum}, RescaleSlope = ${this.m_rescaleSlope}`);
}
} // if non zero data
} // rescale slope
// rescale type
if (tag.m_group === TAG_RESCALE_TYPE[0] && tag.m_element === TAG_RESCALE_TYPE[1]) {
if (tag.m_value != null && tag.m_value.byteLength > 0) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const strVal = LoaderDicom.getStringAt(dv, 0, dataLen);
if (strVal === 'HU') {
this.m_rescaleHounsfield = true;
}
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`Str = ${strNum}, RescaleType = ${this.m_rescaleHounsfield}`);
}
} // if non zero data
} // rescale type
// pixel representation
if (tag.m_group === TAG_PIXEL_REPRESENTATION[0] && tag.m_element === TAG_PIXEL_REPRESENTATION[1]) {
if (tag.m_value != null && tag.m_value.byteLength > 0) {
const dataLenPixRep = tag.m_value.byteLength;
const dvPixRep = new DataView(tag.m_value);
const pixRep =
dataLenPixRep === SIZE_SHORT ? dvPixRep.getUint16(0, this.m_littleEndian) : dvPixRep.getUint32(0, this.m_littleEndian);
if (pixRep === 1) {
this.m_pixelRepresentaionSigned = true;
}
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`Str = ${strNum}, Pixel representation is signed = ${this.m_pixelRepresentaionSigned}`);
}
} // if non zero data
} // rescale slope
// get series number
if (tag.m_group === TAG_SERIES_NUMBER[0] && tag.m_element === TAG_SERIES_NUMBER[1]) {
if (tag.m_value != null && tag.m_value.byteLength > 0) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const strNum = LoaderDicom.getStringAt(dv, 0, dataLen);
this.m_seriesNumber = parseInt(strNum, 10);
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`Str = ${strNum}, SeriesNumber = ${this.m_seriesNumber}`);
}
} // if non zero data
} // series number
// get important tag: series description
if (tag.m_group === TAG_SERIES_DESCRIPTION[0] && tag.m_element === TAG_SERIES_DESCRIPTION[1] && tag.m_value !== null) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
this.m_seriesDescr = LoaderDicom.getStringAt(dv, 0, dataLen);
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`Series description = ${this.m_seriesDescr}`);
}
}
// get important tag: hight bit
if (tag.m_group === TAG_IMAGE_HIGH_BIT[0] && tag.m_element === TAG_IMAGE_HIGH_BIT[1]) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const highBit = dataLen === SIZE_SHORT ? dv.getUint16(0, this.m_littleEndian) : dv.getUint32(0, this.m_littleEndian);
pixelBitMask = (1 << (highBit + 1)) - 1;
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`highBit = ${highBit}`);
}
}
// get important tag: min pixel value
if (tag.m_group === TAG_IMAGE_SMALL_PIX_VAL[0] && tag.m_element === TAG_IMAGE_SMALL_PIX_VAL[1]) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
pixelMinValue = dataLen === SIZE_SHORT ? dv.getInt16(0, this.m_littleEndian) : dv.getInt32(0, this.m_littleEndian);
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`pixelMinValue = ${pixelMinValue}`);
}
}
// get important tag: max pixel value
if (tag.m_group === TAG_IMAGE_LARGE_PIX_VAL[0] && tag.m_element === TAG_IMAGE_LARGE_PIX_VAL[1]) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
pixelMaxValue = dataLen === SIZE_SHORT ? dv.getInt16(0, this.m_littleEndian) : dv.getInt32(0, this.m_littleEndian);
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`pixelMaxValue = ${pixelMaxValue}`);
}
}
// get important tag: pixel padding value
if (tag.m_group === TAG_PIXEL_PADDING_VALUE[0] && tag.m_element === TAG_PIXEL_PADDING_VALUE[1]) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
pixelPaddingValue = dataLen === SIZE_SHORT ? dv.getUint16(0, this.m_littleEndian) : dv.getUint32(0, this.m_littleEndian);
this.m_padValue = pixelPaddingValue;
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`pixelPaddingValue = ${pixelPaddingValue}`);
}
}
// get important tag: pixel spacing in 2d (xy)
if (tag.m_group === TAG_PIXEL_SPACING[0] && tag.m_element === TAG_PIXEL_SPACING[1]) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const strPixelSpacing = LoaderDicom.getStringAt(dv, 0, dataLen);
const strArr = strPixelSpacing.split('\\');
if (strArr.length === SIZE_SHORT) {
this.m_pixelSpacing.x = parseFloat(strArr[0]);
this.m_pixelSpacing.y = parseFloat(strArr[1]);
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`TAG. pixel spacing xy = ${this.m_pixelSpacing.x} * ${this.m_pixelSpacing.y}`);
}
}
}
// get important tag: image position (x,y,z)
if (tag.m_group === TAG_IMAGE_POSITION[0] && tag.m_element === TAG_IMAGE_POSITION[1]) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const strImagePosition = LoaderDicom.getStringAt(dv, 0, dataLen);
const strArr = strImagePosition.split('\\');
const NUM_COMPONENTS_3 = 3;
if (strArr.length === NUM_COMPONENTS_3) {
// eslint-disable-next-line
const xPos = parseFloat(strArr[0]);
// eslint-disable-next-line
const yPos = parseFloat(strArr[1]);
// eslint-disable-next-line
const zPos = parseFloat(strArr[2]);
this.m_imagePosMin.x = xPos < this.m_imagePosMin.x ? xPos : this.m_imagePosMin.x;
this.m_imagePosMin.y = yPos < this.m_imagePosMin.y ? yPos : this.m_imagePosMin.y;
this.m_imagePosMin.z = zPos < this.m_imagePosMin.z ? zPos : this.m_imagePosMin.z;
this.m_imagePosMax.x = xPos > this.m_imagePosMax.x ? xPos : this.m_imagePosMax.x;
this.m_imagePosMax.y = yPos > this.m_imagePosMax.y ? yPos : this.m_imagePosMax.y;
this.m_imagePosMax.z = zPos > this.m_imagePosMax.z ? zPos : this.m_imagePosMax.z;
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`TAG. image position x,y,z = ${xPos}, ${yPos}, ${zPos}`);
}
}
}
// slice thickness
if (tag.m_group === TAG_SLICE_THICKNESS[0] && tag.m_element === TAG_SLICE_THICKNESS[1]) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const strSliceThickness = LoaderDicom.getStringAt(dv, 0, dataLen);
this.m_pixelSpacing.z = parseFloat(strSliceThickness);
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`TAG. slice thickness = ${this.m_pixelSpacing.z}`);
}
}
// get important tag: slice location (x,y,z)
if (tag.m_group === TAG_SLICE_LOCATION[0] && tag.m_element === TAG_SLICE_LOCATION[1]) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const strSliceLocation = LoaderDicom.getStringAt(dv, 0, dataLen);
const sliceLoc = parseFloat(strSliceLocation);
this.m_sliceLocation = sliceLoc;
this.m_sliceLocMin = sliceLoc < this.m_sliceLocMin ? sliceLoc : this.m_sliceLocMin;
this.m_sliceLocMax = sliceLoc > this.m_sliceLocMax ? sliceLoc : this.m_sliceLocMax;
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`TAG. Slice location = ${this.m_sliceLocation}`);
}
}
// get important tag: samples per pixel
if (tag.m_group === TAG_SAMPLES_PER_PIXEL[0] && tag.m_element === TAG_SAMPLES_PER_PIXEL[1]) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
this.m_samplesPerPixel = dataLen === SIZE_SHORT ? dv.getUint16(0, this.m_littleEndian) : dv.getUint32(0, this.m_littleEndian);
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`samplesPerPixel = ${this.m_samplesPerPixel}`);
}
}
// dicom info
if (tag.m_group === TAG_SERIES_DESCRIPTION[0] && tag.m_element === TAG_SERIES_DESCRIPTION[1] && tag.m_value !== null) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const strDescr = LoaderDicom.getStringAt(dv, 0, dataLen);
// console.log(`DicomLoader. Series descr read = ${strDescr}`);
this.m_dicomInfo.m_seriesDescr = strDescr;
}
if (tag.m_group === TAG_SERIES_TIME[0] && tag.m_element === TAG_SERIES_TIME[1] && tag.m_value !== null) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const strTimeMerged = LoaderDicom.getStringAt(dv, 0, dataLen);
// eslint-disable-next-line
const strHour = strTimeMerged.substring(0, 2);
// eslint-disable-next-line
const strMinute = strTimeMerged.substring(2, 4);
// eslint-disable-next-line
const strSec = strTimeMerged.substring(4, strTimeMerged.length);
const strTimeBuild = `${strHour}:${strMinute}:${strSec}`;
// console.log(`Series time read = ${strTimeBuild}`);
this.m_dicomInfo.m_seriesTime = strTimeBuild;
if (DEBUG_PRINT_TAGS_INFO) {
console.log(`Series time = ${this.m_dicomInfo.m_seriesTime}`);
}
}
if (tag.m_group === TAG_PATIENT_NAME[0] && tag.m_element === TAG_PATIENT_NAME[1] && tag.m_value !== null) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
this.m_dicomInfo.m_patientName = LoaderDicom.getUtf8StringAt(dv, 0, dataLen);
this.m_dicomInfo.m_patientName = this.m_dicomInfo.m_patientName.trim();
//console.log(`m_patientName = ${this.m_dicomInfo.m_patientName}`);
}
if (tag.m_group === TAG_PATIENT_ID[0] && tag.m_element === TAG_PATIENT_ID[1] && tag.m_value !== null) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
this.m_dicomInfo.m_patientId = LoaderDicom.getStringAt(dv, 0, dataLen);
// console.log(`m_patientId = ${this.m_dicomInfo.m_patientId}`);
}
if (tag.m_group === TAG_PATIENT_GENDER[0] && tag.m_element === TAG_PATIENT_GENDER[1] && tag.m_value !== null) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
this.m_dicomInfo.m_patientGender = LoaderDicom.getStringAt(dv, 0, dataLen);
// console.log(`m_patientGender = ${this.m_dicomInfo.m_patientGender}`);
}
if (tag.m_group === TAG_PATIENT_BIRTH_DATE[0] && tag.m_element === TAG_PATIENT_BIRTH_DATE[1] && tag.m_value !== null) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const strDateMerged = LoaderDicom.getStringAt(dv, 0, dataLen);
// eslint-disable-next-line
const strY = strDateMerged.substring(0, 4);
// eslint-disable-next-line
const strM = strDateMerged.substring(4, 6);
// eslint-disable-next-line
const strD = strDateMerged.substring(6);
this.m_dicomInfo.m_patientDateOfBirth = `${strD}/${strM}/${strY}`;
// console.log(`m_patientDateOfBirth = ${this.m_dicomInfo.m_patientDateOfBirth}`);
}
if (tag.m_group === TAG_STUDY_DATE[0] && tag.m_element === TAG_STUDY_DATE[1] && tag.m_value !== null) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const strDateMerged = LoaderDicom.getStringAt(dv, 0, dataLen);
// eslint-disable-next-line
const strY = strDateMerged.substring(0, 4);
// eslint-disable-next-line
const strM = strDateMerged.substring(4, 6);
// eslint-disable-next-line
const strD = strDateMerged.substring(6);
this.m_dicomInfo.m_studyDate = `${strD}/${strM}/${strY}`;
// console.log(`m_studyDate = ${this.m_dicomInfo.m_studyDate}`);
}
if (tag.m_group === TAG_STUDY_DESCR[0] && tag.m_element === TAG_STUDY_DESCR[1] && tag.m_value !== null) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
const strDescr = LoaderDicom.getStringAt(dv, 0, dataLen);
this.m_dicomInfo.m_studyDescr = strDescr;
this.m_dicomInfo.m_studyDescr = this.m_dicomInfo.m_studyDescr.trim();
// console.log(`m_studyDescr = ${this.m_dicomInfo.m_studyDescr}`);
}
if (tag.m_group === TAG_BODY_PART_EXAMINED[0] && tag.m_element === TAG_BODY_PART_EXAMINED[1] && tag.m_value !== null) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
this.m_dicomInfo.m_bodyPartExamined = LoaderDicom.getStringAt(dv, 0, dataLen);
// console.log(`m_patientName = ${this.m_dicomInfo.m_patientName}`);
}
if (tag.m_group === TAG_ACQUISION_TIME[0] && tag.m_element === TAG_ACQUISION_TIME[1] && tag.m_value !== null) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
this.m_dicomInfo.m_acquisionTime = LoaderDicom.getStringAt(dv, 0, dataLen);
// console.log(`m_acquisionTime = ${this.m_dicomInfo.m_acquisionTime}`);
}
if (tag.m_group === TAG_INSTITUTION_NAME[0] && tag.m_element === TAG_INSTITUTION_NAME[1] && tag.m_value !== null) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
this.m_dicomInfo.m_institutionName = LoaderDicom.getUtf8StringAt(dv, 0, dataLen);
this.m_dicomInfo.m_institutionName = this.m_dicomInfo.m_institutionName.trim();
// console.log(`m_institutionName = ${this.m_dicomInfo.m_institutionName}`);
}
if (tag.m_group === TAG_OPERATORS_NAME[0] && tag.m_element === TAG_OPERATORS_NAME[1] && tag.m_value !== null) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
this.m_dicomInfo.m_operatorsName = LoaderDicom.getUtf8StringAt(dv, 0, dataLen);
this.m_dicomInfo.m_operatorsName = this.m_dicomInfo.m_operatorsName.trim();
// console.log(`m_operatorsName = ${this.m_dicomInfo.m_operatorsName}`);
}
if (tag.m_group === TAG_PHYSICANS_NAME[0] && tag.m_element === TAG_PHYSICANS_NAME[1] && tag.m_value !== null) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
this.m_dicomInfo.m_physicansName = LoaderDicom.getUtf8StringAt(dv, 0, dataLen);
this.m_dicomInfo.m_physicansName = this.m_dicomInfo.m_physicansName.trim();
// console.log(`m_physicansName = ${this.m_dicomInfo.m_physicansName}`);
}
if (tag.m_group === TAG_MANUFACTURER_NAME[0] && tag.m_element === TAG_MANUFACTURER_NAME[1] && tag.m_value !== null) {
const dataLen = tag.m_value.byteLength;
const dv = new DataView(tag.m_value);
this.m_dicomInfo.m_manufacturerName = LoaderDicom.getStringAt(dv, 0, dataLen);
this.m_dicomInfo.m_manufacturerName = this.m_dicomInfo.m_manufacturerName.trim();
// console.log(`m_manufacturerName = ${this.m_dicomInfo.m_manufacturerName}`);
}
} // for all tags readed
if (!pixelsTagReaded) {
if (callbackComplete !== undefined) {
callbackComplete(LoadResult.ERROR_PIXELS_TAG_NOT_FOUND);
}
return LoadResult.ERROR_PIXELS_TAG_NOT_FOUND;
}
// check transform syntax
if (this.m_transformSyntax.length > 1) {
// const decoder = new jpeg.lossless.Decoder();
// const outBuffer = decoder.decompress(tag.m_value);
if (callbackComplete !== undefined) {
callbackComplete(LoadResult.ERROR_COMPRESSED_IMAGE_NOT_SUPPORTED);
}
return LoadResult.ERROR_COMPRESSED_IMAGE_NOT_SUPPORTED;
}
// check correct data from tags
const BITS_IN_BYTE = 8;
const imageSizeBytes = Math.floor(this.m_xDim * this.m_yDim * (this.m_bitsPerPixel / BITS_IN_BYTE) * this.m_samplesPerPixel);
if (imageSizeBytes !== tag.m_value.byteLength || pixelBitMask === 0) {
console.log(`Wrong image pixels size. Readed ${tag.m_value.byteLength}, but expected ${imageSizeBytes}`);
if (callbackComplete !== undefined) {
callbackComplete(LoadResult.ERROR_COMPRESSED_IMAGE_NOT_SUPPORTED);
}
return LoadResult.WRONG_HEADER_DATA_SIZE;
}
const numPixels = this.m_xDim * this.m_yDim;
// const volSlice = this.m_slicesVolume.getNewSlice();
const volSlice = new DicomSlice();
if (volSlice === null) {
console.log('No memory');
return LoadResult.ERROR_NO_MEMORY;
}
if (this.m_pixelRepresentaionSigned) {
volSlice.m_image = new Int16Array(numPixels);
} else {
volSlice.m_image = new Uint16Array(numPixels);
}
if (volSlice.m_image === null) {
console.log('No memory');
return LoadResult.ERROR_NO_MEMORY;
}
volSlice.m_sliceNumber = this.m_imageNumber;
volSlice.m_sliceLocation = this.m_sliceLocation;
volSlice.m_patientName = this.m_dicomInfo.m_patientName;
volSlice.m_studyDescr = this.m_dicomInfo.m_studyDescr;
volSlice.m_studyDate = this.m_dicomInfo.m_studyDate;
volSlice.m_seriesTime = this.m_dicomInfo.m_seriesTime;
volSlice.m_seriesDescr = this.m_dicomInfo.m_seriesDescr;
volSlice.m_bodyPartExamined = this.m_dicomInfo.m_bodyPartExamined;
volSlice.m_institutionName = this.m_dicomInfo.m_institutionName;
volSlice.m_operatorsName = this.m_dicomInfo.m_operatorsName;
volSlice.m_physicansName = this.m_dicomInfo.m_physicansName;
volSlice.buildHash();
volSlice.m_xDim = this.m_xDim;
volSlice.m_yDim = this.m_yDim;
// console.log(`patName = ${volSlice.m_patientName}`);
// console.log(`studyDescr = ${volSlice.m_studyDescr}`);
// console.log(`studyDate = ${volSlice.m_studyDate}`);
// console.log(`seriesTime = ${volSlice.m_seriesTime}`);
// console.log(`seriesDescr = ${volSlice.m_seriesDescr}`);
// console.log(`bodyPartExamined = ${volSlice.m_bodyPartExamined}`);
// Fill slice image
// const imageDst = this.m_slices[this.m_imageNumber];
const imageDst = volSlice.m_image;
const imageSrc = new DataView(tag.m_value);
if (imageSrc === null) {
console.log('No memory');
return LoadResult.ERROR_NO_MEMORY;
}
const BITS_8 = 8;
const BITS_16 = 16;
const NUM_1 = 1;
const NUM_3 = 3;
let i;
if (this.m_bitsPerPixel === BITS_8) {
if (this.m_samplesPerPixel === NUM_1) {
for (i = 0; i < numPixels; i++) {
const val = imageSrc.getUint8(i);
imageDst[i] = val;
}
// if 1 sample per pixel
} else if (this.m_samplesPerPixel === NUM_3) {
// if 3 samples per pixel
let j = 0;
for (i = 0; i < numPixels; i++, j += 3) {
const b0 = imageSrc.getUint8(j + 0);
const b1 = imageSrc.getUint8(j + 1);
const b2 = imageSrc.getUint8(j + 2);
// assert(b0 < 256);
// assert(b1 < 256);
// assert(b2 < 256);
imageDst[i] = Math.floor((b0 + b1 + b2) / 3);
}
}
} else if (this.m_bitsPerPixel === BITS_16) {
let i2 = 0;
for (i = 0; i < numPixels; i++) {
let val = imageSrc.getUint16(i2, this.m_littleEndian);
i2 += SIZE_SHORT;
imageDst[i] = val;
} // end for i pixels
} else {
// if 16 bpp
console.log('TODO: need to implement reading non-8 and non-16 bit dicom images');
}
this.m_error = DICOM_ERROR_OK;
// add volume slice to slices volume (and manage series)
this.m_slicesVolume.addSlice(volSlice);
// console.log(`Dicom read OK. Volume pixels = ${this.m_xDim} * ${this.m_yDim} * ${this.m_zDim}`);
if (callbackComplete !== undefined) {
callbackComplete(LoadResult.SUCCESS);
}
return LoadResult.SUCCESS;
} // end readFromBuffer
} // end class LoaderDicom