diff --git a/extensions/cornerstone/src/Viewport/Overlays/ViewportOrientationMarkers.tsx b/extensions/cornerstone/src/Viewport/Overlays/ViewportOrientationMarkers.tsx index 1e2787b248a..b599897537d 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/ViewportOrientationMarkers.tsx +++ b/extensions/cornerstone/src/Viewport/Overlays/ViewportOrientationMarkers.tsx @@ -77,6 +77,10 @@ function ViewportOrientationMarkers({ const p00 = viewport.canvasToWorld([0, 0]); const p10 = viewport.canvasToWorld([1, 0]); const p01 = viewport.canvasToWorld([0, 1]); + // WSIViewport returns undefined until the slide metadata loads. + if (!p00 || !p10 || !p01) { + return ''; + } const rowCosines = vec3.sub(vec3.create(), p10, p00); const columnCosines = vec3.sub(vec3.create(), p01, p00); diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index b2eafece760..77a4424a776 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -607,12 +607,15 @@ class CornerstoneViewportService // The broadcast event here ensures that listeners have a valid, up to date // viewport to access. Doing it too early can result in exceptions or // invalid data. - displaySetPromise.then(() => { - this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, { - viewportData, - viewportId, - }); - }); + displaySetPromise.then( + () => { + this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, { + viewportData, + viewportId, + }); + }, + error => console.error('[CornerstoneViewportService] Failed to set the viewport data', error) + ); } public getViewportOptions(viewportId: string): ViewportOptions { @@ -910,7 +913,17 @@ class CornerstoneViewportService return; } - return this.backend.mountEcg(viewport, displaySet, imageId); + try { + await this.backend.mountEcg(viewport, displaySet, imageId); + } catch (error) { + // Tell the user, then reject so that setViewportData does not report a successful mount. + this.servicesManager.services.uiNotificationService?.show({ + title: 'ECG', + message: 'The ECG waveform could not be loaded.', + type: 'error', + }); + throw error; + } } // Public so the viewport backends (IViewportServiceInternals) can dispatch to it. @@ -1528,12 +1541,15 @@ class CornerstoneViewportService return; } - displaySetPromise.then(() => { - this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, { - viewportData, - viewportId, - }); - }); + displaySetPromise.then( + () => { + this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, { + viewportData, + viewportId, + }); + }, + error => console.error('[CornerstoneViewportService] Failed to update the viewport', error) + ); } _setDisplaySets( diff --git a/extensions/cornerstone/src/utils/ecgMetadata.test.ts b/extensions/cornerstone/src/utils/ecgMetadata.test.ts index 511b1dbc7d4..39c819a1e5a 100644 --- a/extensions/cornerstone/src/utils/ecgMetadata.test.ts +++ b/extensions/cornerstone/src/utils/ecgMetadata.test.ts @@ -1,217 +1,137 @@ -import { decodeInt16Multiplex, base64ToArrayBuffer, buildEcgModule } from './ecgMetadata'; - -// --------------------------------------------------------------------------- -// decodeInt16Multiplex -// --------------------------------------------------------------------------- -describe('decodeInt16Multiplex', () => { - it('demultiplexes 2 channels correctly', () => { - // Interleaved layout: [ch0_s0, ch1_s0, ch0_s1, ch1_s1, ch0_s2, ch1_s2] - const interleaved = new Int16Array([10, 20, 30, 40, 50, 60]); - const channels = decodeInt16Multiplex(interleaved.buffer, 2, 3); - - expect(channels).toHaveLength(2); - expect(Array.from(channels[0])).toEqual([10, 30, 50]); - expect(Array.from(channels[1])).toEqual([20, 40, 60]); - }); - - it('handles a single channel', () => { - const data = new Int16Array([100, 200, 300]); - const channels = decodeInt16Multiplex(data.buffer, 1, 3); - - expect(channels).toHaveLength(1); - expect(Array.from(channels[0])).toEqual([100, 200, 300]); - }); - - it('handles negative values (signed short)', () => { - const data = new Int16Array([-1000, -2000, -3000]); - const channels = decodeInt16Multiplex(data.buffer, 1, 3); - - expect(Array.from(channels[0])).toEqual([-1000, -2000, -3000]); - }); - - it('returns empty arrays when numberOfChannels or numberOfSamples is 0', () => { - const emptyBuffer = new Int16Array([]).buffer; - - expect(decodeInt16Multiplex(emptyBuffer, 0, 0)).toHaveLength(0); - expect(decodeInt16Multiplex(emptyBuffer, 0, 10)).toHaveLength(0); - }); - - it('produces Int16Array instances for each channel', () => { - const data = new Int16Array([1, 2]); - const channels = decodeInt16Multiplex(data.buffer, 2, 1); - - expect(channels[0]).toBeInstanceOf(Int16Array); - expect(channels[1]).toBeInstanceOf(Int16Array); - }); +import { utilities as csMetadataUtilities } from '@cornerstonejs/metadata'; +import { buildEcgModule } from './ecgMetadata'; + +jest.mock('@cornerstonejs/metadata', () => { + const actual = jest.requireActual('@cornerstonejs/metadata'); + const { buildEcgModuleFromInstance } = actual.utilities; + return { + ...actual, + utilities: { ...actual.utilities, buildEcgModuleFromInstance: jest.fn(buildEcgModuleFromInstance) }, + }; }); -// --------------------------------------------------------------------------- -// base64ToArrayBuffer -// --------------------------------------------------------------------------- -describe('base64ToArrayBuffer', () => { - it('decodes a simple base64 string to the correct bytes', () => { - // "ABC" in ASCII = [65, 66, 67]; base64 of that is 'QUJD' - const buffer = base64ToArrayBuffer('QUJD'); - const bytes = new Uint8Array(buffer); - - expect(bytes).toHaveLength(3); - expect(Array.from(bytes)).toEqual([65, 66, 67]); - }); - - it('round-trips an Int16Array through base64', () => { - const original = new Int16Array([500, -300, 0, 32767]); - const base64 = btoa(String.fromCharCode(...new Uint8Array(original.buffer))); - const recovered = new Int16Array(base64ToArrayBuffer(base64)); - - expect(Array.from(recovered)).toEqual(Array.from(original)); - }); - - it('returns an ArrayBuffer', () => { - const result = base64ToArrayBuffer('AA=='); // single zero byte - expect(result).toBeInstanceOf(ArrayBuffer); - expect(result.byteLength).toBe(1); - }); +// Two channels, two samples, interleaved: ch0=[1, -2], ch1=[3, 4]. +const SAMPLES = new Int16Array([1, 3, -2, 4]); + +const makeInstance = WaveformData => ({ + WaveformSequence: [ + { + NumberOfWaveformChannels: 2, + NumberOfWaveformSamples: 2, + SamplingFrequency: 500, + WaveformBitsAllocated: 16, + WaveformSampleInterpretation: 'SS', + MultiplexGroupLabel: '12 Lead ECG', + ChannelDefinitionSequence: [ + { ChannelSourceSequence: [{ CodeMeaning: 'Lead I' }] }, + { ChannelSourceSequence: [{ CodeMeaning: 'Lead II' }] }, + ], + WaveformData, + }, + ], }); -// --------------------------------------------------------------------------- -// buildEcgModule -// --------------------------------------------------------------------------- -describe('buildEcgModule', () => { - const makeInstance = (overrides = {}) => ({ - WaveformSequence: [ - { - NumberOfWaveformChannels: 12, - NumberOfWaveformSamples: 5000, - SamplingFrequency: 500, - WaveformBitsAllocated: 16, - WaveformSampleInterpretation: 'SS', - MultiplexGroupLabel: '12 Lead ECG', - ChannelDefinitionSequence: [ - { ChannelSourceSequence: [{ CodeMeaning: 'Lead I' }] }, - { ChannelSourceSequence: [{ CodeMeaning: 'Lead II' }] }, - ], - WaveformData: { InlineBinary: btoa('\x00\x01') }, - }, - ], - ...overrides, - }); +const channelsOf = (channels: Int16Array[]) => channels.map(channel => Array.from(channel)); +describe('buildEcgModule', () => { it('returns null when WaveformSequence is absent', () => { expect(buildEcgModule({})).toBeNull(); expect(buildEcgModule({ WaveformSequence: [] })).toBeNull(); }); - it('returns a valid ecgModule for a complete instance', () => { - const module = buildEcgModule(makeInstance()); + it('returns the waveform attributes and the lead names', () => { + const module = buildEcgModule(makeInstance({})); - expect(module).not.toBeNull(); - expect(module!.numberOfWaveformChannels).toBe(12); - expect(module!.numberOfWaveformSamples).toBe(5000); + expect(module!.numberOfWaveformChannels).toBe(2); + expect(module!.numberOfWaveformSamples).toBe(2); expect(module!.samplingFrequency).toBe(500); - expect(module!.waveformBitsAllocated).toBe(16); - expect(module!.waveformSampleInterpretation).toBe('SS'); expect(module!.multiplexGroupLabel).toBe('12 Lead ECG'); - expect(module!.channelDefinitionSequence).toHaveLength(2); expect(module!.channelDefinitionSequence[0].channelSourceSequence.codeMeaning).toBe('Lead I'); - expect(typeof module!.waveformData.retrieveBulkData).toBe('function'); }); - it('applies defaults for missing optional fields', () => { - const instance = { - WaveformSequence: [ - { - // No explicit values — all defaults - WaveformData: { InlineBinary: btoa('\x00\x00') }, - }, - ], - }; - const module = buildEcgModule(instance); + it('decodes InlineBinary data', async () => { + const base64 = btoa(String.fromCharCode(...new Uint8Array(SAMPLES.buffer))); + const module = buildEcgModule(makeInstance({ InlineBinary: base64 })); - expect(module!.numberOfWaveformChannels).toBe(0); - expect(module!.numberOfWaveformSamples).toBe(0); - expect(module!.samplingFrequency).toBe(1); - expect(module!.waveformBitsAllocated).toBe(16); - expect(module!.waveformSampleInterpretation).toBe('SS'); - expect(module!.multiplexGroupLabel).toBe(''); - expect(module!.channelDefinitionSequence).toHaveLength(0); + expect(channelsOf(await module!.waveformData.retrieveBulkData())).toEqual([ + [1, -2], + [3, 4], + ]); }); - it('retrieveBulkData decodes InlineBinary data correctly', async () => { - const samples = new Int16Array([10, 20, 30]); - const base64 = btoa(String.fromCharCode(...new Uint8Array(samples.buffer))); - - const instance = { - WaveformSequence: [ - { - NumberOfWaveformChannels: 1, - NumberOfWaveformSamples: 3, - WaveformData: { InlineBinary: base64 }, - }, - ], - }; - - const module = buildEcgModule(instance); - const channels = await module!.waveformData.retrieveBulkData(); + it('decodes the bytes that the data source retrieveBulkData returns', async () => { + const retrieveBulkData = jest.fn().mockResolvedValue(SAMPLES.buffer.slice(0)); + const module = buildEcgModule( + makeInstance({ BulkDataURI: 'http://example.com/waveform', retrieveBulkData }) + ); - expect(channels).toHaveLength(1); - expect(Array.from(channels[0])).toEqual([10, 20, 30]); + expect(channelsOf(await module!.waveformData.retrieveBulkData())).toEqual([ + [1, -2], + [3, 4], + ]); + expect(retrieveBulkData).toHaveBeenCalledTimes(1); }); - it('retrieveBulkData returns [] when WaveformData is absent', async () => { - const instance = { - WaveformSequence: [ - { NumberOfWaveformChannels: 1, NumberOfWaveformSamples: 1 }, - ], - }; - const module = buildEcgModule(instance); - const channels = await module!.waveformData.retrieveBulkData(); + it('decodes a typed array from the data source retrieveBulkData', async () => { + const bytes = new Uint8Array(SAMPLES.buffer.slice(0)); + const module = buildEcgModule( + makeInstance({ BulkDataURI: 'x', retrieveBulkData: jest.fn().mockResolvedValue(bytes) }) + ); - expect(channels).toEqual([]); + expect(channelsOf(await module!.waveformData.retrieveBulkData())).toEqual([ + [1, -2], + [3, 4], + ]); }); - it('retrieveBulkData fetches BulkDataURI with auth header when provided', async () => { - const samples = new Int16Array([1, 2]); - global.fetch = jest.fn().mockResolvedValue({ - ok: true, - arrayBuffer: () => Promise.resolve(samples.buffer), + it('uses the cached Value and does not fetch again on a re-mount', async () => { + const retrieveBulkData = jest.fn(); + const module = buildEcgModule( + makeInstance({ BulkDataURI: 'x', Value: SAMPLES.buffer.slice(0), retrieveBulkData }) + ); + + await module!.waveformData.retrieveBulkData(); + expect(channelsOf(await module!.waveformData.retrieveBulkData())).toEqual([ + [1, -2], + [3, 4], + ]); + expect(retrieveBulkData).not.toHaveBeenCalled(); + }); + + it('gives the auth headers to the Cornerstone bulkdata fetch', () => { + const builder = csMetadataUtilities.buildEcgModuleFromInstance as jest.Mock; + const authHeader = { Authorization: 'Bearer token' }; + buildEcgModule(makeInstance({ BulkDataURI: 'x' }), { + getAuthorizationHeader: () => authHeader, }); - const userAuthenticationService = { - getAuthorizationHeader: () => ({ Authorization: 'Bearer token123' }), - }; - const instance = { - WaveformSequence: [ - { - NumberOfWaveformChannels: 1, - NumberOfWaveformSamples: 2, - WaveformData: { BulkDataURI: 'http://example.com/waveform' }, - }, - ], - }; + const options = builder.mock.lastCall[2] as { getHeaders: () => unknown }; + expect(options.getHeaders()).toBe(authHeader); + }); - const module = buildEcgModule(instance, userAuthenticationService); - await module!.waveformData.retrieveBulkData(); + it('reads lead names from a lowercase codeMeaning', () => { + const instance = makeInstance({}); + instance.WaveformSequence[0].ChannelDefinitionSequence[0].ChannelSourceSequence = [ + { codeMeaning: 'Lead I' } as never, + ]; - expect(global.fetch).toHaveBeenCalledWith('http://example.com/waveform', { - headers: { - Accept: 'application/octet-stream', - Authorization: 'Bearer token123', - }, - }); + const module = buildEcgModule(instance); + + expect(module!.channelDefinitionSequence[0].channelSourceSequence.codeMeaning).toBe('Lead I'); }); - it('retrieveBulkData throws when BulkDataURI response is not ok', async () => { - global.fetch = jest.fn().mockResolvedValue({ ok: false, status: 403 }); + it('passes the lead code value and coding scheme', () => { + const instance = makeInstance({}); + instance.WaveformSequence[0].ChannelDefinitionSequence[1].ChannelSourceSequence = [ + { CodeValue: '5.6.3-9-2', CodingSchemeDesignator: 'SCPECG', CodeMeaning: 'Lead II' } as never, + ]; - const instance = { - WaveformSequence: [ - { - WaveformData: { BulkDataURI: 'http://example.com/waveform' }, - }, - ], + const source = buildEcgModule(instance)!.channelDefinitionSequence[1].channelSourceSequence as { + codeValue?: string; + codingSchemeDesignator?: string; }; - const module = buildEcgModule(instance); - await expect(module!.waveformData.retrieveBulkData()).rejects.toThrow('403'); + expect(source.codeValue).toBe('5.6.3-9-2'); + expect(source.codingSchemeDesignator).toBe('SCPECG'); }); }); diff --git a/extensions/cornerstone/src/utils/ecgMetadata.ts b/extensions/cornerstone/src/utils/ecgMetadata.ts index 22f9e83ff2e..3aea939d8b1 100644 --- a/extensions/cornerstone/src/utils/ecgMetadata.ts +++ b/extensions/cornerstone/src/utils/ecgMetadata.ts @@ -1,129 +1,109 @@ -/** - * Decode a multiplexed Int16 buffer into per-channel arrays. - * Layout: sample0ch0, sample0ch1 ... sample0chN, sample1ch0, … - * Note: DICOM ECG data is canonically SS (signed short). The sampleInterpretation - * field is forwarded to ECGViewport for its own use; the raw buffer is always - * treated as Int16 because Cornerstone ECGViewport expects Int16Array[]. - */ -export function decodeInt16Multiplex( - buffer: ArrayBuffer, - numberOfChannels: number, - numberOfSamples: number -): Int16Array[] { - const src = new Int16Array(buffer); - const channels: Int16Array[] = []; - for (let ch = 0; ch < numberOfChannels; ch++) { - const out = new Int16Array(numberOfSamples); - for (let s = 0; s < numberOfSamples; s++) { - out[s] = src[s * numberOfChannels + ch]; - } - channels.push(out); +import { utilities as csMetadataUtilities } from '@cornerstonejs/metadata'; + +export type EcgModule = NonNullable< + ReturnType +>; + +type BuildEcgModuleFromInstance = ( + instance: Record, + imageId?: string, + // Released @cornerstonejs/metadata versions ignore this argument. + options?: { getHeaders?: () => Record | undefined } +) => EcgModule | null; + +const buildEcgModuleFromInstance = (...args: Parameters) => + (csMetadataUtilities.buildEcgModuleFromInstance as BuildEcgModuleFromInstance)(...args); + +/** Returns the items of a naturalized sequence: an array, an array-like, or one item. */ +function toList(value: any): any[] { + if (!value) { + return []; } - return channels; + return typeof value.length === 'number' ? Array.from(value) : [value]; } -/** - * Decode a base64 InlineBinary string into a raw ArrayBuffer. - */ -export function base64ToArrayBuffer(base64: string): ArrayBuffer { - const binaryStr = atob(base64); - const bytes = new Uint8Array(binaryStr.length); - for (let i = 0; i < binaryStr.length; i++) { - bytes[i] = binaryStr.charCodeAt(i); +/** Returns the first item of a naturalized value, but keeps binary data whole. */ +function firstItem(value: any): any { + if (!value || value instanceof ArrayBuffer || ArrayBuffer.isView(value)) { + return value; } - return bytes.buffer; + return typeof value.length === 'number' ? value[0] : value; } -export type EcgModule = { - numberOfWaveformChannels: number; - numberOfWaveformSamples: number; - samplingFrequency: number; - waveformBitsAllocated: number; - waveformSampleInterpretation: string; - multiplexGroupLabel: string; - channelDefinitionSequence: Array<{ channelSourceSequence: { codeMeaning: string } }>; - waveformData: { retrieveBulkData: () => Promise }; -}; +/** Copies the bytes of a typed array into an ArrayBuffer, which every Cornerstone version decodes. */ +function toArrayBuffer(bytes: unknown): unknown { + return ArrayBuffer.isView(bytes) + ? bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) + : bytes; +} + +async function decodeBulkData(group: any, waveformData: any): Promise { + let bytes = waveformData.Value; + if (bytes === undefined) { + bytes = await waveformData.retrieveBulkData(); + } + const loaded = { WaveformSequence: [{ ...group, WaveformData: toArrayBuffer(bytes) }] }; + const decoded = buildEcgModuleFromInstance(loaded); + if (!decoded) { + throw new Error('[ECGViewport] Failed to decode the waveform bulkdata'); + } + return decoded.waveformData.retrieveBulkData(); +} /** * Parse the naturalized DICOM instance's WaveformSequence and build the ecgModule * that Cornerstone's ECGViewport.setEcg() expects via * metaData.get(MetadataModules.ECG, imageId). * + * Cornerstone decodes the waveform. When the data source attached a + * `retrieveBulkData` to `WaveformData`, that method fetches the bytes, so the + * data source handles multipart/related, auth and relative bulkdata URIs. + * The bytes come from `WaveformData.Value` when the data source already + * cached them there. The decoded channels are kept, so a re-mount of the + * same display set does not fetch or decode again; a failed load is retried. + * * Returns null if the instance has no WaveformSequence. */ -export function buildEcgModule( - instance: any, - userAuthenticationService?: any -): EcgModule | null { - const waveformGroups = instance?.WaveformSequence; - if (!waveformGroups?.length) { +export function buildEcgModule(instance: any, userAuthenticationService?: any): EcgModule | null { + if (!instance?.WaveformSequence?.length) { return null; } - // Use the first (and typically only) multiplex group - const group = waveformGroups[0]; - - const numberOfChannels = group.NumberOfWaveformChannels ?? 0; - const numberOfSamples = group.NumberOfWaveformSamples ?? 0; - const samplingFrequency = group.SamplingFrequency ?? 1; - const bitsAllocated = group.WaveformBitsAllocated ?? 16; - const sampleInterpretation = group.WaveformSampleInterpretation ?? 'SS'; - const multiplexGroupLabel = group.MultiplexGroupLabel ?? ''; - - const channelDefinitionSequence = (group.ChannelDefinitionSequence ?? []).map(ch => ({ - channelSourceSequence: { - codeMeaning: - ch?.ChannelSourceSequence?.[0]?.CodeMeaning ?? - ch?.ChannelSourceSequence?.[0]?.codeMeaning ?? - '', - }, - })); - - const retrieveBulkData = async (): Promise => { - const waveformData = group.WaveformData; - - if (!waveformData) { - console.warn('[ECGViewport] No WaveformData found on instance'); - return []; - } - - let buffer: ArrayBuffer; - - if (waveformData.InlineBinary) { - buffer = base64ToArrayBuffer(waveformData.InlineBinary); - } else if (waveformData.BulkDataURI) { - const headers: Record = { - Accept: 'application/octet-stream', - }; - const authHeader = userAuthenticationService?.getAuthorizationHeader?.(); - if (authHeader) { - Object.assign(headers, authHeader); - } + const ecgModule = buildEcgModuleFromInstance(instance, instance.imageId, { + getHeaders: () => userAuthenticationService?.getAuthorizationHeader?.(), + }); + if (!ecgModule) { + return null; + } - const response = await fetch(waveformData.BulkDataURI, { headers }); - if (!response.ok) { - throw new Error( - `[ECGViewport] Failed to fetch waveform BulkDataURI: ${response.status}` - ); - } - buffer = await response.arrayBuffer(); - } else { - console.warn('[ECGViewport] WaveformData has no InlineBinary or BulkDataURI'); - return []; + const group = firstItem(instance.WaveformSequence); + const channelDefinitions = toList(group.ChannelDefinitionSequence); + // Works around a Cornerstone bug: buildEcgModuleFromInstance reads only an + // upper-case CodeMeaning, and drops CodeValue and CodingSchemeDesignator. + // No released Cornerstone viewport reads the lead code yet, so only the unit + // test covers it. + ecgModule.channelDefinitionSequence.forEach((definition, index) => { + const source = definition.channelSourceSequence as Record | undefined; + const raw = firstItem(channelDefinitions[index]?.ChannelSourceSequence); + if (!source || !raw) { + return; } + source.codeMeaning ||= raw.codeMeaning ?? raw.CodeMeaning ?? ''; + source.codeValue ??= raw.CodeValue ?? raw.codeValue; + source.codingSchemeDesignator ??= raw.CodingSchemeDesignator ?? raw.codingSchemeDesignator; + }); - return decodeInt16Multiplex(buffer, numberOfChannels, numberOfSamples); - }; + const waveformData = firstItem(group.WaveformData ?? group.waveformData); + if (typeof waveformData?.retrieveBulkData === 'function') { + let channels: Promise | undefined; + ecgModule.waveformData.retrieveBulkData = () => { + channels ??= decodeBulkData(group, waveformData).catch(error => { + channels = undefined; + throw error; + }); + return channels; + }; + } - return { - numberOfWaveformChannels: numberOfChannels, - numberOfWaveformSamples: numberOfSamples, - samplingFrequency, - waveformBitsAllocated: bitsAllocated, - waveformSampleInterpretation: sampleInterpretation, - multiplexGroupLabel, - channelDefinitionSequence, - waveformData: { retrieveBulkData }, - }; + return ecgModule; } diff --git a/extensions/default/src/DicomWebDataSource/index.ts b/extensions/default/src/DicomWebDataSource/index.ts index 9417c528d63..39cab405c5d 100644 --- a/extensions/default/src/DicomWebDataSource/index.ts +++ b/extensions/default/src/DicomWebDataSource/index.ts @@ -167,8 +167,12 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) { if (value && value.BulkDataURI && !value.Value) { // handle the scenarios where bulkDataURI is relative path fixBulkDataURI(value, instance, dicomWebConfig); - // Provide a method to fetch bulkdata - value.retrieveBulkData = retrieveBulkData.bind(dicomWebClient, value); + // Provide a method to fetch bulkdata. The client is shared, so set the + // current auth headers at fetch time, as the other ops here do. + value.retrieveBulkData = options => { + dicomWebClient.headers = getAuthorizationHeader(); + return retrieveBulkData.call(dicomWebClient, value, options); + }; } } return naturalized;