diff --git a/package.json b/package.json index 5b1342e3..b9e4ceb9 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ }, "dependencies": { "@cornerstonejs/codec-charls": "^0.1.1", + "@cornerstonejs/codec-libjpeg-turbo-12bit": "^0.2.0", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^0.0.7", "@cornerstonejs/codec-openjpeg": "^0.1.0", "dicom-parser": "^1.8.9", diff --git a/src/imageLoader/colorSpaceConverters/convertPALETTECOLOR.js b/src/imageLoader/colorSpaceConverters/convertPALETTECOLOR.js index c5bbaefc..cb74440e 100644 --- a/src/imageLoader/colorSpaceConverters/convertPALETTECOLOR.js +++ b/src/imageLoader/colorSpaceConverters/convertPALETTECOLOR.js @@ -16,14 +16,21 @@ function convertLUTto8Bit(lut, shift) { * * @param {ImageFrame} imageFrame * @param {Uint8ClampedArray} rgbaBuffer - * @returns {void} + * @returns {rgbaBuffer} as async function */ -export default function (imageFrame, rgbaBuffer) { +export default async function (imageFrame, rgbaBuffer) { const numPixels = imageFrame.columns * imageFrame.rows; const pixelData = imageFrame.pixelData; - const rData = imageFrame.redPaletteColorLookupTableData; - const gData = imageFrame.greenPaletteColorLookupTableData; - const bData = imageFrame.bluePaletteColorLookupTableData; + const [rData, gData, bData] = await Promise.all([ + imageFrame.redPaletteColorLookupTableData, + imageFrame.greenPaletteColorLookupTableData, + imageFrame.bluePaletteColorLookupTableData, + ]); + + if (!rData || !gData || !bData) { + throw new Error(`Palette data not found in ${imageFrame}`); + } + const len = imageFrame.redPaletteColorLookupTableData.length; let palIndex = 0; @@ -54,4 +61,6 @@ export default function (imageFrame, rgbaBuffer) { rgbaBuffer[rgbaIndex++] = bDataCleaned[value]; rgbaBuffer[rgbaIndex++] = 255; } + + return rgbaBuffer; } diff --git a/src/imageLoader/convertColorSpace.js b/src/imageLoader/convertColorSpace.js index 8985e039..ae098192 100644 --- a/src/imageLoader/convertColorSpace.js +++ b/src/imageLoader/convertColorSpace.js @@ -34,7 +34,7 @@ export default function convertColorSpace(imageFrame, imageData) { } else if (imageFrame.photometricInterpretation === 'YBR_ICT') { convertRGB(imageFrame, rgbaBuffer); } else if (imageFrame.photometricInterpretation === 'PALETTE COLOR') { - convertPALETTECOLOR(imageFrame, rgbaBuffer); + return convertPALETTECOLOR(imageFrame, rgbaBuffer); } else if (imageFrame.photometricInterpretation === 'YBR_FULL_422') { convertYBRFull422ByPixel(imageFrame.pixelData, rgbaBuffer); } else if (imageFrame.photometricInterpretation === 'YBR_FULL') { diff --git a/src/imageLoader/createImage.js b/src/imageLoader/createImage.js index d6551620..38cced4c 100644 --- a/src/imageLoader/createImage.js +++ b/src/imageLoader/createImage.js @@ -7,6 +7,8 @@ import getMinMax from '../shared/getMinMax.js'; import isJPEGBaseline8BitColor from './isJPEGBaseline8BitColor.js'; import { getOptions } from './internal/options.js'; +/*eslint complexity: ["error", 37]*/ + let lastImageIdDrawn = ''; function isModalityLUTForDisplay(sopClassUid) { @@ -67,17 +69,17 @@ function setPixelDataType(imageFrame) { } } -function createImage(imageId, pixelData, transferSyntax, options = {}) { +async function createImage(imageId, pixelData, transferSyntax, options = {}) { if (!pixelData || !pixelData.length) { - return Promise.reject(new Error('The file does not contain image data.')); + throw new Error('The file does not contain image data.'); } const { cornerstone } = external; const canvas = document.createElement('canvas'); - const imageFrame = getImageFrame(imageId); + const imageFrameData = getImageFrame(imageId); - const decodePromise = decodeImageFrame( - imageFrame, + const imageFrame = await decodeImageFrame( + imageFrameData, transferSyntax, pixelData, canvas, @@ -87,210 +89,198 @@ function createImage(imageId, pixelData, transferSyntax, options = {}) { const { decodeConfig } = getOptions(); const { convertFloatPixelDataToInt } = decodeConfig; - return new Promise((resolve, reject) => { - // eslint-disable-next-line complexity - decodePromise.then(function (imageFrame) { - // If we have a target buffer that was written to in the - // Decode task, point the image to it here. - // We can't have done it within the thread incase it was a SharedArrayBuffer. - let alreadyTyped = false; - - if (options.targetBuffer) { - let offset, length; - // If we have a target buffer, write to that instead. This helps reduce memory duplication. - - ({ offset, length } = options.targetBuffer); - const { arrayBuffer, type } = options.targetBuffer; - - let TypedArrayConstructor; - - if (length === null || length === undefined) { - length = imageFrame.pixelDataLength; - } - - if (offset === null || offset === undefined) { - offset = 0; - } - - switch (type) { - case 'Uint8Array': - TypedArrayConstructor = Uint8Array; - break; - case 'Uint16Array': - TypedArrayConstructor = Uint16Array; - break; - case 'Float32Array': - TypedArrayConstructor = Float32Array; - break; - default: - throw new Error( - 'target array for image does not have a valid type.' - ); - } - - if (length !== imageFrame.pixelDataLength) { - throw new Error( - 'target array for image does not have the same length as the decoded image length.' - ); - } - - // TypedArray.Set is api level and ~50x faster than copying elements even for - // Arrays of different types, which aren't simply memcpy ops. - let typedArray; - - if (arrayBuffer) { - typedArray = new TypedArrayConstructor(arrayBuffer, offset, length); - } else { - typedArray = new TypedArrayConstructor(imageFrame.pixelData); - } - - // If need to scale, need to scale correct array. - imageFrame.pixelData = typedArray; - alreadyTyped = true; - } + // If we have a target buffer that was written to in the + // Decode task, point the image to it here. + // We can't have done it within the thread incase it was a SharedArrayBuffer. + let alreadyTyped = false; - const imagePlaneModule = - cornerstone.metaData.get('imagePlaneModule', imageId) || {}; - const voiLutModule = - cornerstone.metaData.get('voiLutModule', imageId) || {}; - const modalityLutModule = - cornerstone.metaData.get('modalityLutModule', imageId) || {}; - const sopCommonModule = - cornerstone.metaData.get('sopCommonModule', imageId) || {}; - const isColorImage = isColorImageFn(imageFrame.photometricInterpretation); - - // JPEGBaseline (8 bits) is already returning the pixel data in the right format (rgba) - // because it's using a canvas to load and decode images. - if (!isJPEGBaseline8BitColor(imageFrame, transferSyntax)) { - if (!alreadyTyped) { - setPixelDataType(imageFrame); - } - - // convert color space - if (isColorImage) { - // setup the canvas context - canvas.height = imageFrame.rows; - canvas.width = imageFrame.columns; - - const context = canvas.getContext('2d'); - const imageData = context.createImageData( - imageFrame.columns, - imageFrame.rows - ); - - convertColorSpace(imageFrame, imageData); - imageFrame.imageData = imageData; - imageFrame.pixelData = imageData.data; - - // calculate smallest and largest PixelValue of the converted pixelData - const minMax = getMinMax(imageFrame.pixelData); - - imageFrame.smallestPixelValue = minMax.min; - imageFrame.largestPixelValue = minMax.max; - } - } + if (options.targetBuffer) { + let offset, length; + // If we have a target buffer, write to that instead. This helps reduce memory duplication. - const image = { - imageId, - color: isColorImage, - columnPixelSpacing: imagePlaneModule.columnPixelSpacing, - columns: imageFrame.columns, - height: imageFrame.rows, - preScale: imageFrame.preScale, - intercept: modalityLutModule.rescaleIntercept - ? modalityLutModule.rescaleIntercept - : 0, - slope: modalityLutModule.rescaleSlope - ? modalityLutModule.rescaleSlope - : 1, - invert: imageFrame.photometricInterpretation === 'MONOCHROME1', - minPixelValue: imageFrame.smallestPixelValue, - maxPixelValue: imageFrame.largestPixelValue, - rowPixelSpacing: imagePlaneModule.rowPixelSpacing, - rows: imageFrame.rows, - sizeInBytes: imageFrame.pixelData.byteLength, - width: imageFrame.columns, - windowCenter: voiLutModule.windowCenter - ? voiLutModule.windowCenter[0] - : undefined, - windowWidth: voiLutModule.windowWidth - ? voiLutModule.windowWidth[0] - : undefined, - decodeTimeInMS: imageFrame.decodeTimeInMS, - floatPixelData: undefined, - imageFrame, - }; - - // If pixel data is intrinsically floating 32 array, we convert it to int for - // display in cornerstone. For other cases when pixel data is typed as - // Float32Array for scaling; this conversion is not needed. - if ( - imageFrame.pixelData instanceof Float32Array && - convertFloatPixelDataToInt - ) { - const floatPixelData = imageFrame.pixelData; - const results = convertToIntPixelData(floatPixelData); - - image.minPixelValue = results.min; - image.maxPixelValue = results.max; - image.slope = results.slope; - image.intercept = results.intercept; - image.floatPixelData = floatPixelData; - image.getPixelData = () => results.intPixelData; - } else { - image.getPixelData = () => imageFrame.pixelData; - } + ({ offset, length } = options.targetBuffer); + const { arrayBuffer, type } = options.targetBuffer; - if (image.color) { - image.getCanvas = function () { - if (lastImageIdDrawn === imageId) { - return canvas; - } + let TypedArrayConstructor; - canvas.height = image.rows; - canvas.width = image.columns; - const context = canvas.getContext('2d'); + if (length === null || length === undefined) { + length = imageFrame.pixelDataLength; + } - context.putImageData(imageFrame.imageData, 0, 0); - lastImageIdDrawn = imageId; + if (offset === null || offset === undefined) { + offset = 0; + } - return canvas; - }; - } + switch (type) { + case 'Uint8Array': + TypedArrayConstructor = Uint8Array; + break; + case 'Uint16Array': + TypedArrayConstructor = Uint16Array; + break; + case 'Float32Array': + TypedArrayConstructor = Float32Array; + break; + default: + throw new Error('target array for image does not have a valid type.'); + } - // Modality LUT - if ( - modalityLutModule.modalityLUTSequence && - modalityLutModule.modalityLUTSequence.length > 0 && - isModalityLUTForDisplay(sopCommonModule.sopClassUID) - ) { - image.modalityLUT = modalityLutModule.modalityLUTSequence[0]; - } + if (length !== imageFrame.pixelDataLength) { + throw new Error( + 'target array for image does not have the same length as the decoded image length.' + ); + } - // VOI LUT - if ( - voiLutModule.voiLUTSequence && - voiLutModule.voiLUTSequence.length > 0 - ) { - image.voiLUT = voiLutModule.voiLUTSequence[0]; - } + // TypedArray.Set is api level and ~50x faster than copying elements even for + // Arrays of different types, which aren't simply memcpy ops. + let typedArray; - if (image.color) { - image.windowWidth = 255; - image.windowCenter = 127; - } + if (arrayBuffer) { + typedArray = new TypedArrayConstructor(arrayBuffer, offset, length); + } else { + typedArray = new TypedArrayConstructor(imageFrame.pixelData); + } - // set the ww/wc to cover the dynamic range of the image if no values are supplied - if (image.windowCenter === undefined || image.windowWidth === undefined) { - const maxVoi = image.maxPixelValue * image.slope + image.intercept; - const minVoi = image.minPixelValue * image.slope + image.intercept; + // If need to scale, need to scale correct array. + imageFrame.pixelData = typedArray; + alreadyTyped = true; + } + + const imagePlaneModule = + cornerstone.metaData.get('imagePlaneModule', imageId) || {}; + const voiLutModule = cornerstone.metaData.get('voiLutModule', imageId) || {}; + const modalityLutModule = + cornerstone.metaData.get('modalityLutModule', imageId) || {}; + const sopCommonModule = + cornerstone.metaData.get('sopCommonModule', imageId) || {}; + const isColorImage = isColorImageFn(imageFrame.photometricInterpretation); + + // JPEGBaseline (8 bits) is already returning the pixel data in the right format (rgba) + // because it's using a canvas to load and decode images. + if (!isJPEGBaseline8BitColor(imageFrame, transferSyntax)) { + if (!alreadyTyped) { + setPixelDataType(imageFrame); + } - image.windowWidth = maxVoi - minVoi; - image.windowCenter = (maxVoi + minVoi) / 2; + // convert color space + if (isColorImage) { + // setup the canvas context + canvas.height = imageFrame.rows; + canvas.width = imageFrame.columns; + + const context = canvas.getContext('2d'); + const imageData = context.createImageData( + imageFrame.columns, + imageFrame.rows + ); + + await convertColorSpace(imageFrame, imageData); + imageFrame.imageData = imageData; + imageFrame.pixelData = imageData.data; + + // calculate smallest and largest PixelValue of the converted pixelData + const minMax = getMinMax(imageFrame.pixelData); + + imageFrame.smallestPixelValue = minMax.min; + imageFrame.largestPixelValue = minMax.max; + } + } + + const image = { + imageId, + color: isColorImage, + columnPixelSpacing: imagePlaneModule.columnPixelSpacing, + columns: imageFrame.columns, + height: imageFrame.rows, + preScale: imageFrame.preScale, + intercept: modalityLutModule.rescaleIntercept + ? modalityLutModule.rescaleIntercept + : 0, + slope: modalityLutModule.rescaleSlope ? modalityLutModule.rescaleSlope : 1, + invert: imageFrame.photometricInterpretation === 'MONOCHROME1', + minPixelValue: imageFrame.smallestPixelValue, + maxPixelValue: imageFrame.largestPixelValue, + rowPixelSpacing: imagePlaneModule.rowPixelSpacing, + rows: imageFrame.rows, + sizeInBytes: imageFrame.pixelData.byteLength, + width: imageFrame.columns, + windowCenter: voiLutModule.windowCenter + ? voiLutModule.windowCenter[0] + : undefined, + windowWidth: voiLutModule.windowWidth + ? voiLutModule.windowWidth[0] + : undefined, + decodeTimeInMS: imageFrame.decodeTimeInMS, + floatPixelData: undefined, + imageFrame, + }; + + // If pixel data is intrinsically floating 32 array, we convert it to int for + // display in cornerstone. For other cases when pixel data is typed as + // Float32Array for scaling; this conversion is not needed. + if ( + imageFrame.pixelData instanceof Float32Array && + convertFloatPixelDataToInt + ) { + const floatPixelData = imageFrame.pixelData; + const results = convertToIntPixelData(floatPixelData); + + image.minPixelValue = results.min; + image.maxPixelValue = results.max; + image.slope = results.slope; + image.intercept = results.intercept; + image.floatPixelData = floatPixelData; + image.getPixelData = () => results.intPixelData; + } else { + image.getPixelData = () => imageFrame.pixelData; + } + + if (image.color) { + image.getCanvas = function () { + if (lastImageIdDrawn === imageId) { + return canvas; } - resolve(image); - }, reject); - }); + + canvas.height = image.rows; + canvas.width = image.columns; + const context = canvas.getContext('2d'); + + context.putImageData(imageFrame.imageData, 0, 0); + lastImageIdDrawn = imageId; + + return canvas; + }; + } + + // Modality LUT + if ( + modalityLutModule.modalityLUTSequence && + modalityLutModule.modalityLUTSequence.length > 0 && + isModalityLUTForDisplay(sopCommonModule.sopClassUID) + ) { + image.modalityLUT = modalityLutModule.modalityLUTSequence[0]; + } + + // VOI LUT + if (voiLutModule.voiLUTSequence && voiLutModule.voiLUTSequence.length > 0) { + image.voiLUT = voiLutModule.voiLUTSequence[0]; + } + + if (image.color) { + image.windowWidth = 255; + image.windowCenter = 127; + } + + // set the ww/wc to cover the dynamic range of the image if no values are supplied + if (image.windowCenter === undefined || image.windowWidth === undefined) { + const maxVoi = image.maxPixelValue * image.slope + image.intercept; + const minVoi = image.minPixelValue * image.slope + image.intercept; + + image.windowWidth = maxVoi - minVoi; + image.windowCenter = (maxVoi + minVoi) / 2; + } + + return image; } export default createImage; diff --git a/src/imageLoader/internal/xhrRequest.js b/src/imageLoader/internal/xhrRequest.js index fdb67b2f..0eb50da9 100644 --- a/src/imageLoader/internal/xhrRequest.js +++ b/src/imageLoader/internal/xhrRequest.js @@ -33,8 +33,12 @@ function xhrRequest(url, imageId, defaultHeaders = {}, params = {}) { const headers = Object.assign({}, defaultHeaders, beforeSendHeaders); Object.keys(headers).forEach(function (key) { - if (headers[key] === null) return; - if (key === 'Accept' && url.indexOf('accept=') !== -1) return; + if (headers[key] === null) { + return; + } + if (key === 'Accept' && url.indexOf('accept=') !== -1) { + return; + } xhr.setRequestHeader(key, headers[key]); });