Skip to content

Commit

Permalink
fix: Retrieve PDF bulkdata, requesting both octet and pdf (#86)
Browse files Browse the repository at this point in the history
* fix: Retrieve PDF bulkdata, requesting both octet and pdf

* PR review comments

* PR comments
  • Loading branch information
wayfarer3130 authored Jan 5, 2024
1 parent 6d6ae85 commit d821e27
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 67 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module.exports = {
root: true,
extends: ['airbnb-base', 'prettier'],
rules: {
'import/extensions': 1, // Better for native ES Module usage
'import/extensions': "always", // Better for native ES Module usage
'no-console': 0, // We can remove this later
'no-underscore-dangle': 0,
'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
Expand Down
171 changes: 105 additions & 66 deletions src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ const MEDIATYPES = {
PNG: 'image/png',
};

/**
* debugLog is a function that can be called with console.log arguments, and will
* be conditionally displayed, only when debug logging is enabled.
*/
let debugLog = () => {};

/**
* @typedef { import("../types/types").InstanceMetadata } InstanceMetadata
*/
Expand Down Expand Up @@ -66,6 +72,7 @@ class DICOMwebClient {
* @param {Object=} options.headers - HTTP headers
* @param {Array.<RequestHook>=} options.requestHooks - Request hooks.
* @param {Object=} options.verbose - print to console request warnings and errors, default true
* @param {Object=} options.debug - print to the console debug level information/status updates.
* @param {boolean|String} options.singlepart - retrieve singlepart for the named types.
* The available types are: bulkdata, video, image. true means all.
*/
Expand All @@ -86,28 +93,28 @@ class DICOMwebClient {
}

if ('qidoURLPrefix' in options) {
console.log(`use URL prefix for QIDO-RS: ${options.qidoURLPrefix}`);
debugLog(`use URL prefix for QIDO-RS: ${options.qidoURLPrefix}`);
this.qidoURL = `${this.baseURL}/${options.qidoURLPrefix}`;
} else {
this.qidoURL = this.baseURL;
}

if ('wadoURLPrefix' in options) {
console.log(`use URL prefix for WADO-RS: ${options.wadoURLPrefix}`);
debugLog(`use URL prefix for WADO-RS: ${options.wadoURLPrefix}`);
this.wadoURL = `${this.baseURL}/${options.wadoURLPrefix}`;
} else {
this.wadoURL = this.baseURL;
}

if ('stowURLPrefix' in options) {
console.log(`use URL prefix for STOW-RS: ${options.stowURLPrefix}`);
debugLog(`use URL prefix for STOW-RS: ${options.stowURLPrefix}`);
this.stowURL = `${this.baseURL}/${options.stowURLPrefix}`;
} else {
this.stowURL = this.baseURL;
}

if (options.singlepart) {
console.log('use singlepart', options.singlepart);
debugLog('use singlepart', options.singlepart);
this.singlepart = options.singlepart === true ? 'bulkdata,video,image' : options.singlepart;
} else {
this.singlepart = '';
Expand All @@ -125,8 +132,33 @@ class DICOMwebClient {

// Verbose - print to console request warnings and errors, default true
this.verbose = options.verbose !== false;

this.setDebug(options.debug);


}

/**
* Allows setting the debug log information.
* Note this is different from verbose in that verbose is whether to include warning/error information, defaulting to true
*
* @param {boolean} debugLevel
* @param {function} debugLogFunction to call with the debug output arguments.
*/
setDebug(debugLevel = false, debugLogFunction = null) {
this.debugLevel = !!debugLevel;
debugLog = debugLogFunction || debugLevel ? console.log : () => {};
}

/**
* Gets debug flag
*
* @returns true if debug logging is enabled
*/
getDebug() {
return this.debugLevel;
}

/**
* Sets verbose flag.
*
Expand Down Expand Up @@ -194,12 +226,12 @@ class DICOMwebClient {

// Event triggered when upload starts
request.onloadstart = function onloadstart() {
// console.log('upload started: ', url)
debugLog('upload started: ', url)
};

// Event triggered when upload ends
request.onloadend = function onloadend() {
// console.log('upload finished')
debugLog('upload finished')
};

// Handle response message
Expand Down Expand Up @@ -699,7 +731,8 @@ class DICOMwebClient {

/**
* Performs an HTTP GET request that accepts a multipart message
* with a application/octet-stream media type.
* with a application/octet-stream, OR any of the equivalencies for that (eg
* application/pdf etc)
*
* @param {String} url - Unique resource locator
* @param {Object[]} mediaTypes - Acceptable media types and optionally the UIDs of the
Expand All @@ -721,7 +754,7 @@ class DICOMwebClient {
const headers = {};
const defaultMediaType = 'application/octet-stream';
const supportedMediaTypes = {
'1.2.840.10008.1.2.1': [defaultMediaType],
'1.2.840.10008.1.2.1': [...Object.values(MEDIATYPES)],
};

let acceptableMediaTypes = mediaTypes;
Expand Down Expand Up @@ -826,13 +859,16 @@ class DICOMwebClient {
}

/**
* Builds an accept header field value for HTTP GET multipart request
messages.
*
* @param {Object[]} mediaTypes - Acceptable media types
* @param {Object[]} supportedMediaTypes - Supported media types
* @private
*/
* Builds an accept header field value for HTTP GET multipart request
* messages. Will throw an exception if no media types are found which are acceptable,
* but will only log a verbose level message when types are specified which are
* not acceptable. This allows requesting several types with having to know
* whether they are all acceptable or not.
*
* @param {Object[]} mediaTypes - Acceptable media types
* @param {Object[]} supportedMediaTypes - Supported media types
* @private
*/
static _buildMultipartAcceptHeaderFieldValue(
mediaTypes,
supportedMediaTypes,
Expand Down Expand Up @@ -863,9 +899,10 @@ class DICOMwebClient {
.includes(mediaType)
) {
if (!mediaType.endsWith('/*') || !mediaType.endsWith('/')) {
throw new Error(
debugLog(
`Media type ${mediaType} is not supported for requested resource`,
);
return;
}
}

Expand Down Expand Up @@ -907,14 +944,21 @@ class DICOMwebClient {
Array.isArray(supportedMediaTypes) &&
!supportedMediaTypes.includes(mediaType)
) {
throw new Error(
`Media type ${mediaType} is not supported for requested resource`,
);
if( this.verbose ) {
console.warn(
`Media type ${mediaType} is not supported for requested resource`,
);
}
return;
}

fieldValueParts.push(fieldValue);
});

if( !fieldValueParts.length ) {
throw new Error(`No acceptable media types found among ${JSON.stringify(mediaTypes)}`);
}

return fieldValueParts.join(', ');
}

Expand Down Expand Up @@ -961,15 +1005,15 @@ class DICOMwebClient {
}

/**
* Gets common type of acceptable media types and asserts that only
* Gets common base type of acceptable media types and asserts that only
one type is specified. For example, ``("image/jpeg", "image/jp2")``
will pass, but ``("image/jpeg", "video/mpeg2")`` will raise an
exception.
*
* @param {Object[]} mediaTypes - Acceptable media types and optionally the UIDs of the
corresponding transfer syntaxes
* @private
* @returns {String[]} Common media type
* @returns {String[]} Common media type, eg `image/` for the above example.
*/
static _getCommonMediaType(mediaTypes) {
if (!mediaTypes || !mediaTypes.length) {
Expand All @@ -994,7 +1038,7 @@ class DICOMwebClient {
* @return {Object[]} Study representations (http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_6.7.html#table_6.7.1-2)
*/
searchForStudies(options = {}) {
console.log('search for studies');
debugLog('search for studies');
let withCredentials = false;
let url = `${this.qidoURL}/studies`;
if ('queryParams' in options) {
Expand Down Expand Up @@ -1022,7 +1066,7 @@ class DICOMwebClient {
'Study Instance UID is required for retrieval of study metadata',
);
}
console.log(`retrieve metadata of study ${options.studyInstanceUID}`);
debugLog(`retrieve metadata of study ${options.studyInstanceUID}`);
const url = `${this.wadoURL}/studies/${options.studyInstanceUID}/metadata`;
let withCredentials = false;
if ('withCredentials' in options) {
Expand All @@ -1044,7 +1088,7 @@ class DICOMwebClient {
searchForSeries(options = {}) {
let url = this.qidoURL;
if ('studyInstanceUID' in options) {
console.log(`search series of study ${options.studyInstanceUID}`);
debugLog(`search series of study ${options.studyInstanceUID}`);
url += `/studies/${options.studyInstanceUID}`;
}
url += '/series';
Expand Down Expand Up @@ -1081,7 +1125,7 @@ class DICOMwebClient {
);
}

console.log(`retrieve metadata of series ${options.seriesInstanceUID}`);
debugLog(`retrieve metadata of series ${options.seriesInstanceUID}`);
const url = `${this.wadoURL}/studies/${options.studyInstanceUID}/series/${options.seriesInstanceUID}/metadata`;
let withCredentials = false;
if ('withCredentials' in options) {
Expand All @@ -1107,17 +1151,17 @@ class DICOMwebClient {
if ('studyInstanceUID' in options) {
url += `/studies/${options.studyInstanceUID}`;
if ('seriesInstanceUID' in options) {
console.log(
debugLog(
`search for instances of series ${options.seriesInstanceUID}`,
);
url += `/series/${options.seriesInstanceUID}`;
} else {
console.log(
debugLog(
`search for instances of study ${options.studyInstanceUID}`,
);
}
} else {
console.log('search for instances');
debugLog('search for instances');
}
url += '/instances';
if ('queryParams' in options) {
Expand Down Expand Up @@ -1191,7 +1235,7 @@ class DICOMwebClient {
'SOP Instance UID is required for retrieval of instance metadata',
);
}
console.log(`retrieve metadata of instance ${options.sopInstanceUID}`);
debugLog(`retrieve metadata of instance ${options.sopInstanceUID}`);
const url = `${this.wadoURL}/studies/${options.studyInstanceUID}/series/${options.seriesInstanceUID}/instances/${options.sopInstanceUID}/metadata`;
let withCredentials = false;
if ('withCredentials' in options) {
Expand Down Expand Up @@ -1232,7 +1276,7 @@ class DICOMwebClient {
'frame numbers are required for retrieval of instance frames',
);
}
console.log(
debugLog(
`retrieve frames ${options.frameNumbers.toString()} of instance ${
options.sopInstanceUID
}`,
Expand Down Expand Up @@ -1287,6 +1331,8 @@ class DICOMwebClient {
'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'],
'1.2.840.10008.1.2.4.201': ['image/jhc'],
'1.2.840.10008.1.2.4.202': ['image/jhc'],
};

const headers = {
Expand Down Expand Up @@ -1554,7 +1600,7 @@ class DICOMwebClient {
);
}

console.debug(
debugLog(
`retrieve rendered frames ${options.frameNumbers.toString()} of instance ${
options.sopInstanceUID
}`,
Expand Down Expand Up @@ -1881,43 +1927,34 @@ class DICOMwebClient {
return this._httpGet(url, options.headers, 'arraybuffer', null, withCredentials);
}

if (!mediaTypes) {
return this._httpGetMultipartApplicationOctetStream(
url,
mediaTypes,
byteRange,
false,
false,
withCredentials,
);
}

const commonMediaType = DICOMwebClient._getCommonMediaType(mediaTypes);

if (commonMediaType === MEDIATYPES.OCTET_STREAM) {
return this._httpGetMultipartApplicationOctetStream(
url,
mediaTypes,
byteRange,
false,
progressCallback,
withCredentials,
);
}
if (commonMediaType.startsWith('image')) {
return this._httpGetMultipartImage(
url,
mediaTypes,
byteRange,
false,
false,
progressCallback,
withCredentials,
);
if (mediaTypes) {
try {
const commonMediaType = DICOMwebClient._getCommonMediaType(mediaTypes);

if (commonMediaType==='image/') {
return this._httpGetMultipartImage(
url,
mediaTypes,
byteRange,
false,
false,
progressCallback,
withCredentials,
);
}
} catch(e) {
// No-op - this happens sometimes if trying to fetch the specific desired type but want to fallback to octet-stream
}
}

throw new Error(
`Media type ${commonMediaType} is not supported for retrieval of bulk data.`,
// Just use the media types provided
return this._httpGetMultipartApplicationOctetStream(
url,
mediaTypes,
byteRange,
false,
progressCallback,
withCredentials,
);
}

Expand Down Expand Up @@ -1954,6 +1991,8 @@ class DICOMwebClient {
options.request,
);
}


}


Expand Down

0 comments on commit d821e27

Please sign in to comment.