Retrieving and Caching Palette Colors on GetStudyMetadata request...
This commit is contained in:
parent
c27b1f2290
commit
fc648e38ab
@ -18,6 +18,7 @@ Package.onUse(function(api) {
|
||||
api.addFiles('server/DICOMWeb/getString.js', 'server');
|
||||
api.addFiles('server/DICOMWeb/getModalities.js', 'server');
|
||||
api.addFiles('server/DICOMWeb/getAttribute.js', 'server');
|
||||
api.addFiles('server/DICOMWeb/getBulkData.js', 'server');
|
||||
|
||||
api.export('DICOMWeb', 'server');
|
||||
|
||||
|
||||
193
Packages/ohif-dicom-services/server/DICOMWeb/getBulkData.js
Normal file
193
Packages/ohif-dicom-services/server/DICOMWeb/getBulkData.js
Normal file
@ -0,0 +1,193 @@
|
||||
|
||||
const ASCII = 'ascii';
|
||||
const http = Npm.require('http'), url = Npm.require('url');
|
||||
|
||||
function getMultipartContentInfo(headers) {
|
||||
|
||||
let dict = null,
|
||||
multipartRegex = /^\s*multipart\/[^;]+/g,
|
||||
contentType = headers['content-type'];
|
||||
|
||||
if (typeof contentType === 'string' && multipartRegex.test(contentType)) {
|
||||
let match,
|
||||
pairRegex = /;\s*([^=]+)=([^;\r\n]+)/g;
|
||||
pairRegex.lastIndex = multipartRegex.lastIndex;
|
||||
while ((match = pairRegex.exec(contentType)) !== null) {
|
||||
let key = match[1], value = match[2];
|
||||
if (dict === null) {
|
||||
dict = {};
|
||||
}
|
||||
dict[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return dict;
|
||||
|
||||
}
|
||||
|
||||
function parseContentHeader(data, offset) {
|
||||
|
||||
let endOfHeader = data.indexOf('\r\n\r\n', offset, ASCII);
|
||||
|
||||
if (endOfHeader < 0 || endOfHeader <= offset || endOfHeader > data.length) {
|
||||
throw {
|
||||
name: 'DICOMWebContentParsingError',
|
||||
message: 'End of content header cannot be determined...'
|
||||
};
|
||||
}
|
||||
|
||||
let match,
|
||||
header = {
|
||||
start: offset,
|
||||
end: endOfHeader + 4,
|
||||
fields: {}
|
||||
},
|
||||
headerRegex = /([\w\-]+):\s*([^\r\n]+)(?:\r\n)?/g,
|
||||
headerContentRegex = /\t([^\r\n]+)(?:\r\n)?/g,
|
||||
headerString = data.toString('ascii', offset, endOfHeader);
|
||||
|
||||
while ((match = headerRegex.exec(headerString)) !== null) {
|
||||
let key = match[1].toLowerCase(), value = match[2];
|
||||
while (headerString.charAt(headerRegex.lastIndex) === '\t') {
|
||||
headerContentRegex.lastIndex = headerRegex.lastIndex;
|
||||
if ((match = headerContentRegex.exec(headerString)) !== null) {
|
||||
headerRegex.lastIndex = headerContentRegex.lastIndex;
|
||||
value += ' ' + match[1];
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
header.fields[key] = value;
|
||||
}
|
||||
|
||||
return header;
|
||||
|
||||
}
|
||||
|
||||
function parseResponse(headers, data) {
|
||||
|
||||
let contentInfo = getMultipartContentInfo(headers);
|
||||
|
||||
if (!contentInfo || !contentInfo.boundary) {
|
||||
throw {
|
||||
name: 'DICOMWebContentParsingError',
|
||||
message: 'Content boundary not specified...'
|
||||
};
|
||||
}
|
||||
|
||||
let boundary = contentInfo.boundary,
|
||||
delimiter = '--' + boundary + '\r\n',
|
||||
index = data.indexOf(delimiter, 0, ASCII);
|
||||
|
||||
if (index < 0) {
|
||||
throw {
|
||||
name: 'DICOMWebContentParsingError',
|
||||
message: 'The specified boundary could not be found...'
|
||||
};
|
||||
}
|
||||
|
||||
let closingIndex, closingDelimiter = Buffer.from('\r\n--' + boundary + '--', ASCII);
|
||||
|
||||
index += delimiter.length;
|
||||
if (data[index] !== 0x0D || data[index + 1] !== 0x0A) {
|
||||
// multipart content headers are present, so let's parse them...
|
||||
let contentHeader = parseContentHeader(data, index);
|
||||
if (contentHeader && contentHeader.end > index) {
|
||||
if (contentHeader.fields['content-length'] > 0) {
|
||||
let endOfData = contentHeader.end + parseInt(contentHeader.fields['content-length'], 10);
|
||||
if (endOfData === data.indexOf(closingDelimiter, endOfData)) {
|
||||
// content-length is valid...
|
||||
return data.slice(contentHeader.end, endOfData);
|
||||
}
|
||||
}
|
||||
index = contentHeader.end;
|
||||
}
|
||||
} else {
|
||||
// no headers, the content comes right after...
|
||||
index += 2;
|
||||
}
|
||||
|
||||
closingIndex = data.indexOf(closingDelimiter, index);
|
||||
if (closingIndex < 0 || closingIndex < index) {
|
||||
throw {
|
||||
name: 'DICOMWebContentParsingError',
|
||||
message: 'The end of the content could not be determined...'
|
||||
};
|
||||
}
|
||||
|
||||
return data.slice(index, closingIndex);
|
||||
|
||||
}
|
||||
|
||||
function makeRequest(geturl, options, callback) {
|
||||
|
||||
let parsedUrl = url.parse(geturl);
|
||||
|
||||
let requestOpt = {
|
||||
hostname: parsedUrl.hostname,
|
||||
port: parsedUrl.port,
|
||||
headers: {
|
||||
Accept: 'multipart/related; type=application/octet-stream'
|
||||
},
|
||||
path: parsedUrl.path,
|
||||
method: 'GET'
|
||||
};
|
||||
|
||||
if (options.auth) {
|
||||
requestOpt.auth = options.auth;
|
||||
}
|
||||
|
||||
let req = http.request(requestOpt, function(resp) {
|
||||
|
||||
let data = [];
|
||||
|
||||
if (resp.statusCode !== 200) {
|
||||
callback({
|
||||
name: 'DICOMWebRequestError',
|
||||
message: `Unexpected status code for DICOMWeb response (${resp.statusCode})...`
|
||||
}, null);
|
||||
}
|
||||
|
||||
resp.on('data', function(chunk) {
|
||||
data.push(chunk);
|
||||
});
|
||||
|
||||
resp.on('end', function() {
|
||||
try {
|
||||
callback(null, parseResponse(resp.headers, Buffer.concat(data)));
|
||||
} catch (error) {
|
||||
callback(error, null);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
req.end();
|
||||
|
||||
}
|
||||
|
||||
const makeRequestSync = Meteor.wrapAsync(makeRequest);
|
||||
|
||||
DICOMWeb.getBulkData = function(geturl, options) {
|
||||
|
||||
if (options.logRequests) {
|
||||
console.log(geturl);
|
||||
}
|
||||
|
||||
if (options.logTiming) {
|
||||
console.time(geturl);
|
||||
}
|
||||
|
||||
var result = makeRequestSync(geturl, options);
|
||||
|
||||
if (options.logTiming) {
|
||||
console.timeEnd(geturl);
|
||||
}
|
||||
|
||||
if (options.logResponses) {
|
||||
console.log(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
};
|
||||
@ -1,5 +1,42 @@
|
||||
import { parseFloatArray } from '../../lib/parseFloatArray';
|
||||
|
||||
/**
|
||||
* Simple cache schema for retrieved color palettes.
|
||||
*/
|
||||
const paletteColorCache = {
|
||||
count: 0,
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24h cache?
|
||||
entries: {},
|
||||
isValidUID: function (paletteUID) {
|
||||
return typeof paletteUID === 'string' && paletteUID.length > 0;
|
||||
},
|
||||
get: function (paletteUID) {
|
||||
let entry = null;
|
||||
if (this.entries.hasOwnProperty(paletteUID)) {
|
||||
entry = this.entries[paletteUID];
|
||||
// check how the entry is...
|
||||
if ((Date.now() - entry.time) > this.maxAge) {
|
||||
// entry is too old... remove entry.
|
||||
delete this.entries[paletteUID];
|
||||
this.count--;
|
||||
entry = null;
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
},
|
||||
add: function (entry) {
|
||||
if (this.isValidUID(entry.uid)) {
|
||||
let paletteUID = entry.uid;
|
||||
if (this.entries.hasOwnProperty(paletteUID) !== true) {
|
||||
this.count++; // increment cache entry count...
|
||||
}
|
||||
entry.time = Date.now();
|
||||
this.entries[paletteUID] = entry;
|
||||
// @TODO: Add logic to get rid of old entries and reduce memory usage...
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a URL for a WADO search
|
||||
*
|
||||
@ -29,6 +66,51 @@ function getSourceImageInstanceUid(instance) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch palette colors for instances with "PALETTE COLOR" photometricInterpretation.
|
||||
*
|
||||
* @param server {Object} Current server;
|
||||
* @param instance {Object} The retrieved instance metadata;
|
||||
* @returns {String} The ReferenceSOPInstanceUID
|
||||
*/
|
||||
function getPaletteColors(server, instance) {
|
||||
|
||||
let entry = null,
|
||||
paletteUID = DICOMWeb.getString(instance['00281199']);
|
||||
|
||||
if (paletteColorCache.isValidUID(paletteUID)) {
|
||||
entry = paletteColorCache.get(paletteUID);
|
||||
} else {
|
||||
paletteUID = null;
|
||||
}
|
||||
|
||||
if (!entry) {
|
||||
// no entry on cache... Fetch remote data.
|
||||
try {
|
||||
let r, g, b;
|
||||
// Palettes are quite small (standard defines a limit of 64KiB but it's usually much less... like 512 ~ 1024 bytes)
|
||||
// ... so no problem if we store them using base64 encoding.
|
||||
r = DICOMWeb.getBulkData(instance['00281201'].BulkDataURI, server.requestOptions).toString('base64');
|
||||
g = DICOMWeb.getBulkData(instance['00281202'].BulkDataURI, server.requestOptions).toString('base64');
|
||||
b = DICOMWeb.getBulkData(instance['00281203'].BulkDataURI, server.requestOptions).toString('base64');
|
||||
entry = { red: r, green: g, blue: b };
|
||||
if (paletteUID !== null) {
|
||||
// when paletteUID is present, the entry can be cached...
|
||||
entry.uid = paletteUID;
|
||||
paletteColorCache.add(entry);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`(${error.name}) ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Parses result data from a WADO search into Study MetaData
|
||||
* Returns an object populated with study metadata, including the
|
||||
@ -125,13 +207,18 @@ function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
|
||||
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
|
||||
if (instanceSummary.photometricInterpretation === 'PALETTE COLOR') {
|
||||
instanceSummary.paletteColorLookupTableUID = DICOMWeb.getString(instance['00281199']);
|
||||
instanceSummary.redPaletteColorLookupTableDescriptor = DICOMWeb.getString(instance['00281101']);
|
||||
instanceSummary.greenPaletteColorLookupTableDescriptor = DICOMWeb.getString(instance['00281102']);
|
||||
instanceSummary.bluePaletteColorLookupTableDescriptor = DICOMWeb.getString(instance['00281103']);
|
||||
instanceSummary.redPaletteColorLookupTableData = instance['00281201'];
|
||||
instanceSummary.greenPaletteColorLookupTableData = instance['00281202'];
|
||||
instanceSummary.bluePaletteColorLookupTableData = instance['00281203'];
|
||||
let palettes = getPaletteColors(server, instance);
|
||||
if (palettes) {
|
||||
if (palettes.uid) {
|
||||
instanceSummary.paletteColorLookupTableUID = palettes.uid;
|
||||
}
|
||||
instanceSummary.redPaletteColorLookupTable = palettes.red;
|
||||
instanceSummary.greenPaletteColorLookupTable = palettes.green;
|
||||
instanceSummary.bluePaletteColorLookupTable = palettes.blue;
|
||||
instanceSummary.redPaletteColorLookupTableDescriptor = DICOMWeb.getString(instance['00281101']);
|
||||
instanceSummary.greenPaletteColorLookupTableDescriptor = DICOMWeb.getString(instance['00281102']);
|
||||
instanceSummary.bluePaletteColorLookupTableDescriptor = DICOMWeb.getString(instance['00281103']);
|
||||
}
|
||||
}
|
||||
|
||||
if (server.imageRendering === 'wadouri') {
|
||||
@ -142,6 +229,7 @@ function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
}
|
||||
|
||||
series.instances.push(instanceSummary);
|
||||
|
||||
});
|
||||
|
||||
return studyData;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user