@radicalimaging/static-wado-creator 1.5.0 → 1.6.10
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/CHANGELOG.md +234 -0
- package/README.md +1 -1
- package/bin/mkdicomweb.mjs +8 -0
- package/dist/canvas-xaqc8v6y.node +0 -0
- package/dist/index.js +481518 -0
- package/lib/DeleteStudy.js +9 -2
- package/lib/RejectInstance.js +15 -3
- package/lib/StaticWado.js +245 -41
- package/lib/createPart10.js +8 -4
- package/lib/createThumbnail.js +181 -0
- package/lib/index.js +2 -0
- package/lib/instanceMain.js +1 -0
- package/lib/mkdicomwebConfig.js +122 -19
- package/lib/model/TagLists.js +24 -3
- package/lib/model/uids.js +5 -1
- package/lib/operation/InstanceDeduplicate.js +14 -4
- package/lib/operation/ScanStudy.js +38 -5
- package/lib/operation/StudyData.js +158 -38
- package/lib/operation/ThumbnailService.js +60 -27
- package/lib/operation/adapter/decodeImage.js +5 -2
- package/lib/operation/adapter/getImageInfo.js +19 -1
- package/lib/operation/adapter/transcodeImage.js +170 -52
- package/lib/operation/extractImageFrames.js +19 -4
- package/lib/operation/getDataSet.js +41 -7
- package/lib/operation/getEncapsulatedImageFrame.js +30 -6
- package/lib/operation/getUncompressedImageFrame.js +10 -2
- package/lib/operation/getVR.js +10 -2
- package/lib/operation/getValue.js +4 -4
- package/lib/operation/validateMetadata.js +40 -0
- package/lib/program/index.js +5 -3
- package/lib/serverMain.js +27 -0
- package/lib/util/IdCreator.js +32 -6
- package/lib/util/adaptProgramOpts.js +1 -1
- package/lib/util/compressionOptionParser.js +12 -3
- package/lib/util/resultsReport.ts +88 -0
- package/lib/writer/CompleteStudyWriter.js +49 -13
- package/lib/writer/DeduplicateWriter.js +2 -1
- package/lib/writer/ExpandUriPath.js +4 -1
- package/lib/writer/HashDataWriter.js +5 -1
- package/lib/writer/ImageFrameWriter.js +38 -13
- package/lib/writer/RawDicomWriter.js +11 -2
- package/lib/writer/VideoWriter.js +3 -3
- package/lib/writer/WriteMultipart.js +3 -1
- package/lib/writer/WriteStream.js +12 -4
- package/lib/writer/static-dicomweb.code-workspace +8 -0
- package/package.json +22 -18
- package/bin/mkdicomweb.js +0 -9
|
@@ -11,6 +11,26 @@ const transcodeOp = {
|
|
|
11
11
|
transcode: 3, // decode and then encode
|
|
12
12
|
};
|
|
13
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
|
+
|
|
14
34
|
/**
|
|
15
35
|
* Static mapping for transcoding decoder destination names/uids - currently only lei, jls and jls-lossy appear to work.
|
|
16
36
|
*/
|
|
@@ -22,10 +42,12 @@ const transcodeDestinationMap = {
|
|
|
22
42
|
jls: {
|
|
23
43
|
transferSyntaxUid: "1.2.840.10008.1.2.4.80",
|
|
24
44
|
transcodeOp: transcodeOp.encode,
|
|
45
|
+
beforeEncode: jlsBeforeEncode.bind(null, { lossy: false }),
|
|
25
46
|
},
|
|
26
47
|
"jls-lossy": {
|
|
27
48
|
transferSyntaxUid: "1.2.840.10008.1.2.4.81",
|
|
28
49
|
transcodeOp: transcodeOp.encode,
|
|
50
|
+
beforeEncode: jlsBeforeEncode.bind(null, { lossy: true, delta: 3 }),
|
|
29
51
|
},
|
|
30
52
|
jp2: {
|
|
31
53
|
transferSyntaxUid: "1.2.840.10008.1.2.4.90",
|
|
@@ -36,12 +58,19 @@ const transcodeDestinationMap = {
|
|
|
36
58
|
transcodeOp: transcodeOp.encode,
|
|
37
59
|
},
|
|
38
60
|
jhc: {
|
|
61
|
+
transferSyntaxUid: "1.2.840.10008.1.2.4.201",
|
|
62
|
+
transcodeOp: transcodeOp.encode,
|
|
63
|
+
beforeEncode: htj2kBeforeEncode({}),
|
|
64
|
+
},
|
|
65
|
+
"jhc-lossy": {
|
|
39
66
|
transferSyntaxUid: "1.2.840.10008.1.2.4.202",
|
|
40
67
|
transcodeOp: transcodeOp.encode,
|
|
68
|
+
beforeEncode: htj2kBeforeEncode({ lossy: true }),
|
|
41
69
|
},
|
|
42
70
|
jpeg: {
|
|
43
71
|
transferSyntaxUid: "1.2.840.10008.1.2.4.50",
|
|
44
72
|
transcodeOp: transcodeOp.encode,
|
|
73
|
+
// beforeEncode: jpegBeforeEncode({}),
|
|
45
74
|
},
|
|
46
75
|
};
|
|
47
76
|
|
|
@@ -103,11 +132,12 @@ const transcodeSourceMap = {
|
|
|
103
132
|
* @returns A partial transcoder definition. Otherwise it returns undefined.
|
|
104
133
|
*/
|
|
105
134
|
function getDestinationTranscoder(id) {
|
|
106
|
-
const destinationTranscoderEntry =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
135
|
+
const destinationTranscoderEntry =
|
|
136
|
+
transcodeDestinationMap[id] ||
|
|
137
|
+
Object.values(transcodeDestinationMap).find(
|
|
138
|
+
(value) => value.transferSyntaxUid === id,
|
|
139
|
+
);
|
|
140
|
+
return destinationTranscoderEntry;
|
|
111
141
|
}
|
|
112
142
|
|
|
113
143
|
/**
|
|
@@ -116,8 +146,13 @@ function getDestinationTranscoder(id) {
|
|
|
116
146
|
* @param {*} transferSyntaxUid
|
|
117
147
|
* @returns
|
|
118
148
|
*/
|
|
119
|
-
function getTranscoder(
|
|
120
|
-
|
|
149
|
+
function getTranscoder(
|
|
150
|
+
transferSyntaxUid,
|
|
151
|
+
{ contentType: greyContentType, colorContentType },
|
|
152
|
+
samplesPerPixel,
|
|
153
|
+
) {
|
|
154
|
+
const contentType =
|
|
155
|
+
samplesPerPixel === 3 ? colorContentType : greyContentType;
|
|
121
156
|
const sourceTranscoder = transcodeSourceMap[transferSyntaxUid];
|
|
122
157
|
const destinationTranscoder = getDestinationTranscoder(contentType);
|
|
123
158
|
if (!sourceTranscoder || !destinationTranscoder) {
|
|
@@ -126,8 +161,10 @@ function getTranscoder(transferSyntaxUid, { contentType: greyContentType, colorC
|
|
|
126
161
|
|
|
127
162
|
return {
|
|
128
163
|
transferSyntaxUid: destinationTranscoder.transferSyntaxUid,
|
|
129
|
-
transcodeOp:
|
|
164
|
+
transcodeOp:
|
|
165
|
+
sourceTranscoder.transcodeOp | destinationTranscoder.transcodeOp,
|
|
130
166
|
alias: sourceTranscoder.alias,
|
|
167
|
+
beforeEncode: destinationTranscoder.beforeEncode,
|
|
131
168
|
};
|
|
132
169
|
}
|
|
133
170
|
|
|
@@ -148,12 +185,22 @@ function shouldTranscodeImageFrame(id, options, samplesPerPixel) {
|
|
|
148
185
|
const transcoder = getTranscoder(transferSyntaxUid, options, samplesPerPixel);
|
|
149
186
|
const validTranscoder = transcoder && transcoder.transferSyntaxUid;
|
|
150
187
|
if (!validTranscoder) {
|
|
151
|
-
console.verbose(
|
|
188
|
+
console.verbose(
|
|
189
|
+
"Not transcoding because no decoder found for",
|
|
190
|
+
transferSyntaxUid,
|
|
191
|
+
samplesPerPixel,
|
|
192
|
+
);
|
|
152
193
|
return false;
|
|
153
194
|
}
|
|
154
|
-
const validRecompress =
|
|
195
|
+
const validRecompress =
|
|
196
|
+
recompress.includes("true") || recompress.includes(transcoder.alias);
|
|
155
197
|
if (!validRecompress) {
|
|
156
|
-
console.verbose(
|
|
198
|
+
console.verbose(
|
|
199
|
+
"Not transcoding because recompress",
|
|
200
|
+
recompress,
|
|
201
|
+
"does not include",
|
|
202
|
+
transcoder.alias,
|
|
203
|
+
);
|
|
157
204
|
return false;
|
|
158
205
|
}
|
|
159
206
|
|
|
@@ -176,7 +223,11 @@ function shouldThumbUseTranscoded(id, options) {
|
|
|
176
223
|
const { transferSyntaxUid } = id;
|
|
177
224
|
// Ignore the samples per pixel for thumbnails
|
|
178
225
|
const transcoder = getTranscoder(transferSyntaxUid, options, "thumbnail");
|
|
179
|
-
const result =
|
|
226
|
+
const result =
|
|
227
|
+
transcoder &&
|
|
228
|
+
transcoder.transferSyntaxUid &&
|
|
229
|
+
options.recompress.includes(transcoder.alias) &&
|
|
230
|
+
options.recompressThumb.includes(transcoder.alias);
|
|
180
231
|
|
|
181
232
|
return result;
|
|
182
233
|
}
|
|
@@ -190,17 +241,9 @@ function transcodeLog(options, msg, error = "") {
|
|
|
190
241
|
}
|
|
191
242
|
}
|
|
192
243
|
|
|
193
|
-
const beforeEncode = (options, encoder) => {
|
|
194
|
-
const lossy = !!options.lossy;
|
|
195
|
-
const quality = lossy ? 0.002 : -1;
|
|
196
|
-
const delta = lossy ? 3 : 0;
|
|
197
|
-
// First value is to encode as reversible lossless colour
|
|
198
|
-
encoder.setQuality?.(!lossy, quality);
|
|
199
|
-
encoder.setNearLossless?.(delta);
|
|
200
|
-
};
|
|
201
|
-
|
|
202
244
|
function scale(imageFrame, imageInfo) {
|
|
203
|
-
const { rows, columns, bitsPerPixel, pixelRepresentation, samplesPerPixel } =
|
|
245
|
+
const { rows, columns, bitsPerPixel, pixelRepresentation, samplesPerPixel } =
|
|
246
|
+
imageInfo;
|
|
204
247
|
let ArrayConstructor = Float32Array;
|
|
205
248
|
if (bitsPerPixel === 8) {
|
|
206
249
|
ArrayConstructor = pixelRepresentation ? Int8Array : Uint8Array;
|
|
@@ -218,7 +261,9 @@ function scale(imageFrame, imageInfo) {
|
|
|
218
261
|
columns: Math.round(columns / 4),
|
|
219
262
|
samplesPerPixel,
|
|
220
263
|
};
|
|
221
|
-
dest.pixelData = new ArrayConstructor(
|
|
264
|
+
dest.pixelData = new ArrayConstructor(
|
|
265
|
+
dest.rows * dest.columns * samplesPerPixel,
|
|
266
|
+
);
|
|
222
267
|
replicate(src, dest);
|
|
223
268
|
|
|
224
269
|
return {
|
|
@@ -243,12 +288,16 @@ function scale(imageFrame, imageInfo) {
|
|
|
243
288
|
async function generateLossyImage(id, decoded, options) {
|
|
244
289
|
if (!options.alternate) return;
|
|
245
290
|
if (!decoded?.imageFrame) return;
|
|
291
|
+
console.verbose("Writing alternate thumbnail");
|
|
246
292
|
|
|
247
293
|
try {
|
|
248
294
|
let { imageFrame, imageInfo } = decoded;
|
|
249
295
|
const lossyId = {
|
|
250
296
|
...id,
|
|
251
|
-
imageFrameRootPath: id.imageFrameRootPath.replace(
|
|
297
|
+
imageFrameRootPath: id.imageFrameRootPath.replace(
|
|
298
|
+
"frames",
|
|
299
|
+
options.alternateName,
|
|
300
|
+
),
|
|
252
301
|
transferSyntaxUid: transcodeDestinationMap.jhc.transferSyntaxUid,
|
|
253
302
|
};
|
|
254
303
|
|
|
@@ -264,7 +313,8 @@ async function generateLossyImage(id, decoded, options) {
|
|
|
264
313
|
}
|
|
265
314
|
|
|
266
315
|
if (options.alternate === "jls") {
|
|
267
|
-
lossyId.transferSyntaxUid =
|
|
316
|
+
lossyId.transferSyntaxUid =
|
|
317
|
+
transcodeDestinationMap["jls-lossy"].transferSyntaxUid;
|
|
268
318
|
} else if (options.alternate === "jlsLossless") {
|
|
269
319
|
lossyId.transferSyntaxUid = transcodeDestinationMap.jls.transferSyntaxUid;
|
|
270
320
|
lossy = false;
|
|
@@ -273,13 +323,22 @@ async function generateLossyImage(id, decoded, options) {
|
|
|
273
323
|
}
|
|
274
324
|
|
|
275
325
|
const encodeOptions = {
|
|
276
|
-
beforeEncode: beforeEncode
|
|
277
|
-
lossy,
|
|
278
|
-
}),
|
|
326
|
+
beforeEncode: transcodeDestinationMap[options.alternate]?.beforeEncode,
|
|
279
327
|
};
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
328
|
+
|
|
329
|
+
const lossyEncoding = await dicomCodec.encode(
|
|
330
|
+
imageFrame,
|
|
331
|
+
imageInfo,
|
|
332
|
+
lossyId.transferSyntaxUid,
|
|
333
|
+
encodeOptions,
|
|
334
|
+
);
|
|
335
|
+
console.verbose(
|
|
336
|
+
"Encoded alternate",
|
|
337
|
+
lossyId.transferSyntaxUid,
|
|
338
|
+
"of size",
|
|
339
|
+
lossyEncoding.imageFrame.length,
|
|
340
|
+
);
|
|
341
|
+
|
|
283
342
|
return { id: lossyId, imageFrame: lossyEncoding.imageFrame };
|
|
284
343
|
} catch (e) {
|
|
285
344
|
console.warn("Unable to create alternate:", e);
|
|
@@ -300,13 +359,23 @@ function isPalette(dataSet) {
|
|
|
300
359
|
* @param {*} options runner options
|
|
301
360
|
* @returns object result for transcoding operation with id and image frame.
|
|
302
361
|
*/
|
|
303
|
-
async function transcodeImageFrame(
|
|
362
|
+
async function transcodeImageFrame(
|
|
363
|
+
id,
|
|
364
|
+
targetIdSrc,
|
|
365
|
+
imageFrame,
|
|
366
|
+
dataSet,
|
|
367
|
+
options = {},
|
|
368
|
+
) {
|
|
304
369
|
let targetId = targetIdSrc;
|
|
305
370
|
let result = {};
|
|
306
371
|
|
|
307
372
|
const samplesPerPixel = dataSet.uint16(Tags.RawSamplesPerPixel);
|
|
308
373
|
const planarConfiguration = dataSet.uint16("x00280006");
|
|
309
|
-
if (
|
|
374
|
+
if (
|
|
375
|
+
!shouldTranscodeImageFrame(id, options, samplesPerPixel) ||
|
|
376
|
+
planarConfiguration === 1 ||
|
|
377
|
+
isPalette(dataSet)
|
|
378
|
+
) {
|
|
310
379
|
console.verbose("Shouldn't transcode");
|
|
311
380
|
return {
|
|
312
381
|
id,
|
|
@@ -315,11 +384,22 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
|
|
|
315
384
|
};
|
|
316
385
|
}
|
|
317
386
|
|
|
318
|
-
const transcoder = getTranscoder(
|
|
387
|
+
const transcoder = getTranscoder(
|
|
388
|
+
id.transferSyntaxUid,
|
|
389
|
+
options,
|
|
390
|
+
samplesPerPixel,
|
|
391
|
+
);
|
|
319
392
|
|
|
320
393
|
// Don't transcode if not required
|
|
321
|
-
if (
|
|
322
|
-
|
|
394
|
+
if (
|
|
395
|
+
targetId.transferSyntaxUid !== transcoder.transferSyntaxUid &&
|
|
396
|
+
!options.forceTranscode
|
|
397
|
+
) {
|
|
398
|
+
console.verbose(
|
|
399
|
+
"Image is already in",
|
|
400
|
+
targetId.transferSyntaxUid,
|
|
401
|
+
"not transcoding",
|
|
402
|
+
);
|
|
323
403
|
return {
|
|
324
404
|
id,
|
|
325
405
|
imageFrame,
|
|
@@ -333,33 +413,63 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
|
|
|
333
413
|
let done = false;
|
|
334
414
|
let processResultMsg = "";
|
|
335
415
|
const encodeOptions = {
|
|
336
|
-
beforeEncode: beforeEncode
|
|
337
|
-
lossy: options.lossy,
|
|
338
|
-
}),
|
|
416
|
+
beforeEncode: transcoder.beforeEncode,
|
|
339
417
|
};
|
|
340
418
|
let decoded;
|
|
341
419
|
|
|
342
420
|
try {
|
|
343
421
|
switch (transcoder.transcodeOp) {
|
|
344
422
|
case transcodeOp.transcode:
|
|
345
|
-
transcodeLog(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
423
|
+
transcodeLog(
|
|
424
|
+
options,
|
|
425
|
+
`Full transcoding image from \x1b[43m${id.transferSyntaxUid}\x1b[0m to \x1b[43m${targetId.transferSyntaxUid}\x1b[0m`,
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
decoded = await dicomCodec.decode(
|
|
429
|
+
imageFrame,
|
|
430
|
+
imageInfo,
|
|
431
|
+
id.transferSyntaxUid,
|
|
432
|
+
);
|
|
433
|
+
result = await dicomCodec.encode(
|
|
434
|
+
decoded.imageFrame,
|
|
435
|
+
decoded.imageInfo,
|
|
436
|
+
targetId.transferSyntaxUid,
|
|
437
|
+
encodeOptions,
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
console.verbose(
|
|
441
|
+
"transcoded image to",
|
|
442
|
+
targetId.transferSyntaxUid,
|
|
443
|
+
"of size",
|
|
444
|
+
result.imageFrame.length,
|
|
445
|
+
);
|
|
351
446
|
processResultMsg = `Transcoding finished`;
|
|
352
447
|
break;
|
|
353
448
|
case transcodeOp.encode:
|
|
354
|
-
transcodeLog(
|
|
355
|
-
|
|
356
|
-
|
|
449
|
+
transcodeLog(
|
|
450
|
+
options,
|
|
451
|
+
`Encoding image to \x1b[43m${targetId.transferSyntaxUid}\x1b[0m`,
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
result = await dicomCodec.encode(
|
|
455
|
+
imageFrame,
|
|
456
|
+
imageInfo,
|
|
457
|
+
targetId.transferSyntaxUid,
|
|
458
|
+
encodeOptions,
|
|
459
|
+
);
|
|
357
460
|
|
|
358
461
|
processResultMsg = `Encoding finished`;
|
|
359
462
|
break;
|
|
360
463
|
case transcodeOp.decode:
|
|
361
|
-
transcodeLog(
|
|
362
|
-
|
|
464
|
+
transcodeLog(
|
|
465
|
+
options,
|
|
466
|
+
`Decoding image from \x1b[43m${id.transferSyntaxUid}\x1b[0m`,
|
|
467
|
+
);
|
|
468
|
+
result = await dicomCodec.decode(
|
|
469
|
+
imageFrame,
|
|
470
|
+
imageInfo,
|
|
471
|
+
id.transferSyntaxUid,
|
|
472
|
+
);
|
|
363
473
|
|
|
364
474
|
processResultMsg = `Decoding finished`;
|
|
365
475
|
break;
|
|
@@ -407,7 +517,11 @@ function transcodeId(id, options, samplesPerPixel) {
|
|
|
407
517
|
}
|
|
408
518
|
|
|
409
519
|
const targetId = { ...id };
|
|
410
|
-
const { transferSyntaxUid } = getTranscoder(
|
|
520
|
+
const { transferSyntaxUid } = getTranscoder(
|
|
521
|
+
id.transferSyntaxUid,
|
|
522
|
+
options,
|
|
523
|
+
samplesPerPixel,
|
|
524
|
+
);
|
|
411
525
|
|
|
412
526
|
targetId.transferSyntaxUid = transferSyntaxUid;
|
|
413
527
|
|
|
@@ -436,7 +550,11 @@ function transcodeMetadata(metadata, id, options) {
|
|
|
436
550
|
const result = { ...metadata };
|
|
437
551
|
|
|
438
552
|
if (result[Tags.AvailableTransferSyntaxUID]) {
|
|
439
|
-
Tags.setValue(
|
|
553
|
+
Tags.setValue(
|
|
554
|
+
result,
|
|
555
|
+
Tags.AvailableTransferSyntaxUID,
|
|
556
|
+
transcodedId.transferSyntaxUid,
|
|
557
|
+
);
|
|
440
558
|
console.verbose("Apply available tsuid", transcodeId.transferSyntaxUid);
|
|
441
559
|
}
|
|
442
560
|
|
|
@@ -5,7 +5,8 @@ const getUncompressedImageFrame = require("./getUncompressedImageFrame");
|
|
|
5
5
|
const getEncapsulatedImageFrame = require("./getEncapsulatedImageFrame");
|
|
6
6
|
const { isVideo } = require("../writer/VideoWriter");
|
|
7
7
|
|
|
8
|
-
const areFramesAreFragmented = (attr, numberOfFrames) =>
|
|
8
|
+
const areFramesAreFragmented = (attr, numberOfFrames) =>
|
|
9
|
+
attr.encapsulatedPixelData && numberOfFrames != attr.fragments.length;
|
|
9
10
|
|
|
10
11
|
const getFrameSize = (dataSet) => {
|
|
11
12
|
const rows = dataSet.uint16("x00280010");
|
|
@@ -30,11 +31,25 @@ const extractImageFrames = async (dataSet, attr, vr, callback) => {
|
|
|
30
31
|
|
|
31
32
|
for (let frameIndex = 0; frameIndex < numberOfFrames; frameIndex++) {
|
|
32
33
|
if (attr.encapsulatedPixelData) {
|
|
33
|
-
const compressedFrame = getEncapsulatedImageFrame(
|
|
34
|
+
const compressedFrame = getEncapsulatedImageFrame(
|
|
35
|
+
dataSet,
|
|
36
|
+
attr,
|
|
37
|
+
frameIndex,
|
|
38
|
+
framesAreFragmented,
|
|
39
|
+
);
|
|
34
40
|
BulkDataURI = await callback.imageFrame(compressedFrame, { dataSet });
|
|
35
|
-
Stats.OverallStats.add(
|
|
41
|
+
Stats.OverallStats.add(
|
|
42
|
+
"Image Write",
|
|
43
|
+
`Write image frame ${frameIndex + 1}`,
|
|
44
|
+
5000,
|
|
45
|
+
);
|
|
36
46
|
} else {
|
|
37
|
-
const uncompressedFrame = getUncompressedImageFrame(
|
|
47
|
+
const uncompressedFrame = getUncompressedImageFrame(
|
|
48
|
+
dataSet,
|
|
49
|
+
attr,
|
|
50
|
+
frameIndex,
|
|
51
|
+
uncompressedFrameSize,
|
|
52
|
+
);
|
|
38
53
|
BulkDataURI = await callback.imageFrame(uncompressedFrame, { dataSet });
|
|
39
54
|
}
|
|
40
55
|
}
|
|
@@ -13,14 +13,23 @@ const getValue = require("./getValue");
|
|
|
13
13
|
* @param {*} parentAttr Parent reference for sequence element tags.
|
|
14
14
|
* @returns
|
|
15
15
|
*/
|
|
16
|
-
async function getDataSet(
|
|
16
|
+
async function getDataSet(
|
|
17
|
+
dataSet,
|
|
18
|
+
callback,
|
|
19
|
+
optionsOrig,
|
|
20
|
+
parentAttr = undefined,
|
|
21
|
+
) {
|
|
17
22
|
const metadata = {};
|
|
18
23
|
let options = optionsOrig;
|
|
19
24
|
|
|
20
25
|
// iterate over dataSet attributes in order
|
|
21
26
|
for (const tag in dataSet.elements) {
|
|
22
27
|
// Raw versions have the x in front of them
|
|
23
|
-
if (
|
|
28
|
+
if (
|
|
29
|
+
tag != Tags.RawTransferSyntaxUID &&
|
|
30
|
+
tag >= Tags.RawMinTag &&
|
|
31
|
+
tag < Tags.RawFirstBodyTag
|
|
32
|
+
) {
|
|
24
33
|
continue;
|
|
25
34
|
}
|
|
26
35
|
if (tag === Tags.RawSpecificCharacterSet) {
|
|
@@ -28,21 +37,46 @@ async function getDataSet(dataSet, callback, optionsOrig, parentAttr = undefined
|
|
|
28
37
|
options = { ...options, SpecificCharacterSet };
|
|
29
38
|
}
|
|
30
39
|
const attr = dataSet.elements[tag];
|
|
31
|
-
|
|
32
|
-
await attributeToJS(
|
|
40
|
+
|
|
41
|
+
await attributeToJS(
|
|
42
|
+
metadata,
|
|
43
|
+
tag,
|
|
44
|
+
dataSet,
|
|
45
|
+
attr,
|
|
46
|
+
callback,
|
|
47
|
+
options,
|
|
48
|
+
parentAttr,
|
|
49
|
+
);
|
|
33
50
|
}
|
|
34
51
|
if (metadata[Tags.TransferSyntaxUID]) {
|
|
35
52
|
// console.log(`Found tsuid ${JSON.stringify(metadata[Tags.TransferSyntaxUID])} assigning to ${Tags.AvailableTransferSyntaxUID}`)
|
|
36
|
-
metadata[Tags.AvailableTransferSyntaxUID] =
|
|
53
|
+
metadata[Tags.AvailableTransferSyntaxUID] =
|
|
54
|
+
metadata[Tags.TransferSyntaxUID];
|
|
37
55
|
delete metadata[Tags.TransferSyntaxUID];
|
|
38
56
|
}
|
|
39
57
|
return { metadata };
|
|
40
58
|
}
|
|
41
59
|
|
|
42
|
-
async function attributeToJS(
|
|
60
|
+
async function attributeToJS(
|
|
61
|
+
metadataSrc,
|
|
62
|
+
tag,
|
|
63
|
+
dataSet,
|
|
64
|
+
attr,
|
|
65
|
+
callback,
|
|
66
|
+
options,
|
|
67
|
+
parentAttr,
|
|
68
|
+
) {
|
|
43
69
|
const metadata = metadataSrc;
|
|
44
70
|
const vr = getVR(attr);
|
|
45
|
-
const value = await getValue(
|
|
71
|
+
const value = await getValue(
|
|
72
|
+
dataSet,
|
|
73
|
+
attr,
|
|
74
|
+
vr,
|
|
75
|
+
getDataSet,
|
|
76
|
+
callback,
|
|
77
|
+
options,
|
|
78
|
+
parentAttr,
|
|
79
|
+
);
|
|
46
80
|
const key = tag.substring(1).toUpperCase();
|
|
47
81
|
if (value === undefined || value === null || value.length === 0) {
|
|
48
82
|
if (!vr) return;
|
|
@@ -4,20 +4,44 @@ const dicomParser = require("dicom-parser");
|
|
|
4
4
|
* Function to deal with extracting an image frame from an encapsulated data set.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const getEncapsulatedImageFrame = (
|
|
8
|
-
|
|
7
|
+
const getEncapsulatedImageFrame = (
|
|
8
|
+
dataSet,
|
|
9
|
+
attr,
|
|
10
|
+
frameIndex,
|
|
11
|
+
framesAreFragmented,
|
|
12
|
+
) => {
|
|
13
|
+
if (
|
|
14
|
+
dataSet.elements.x7fe00010 &&
|
|
15
|
+
dataSet.elements.x7fe00010.basicOffsetTable.length
|
|
16
|
+
) {
|
|
9
17
|
// Basic Offset Table is not empty
|
|
10
|
-
return dicomParser.readEncapsulatedImageFrame(
|
|
18
|
+
return dicomParser.readEncapsulatedImageFrame(
|
|
19
|
+
dataSet,
|
|
20
|
+
dataSet.elements.x7fe00010,
|
|
21
|
+
frameIndex,
|
|
22
|
+
);
|
|
11
23
|
}
|
|
12
24
|
|
|
13
25
|
// Empty basic offset table
|
|
14
26
|
if (framesAreFragmented) {
|
|
15
|
-
const basicOffsetTable = dicomParser.createJPEGBasicOffsetTable(
|
|
27
|
+
const basicOffsetTable = dicomParser.createJPEGBasicOffsetTable(
|
|
28
|
+
dataSet,
|
|
29
|
+
dataSet.elements.x7fe00010,
|
|
30
|
+
);
|
|
16
31
|
|
|
17
|
-
return dicomParser.readEncapsulatedImageFrame(
|
|
32
|
+
return dicomParser.readEncapsulatedImageFrame(
|
|
33
|
+
dataSet,
|
|
34
|
+
dataSet.elements.x7fe00010,
|
|
35
|
+
frameIndex,
|
|
36
|
+
basicOffsetTable,
|
|
37
|
+
);
|
|
18
38
|
}
|
|
19
39
|
|
|
20
|
-
return dicomParser.readEncapsulatedPixelDataFromFragments(
|
|
40
|
+
return dicomParser.readEncapsulatedPixelDataFromFragments(
|
|
41
|
+
dataSet,
|
|
42
|
+
dataSet.elements.x7fe00010,
|
|
43
|
+
frameIndex,
|
|
44
|
+
);
|
|
21
45
|
};
|
|
22
46
|
|
|
23
47
|
module.exports = getEncapsulatedImageFrame;
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
function getUncompressedImageFrame(
|
|
1
|
+
function getUncompressedImageFrame(
|
|
2
|
+
dataSet,
|
|
3
|
+
attr,
|
|
4
|
+
frame,
|
|
5
|
+
uncompressedFrameSize,
|
|
6
|
+
) {
|
|
2
7
|
const start = attr.dataOffset + frame * uncompressedFrameSize;
|
|
3
|
-
const binaryValue = dataSet.byteArray.slice(
|
|
8
|
+
const binaryValue = dataSet.byteArray.slice(
|
|
9
|
+
start,
|
|
10
|
+
start + uncompressedFrameSize,
|
|
11
|
+
);
|
|
4
12
|
return binaryValue;
|
|
5
13
|
}
|
|
6
14
|
|
package/lib/operation/getVR.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const { dataDictionary } = require("@radicalimaging/static-wado-util");
|
|
2
2
|
|
|
3
|
+
const nullVrTags = new Set();
|
|
4
|
+
nullVrTags.add("FFFEE00D");
|
|
5
|
+
|
|
3
6
|
const getVR = (attr) => {
|
|
4
7
|
if (attr.vr) {
|
|
5
8
|
return attr.vr;
|
|
@@ -7,10 +10,15 @@ const getVR = (attr) => {
|
|
|
7
10
|
// lookup the vr using the data dictionary
|
|
8
11
|
const tag = attr.tag.substring(1).toUpperCase();
|
|
9
12
|
const dataDictAttr = dataDictionary[tag];
|
|
10
|
-
if (dataDictAttr) {
|
|
13
|
+
if (dataDictAttr?.vr) {
|
|
11
14
|
return dataDictAttr.vr;
|
|
12
15
|
}
|
|
13
|
-
|
|
16
|
+
if (nullVrTags.has(tag)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
console.log("Unknown VR", tag, attr, dataDictAttr);
|
|
21
|
+
return "UN";
|
|
14
22
|
};
|
|
15
23
|
|
|
16
24
|
module.exports = getVR;
|
|
@@ -32,7 +32,7 @@ const getValueInlineBinary = (dataSet, attr) => {
|
|
|
32
32
|
if (attr.BulkDataURI) return { BulkDataURI: attr.BulkDataURI };
|
|
33
33
|
const binaryValue = dataSet.byteArray.slice(
|
|
34
34
|
attr.dataOffset,
|
|
35
|
-
attr.dataOffset + attr.length
|
|
35
|
+
attr.dataOffset + attr.length,
|
|
36
36
|
);
|
|
37
37
|
return { InlineBinary: binaryValue.toString("base64") };
|
|
38
38
|
};
|
|
@@ -194,7 +194,7 @@ const getValue = async (
|
|
|
194
194
|
getDataSet,
|
|
195
195
|
callback,
|
|
196
196
|
options,
|
|
197
|
-
parentAttr
|
|
197
|
+
parentAttr,
|
|
198
198
|
) => {
|
|
199
199
|
// It will only process pixelData tag if on metadata root. Otherwise it will be skiped.
|
|
200
200
|
if (attr.tag === "x7fe00010" && !parentAttr) {
|
|
@@ -203,7 +203,7 @@ const getValue = async (
|
|
|
203
203
|
attr,
|
|
204
204
|
vr,
|
|
205
205
|
callback,
|
|
206
|
-
options
|
|
206
|
+
options,
|
|
207
207
|
);
|
|
208
208
|
return { BulkDataURI };
|
|
209
209
|
}
|
|
@@ -229,7 +229,7 @@ const getValue = async (
|
|
|
229
229
|
}
|
|
230
230
|
const binaryValue = dataSet.byteArray.slice(
|
|
231
231
|
attr.dataOffset,
|
|
232
|
-
attr.dataOffset + attr.length
|
|
232
|
+
attr.dataOffset + attr.length,
|
|
233
233
|
);
|
|
234
234
|
const mimeType = attr.tag == "x00420011" && dataSet.string("x00420012");
|
|
235
235
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { Tags } = require("@radicalimaging/static-wado-util");
|
|
2
|
+
|
|
3
|
+
function validateMetadata(options) {
|
|
4
|
+
const serviceUrl = "";
|
|
5
|
+
const { verify, multipart: stowResponse } = options;
|
|
6
|
+
|
|
7
|
+
return function (id, result) {
|
|
8
|
+
const getValue = Tags.getValue.bind(null, result.metadata);
|
|
9
|
+
if (verify) {
|
|
10
|
+
console.verbose("Validating metadata", id);
|
|
11
|
+
if (id.studyInstanceUid.length > 64) {
|
|
12
|
+
throw new Error(`StudyInstanceUID too long: ${id.studyInstanceUid}`);
|
|
13
|
+
}
|
|
14
|
+
if (id.seriesInstanceUid.length > 64) {
|
|
15
|
+
throw new Error(`SeriesInstanceUID too long: ${id.studyInstanceUid}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (stowResponse) {
|
|
19
|
+
const RetrieveURL = `${serviceUrl}${id.studyInstanceUid}/series/${id.seriesInstanceUid}/instances/${id.sopInstanceUid}`;
|
|
20
|
+
|
|
21
|
+
// TODO - add check here from plugin to add remote query checks, eg kheops or similar
|
|
22
|
+
const { filename } = id;
|
|
23
|
+
const successMessage = {
|
|
24
|
+
ReferencedSOPSequence: [
|
|
25
|
+
{
|
|
26
|
+
StudyInstanceUID: id.studyInstanceUid,
|
|
27
|
+
SeriesInstanceUID: id.seriesInstanceUid,
|
|
28
|
+
ReferencedSOPClassUID: getValue(Tags.SOPClassUID),
|
|
29
|
+
ReferencedSOPInstanceUID: id.sopInstanceUid,
|
|
30
|
+
RetrieveURL,
|
|
31
|
+
StorageURL: filename,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
this.success("instanceStored", successMessage, filename);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = validateMetadata;
|
package/lib/program/index.js
CHANGED
|
@@ -7,9 +7,11 @@ const staticWadoUtil = require("@radicalimaging/static-wado-util");
|
|
|
7
7
|
* @returns Program object
|
|
8
8
|
*/
|
|
9
9
|
async function configureProgram(defaults) {
|
|
10
|
-
const configurationFile = await staticWadoUtil.loadConfiguration(
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const configurationFile = await staticWadoUtil.loadConfiguration(
|
|
11
|
+
defaults,
|
|
12
|
+
process.argv,
|
|
13
|
+
);
|
|
14
|
+
return staticWadoUtil.configureCommands(defaults);
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
exports.configureProgram = configureProgram;
|