diff --git a/extensions/default/src/utils/createRenderedRetrieve.js b/extensions/default/src/utils/createRenderedRetrieve.js new file mode 100644 index 000000000..e92e542dd --- /dev/null +++ b/extensions/default/src/utils/createRenderedRetrieve.js @@ -0,0 +1,26 @@ +/** + * Generates the rendered URL that can be used for direct retrieve of the pixel data binary stream. + * + * @param {object} config - The configuration object. + * @param {string} config.wadoRoot - The root URL for the WADO service. + * @param {object} params - The parameters object. + * @param {string} params.tag - The tag name of the URL to retrieve. + * @param {string} params.defaultPath - The path for the pixel data URL. + * @param {object} params.instance - The instance object that the tag is in. + * @param {string} params.defaultType - The mime type of the response. + * @param {string} params.singlepart - The type of the part to retrieve. + * @param {string} params.fetchPart - Unknown parameter. + * @param {string} params.url - Unknown parameter. + * @returns {string|Promise} - An absolute URL to the binary stream. + */ +const createRenderedRetrieve = (config, params) => { + const { wadoRoot } = config; + const { instance, tag = 'PixelData' } = params; + const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; + + if (tag === 'PixelData' || tag === 'EncapsulatedDocument') { + return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/rendered`; + } +}; + +export default createRenderedRetrieve; diff --git a/extensions/default/src/utils/createRenderedRetrieve.test.js b/extensions/default/src/utils/createRenderedRetrieve.test.js new file mode 100644 index 000000000..f0d7bec6a --- /dev/null +++ b/extensions/default/src/utils/createRenderedRetrieve.test.js @@ -0,0 +1,46 @@ +import createRenderedRetrieve from './createRenderedRetrieve'; + +describe('createRenderedRetrieve', () => { + const config = { + wadoRoot: 'https://example.com/wado', + }; + + const params = { + instance: { + StudyInstanceUID: 'study-uid', + SeriesInstanceUID: 'series-uid', + SOPInstanceUID: 'sop-uid', + }, + }; + + it('should return the rendered URL for PixelData tag', () => { + const result = createRenderedRetrieve(config, { + ...params, + tag: 'PixelData', + }); + + expect(result).toBe( + 'https://example.com/wado/studies/study-uid/series/series-uid/instances/sop-uid/rendered' + ); + }); + + it('should return the rendered URL for EncapsulatedDocument tag', () => { + const result = createRenderedRetrieve(config, { + ...params, + tag: 'EncapsulatedDocument', + }); + + expect(result).toBe( + 'https://example.com/wado/studies/study-uid/series/series-uid/instances/sop-uid/rendered' + ); + }); + + it('should return undefined for unknown tag', () => { + const result = createRenderedRetrieve(config, { + ...params, + tag: 'UnknownTag', + }); + + expect(result).toBeUndefined(); + }); +}); diff --git a/extensions/default/src/utils/getBulkdataValue.js b/extensions/default/src/utils/getBulkdataValue.js new file mode 100644 index 000000000..1df10fb97 --- /dev/null +++ b/extensions/default/src/utils/getBulkdataValue.js @@ -0,0 +1,40 @@ +/** + * Generates a URL that can be used for direct retrieve of the bulkdata. + * + * @param {object} config - The configuration object. + * @param {object} params - The parameters object. + * @param {string} params.tag - The tag name of the URL to retrieve. + * @param {string} params.defaultPath - The path for the pixel data URL. + * @param {object} params.instance - The instance object that the tag is in. + * @param {string} params.defaultType - The mime type of the response. + * @param {string} params.singlepart - The type of the part to retrieve. + * @param {string} params.fetchPart - Unknown. + * @returns {string|Promise} - An absolute URL to the resource, if the absolute URL can be retrieved as singlepart, + * or is already retrieved, or a promise to a URL for such use if a BulkDataURI. + */ +const getBulkdataValue = (config, params) => { + const { + instance, + tag = 'PixelData', + defaultPath = '/pixeldata', + defaultType = 'video/mp4', + } = params; + + const value = instance[tag]; + + const { SeriesInstanceUID, SOPInstanceUID } = instance; + + const BulkDataURI = + (value && value.BulkDataURI) || + `series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`; + const hasQuery = BulkDataURI.indexOf('?') !== -1; + const hasAccept = BulkDataURI.indexOf('accept=') !== -1; + const acceptUri = + BulkDataURI + (hasAccept ? '' : (hasQuery ? '&' : '?') + `accept=${defaultType}`); + + // The DICOMweb standard states that the default is multipart related, and then + // separately states that the accept parameter is the URL parameter equivalent of the accept header. + return acceptUri; +}; + +export default getBulkdataValue; diff --git a/extensions/default/src/utils/getBulkdataValue.test.js b/extensions/default/src/utils/getBulkdataValue.test.js new file mode 100644 index 000000000..542358aac --- /dev/null +++ b/extensions/default/src/utils/getBulkdataValue.test.js @@ -0,0 +1,105 @@ +import getBulkdataValue from './getBulkdataValue'; + +jest.mock('@ohif/core'); + +global.URL.createObjectURL = jest.fn(() => 'blob:'); + +describe('getBulkdataValue', () => { + const config = { + singlepart: true, + }; + + const params = { + instance: { + StudyInstanceUID: 'study-uid', + SeriesInstanceUID: 'series-uid', + SOPInstanceUID: 'sop-uid', + }, + }; + + it('should return the BulkDataURI with defaultType if singlepart is true without accept', () => { + const value = { + BulkDataURI: 'https://example.com/bulkdata', + retrieveBulkData: jest.fn().mockResolvedValueOnce(new Uint8Array([0, 1, 2])), + }; + + const result = getBulkdataValue(config, { + ...params, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + }); + + expect(result).toContain(value.BulkDataURI); + expect(result).toContain('accept=video/mp4'); + const acceptCount = result.match(/accept=video\/mp4/g)?.length || 0; + expect(acceptCount).toBe(1); + }); + + it('should return the BulkDataURI with accept', () => { + const value = { + BulkDataURI: 'https://example.com/bulkdata?accept=video/mp4', + }; + + const result = getBulkdataValue(config, { + ...params, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + }); + + expect(result).toContain(value.BulkDataURI); + expect(result).toContain('accept=video/mp4'); + const acceptCount = result.match(/accept=video\/mp4/g)?.length || 0; + expect(acceptCount).toBe(1); + }); + + it('should return the BulkDataURI with accept and query params', () => { + const value = { + BulkDataURI: 'https://example.com/bulkdata?test=123', + }; + + const result = getBulkdataValue(config, { + ...params, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + }); + + expect(result).toContain(value.BulkDataURI); + expect(result).toContain('accept=video/mp4'); + expect(result).toContain('test=123'); + const acceptCount = result.match(/accept=video\/mp4/g)?.length || 0; + expect(acceptCount).toBe(1); + }); + + it('should return default path with accept', () => { + const value = { + BulkDataURI: null, + }; + + const defaultPath = '/testing'; + const defaultURI = `series/${params.instance.SeriesInstanceUID}/instances/${params.instance.SOPInstanceUID}${defaultPath}`; + + const result = getBulkdataValue(config, { + ...params, + defaultPath, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + }); + + expect(result).toContain(defaultURI); + expect(result).toContain('accept=video/mp4'); + const acceptCount = result.match(/accept=video\/mp4/g)?.length || 0; + expect(acceptCount).toBe(1); + }); +}); diff --git a/extensions/default/src/utils/getDirectURL.js b/extensions/default/src/utils/getDirectURL.js index d9ea92499..54e7745c9 100644 --- a/extensions/default/src/utils/getDirectURL.js +++ b/extensions/default/src/utils/getDirectURL.js @@ -1,5 +1,8 @@ import { utils } from '@ohif/core'; +import getBulkdataValue from './getBulkdataValue'; +import createRenderedRetrieve from './createRenderedRetrieve'; + /** * Generates a URL that can be used for direct retrieve of the bulkdata * @@ -15,63 +18,48 @@ import { utils } from '@ohif/core'; * or is already retrieved, or a promise to a URL for such use if a BulkDataURI */ const getDirectURL = (config, params) => { - const { wadoRoot, singlepart } = config; + const { singlepart } = config; const { instance, tag = 'PixelData', - defaultPath = '/pixeldata', defaultType = 'video/mp4', singlepart: fetchPart = 'video', url = null, } = params; + if (url) { return url; } const value = instance[tag]; - if (!value) { - return undefined; - } - - if (value.DirectRetrieveURL) { - return value.DirectRetrieveURL; - } - if (value.InlineBinary) { - const blob = utils.b64toBlob(value.InlineBinary, defaultType); - value.DirectRetrieveURL = URL.createObjectURL(blob); - return value.DirectRetrieveURL; - } - if (!singlepart || (singlepart !== true && singlepart.indexOf(fetchPart) === -1)) { - if (value.retrieveBulkData) { - // Try the specified retrieve type. - const options = { - mediaType: defaultType, - }; - return value.retrieveBulkData(options).then(arr => { - value.DirectRetrieveURL = URL.createObjectURL(new Blob([arr], { type: defaultType })); - return value.DirectRetrieveURL; - }); + if (value) { + if (value.DirectRetrieveURL) { + return value.DirectRetrieveURL; + } + + if (value.InlineBinary) { + const blob = utils.b64toBlob(value.InlineBinary, defaultType); + value.DirectRetrieveURL = URL.createObjectURL(blob); + return value.DirectRetrieveURL; + } + + if (!singlepart || (singlepart !== true && singlepart.indexOf(fetchPart) === -1)) { + if (value.retrieveBulkData) { + // Try the specified retrieve type. + const options = { + mediaType: defaultType, + }; + return value.retrieveBulkData(options).then(arr => { + value.DirectRetrieveURL = URL.createObjectURL(new Blob([arr], { type: defaultType })); + return value.DirectRetrieveURL; + }); + } + console.warn('Unable to retrieve', tag, 'from', instance); + return undefined; } - console.warn('Unable to retrieve', tag, 'from', instance); - return undefined; } - const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; - const BulkDataURI = - (value && value.BulkDataURI) || - `series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`; - const hasQuery = BulkDataURI.indexOf('?') !== -1; - const hasAccept = BulkDataURI.indexOf('accept=') !== -1; - const acceptUri = - BulkDataURI + (hasAccept ? '' : (hasQuery ? '&' : '?') + `accept=${defaultType}`); - - if (tag === 'PixelData' || tag === 'EncapsulatedDocument') { - return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/rendered`; - } - - // The DICOMweb standard states that the default is multipart related, and then - // separately states that the accept parameter is the URL parameter equivalent of the accept header. - return acceptUri; + return createRenderedRetrieve(config, params) || getBulkdataValue(config, params); }; export default getDirectURL; diff --git a/extensions/default/src/utils/getDirectURL.test.js b/extensions/default/src/utils/getDirectURL.test.js new file mode 100644 index 000000000..eb13e5ac9 --- /dev/null +++ b/extensions/default/src/utils/getDirectURL.test.js @@ -0,0 +1,169 @@ +import getDirectURL from './getDirectURL'; +import getBulkdataValue from './getBulkdataValue'; +import createRenderedRetrieve from './createRenderedRetrieve'; + +jest.mock('@ohif/core'); +jest.mock('./getBulkdataValue'); +jest.mock('./createRenderedRetrieve'); + +global.URL.createObjectURL = jest.fn(() => 'blob:'); + +describe('getDirectURL', () => { + const config = { + singlepart: true, + defaultType: 'video/mp4', + }; + + const params = { + tag: 'PixelData', + defaultPath: '/path/to/pixeldata', + instance: { + StudyInstanceUID: 'study-uid', + SeriesInstanceUID: 'series-uid', + SOPInstanceUID: 'sop-uid', + }, + }; + + it('should return the provided URL if it exists', () => { + const url = 'https://example.com/direct-retrieve'; + + const result = getDirectURL(config, { + ...params, + url: 'https://example.com/direct-retrieve', + }); + + expect(result).toBe(url); + }); + + it('should return the DirectRetrieveURL if it exists', () => { + const value = { + DirectRetrieveURL: 'https://example.com/direct-retrieve', + }; + + const result = getDirectURL(config, { + ...params, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + }); + + expect(result).toBe(value.DirectRetrieveURL); + }); + + it('should return the URL for InlineBinary', () => { + const value = { + InlineBinary: 'base64-encoded-data', + }; + + const result = getDirectURL(config, { + ...params, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + }); + + expect(result).toContain('blob:'); + }); + + it('should return the BulkDataURI with defaultType if singlepart is false and there is no retrieveBulkData', () => { + const value = { + BulkDataURI: 'https://example.com/bulkdata', + }; + + const result = getDirectURL( + { + ...config, + singlepart: false, + }, + { + ...params, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + } + ); + + expect(result).toBeUndefined(); + }); + + it('should return the BulkDataURI with defaultType if singlepart is false with retrieveBulkData', async () => { + const value = { + BulkDataURI: 'https://example.com/bulkdata', + retrieveBulkData: jest.fn().mockResolvedValueOnce(new Uint8Array([0, 1, 2])), + }; + + const result = await getDirectURL( + { + ...config, + singlepart: false, + }, + { + ...params, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + } + ); + + expect(result).toContain('blob:'); + }); + + it('should return the BulkDataURI with defaultType if singlepart does not include fetchPart', async () => { + const arr = new Uint8Array([0, 1, 2]); + + const value = { + BulkDataURI: 'https://example.com/bulkdata', + retrieveBulkData: jest.fn().mockResolvedValueOnce(arr), + }; + + const result = await getDirectURL( + { + ...config, + singlepart: ['audio'], + }, + { + ...params, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + } + ); + + expect(result).toContain('blob:'); + expect(URL.createObjectURL).toHaveBeenCalledWith(new Blob([arr], { type: 'accept=video/mp4' })); + }); + + it('should return the URL from getBulkdataValue if it exists', () => { + const bulkDataURL = 'https://example.com/bulkdata'; + + getBulkdataValue.mockReturnValueOnce(bulkDataURL); + + const result = getDirectURL(config, params); + + expect(getBulkdataValue).toHaveBeenCalledWith(config, params); + expect(result).toBe(bulkDataURL); + }); + + it('should return the URL from createRenderedRetrieve if getBulkdataValue returns falsy', () => { + const renderedRetrieveURL = 'https://example.com/rendered-retrieve'; + + getBulkdataValue.mockReturnValueOnce(null); + createRenderedRetrieve.mockReturnValueOnce(renderedRetrieveURL); + + const result = getDirectURL(config, params); + + expect(getBulkdataValue).toHaveBeenCalledWith(config, params); + expect(createRenderedRetrieve).toHaveBeenCalledWith(config, params); + expect(result).toBe(renderedRetrieveURL); + }); +});