cod-dicomweb-server 1.0.0 → 1.0.2
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/dist/430.min.js +7 -0
- package/dist/663.min.js +7 -0
- package/dist/index.html +1 -1
- package/dist/main.min.js +9 -0
- package/package.json +3 -3
- package/dist/assets/js/430.ae979bb9f7321087b4cd.js +0 -2
- package/dist/assets/js/430.ae979bb9f7321087b4cd.js.LICENSE.txt +0 -5
- package/dist/assets/js/663.f8ac8210581651c53c7e.js +0 -2
- package/dist/assets/js/663.f8ac8210581651c53c7e.js.LICENSE.txt +0 -5
- package/dist/assets/js/main.bd27b3d8a119b2e0661f.js +0 -2
- package/dist/assets/js/main.bd27b3d8a119b2e0661f.js.LICENSE.txt +0 -7
- package/dist/classes/CodDicomWebServer.ts +0 -423
- package/dist/classes/utils.ts +0 -176
- package/dist/constants/enums.ts +0 -18
- package/dist/constants/index.ts +0 -8
- package/dist/constants/url.ts +0 -5
- package/dist/constants/worker.ts +0 -4
- package/dist/fileManager.ts +0 -84
- package/dist/index.ts +0 -5
- package/dist/metadataManager.ts +0 -32
- package/dist/types/codDicomWebServerOptions.ts +0 -7
- package/dist/types/fileManagerOptions.ts +0 -3
- package/dist/types/index.ts +0 -7
- package/dist/types/metadata.ts +0 -57
- package/dist/types/metadataUrlCreationParams.ts +0 -9
- package/dist/types/parsedWadoRsUrlDetails.ts +0 -13
- package/dist/types/requestOptions.ts +0 -12
- package/dist/types/workerCustomMessageEvents.ts +0 -11
- package/dist/webWorker/registerWorker.ts +0 -33
- package/dist/webWorker/workerManager.ts +0 -83
- package/dist/webWorker/workers/filePartial.ts +0 -20
- package/dist/webWorker/workers/fileStreaming.ts +0 -130
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
import { parseDicom } from 'dicom-parser';
|
|
2
|
-
|
|
3
|
-
import FileManager from '../fileManager';
|
|
4
|
-
import { getMetadata } from '../metadataManager';
|
|
5
|
-
import { getFrameDetailsFromMetadata, parseWadorsURL } from './utils';
|
|
6
|
-
import { getWebWorkerManager } from '../webWorker/workerManager';
|
|
7
|
-
import { registerWorkers } from '../webWorker/registerWorker';
|
|
8
|
-
import constants, { Enums } from '../constants';
|
|
9
|
-
import type {
|
|
10
|
-
CodDicomWebServerOptions,
|
|
11
|
-
InstanceMetadata,
|
|
12
|
-
JsonMetadata,
|
|
13
|
-
SeriesMetadata,
|
|
14
|
-
CODRequestOptions,
|
|
15
|
-
FileRequestOptions,
|
|
16
|
-
FileStreamingMessageEvent,
|
|
17
|
-
} from '../types';
|
|
18
|
-
|
|
19
|
-
class CodDicomWebServer {
|
|
20
|
-
private filePromises: Record<string, Promise<void>> = {};
|
|
21
|
-
private options: CodDicomWebServerOptions = {
|
|
22
|
-
maxWorkerFetchSize: Infinity,
|
|
23
|
-
domain: constants.url.DOMAIN,
|
|
24
|
-
};
|
|
25
|
-
private fileManager;
|
|
26
|
-
private seriesUidFileUrls: Record<string, string[]> = {};
|
|
27
|
-
|
|
28
|
-
constructor(args: { maxWorkerFetchSize?: number; domain?: string }) {
|
|
29
|
-
const { maxWorkerFetchSize, domain } = args;
|
|
30
|
-
|
|
31
|
-
this.options.maxWorkerFetchSize =
|
|
32
|
-
maxWorkerFetchSize || this.options.maxWorkerFetchSize;
|
|
33
|
-
this.options.domain = domain || this.options.domain;
|
|
34
|
-
const fileStreamingWorkerName = constants.worker.FILE_STREAMING_WORKER_NAME;
|
|
35
|
-
const filePartialWorkerName = constants.worker.FILE_PARTIAL_WORKER_NAME;
|
|
36
|
-
this.fileManager = new FileManager({ fileStreamingWorkerName });
|
|
37
|
-
|
|
38
|
-
registerWorkers(
|
|
39
|
-
{ fileStreamingWorkerName, filePartialWorkerName },
|
|
40
|
-
this.options.maxWorkerFetchSize,
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
public setOptions = (newOptions: CodDicomWebServerOptions): void => {
|
|
45
|
-
Object.keys(newOptions).forEach((key) => {
|
|
46
|
-
if (!newOptions[key] === undefined) {
|
|
47
|
-
this.options[key] = newOptions[key];
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
public getOptions = (): CodDicomWebServerOptions => {
|
|
53
|
-
return this.options;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
private addFileUrl(seriesInstanceUID: string, url: string): void {
|
|
57
|
-
if (this.seriesUidFileUrls[seriesInstanceUID]) {
|
|
58
|
-
this.seriesUidFileUrls[seriesInstanceUID].push(url);
|
|
59
|
-
} else {
|
|
60
|
-
this.seriesUidFileUrls[seriesInstanceUID] = [url];
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
public async fetchCod(
|
|
65
|
-
wadorsUrl: string,
|
|
66
|
-
imageId: string,
|
|
67
|
-
headers: Record<string, string> | undefined = {},
|
|
68
|
-
{ useSharedArrayBuffer, fetchType }: CODRequestOptions = {},
|
|
69
|
-
): Promise<ArrayBufferLike | InstanceMetadata | SeriesMetadata | undefined> {
|
|
70
|
-
try {
|
|
71
|
-
const parsedDetails = parseWadorsURL(wadorsUrl, this.options.domain);
|
|
72
|
-
|
|
73
|
-
if (parsedDetails) {
|
|
74
|
-
const {
|
|
75
|
-
type,
|
|
76
|
-
bucketName,
|
|
77
|
-
bucketPrefix,
|
|
78
|
-
studyInstanceUID,
|
|
79
|
-
seriesInstanceUID,
|
|
80
|
-
sopInstanceUID,
|
|
81
|
-
frameNumber,
|
|
82
|
-
} = parsedDetails;
|
|
83
|
-
|
|
84
|
-
const metadataJson = await getMetadata(
|
|
85
|
-
{
|
|
86
|
-
domain: this.options.domain,
|
|
87
|
-
bucketName,
|
|
88
|
-
bucketPrefix,
|
|
89
|
-
studyInstanceUID,
|
|
90
|
-
seriesInstanceUID,
|
|
91
|
-
},
|
|
92
|
-
headers,
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
if (!metadataJson) {
|
|
96
|
-
throw new Error(`Metadata not found for ${wadorsUrl}`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const {
|
|
100
|
-
url: fileUrl,
|
|
101
|
-
startByte,
|
|
102
|
-
endByte,
|
|
103
|
-
thumbnailUrl,
|
|
104
|
-
isMultiframe,
|
|
105
|
-
} = getFrameDetailsFromMetadata(
|
|
106
|
-
metadataJson,
|
|
107
|
-
sopInstanceUID,
|
|
108
|
-
frameNumber - 1,
|
|
109
|
-
{
|
|
110
|
-
domain: this.options.domain,
|
|
111
|
-
bucketName,
|
|
112
|
-
bucketPrefix,
|
|
113
|
-
},
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
switch (type) {
|
|
117
|
-
case Enums.RequestType.THUMBNAIL:
|
|
118
|
-
this.addFileUrl(seriesInstanceUID, thumbnailUrl);
|
|
119
|
-
|
|
120
|
-
return this.fetchFile(thumbnailUrl, headers, {
|
|
121
|
-
useSharedArrayBuffer,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
case Enums.RequestType.FRAME: {
|
|
125
|
-
let urlWithBytes: string = fileUrl;
|
|
126
|
-
if (fetchType === Enums.FetchType.BYTES_OPTIMIZED) {
|
|
127
|
-
urlWithBytes = `${fileUrl}?bytes=${startByte}-${endByte}`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
this.addFileUrl(seriesInstanceUID, fileUrl);
|
|
131
|
-
|
|
132
|
-
return this.fetchFile(urlWithBytes, headers, {
|
|
133
|
-
offsets: { startByte, endByte },
|
|
134
|
-
useSharedArrayBuffer,
|
|
135
|
-
fetchType,
|
|
136
|
-
}).then((arraybuffer) => {
|
|
137
|
-
if (!arraybuffer?.byteLength) {
|
|
138
|
-
throw new Error('File Arraybuffer is not found');
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (isMultiframe) {
|
|
142
|
-
return arraybuffer;
|
|
143
|
-
} else {
|
|
144
|
-
const dataSet = parseDicom(new Uint8Array(arraybuffer));
|
|
145
|
-
|
|
146
|
-
const pixelDataElement = dataSet.elements.x7fe00010;
|
|
147
|
-
let { dataOffset, length } = pixelDataElement;
|
|
148
|
-
if (
|
|
149
|
-
pixelDataElement.hadUndefinedLength &&
|
|
150
|
-
pixelDataElement.fragments
|
|
151
|
-
) {
|
|
152
|
-
({ position: dataOffset, length } =
|
|
153
|
-
pixelDataElement.fragments[0]);
|
|
154
|
-
} else {
|
|
155
|
-
// Adding 8 bytes for 4 bytes tag + 4 bytes length for uncomppressed pixelData
|
|
156
|
-
dataOffset += 8;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return arraybuffer.slice(dataOffset, dataOffset + length);
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
case Enums.RequestType.SERIES_METADATA:
|
|
164
|
-
case Enums.RequestType.INSTANCE_METADATA:
|
|
165
|
-
return this.parseMetadata(metadataJson, type, sopInstanceUID);
|
|
166
|
-
|
|
167
|
-
default:
|
|
168
|
-
throw new Error(`Unsupported request type: ${type}`);
|
|
169
|
-
}
|
|
170
|
-
} else {
|
|
171
|
-
return new Promise((resolve, reject) => {
|
|
172
|
-
return this.fetchFile(wadorsUrl, headers, { useSharedArrayBuffer })
|
|
173
|
-
.then((result) => {
|
|
174
|
-
if (result instanceof ArrayBuffer) {
|
|
175
|
-
try {
|
|
176
|
-
const dataSet = parseDicom(new Uint8Array(result));
|
|
177
|
-
const seriesInstanceUID = dataSet.string('0020000e');
|
|
178
|
-
|
|
179
|
-
!!seriesInstanceUID &&
|
|
180
|
-
this.addFileUrl(seriesInstanceUID, wadorsUrl);
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.warn('There is some issue parsing the file.', error);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
resolve(result);
|
|
186
|
-
})
|
|
187
|
-
.catch((error) => reject(error));
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
} catch (error) {
|
|
191
|
-
console.error(error);
|
|
192
|
-
throw error;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
public async fetchFile(
|
|
197
|
-
fileUrl: string,
|
|
198
|
-
headers: Record<string, string>,
|
|
199
|
-
{
|
|
200
|
-
offsets,
|
|
201
|
-
useSharedArrayBuffer = false,
|
|
202
|
-
fetchType = constants.Enums.FetchType.API_OPTIMIZED,
|
|
203
|
-
}: FileRequestOptions = {},
|
|
204
|
-
): Promise<ArrayBufferLike | undefined> {
|
|
205
|
-
const isBytesOptimized = fetchType === Enums.FetchType.BYTES_OPTIMIZED;
|
|
206
|
-
const extractedFile = this.fileManager.get(
|
|
207
|
-
fileUrl,
|
|
208
|
-
isBytesOptimized ? undefined : offsets,
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
if (extractedFile) {
|
|
212
|
-
return new Promise<ArrayBufferLike>((resolveRequest, rejectRequest) => {
|
|
213
|
-
try {
|
|
214
|
-
resolveRequest(extractedFile.buffer);
|
|
215
|
-
} catch (error) {
|
|
216
|
-
rejectRequest(error);
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const { maxWorkerFetchSize } = this.getOptions();
|
|
222
|
-
const webWorkerManager = getWebWorkerManager();
|
|
223
|
-
const { FILE_STREAMING_WORKER_NAME, FILE_PARTIAL_WORKER_NAME, THRESHOLD } =
|
|
224
|
-
constants.worker;
|
|
225
|
-
let tarPromise: Promise<void>;
|
|
226
|
-
|
|
227
|
-
if (!this.filePromises[fileUrl]) {
|
|
228
|
-
tarPromise = new Promise<void>((resolveFile, rejectFile) => {
|
|
229
|
-
if (this.fileManager.getTotalSize() + THRESHOLD > maxWorkerFetchSize) {
|
|
230
|
-
throw new Error(
|
|
231
|
-
`fileStreaming.ts: Maximum size(${maxWorkerFetchSize}) for fetching files reached`,
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const FetchTypeEnum = constants.Enums.FetchType;
|
|
236
|
-
|
|
237
|
-
if (fetchType === FetchTypeEnum.API_OPTIMIZED) {
|
|
238
|
-
const handleFirstChunk = (
|
|
239
|
-
evt: FileStreamingMessageEvent | ErrorEvent,
|
|
240
|
-
): void => {
|
|
241
|
-
if (evt instanceof ErrorEvent) {
|
|
242
|
-
rejectFile(evt.error);
|
|
243
|
-
throw evt.error;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const { url, position, fileArraybuffer } = evt.data;
|
|
247
|
-
|
|
248
|
-
if (url === fileUrl && fileArraybuffer) {
|
|
249
|
-
this.fileManager.set(url, { data: fileArraybuffer, position });
|
|
250
|
-
|
|
251
|
-
webWorkerManager.removeEventListener(
|
|
252
|
-
FILE_STREAMING_WORKER_NAME,
|
|
253
|
-
'message',
|
|
254
|
-
handleFirstChunk,
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
webWorkerManager.addEventListener(
|
|
260
|
-
FILE_STREAMING_WORKER_NAME,
|
|
261
|
-
'message',
|
|
262
|
-
handleFirstChunk,
|
|
263
|
-
);
|
|
264
|
-
webWorkerManager
|
|
265
|
-
.executeTask(FILE_STREAMING_WORKER_NAME, 'stream', {
|
|
266
|
-
url: fileUrl,
|
|
267
|
-
headers: headers,
|
|
268
|
-
useSharedArrayBuffer,
|
|
269
|
-
})
|
|
270
|
-
.then(() => {
|
|
271
|
-
resolveFile();
|
|
272
|
-
})
|
|
273
|
-
.catch((error) => {
|
|
274
|
-
webWorkerManager.removeEventListener(
|
|
275
|
-
FILE_STREAMING_WORKER_NAME,
|
|
276
|
-
'message',
|
|
277
|
-
handleFirstChunk,
|
|
278
|
-
);
|
|
279
|
-
rejectFile(error);
|
|
280
|
-
})
|
|
281
|
-
.then(() => delete this.filePromises[fileUrl]);
|
|
282
|
-
} else if (fetchType === FetchTypeEnum.BYTES_OPTIMIZED && offsets) {
|
|
283
|
-
const { startByte, endByte } = offsets;
|
|
284
|
-
headers['Range'] = `bytes=${startByte}-${endByte - 1}`;
|
|
285
|
-
const url = fileUrl.split('?bytes=')[0];
|
|
286
|
-
|
|
287
|
-
webWorkerManager
|
|
288
|
-
.executeTask(FILE_PARTIAL_WORKER_NAME, 'partial', {
|
|
289
|
-
url: url,
|
|
290
|
-
headers: headers,
|
|
291
|
-
useSharedArrayBuffer,
|
|
292
|
-
})
|
|
293
|
-
.then((data) => {
|
|
294
|
-
if (data) {
|
|
295
|
-
this.fileManager.set(fileUrl, {
|
|
296
|
-
data: new Uint8Array(data),
|
|
297
|
-
position: data.byteLength,
|
|
298
|
-
});
|
|
299
|
-
resolveFile();
|
|
300
|
-
} else {
|
|
301
|
-
rejectFile(new Error(`File - ${url} not found`));
|
|
302
|
-
}
|
|
303
|
-
})
|
|
304
|
-
.catch((error) => {
|
|
305
|
-
rejectFile(error);
|
|
306
|
-
})
|
|
307
|
-
.then(() => delete this.filePromises[fileUrl]);
|
|
308
|
-
} else {
|
|
309
|
-
rejectFile('Offsets is needed in bytes optimized fetching');
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
this.filePromises[fileUrl] = tarPromise;
|
|
314
|
-
} else {
|
|
315
|
-
tarPromise = this.filePromises[fileUrl];
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return new Promise<ArrayBufferLike | undefined>(
|
|
319
|
-
(resolveRequest, rejectRequest) => {
|
|
320
|
-
let requestResolved = false;
|
|
321
|
-
|
|
322
|
-
const handleChunkAppend = (
|
|
323
|
-
evt: FileStreamingMessageEvent | ErrorEvent,
|
|
324
|
-
): void => {
|
|
325
|
-
if (evt instanceof ErrorEvent) {
|
|
326
|
-
rejectRequest(evt.message);
|
|
327
|
-
throw evt.error;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const { url, position, chunk, isAppending } = evt.data;
|
|
331
|
-
|
|
332
|
-
if (isAppending) {
|
|
333
|
-
if (chunk) {
|
|
334
|
-
this.fileManager.append(url, chunk, position);
|
|
335
|
-
} else {
|
|
336
|
-
this.fileManager.setPosition(url, position);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (
|
|
341
|
-
!requestResolved &&
|
|
342
|
-
url === fileUrl &&
|
|
343
|
-
offsets &&
|
|
344
|
-
position > offsets.endByte
|
|
345
|
-
) {
|
|
346
|
-
try {
|
|
347
|
-
const file = this.fileManager.get(url, offsets);
|
|
348
|
-
requestResolved = true;
|
|
349
|
-
resolveRequest(file?.buffer);
|
|
350
|
-
} catch (error) {
|
|
351
|
-
rejectRequest(error);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
if (offsets && !isBytesOptimized) {
|
|
357
|
-
webWorkerManager.addEventListener(
|
|
358
|
-
FILE_STREAMING_WORKER_NAME,
|
|
359
|
-
'message',
|
|
360
|
-
handleChunkAppend,
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
tarPromise
|
|
365
|
-
.then(() => {
|
|
366
|
-
if (!requestResolved) {
|
|
367
|
-
const file = this.fileManager.get(
|
|
368
|
-
fileUrl,
|
|
369
|
-
isBytesOptimized ? undefined : offsets,
|
|
370
|
-
);
|
|
371
|
-
requestResolved = true;
|
|
372
|
-
resolveRequest(file?.buffer);
|
|
373
|
-
}
|
|
374
|
-
})
|
|
375
|
-
.catch((error) => {
|
|
376
|
-
rejectRequest(error);
|
|
377
|
-
})
|
|
378
|
-
.then(() => {
|
|
379
|
-
webWorkerManager.removeEventListener(
|
|
380
|
-
FILE_STREAMING_WORKER_NAME,
|
|
381
|
-
'message',
|
|
382
|
-
handleChunkAppend,
|
|
383
|
-
);
|
|
384
|
-
});
|
|
385
|
-
},
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
public delete(seriesInstanceUID: string): void {
|
|
390
|
-
const fileUrls = this.seriesUidFileUrls[seriesInstanceUID];
|
|
391
|
-
if (fileUrls) {
|
|
392
|
-
fileUrls.forEach((fileUrl) => {
|
|
393
|
-
this.fileManager.remove(fileUrl);
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
delete this.seriesUidFileUrls[seriesInstanceUID];
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
public deleteAll(): void {
|
|
400
|
-
Object.values(this.seriesUidFileUrls).forEach((fileUrls) => {
|
|
401
|
-
fileUrls.forEach((fileUrl) => {
|
|
402
|
-
this.fileManager.remove(fileUrl);
|
|
403
|
-
});
|
|
404
|
-
});
|
|
405
|
-
this.seriesUidFileUrls = {};
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
public parseMetadata(
|
|
409
|
-
metadata: JsonMetadata,
|
|
410
|
-
type: Enums.RequestType,
|
|
411
|
-
sopInstanceUID: string,
|
|
412
|
-
): InstanceMetadata | SeriesMetadata {
|
|
413
|
-
if (type === Enums.RequestType.INSTANCE_METADATA) {
|
|
414
|
-
return metadata.cod.instances[sopInstanceUID].metadata;
|
|
415
|
-
} else {
|
|
416
|
-
return Object.values(metadata.cod.instances).map(
|
|
417
|
-
(instance) => instance.metadata,
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
export default CodDicomWebServer;
|
package/dist/classes/utils.ts
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import constants, { Enums } from '../constants';
|
|
2
|
-
import type {
|
|
3
|
-
JsonMetadata,
|
|
4
|
-
MetadataUrlCreationParams,
|
|
5
|
-
ParsedWadoRsUrlDetails,
|
|
6
|
-
} from '../types';
|
|
7
|
-
|
|
8
|
-
export function parseWadorsURL(
|
|
9
|
-
url: string,
|
|
10
|
-
domain: string,
|
|
11
|
-
): ParsedWadoRsUrlDetails | undefined {
|
|
12
|
-
if (!url.includes(constants.url.URL_VALIDATION_STRING)) {
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const filePath = url.split(domain + '/')[1];
|
|
17
|
-
|
|
18
|
-
const prefix = filePath.split('/studies')[0];
|
|
19
|
-
const prefixParts = prefix.split('/');
|
|
20
|
-
|
|
21
|
-
const bucketName = prefixParts[0];
|
|
22
|
-
const bucketPrefix = prefixParts.slice(1).join('/');
|
|
23
|
-
|
|
24
|
-
const imagePath = filePath.split(prefix + '/')[1];
|
|
25
|
-
const imageParts = imagePath.split('/');
|
|
26
|
-
|
|
27
|
-
const studyInstanceUID = imageParts[1];
|
|
28
|
-
const seriesInstanceUID = imageParts[3];
|
|
29
|
-
let sopInstanceUID = '',
|
|
30
|
-
frameNumber = 1,
|
|
31
|
-
type: Enums.RequestType;
|
|
32
|
-
|
|
33
|
-
switch (true) {
|
|
34
|
-
case imageParts.includes('thumbnail'):
|
|
35
|
-
type = Enums.RequestType.THUMBNAIL;
|
|
36
|
-
break;
|
|
37
|
-
case imageParts.includes('metadata'):
|
|
38
|
-
if (imageParts.includes('instances')) {
|
|
39
|
-
sopInstanceUID = imageParts[5];
|
|
40
|
-
type = Enums.RequestType.INSTANCE_METADATA;
|
|
41
|
-
} else {
|
|
42
|
-
type = Enums.RequestType.SERIES_METADATA;
|
|
43
|
-
}
|
|
44
|
-
break;
|
|
45
|
-
case imageParts.includes('frames'):
|
|
46
|
-
sopInstanceUID = imageParts[5];
|
|
47
|
-
frameNumber = +(imageParts[7] || frameNumber);
|
|
48
|
-
type = Enums.RequestType.FRAME;
|
|
49
|
-
break;
|
|
50
|
-
default:
|
|
51
|
-
throw new Error('Invalid type of request');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
type,
|
|
56
|
-
bucketName,
|
|
57
|
-
bucketPrefix,
|
|
58
|
-
studyInstanceUID,
|
|
59
|
-
seriesInstanceUID,
|
|
60
|
-
sopInstanceUID,
|
|
61
|
-
frameNumber,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function getFrameDetailsFromMetadata(
|
|
66
|
-
seriesMetadata: JsonMetadata,
|
|
67
|
-
sopInstanceUID: string,
|
|
68
|
-
frameIndex: number,
|
|
69
|
-
bucketDetails: { domain: string; bucketName: string; bucketPrefix: string },
|
|
70
|
-
): {
|
|
71
|
-
url: string;
|
|
72
|
-
startByte: number;
|
|
73
|
-
endByte: number;
|
|
74
|
-
thumbnailUrl: string;
|
|
75
|
-
isMultiframe: boolean;
|
|
76
|
-
} {
|
|
77
|
-
if (
|
|
78
|
-
!seriesMetadata ||
|
|
79
|
-
!seriesMetadata.cod?.instances ||
|
|
80
|
-
!seriesMetadata.thumbnail
|
|
81
|
-
) {
|
|
82
|
-
throw new Error('Invalid seriesMetadata provided.');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (!sopInstanceUID) {
|
|
86
|
-
throw new Error('SOP Instance UID is required.');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (frameIndex === null || frameIndex === undefined) {
|
|
90
|
-
throw new Error('Frame index is required.');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const instanceFound = Object.values(seriesMetadata.cod.instances).find(
|
|
94
|
-
(instance) => instance.metadata['00080018']?.Value?.[0] === sopInstanceUID,
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
if (!instanceFound) {
|
|
98
|
-
throw new Error(
|
|
99
|
-
`Instance with SOPInstanceUID ${sopInstanceUID} not found.`,
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const { domain, bucketName, bucketPrefix } = bucketDetails;
|
|
104
|
-
const { url, uri, headers: offsetHeaders, offset_tables } = instanceFound;
|
|
105
|
-
const modifiedUrl = handleUrl(url || uri, domain, bucketName, bucketPrefix);
|
|
106
|
-
|
|
107
|
-
const { CustomOffsetTable, CustomOffsetTableLengths } = offset_tables || {};
|
|
108
|
-
|
|
109
|
-
let sliceStart: number | undefined,
|
|
110
|
-
sliceEnd: number | undefined,
|
|
111
|
-
isMultiframe = false;
|
|
112
|
-
if (CustomOffsetTable?.length && CustomOffsetTableLengths?.length) {
|
|
113
|
-
sliceStart = CustomOffsetTable[frameIndex];
|
|
114
|
-
sliceEnd = sliceStart + CustomOffsetTableLengths[frameIndex];
|
|
115
|
-
isMultiframe = true;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const { start_byte: fileStartByte, end_byte: fileEndByte } = offsetHeaders;
|
|
119
|
-
|
|
120
|
-
const startByte =
|
|
121
|
-
sliceStart !== undefined ? fileStartByte + sliceStart : fileStartByte;
|
|
122
|
-
const endByte =
|
|
123
|
-
sliceEnd !== undefined ? fileStartByte + sliceEnd : fileEndByte;
|
|
124
|
-
|
|
125
|
-
const thumbnailGsUtilUri = seriesMetadata.thumbnail.uri;
|
|
126
|
-
const thumbnailUrl = `${domain}/${thumbnailGsUtilUri.split('gs://')[1]}`;
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
url: modifiedUrl,
|
|
130
|
-
startByte,
|
|
131
|
-
endByte,
|
|
132
|
-
thumbnailUrl,
|
|
133
|
-
isMultiframe,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function handleUrl(
|
|
138
|
-
url: string,
|
|
139
|
-
domain: string,
|
|
140
|
-
bucketName: string,
|
|
141
|
-
bucketPrefix: string,
|
|
142
|
-
): string {
|
|
143
|
-
let modifiedUrl = url;
|
|
144
|
-
|
|
145
|
-
const matchingExtension = constants.url.FILE_EXTENSIONS.find((extension) =>
|
|
146
|
-
url.includes(extension),
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
if (matchingExtension) {
|
|
150
|
-
const fileParts = url.split(matchingExtension);
|
|
151
|
-
modifiedUrl = fileParts[0] + matchingExtension;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const filePath = modifiedUrl.split('studies/')[1];
|
|
155
|
-
modifiedUrl = `${domain}/${bucketName}/${bucketPrefix ? bucketPrefix + '/' : ''}studies/${filePath}`;
|
|
156
|
-
|
|
157
|
-
return modifiedUrl;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function createMetadataJsonUrl(
|
|
161
|
-
params: MetadataUrlCreationParams,
|
|
162
|
-
): string | undefined {
|
|
163
|
-
const {
|
|
164
|
-
domain = constants.url.DOMAIN,
|
|
165
|
-
bucketName,
|
|
166
|
-
bucketPrefix,
|
|
167
|
-
studyInstanceUID,
|
|
168
|
-
seriesInstanceUID,
|
|
169
|
-
} = params;
|
|
170
|
-
|
|
171
|
-
if (!bucketName || !bucketPrefix || !studyInstanceUID || !seriesInstanceUID) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return `${domain}/${bucketName}/${bucketPrefix}/studies/${studyInstanceUID}/series/${seriesInstanceUID}/metadata.json`;
|
|
176
|
-
}
|
package/dist/constants/enums.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export enum FetchType {
|
|
2
|
-
/**
|
|
3
|
-
* Fetch only the part of the file according to the offsets provided.
|
|
4
|
-
*/
|
|
5
|
-
BYTES_OPTIMIZED,
|
|
6
|
-
/**
|
|
7
|
-
* Stream the file and returns the part of the file if offsets are provided.
|
|
8
|
-
* Or returns the whole file.
|
|
9
|
-
*/
|
|
10
|
-
API_OPTIMIZED,
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export enum RequestType {
|
|
14
|
-
FRAME,
|
|
15
|
-
THUMBNAIL,
|
|
16
|
-
SERIES_METADATA,
|
|
17
|
-
INSTANCE_METADATA,
|
|
18
|
-
}
|
package/dist/constants/index.ts
DELETED
package/dist/constants/url.ts
DELETED
package/dist/constants/worker.ts
DELETED