cod-dicomweb-server 1.2.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/cjs/1104a37b16dee0d2ada1.ts +14 -0
- package/dist/cjs/7d4e5892d21def245792.ts +14 -0
- package/dist/cjs/main.js +1957 -0
- package/dist/{types → esm}/classes/CodDicomWebServer.d.ts +2 -1
- package/dist/esm/classes/CodDicomWebServer.js +323 -0
- package/dist/esm/classes/customClasses.d.ts +19 -0
- package/dist/esm/classes/customClasses.js +13 -0
- package/dist/esm/classes/index.d.ts +2 -0
- package/dist/esm/classes/index.js +2 -0
- package/dist/esm/classes/utils.js +102 -0
- package/dist/esm/constants/dataRetrieval.js +3 -0
- package/dist/{types → esm}/constants/enums.d.ts +4 -0
- package/dist/esm/constants/enums.js +24 -0
- package/dist/{types → esm}/constants/index.d.ts +3 -3
- package/dist/esm/constants/index.js +6 -0
- package/dist/esm/constants/url.js +3 -0
- package/dist/esm/dataRetrieval/dataRetrievalManager.d.ts +17 -0
- package/dist/esm/dataRetrieval/dataRetrievalManager.js +54 -0
- package/dist/esm/dataRetrieval/register.d.ts +4 -0
- package/dist/esm/dataRetrieval/register.js +25 -0
- package/dist/esm/dataRetrieval/requestManager.d.ts +12 -0
- package/dist/esm/dataRetrieval/requestManager.js +65 -0
- package/dist/esm/dataRetrieval/scripts/filePartial.d.ts +18 -0
- package/dist/esm/dataRetrieval/scripts/filePartial.js +16 -0
- package/dist/{types/webWorker → esm/dataRetrieval}/scripts/fileStreaming.d.ts +7 -1
- package/dist/esm/dataRetrieval/scripts/fileStreaming.js +93 -0
- package/dist/esm/dataRetrieval/utils/environment.d.ts +1 -0
- package/dist/esm/dataRetrieval/utils/environment.js +3 -0
- package/dist/esm/dataRetrieval/workerManager.d.ts +10 -0
- package/dist/esm/dataRetrieval/workerManager.js +55 -0
- package/dist/esm/dataRetrieval/workers/filePartialWorker.js +7 -0
- package/dist/esm/dataRetrieval/workers/fileStreamingWorker.js +7 -0
- package/dist/{types → esm}/fileManager.d.ts +2 -2
- package/dist/esm/fileManager.js +52 -0
- package/dist/{types → esm}/index.d.ts +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/{types → esm}/metadataManager.d.ts +1 -0
- package/dist/esm/metadataManager.js +47 -0
- package/dist/esm/types/codDicomWebServerOptions.js +1 -0
- package/dist/{types → esm}/types/fileManagerOptions.d.ts +1 -1
- package/dist/esm/types/fileManagerOptions.js +1 -0
- package/dist/{types → esm}/types/index.d.ts +1 -1
- package/dist/esm/types/index.js +7 -0
- package/dist/{types → esm}/types/metadata.d.ts +1 -1
- package/dist/esm/types/metadata.js +1 -0
- package/dist/esm/types/metadataUrlCreationParams.js +1 -0
- package/dist/esm/types/parsedWadoRsUrlDetails.js +1 -0
- package/dist/esm/types/requestOptions.js +1 -0
- package/dist/esm/types/scriptObject.d.ts +4 -0
- package/dist/esm/types/scriptObject.js +1 -0
- package/dist/umd/614.js +19 -0
- package/dist/umd/614.js.map +1 -0
- package/dist/umd/66.js +19 -0
- package/dist/umd/66.js.map +1 -0
- package/dist/umd/main.js +21 -0
- package/dist/umd/main.js.map +1 -0
- package/package.json +18 -6
- package/dist/16.js +0 -19
- package/dist/16.js.map +0 -1
- package/dist/170.js +0 -19
- package/dist/170.js.map +0 -1
- package/dist/main.js +0 -21
- package/dist/main.js.map +0 -1
- package/dist/types/types/workerCustomMessageEvents.d.ts +0 -10
- package/dist/types/webWorker/registerWorkers.d.ts +0 -4
- package/dist/types/webWorker/scripts/filePartial.d.ts +0 -7
- package/dist/types/webWorker/workerManager.d.ts +0 -10
- /package/dist/{types → esm}/classes/utils.d.ts +0 -0
- /package/dist/{types/constants/worker.d.ts → esm/constants/dataRetrieval.d.ts} +0 -0
- /package/dist/{types → esm}/constants/url.d.ts +0 -0
- /package/dist/{types/webWorker → esm/dataRetrieval}/workers/filePartialWorker.d.ts +0 -0
- /package/dist/{types/webWorker → esm/dataRetrieval}/workers/fileStreamingWorker.d.ts +0 -0
- /package/dist/{types → esm}/types/codDicomWebServerOptions.d.ts +0 -0
- /package/dist/{types → esm}/types/metadataUrlCreationParams.d.ts +0 -0
- /package/dist/{types → esm}/types/parsedWadoRsUrlDetails.d.ts +0 -0
- /package/dist/{types → esm}/types/requestOptions.d.ts +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Enums } from '../constants';
|
|
2
|
-
import type { CodDicomWebServerOptions,
|
|
2
|
+
import type { CodDicomWebServerOptions, CODRequestOptions, FileRequestOptions, InstanceMetadata, JsonMetadata, SeriesMetadata } from '../types';
|
|
3
3
|
declare class CodDicomWebServer {
|
|
4
4
|
private filePromises;
|
|
5
5
|
private options;
|
|
@@ -9,6 +9,7 @@ declare class CodDicomWebServer {
|
|
|
9
9
|
constructor(args?: {
|
|
10
10
|
maxWorkerFetchSize?: number;
|
|
11
11
|
domain?: string;
|
|
12
|
+
disableWorker?: boolean;
|
|
12
13
|
});
|
|
13
14
|
setOptions: (newOptions: Partial<CodDicomWebServerOptions>) => void;
|
|
14
15
|
getOptions: () => CodDicomWebServerOptions;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { parseDicom } from 'dicom-parser';
|
|
2
|
+
import FileManager from '../fileManager';
|
|
3
|
+
import MetadataManager from '../metadataManager';
|
|
4
|
+
import { getFrameDetailsFromMetadata, parseWadorsURL } from './utils';
|
|
5
|
+
import { register } from '../dataRetrieval/register';
|
|
6
|
+
import constants, { Enums } from '../constants';
|
|
7
|
+
import { getDataRetrievalManager } from '../dataRetrieval/dataRetrievalManager';
|
|
8
|
+
import { CustomError } from './customClasses';
|
|
9
|
+
import { CustomErrorEvent } from './customClasses';
|
|
10
|
+
class CodDicomWebServer {
|
|
11
|
+
filePromises = {};
|
|
12
|
+
options = {
|
|
13
|
+
maxWorkerFetchSize: Infinity,
|
|
14
|
+
domain: constants.url.DOMAIN
|
|
15
|
+
};
|
|
16
|
+
fileManager;
|
|
17
|
+
metadataManager;
|
|
18
|
+
seriesUidFileUrls = {};
|
|
19
|
+
constructor(args = {}) {
|
|
20
|
+
const { maxWorkerFetchSize, domain, disableWorker } = args;
|
|
21
|
+
this.options.maxWorkerFetchSize = maxWorkerFetchSize || this.options.maxWorkerFetchSize;
|
|
22
|
+
this.options.domain = domain || this.options.domain;
|
|
23
|
+
const fileStreamingScriptName = constants.dataRetrieval.FILE_STREAMING_WORKER_NAME;
|
|
24
|
+
const filePartialScriptName = constants.dataRetrieval.FILE_PARTIAL_WORKER_NAME;
|
|
25
|
+
this.fileManager = new FileManager({ fileStreamingScriptName });
|
|
26
|
+
this.metadataManager = new MetadataManager();
|
|
27
|
+
if (disableWorker) {
|
|
28
|
+
const dataRetrievalManager = getDataRetrievalManager();
|
|
29
|
+
dataRetrievalManager.setDataRetrieverMode(Enums.DataRetrieveMode.REQUEST);
|
|
30
|
+
}
|
|
31
|
+
register({ fileStreamingScriptName, filePartialScriptName }, this.options.maxWorkerFetchSize);
|
|
32
|
+
}
|
|
33
|
+
setOptions = (newOptions) => {
|
|
34
|
+
Object.keys(newOptions).forEach((key) => {
|
|
35
|
+
if (newOptions[key] !== undefined) {
|
|
36
|
+
this.options[key] = newOptions[key];
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
getOptions = () => {
|
|
41
|
+
return this.options;
|
|
42
|
+
};
|
|
43
|
+
addFileUrl(seriesInstanceUID, url) {
|
|
44
|
+
if (this.seriesUidFileUrls[seriesInstanceUID]) {
|
|
45
|
+
this.seriesUidFileUrls[seriesInstanceUID].push(url);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
this.seriesUidFileUrls[seriesInstanceUID] = [url];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async fetchCod(wadorsUrl, headers = {}, { useSharedArrayBuffer = false, fetchType = constants.Enums.FetchType.API_OPTIMIZED } = {}) {
|
|
52
|
+
try {
|
|
53
|
+
if (!wadorsUrl) {
|
|
54
|
+
throw new CustomError('Url not provided');
|
|
55
|
+
}
|
|
56
|
+
const parsedDetails = parseWadorsURL(wadorsUrl, this.options.domain);
|
|
57
|
+
if (parsedDetails) {
|
|
58
|
+
const { type, bucketName, bucketPrefix, studyInstanceUID, seriesInstanceUID, sopInstanceUID, frameNumber } = parsedDetails;
|
|
59
|
+
const metadataJson = await this.metadataManager.getMetadata({
|
|
60
|
+
domain: this.options.domain,
|
|
61
|
+
bucketName,
|
|
62
|
+
bucketPrefix,
|
|
63
|
+
studyInstanceUID,
|
|
64
|
+
seriesInstanceUID
|
|
65
|
+
}, headers);
|
|
66
|
+
if (!metadataJson) {
|
|
67
|
+
throw new CustomError(`Metadata not found for ${wadorsUrl}`);
|
|
68
|
+
}
|
|
69
|
+
const { url: fileUrl, startByte, endByte, thumbnailUrl, isMultiframe } = getFrameDetailsFromMetadata(metadataJson, sopInstanceUID, frameNumber - 1, {
|
|
70
|
+
domain: this.options.domain,
|
|
71
|
+
bucketName,
|
|
72
|
+
bucketPrefix
|
|
73
|
+
});
|
|
74
|
+
switch (type) {
|
|
75
|
+
case Enums.RequestType.THUMBNAIL:
|
|
76
|
+
if (!thumbnailUrl) {
|
|
77
|
+
throw new CustomError(`Thumbnail not found for ${wadorsUrl}`);
|
|
78
|
+
}
|
|
79
|
+
this.addFileUrl(seriesInstanceUID, thumbnailUrl);
|
|
80
|
+
return this.fetchFile(thumbnailUrl, headers, {
|
|
81
|
+
useSharedArrayBuffer
|
|
82
|
+
});
|
|
83
|
+
case Enums.RequestType.FRAME: {
|
|
84
|
+
if (!fileUrl) {
|
|
85
|
+
throw new CustomError('Url not found for frame');
|
|
86
|
+
}
|
|
87
|
+
let urlWithBytes = fileUrl;
|
|
88
|
+
if (fetchType === Enums.FetchType.BYTES_OPTIMIZED) {
|
|
89
|
+
urlWithBytes = `${fileUrl}?bytes=${startByte}-${endByte}`;
|
|
90
|
+
}
|
|
91
|
+
this.addFileUrl(seriesInstanceUID, fileUrl);
|
|
92
|
+
return this.fetchFile(urlWithBytes, headers, {
|
|
93
|
+
offsets: { startByte, endByte },
|
|
94
|
+
useSharedArrayBuffer,
|
|
95
|
+
fetchType
|
|
96
|
+
}).then((arraybuffer) => {
|
|
97
|
+
if (!arraybuffer?.byteLength) {
|
|
98
|
+
throw new CustomError('File Arraybuffer is not found');
|
|
99
|
+
}
|
|
100
|
+
if (isMultiframe) {
|
|
101
|
+
return arraybuffer;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const dataSet = parseDicom(new Uint8Array(arraybuffer));
|
|
105
|
+
const pixelDataElement = dataSet.elements.x7fe00010;
|
|
106
|
+
let { dataOffset, length } = pixelDataElement;
|
|
107
|
+
if (pixelDataElement.hadUndefinedLength && pixelDataElement.fragments) {
|
|
108
|
+
({ position: dataOffset, length } = pixelDataElement.fragments[0]);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Adding 8 bytes for 4 bytes tag + 4 bytes length for uncomppressed pixelData
|
|
112
|
+
dataOffset += 8;
|
|
113
|
+
}
|
|
114
|
+
return arraybuffer.slice(dataOffset, dataOffset + length);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
case Enums.RequestType.SERIES_METADATA:
|
|
119
|
+
case Enums.RequestType.INSTANCE_METADATA:
|
|
120
|
+
return this.parseMetadata(metadataJson, type, sopInstanceUID);
|
|
121
|
+
default:
|
|
122
|
+
throw new CustomError(`Unsupported request type: ${type}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
return this.fetchFile(wadorsUrl, headers, { useSharedArrayBuffer })
|
|
128
|
+
.then((result) => {
|
|
129
|
+
if (result instanceof ArrayBuffer) {
|
|
130
|
+
try {
|
|
131
|
+
const dataSet = parseDicom(new Uint8Array(result));
|
|
132
|
+
const seriesInstanceUID = dataSet.string('0020000e');
|
|
133
|
+
if (seriesInstanceUID) {
|
|
134
|
+
this.addFileUrl(seriesInstanceUID, wadorsUrl);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
console.warn('CodDicomWebServer.ts: There is some issue parsing the file.', error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
resolve(result);
|
|
142
|
+
})
|
|
143
|
+
.catch((error) => reject(error));
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const newError = new CustomError(`CodDicomWebServer.ts: ${error.message || 'An error occured when fetching the COD'}`);
|
|
149
|
+
console.error(newError);
|
|
150
|
+
throw newError;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async fetchFile(fileUrl, headers, { offsets, useSharedArrayBuffer = false, fetchType = constants.Enums.FetchType.API_OPTIMIZED } = {}) {
|
|
154
|
+
const isBytesOptimized = fetchType === Enums.FetchType.BYTES_OPTIMIZED;
|
|
155
|
+
const extractedFile = this.fileManager.get(fileUrl, isBytesOptimized ? undefined : offsets);
|
|
156
|
+
if (extractedFile) {
|
|
157
|
+
return new Promise((resolveRequest, rejectRequest) => {
|
|
158
|
+
try {
|
|
159
|
+
resolveRequest(extractedFile.buffer);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
rejectRequest(error);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const { maxWorkerFetchSize } = this.getOptions();
|
|
167
|
+
const dataRetrievalManager = getDataRetrievalManager();
|
|
168
|
+
const { FILE_STREAMING_WORKER_NAME, FILE_PARTIAL_WORKER_NAME, THRESHOLD } = constants.dataRetrieval;
|
|
169
|
+
let tarPromise;
|
|
170
|
+
if (!this.filePromises[fileUrl]) {
|
|
171
|
+
tarPromise = new Promise((resolveFile, rejectFile) => {
|
|
172
|
+
if (this.fileManager.getTotalSize() + THRESHOLD > maxWorkerFetchSize) {
|
|
173
|
+
throw new CustomError(`CodDicomWebServer.ts: Maximum size(${maxWorkerFetchSize}) for fetching files reached`);
|
|
174
|
+
}
|
|
175
|
+
const FetchTypeEnum = constants.Enums.FetchType;
|
|
176
|
+
if (fetchType === FetchTypeEnum.API_OPTIMIZED) {
|
|
177
|
+
const handleFirstChunk = (evt) => {
|
|
178
|
+
if (evt instanceof CustomErrorEvent) {
|
|
179
|
+
rejectFile(evt.error);
|
|
180
|
+
throw evt.error;
|
|
181
|
+
}
|
|
182
|
+
const { url, position, fileArraybuffer } = evt.data;
|
|
183
|
+
if (url === fileUrl && fileArraybuffer) {
|
|
184
|
+
this.fileManager.set(url, { data: fileArraybuffer, position });
|
|
185
|
+
dataRetrievalManager.removeEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleFirstChunk);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
dataRetrievalManager.addEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleFirstChunk);
|
|
189
|
+
dataRetrievalManager
|
|
190
|
+
.executeTask(FILE_STREAMING_WORKER_NAME, 'stream', {
|
|
191
|
+
url: fileUrl,
|
|
192
|
+
headers: headers,
|
|
193
|
+
useSharedArrayBuffer
|
|
194
|
+
})
|
|
195
|
+
.then(() => {
|
|
196
|
+
resolveFile();
|
|
197
|
+
})
|
|
198
|
+
.catch((error) => {
|
|
199
|
+
rejectFile(error);
|
|
200
|
+
})
|
|
201
|
+
.then(() => {
|
|
202
|
+
dataRetrievalManager.removeEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleFirstChunk);
|
|
203
|
+
delete this.filePromises[fileUrl];
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
else if (fetchType === FetchTypeEnum.BYTES_OPTIMIZED && offsets) {
|
|
207
|
+
const { startByte, endByte } = offsets;
|
|
208
|
+
const bytesRemovedUrl = fileUrl.split('?bytes=')[0];
|
|
209
|
+
const handleSlice = (evt) => {
|
|
210
|
+
if (evt instanceof CustomErrorEvent) {
|
|
211
|
+
rejectFile(evt.error);
|
|
212
|
+
throw evt.error;
|
|
213
|
+
}
|
|
214
|
+
const { url, fileArraybuffer, offsets } = evt.data;
|
|
215
|
+
if (url === bytesRemovedUrl && offsets.startByte === startByte && offsets.endByte === endByte) {
|
|
216
|
+
this.fileManager.set(fileUrl, { data: fileArraybuffer, position: fileArraybuffer.length });
|
|
217
|
+
dataRetrievalManager.removeEventListener(FILE_PARTIAL_WORKER_NAME, 'message', handleSlice);
|
|
218
|
+
resolveFile();
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
dataRetrievalManager.addEventListener(FILE_PARTIAL_WORKER_NAME, 'message', handleSlice);
|
|
222
|
+
dataRetrievalManager
|
|
223
|
+
.executeTask(FILE_PARTIAL_WORKER_NAME, 'partial', {
|
|
224
|
+
url: bytesRemovedUrl,
|
|
225
|
+
offsets: { startByte, endByte },
|
|
226
|
+
headers,
|
|
227
|
+
useSharedArrayBuffer
|
|
228
|
+
})
|
|
229
|
+
.catch((error) => {
|
|
230
|
+
rejectFile(error);
|
|
231
|
+
})
|
|
232
|
+
.then(() => {
|
|
233
|
+
dataRetrievalManager.removeEventListener(FILE_PARTIAL_WORKER_NAME, 'message', handleSlice);
|
|
234
|
+
delete this.filePromises[fileUrl];
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
rejectFile(new CustomError('CodDicomWebServer.ts: Offsets is needed in bytes optimized fetching'));
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
this.filePromises[fileUrl] = tarPromise;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
tarPromise = this.filePromises[fileUrl];
|
|
245
|
+
}
|
|
246
|
+
return new Promise((resolveRequest, rejectRequest) => {
|
|
247
|
+
let requestResolved = false;
|
|
248
|
+
const handleChunkAppend = (evt) => {
|
|
249
|
+
if (evt instanceof CustomErrorEvent) {
|
|
250
|
+
rejectRequest(evt.message);
|
|
251
|
+
throw evt.error;
|
|
252
|
+
}
|
|
253
|
+
const { url, position, chunk, isAppending } = evt.data;
|
|
254
|
+
if (isAppending) {
|
|
255
|
+
if (chunk) {
|
|
256
|
+
this.fileManager.append(url, chunk, position);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
this.fileManager.setPosition(url, position);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (!requestResolved && url === fileUrl && offsets && position > offsets.endByte) {
|
|
263
|
+
try {
|
|
264
|
+
const file = this.fileManager.get(url, offsets);
|
|
265
|
+
requestResolved = true;
|
|
266
|
+
resolveRequest(file?.buffer);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
rejectRequest(error);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
if (offsets && !isBytesOptimized) {
|
|
274
|
+
dataRetrievalManager.addEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleChunkAppend);
|
|
275
|
+
}
|
|
276
|
+
tarPromise
|
|
277
|
+
.then(() => {
|
|
278
|
+
if (!requestResolved) {
|
|
279
|
+
if (this.fileManager.getPosition(fileUrl)) {
|
|
280
|
+
const file = this.fileManager.get(fileUrl, isBytesOptimized ? undefined : offsets);
|
|
281
|
+
requestResolved = true;
|
|
282
|
+
resolveRequest(file?.buffer);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
rejectRequest(new CustomError(`File - ${fileUrl} not found`));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
.catch((error) => {
|
|
290
|
+
rejectRequest(error);
|
|
291
|
+
})
|
|
292
|
+
.then(() => {
|
|
293
|
+
dataRetrievalManager.removeEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleChunkAppend);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
delete(seriesInstanceUID) {
|
|
298
|
+
const fileUrls = this.seriesUidFileUrls[seriesInstanceUID];
|
|
299
|
+
if (fileUrls) {
|
|
300
|
+
fileUrls.forEach((fileUrl) => {
|
|
301
|
+
this.fileManager.remove(fileUrl);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
delete this.seriesUidFileUrls[seriesInstanceUID];
|
|
305
|
+
}
|
|
306
|
+
deleteAll() {
|
|
307
|
+
Object.values(this.seriesUidFileUrls).forEach((fileUrls) => {
|
|
308
|
+
fileUrls.forEach((fileUrl) => {
|
|
309
|
+
this.fileManager.remove(fileUrl);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
this.seriesUidFileUrls = {};
|
|
313
|
+
}
|
|
314
|
+
parseMetadata(metadata, type, sopInstanceUID) {
|
|
315
|
+
if (type === Enums.RequestType.INSTANCE_METADATA) {
|
|
316
|
+
return Object.values(metadata.cod.instances).find((instance) => instance.metadata['00080018']?.Value?.[0] === sopInstanceUID)?.metadata;
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
return Object.values(metadata.cod.instances).map((instance) => instance.metadata);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
export default CodDicomWebServer;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare class CustomError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export declare class CustomErrorEvent extends Event {
|
|
4
|
+
error: CustomError;
|
|
5
|
+
message: string;
|
|
6
|
+
constructor(message: string, error: CustomError);
|
|
7
|
+
}
|
|
8
|
+
export declare class CustomMessageEvent extends MessageEvent<{
|
|
9
|
+
url: string;
|
|
10
|
+
position: number;
|
|
11
|
+
chunk?: Uint8Array;
|
|
12
|
+
isAppending?: boolean;
|
|
13
|
+
fileArraybuffer?: Uint8Array;
|
|
14
|
+
offsets?: {
|
|
15
|
+
startByte: number;
|
|
16
|
+
endByte: number;
|
|
17
|
+
};
|
|
18
|
+
}> {
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class CustomError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export class CustomErrorEvent extends Event {
|
|
4
|
+
error;
|
|
5
|
+
message;
|
|
6
|
+
constructor(message, error) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.message = message;
|
|
9
|
+
this.error = error;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class CustomMessageEvent extends MessageEvent {
|
|
13
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import constants, { Enums } from '../constants';
|
|
2
|
+
import { CustomError } from './customClasses';
|
|
3
|
+
export function parseWadorsURL(url, domain) {
|
|
4
|
+
if (!url.includes(constants.url.URL_VALIDATION_STRING)) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const filePath = url.split(domain + '/')[1];
|
|
8
|
+
const prefix = filePath.split('/studies')[0];
|
|
9
|
+
const prefixParts = prefix.split('/');
|
|
10
|
+
const bucketName = prefixParts[0];
|
|
11
|
+
const bucketPrefix = prefixParts.slice(1).join('/');
|
|
12
|
+
const imagePath = filePath.split(prefix + '/')[1];
|
|
13
|
+
const imageParts = imagePath.split('/');
|
|
14
|
+
const studyInstanceUID = imageParts[1];
|
|
15
|
+
const seriesInstanceUID = imageParts[3];
|
|
16
|
+
let sopInstanceUID = '', frameNumber = 1, type;
|
|
17
|
+
switch (true) {
|
|
18
|
+
case imageParts.includes('thumbnail'):
|
|
19
|
+
type = Enums.RequestType.THUMBNAIL;
|
|
20
|
+
break;
|
|
21
|
+
case imageParts.includes('metadata'):
|
|
22
|
+
if (imageParts.includes('instances')) {
|
|
23
|
+
sopInstanceUID = imageParts[5];
|
|
24
|
+
type = Enums.RequestType.INSTANCE_METADATA;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
type = Enums.RequestType.SERIES_METADATA;
|
|
28
|
+
}
|
|
29
|
+
break;
|
|
30
|
+
case imageParts.includes('frames'):
|
|
31
|
+
sopInstanceUID = imageParts[5];
|
|
32
|
+
frameNumber = +imageParts[7];
|
|
33
|
+
type = Enums.RequestType.FRAME;
|
|
34
|
+
break;
|
|
35
|
+
default:
|
|
36
|
+
throw new CustomError('Invalid type of request');
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
type,
|
|
40
|
+
bucketName,
|
|
41
|
+
bucketPrefix,
|
|
42
|
+
studyInstanceUID,
|
|
43
|
+
seriesInstanceUID,
|
|
44
|
+
sopInstanceUID,
|
|
45
|
+
frameNumber
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function getFrameDetailsFromMetadata(seriesMetadata, sopInstanceUID, frameIndex, bucketDetails) {
|
|
49
|
+
if (!seriesMetadata || !seriesMetadata.cod?.instances) {
|
|
50
|
+
throw new CustomError('Invalid seriesMetadata provided.');
|
|
51
|
+
}
|
|
52
|
+
if (frameIndex === null || frameIndex === undefined) {
|
|
53
|
+
throw new CustomError('Frame index is required.');
|
|
54
|
+
}
|
|
55
|
+
const { domain, bucketName, bucketPrefix } = bucketDetails;
|
|
56
|
+
let thumbnailUrl;
|
|
57
|
+
if (seriesMetadata.thumbnail) {
|
|
58
|
+
const thumbnailGsUtilUri = seriesMetadata.thumbnail.uri;
|
|
59
|
+
thumbnailUrl = `${domain}/${thumbnailGsUtilUri.split('gs://')[1]}`;
|
|
60
|
+
}
|
|
61
|
+
const instanceFound = Object.values(seriesMetadata.cod.instances).find((instance) => instance.metadata['00080018']?.Value?.[0] === sopInstanceUID);
|
|
62
|
+
if (!instanceFound) {
|
|
63
|
+
return { thumbnailUrl };
|
|
64
|
+
}
|
|
65
|
+
const { url, uri, headers: offsetHeaders, offset_tables } = instanceFound;
|
|
66
|
+
const modifiedUrl = handleUrl(url || uri, domain, bucketName, bucketPrefix);
|
|
67
|
+
const { CustomOffsetTable, CustomOffsetTableLengths } = offset_tables;
|
|
68
|
+
let sliceStart, sliceEnd, isMultiframe = false;
|
|
69
|
+
if (CustomOffsetTable?.length && CustomOffsetTableLengths?.length) {
|
|
70
|
+
sliceStart = CustomOffsetTable[frameIndex];
|
|
71
|
+
sliceEnd = sliceStart + CustomOffsetTableLengths[frameIndex];
|
|
72
|
+
isMultiframe = true;
|
|
73
|
+
}
|
|
74
|
+
const { start_byte: fileStartByte, end_byte: fileEndByte } = offsetHeaders;
|
|
75
|
+
const startByte = sliceStart !== undefined ? fileStartByte + sliceStart : fileStartByte;
|
|
76
|
+
const endByte = sliceEnd !== undefined ? fileStartByte + sliceEnd : fileEndByte;
|
|
77
|
+
return {
|
|
78
|
+
url: modifiedUrl,
|
|
79
|
+
startByte,
|
|
80
|
+
endByte,
|
|
81
|
+
thumbnailUrl,
|
|
82
|
+
isMultiframe
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function handleUrl(url, domain, bucketName, bucketPrefix) {
|
|
86
|
+
let modifiedUrl = url;
|
|
87
|
+
const matchingExtension = constants.url.FILE_EXTENSIONS.find((extension) => url.includes(extension));
|
|
88
|
+
if (matchingExtension) {
|
|
89
|
+
const fileParts = url.split(matchingExtension);
|
|
90
|
+
modifiedUrl = fileParts[0] + matchingExtension;
|
|
91
|
+
}
|
|
92
|
+
const filePath = modifiedUrl.split('studies/')[1];
|
|
93
|
+
modifiedUrl = `${domain}/${bucketName}/${bucketPrefix ? bucketPrefix + '/' : ''}studies/${filePath}`;
|
|
94
|
+
return modifiedUrl;
|
|
95
|
+
}
|
|
96
|
+
export function createMetadataJsonUrl(params) {
|
|
97
|
+
const { domain = constants.url.DOMAIN, bucketName, bucketPrefix, studyInstanceUID, seriesInstanceUID } = params;
|
|
98
|
+
if (!bucketName || !bucketPrefix || !studyInstanceUID || !seriesInstanceUID) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
return `${domain}/${bucketName}/${bucketPrefix}/studies/${studyInstanceUID}/series/${seriesInstanceUID}/metadata.json`;
|
|
102
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export var FetchType;
|
|
2
|
+
(function (FetchType) {
|
|
3
|
+
/**
|
|
4
|
+
* Fetch only the part of the file according to the offsets provided.
|
|
5
|
+
*/
|
|
6
|
+
FetchType[FetchType["BYTES_OPTIMIZED"] = 0] = "BYTES_OPTIMIZED";
|
|
7
|
+
/**
|
|
8
|
+
* Stream the file and returns the part of the file if offsets are provided.
|
|
9
|
+
* Or returns the whole file.
|
|
10
|
+
*/
|
|
11
|
+
FetchType[FetchType["API_OPTIMIZED"] = 1] = "API_OPTIMIZED";
|
|
12
|
+
})(FetchType || (FetchType = {}));
|
|
13
|
+
export var RequestType;
|
|
14
|
+
(function (RequestType) {
|
|
15
|
+
RequestType[RequestType["FRAME"] = 0] = "FRAME";
|
|
16
|
+
RequestType[RequestType["THUMBNAIL"] = 1] = "THUMBNAIL";
|
|
17
|
+
RequestType[RequestType["SERIES_METADATA"] = 2] = "SERIES_METADATA";
|
|
18
|
+
RequestType[RequestType["INSTANCE_METADATA"] = 3] = "INSTANCE_METADATA";
|
|
19
|
+
})(RequestType || (RequestType = {}));
|
|
20
|
+
export var DataRetrieveMode;
|
|
21
|
+
(function (DataRetrieveMode) {
|
|
22
|
+
DataRetrieveMode[DataRetrieveMode["WORKER"] = 0] = "WORKER";
|
|
23
|
+
DataRetrieveMode[DataRetrieveMode["REQUEST"] = 1] = "REQUEST";
|
|
24
|
+
})(DataRetrieveMode || (DataRetrieveMode = {}));
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as Enums from './enums';
|
|
2
2
|
import * as url from './url';
|
|
3
|
-
import * as
|
|
3
|
+
import * as dataRetrieval from './dataRetrieval';
|
|
4
4
|
declare const constants: {
|
|
5
5
|
Enums: typeof Enums;
|
|
6
6
|
url: typeof url;
|
|
7
|
-
|
|
7
|
+
dataRetrieval: typeof dataRetrieval;
|
|
8
8
|
};
|
|
9
|
-
export { Enums, url,
|
|
9
|
+
export { Enums, url, dataRetrieval };
|
|
10
10
|
export default constants;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CustomErrorEvent, CustomMessageEvent } from '../classes/customClasses';
|
|
2
|
+
import { Enums } from '../constants';
|
|
3
|
+
import { ScriptObject } from '../types';
|
|
4
|
+
declare class DataRetrievalManager {
|
|
5
|
+
private dataRetriever;
|
|
6
|
+
private dataRetrieverMode;
|
|
7
|
+
constructor();
|
|
8
|
+
getDataRetrieverMode(): Enums.DataRetrieveMode;
|
|
9
|
+
setDataRetrieverMode(mode: Enums.DataRetrieveMode): void;
|
|
10
|
+
register(name: string, arg: (() => Worker) | ScriptObject): void;
|
|
11
|
+
executeTask(loaderName: string, taskName: string, options: Record<string, unknown> | unknown): Promise<void>;
|
|
12
|
+
addEventListener(workerName: string, eventType: keyof WorkerEventMap, listener: (evt: CustomMessageEvent | CustomErrorEvent) => unknown): void;
|
|
13
|
+
removeEventListener(workerName: string, eventType: keyof WorkerEventMap, listener: (evt: CustomMessageEvent | CustomErrorEvent) => unknown): void;
|
|
14
|
+
reset(): void;
|
|
15
|
+
}
|
|
16
|
+
export declare function getDataRetrievalManager(): DataRetrievalManager;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { CustomError } from '../classes/customClasses';
|
|
2
|
+
import { Enums } from '../constants';
|
|
3
|
+
import RequestManager from './requestManager';
|
|
4
|
+
import { isNodeEnvironment } from './utils/environment';
|
|
5
|
+
import WebWorkerManager from './workerManager';
|
|
6
|
+
class DataRetrievalManager {
|
|
7
|
+
dataRetriever;
|
|
8
|
+
dataRetrieverMode;
|
|
9
|
+
constructor() {
|
|
10
|
+
if (isNodeEnvironment()) {
|
|
11
|
+
this.dataRetriever = new RequestManager();
|
|
12
|
+
this.dataRetrieverMode = Enums.DataRetrieveMode.REQUEST;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
this.dataRetriever = new WebWorkerManager();
|
|
16
|
+
this.dataRetrieverMode = Enums.DataRetrieveMode.WORKER;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
getDataRetrieverMode() {
|
|
20
|
+
return this.dataRetrieverMode;
|
|
21
|
+
}
|
|
22
|
+
setDataRetrieverMode(mode) {
|
|
23
|
+
const managers = {
|
|
24
|
+
[Enums.DataRetrieveMode.WORKER]: WebWorkerManager,
|
|
25
|
+
[Enums.DataRetrieveMode.REQUEST]: RequestManager
|
|
26
|
+
};
|
|
27
|
+
if (!(mode in managers)) {
|
|
28
|
+
throw new CustomError('Invalid mode');
|
|
29
|
+
}
|
|
30
|
+
this.dataRetriever.reset();
|
|
31
|
+
this.dataRetriever = new managers[mode]();
|
|
32
|
+
this.dataRetrieverMode = mode;
|
|
33
|
+
}
|
|
34
|
+
register(name, arg) {
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
this.dataRetriever.register(name, arg);
|
|
37
|
+
}
|
|
38
|
+
async executeTask(loaderName, taskName, options) {
|
|
39
|
+
return await this.dataRetriever.executeTask(loaderName, taskName, options);
|
|
40
|
+
}
|
|
41
|
+
addEventListener(workerName, eventType, listener) {
|
|
42
|
+
this.dataRetriever.addEventListener(workerName, eventType, listener);
|
|
43
|
+
}
|
|
44
|
+
removeEventListener(workerName, eventType, listener) {
|
|
45
|
+
this.dataRetriever.removeEventListener(workerName, eventType, listener);
|
|
46
|
+
}
|
|
47
|
+
reset() {
|
|
48
|
+
this.dataRetriever.reset();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const dataRetrievalManager = new DataRetrievalManager();
|
|
52
|
+
export function getDataRetrievalManager() {
|
|
53
|
+
return dataRetrievalManager;
|
|
54
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Enums } from '../constants';
|
|
2
|
+
import { getDataRetrievalManager } from './dataRetrievalManager';
|
|
3
|
+
import filePartial from './scripts/filePartial';
|
|
4
|
+
import fileStreaming from './scripts/fileStreaming';
|
|
5
|
+
export function register(workerNames, maxFetchSize) {
|
|
6
|
+
const { fileStreamingScriptName, filePartialScriptName } = workerNames;
|
|
7
|
+
const dataRetrievalManager = getDataRetrievalManager();
|
|
8
|
+
if (dataRetrievalManager.getDataRetrieverMode() === Enums.DataRetrieveMode.REQUEST) {
|
|
9
|
+
dataRetrievalManager.register(fileStreamingScriptName, fileStreaming);
|
|
10
|
+
dataRetrievalManager.register(filePartialScriptName, filePartial);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
// fileStreaming worker
|
|
14
|
+
const streamingWorkerFn = () => new Worker(new URL('./workers/fileStreamingWorker', import.meta.url), {
|
|
15
|
+
name: fileStreamingScriptName
|
|
16
|
+
});
|
|
17
|
+
dataRetrievalManager.register(fileStreamingScriptName, streamingWorkerFn);
|
|
18
|
+
// filePartial worker
|
|
19
|
+
const partialWorkerFn = () => new Worker(new URL('./workers/filePartialWorker', import.meta.url), {
|
|
20
|
+
name: filePartialScriptName
|
|
21
|
+
});
|
|
22
|
+
dataRetrievalManager.register(filePartialScriptName, partialWorkerFn);
|
|
23
|
+
}
|
|
24
|
+
dataRetrievalManager.executeTask(fileStreamingScriptName, 'setMaxFetchSize', maxFetchSize);
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CustomMessageEvent, CustomErrorEvent } from '../classes/customClasses';
|
|
2
|
+
import { ScriptObject } from '../types';
|
|
3
|
+
declare class RequestManager {
|
|
4
|
+
private loaderRegistry;
|
|
5
|
+
register(loaderName: string, loaderObject: ScriptObject): void;
|
|
6
|
+
private listenerCallback;
|
|
7
|
+
executeTask(loaderName: string, taskName: string, options: Record<string, unknown> | unknown): Promise<void>;
|
|
8
|
+
addEventListener(workerName: string, eventType: keyof WorkerEventMap, listener: (evt: CustomMessageEvent | CustomErrorEvent) => unknown): void;
|
|
9
|
+
removeEventListener(workerName: string, eventType: keyof WorkerEventMap, listener: (evt: CustomMessageEvent | CustomErrorEvent) => unknown): void;
|
|
10
|
+
reset(): void;
|
|
11
|
+
}
|
|
12
|
+
export default RequestManager;
|