fix: Microscopy bulkdata and image retrieve (#3894)

This commit is contained in:
Bill Wallace 2024-04-10 10:16:09 -04:00 committed by GitHub
parent 7014e6ca4a
commit 7fac49b449
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 187 additions and 12 deletions

View File

@ -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<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);
}
}
/**

View File

@ -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;

View File

@ -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;
}

View File

@ -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,
};

View File

@ -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') {

View File

@ -103,7 +103,7 @@ window.config = {
supportsFuzzyMatching: false,
supportsWildcard: true,
staticWado: true,
singlepart: 'bulkdata,video,pdf',
singlepart: 'video,pdf',
bulkDataURI: {
enabled: true,
relativeResolution: 'studies',