UNPKG

dicom-microscopy-viewer-changed

Version:
1,588 lines (1,433 loc) 57.3 kB
import { multipartEncode, multipartDecode } from "./message.js"; function isObject(obj) { return typeof obj === "object" && obj !== null; } function isEmptyObject(obj) { return Object.keys(obj).length === 0 && obj.constructor === Object; } function areValidRequestHooks(requestHooks) { const isValid = Array.isArray(requestHooks) && requestHooks.every(requestHook => typeof requestHook === 'function' && requestHook.length === 2 ); if (!isValid) { console.warn( 'Request hooks should have the following signature: ' + 'function requestHook(request, metadata) { return request; }' ); } return isValid; } const getFirstResult = result => result[0]; const getFirstResultIfLengthGtOne = result => { if (result.length > 1) { return result; } return result[0] }; const MEDIATYPES = { DICOM: "application/dicom", DICOM_JSON: "application/dicom+json", OCTET_STREAM: "application/octet-stream", PDF: "application/pdf", JPEG: "image/jpeg", PNG: "image/png" }; /** * A callback with the request instance and metadata information * of the currently request being executed that should necessarily * return the given request optionally modified. * @typedef {function} RequestHook * @param {XMLHttpRequest} request - The original XMLHttpRequest instance. * @param {object} metadata - The metadata used by the request. */ /** * Class for interacting with DICOMweb RESTful services. */ class DICOMwebClient { /** * @constructor * @param {Object} options * @param {String} options.url - URL of the DICOMweb RESTful Service endpoint * @param {String} options.qidoURLPrefix - URL path prefix for QIDO-RS * @param {String} options.wadoURLPrefix - URL path prefix for WADO-RS * @param {String} options.stowURLPrefix - URL path prefix for STOW-RS * @param {String} options.username - Username * @param {String} options.password - Password * @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 */ constructor(options) { this.baseURL = options.url; if (!this.baseURL) { console.error("no DICOMweb base url provided - calls will fail"); } if ("username" in options) { this.username = options.username; if (!("password" in options)) { console.error( "no password provided to authenticate with DICOMweb service" ); } this.password = options.password; } if ("qidoURLPrefix" in options) { console.log(`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}`); 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}`); this.stowURL = `${this.baseURL}/${options.stowURLPrefix}`; } else { this.stowURL = this.baseURL; } if ("requestHooks" in options) { this.requestHooks = options.requestHooks; } // Headers to pass to requests. this.headers = options.headers || {}; // Optional error interceptor callback to handle any failed request. this.errorInterceptor = options.errorInterceptor || function() {}; // Verbose - print to console request warnings and errors, default true this.verbose = options.verbose === false ? false : true; } /** * Sets verbose flag. * * @param {Boolean} verbose */ setVerbose(verbose) { this.verbose = verbose } /** * Gets verbose flag. * * @return {Boolean} verbose */ getVerbose() { return this.verbose; } static _parseQueryParameters(params = {}) { let queryString = "?"; Object.keys(params).forEach((key, index) => { if (index !== 0) { queryString += "&"; } queryString += `${key}=${encodeURIComponent(params[key])}`; }); return queryString; } /** * Performs an HTTP request. * * @param {String} url * @param {String} method * @param {Object} headers * @param {Object} options * @param {Array.<RequestHook>} options.requestHooks - Request hooks. * @return {*} * @private */ _httpRequest(url, method, headers = {}, options = {}) { const { errorInterceptor, requestHooks } = this; return new Promise((resolve, reject) => { let request = new XMLHttpRequest(); request.open(method, url, true); if ("responseType" in options) { request.responseType = options.responseType; } if (typeof headers === "object") { Object.keys(headers).forEach(key => { request.setRequestHeader(key, headers[key]); }); } // now add custom headers from the user // (e.g. access tokens) const userHeaders = this.headers; Object.keys(userHeaders).forEach(key => { request.setRequestHeader(key, userHeaders[key]); }); // Event triggered when upload starts request.onloadstart = function onloadstart() { // console.log('upload started: ', url) }; // Event triggered when upload ends request.onloadend = function onloadend() { // console.log('upload finished') }; // Handle response message request.onreadystatechange = () => { if (request.readyState === 4) { if (request.status === 200) { resolve(request.response); } else if (request.status === 202) { if (this.verbose) { console.warn("some resources already existed: ", request); } resolve(request.response); } else if (request.status === 204) { if (this.verbose) { console.warn("empty response for request: ", request); } resolve([]); } else { const error = new Error("request failed"); error.request = request; error.response = request.response; error.status = request.status; if (this.verbose) { console.error("request failed: ", request); console.error(error); console.error(error.response); } errorInterceptor(error); reject(error); } } }; // Event triggered while download progresses if ("progressCallback" in options) { if (typeof options.progressCallback === "function") { request.onprogress = options.progressCallback; } } if (requestHooks && areValidRequestHooks(requestHooks)) { const headers = Object.assign({}, headers, this.headers); const metadata = { method, url, headers }; const pipeRequestHooks = functions => (args) => functions.reduce((args, fn) => fn(args, metadata), args); const pipedRequest = pipeRequestHooks(requestHooks); request = pipedRequest(request); } // Add withCredentials to request if needed if ("withCredentials" in options) { if (options.withCredentials) { request.withCredentials = true; } } if ("data" in options) { request.send(options.data); } else { request.send(); } }); } /** * Performs an HTTP GET request. * * @param {String} url * @param {Object} headers * @param {Object} responseType * @param {Function} progressCallback * @return {*} * @private */ _httpGet(url, headers, responseType, progressCallback, withCredentials) { return this._httpRequest(url, "get", headers, { responseType, progressCallback, withCredentials }); } /** * Performs an HTTP GET request that accepts a message with application/json * media type. * * @param {String} url * @param {Object} params * @param {Function} progressCallback * @return {*} * @private */ _httpGetApplicationJson(url, params = {}, progressCallback, withCredentials) { let urlWithQueryParams = url; if (typeof params === "object") { if (!isEmptyObject(params)) { urlWithQueryParams += DICOMwebClient._parseQueryParameters(params); } } const headers = { Accept: MEDIATYPES.DICOM_JSON }; const responseType = "json"; return this._httpGet( urlWithQueryParams, headers, responseType, progressCallback, withCredentials ); } /** * Performs an HTTP GET request that accepts a message with application/pdf * media type. * * @param {String} url * @param {Object} params * @param {Function} progressCallback * @return {*} * @private */ _httpGetApplicationPdf(url, params = {}, progressCallback, withCredentials) { let urlWithQueryParams = url; if (typeof params === "object") { if (!isEmptyObject(params)) { urlWithQueryParams += DICOMwebClient._parseQueryParameters(params); } } const headers = { Accept: MEDIATYPES.PDF }; const responseType = "json"; return this._httpGet( urlWithQueryParams, headers, responseType, progressCallback, withCredentials ); } /** * Performs an HTTP GET request that accepts a message with an image media type. * * @param {String} url * @param {Object[]} mediaTypes * @param {Object} params * @param {Function} progressCallback * @return {*} * @private */ _httpGetImage(url, mediaTypes, params = {}, progressCallback, withCredentials) { let urlWithQueryParams = url; if (typeof params === "object") { if (!isEmptyObject(params)) { urlWithQueryParams += DICOMwebClient._parseQueryParameters(params); } } const supportedMediaTypes = [ "image/", "image/*", "image/jpeg", "image/jp2", "image/gif", "image/png" ]; const acceptHeaderFieldValue = DICOMwebClient._buildAcceptHeaderFieldValue( mediaTypes, supportedMediaTypes ); const headers = { Accept: acceptHeaderFieldValue }; const responseType = "arraybuffer"; return this._httpGet( urlWithQueryParams, headers, responseType, progressCallback, withCredentials ); } /** * Performs an HTTP GET request that accepts a message with a text media type. * * @param {String} url * @param {Object[]} mediaTypes * @param {Object} params * @param {Function} progressCallback * @return {*} * @private */ _httpGetText(url, mediaTypes, params = {}, progressCallback, withCredentials) { let urlWithQueryParams = url; if (typeof params === "object") { if (!isEmptyObject(params)) { urlWithQueryParams += DICOMwebClient._parseQueryParameters(params); } } const supportedMediaTypes = [ "text/", "text/*", "text/html", "text/plain", "text/rtf", "text/xml" ]; const acceptHeaderFieldValue = DICOMwebClient._buildAcceptHeaderFieldValue( mediaTypes, supportedMediaTypes ); const headers = { Accept: acceptHeaderFieldValue }; const responseType = "arraybuffer"; return this._httpGet( urlWithQueryParams, headers, responseType, progressCallback, withCredentials ); } /** * Performs an HTTP GET request that accepts a message with a video media type. * * @param {String} url * @param {Object[]} mediaTypes * @param {Object} params * @param {Function} progressCallback * @return {*} * @private */ _httpGetVideo(url, mediaTypes, params = {}, progressCallback, withCredentials) { let urlWithQueryParams = url; if (typeof params === "object") { if (!isEmptyObject(params)) { urlWithQueryParams += DICOMwebClient._parseQueryParameters(params); } } const supportedMediaTypes = [ "video/", "video/*", "video/mpeg", "video/mp4", "video/H265" ]; const acceptHeaderFieldValue = DICOMwebClient._buildAcceptHeaderFieldValue( mediaTypes, supportedMediaTypes ); const headers = { Accept: acceptHeaderFieldValue }; const responseType = "arraybuffer"; return this._httpGet( urlWithQueryParams, headers, responseType, progressCallback, withCredentials ); } /** * Asserts that a given media type is valid. * * @params {String} mediaType media type */ static _assertMediaTypeIsValid(mediaType) { if (!mediaType) { throw new Error(`Not a valid media type: ${mediaType}`); } const sepIndex = mediaType.indexOf("/"); if (sepIndex === -1) { throw new Error(`Not a valid media type: ${mediaType}`); } const mediaTypeType = mediaType.slice(0, sepIndex); const types = ["application", "image", "text", "video"]; if (!types.includes(mediaTypeType)) { throw new Error(`Not a valid media type: ${mediaType}`); } if (mediaType.slice(sepIndex + 1).includes("/")) { throw new Error(`Not a valid media type: ${mediaType}`); } } /** * Performs an HTTP GET request that accepts a multipart message with an image media type. * * @param {String} url - Unique resource locator * @param {Object[]} mediaTypes - Acceptable media types and optionally the UIDs of the corresponding transfer syntaxes * @param {Array} byteRange - Start and end of byte range * @param {Object} params - Additional HTTP GET query parameters * @param {Boolean} rendered - Whether resource should be requested using rendered media types * @param {Function} progressCallback * @private * @returns {Promise<Array>} Content of HTTP message body parts */ _httpGetMultipartImage( url, mediaTypes, byteRange, params, rendered = false, progressCallback, withCredentials ) { const headers = {}; let supportedMediaTypes; if (rendered) { supportedMediaTypes = [ "image/jpeg", "image/gif", "image/png", "image/jp2" ]; } else { supportedMediaTypes = { "1.2.840.10008.1.2.5": ["image/x-dicom-rle"], "1.2.840.10008.1.2.4.50": ["image/jpeg"], "1.2.840.10008.1.2.4.51": ["image/jpeg"], "1.2.840.10008.1.2.4.57": ["image/jpeg"], "1.2.840.10008.1.2.4.70": ["image/jpeg"], "1.2.840.10008.1.2.4.80": ["image/x-jls", "image/jls"], "1.2.840.10008.1.2.4.81": ["image/x-jls", "image/jls"], "1.2.840.10008.1.2.4.90": ["image/jp2"], "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"] }; if (byteRange) { headers.Range = DICOMwebClient._buildRangeHeaderFieldValue(byteRange); } } headers.Accept = DICOMwebClient._buildMultipartAcceptHeaderFieldValue( mediaTypes, supportedMediaTypes ); return this._httpGet(url, headers, "arraybuffer", progressCallback, withCredentials).then( multipartDecode ); } /** * Performs an HTTP GET request that accepts a multipart message with a video media type. * * @param {String} url - Unique resource locator * @param {Object[]} mediaTypes - Acceptable media types and optionally the UIDs of the corresponding transfer syntaxes * @param {Array} byteRange - Start and end of byte range * @param {Object} params - Additional HTTP GET query parameters * @param {Boolean} rendered - Whether resource should be requested using rendered media types * @param {Function} progressCallback * @private * @returns {Promise<Array>} Content of HTTP message body parts */ _httpGetMultipartVideo( url, mediaTypes, byteRange, params, rendered = false, progressCallback, withCredentials ) { const headers = {}; let supportedMediaTypes; if (rendered) { supportedMediaTypes = [ "video/", "video/*", "video/mpeg2", "video/mp4", "video/H265" ]; } else { supportedMediaTypes = { "1.2.840.10008.1.2.4.100": ["video/mpeg2"], "1.2.840.10008.1.2.4.101": ["video/mpeg2"], "1.2.840.10008.1.2.4.102": ["video/mp4"], "1.2.840.10008.1.2.4.103": ["video/mp4"], "1.2.840.10008.1.2.4.104": ["video/mp4"], "1.2.840.10008.1.2.4.105": ["video/mp4"], "1.2.840.10008.1.2.4.106": ["video/mp4"] }; if (byteRange) { headers.Range = DICOMwebClient._buildRangeHeaderFieldValue(byteRange); } } headers.Accept = DICOMwebClient._buildMultipartAcceptHeaderFieldValue( mediaTypes, supportedMediaTypes ); return this._httpGet(url, headers, "arraybuffer", progressCallback, withCredentials).then( multipartDecode ); } /** * Performs an HTTP GET request that accepts a multipart message with a application/dicom media type. * * @param {String} url - Unique resource locator * @param {Object[]} mediaTypes - Acceptable media types and optionally the UIDs of the corresponding transfer syntaxes * @param {Object} params - Additional HTTP GET query parameters * @param {Function} progressCallback * @private * @returns {Promise<Array>} Content of HTTP message body parts */ _httpGetMultipartApplicationDicom(url, mediaTypes, params, progressCallback, withCredentials) { const headers = {}; const defaultMediaType = "application/dicom"; const supportedMediaTypes = { "1.2.840.10008.1.2.1": [defaultMediaType], "1.2.840.10008.1.2.5": [defaultMediaType], "1.2.840.10008.1.2.4.50": [defaultMediaType], "1.2.840.10008.1.2.4.51": [defaultMediaType], "1.2.840.10008.1.2.4.57": [defaultMediaType], "1.2.840.10008.1.2.4.70": [defaultMediaType], "1.2.840.10008.1.2.4.80": [defaultMediaType], "1.2.840.10008.1.2.4.81": [defaultMediaType], "1.2.840.10008.1.2.4.90": [defaultMediaType], "1.2.840.10008.1.2.4.91": [defaultMediaType], "1.2.840.10008.1.2.4.92": [defaultMediaType], "1.2.840.10008.1.2.4.93": [defaultMediaType], "1.2.840.10008.1.2.4.100": [defaultMediaType], "1.2.840.10008.1.2.4.101": [defaultMediaType], "1.2.840.10008.1.2.4.102": [defaultMediaType], "1.2.840.10008.1.2.4.103": [defaultMediaType], "1.2.840.10008.1.2.4.104": [defaultMediaType], "1.2.840.10008.1.2.4.105": [defaultMediaType], "1.2.840.10008.1.2.4.106": [defaultMediaType] }; let acceptableMediaTypes = mediaTypes; if (!mediaTypes) { acceptableMediaTypes = [{ mediaType: defaultMediaType }]; } headers.Accept = DICOMwebClient._buildMultipartAcceptHeaderFieldValue( acceptableMediaTypes, supportedMediaTypes ); return this._httpGet(url, headers, "arraybuffer", progressCallback, withCredentials).then( multipartDecode ); } /** * Performs an HTTP GET request that accepts a multipart message with a application/octet-stream media type. * * @param {String} url - Unique resource locator * @param {Object[]} mediaTypes - Acceptable media types and optionally the UIDs of the corresponding transfer syntaxes * @param {Array} byteRange start and end of byte range * @param {Object} params - Additional HTTP GET query parameters * @param {Function} progressCallback * @private * @returns {Promise<Array>} Content of HTTP message body parts */ _httpGetMultipartApplicationOctetStream( url, mediaTypes, byteRange, params, progressCallback, withCredentials ) { const headers = {}; const defaultMediaType = "application/octet-stream"; const supportedMediaTypes = { "1.2.840.10008.1.2.1": [defaultMediaType] }; let acceptableMediaTypes = mediaTypes; if (!mediaTypes) { acceptableMediaTypes = [{ mediaType: defaultMediaType }]; } if (byteRange) { headers.Range = DICOMwebClient._buildRangeHeaderFieldValue(byteRange); } headers.Accept = DICOMwebClient._buildMultipartAcceptHeaderFieldValue( acceptableMediaTypes, supportedMediaTypes ); return this._httpGet(url, headers, "arraybuffer", progressCallback, withCredentials).then( multipartDecode ); } /** * Performs an HTTP POST request. * * @param {String} url - Unique resource locator * @param {Object} headers - HTTP header fields * @param {Array} data - Data that should be stored * @param {Function} progressCallback * @private * @returns {Promise} Response */ _httpPost(url, headers, data, progressCallback, withCredentials) { return this._httpRequest(url, "post", headers, { data, progressCallback, withCredentials }); } /** * Performs an HTTP POST request with content-type application/dicom+json. * * @param {String} url - Unique resource locator * @param {Object} headers - HTTP header fields * @param {Array} data - Data that should be stored * @param {Function} progressCallback * @private * @returns {Promise} Response */ _httpPostApplicationJson(url, data, progressCallback, withCredentials) { const headers = { "Content-Type": MEDIATYPES.DICOM_JSON }; return this._httpPost(url, headers, data, progressCallback, withCredentials); } /** * Parses media type and extracts its type and subtype. * * @param {String} mediaType - HTTP media type (e.g. image/jpeg) * @private * @returns {String[]} Media type and subtype */ static _parseMediaType(mediaType) { DICOMwebClient._assertMediaTypeIsValid(mediaType); return mediaType.split("/"); } /** * Builds an accept header field value for HTTP GET request messages. * * @param {Object[]} mediaTypes - Acceptable media types * @param {Object[]} supportedMediaTypes - Supported media types * @return {*} * @private */ static _buildAcceptHeaderFieldValue(mediaTypes, supportedMediaTypes) { if (!Array.isArray(mediaTypes)) { throw new Error("Acceptable media types must be provided as an Array"); } const fieldValueParts = mediaTypes.map(item => { const { mediaType } = item; DICOMwebClient._assertMediaTypeIsValid(mediaType); if (!supportedMediaTypes.includes(mediaType)) { throw new Error( `Media type ${mediaType} is not supported for requested resource` ); } return mediaType; }); return fieldValueParts.join(", "); } /** * 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 */ static _buildMultipartAcceptHeaderFieldValue( mediaTypes, supportedMediaTypes ) { if (!Array.isArray(mediaTypes)) { throw new Error("Acceptable media types must be provided as an Array"); } if (!Array.isArray(supportedMediaTypes) && !isObject(supportedMediaTypes)) { throw new Error( "Supported media types must be provided as an Array or an Object" ); } const fieldValueParts = []; mediaTypes.forEach(item => { const { transferSyntaxUID, mediaType } = item; DICOMwebClient._assertMediaTypeIsValid(mediaType); let fieldValue = `multipart/related; type="${mediaType}"`; if (isObject(supportedMediaTypes)) { // SupportedMediaTypes is a lookup table that maps Transfer Syntax UID // to one or more Media Types if (!Object.values(supportedMediaTypes).flat(1).includes(mediaType)) { if (!mediaType.endsWith("/*") || !mediaType.endsWith("/")) { throw new Error( `Media type ${mediaType} is not supported for requested resource` ); } } if (transferSyntaxUID) { if (transferSyntaxUID !== "*") { if (!Object.keys(supportedMediaTypes).includes(transferSyntaxUID)) { throw new Error( `Transfer syntax ${transferSyntaxUID} is not supported for requested resource` ); } const expectedMediaTypes = supportedMediaTypes[transferSyntaxUID]; if (!expectedMediaTypes.includes(mediaType)) { const actualType = DICOMwebClient._parseMediaType(mediaType)[0]; expectedMediaTypes.map(expectedMediaType => { const expectedType = DICOMwebClient._parseMediaType( expectedMediaType )[0]; const haveSameType = actualType === expectedType; if ( haveSameType && (mediaType.endsWith("/*") || mediaType.endsWith("/")) ) { return; } throw new Error( `Transfer syntax ${transferSyntaxUID} is not supported for requested resource` ); }) } } fieldValue += `; transfer-syntax=${transferSyntaxUID}`; } } else if ( Array.isArray(supportedMediaTypes) && !supportedMediaTypes.includes(mediaType) ) { throw new Error( `Media type ${mediaType} is not supported for requested resource` ); } fieldValueParts.push(fieldValue); }); return fieldValueParts.join(", "); } /** * Builds a range header field value for HTTP GET request messages. * * @param {Array} byteRange - Start and end of byte range * @returns {String} Range header field value * @private */ static _buildRangeHeaderFieldValue(byteRange = []) { if (byteRange.length === 1) { return `bytes=${byteRange[0]}-`; } if (byteRange.length === 2) { return `bytes=${byteRange[0]}-${byteRange[1]}`; } return "bytes=0-"; } /** * Gets types that are shared among acceptable media types. * * @param {Object[]} mediaTypes - Acceptable media types and optionally the UIDs of the corresponding transfer syntaxes * @private * @returns {String[]} Types that are shared among acceptable media types */ static _getSharedMediaTypes(mediaTypes) { const types = new Set(); if (!mediaTypes || !mediaTypes.length) { return types } mediaTypes.forEach(item => { const { mediaType } = item; const type = DICOMwebClient._parseMediaType(mediaType)[0]; types.add(`${type}/`); }); return Array.from(types) } /** * Gets common 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 */ static _getCommonMediaType(mediaTypes) { if (!mediaTypes || !mediaTypes.length) { throw new Error("No acceptable media types provided"); } const sharedMediaTypes = DICOMwebClient._getSharedMediaTypes(mediaTypes); if (sharedMediaTypes.length === 0) { throw new Error("No common acceptable media type could be identified."); } else if (sharedMediaTypes.length > 1) { throw new Error("Acceptable media types must have the same type."); } return sharedMediaTypes[0]; } /** * Searches for DICOM studies. * * @param {Object} options * @param {Object} [options.queryParams] - HTTP query parameters * @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"); let withCredentials = false; let url = `${this.qidoURL}/studies`; if ("queryParams" in options) { url += DICOMwebClient._parseQueryParameters(options.queryParams); } if ("withCredentials" in options) { if(options.withCredentials) { withCredentials = options.withCredentials; } } return this._httpGetApplicationJson(url, {}, false, withCredentials); } /** * Retrieves metadata for a DICOM study. * * @param {Object} options * @param {Object} studyInstanceUID - Study Instance UID * @returns {Object[]} Metadata elements in DICOM JSON format for each instance belonging to the study */ retrieveStudyMetadata(options) { if (!("studyInstanceUID" in options)) { throw new Error( "Study Instance UID is required for retrieval of study metadata" ); } console.log(`retrieve metadata of study ${options.studyInstanceUID}`); const url = `${this.wadoURL}/studies/${options.studyInstanceUID}/metadata`; let withCredentials = false; if ("withCredentials" in options) { if(options.withCredentials) { withCredentials = options.withCredentials; } } return this._httpGetApplicationJson(url, {}, false, withCredentials); } /** * Searches for DICOM series. * * @param {Object} options * @param {Object} [options.studyInstanceUID] - Study Instance UID * @param {Object} [options.queryParams] - HTTP query parameters * @returns {Object[]} Series representations (http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_6.7.html#table_6.7.1-2a) */ searchForSeries(options = {}) { let url = this.qidoURL; if ("studyInstanceUID" in options) { console.log(`search series of study ${options.studyInstanceUID}`); url += `/studies/${options.studyInstanceUID}`; } url += "/series"; if ("queryParams" in options) { url += DICOMwebClient._parseQueryParameters(options.queryParams); } let withCredentials = false; if ("withCredentials" in options) { if(options.withCredentials) { withCredentials = options.withCredentials; } } return this._httpGetApplicationJson(url, {}, false, withCredentials); } /** * Retrieves metadata for a DICOM series. * * @param {Object} options * @param {Object} options.studyInstanceUID - Study Instance UID * @param {Object} options.seriesInstanceUID - Series Instance UID * @returns {Object[]} Metadata elements in DICOM JSON format for each instance belonging to the series */ retrieveSeriesMetadata(options) { if (!("studyInstanceUID" in options)) { throw new Error( "Study Instance UID is required for retrieval of series metadata" ); } if (!("seriesInstanceUID" in options)) { throw new Error( "Series Instance UID is required for retrieval of series metadata" ); } console.log(`retrieve metadata of series ${options.seriesInstanceUID}`); const url = `${this.wadoURL}/studies/${options.studyInstanceUID}/series/${ options.seriesInstanceUID }/metadata`; let withCredentials = false; if ("withCredentials" in options) { if(options.withCredentials) { withCredentials = options.withCredentials; } } return this._httpGetApplicationJson(url, {}, false, withCredentials); } /** * Searches for DICOM Instances. * * @param {Object} options * @param {Object} [options.studyInstanceUID] - Study Instance UID * @param {Object} [options.seriesInstanceUID] - Series Instance UID * @param {Object} [options.queryParams] - HTTP query parameters * @returns {Object[]} Instance representations (http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_6.7.html#table_6.7.1-2b) */ searchForInstances(options = {}) { let url = this.qidoURL; let withCredentials = false; if ("studyInstanceUID" in options) { url += `/studies/${options.studyInstanceUID}`; if ("seriesInstanceUID" in options) { console.log( `search for instances of series ${options.seriesInstanceUID}` ); url += `/series/${options.seriesInstanceUID}`; } else { console.log( `search for instances of study ${options.studyInstanceUID}` ); } } else { console.log("search for instances"); } url += "/instances"; if ("queryParams" in options) { url += DICOMwebClient._parseQueryParameters(options.queryParams); } if ("withCredentials" in options) { if(options.withCredentials) { withCredentials = options.withCredentials; } } return this._httpGetApplicationJson(url, {}, false, withCredentials); } /** Returns a WADO-URI URL for an instance * * @param {Object} options * @param {Object} options.studyInstanceUID - Study Instance UID * @param {Object} options.seriesInstanceUID - Series Instance UID * @param {Object} options.sopInstanceUID - SOP Instance UID * @returns {String} WADO-URI URL */ buildInstanceWadoURIUrl(options) { if (!("studyInstanceUID" in options)) { throw new Error("Study Instance UID is required."); } if (!("seriesInstanceUID" in options)) { throw new Error("Series Instance UID is required."); } if (!("sopInstanceUID" in options)) { throw new Error("SOP Instance UID is required."); } const contentType = options.contentType || MEDIATYPES.DICOM; const transferSyntax = options.transferSyntax || "*"; const params = []; params.push("requestType=WADO"); params.push(`studyUID=${options.studyInstanceUID}`); params.push(`seriesUID=${options.seriesInstanceUID}`); params.push(`objectUID=${options.sopInstanceUID}`); params.push(`contentType=${contentType}`); params.push(`transferSyntax=${transferSyntax}`); const paramString = params.join("&"); return `${this.wadoURL}?${paramString}`; } /** * Retrieves metadata for a DICOM Instance. * * @param {Object} options object * @param {String} options.studyInstanceUID - Study Instance UID * @param {String} options.seriesInstanceUID - Series Instance UID * @param {String} options.sopInstanceUID - SOP Instance UID * @returns {Object} metadata elements in DICOM JSON format */ retrieveInstanceMetadata(options) { if (!("studyInstanceUID" in options)) { throw new Error( "Study Instance UID is required for retrieval of instance metadata" ); } if (!("seriesInstanceUID" in options)) { throw new Error( "Series Instance UID is required for retrieval of instance metadata" ); } if (!("sopInstanceUID" in options)) { throw new Error( "SOP Instance UID is required for retrieval of instance metadata" ); } console.log(`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) { if(options.withCredentials) { withCredentials = options.withCredentials; } } return this._httpGetApplicationJson(url, {}, false, withCredentials); } /** * Retrieves frames for a DICOM Instance. * @param {Object} options options object * @param {String} options.studyInstanceUID - Study Instance UID * @param {String} options.seriesInstanceUID - Series Instance UID * @param {String} options.sopInstanceUID - SOP Instance UID * @param {String} options.frameNumbers - One-based indices of Frame Items * @returns {Array} frame items as byte arrays of the pixel data element */ retrieveInstanceFrames(options) { if (!("studyInstanceUID" in options)) { throw new Error( "Study Instance UID is required for retrieval of instance frames" ); } if (!("seriesInstanceUID" in options)) { throw new Error( "Series Instance UID is required for retrieval of instance frames" ); } if (!("sopInstanceUID" in options)) { throw new Error( "SOP Instance UID is required for retrieval of instance frames" ); } if (!("frameNumbers" in options)) { throw new Error( "frame numbers are required for retrieval of instance frames" ); } console.log( `retrieve frames ${options.frameNumbers.toString()} of instance ${ options.sopInstanceUID }` ); const url = `${this.wadoURL}/studies/${options.studyInstanceUID}/series/${ options.seriesInstanceUID }/instances/${ options.sopInstanceUID }/frames/${options.frameNumbers.toString()}`; const { mediaTypes } = options; let withCredentials = false; if ("withCredentials" in options) { if(options.withCredentials) { withCredentials = options.withCredentials; } } let progressCallback = false; if ("progressCallback" in options) { progressCallback = options.progressCallback; } if (!mediaTypes) { return this._httpGetMultipartApplicationOctetStream( url, false, false, false, progressCallback, withCredentials ); } const sharedMediaTypes = DICOMwebClient._getSharedMediaTypes(mediaTypes); if (sharedMediaTypes.length > 1) { /** * Enable request of frames that are stored either compressed * (image/* media type) or uncompressed (application/octet-stream * media type). */ const supportedMediaTypes = { "1.2.840.10008.1.2.1": ["application/octet-stream"], "1.2.840.10008.1.2.5": ["image/x-dicom-rle"], "1.2.840.10008.1.2.4.50": ["image/jpeg"], "1.2.840.10008.1.2.4.51": ["image/jpeg"], "1.2.840.10008.1.2.4.57": ["image/jpeg"], "1.2.840.10008.1.2.4.70": ["image/jpeg"], "1.2.840.10008.1.2.4.80": ["image/x-jls", "image/jls"], "1.2.840.10008.1.2.4.81": ["image/x-jls", "image/jls"], "1.2.840.10008.1.2.4.90": ["image/jp2"], "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"], } const headers = { Accept: DICOMwebClient._buildMultipartAcceptHeaderFieldValue( mediaTypes, supportedMediaTypes ) } return this._httpGet( url, headers, "arraybuffer", progressCallback, withCredentials ).then(multipartDecode); } const commonMediaType = DICOMwebClient._getCommonMediaType(mediaTypes); if (commonMediaType.startsWith("application")) { return this._httpGetMultipartApplicationOctetStream( url, mediaTypes, false, false, progressCallback, withCredentials ); } else if (commonMediaType.startsWith("image")) { return this._httpGetMultipartImage( url, mediaTypes, false, false, false, progressCallback, withCredentials ); } else if (commonMediaType.startsWith("video")) { return this._httpGetMultipartVideo( url, mediaTypes, false, false, false, progressCallback, withCredentials ); } throw new Error( `Media type ${commonMediaType} is not supported for retrieval of frames.` ); } /** * Retrieves an individual, server-side rendered DICOM Instance. * * @param {Object} options * @param {String} options.studyInstanceUID - Study Instance UID * @param {String} options.seriesInstanceUID - Series Instance UID * @param {String} options.sopInstanceUID - SOP Instance UID * @param {String[]} [options.mediaType] - Acceptable HTTP media types * @param {Object} [options.queryParams] - HTTP query parameters * @returns {ArrayBuffer} Rendered DICOM Instance */ retrieveInstanceRendered(options) { if (!("studyInstanceUID" in options)) { throw new Error( "Study Instance UID is required for retrieval of rendered instance" ); } if (!("seriesInstanceUID" in options)) { throw new Error( "Series Instance UID is required for retrieval of rendered instance" ); } if (!("sopInstanceUID" in options)) { throw new Error( "SOP Instance UID is required for retrieval of rendered instance" ); } const url = `${this.wadoURL}/studies/${options.studyInstanceUID}/series/${ options.seriesInstanceUID }/instances/${options.sopInstanceUID}/rendered`; const { mediaTypes, queryParams } = options; const headers = {}; let withCredentials = false; if ("withCredentials" in options) { if(options.withCredentials) { withCredentials = options.withCredentials; } } let progressCallback = false; if ("progressCallback" in options) { progressCallback = options.progressCallback; } if (!mediaTypes) { const responseType = "arraybuffer"; if (queryParams) { url += DICOMwebClient._parseQueryParameters(queryParams); } return this._httpGet(url, headers, responseType, progressCallback, withCredentials); } const commonMediaType = DICOMwebClient._getCommonMediaType(mediaTypes); if (commonMediaType.startsWith("image")) { return this._httpGetImage( url, mediaTypes, queryParams, progressCallback, withCredentials ); } else if (commonMediaType.startsWith("video")) { return this._httpGetVideo( url, mediaTypes, queryParams, progressCallback, withCredentials ); } else if (commonMediaType.startsWith("text")) { return this._httpGetText( url, mediaTypes, queryParams, progressCallback, withCredentials ); } else if (commonMediaType === MEDIATYPES.PDF) { return this._httpGetApplicationPdf( url, queryParams, progressCallback, withCredentials ); } throw new Error( `Media type ${commonMediaType} is not supported ` + 'for retrieval of rendered instance.' ); } /** * Retrieves a thumbnail of an DICOM Instance. * * @param {Object} options * @param {String} options.studyInstanceUID - Study Instance UID * @param {String} options.seriesInstanceUID - Series Instance UID * @param {String} options.sopInstanceUID - SOP Instance UID * @param {String[]} [options.mediaType] - Acceptable HTTP media types * @param {Object} [options.queryParams] - HTTP query parameters * @returns {ArrayBuffer} Thumbnail */ retrieveInstanceThumbnail(options) { if (!("studyInstanceUID" in options)) { throw new Error( "Study Instance UID is required for retrieval of rendered instance" ); } if (!("seriesInstanceUID" in options)) { throw new Error( "Series Instance UID is required for retrieval of rendered instance" ); } if (!("sopInstanceUID" in options)) { throw new Error( "SOP Instance UID is required for retrieval of rendered instance" ); } const url = `${this.wadoURL}/studies/${options.studyInstanceUID}/series/${ options.seriesInstanceUID }/instances/${options.sopInstanceUID}/thumbnail`; const { mediaTypes, queryParams } = options; const headers = {}; let withCredentials = false; if ("withCredentials" in options) { if(options.withCredentials) { withCredentials = options.withCredentials; } } let progressCallback = false; if ("progressCallback" in options) { progressCallback = options.progressCallback; } if (!mediaTypes) { const responseType = "arraybuffer"; if (queryParams) { url += DICOMwebClient._parseQueryParameters(queryParams); } return this._httpGet( url, headers, responseType, progressCallback, withCredentials ); } const commonMediaType = DICOMwebClient._getCommonMediaType(mediaTypes); if (commonMediaType.startsWith("image")) { return this._httpGetImage( url, mediaTypes, queryParams, progressCallback, withCredentials ); } throw new Error( `Media type ${commonMediaType} is not supported ` + 'for retrieval of rendered instance.' ); } /** * Retrieves rendered frames for a DICOM Instance. * * @param {Object} options * @param {String} options.studyInstanceUID - Study Instance UID * @param {String} options.seriesInstanceUID - Series Instance UID * @param {String} options.sopInstanceUID - SOP Instance UID * @param {String} options.frameNumbers - One-based indices of Frame Items * @param {String[]} [options.mediaType] - Acceptable HTTP media types * @param {Object} [options.queryParams] - HTTP query parameters * @returns {ArrayBuffer[]} Rendered Frame Items as byte arrays */ retrieveInstanceFramesRendered(options) { if (!("studyInstanceUID" in options)) { throw new Error( "Study Instance UID is required for retrieval of rendered instance frames" ); } if (!("seriesInstanceUID" in options)) { throw new Error( "Series Instance UID is required for retrieval of rendered instance frames" ); } if (!("sopInstanceUID" in options)) { throw new Error( "SOP Instance UID is required for retrieval of rendered instance frames" ); } if (!("frameNumbers" in options)) { throw new Error( "frame numbers are required for retrieval of rendered instance frames" ); } console.debug( `retrieve rendered frames ${options.frameNumbers.toString()} of instance ${ options.sopInstanceUID }` ); const url = `${this.wadoURL}/studies/${options.studyInstanceUID}/series/${ options.seriesInstanceUID }/instances/${ options.sopInstanceUID }/frames/${options.frameNumbers.toString()}/rendered`; const { mediaTypes, queryParams } = options; const headers = {}; let withCredentials = false; if ("withCredentials" in options) { if(options.withCredentials) { withCredentials = options.withCredentials; } } let progressCallback = false; if ("progressCallback" in options) { progressCallback = options.progressCallback; } if (!mediaTypes) { const responseType = "arraybuffer"; if (queryParams) { url += DICOMwebClient._parseQueryParameters(queryParams); } return this._httpGet(url, headers, responseType, false, withCredentials); } const commonMediaType = DICOMwebClient._getCommonMediaType(mediaTypes); if (commonMediaType.startsWith("image")) { return this._httpGetImage( url, mediaTypes, queryParams, progressCallback, withCredentials ); } else if (commonMediaType.startsWith("video")) { return this._httpGetVideo( url, mediaTypes, queryParams, progressCallback, withCredentials ); } throw new Error( `Media type ${commonMediaType} is not supported ` + 'for retrieval of rendered frame.' ); } /** * Retrieves thumbnail of frames for a DICOM Instance. * * @param {Object} options * @param {String} options.studyInstanceUID - Study Instance UID * @param {String} options.seriesInstanceUID - Series Instance UID * @param {String} options.sopInstanceUID - SOP Instance UID * @param {String} options.frameNumbers - One-based indices of Frame Items * @param {Object} [options.queryParams] - HTTP query parameters * @returns {ArrayBuffer[]} Rendered Frame Items as byte arrays */ retrieveInstanceFramesThumbnail(options) { if (!("studyInstanceUID" in options)) { throw new Error( "Study Instance UID is required for retrieval of rendered instance frames" ); } if (!("seriesInstanceUID" in options)) { throw new Error( "Series Instance UID is required for retrieval of rendered instance frames" ); } if (!("sopInstanceUID" in options)) { throw new Error( "SOP Instance UID is required for retrieval of rendered instance frames" ); } if (!("frameNumbers" in options)) { throw new Error( "frame numbers are required for retrieval of rendered instance frames" ); } console.debug( `retrieve rendered frames ${options.frameNumbers.toString()} of instance ${ options.sopInstanceUID }` ); const url = `${this.wadoURL}/studies/${options.studyInstanceUID}/series/${ options.seriesInstanceUID }/instances/${ options.sopInstanceUID }/frames/${options.frameNumbers.toString()}/thumbnail`; const { mediaTypes, queryParams } = options; const headers = {}; let withCredentials = false; if ("withCredentials" in options) { if(options.withCredentials) { withCredentials = options.withCredentials; } } let progressCallback = false; if ("progressCallback" in options) { progressCallback = options.progressCallback; } if (!mediaTypes) { const responseType = "arraybuffer"; if (queryParams) { url += DICOMwebClient._parseQueryParameters(queryParams); } return this._httpGet( url, headers, responseType, progressCallback, withCredentials ); } const commonMediaType = DICOMwebClient._getCommonMediaType(mediaTypes); if (commonMediaType.startsWith("image")) { return this._httpGetImage( url, mediaTypes, queryParams, progressCallback, withCredentials ); } throw new Error( `Media type ${commonMediaType} is not supported ` + 'for retrieval of rendered frame.' ); } /** * Retrieves a DICOM Instance.