From 6567df0afc93b19a9e558d45a46bc1a4e269747c Mon Sep 17 00:00:00 2001 From: Davide Punzo Date: Mon, 26 Sep 2022 14:31:13 +0200 Subject: [PATCH 1/3] fix: use array of mediatypes and transferSyntax when calling a get xhrRequest for image loading --- src/imageLoader/internal/xhrRequest.js | 2 + src/imageLoader/wadors/getPixelData.js | 139 ++++++++++++++++++++++++- src/imageLoader/wadors/loadImage.js | 78 ++++++++++++-- 3 files changed, 206 insertions(+), 13 deletions(-) diff --git a/src/imageLoader/internal/xhrRequest.js b/src/imageLoader/internal/xhrRequest.js index 0eb50da9..b3714dc8 100644 --- a/src/imageLoader/internal/xhrRequest.js +++ b/src/imageLoader/internal/xhrRequest.js @@ -21,6 +21,7 @@ function xhrRequest(url, imageId, defaultHeaders = {}, params = {}) { const xhr = new XMLHttpRequest(); xhr.open('get', url, true); + const beforeSendHeaders = options.beforeSend( xhr, imageId, @@ -39,6 +40,7 @@ function xhrRequest(url, imageId, defaultHeaders = {}, params = {}) { if (key === 'Accept' && url.indexOf('accept=') !== -1) { return; } + xhr.setRequestHeader(key, headers[key]); }); diff --git a/src/imageLoader/wadors/getPixelData.js b/src/imageLoader/wadors/getPixelData.js index afade193..2c6dfddc 100644 --- a/src/imageLoader/wadors/getPixelData.js +++ b/src/imageLoader/wadors/getPixelData.js @@ -29,9 +29,144 @@ function uint8ArrayToString(data, offset, length) { return str; } -function getPixelData(uri, imageId, mediaType = 'application/octet-stream') { +/** + * Asserts that a given media type is valid. + * + * @params {String} mediaType media type + */ + +function assertMediaTypeIsValid(mediaType) { + if (!mediaType) { + throw new Error(`Not a valid media type: ${mediaType}`); + } + const sepIndex = mediaType.indexOf('/'); + + if (sepIndex === -1) { + throw new Error(`Not a valid media type: ${mediaType}`); + } + const mediaTypeType = mediaType.slice(0, sepIndex); + + const types = ['application', 'image', 'text', 'video']; + + if (!types.includes(mediaTypeType)) { + throw new Error(`Not a valid media type: ${mediaType}`); + } + if (mediaType.slice(sepIndex + 1).includes('/')) { + throw new Error(`Not a valid media type: ${mediaType}`); + } +} + +/** + * Parses media type and extracts its type and subtype. + * + * @param mediaType e.g. image/jpeg + * @private + */ +function parseMediaType(mediaType) { + assertMediaTypeIsValid(mediaType); + + return mediaType.split('/'); +} + +/** + * Builds an accept header field value for HTTP GET multipart request + messages. + * + * @param {Array} mediaTypes Acceptable media types + * @param {Object} supportedMediaTypes Supported media types + */ + +function buildMultipartAcceptHeaderFieldValue(mediaTypes, supportedMediaTypes) { + if (!Array.isArray(mediaTypes)) { + throw new Error('Acceptable media types must be provided as an Array'); + } + + if (typeof supportedMediaTypes !== 'object') { + throw new Error( + 'Supported media types must be provided as an Array or an Object' + ); + } + + const fieldValueParts = []; + + mediaTypes.forEach((item) => { + const { transferSyntaxUID, mediaType } = item; + + assertMediaTypeIsValid(mediaType); + + let fieldValue = `multipart/related; type="${mediaType}"`; + + // SupportedMediaTypes is a lookup table that maps Transfer Syntax UID + // to one or more Media Types + if (!Object.values(supportedMediaTypes).flat(1).includes(mediaType)) { + if ( + (!mediaType.endsWith('/*') || !mediaType.endsWith('/')) && + mediaType !== 'application/octet-stream' + ) { + throw new Error( + `Media type ${mediaType} is not supported for requested resource` + ); + } + } + if (transferSyntaxUID) { + if (transferSyntaxUID !== '*') { + if (!Object.keys(supportedMediaTypes).includes(transferSyntaxUID)) { + throw new Error( + `Transfer syntax ${transferSyntaxUID} is not supported for requested resource` + ); + } + const expectedMediaTypes = supportedMediaTypes[transferSyntaxUID]; + + if (!expectedMediaTypes.includes(mediaType)) { + const actualType = parseMediaType(mediaType)[0]; + + expectedMediaTypes.map((expectedMediaType) => { + const expectedType = parseMediaType(expectedMediaType)[0]; + + const haveSameType = actualType === expectedType; + + if ( + haveSameType && + (mediaType.endsWith('/*') || mediaType.endsWith('/')) + ) { + return null; + } + + throw new Error( + `Transfer syntax ${transferSyntaxUID} is not supported for requested resource` + ); + }); + } + } + fieldValue += `; transfer-syntax=${transferSyntaxUID}`; + } + + fieldValueParts.push(fieldValue); + }); + + return fieldValueParts.join(', '); +} + +function getPixelData(uri, imageId, mediaTypes) { + const supportedMediaTypes = { + '1.2.840.10008.1.2.5': ['image/x-dicom-rle'], + '1.2.840.10008.1.2.4.50': ['image/jpeg'], + '1.2.840.10008.1.2.4.51': ['image/jpeg'], + '1.2.840.10008.1.2.4.57': ['image/jpeg'], + '1.2.840.10008.1.2.4.70': ['image/jpeg', 'image/jll'], + '1.2.840.10008.1.2.4.80': ['image/x-jls', 'image/jls'], + '1.2.840.10008.1.2.4.81': ['image/x-jls', 'image/jls'], + '1.2.840.10008.1.2.4.90': ['image/jp2'], + '1.2.840.10008.1.2.4.91': ['image/jp2'], + '1.2.840.10008.1.2.4.92': ['image/jpx'], + '1.2.840.10008.1.2.4.93': ['image/jpx'], + }; + const headers = { - Accept: mediaType, + Accept: buildMultipartAcceptHeaderFieldValue( + mediaTypes, + supportedMediaTypes + ), }; return new Promise((resolve, reject) => { diff --git a/src/imageLoader/wadors/loadImage.js b/src/imageLoader/wadors/loadImage.js index 9fd63a54..9faa4c17 100644 --- a/src/imageLoader/wadors/loadImage.js +++ b/src/imageLoader/wadors/loadImage.js @@ -72,21 +72,77 @@ function loadImage(imageId, options = {}) { const promise = new Promise((resolve, reject) => { // TODO: load bulk data items that we might need - // Uncomment this on to test jpegls codec in OHIF - // const mediaType = 'multipart/related; type="image/x-jls"'; - // const mediaType = 'multipart/related; type="application/octet-stream"; transfer-syntax="image/x-jls"'; - const mediaType = - 'multipart/related; type=application/octet-stream; transfer-syntax=*'; - // const mediaType = - // 'multipart/related; type="image/jpeg"; transfer-syntax=1.2.840.10008.1.2.4.50'; - - function sendXHR(imageURI, imageId, mediaType) { + const xdicomrleMediaType = 'image/x-dicom-rle'; + const xdicomrleTransferSyntaxUID = '1.2.840.10008.1.2.5'; + const jpegMediaType = 'image/jpeg'; + const jpegTransferSyntaxUID1 = '1.2.840.10008.1.2.4.50'; + const jpegTransferSyntaxUID2 = '1.2.840.10008.1.2.4.51'; + const jpegTransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.57'; + const jllMediaType = 'image/jll'; + const jlllTransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.70'; + const jlsMediaType = 'image/jls'; + const jlsTransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.80'; + const jlsTransferSyntaxUID = '1.2.840.10008.1.2.4.81'; + const jp2MediaType = 'image/jp2'; + const jp2TransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.90'; + const jp2TransferSyntaxUID = '1.2.840.10008.1.2.4.91'; + const octetStreamMediaType = 'application/octet-stream'; + const octetStreamTransferSyntaxUID = '*'; + const mediaTypes = []; + + mediaTypes.push( + ...[ + { + mediaType: xdicomrleMediaType, + transferSyntaxUID: xdicomrleTransferSyntaxUID, + }, + { + mediaType: jpegMediaType, + transferSyntaxUID: jpegTransferSyntaxUID1, + }, + { + mediaType: jpegMediaType, + transferSyntaxUID: jpegTransferSyntaxUID2, + }, + { + mediaType: jpegMediaType, + transferSyntaxUID: jpegTransferSyntaxUIDlossless, + }, + { + mediaType: jllMediaType, + transferSyntaxUID: jlllTransferSyntaxUIDlossless, + }, + { + mediaType: jlsMediaType, + transferSyntaxUID: jlsTransferSyntaxUIDlossless, + }, + { + mediaType: jlsMediaType, + transferSyntaxUID: jlsTransferSyntaxUID, + }, + { + mediaType: jp2MediaType, + transferSyntaxUID: jp2TransferSyntaxUIDlossless, + }, + { + mediaType: jp2MediaType, + transferSyntaxUID: jp2TransferSyntaxUID, + }, + { + mediaType: octetStreamMediaType, + transferSyntaxUID: octetStreamTransferSyntaxUID, + }, + ] + ); + + function sendXHR(imageURI, imageId, mediaTypes) { // get the pixel data from the server - return getPixelData(imageURI, imageId, mediaType) + return getPixelData(imageURI, imageId, mediaTypes) .then((result) => { const transferSyntax = getTransferSyntaxForContentType( result.contentType ); + const pixelData = result.imageFrame.pixelData; const imagePromise = createImage( imageId, @@ -115,7 +171,7 @@ function loadImage(imageId, options = {}) { const uri = imageId.substring(7); imageRetrievalPool.addRequest( - sendXHR.bind(this, uri, imageId, mediaType), + sendXHR.bind(this, uri, imageId, mediaTypes), requestType, additionalDetails, priority, From 55628dd0ed4ef2b8d77cd6cb3394f9ba264ad5e6 Mon Sep 17 00:00:00 2001 From: Davide Punzo Date: Thu, 29 Sep 2022 00:18:02 +0200 Subject: [PATCH 2/3] Apply Alireza review --- src/imageLoader/wadors/getPixelData.js | 119 +----------------- src/imageLoader/wadors/loadImage.js | 65 +--------- .../mediaTypesUtils/assertMediaTypeIsValid.js | 26 ++++ .../buildMultipartAcceptHeaderFieldValue.js | 102 +++++++++++++++ src/shared/mediaTypesUtils/mediaTypeJLL.js | 4 + src/shared/mediaTypesUtils/mediaTypeJLS.js | 5 + src/shared/mediaTypesUtils/mediaTypeJP2.js | 5 + src/shared/mediaTypesUtils/mediaTypeJPEG.js | 11 ++ .../mediaTypesUtils/mediaTypeOctetStream.js | 4 + .../mediaTypesUtils/mediaTypeXDicomRLE.js | 4 + src/shared/mediaTypesUtils/mediaTypes.js | 73 +++++++++++ 11 files changed, 236 insertions(+), 182 deletions(-) create mode 100644 src/shared/mediaTypesUtils/assertMediaTypeIsValid.js create mode 100644 src/shared/mediaTypesUtils/buildMultipartAcceptHeaderFieldValue.js create mode 100644 src/shared/mediaTypesUtils/mediaTypeJLL.js create mode 100644 src/shared/mediaTypesUtils/mediaTypeJLS.js create mode 100644 src/shared/mediaTypesUtils/mediaTypeJP2.js create mode 100644 src/shared/mediaTypesUtils/mediaTypeJPEG.js create mode 100644 src/shared/mediaTypesUtils/mediaTypeOctetStream.js create mode 100644 src/shared/mediaTypesUtils/mediaTypeXDicomRLE.js create mode 100644 src/shared/mediaTypesUtils/mediaTypes.js diff --git a/src/imageLoader/wadors/getPixelData.js b/src/imageLoader/wadors/getPixelData.js index 2c6dfddc..e0f725bf 100644 --- a/src/imageLoader/wadors/getPixelData.js +++ b/src/imageLoader/wadors/getPixelData.js @@ -1,5 +1,6 @@ import { xhrRequest } from '../internal/index.js'; import findIndexOfString from './findIndexOfString.js'; +import buildMultipartAcceptHeaderFieldValue from '../../shared/mediaTypesUtils/buildMultipartAcceptHeaderFieldValue.js'; function findBoundary(header) { for (let i = 0; i < header.length; i++) { @@ -29,124 +30,6 @@ function uint8ArrayToString(data, offset, length) { return str; } -/** - * Asserts that a given media type is valid. - * - * @params {String} mediaType media type - */ - -function assertMediaTypeIsValid(mediaType) { - if (!mediaType) { - throw new Error(`Not a valid media type: ${mediaType}`); - } - const sepIndex = mediaType.indexOf('/'); - - if (sepIndex === -1) { - throw new Error(`Not a valid media type: ${mediaType}`); - } - const mediaTypeType = mediaType.slice(0, sepIndex); - - const types = ['application', 'image', 'text', 'video']; - - if (!types.includes(mediaTypeType)) { - throw new Error(`Not a valid media type: ${mediaType}`); - } - if (mediaType.slice(sepIndex + 1).includes('/')) { - throw new Error(`Not a valid media type: ${mediaType}`); - } -} - -/** - * Parses media type and extracts its type and subtype. - * - * @param mediaType e.g. image/jpeg - * @private - */ -function parseMediaType(mediaType) { - assertMediaTypeIsValid(mediaType); - - return mediaType.split('/'); -} - -/** - * Builds an accept header field value for HTTP GET multipart request - messages. - * - * @param {Array} mediaTypes Acceptable media types - * @param {Object} supportedMediaTypes Supported media types - */ - -function buildMultipartAcceptHeaderFieldValue(mediaTypes, supportedMediaTypes) { - if (!Array.isArray(mediaTypes)) { - throw new Error('Acceptable media types must be provided as an Array'); - } - - if (typeof supportedMediaTypes !== 'object') { - throw new Error( - 'Supported media types must be provided as an Array or an Object' - ); - } - - const fieldValueParts = []; - - mediaTypes.forEach((item) => { - const { transferSyntaxUID, mediaType } = item; - - assertMediaTypeIsValid(mediaType); - - let fieldValue = `multipart/related; type="${mediaType}"`; - - // SupportedMediaTypes is a lookup table that maps Transfer Syntax UID - // to one or more Media Types - if (!Object.values(supportedMediaTypes).flat(1).includes(mediaType)) { - if ( - (!mediaType.endsWith('/*') || !mediaType.endsWith('/')) && - mediaType !== 'application/octet-stream' - ) { - throw new Error( - `Media type ${mediaType} is not supported for requested resource` - ); - } - } - if (transferSyntaxUID) { - if (transferSyntaxUID !== '*') { - if (!Object.keys(supportedMediaTypes).includes(transferSyntaxUID)) { - throw new Error( - `Transfer syntax ${transferSyntaxUID} is not supported for requested resource` - ); - } - const expectedMediaTypes = supportedMediaTypes[transferSyntaxUID]; - - if (!expectedMediaTypes.includes(mediaType)) { - const actualType = parseMediaType(mediaType)[0]; - - expectedMediaTypes.map((expectedMediaType) => { - const expectedType = parseMediaType(expectedMediaType)[0]; - - const haveSameType = actualType === expectedType; - - if ( - haveSameType && - (mediaType.endsWith('/*') || mediaType.endsWith('/')) - ) { - return null; - } - - throw new Error( - `Transfer syntax ${transferSyntaxUID} is not supported for requested resource` - ); - }); - } - } - fieldValue += `; transfer-syntax=${transferSyntaxUID}`; - } - - fieldValueParts.push(fieldValue); - }); - - return fieldValueParts.join(', '); -} - function getPixelData(uri, imageId, mediaTypes) { const supportedMediaTypes = { '1.2.840.10008.1.2.5': ['image/x-dicom-rle'], diff --git a/src/imageLoader/wadors/loadImage.js b/src/imageLoader/wadors/loadImage.js index 9faa4c17..773202de 100644 --- a/src/imageLoader/wadors/loadImage.js +++ b/src/imageLoader/wadors/loadImage.js @@ -1,7 +1,7 @@ import external from '../../externalModules.js'; import getPixelData from './getPixelData.js'; import createImage from '../createImage.js'; - +import { mediaTypes } from '../../shared/mediaTypesUtils/mediaTypes.js'; /** * Helper method to extract the transfer-syntax from the response of the server. * @param {string} contentType The value of the content-type header as returned by the WADO-RS server. @@ -72,69 +72,6 @@ function loadImage(imageId, options = {}) { const promise = new Promise((resolve, reject) => { // TODO: load bulk data items that we might need - const xdicomrleMediaType = 'image/x-dicom-rle'; - const xdicomrleTransferSyntaxUID = '1.2.840.10008.1.2.5'; - const jpegMediaType = 'image/jpeg'; - const jpegTransferSyntaxUID1 = '1.2.840.10008.1.2.4.50'; - const jpegTransferSyntaxUID2 = '1.2.840.10008.1.2.4.51'; - const jpegTransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.57'; - const jllMediaType = 'image/jll'; - const jlllTransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.70'; - const jlsMediaType = 'image/jls'; - const jlsTransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.80'; - const jlsTransferSyntaxUID = '1.2.840.10008.1.2.4.81'; - const jp2MediaType = 'image/jp2'; - const jp2TransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.90'; - const jp2TransferSyntaxUID = '1.2.840.10008.1.2.4.91'; - const octetStreamMediaType = 'application/octet-stream'; - const octetStreamTransferSyntaxUID = '*'; - const mediaTypes = []; - - mediaTypes.push( - ...[ - { - mediaType: xdicomrleMediaType, - transferSyntaxUID: xdicomrleTransferSyntaxUID, - }, - { - mediaType: jpegMediaType, - transferSyntaxUID: jpegTransferSyntaxUID1, - }, - { - mediaType: jpegMediaType, - transferSyntaxUID: jpegTransferSyntaxUID2, - }, - { - mediaType: jpegMediaType, - transferSyntaxUID: jpegTransferSyntaxUIDlossless, - }, - { - mediaType: jllMediaType, - transferSyntaxUID: jlllTransferSyntaxUIDlossless, - }, - { - mediaType: jlsMediaType, - transferSyntaxUID: jlsTransferSyntaxUIDlossless, - }, - { - mediaType: jlsMediaType, - transferSyntaxUID: jlsTransferSyntaxUID, - }, - { - mediaType: jp2MediaType, - transferSyntaxUID: jp2TransferSyntaxUIDlossless, - }, - { - mediaType: jp2MediaType, - transferSyntaxUID: jp2TransferSyntaxUID, - }, - { - mediaType: octetStreamMediaType, - transferSyntaxUID: octetStreamTransferSyntaxUID, - }, - ] - ); - function sendXHR(imageURI, imageId, mediaTypes) { // get the pixel data from the server return getPixelData(imageURI, imageId, mediaTypes) diff --git a/src/shared/mediaTypesUtils/assertMediaTypeIsValid.js b/src/shared/mediaTypesUtils/assertMediaTypeIsValid.js new file mode 100644 index 00000000..ab525caa --- /dev/null +++ b/src/shared/mediaTypesUtils/assertMediaTypeIsValid.js @@ -0,0 +1,26 @@ +/** + * Asserts that a given media type is valid. + * + * @params {String} mediaType media type + */ + +export default function assertMediaTypeIsValid(mediaType) { + if (!mediaType) { + throw new Error(`Not a valid media type: ${mediaType}`); + } + const sepIndex = mediaType.indexOf('/'); + + if (sepIndex === -1) { + throw new Error(`Not a valid media type: ${mediaType}`); + } + const mediaTypeType = mediaType.slice(0, sepIndex); + + const types = ['application', 'image', 'text', 'video']; + + if (!types.includes(mediaTypeType)) { + throw new Error(`Not a valid media type: ${mediaType}`); + } + if (mediaType.slice(sepIndex + 1).includes('/')) { + throw new Error(`Not a valid media type: ${mediaType}`); + } +} diff --git a/src/shared/mediaTypesUtils/buildMultipartAcceptHeaderFieldValue.js b/src/shared/mediaTypesUtils/buildMultipartAcceptHeaderFieldValue.js new file mode 100644 index 00000000..1fe37bb5 --- /dev/null +++ b/src/shared/mediaTypesUtils/buildMultipartAcceptHeaderFieldValue.js @@ -0,0 +1,102 @@ +import assertMediaTypeIsValid from './assertMediaTypeIsValid.js'; + +/** + * Builds an accept header field value for HTTP GET multipart request messages. + * + * Takes in input a media types array of type [{mediaType, transferSyntaxUID}, ... ], + * cross-compares with a map defining the supported media types per transferSyntaxUID + * and finally composes a string for the accept header field value as in example below: + * + * "multipart/related; type="image/x-dicom-rle"; transfer-syntax=1.2.840.10008.1.2.5, + * multipart/related; type="image/jpeg"; transfer-syntax=1.2.840.10008.1.2.4.50, + * multipart/related; type="application/octet-stream"; transfer-syntax=*" + * + * NOTE: the xhr request will try to fetch with all the transfer-syntax syntaxes + * specified in the accept header field value in descending order. + * The first element ("image/x-dicom-rle" in this example) has the highest priority. + * + * @param {Array} mediaTypes Acceptable media types + * @param {Object} supportedMediaTypes Supported media types + * + * @returns {string} accept header field value + */ + +export default function buildMultipartAcceptHeaderFieldValue( + mediaTypes, + supportedMediaTypes +) { + if (!Array.isArray(mediaTypes)) { + throw new Error('Acceptable media types must be provided as an Array'); + } + + if (typeof supportedMediaTypes !== 'object') { + throw new Error( + 'Supported media types must be provided as an Array or an Object' + ); + } + + const supportedMediaTypesArray = Object.values(supportedMediaTypes).flat(1); + + supportedMediaTypesArray.forEach((supportedMediaType) => { + assertMediaTypeIsValid(supportedMediaType); + }); + + const fieldValueParts = []; + + mediaTypes.forEach((item) => { + const { transferSyntaxUID, mediaType } = item; + + assertMediaTypeIsValid(mediaType); + + let fieldValue = `multipart/related; type="${mediaType}"`; + + // supportedMediaTypesArray is a lookup table that maps Transfer Syntax UID + // to one or more Media Types + if (!supportedMediaTypesArray.includes(mediaType)) { + if ( + (!mediaType.endsWith('/*') || !mediaType.endsWith('/')) && + mediaType !== 'application/octet-stream' + ) { + throw new Error( + `Media type ${mediaType} is not supported for requested resource` + ); + } + } + if (transferSyntaxUID) { + if (transferSyntaxUID !== '*') { + if (!Object.keys(supportedMediaTypes).includes(transferSyntaxUID)) { + throw new Error( + `Transfer syntax ${transferSyntaxUID} is not supported for requested resource` + ); + } + const expectedMediaTypes = supportedMediaTypes[transferSyntaxUID]; + + if (!expectedMediaTypes.includes(mediaType)) { + const actualType = mediaType.split('/')[0]; + + expectedMediaTypes.map((expectedMediaType) => { + const expectedType = expectedMediaType.split('/')[0]; + + const haveSameType = actualType === expectedType; + + if ( + haveSameType && + (mediaType.endsWith('/*') || mediaType.endsWith('/')) + ) { + return null; + } + + throw new Error( + `Transfer syntax ${transferSyntaxUID} is not supported for requested resource` + ); + }); + } + } + fieldValue += `; transfer-syntax=${transferSyntaxUID}`; + } + + fieldValueParts.push(fieldValue); + }); + + return fieldValueParts.join(', '); +} diff --git a/src/shared/mediaTypesUtils/mediaTypeJLL.js b/src/shared/mediaTypesUtils/mediaTypeJLL.js new file mode 100644 index 00000000..e2b5ecaa --- /dev/null +++ b/src/shared/mediaTypesUtils/mediaTypeJLL.js @@ -0,0 +1,4 @@ +const jllMediaType = 'image/jll'; +const jllTransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.70'; + +export { jllMediaType, jllTransferSyntaxUIDlossless }; diff --git a/src/shared/mediaTypesUtils/mediaTypeJLS.js b/src/shared/mediaTypesUtils/mediaTypeJLS.js new file mode 100644 index 00000000..53e0d697 --- /dev/null +++ b/src/shared/mediaTypesUtils/mediaTypeJLS.js @@ -0,0 +1,5 @@ +const jlsMediaType = 'image/jls'; +const jlsTransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.80'; +const jlsTransferSyntaxUID = '1.2.840.10008.1.2.4.81'; + +export { jlsMediaType, jlsTransferSyntaxUIDlossless, jlsTransferSyntaxUID }; diff --git a/src/shared/mediaTypesUtils/mediaTypeJP2.js b/src/shared/mediaTypesUtils/mediaTypeJP2.js new file mode 100644 index 00000000..9bc56417 --- /dev/null +++ b/src/shared/mediaTypesUtils/mediaTypeJP2.js @@ -0,0 +1,5 @@ +const jp2MediaType = 'image/jp2'; +const jp2TransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.90'; +const jp2TransferSyntaxUID = '1.2.840.10008.1.2.4.91'; + +export { jp2MediaType, jp2TransferSyntaxUIDlossless, jp2TransferSyntaxUID }; diff --git a/src/shared/mediaTypesUtils/mediaTypeJPEG.js b/src/shared/mediaTypesUtils/mediaTypeJPEG.js new file mode 100644 index 00000000..616a12ec --- /dev/null +++ b/src/shared/mediaTypesUtils/mediaTypeJPEG.js @@ -0,0 +1,11 @@ +const jpegMediaType = 'image/jpeg'; +const jpegTransferSyntaxUIDlossy1 = '1.2.840.10008.1.2.4.50'; +const jpegTransferSyntaxUIDlossy2 = '1.2.840.10008.1.2.4.51'; +const jpegTransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.57'; + +export { + jpegMediaType, + jpegTransferSyntaxUIDlossy1, + jpegTransferSyntaxUIDlossy2, + jpegTransferSyntaxUIDlossless, +}; diff --git a/src/shared/mediaTypesUtils/mediaTypeOctetStream.js b/src/shared/mediaTypesUtils/mediaTypeOctetStream.js new file mode 100644 index 00000000..8f7190ef --- /dev/null +++ b/src/shared/mediaTypesUtils/mediaTypeOctetStream.js @@ -0,0 +1,4 @@ +const octetStreamMediaType = 'application/octet-stream'; +const octetStreamTransferSyntaxUID = '*'; + +export { octetStreamMediaType, octetStreamTransferSyntaxUID }; diff --git a/src/shared/mediaTypesUtils/mediaTypeXDicomRLE.js b/src/shared/mediaTypesUtils/mediaTypeXDicomRLE.js new file mode 100644 index 00000000..265e213c --- /dev/null +++ b/src/shared/mediaTypesUtils/mediaTypeXDicomRLE.js @@ -0,0 +1,4 @@ +const xdicomrleMediaType = 'image/x-dicom-rle'; +const xdicomrleTransferSyntaxUID = '1.2.840.10008.1.2.5'; + +export { xdicomrleMediaType, xdicomrleTransferSyntaxUID }; diff --git a/src/shared/mediaTypesUtils/mediaTypes.js b/src/shared/mediaTypesUtils/mediaTypes.js new file mode 100644 index 00000000..b585a11b --- /dev/null +++ b/src/shared/mediaTypesUtils/mediaTypes.js @@ -0,0 +1,73 @@ +import { + xdicomrleMediaType, + xdicomrleTransferSyntaxUID, +} from './mediaTypeXDicomRLE.js'; +import { + jpegMediaType, + jpegTransferSyntaxUIDlossy1, + jpegTransferSyntaxUIDlossy2, + jpegTransferSyntaxUIDlossless, +} from './mediaTypeJPEG.js'; +import { jllMediaType, jllTransferSyntaxUIDlossless } from './mediaTypeJLL.js'; +import { + jlsMediaType, + jlsTransferSyntaxUID, + jlsTransferSyntaxUIDlossless, +} from './mediaTypeJLS.js'; +import { + jp2MediaType, + jp2TransferSyntaxUID, + jp2TransferSyntaxUIDlossless, +} from './mediaTypeJP2.js'; +import { + octetStreamMediaType, + octetStreamTransferSyntaxUID, +} from './mediaTypeOctetStream.js'; + +// NOTE: the position of the elements in the array indicates the mediaType +// priority when fetching an image. An element at the beginning of the array +// has the highest priority. +const mediaTypes = [ + { + mediaType: xdicomrleMediaType, + transferSyntaxUID: xdicomrleTransferSyntaxUID, + }, + { + mediaType: jpegMediaType, + transferSyntaxUID: jpegTransferSyntaxUIDlossy1, + }, + { + mediaType: jpegMediaType, + transferSyntaxUID: jpegTransferSyntaxUIDlossy2, + }, + { + mediaType: jpegMediaType, + transferSyntaxUID: jpegTransferSyntaxUIDlossless, + }, + { + mediaType: jllMediaType, + transferSyntaxUID: jllTransferSyntaxUIDlossless, + }, + { + mediaType: jlsMediaType, + transferSyntaxUID: jlsTransferSyntaxUIDlossless, + }, + { + mediaType: jlsMediaType, + transferSyntaxUID: jlsTransferSyntaxUID, + }, + { + mediaType: jp2MediaType, + transferSyntaxUID: jp2TransferSyntaxUIDlossless, + }, + { + mediaType: jp2MediaType, + transferSyntaxUID: jp2TransferSyntaxUID, + }, + { + mediaType: octetStreamMediaType, + transferSyntaxUID: octetStreamTransferSyntaxUID, + }, +]; + +export { mediaTypes }; From 213db6ad978de32cadc5cc7a2b9d17482c313fee Mon Sep 17 00:00:00 2001 From: Davide Punzo Date: Thu, 29 Sep 2022 10:38:44 +0200 Subject: [PATCH 3/3] Apply Alireza review --- src/imageLoader/wadors/getPixelData.js | 24 +---- src/imageLoader/wadors/loadImage.js | 7 +- .../buildMultipartAcceptHeaderFieldValue.js | 102 ------------------ .../multipartAcceptHeaderFieldValue.js | 50 +++++++++ 4 files changed, 56 insertions(+), 127 deletions(-) delete mode 100644 src/shared/mediaTypesUtils/buildMultipartAcceptHeaderFieldValue.js create mode 100644 src/shared/mediaTypesUtils/multipartAcceptHeaderFieldValue.js diff --git a/src/imageLoader/wadors/getPixelData.js b/src/imageLoader/wadors/getPixelData.js index e0f725bf..7e1bc769 100644 --- a/src/imageLoader/wadors/getPixelData.js +++ b/src/imageLoader/wadors/getPixelData.js @@ -1,7 +1,6 @@ import { xhrRequest } from '../internal/index.js'; import findIndexOfString from './findIndexOfString.js'; -import buildMultipartAcceptHeaderFieldValue from '../../shared/mediaTypesUtils/buildMultipartAcceptHeaderFieldValue.js'; - +import { multipartAcceptHeaderFieldValue } from '../../shared/mediaTypesUtils/multipartAcceptHeaderFieldValue.js'; function findBoundary(header) { for (let i = 0; i < header.length; i++) { if (header[i].substr(0, 2) === '--') { @@ -30,26 +29,9 @@ function uint8ArrayToString(data, offset, length) { return str; } -function getPixelData(uri, imageId, mediaTypes) { - const supportedMediaTypes = { - '1.2.840.10008.1.2.5': ['image/x-dicom-rle'], - '1.2.840.10008.1.2.4.50': ['image/jpeg'], - '1.2.840.10008.1.2.4.51': ['image/jpeg'], - '1.2.840.10008.1.2.4.57': ['image/jpeg'], - '1.2.840.10008.1.2.4.70': ['image/jpeg', 'image/jll'], - '1.2.840.10008.1.2.4.80': ['image/x-jls', 'image/jls'], - '1.2.840.10008.1.2.4.81': ['image/x-jls', 'image/jls'], - '1.2.840.10008.1.2.4.90': ['image/jp2'], - '1.2.840.10008.1.2.4.91': ['image/jp2'], - '1.2.840.10008.1.2.4.92': ['image/jpx'], - '1.2.840.10008.1.2.4.93': ['image/jpx'], - }; - +function getPixelData(uri, imageId) { const headers = { - Accept: buildMultipartAcceptHeaderFieldValue( - mediaTypes, - supportedMediaTypes - ), + Accept: multipartAcceptHeaderFieldValue, }; return new Promise((resolve, reject) => { diff --git a/src/imageLoader/wadors/loadImage.js b/src/imageLoader/wadors/loadImage.js index 773202de..44615448 100644 --- a/src/imageLoader/wadors/loadImage.js +++ b/src/imageLoader/wadors/loadImage.js @@ -1,7 +1,6 @@ import external from '../../externalModules.js'; import getPixelData from './getPixelData.js'; import createImage from '../createImage.js'; -import { mediaTypes } from '../../shared/mediaTypesUtils/mediaTypes.js'; /** * Helper method to extract the transfer-syntax from the response of the server. * @param {string} contentType The value of the content-type header as returned by the WADO-RS server. @@ -72,9 +71,9 @@ function loadImage(imageId, options = {}) { const promise = new Promise((resolve, reject) => { // TODO: load bulk data items that we might need - function sendXHR(imageURI, imageId, mediaTypes) { + function sendXHR(imageURI, imageId) { // get the pixel data from the server - return getPixelData(imageURI, imageId, mediaTypes) + return getPixelData(imageURI, imageId) .then((result) => { const transferSyntax = getTransferSyntaxForContentType( result.contentType @@ -108,7 +107,7 @@ function loadImage(imageId, options = {}) { const uri = imageId.substring(7); imageRetrievalPool.addRequest( - sendXHR.bind(this, uri, imageId, mediaTypes), + sendXHR.bind(this, uri, imageId), requestType, additionalDetails, priority, diff --git a/src/shared/mediaTypesUtils/buildMultipartAcceptHeaderFieldValue.js b/src/shared/mediaTypesUtils/buildMultipartAcceptHeaderFieldValue.js deleted file mode 100644 index 1fe37bb5..00000000 --- a/src/shared/mediaTypesUtils/buildMultipartAcceptHeaderFieldValue.js +++ /dev/null @@ -1,102 +0,0 @@ -import assertMediaTypeIsValid from './assertMediaTypeIsValid.js'; - -/** - * Builds an accept header field value for HTTP GET multipart request messages. - * - * Takes in input a media types array of type [{mediaType, transferSyntaxUID}, ... ], - * cross-compares with a map defining the supported media types per transferSyntaxUID - * and finally composes a string for the accept header field value as in example below: - * - * "multipart/related; type="image/x-dicom-rle"; transfer-syntax=1.2.840.10008.1.2.5, - * multipart/related; type="image/jpeg"; transfer-syntax=1.2.840.10008.1.2.4.50, - * multipart/related; type="application/octet-stream"; transfer-syntax=*" - * - * NOTE: the xhr request will try to fetch with all the transfer-syntax syntaxes - * specified in the accept header field value in descending order. - * The first element ("image/x-dicom-rle" in this example) has the highest priority. - * - * @param {Array} mediaTypes Acceptable media types - * @param {Object} supportedMediaTypes Supported media types - * - * @returns {string} accept header field value - */ - -export default function buildMultipartAcceptHeaderFieldValue( - mediaTypes, - supportedMediaTypes -) { - if (!Array.isArray(mediaTypes)) { - throw new Error('Acceptable media types must be provided as an Array'); - } - - if (typeof supportedMediaTypes !== 'object') { - throw new Error( - 'Supported media types must be provided as an Array or an Object' - ); - } - - const supportedMediaTypesArray = Object.values(supportedMediaTypes).flat(1); - - supportedMediaTypesArray.forEach((supportedMediaType) => { - assertMediaTypeIsValid(supportedMediaType); - }); - - const fieldValueParts = []; - - mediaTypes.forEach((item) => { - const { transferSyntaxUID, mediaType } = item; - - assertMediaTypeIsValid(mediaType); - - let fieldValue = `multipart/related; type="${mediaType}"`; - - // supportedMediaTypesArray is a lookup table that maps Transfer Syntax UID - // to one or more Media Types - if (!supportedMediaTypesArray.includes(mediaType)) { - if ( - (!mediaType.endsWith('/*') || !mediaType.endsWith('/')) && - mediaType !== 'application/octet-stream' - ) { - throw new Error( - `Media type ${mediaType} is not supported for requested resource` - ); - } - } - if (transferSyntaxUID) { - if (transferSyntaxUID !== '*') { - if (!Object.keys(supportedMediaTypes).includes(transferSyntaxUID)) { - throw new Error( - `Transfer syntax ${transferSyntaxUID} is not supported for requested resource` - ); - } - const expectedMediaTypes = supportedMediaTypes[transferSyntaxUID]; - - if (!expectedMediaTypes.includes(mediaType)) { - const actualType = mediaType.split('/')[0]; - - expectedMediaTypes.map((expectedMediaType) => { - const expectedType = expectedMediaType.split('/')[0]; - - const haveSameType = actualType === expectedType; - - if ( - haveSameType && - (mediaType.endsWith('/*') || mediaType.endsWith('/')) - ) { - return null; - } - - throw new Error( - `Transfer syntax ${transferSyntaxUID} is not supported for requested resource` - ); - }); - } - } - fieldValue += `; transfer-syntax=${transferSyntaxUID}`; - } - - fieldValueParts.push(fieldValue); - }); - - return fieldValueParts.join(', '); -} diff --git a/src/shared/mediaTypesUtils/multipartAcceptHeaderFieldValue.js b/src/shared/mediaTypesUtils/multipartAcceptHeaderFieldValue.js new file mode 100644 index 00000000..f236bd2d --- /dev/null +++ b/src/shared/mediaTypesUtils/multipartAcceptHeaderFieldValue.js @@ -0,0 +1,50 @@ +import assertMediaTypeIsValid from './assertMediaTypeIsValid.js'; +import { mediaTypes } from './mediaTypes.js'; + +/** + * Builds an accept header field value for HTTP GET multipart request messages. + * + * Takes in input a media types array of type [{mediaType, transferSyntaxUID}, ... ] + * and finally composes a string for the accept header field value as in example below: + * + * "multipart/related; type="image/x-dicom-rle"; transfer-syntax=1.2.840.10008.1.2.5, + * multipart/related; type="image/jpeg"; transfer-syntax=1.2.840.10008.1.2.4.50, + * multipart/related; type="application/octet-stream"; transfer-syntax=*" + * + * NOTE: the xhr request will try to fetch with all the transfer-syntax syntaxes + * specified in the accept header field value in descending order. + * The first element ("image/x-dicom-rle" in this example) has the highest priority. + * + * @param {Array} mediaTypes Acceptable media types + * + * @returns {string} accept header field value + */ + +export default function buildMultipartAcceptHeaderFieldValue(mediaTypes) { + if (!Array.isArray(mediaTypes)) { + throw new Error('Acceptable media types must be provided as an Array'); + } + + const fieldValueParts = []; + + mediaTypes.forEach((item) => { + const { transferSyntaxUID, mediaType } = item; + + assertMediaTypeIsValid(mediaType); + + let fieldValue = `multipart/related; type="${mediaType}"`; + + if (transferSyntaxUID) { + fieldValue += `; transfer-syntax=${transferSyntaxUID}`; + } + + fieldValueParts.push(fieldValue); + }); + + return fieldValueParts.join(', '); +} + +const multipartAcceptHeaderFieldValue = + buildMultipartAcceptHeaderFieldValue(mediaTypes); + +export { multipartAcceptHeaderFieldValue };