src_image_dicomBufferToData.js
import {logger} from '../utils/logger.js';
import {safeGet} from '../dicom/dataElement.js';
import {
DicomParser,
getSyntaxDecompressionName
} from '../dicom/dicomParser.js';
import {getAnyPixelDataElement} from '../dicom/dicomTag.js';
import {PixelBufferDecoder} from './decoder.js';
import {DicomData} from '../app/dataController.js';
/**
* List of compatible typed arrays.
*
* @typedef {(
* Uint8Array | Int8Array |
* Uint16Array | Int16Array |
* Uint32Array | Int32Array
* )} TypedArray
*/
/**
* Related DICOM tag keys.
*/
const TagKeys = {
TransferSyntaxUID: '00020010',
SamplesPerPixel: '00280002',
PlanarConfiguration: '00280006',
Rows: '00280010',
Columns: '00280011',
BitsAllocated: '00280100',
PixelRepresentation: '00280103'
};
/**
* Create a DicomData from a DICOM buffer: parses it, stores the meta data and
* the image buffer if pixel data is present. Buffer is decoded if needed,
* only necessary tags for decompression are checked.
*/
export class DicomBufferToData {
/**
* Converter options.
*
* @type {object}
*/
#options;
/**
* Set the converter options.
*
* @param {object} opt The input options.
*/
setOptions(opt) {
this.#options = opt;
}
/**
* Pixel buffer decoder.
* Define only once to allow optional asynchronous mode.
*
* @type {PixelBufferDecoder|undefined}
*/
#pixelDecoder;
/**
* List of dicom parsers.
*
* @type {DicomParser[]}
*/
#dicomParserStore = [];
/**
* List of decompressed data sizes.
*
* @type {number[]}
*/
#decompressedSizes = [];
/**
* Local buffer storage.
*
* @type {TypedArray[]}
*/
#finalBufferStore = [];
/**
* Abort flag.
*
* @type {boolean}
*/
#aborted = false;
/**
* Generate the data object.
*
* @param {number} index The data index.
* @param {string} origin The data origin.
*/
#generateData(index, origin) {
const dataElements = this.#dicomParserStore[index].getDicomElements();
// create data
const data = new DicomData(dataElements);
if (typeof this.#finalBufferStore[index] !== 'undefined') {
data.buffer = this.#finalBufferStore[index];
}
data.numberOfFiles = this.#options.numberOfFiles;
// call onloaditem
this.onloaditem({
data: data,
source: origin
});
}
/**
* Send final events for a succesfull load.
*
* @param {number} index The data index.
* @param {string} origin The data origin.
*/
#sendFinalEvents(index, origin) {
if (this.#aborted) {
return;
}
// send 100% progress
this.onprogress({
lengthComputable: true,
loaded: 100,
total: 100,
index: index,
source: origin
});
// send load event
this.onload({
source: origin
});
// allways send loadend
this.onloadend({
source: origin
});
}
/**
* Setup the pixel decoder.
*
* @param {string} algoName The algorithm name.
*/
#setupPixelDecoder(algoName) {
this.#pixelDecoder = new PixelBufferDecoder(algoName);
// callbacks
// pixelDecoder.ondecodestart: nothing to do
this.#pixelDecoder.ondecodeditem = (event) => {
this.#onDecodedItem(event);
// send onload and onloadend when all items have been decoded
if (event.itemNumber + 1 === event.numberOfItems) {
this.onload(event);
this.onloadend(event);
}
};
// pixelDecoder.ondecoded: nothing to do
// pixelDecoder.ondecodeend: nothing to do
this.#pixelDecoder.onerror = this.onerror;
this.#pixelDecoder.onabort = this.onabort;
}
/**
* Get the dicom meta used by decoders.
*
* @param {number} dataIndex The data index.
* @returns {object} The DICOM meta data.
*/
#getPixelMeta(dataIndex) {
const dataElements = this.#dicomParserStore[dataIndex].getDicomElements();
const pixelMeta = {missing: []};
// bits allocated
const bitsAllocated = safeGet(dataElements, TagKeys.BitsAllocated);
if (typeof bitsAllocated !== 'undefined') {
pixelMeta.bitsAllocated = bitsAllocated;
} else {
pixelMeta.missing.push('bitsAllocated');
}
// pixel reprensentation
const pixelRepresentation =
safeGet(dataElements, TagKeys.PixelRepresentation);
if (typeof pixelRepresentation !== 'undefined') {
pixelMeta.isSigned = (pixelRepresentation === 1);
} else {
pixelMeta.missing.push('pixelRepresentation');
}
// slice size
const columns = safeGet(dataElements, TagKeys.Columns);
const rows = safeGet(dataElements, TagKeys.Rows);
if (typeof columns !== 'undefined' &&
typeof rows !== 'undefined') {
pixelMeta.sliceSize = columns * rows;
} else {
if (typeof columns !== 'undefined') {
pixelMeta.missing.push('columns');
}
if (typeof rows !== 'undefined') {
pixelMeta.missing.push('rows');
}
}
// samples per pixel
const samplesPerPixel =
safeGet(dataElements, TagKeys.SamplesPerPixel);
if (typeof samplesPerPixel !== 'undefined') {
pixelMeta.samplesPerPixel = samplesPerPixel;
// planar configuration
if (samplesPerPixel !== 1) {
const planarConfiguration =
safeGet(dataElements, TagKeys.PlanarConfiguration);
if (typeof planarConfiguration !== 'undefined') {
pixelMeta.planarConfiguration = planarConfiguration;
} else {
pixelMeta.missing.push('planarConfiguration');
}
}
} else {
pixelMeta.missing.push('samplesPerPixel');
}
return pixelMeta;
}
/**
* Decode and generate data for a compressed buffer.
*
* @param {number} dataIndex The data index.
* @param {string} origin The data origin.
* @param {TypedArray[]} pixelBuffer The pixel buffer.
*/
#decodeAndGenerateData(dataIndex, origin, pixelBuffer) {
// get pixel meta
const pixelMeta = this.#getPixelMeta(dataIndex);
// abort if missing data
if (pixelMeta.missing.length !== 0) {
// abort
this.#pixelDecoder.abort();
// send events
this.onerror({
error: new Error('Missing tags to decompress data:' +
pixelMeta.missing.toString()),
source: origin
});
this.onloadend({
source: origin
});
return;
}
const numberOfItems = pixelBuffer.length;
// launch decode
for (let i = 0; i < numberOfItems; ++i) {
this.#pixelDecoder.decode(pixelBuffer[i], pixelMeta,
{
itemNumber: i,
numberOfItems: numberOfItems,
index: dataIndex,
indexOrigin: origin
}
);
}
}
/**
* Handle a decoded item event.
*
* @param {object} event The decoded item event.
*/
#onDecodedItem(event) {
const dataIndex = event.index;
const origin = event.indexOrigin;
// send progress
this.onprogress({
lengthComputable: true,
loaded: event.itemNumber + 1,
total: event.numberOfItems,
index: event.index,
source: origin
});
// store decoded data
const decodedData = event.data[0];
if (event.numberOfItems !== 1) {
// allocate buffer if not done yet
if (typeof this.#decompressedSizes[dataIndex] === 'undefined') {
this.#decompressedSizes[dataIndex] = decodedData.length;
const fullSize = event.numberOfItems *
this.#decompressedSizes[dataIndex];
try {
this.#finalBufferStore[dataIndex] =
new decodedData.constructor(fullSize);
} catch (error) {
if (error instanceof RangeError) {
const powerOf2 = Math.floor(Math.log(fullSize) / Math.log(2));
logger.error('Cannot allocate ' +
decodedData.constructor.name +
' of size: ' +
fullSize + ' (>2^' + powerOf2 + ') for decompressed data.');
}
// abort
this.#pixelDecoder.abort();
// send events
this.onerror({
error: error,
source: origin
});
this.onloadend({
source: origin
});
// exit
return;
}
}
// hoping for all items to have the same size...
if (decodedData.length !== this.#decompressedSizes[dataIndex]) {
logger.warn('Unsupported varying decompressed data size: ' +
decodedData.length + ' != ' + this.#decompressedSizes[dataIndex]);
}
// set buffer item data
this.#finalBufferStore[dataIndex].set(
decodedData, this.#decompressedSizes[dataIndex] * event.itemNumber);
} else {
this.#finalBufferStore[dataIndex] = decodedData;
}
// create data for the first item
if (event.itemNumber === 0) {
this.#generateData(dataIndex, origin);
}
}
/**
* Convert an input buffer into DicomData using a DICOM parser. Asynchronous
* method in case of possible buffer decompression. Get the data
* from the 'onload' event.
*
* @param {TypedArray} buffer The input data buffer.
* @param {string} origin The data origin.
* @param {number} dataIndex The data index.
*/
convert(buffer, origin, dataIndex) {
this.#aborted = false;
// start event
this.onloadstart({
source: origin,
index: dataIndex
});
// DICOM parser
const dicomParser = new DicomParser();
if (typeof this.#options.defaultCharacterSet !== 'undefined') {
dicomParser.setDefaultCharacterSet(this.#options.defaultCharacterSet);
}
// parse the buffer
try {
dicomParser.parse(buffer);
// check elements
} catch (error) {
this.onerror({
error: error,
source: origin
});
this.onloadend({
source: origin
});
return;
}
// store parser
this.#dicomParserStore[dataIndex] = dicomParser;
const elements = dicomParser.getDicomElements();
const pixelDataElement = getAnyPixelDataElement(elements);
if (typeof pixelDataElement !== 'undefined') {
const rawBuffer = pixelDataElement.value;
// transfer syntax (always there)
const transferSyntax = safeGet(elements, TagKeys.TransferSyntaxUID);
const algoName = getSyntaxDecompressionName(transferSyntax);
if (typeof algoName !== 'undefined') {
// setup the decoder (one decoder per all converts)
// TODO check constant algo name?
if (typeof this.#pixelDecoder === 'undefined') {
this.#setupPixelDecoder(algoName);
}
// decode and generate data (asynchronous)
this.#decodeAndGenerateData(dataIndex, origin, rawBuffer);
} else {
// store buffer
this.#finalBufferStore[dataIndex] = rawBuffer[0];
// generate data
this.#generateData(dataIndex, origin);
this.#sendFinalEvents(dataIndex, origin);
}
} else {
// non image data -> no buffer
// generate data
this.#generateData(dataIndex, origin);
this.#sendFinalEvents(dataIndex, origin);
}
}
/**
* Abort a conversion.
*/
abort() {
this.#aborted = true;
// abort decoding, will trigger pixelDecoder.onabort
if (typeof this.#pixelDecoder !== 'undefined') {
this.#pixelDecoder.abort();
} else {
this.onabort({});
}
}
/**
* Handle a load start event.
* Default does nothing.
*
* @param {object} _event The load start event.
*/
onloadstart(_event) {}
/**
* Handle a load item event.
* Default does nothing.
*
* @param {object} _event The load item event.
*/
onloaditem(_event) {}
/**
* Handle a load progress event.
* Default does nothing.
*
* @param {object} _event The progress event.
*/
onprogress(_event) {}
/**
* Handle a load event.
* Default does nothing.
*
* @param {object} _event The load event fired
* when a file has been loaded successfully.
*/
onload(_event) {}
/**
* Handle a load end event.
* Default does nothing.
*
* @param {object} _event The load end event fired
* when a file load has completed, successfully or not.
*/
onloadend(_event) {}
/**
* Handle an error event.
* Default does nothing.
*
* @param {object} _event The error event.
*/
onerror(_event) {}
/**
* Handle an abort event.
* Default does nothing.
*
* @param {object} _event The abort event.
*/
onabort(_event) {}
} // class DicomBufferToView