@radicalimaging/static-wado-creator 1.7.3 → 1.7.6

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.
Files changed (48) hide show
  1. package/dist/DeleteStudy.js +16 -0
  2. package/dist/RejectInstance.js +25 -0
  3. package/dist/StaticWado.js +419 -0
  4. package/dist/createMain.js +18 -0
  5. package/dist/createPart10.js +113 -0
  6. package/dist/createThumbnail.js +163 -0
  7. package/dist/deleteMain.js +12 -0
  8. package/dist/groupMain.js +17 -0
  9. package/dist/index.mjs +18 -0
  10. package/dist/indexMain.js +10 -0
  11. package/dist/instanceMain.js +16 -0
  12. package/dist/metadataMain.js +15 -0
  13. package/dist/mkdicomwebConfig.js +342 -0
  14. package/dist/operation/InstanceDeduplicate.js +103 -0
  15. package/dist/operation/ScanStudy.js +47 -0
  16. package/dist/operation/StudyData.js +520 -0
  17. package/dist/operation/ThumbnailService.js +198 -0
  18. package/dist/operation/adapter/decodeImage.js +12 -0
  19. package/dist/operation/adapter/getImageInfo.js +38 -0
  20. package/dist/operation/adapter/transcodeImage.js +519 -0
  21. package/dist/operation/extractImageFrames.js +55 -0
  22. package/dist/operation/getDataSet.js +65 -0
  23. package/dist/operation/getEncapsulatedImageFrame.js +35 -0
  24. package/dist/operation/getNumberOfFrames.js +13 -0
  25. package/dist/operation/getUncompressedImageFrame.js +7 -0
  26. package/dist/operation/getVR.js +24 -0
  27. package/dist/operation/getValue.js +216 -0
  28. package/dist/operation/validateMetadata.js +40 -0
  29. package/dist/program/index.js +14 -0
  30. package/dist/rejectMain.js +12 -0
  31. package/dist/serverMain.js +21 -0
  32. package/dist/util/IdCreator.js +42 -0
  33. package/dist/util/adaptProgramOpts.js +39 -0
  34. package/dist/util/compressionOptionParser.js +13 -0
  35. package/dist/util/resultsReport.ts +87 -0
  36. package/dist/writer/CompleteStudyWriter.js +129 -0
  37. package/dist/writer/DeduplicateWriter.js +33 -0
  38. package/dist/writer/ExpandUriPath.js +16 -0
  39. package/dist/writer/HashDataWriter.js +74 -0
  40. package/dist/writer/ImageFrameWriter.js +59 -0
  41. package/dist/writer/MultipartAttribute.js +8 -0
  42. package/dist/writer/MultipartHeader.js +14 -0
  43. package/dist/writer/RawDicomWriter.js +44 -0
  44. package/dist/writer/ThumbnailWriter.js +14 -0
  45. package/dist/writer/VideoWriter.js +53 -0
  46. package/dist/writer/WriteMultipart.js +22 -0
  47. package/dist/writer/WriteStream.js +64 -0
  48. package/package.json +11 -6
@@ -0,0 +1,519 @@
1
+ const dicomCodec = require('@cornerstonejs/dicom-codec');
2
+ const { Tags, replicate } = require('@radicalimaging/static-wado-util');
3
+ const getImageInfo = require('./getImageInfo');
4
+
5
+ dicomCodec.setConfig('verbose: false');
6
+
7
+ const transcodeOp = {
8
+ none: 0,
9
+ decode: 1,
10
+ encode: 2,
11
+ transcode: 3, // decode and then encode
12
+ };
13
+
14
+ const jpegBeforeEncode = (options, encoder) => {
15
+ const quality = options.quality ?? 99;
16
+ // First value is to encode as reversible lossless colour
17
+ return encoder => {
18
+ console.log('Calling setQuality', quality, !!encoder.setQuality);
19
+ encoder.setQuality(quality);
20
+ };
21
+ };
22
+
23
+ const jlsBeforeEncode = options => {
24
+ const delta = (options.delta ?? lossy) ? 3 : 0;
25
+ return encoder => encoder.setNearLossless(delta);
26
+ };
27
+
28
+ const htj2kBeforeEncode = options => {
29
+ const { lossy } = options;
30
+ const quality = lossy === true ? 0.002 : -1;
31
+ return encoder => encoder.setQuality(!lossy, quality);
32
+ };
33
+
34
+ /**
35
+ * Static mapping for transcoding decoder destination names/uids - currently only lei, jls and jls-lossy appear to work.
36
+ */
37
+ const transcodeDestinationMap = {
38
+ lei: {
39
+ transferSyntaxUid: '1.2.840.10008.1.2.1',
40
+ transcodeOp: transcodeOp.none,
41
+ },
42
+ jls: {
43
+ transferSyntaxUid: '1.2.840.10008.1.2.4.80',
44
+ transcodeOp: transcodeOp.encode,
45
+ beforeEncode: jlsBeforeEncode.bind(null, { lossy: false }),
46
+ },
47
+ 'jls-lossy': {
48
+ transferSyntaxUid: '1.2.840.10008.1.2.4.81',
49
+ transcodeOp: transcodeOp.encode,
50
+ beforeEncode: jlsBeforeEncode.bind(null, { lossy: true, delta: 3 }),
51
+ },
52
+ jp2: {
53
+ transferSyntaxUid: '1.2.840.10008.1.2.4.90',
54
+ transcodeOp: transcodeOp.encode,
55
+ },
56
+ 'jp2-lossy': {
57
+ transferSyntaxUid: '1.2.840.10008.1.2.4.91',
58
+ transcodeOp: transcodeOp.encode,
59
+ },
60
+ jhc: {
61
+ transferSyntaxUid: '1.2.840.10008.1.2.4.201',
62
+ transcodeOp: transcodeOp.encode,
63
+ beforeEncode: htj2kBeforeEncode({}),
64
+ },
65
+ 'jhc-lossy': {
66
+ transferSyntaxUid: '1.2.840.10008.1.2.4.202',
67
+ transcodeOp: transcodeOp.encode,
68
+ beforeEncode: htj2kBeforeEncode({ lossy: true }),
69
+ },
70
+ jpeg: {
71
+ transferSyntaxUid: '1.2.840.10008.1.2.4.50',
72
+ transcodeOp: transcodeOp.encode,
73
+ // beforeEncode: jpegBeforeEncode({}),
74
+ },
75
+ };
76
+
77
+ const transcodeSourceMap = {
78
+ '1.2.840.10008.1.2': {
79
+ transcodeOp: transcodeOp.decode,
80
+ alias: 'uncompressed',
81
+ },
82
+ '1.2.840.10008.1.2.1': {
83
+ transcodeOp: transcodeOp.none,
84
+ alias: 'uncompressed',
85
+ },
86
+ '1.2.840.10008.1.2.2': {
87
+ transcodeOp: transcodeOp.decode,
88
+ alias: 'uncompressed',
89
+ },
90
+ '1.2.840.10008.1.2.4.50': {
91
+ transcodeOp: transcodeOp.decode,
92
+ alias: 'jpeg',
93
+ },
94
+ '1.2.840.10008.1.2.4.57': {
95
+ transcodeOp: transcodeOp.decode,
96
+ alias: 'jls',
97
+ },
98
+ '1.2.840.10008.1.2.4.70': {
99
+ transcodeOp: transcodeOp.decode,
100
+ alias: 'jpll',
101
+ },
102
+ '1.2.840.10008.1.2.4.80': {
103
+ transcodeOp: transcodeOp.decode,
104
+ alias: 'jls',
105
+ },
106
+ '1.2.840.10008.1.2.4.90': {
107
+ transcodeOp: transcodeOp.decode,
108
+ alias: 'jp2',
109
+ },
110
+ '1.2.840.10008.1.2.4.91': {
111
+ transcodeOp: transcodeOp.decode,
112
+ alias: 'jp2',
113
+ },
114
+ '3.2.840.10008.1.2.4.96': {
115
+ transcodeOp: transcodeOp.decode,
116
+ alias: 'jhc',
117
+ },
118
+ '1.2.840.10008.1.2.4.202': {
119
+ transcodeOp: transcodeOp.decode,
120
+ alias: 'jhc',
121
+ },
122
+ '1.2.840.10008.1.2.5': {
123
+ transcodeOp: transcodeOp.decode,
124
+ alias: 'rle',
125
+ },
126
+ };
127
+
128
+ /**
129
+ * Get an existing destination transcoder from destination map.
130
+ *
131
+ * @param {*} id Destination type to compress to
132
+ * @returns A partial transcoder definition. Otherwise it returns undefined.
133
+ */
134
+ function getDestinationTranscoder(id) {
135
+ const destinationTranscoderEntry =
136
+ transcodeDestinationMap[id] ||
137
+ Object.values(transcodeDestinationMap).find(value => value.transferSyntaxUid === id);
138
+ return destinationTranscoderEntry;
139
+ }
140
+
141
+ /**
142
+ * Return a merged source and destination transcoder for the given transferSyntaxUid. Otherwise it returns undefined.
143
+ *
144
+ * @param {*} transferSyntaxUid
145
+ * @returns
146
+ */
147
+ function getTranscoder(
148
+ transferSyntaxUid,
149
+ { contentType: greyContentType, colorContentType },
150
+ samplesPerPixel
151
+ ) {
152
+ const contentType = samplesPerPixel === 3 ? colorContentType : greyContentType;
153
+ const sourceTranscoder = transcodeSourceMap[transferSyntaxUid];
154
+ const destinationTranscoder = getDestinationTranscoder(contentType);
155
+ if (!sourceTranscoder || !destinationTranscoder) {
156
+ return undefined;
157
+ }
158
+
159
+ return {
160
+ transferSyntaxUid: destinationTranscoder.transferSyntaxUid,
161
+ transcodeOp: sourceTranscoder.transcodeOp | destinationTranscoder.transcodeOp,
162
+ alias: sourceTranscoder.alias,
163
+ beforeEncode: destinationTranscoder.beforeEncode,
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Tell whether given id contain transferSyntaxUid which can be transcoded or not.
169
+ * @param {*} id object containing transferSyntaxUid
170
+ * @param {*} options runner options
171
+ */
172
+ function shouldTranscodeImageFrame(id, options, samplesPerPixel) {
173
+ const { recompress: recompressGrey, recompressColor } = options;
174
+ const recompress = samplesPerPixel === 3 ? recompressColor : recompressGrey;
175
+ if (!recompress) {
176
+ console.verbose('Not transcoding because recompress not set');
177
+ return false;
178
+ }
179
+
180
+ const { transferSyntaxUid } = id;
181
+ const transcoder = getTranscoder(transferSyntaxUid, options, samplesPerPixel);
182
+ const validTranscoder = transcoder && transcoder.transferSyntaxUid;
183
+ if (!validTranscoder) {
184
+ console.verbose(
185
+ 'Not transcoding because no decoder found for',
186
+ transferSyntaxUid,
187
+ samplesPerPixel
188
+ );
189
+ return false;
190
+ }
191
+ const validRecompress = recompress.includes('true') || recompress.includes(transcoder.alias);
192
+ if (!validRecompress) {
193
+ console.verbose(
194
+ 'Not transcoding because recompress',
195
+ recompress,
196
+ 'does not include',
197
+ transcoder.alias
198
+ );
199
+ return false;
200
+ }
201
+
202
+ return true;
203
+ }
204
+
205
+ /**
206
+ * It tells if we should use transcoded image to generate thumb or not.
207
+ *
208
+ * @param {*} id
209
+ * @param {*} options
210
+ * @returns True if transcoder.alias is present on intersection of recompressThumb and recompress options.
211
+ */
212
+ function shouldThumbUseTranscoded(id, options) {
213
+ if (!options.recompressThumb) {
214
+ return false;
215
+ }
216
+
217
+ function isValidTranscoder() {
218
+ const { transferSyntaxUid } = id;
219
+ // Ignore the samples per pixel for thumbnails
220
+ const transcoder = getTranscoder(transferSyntaxUid, options, 'thumbnail');
221
+ const result =
222
+ transcoder &&
223
+ transcoder.transferSyntaxUid &&
224
+ options.recompress.includes(transcoder.alias) &&
225
+ options.recompressThumb.includes(transcoder.alias);
226
+
227
+ return result;
228
+ }
229
+
230
+ return isValidTranscoder();
231
+ }
232
+
233
+ function transcodeLog(options, msg, error = '') {
234
+ if (options.verbose) {
235
+ console.log(`\x1b[34m${msg}\x1b[0m`, error);
236
+ }
237
+ }
238
+
239
+ function scale(imageFrame, imageInfo) {
240
+ const { rows, columns, bitsPerPixel, pixelRepresentation, samplesPerPixel } = imageInfo;
241
+ let ArrayConstructor = Float32Array;
242
+ if (bitsPerPixel === 8) {
243
+ ArrayConstructor = pixelRepresentation ? Int8Array : Uint8Array;
244
+ } else if (bitsPerPixel > 8 && bitsPerPixel <= 16) {
245
+ ArrayConstructor = pixelRepresentation ? Int16Array : Uint16Array;
246
+ }
247
+ const src = {
248
+ pixelData: new ArrayConstructor(imageFrame.buffer),
249
+ rows,
250
+ columns,
251
+ samplesPerPixel,
252
+ };
253
+ const dest = {
254
+ rows: Math.round(rows / 4),
255
+ columns: Math.round(columns / 4),
256
+ samplesPerPixel,
257
+ };
258
+ dest.pixelData = new ArrayConstructor(dest.rows * dest.columns * samplesPerPixel);
259
+ replicate(src, dest);
260
+
261
+ return {
262
+ imageFrame: dest.pixelData,
263
+ imageInfo: {
264
+ ...imageInfo,
265
+ rows: dest.rows,
266
+ columns: dest.columns,
267
+ width: dest.columns,
268
+ height: dest.rows,
269
+ },
270
+ };
271
+ }
272
+
273
+ /** Generates a lossy version of the image.
274
+ * Options for lossy are:
275
+ * * htj2k - generates a full resolution but high loss htj2k representation of the frames
276
+ * * jls - generates a +/- 2 value, maximum 128 on an edge JLS representation
277
+ *
278
+ * In either case, saves it to <sopUID>/fsiz/frameNo as multipart related
279
+ */
280
+ async function generateLossyImage(id, decoded, options) {
281
+ if (!options.alternate) return;
282
+ if (!decoded?.imageFrame) return;
283
+ console.verbose('Writing alternate thumbnail');
284
+
285
+ try {
286
+ let { imageFrame, imageInfo } = decoded;
287
+ const lossyId = {
288
+ ...id,
289
+ imageFrameRootPath: id.imageFrameRootPath.replace('frames', options.alternateName),
290
+ transferSyntaxUid: transcodeDestinationMap.jhc.transferSyntaxUid,
291
+ };
292
+
293
+ let lossy = true;
294
+ if (options.alternateThumbnail && imageInfo.rows >= 512) {
295
+ const scaled = scale(imageFrame, imageInfo);
296
+ if (!scaled) {
297
+ console.log("Couldn't scale");
298
+ return;
299
+ }
300
+ imageFrame = Buffer.from(scaled.imageFrame.buffer);
301
+ imageInfo = scaled.imageInfo;
302
+ }
303
+
304
+ if (options.alternate === 'jls') {
305
+ lossyId.transferSyntaxUid = transcodeDestinationMap['jls-lossy'].transferSyntaxUid;
306
+ } else if (options.alternate === 'jlsLossless') {
307
+ lossyId.transferSyntaxUid = transcodeDestinationMap.jls.transferSyntaxUid;
308
+ lossy = false;
309
+ } else if (options.alternate === 'jhcLossless') {
310
+ lossy = false;
311
+ }
312
+
313
+ const encodeOptions = {
314
+ beforeEncode: transcodeDestinationMap[options.alternate]?.beforeEncode,
315
+ };
316
+
317
+ const lossyEncoding = await dicomCodec.encode(
318
+ imageFrame,
319
+ imageInfo,
320
+ lossyId.transferSyntaxUid,
321
+ encodeOptions
322
+ );
323
+ console.verbose(
324
+ 'Encoded alternate',
325
+ lossyId.transferSyntaxUid,
326
+ 'of size',
327
+ lossyEncoding.imageFrame.length
328
+ );
329
+
330
+ return { id: lossyId, imageFrame: lossyEncoding.imageFrame };
331
+ } catch (e) {
332
+ console.warn('Unable to create alternate:', e);
333
+ }
334
+ }
335
+
336
+ function isPalette(dataSet) {
337
+ return dataSet.string(Tags.RawPhotometricInterpretation) === 'PALETTE COLOR';
338
+ }
339
+
340
+ /**
341
+ * Transcode imageFrame. It uses transcodeMap to define the target encoding based on source encoding.
342
+ *
343
+ * @param {*} id object containing transferSyntaxUid
344
+ * @param {*} targetId target object containing transferSyntaxUid
345
+ * @param {*} imageFrame data to be transcoded
346
+ * @param {*} dataSet related image frame dataset
347
+ * @param {*} options runner options
348
+ * @returns object result for transcoding operation with id and image frame.
349
+ */
350
+ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options = {}) {
351
+ let targetId = targetIdSrc;
352
+ let result = {};
353
+
354
+ const samplesPerPixel = dataSet.uint16(Tags.RawSamplesPerPixel);
355
+ const planarConfiguration = dataSet.uint16('x00280006');
356
+ if (
357
+ !shouldTranscodeImageFrame(id, options, samplesPerPixel) ||
358
+ planarConfiguration === 1 ||
359
+ isPalette(dataSet)
360
+ ) {
361
+ console.verbose("Shouldn't transcode");
362
+ return {
363
+ id,
364
+ imageFrame,
365
+ done: false,
366
+ };
367
+ }
368
+
369
+ const transcoder = getTranscoder(id.transferSyntaxUid, options, samplesPerPixel);
370
+
371
+ // Don't transcode if not required
372
+ if (targetId.transferSyntaxUid !== transcoder.transferSyntaxUid && !options.forceTranscode) {
373
+ console.verbose('Image is already in', targetId.transferSyntaxUid, 'not transcoding');
374
+ return {
375
+ id,
376
+ imageFrame,
377
+ done: false,
378
+ };
379
+ }
380
+
381
+ console.verbose('Transcoding to', transcoder.transferSyntaxUid);
382
+
383
+ const imageInfo = getImageInfo(dataSet);
384
+ let done = false;
385
+ let processResultMsg = '';
386
+ const encodeOptions = {
387
+ beforeEncode: transcoder.beforeEncode,
388
+ };
389
+ let decoded;
390
+
391
+ try {
392
+ switch (transcoder.transcodeOp) {
393
+ case transcodeOp.transcode:
394
+ transcodeLog(
395
+ options,
396
+ `Full transcoding image from \x1b[43m${id.transferSyntaxUid}\x1b[0m to \x1b[43m${targetId.transferSyntaxUid}\x1b[0m`
397
+ );
398
+
399
+ decoded = await dicomCodec.decode(imageFrame, imageInfo, id.transferSyntaxUid);
400
+ result = await dicomCodec.encode(
401
+ decoded.imageFrame,
402
+ decoded.imageInfo,
403
+ targetId.transferSyntaxUid,
404
+ encodeOptions
405
+ );
406
+
407
+ console.verbose(
408
+ 'transcoded image to',
409
+ targetId.transferSyntaxUid,
410
+ 'of size',
411
+ result.imageFrame.length
412
+ );
413
+ processResultMsg = `Transcoding finished`;
414
+ break;
415
+ case transcodeOp.encode:
416
+ transcodeLog(options, `Encoding image to \x1b[43m${targetId.transferSyntaxUid}\x1b[0m`);
417
+
418
+ result = await dicomCodec.encode(
419
+ imageFrame,
420
+ imageInfo,
421
+ targetId.transferSyntaxUid,
422
+ encodeOptions
423
+ );
424
+
425
+ processResultMsg = `Encoding finished`;
426
+ break;
427
+ case transcodeOp.decode:
428
+ transcodeLog(options, `Decoding image from \x1b[43m${id.transferSyntaxUid}\x1b[0m`);
429
+ result = await dicomCodec.decode(imageFrame, imageInfo, id.transferSyntaxUid);
430
+
431
+ processResultMsg = `Decoding finished`;
432
+ break;
433
+ default:
434
+ processResultMsg = '';
435
+ }
436
+
437
+ done = !!result.imageFrame;
438
+ } catch (e) {
439
+ transcodeLog(options, 'Failed to transcode image', e);
440
+ }
441
+
442
+ // recover transfer syntax
443
+ if (!done) {
444
+ targetId = id;
445
+ }
446
+
447
+ // log final message only if resultMsg is present
448
+ if (processResultMsg) {
449
+ processResultMsg += done ? ' successfully' : ' unsuccessfully';
450
+ transcodeLog(options, processResultMsg);
451
+ }
452
+
453
+ const _imageFrame = result.imageFrame ?? imageFrame;
454
+
455
+ return {
456
+ id: targetId,
457
+ imageFrame: _imageFrame,
458
+ done,
459
+ decoded,
460
+ };
461
+ }
462
+
463
+ /**
464
+ * Returns transcoded id object based on the given id's transferSyntaxUid. If transcoding should not occur it will return original id value.
465
+ * It does not mutate id param.
466
+ *
467
+ * @param {*} id Object that contains (but not only) data transferSyntaxUid.
468
+ * @param {*} options runner options.
469
+ * @returns Transcoded id object
470
+ */
471
+ function transcodeId(id, options, samplesPerPixel) {
472
+ if (!shouldTranscodeImageFrame(id, options, samplesPerPixel)) {
473
+ return id;
474
+ }
475
+
476
+ const targetId = { ...id };
477
+ const { transferSyntaxUid } = getTranscoder(id.transferSyntaxUid, options, samplesPerPixel);
478
+
479
+ targetId.transferSyntaxUid = transferSyntaxUid;
480
+
481
+ return targetId;
482
+ }
483
+
484
+ /**
485
+ * Returns transcoded metadata based on the given id's transferSyntaxUid.
486
+ * Basically it changes the metadata AvailableTransferSyntaxUID tag's value to be transcodedId.transferSyntaxUid
487
+ * If transcoding should not occur it will return original metadata value.
488
+ *
489
+ * It does not mutate metadata param.
490
+ * @param {*} metadata Metadata object.
491
+ * @param {*} id Object that contains (but not only) data transferSyntaxUid.
492
+ * @param {*} options runner options.
493
+ * @returns Transcoded metadata.
494
+ */
495
+ function transcodeMetadata(metadata, id, options) {
496
+ const samplesPerPixel = Tags.getValue(metadata, Tags.SamplesPerPixel);
497
+ if (!shouldTranscodeImageFrame(id, options, samplesPerPixel)) {
498
+ return metadata;
499
+ }
500
+
501
+ const transcodedId = transcodeId(id, options, samplesPerPixel);
502
+
503
+ const result = { ...metadata };
504
+
505
+ if (result[Tags.AvailableTransferSyntaxUID]) {
506
+ Tags.setValue(result, Tags.AvailableTransferSyntaxUID, transcodedId.transferSyntaxUid);
507
+ console.verbose('Apply available tsuid', transcodeId.transferSyntaxUid);
508
+ }
509
+
510
+ return result;
511
+ }
512
+
513
+ exports.shouldTranscodeImageFrame = shouldTranscodeImageFrame;
514
+ exports.shouldThumbUseTranscoded = shouldThumbUseTranscoded;
515
+ exports.transcodeId = transcodeId;
516
+ exports.getDestinationTranscoder = getDestinationTranscoder;
517
+ exports.transcodeImageFrame = transcodeImageFrame;
518
+ exports.transcodeMetadata = transcodeMetadata;
519
+ exports.generateLossyImage = generateLossyImage;
@@ -0,0 +1,55 @@
1
+ const { Stats } = require('@radicalimaging/static-wado-util');
2
+
3
+ const getNumberOfFrames = require('./getNumberOfFrames');
4
+ const getUncompressedImageFrame = require('./getUncompressedImageFrame');
5
+ const getEncapsulatedImageFrame = require('./getEncapsulatedImageFrame');
6
+ const { isVideo } = require('../writer/VideoWriter');
7
+
8
+ const areFramesAreFragmented = (attr, numberOfFrames) =>
9
+ attr.encapsulatedPixelData && numberOfFrames != attr.fragments.length;
10
+
11
+ const getFrameSize = dataSet => {
12
+ const rows = dataSet.uint16('x00280010');
13
+ const columns = dataSet.uint16('x00280011');
14
+ const samplesPerPixel = dataSet.uint16('x00280002');
15
+ const bitsAllocated = dataSet.uint16('x00280100');
16
+ return (rows * columns * samplesPerPixel * bitsAllocated) / 8;
17
+ };
18
+
19
+ const extractImageFrames = async (dataSet, attr, vr, callback) => {
20
+ const numberOfFrames = getNumberOfFrames(dataSet);
21
+ const framesAreFragmented = areFramesAreFragmented(attr, numberOfFrames);
22
+ const uncompressedFrameSize = getFrameSize(dataSet);
23
+
24
+ const videoType = isVideo(dataSet);
25
+ if (videoType) {
26
+ // The top level function is asynchronous, so this one doesn't need an additional await
27
+ return callback.videoWriter(dataSet);
28
+ }
29
+
30
+ let BulkDataURI;
31
+
32
+ for (let frameIndex = 0; frameIndex < numberOfFrames; frameIndex++) {
33
+ if (attr.encapsulatedPixelData) {
34
+ const compressedFrame = getEncapsulatedImageFrame(
35
+ dataSet,
36
+ attr,
37
+ frameIndex,
38
+ framesAreFragmented
39
+ );
40
+ BulkDataURI = await callback.imageFrame(compressedFrame, { dataSet });
41
+ Stats.OverallStats.add('Image Write', `Write image frame ${frameIndex + 1}`, 5000);
42
+ } else {
43
+ const uncompressedFrame = getUncompressedImageFrame(
44
+ dataSet,
45
+ attr,
46
+ frameIndex,
47
+ uncompressedFrameSize
48
+ );
49
+ BulkDataURI = await callback.imageFrame(uncompressedFrame, { dataSet });
50
+ }
51
+ }
52
+ return BulkDataURI;
53
+ };
54
+
55
+ module.exports = extractImageFrames;
@@ -0,0 +1,65 @@
1
+ const { Tags } = require('@radicalimaging/static-wado-util');
2
+ const getVR = require('./getVR');
3
+ const getValue = require('./getValue');
4
+
5
+ /**
6
+ * Get dataset.
7
+ *
8
+ * ParentAttr is used to control root/non-root attr scenarios (such as pixel data).
9
+ *
10
+ * @param {*} dataSet
11
+ * @param {*} callback
12
+ * @param {*} options
13
+ * @param {*} parentAttr Parent reference for sequence element tags.
14
+ * @returns
15
+ */
16
+ async function getDataSet(dataSet, callback, optionsOrig, parentAttr = undefined) {
17
+ const metadata = {};
18
+ let options = optionsOrig;
19
+
20
+ // iterate over dataSet attributes in order
21
+ for (const tag in dataSet.elements) {
22
+ // Raw versions have the x in front of them
23
+ if (tag != Tags.RawTransferSyntaxUID && tag >= Tags.RawMinTag && tag < Tags.RawFirstBodyTag) {
24
+ continue;
25
+ }
26
+ if (tag === Tags.RawSpecificCharacterSet) {
27
+ const SpecificCharacterSet = dataSet.string(tag);
28
+ options = { ...options, SpecificCharacterSet };
29
+ }
30
+ const attr = dataSet.elements[tag];
31
+
32
+ await attributeToJS(metadata, tag, dataSet, attr, callback, options, parentAttr);
33
+ }
34
+ if (metadata[Tags.TransferSyntaxUID]) {
35
+ // console.log(`Found tsuid ${JSON.stringify(metadata[Tags.TransferSyntaxUID])} assigning to ${Tags.AvailableTransferSyntaxUID}`)
36
+ metadata[Tags.AvailableTransferSyntaxUID] = metadata[Tags.TransferSyntaxUID];
37
+ delete metadata[Tags.TransferSyntaxUID];
38
+ }
39
+ return { metadata };
40
+ }
41
+
42
+ async function attributeToJS(metadataSrc, tag, dataSet, attr, callback, options, parentAttr) {
43
+ const metadata = metadataSrc;
44
+ const vr = getVR(attr);
45
+ const value = await getValue(dataSet, attr, vr, getDataSet, callback, options, parentAttr);
46
+ const key = tag.substring(1).toUpperCase();
47
+ if (value === undefined || value === null || value.length === 0) {
48
+ if (!vr) return;
49
+ metadata[key] = {
50
+ vr,
51
+ };
52
+ } else if (value.InlineBinary || value.BulkDataURI) {
53
+ metadata[key] = {
54
+ vr,
55
+ ...value,
56
+ };
57
+ } else {
58
+ metadata[key] = {
59
+ vr,
60
+ Value: value,
61
+ };
62
+ }
63
+ }
64
+
65
+ module.exports = getDataSet;
@@ -0,0 +1,35 @@
1
+ const dicomParser = require('dicom-parser');
2
+
3
+ /**
4
+ * Function to deal with extracting an image frame from an encapsulated data set.
5
+ */
6
+
7
+ const getEncapsulatedImageFrame = (dataSet, attr, frameIndex, framesAreFragmented) => {
8
+ if (dataSet.elements.x7fe00010 && dataSet.elements.x7fe00010.basicOffsetTable.length) {
9
+ // Basic Offset Table is not empty
10
+ return dicomParser.readEncapsulatedImageFrame(dataSet, dataSet.elements.x7fe00010, frameIndex);
11
+ }
12
+
13
+ // Empty basic offset table
14
+ if (framesAreFragmented) {
15
+ const basicOffsetTable = dicomParser.createJPEGBasicOffsetTable(
16
+ dataSet,
17
+ dataSet.elements.x7fe00010
18
+ );
19
+
20
+ return dicomParser.readEncapsulatedImageFrame(
21
+ dataSet,
22
+ dataSet.elements.x7fe00010,
23
+ frameIndex,
24
+ basicOffsetTable
25
+ );
26
+ }
27
+
28
+ return dicomParser.readEncapsulatedPixelDataFromFragments(
29
+ dataSet,
30
+ dataSet.elements.x7fe00010,
31
+ frameIndex
32
+ );
33
+ };
34
+
35
+ module.exports = getEncapsulatedImageFrame;
@@ -0,0 +1,13 @@
1
+ const getNumberOfFrames = dataSet => {
2
+ if (dataSet.elements.x00280008) {
3
+ const numberOfFramesString = dataSet.string('x00280008');
4
+ if (numberOfFramesString.length === 0) {
5
+ return 1;
6
+ }
7
+ const numberOfFrames = parseInt(numberOfFramesString);
8
+ return numberOfFrames;
9
+ }
10
+ return 1;
11
+ };
12
+
13
+ module.exports = getNumberOfFrames;
@@ -0,0 +1,7 @@
1
+ function getUncompressedImageFrame(dataSet, attr, frame, uncompressedFrameSize) {
2
+ const start = attr.dataOffset + frame * uncompressedFrameSize;
3
+ const binaryValue = dataSet.byteArray.slice(start, start + uncompressedFrameSize);
4
+ return binaryValue;
5
+ }
6
+
7
+ module.exports = getUncompressedImageFrame;