diff --git a/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.ts b/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.ts index ed2815c60..519e8109c 100644 --- a/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.ts +++ b/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.ts @@ -1,4 +1,25 @@ import { api } from 'dicomweb-client'; +import fixMultipart from './fixMultipart'; + +const { DICOMwebClient } = api; + +const anyDicomwebClient = DICOMwebClient as any; + +// Ugly over-ride, but the internals aren't otherwise accessible. +if (!anyDicomwebClient._orig_buildMultipartAcceptHeaderFieldValue) { + anyDicomwebClient._orig_buildMultipartAcceptHeaderFieldValue = + anyDicomwebClient._buildMultipartAcceptHeaderFieldValue; + anyDicomwebClient._buildMultipartAcceptHeaderFieldValue = function (mediaTypes, acceptableTypes) { + if (mediaTypes.length === 1 && mediaTypes[0].mediaType.endsWith('/*')) { + return '*/*'; + } else { + return anyDicomwebClient._orig_buildMultipartAcceptHeaderFieldValue( + mediaTypes, + acceptableTypes + ); + } + }; +} /** * An implementation of the static wado client, that fetches data from @@ -25,9 +46,50 @@ export default class StaticWadoClient extends api.DICOMwebClient { modality: '00080060', }; - constructor(qidoConfig) { - super(qidoConfig); - this.staticWado = qidoConfig.staticWado; + protected config; + protected staticWado; + + constructor(config) { + super(config); + this.staticWado = config.staticWado; + this.config = config; + } + + /** + * Handle improperly specified multipart/related return type. + * Note if the response is SUPPOSED to be multipart encoded already, then this + * will double-decode it. + * + * @param options + * @returns De-multiparted response data. + * + */ + public retrieveBulkData(options): Promise { + const shouldFixMultipart = this.config.fixBulkdataMultipart !== false; + const useOptions = { + ...options, + }; + if (this.staticWado) { + useOptions.mediaTypes = [{ mediaType: 'application/*' }]; + } + return super + .retrieveBulkData(useOptions) + .then(result => (shouldFixMultipart ? fixMultipart(result) : result)); + } + + /** + * Retrieves instance frames using the image/* media type when configured + * to do so (static wado back end). + */ + public retrieveInstanceFrames(options) { + if (this.staticWado) { + return super.retrieveInstanceFrames({ + ...options, + mediaTypes: [{ mediaType: 'image/*' }], + }); + } else { + return super.retrieveInstanceFrames(options); + } } /** diff --git a/extensions/default/src/DicomWebDataSource/utils/findIndexOfString.ts b/extensions/default/src/DicomWebDataSource/utils/findIndexOfString.ts new file mode 100644 index 000000000..f5bbd3583 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/utils/findIndexOfString.ts @@ -0,0 +1,47 @@ +function checkToken(token, data, dataOffset): boolean { + if (dataOffset + token.length > data.length) { + return false; + } + + let endIndex = dataOffset; + + for (let i = 0; i < token.length; i++) { + if (token[i] !== data[endIndex++]) { + return false; + } + } + + return true; +} + +function stringToUint8Array(str: string): Uint8Array { + const uint = new Uint8Array(str.length); + + for (let i = 0, j = str.length; i < j; i++) { + uint[i] = str.charCodeAt(i); + } + + return uint; +} + +function findIndexOfString( + data: Uint8Array, + str: string, + offset?: number +): number { + offset = offset || 0; + + const token = stringToUint8Array(str); + + for (let i = offset; i < data.length; i++) { + if (token[0] === data[i]) { + // console.log('match @', i); + if (checkToken(token, data, i)) { + return i; + } + } + } + + return -1; +} +export default findIndexOfString; diff --git a/extensions/default/src/DicomWebDataSource/utils/fixMultipart.ts b/extensions/default/src/DicomWebDataSource/utils/fixMultipart.ts new file mode 100644 index 000000000..77e3071a2 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/utils/fixMultipart.ts @@ -0,0 +1,70 @@ +import findIndexOfString from './findIndexOfString'; + +/** + * Fix multipart data coming back from the retrieve bulkdata request, but + * incorrectly tagged as application/octet-stream. Some servers don't handle + * the response type correctly, and this method is relatively robust about + * detecting multipart data correctly. It will only extract one value. + */ +export default function fixMultipart(arrayData) { + const data = new Uint8Array(arrayData[0]); + // Don't know the exact minimum length, but it is at least 25 to encode multipart + if (data.length < 25) { + return arrayData; + } + const dashIndex = findIndexOfString(data, '--'); + if (dashIndex > 6) { + return arrayData; + } + const tokenIndex = findIndexOfString(data, '\r\n\r\n', dashIndex); + if (tokenIndex > 512) { + // Allow for 512 characters in the header - there is no apriori limit, but + // this seems ok for now as we only expect it to have content type in it. + return arrayData; + } + const header = uint8ArrayToString(data, 0, tokenIndex); + // Now find the boundary marker + const responseHeaders = header.split('\r\n'); + const boundary = findBoundary(responseHeaders); + + if (!boundary) { + return arrayData; + } + // Start of actual data is 4 characters after the token + const offset = tokenIndex + 4; + + const endIndex = findIndexOfString(data, boundary, offset); + if (endIndex === -1) { + return arrayData; + } + + return [data.slice(offset, endIndex - 2).buffer]; +} + +export function findBoundary(header: string[]): string { + for (let i = 0; i < header.length; i++) { + if (header[i].substr(0, 2) === '--') { + return header[i]; + } + } +} + +export function findContentType(header: string[]): string { + for (let i = 0; i < header.length; i++) { + if (header[i].substr(0, 13) === 'Content-Type:') { + return header[i].substr(13).trim(); + } + } +} + +export function uint8ArrayToString(data, offset, length) { + offset = offset || 0; + length = length || data.length - offset; + let str = ''; + + for (let i = offset; i < offset + length; i++) { + str += String.fromCharCode(data[i]); + } + + return str; +} diff --git a/extensions/default/src/index.ts b/extensions/default/src/index.ts index 49ec15d24..3d4c9ac48 100644 --- a/extensions/default/src/index.ts +++ b/extensions/default/src/index.ts @@ -16,6 +16,7 @@ import { ContextMenuController, CustomizableContextMenuTypes } from './Customiza import * as dicomWebUtils from './DicomWebDataSource/utils'; import { createReportDialogPrompt } from './Panels'; import createReportAsync from './Actions/createReportAsync'; +import StaticWadoClient from './DicomWebDataSource/utils/StaticWadoClient'; const defaultExtension: Types.Extensions.Extension = { /** @@ -54,4 +55,5 @@ export { dicomWebUtils, createReportDialogPrompt, createReportAsync, + StaticWadoClient, }; diff --git a/extensions/dicom-microscopy/src/utils/dicomWebClient.ts b/extensions/dicom-microscopy/src/utils/dicomWebClient.ts index 8d6d78462..dc906b120 100644 --- a/extensions/dicom-microscopy/src/utils/dicomWebClient.ts +++ b/extensions/dicom-microscopy/src/utils/dicomWebClient.ts @@ -1,11 +1,5 @@ -import { api } from 'dicomweb-client'; import { errorHandler, DicomMetadataStore } from '@ohif/core'; - -const { DICOMwebClient } = api; - -DICOMwebClient._buildMultipartAcceptHeaderFieldValue = () => { - return '*/*'; -}; +import { StaticWadoClient } from '@ohif/extension-default'; /** * create a DICOMwebClient object to be used by Dicom Microscopy Viewer @@ -31,7 +25,7 @@ export default function getDicomWebClient({ extensionManager, servicesManager }) errorInterceptor: errorHandler.getHTTPErrorHandler(), }; - const client = new api.DICOMwebClient(wadoConfig); + const client = new StaticWadoClient(wadoConfig); client.wadoURL = wadoConfig.url; if (extensionManager.activeDataSource === 'dicomlocal') { diff --git a/platform/app/public/config/e2e.js b/platform/app/public/config/e2e.js index 3a2fdb2ea..90827b906 100644 --- a/platform/app/public/config/e2e.js +++ b/platform/app/public/config/e2e.js @@ -103,7 +103,7 @@ window.config = { supportsFuzzyMatching: false, supportsWildcard: true, staticWado: true, - singlepart: 'bulkdata,video,pdf', + singlepart: 'video,pdf', bulkDataURI: { enabled: true, relativeResolution: 'studies',