diff --git a/src/imageLoader/createImage.js b/src/imageLoader/createImage.js index 3a5dfac4..0662e2c5 100644 --- a/src/imageLoader/createImage.js +++ b/src/imageLoader/createImage.js @@ -122,6 +122,8 @@ function createImage(imageId, pixelData, transferSyntax, options) { imageFrame.smallestPixelValue = minMax.min; imageFrame.largestPixelValue = minMax.max; } + } else { + imageFrame.pixelData = new Uint8Array(imageFrame.pixelData); } const image = { diff --git a/src/imageLoader/decodeImageFrame.js b/src/imageLoader/decodeImageFrame.js index f576cbaa..6b0c9be4 100644 --- a/src/imageLoader/decodeImageFrame.js +++ b/src/imageLoader/decodeImageFrame.js @@ -1,7 +1,8 @@ import { getOptions } from './internal/options.js'; import webWorkerManager from './webWorkerManager.js'; import decodeJPEGBaseline8BitColor from './decodeJPEGBaseline8BitColor.js'; - +import isNativeJpeg8ColorDecoderSupported from './isNativeJpeg8ColorDecoderSupported.js'; +import isJPEGBaseline8BitColor from './isJPEGBaseline8BitColor.js'; // TODO: Find a way to allow useWebWorkers: false that doesn't make the main bundle huge import { default as decodeImageFrameHandler } from '../shared/decodeImageFrame.js'; import calculateMinMax from '../shared/calculateMinMax.js'; @@ -88,8 +89,9 @@ function decodeImageFrame( // Handle 8-bit JPEG Baseline color images using the browser's built-in // JPEG decoding if ( - imageFrame.bitsAllocated === 8 && - (imageFrame.samplesPerPixel === 3 || imageFrame.samplesPerPixel === 4) + isJPEGBaseline8BitColor(imageFrame, transferSyntax) && + (options.useBrowserDecoderForJpeg8Color || + !isNativeJpeg8ColorDecoderSupported()) ) { return decodeJPEGBaseline8BitColor(imageFrame, pixelData, canvas); } diff --git a/src/imageLoader/isNativeJpeg8ColorDecoderSupported.js b/src/imageLoader/isNativeJpeg8ColorDecoderSupported.js new file mode 100644 index 00000000..f8c8534e --- /dev/null +++ b/src/imageLoader/isNativeJpeg8ColorDecoderSupported.js @@ -0,0 +1,3 @@ +export default function isNativeJpeg8ColorDecoderSupported() { + return 'createImageBitmap' in window && 'OffscreenCanvas' in window; +} diff --git a/src/shared/decodeImageFrame.js b/src/shared/decodeImageFrame.js index a4b9ea19..9e998bbd 100644 --- a/src/shared/decodeImageFrame.js +++ b/src/shared/decodeImageFrame.js @@ -5,6 +5,7 @@ import decodeJPEGBaseline from './decoders/decodeJPEGBaseline.js'; import decodeJPEGLossless from './decoders/decodeJPEGLossless.js'; import decodeJPEGLS from './decoders/decodeJPEGLS.js'; import decodeJPEG2000 from './decoders/decodeJPEG2000.js'; +import decodeJPEGBaseline8BitColor from './decoders/decodeJPEGBaseline8BitColor.js'; function decodeImageFrame( imageFrame, @@ -32,6 +33,12 @@ function decodeImageFrame( imageFrame = decodeRLE(imageFrame, pixelData); } else if (transferSyntax === '1.2.840.10008.1.2.4.50') { // JPEG Baseline lossy process 1 (8 bit) + if ( + imageFrame.bitsAllocated === 8 && + (imageFrame.samplesPerPixel === 3 || imageFrame.samplesPerPixel === 4) + ) { + return decodeJPEGBaseline8BitColor(imageFrame, pixelData); + } imageFrame = decodeJPEGBaseline(imageFrame, pixelData); } else if (transferSyntax === '1.2.840.10008.1.2.4.51') { // JPEG Baseline lossy process 2 & 4 (12 bit) diff --git a/src/shared/decoders/decodeJPEGBaseline8BitColor.js b/src/shared/decoders/decodeJPEGBaseline8BitColor.js new file mode 100644 index 00000000..de6afbce --- /dev/null +++ b/src/shared/decoders/decodeJPEGBaseline8BitColor.js @@ -0,0 +1,40 @@ +function decodeJPEGBaseline8BitColor(imageFrame, pixelData) { + const imgBlob = new Blob([pixelData], { type: 'image/jpeg' }); + + return new Promise((resolve, reject) => { + try { + const start = new Date().getTime(); + + createImageBitmap(imgBlob).then(imageBitmap => { + const offscreen = new OffscreenCanvas( + imageBitmap.width, + imageBitmap.height + ); + + imageFrame.rows = imageBitmap.height; + imageFrame.columns = imageBitmap.width; + const context = offscreen.getContext('2d'); + + context.drawImage(imageBitmap, 0, 0); + const imageData = context.getImageData( + 0, + 0, + imageBitmap.width, + imageBitmap.height + ); + const end = new Date().getTime(); + + imageFrame.pixelData = imageData.data; + imageFrame.imageData = imageData; + imageFrame.decodeTimeInMS = end - start; + + // calculate smallest and largest PixelValue + resolve(imageFrame); + }); + } catch (error) { + reject(error); + } + }); +} + +export default decodeJPEGBaseline8BitColor; diff --git a/src/webWorker/decodeTask/decodeTask.js b/src/webWorker/decodeTask/decodeTask.js index ef7d9445..b3026644 100644 --- a/src/webWorker/decodeTask/decodeTask.js +++ b/src/webWorker/decodeTask/decodeTask.js @@ -38,35 +38,38 @@ function handler(data, doneCallback) { const strict = decodeConfig && decodeConfig.decodeTask && decodeConfig.decodeTask.strict; - const imageFrame = data.data.imageFrame; + const imageFrameData = data.data.imageFrame; // convert pixel data from ArrayBuffer to Uint8Array since web workers support passing ArrayBuffers but // not typed arrays const pixelData = new Uint8Array(data.data.pixelData); - decodeImageFrame( - imageFrame, + const result = decodeImageFrame( + imageFrameData, data.data.transferSyntax, pixelData, decodeConfig.decodeTask, data.data.options ); - if (!imageFrame.pixelData) { - throw new Error( - 'decodeTask: imageFrame.pixelData is undefined after decoding' - ); - } + Promise.resolve(result).then(imageFrame => { + if (!imageFrame.pixelData) { + throw new Error( + 'decodeTask: imageFrame.pixelData is undefined after decoding' + ); + } - calculateMinMax(imageFrame, strict); + // TODO: do we need this here? min/max is being calculated in imageLoader\createImage again! + calculateMinMax(imageFrame, strict); - // convert from TypedArray to ArrayBuffer since web workers support passing ArrayBuffers but not - // typed arrays - imageFrame.pixelData = imageFrame.pixelData.buffer; + // convert from TypedArray to ArrayBuffer since web workers support passing ArrayBuffers but not + // typed arrays + imageFrame.pixelData = imageFrame.pixelData.buffer; - // invoke the callback with our result and pass the pixelData in the transferList to move it to - // UI thread without making a copy - doneCallback(imageFrame, [imageFrame.pixelData]); + // invoke the callback with our result and pass the pixelData in the transferList to move it to + // UI thread without making a copy + doneCallback(imageFrame, [imageFrame.pixelData]); + }); } export default {