Skip to content

Commit

Permalink
fix: Microscopy bulkdata and image retrieve (#3894)
Browse files Browse the repository at this point in the history
  • Loading branch information
wayfarer3130 authored Apr 10, 2024
1 parent 7014e6c commit 7fac49b
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<any[]> {
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);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
70 changes: 70 additions & 0 deletions extensions/default/src/DicomWebDataSource/utils/fixMultipart.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions extensions/default/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
/**
Expand Down Expand Up @@ -54,4 +55,5 @@ export {
dicomWebUtils,
createReportDialogPrompt,
createReportAsync,
StaticWadoClient,
};
10 changes: 2 additions & 8 deletions extensions/dicom-microscopy/src/utils/dicomWebClient.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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') {
Expand Down
2 changes: 1 addition & 1 deletion platform/app/public/config/e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ window.config = {
supportsFuzzyMatching: false,
supportsWildcard: true,
staticWado: true,
singlepart: 'bulkdata,video,pdf',
singlepart: 'video,pdf',
bulkDataURI: {
enabled: true,
relativeResolution: 'studies',
Expand Down

0 comments on commit 7fac49b

Please sign in to comment.