@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +234 -0
  2. package/README.md +1 -1
  3. package/bin/mkdicomweb.mjs +8 -0
  4. package/dist/canvas-xaqc8v6y.node +0 -0
  5. package/dist/index.js +481518 -0
  6. package/lib/DeleteStudy.js +9 -2
  7. package/lib/RejectInstance.js +15 -3
  8. package/lib/StaticWado.js +245 -41
  9. package/lib/createPart10.js +8 -4
  10. package/lib/createThumbnail.js +181 -0
  11. package/lib/index.js +2 -0
  12. package/lib/instanceMain.js +1 -0
  13. package/lib/mkdicomwebConfig.js +122 -19
  14. package/lib/model/TagLists.js +24 -3
  15. package/lib/model/uids.js +5 -1
  16. package/lib/operation/InstanceDeduplicate.js +14 -4
  17. package/lib/operation/ScanStudy.js +38 -5
  18. package/lib/operation/StudyData.js +158 -38
  19. package/lib/operation/ThumbnailService.js +60 -27
  20. package/lib/operation/adapter/decodeImage.js +5 -2
  21. package/lib/operation/adapter/getImageInfo.js +19 -1
  22. package/lib/operation/adapter/transcodeImage.js +170 -52
  23. package/lib/operation/extractImageFrames.js +19 -4
  24. package/lib/operation/getDataSet.js +41 -7
  25. package/lib/operation/getEncapsulatedImageFrame.js +30 -6
  26. package/lib/operation/getUncompressedImageFrame.js +10 -2
  27. package/lib/operation/getVR.js +10 -2
  28. package/lib/operation/getValue.js +4 -4
  29. package/lib/operation/validateMetadata.js +40 -0
  30. package/lib/program/index.js +5 -3
  31. package/lib/serverMain.js +27 -0
  32. package/lib/util/IdCreator.js +32 -6
  33. package/lib/util/adaptProgramOpts.js +1 -1
  34. package/lib/util/compressionOptionParser.js +12 -3
  35. package/lib/util/resultsReport.ts +88 -0
  36. package/lib/writer/CompleteStudyWriter.js +49 -13
  37. package/lib/writer/DeduplicateWriter.js +2 -1
  38. package/lib/writer/ExpandUriPath.js +4 -1
  39. package/lib/writer/HashDataWriter.js +5 -1
  40. package/lib/writer/ImageFrameWriter.js +38 -13
  41. package/lib/writer/RawDicomWriter.js +11 -2
  42. package/lib/writer/VideoWriter.js +3 -3
  43. package/lib/writer/WriteMultipart.js +3 -1
  44. package/lib/writer/WriteStream.js +12 -4
  45. package/lib/writer/static-dicomweb.code-workspace +8 -0
  46. package/package.json +22 -18
  47. 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 = Object.entries(transcodeDestinationMap).find(([key, value]) => key === id || value.transferSyntaxUid === id);
107
- if (destinationTranscoderEntry) {
108
- return destinationTranscoderEntry[1];
109
- }
110
- return undefined;
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(transferSyntaxUid, { contentType: greyContentType, colorContentType }, samplesPerPixel) {
120
- const contentType = samplesPerPixel === 3 ? colorContentType : greyContentType;
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: sourceTranscoder.transcodeOp | destinationTranscoder.transcodeOp, // eslint-disable-line no-bitwise
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("Not transcoding because no decoder found for", transferSyntaxUid, samplesPerPixel);
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 = recompress.includes("true") || recompress.includes(transcoder.alias);
195
+ const validRecompress =
196
+ recompress.includes("true") || recompress.includes(transcoder.alias);
155
197
  if (!validRecompress) {
156
- console.verbose("Not transcoding because recompress", recompress, "does not include", transcoder.alias);
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 = transcoder && transcoder.transferSyntaxUid && options.recompress.includes(transcoder.alias) && options.recompressThumb.includes(transcoder.alias);
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 } = imageInfo;
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(dest.rows * dest.columns * samplesPerPixel);
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("frames", options.alternateName),
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 = transcodeDestinationMap["jls-lossy"].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.bind(null, {
277
- lossy,
278
- }),
326
+ beforeEncode: transcodeDestinationMap[options.alternate]?.beforeEncode,
279
327
  };
280
- const lossyEncoding = await dicomCodec.encode(imageFrame, imageInfo, lossyId.transferSyntaxUid, encodeOptions);
281
- console.log("Encoded alternate", lossyId.transferSyntaxUid, "of size", lossyEncoding.imageFrame.length);
282
- // eslint-disable-next-line consistent-return
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(id, targetIdSrc, imageFrame, dataSet, options = {}) {
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 (!shouldTranscodeImageFrame(id, options, samplesPerPixel) || planarConfiguration === 1 || isPalette(dataSet)) {
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(id.transferSyntaxUid, options, samplesPerPixel);
387
+ const transcoder = getTranscoder(
388
+ id.transferSyntaxUid,
389
+ options,
390
+ samplesPerPixel,
391
+ );
319
392
 
320
393
  // Don't transcode if not required
321
- if (targetId.transferSyntaxUid !== transcoder.transferSyntaxUid && !options.forceTranscode) {
322
- console.verbose("Image is already in", targetId.transferSyntaxUid, "not transcoding");
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.bind(null, {
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(options, `Full transcoding image from \x1b[43m${id.transferSyntaxUid}\x1b[0m to \x1b[43m${targetId.transferSyntaxUid}\x1b[0m`);
346
-
347
- decoded = await dicomCodec.decode(imageFrame, imageInfo, id.transferSyntaxUid);
348
- result = await dicomCodec.encode(decoded.imageFrame, decoded.imageInfo, targetId.transferSyntaxUid, encodeOptions);
349
-
350
- console.log("transcoded image to", targetId.transferSyntaxUid, "of size", result.imageFrame.length);
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(options, `Encoding image to \x1b[43m${targetId.transferSyntaxUid}\x1b[0m`);
355
-
356
- result = await dicomCodec.encode(imageFrame, imageInfo, targetId.transferSyntaxUid, encodeOptions);
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(options, `Decoding image from \x1b[43m${id.transferSyntaxUid}\x1b[0m`);
362
- result = await dicomCodec.decode(imageFrame, imageInfo, id.transferSyntaxUid);
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(id.transferSyntaxUid, options, samplesPerPixel);
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(result, Tags.AvailableTransferSyntaxUID, transcodedId.transferSyntaxUid);
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) => attr.encapsulatedPixelData && numberOfFrames != attr.fragments.length;
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(dataSet, attr, frameIndex, framesAreFragmented);
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("Image Write", `Write image frame ${frameIndex + 1}`, 5000);
41
+ Stats.OverallStats.add(
42
+ "Image Write",
43
+ `Write image frame ${frameIndex + 1}`,
44
+ 5000,
45
+ );
36
46
  } else {
37
- const uncompressedFrame = getUncompressedImageFrame(dataSet, attr, frameIndex, uncompressedFrameSize);
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(dataSet, callback, optionsOrig, parentAttr = undefined) {
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 (tag != Tags.RawTransferSyntaxUID && tag >= Tags.RawMinTag && tag < Tags.RawFirstBodyTag) {
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
- /* eslint-disable-next-line no-use-before-define */
32
- await attributeToJS(metadata, tag, dataSet, attr, callback, options, parentAttr);
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] = metadata[Tags.TransferSyntaxUID];
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(metadataSrc, tag, dataSet, attr, callback, options, parentAttr) {
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(dataSet, attr, vr, getDataSet, callback, options, parentAttr);
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 = (dataSet, attr, frameIndex, framesAreFragmented) => {
8
- if (dataSet.elements.x7fe00010 && dataSet.elements.x7fe00010.basicOffsetTable.length) {
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(dataSet, dataSet.elements.x7fe00010, frameIndex);
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(dataSet, dataSet.elements.x7fe00010);
27
+ const basicOffsetTable = dicomParser.createJPEGBasicOffsetTable(
28
+ dataSet,
29
+ dataSet.elements.x7fe00010,
30
+ );
16
31
 
17
- return dicomParser.readEncapsulatedImageFrame(dataSet, dataSet.elements.x7fe00010, frameIndex, basicOffsetTable);
32
+ return dicomParser.readEncapsulatedImageFrame(
33
+ dataSet,
34
+ dataSet.elements.x7fe00010,
35
+ frameIndex,
36
+ basicOffsetTable,
37
+ );
18
38
  }
19
39
 
20
- return dicomParser.readEncapsulatedPixelDataFromFragments(dataSet, dataSet.elements.x7fe00010, frameIndex);
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(dataSet, attr, frame, uncompressedFrameSize) {
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(start, start + uncompressedFrameSize);
8
+ const binaryValue = dataSet.byteArray.slice(
9
+ start,
10
+ start + uncompressedFrameSize,
11
+ );
4
12
  return binaryValue;
5
13
  }
6
14
 
@@ -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
- return undefined;
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;
@@ -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(defaults, process.argv);
11
- console.log("Loaded configuration from", configurationFile);
12
- staticWadoUtil.configureCommands(defaults);
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;