Skip to content

Commit 5cea7d1

Browse files
authored
fix: instance transfer syntax UID is required for decoding images (#108)
* fix: Missing variable name * fix: linting issues * fix: Undo format changes * fix: Instance TSUID is needed to decode images * fix: Tests had a couple of issues runnning
1 parent 214a9ab commit 5cea7d1

File tree

5 files changed

+94
-27
lines changed

5 files changed

+94
-27
lines changed

dcm4chee-docker-compose.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
version: "3"
21
services:
32
ldap:
43
image: dcm4che/slapd-dcm4chee:2.4.44-13.3

src/api.js

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { multipartEncode, multipartDecode } from './message.js';
2-
3-
1+
import { multipartEncode, multipartDecode, addHeaders } from './message.js';
42

53
function isObject(obj) {
64
return typeof obj === 'object' && obj !== null;
@@ -225,7 +223,7 @@ class DICOMwebClient {
225223
let requestInstance = request.instance ? request.instance : new XMLHttpRequest();
226224

227225
requestInstance.open(method, url, true);
228-
if ('responseType' in request) {
226+
if (request.responseType) {
229227
requestInstance.responseType = request.responseType;
230228
}
231229

@@ -256,12 +254,16 @@ class DICOMwebClient {
256254
requestInstance.onreadystatechange = () => {
257255
if (requestInstance.readyState === 4) {
258256
if (requestInstance.status === 200) {
259-
const contentType = requestInstance.getResponseHeader('Content-Type');
257+
const contentType = requestInstance.getResponseHeader(
258+
'Content-Type',
259+
);
260+
const headers = requestInstance.getAllResponseHeaders();
260261
// Automatically distinguishes between multipart and singlepart in an array buffer, and
261262
// converts them into a consistent type.
262263
if (contentType && contentType.indexOf('multipart') !== -1) {
263264
resolve(multipartDecode(requestInstance.response));
264265
} else if (requestInstance.responseType === 'arraybuffer') {
266+
addHeaders(requestInstance.response, headers);
265267
resolve([requestInstance.response]);
266268
} else {
267269
resolve(requestInstance.response);
@@ -295,10 +297,8 @@ class DICOMwebClient {
295297
};
296298

297299
// Event triggered while download progresses
298-
if ('progressCallback' in request) {
299-
if (typeof request.progressCallback === 'function') {
300-
requestInstance.onprogress = request.progressCallback;
301-
}
300+
if (typeof request.progressCallback === 'function') {
301+
requestInstance.onprogress = request.progressCallback;
302302
}
303303

304304
if (requestHooks && areValidRequestHooks(requestHooks)) {
@@ -311,13 +311,11 @@ class DICOMwebClient {
311311
}
312312

313313
// Add withCredentials to request if needed
314-
if ('withCredentials' in request) {
315-
if (request.withCredentials) {
316-
requestInstance.withCredentials = true;
317-
}
314+
if (request.withCredentials) {
315+
requestInstance.withCredentials = true;
318316
}
319317

320-
if ('data' in request) {
318+
if (request.data) {
321319
requestInstance.send(request.data);
322320
} else {
323321
requestInstance.send();
@@ -594,6 +592,7 @@ class DICOMwebClient {
594592
'image/gif',
595593
'image/png',
596594
'image/jp2',
595+
'image/*',
597596
];
598597
} else {
599598
supportedMediaTypes = {
@@ -608,6 +607,7 @@ class DICOMwebClient {
608607
'1.2.840.10008.1.2.4.91': ['image/jp2'],
609608
'1.2.840.10008.1.2.4.92': ['image/jpx'],
610609
'1.2.840.10008.1.2.4.93': ['image/jpx'],
610+
'*': ['image/*'],
611611
};
612612

613613
if (byteRange) {
@@ -961,7 +961,7 @@ class DICOMwebClient {
961961
});
962962

963963
if( !fieldValueParts.length ) {
964-
throw new Error(`No acceptable media types found among ${JSON.stringify(mediaTypes)}`);
964+
throw new Error(`No acceptable media types found among ${JSON.stringify(mediaTypes)} testing against ${JSON.stringify(acceptableMediaTypes)}`);
965965
}
966966

967967
return fieldValueParts.join(', ');
@@ -1227,7 +1227,7 @@ class DICOMwebClient {
12271227
debugLog(`retrieve metadata of instance ${options.sopInstanceUID}`);
12281228
const url = `${this.wadoURL}/studies/${options.studyInstanceUID}/series/${options.seriesInstanceUID}/instances/${options.sopInstanceUID}/metadata`;
12291229

1230-
const request = getRequestOptions(options.request)
1230+
const request = getRequestOptions(options.request);
12311231
return this._httpGetApplicationJson(url, {}, request);
12321232
}
12331233

@@ -1276,6 +1276,7 @@ class DICOMwebClient {
12761276
const { mediaTypes } = options;
12771277

12781278
const request = getRequestOptions(options.request)
1279+
request.responseType = 'arraybuffer';
12791280

12801281
if (!mediaTypes) {
12811282
return this._httpGetMultipartApplicationOctetStream(
@@ -1317,7 +1318,6 @@ class DICOMwebClient {
13171318
supportedMediaTypes,
13181319
),
13191320
};
1320-
request.responseType = 'arraybuffer';
13211321
return this._httpGet(url, headers, request);
13221322
}
13231323

src/message.js

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function identifyBoundary(header) {
3838
const parts = header.split('\r\n');
3939

4040
for (let i = 0; i < parts.length; i++) {
41-
if (parts[i].substr(0, 2) === '--') {
41+
if (parts[i].substring(0, 2) === '--') {
4242
return parts[i];
4343
}
4444
}
@@ -179,16 +179,72 @@ function multipartEncode(
179179
};
180180
}
181181

182+
/**
183+
* Splits the header string into parts and extracts the simple contentType
184+
* and transferSyntaxUID, assigning them, plus the headers map into the destination object.
185+
*
186+
* @param {*} destination
187+
* @param {string} headerString
188+
*/
189+
function addHeaders(destination, headerString) {
190+
if (!headerString) {
191+
return;
192+
}
193+
const headerLines = headerString.split('\r\n').filter(Boolean);
194+
const headers = new Map();
195+
let transferSyntaxUID = null,
196+
contentType = null;
197+
198+
for (const line of headerLines) {
199+
const colon = line.indexOf(':');
200+
if (colon === -1) {
201+
continue;
202+
}
203+
const name = line.substring(0, colon).toLowerCase();
204+
const value = line.substring(colon + 1).trim();
205+
if (headers.has(name)) {
206+
headers.get(name).push(value);
207+
} else {
208+
headers.set(name, [value]);
209+
}
210+
if (name === 'content-type') {
211+
const endSimpleType = value.indexOf(';');
212+
contentType = value.substring(
213+
0,
214+
endSimpleType === -1 ? value.length : endSimpleType,
215+
);
216+
const transferSyntaxStart = value.indexOf('transfer-syntax=');
217+
if (transferSyntaxStart !== -1) {
218+
const endTsuid = value.indexOf(';', transferSyntaxStart);
219+
transferSyntaxUID = value.substring(
220+
transferSyntaxStart + 16,
221+
endTsuid === -1 ? value.length : endTsuid,
222+
);
223+
}
224+
}
225+
}
226+
227+
Object.defineProperty(destination, 'headers', { value: headers });
228+
Object.defineProperty(destination, 'contentType', { value: contentType });
229+
Object.defineProperty(destination, 'transferSyntaxUID', {
230+
value: transferSyntaxUID,
231+
});
232+
}
233+
182234
/**
183235
* Decode a Multipart encoded ArrayBuffer and return the components as an Array.
184236
*
185237
* @param {ArrayBuffer} response Data encoded as a 'multipart/related' message
186-
* @returns {Array} The content
238+
* @returns {Uint8Array[]} The content as an array of Uint8Array
239+
* Each item shall have a contentType value, and a transferSyntaxUID if available,
240+
* as well as the headers Map. See parseHeaders for output.
241+
*
187242
*/
188243
function multipartDecode(response) {
189244
// Use the raw data if it is provided in an appropriate format
190-
const message = ArrayBuffer.isView(response) ? response : new Uint8Array(response);
191-
245+
const message = ArrayBuffer.isView(response)
246+
? response
247+
: new Uint8Array(response);
192248
/* Set a maximum length to search for the header boundaries, otherwise
193249
findToken can run for a long time
194250
*/
@@ -211,6 +267,8 @@ function multipartDecode(response) {
211267
const boundaryLength = boundary.length;
212268
const components = [];
213269

270+
const headers = header.substring(boundary.length + 2);
271+
214272
let offset = boundaryLength;
215273

216274
// Loop until we cannot find any more boundaries
@@ -240,6 +298,8 @@ function multipartDecode(response) {
240298
// Extract data from response message, excluding "\r\n"
241299
const spacingLength = 2;
242300
const data = response.slice(offset, boundaryIndex - spacingLength);
301+
// TODO - extract header data on a per frame basis.
302+
addHeaders(data, headers);
243303

244304
// Add the data to the array of results
245305
components.push(data);
@@ -261,4 +321,5 @@ export {
261321
multipartEncode,
262322
multipartDecode,
263323
guid,
324+
addHeaders,
264325
};

test/test.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ let dwc = new DICOMwebClient.api.DICOMwebClient({
2424
url: 'http://localhost:8008/dcm4chee-arc/aets/DCM4CHEE/rs',
2525
retrieveRendered: false,
2626
});
27+
2728
describe('dicomweb.api.DICOMwebClient', function() {
2829
//
2930
// Note: you can add the following for debugging tests locally
@@ -124,10 +125,17 @@ describe('dicomweb.api.DICOMwebClient', function() {
124125
sopInstanceUID:
125126
'1.3.6.1.4.1.14519.5.2.1.2744.7002.325971588264730726076978589153',
126127
frameNumbers: '1',
128+
// The next line should work, but the server side is broken
129+
// mediaTypes: [ {mediaType: 'image/*' }],
127130
};
128131

129-
const frames = dwc.retrieveInstance(options);
130-
});
132+
const frames = await dwc.retrieveInstanceFrames(options);
133+
expect(frames instanceof Array).toBe(true);
134+
expect(frames.length).toBe(1);
135+
expect(frames[0].contentType).toBe("application/octet-stream");
136+
// The next line is the correct value for servers supporting image/*
137+
//expect(frames[0].transferSyntaxUID).toBe('1.2.3');
138+
}, 15000);
131139

132140
it('should retrieve a single instance', async function() {
133141
// from sample.dcm
@@ -173,10 +181,9 @@ describe('dicomweb.api.DICOMwebClient', function() {
173181
const options = {
174182
studyInstanceUID: '999.999.3859744',
175183
seriesInstanceUID: '999.999.94827453',
176-
sopInstanceUID: '999.999.133.1996.1.1800.1.6.25',
177184
};
178185

179-
const metadata = await dwc.retrieveInstanceMetadata(options);
186+
const metadata = await dwc.retrieveSeriesMetadata(options);
180187

181188
// TODO: Check why metadata is an array of objects, not just an object
182189
const bulkDataOptions = {

test_ci.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Clear any previous data from the last test run
2-
rm -rf /tmp/dcm4chee-arc/db
2+
rm -rf ./tmp/dcm4chee-arc/db
33

44
# now start dcm4chee archive and wait for it to startup
55
echo 'Starting dcm4chee Docker container'

0 commit comments

Comments
 (0)