From 55628dd0ed4ef2b8d77cd6cb3394f9ba264ad5e6 Mon Sep 17 00:00:00 2001 From: Davide Punzo Date: Thu, 29 Sep 2022 00:18:02 +0200 Subject: [PATCH] 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 };