Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Retrieve PDF bulkdata, requesting both octet and pdf #86

Merged
merged 3 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
wayfarer3130 marked this conversation as resolved.
Show resolved Hide resolved


}

/**
* 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) {
wayfarer3130 marked this conversation as resolved.
Show resolved Hide resolved
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/') {
wayfarer3130 marked this conversation as resolved.
Show resolved Hide resolved
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