@radicalimaging/static-wado-creator 1.3.2 → 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 (48) 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 +65 -31
  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 +174 -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 +40 -12
  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 +19 -7
  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 +23 -19
  47. package/bin/mkdicomweb.js +0 -9
  48. package/license.md +0 -21
@@ -7,8 +7,15 @@ module.exports = (options) =>
7
7
  console.log("Delete Study", studyInstanceUid);
8
8
  const studyData = await this.scanStudy("studies", studyInstanceUid);
9
9
  studyData.delete();
10
- const allStudies = await JSONReader(options.directoryName, "studies/index.json.gz", []);
11
- const studiesWithoutDeleted = allStudies.filter((study) => studyInstanceUid != Tags.getValue(study, Tags.StudyInstanceUID));
10
+ const allStudies = await JSONReader(
11
+ options.directoryName,
12
+ "studies/index.json.gz",
13
+ [],
14
+ );
15
+ const studiesWithoutDeleted = allStudies.filter(
16
+ (study) =>
17
+ studyInstanceUid != Tags.getValue(study, Tags.StudyInstanceUID),
18
+ );
12
19
  await JSONWriter(options.directoryName, "studies", studiesWithoutDeleted);
13
20
  delete this.studyData;
14
21
  };
@@ -1,4 +1,5 @@
1
- const dataExtractor = /^(.*studies)?[\\/]?([0-9.a-zA-Z]+)[\\/](series[\\/])?([0-9.a-zA-Z]+)([\\/]instances[\\/]([0-9.a-zA-Z]*))?$/;
1
+ const dataExtractor =
2
+ /^(.*studies)?[\\/]?([0-9.a-zA-Z]+)[\\/](series[\\/])?([0-9.a-zA-Z]+)([\\/]instances[\\/]([0-9.a-zA-Z]*))?$/;
2
3
 
3
4
  module.exports = () =>
4
5
  async function rejectInstanceWithOptions(args) {
@@ -11,9 +12,20 @@ module.exports = () =>
11
12
  studyInstanceUid = extracted[2];
12
13
  seriesInstanceUid = extracted[4];
13
14
  sopInstanceUid = extracted[6];
14
- console.log("Extracted", studyInstanceUid, seriesInstanceUid, sopInstanceUid, reason);
15
+ console.log(
16
+ "Extracted",
17
+ studyInstanceUid,
18
+ seriesInstanceUid,
19
+ sopInstanceUid,
20
+ reason,
21
+ );
15
22
  } else {
16
- console.log("Not extracting from URL", studyInstanceUid, seriesInstanceUid, reason);
23
+ console.log(
24
+ "Not extracting from URL",
25
+ studyInstanceUid,
26
+ seriesInstanceUid,
27
+ reason,
28
+ );
17
29
  seriesInstanceUid = args[1];
18
30
  }
19
31
  const studyData = await this.scanStudy(studyInstanceUid);
package/lib/StaticWado.js CHANGED
@@ -1,6 +1,14 @@
1
1
  const dicomCodec = require("@cornerstonejs/dicom-codec");
2
- const staticCS = require("@radicalimaging/static-cs-lite");
3
- const { Stats, handleHomeRelative, dirScanner, JSONReader, JSONWriter, asyncIterableToBuffer, Tags } = require("@radicalimaging/static-wado-util");
2
+ const staticCS = require("@radicalimaging/cs3d");
3
+ const {
4
+ Stats,
5
+ handleHomeRelative,
6
+ dirScanner,
7
+ JSONReader,
8
+ JSONWriter,
9
+ asyncIterableToBuffer,
10
+ Tags,
11
+ } = require("@radicalimaging/static-wado-util");
4
12
  const dicomParser = require("dicom-parser");
5
13
  const fs = require("fs");
6
14
  const path = require("path");
@@ -15,7 +23,12 @@ const IdCreator = require("./util/IdCreator");
15
23
  const ScanStudy = require("./operation/ScanStudy");
16
24
  const HashDataWriter = require("./writer/HashDataWriter");
17
25
  const VideoWriter = require("./writer/VideoWriter");
18
- const { transcodeImageFrame, generateLossyImage, transcodeId, transcodeMetadata } = require("./operation/adapter/transcodeImage");
26
+ const {
27
+ transcodeImageFrame,
28
+ generateLossyImage,
29
+ transcodeId,
30
+ transcodeMetadata,
31
+ } = require("./operation/adapter/transcodeImage");
19
32
  const ThumbnailWriter = require("./writer/ThumbnailWriter");
20
33
  const decodeImage = require("./operation/adapter/decodeImage");
21
34
  const ThumbnailService = require("./operation/ThumbnailService");
@@ -23,33 +36,87 @@ const DeleteStudy = require("./DeleteStudy");
23
36
  const RejectInstance = require("./RejectInstance");
24
37
  const RawDicomWriter = require("./writer/RawDicomWriter");
25
38
  const { isVideo } = require("./writer/VideoWriter");
39
+ const validateMetadata = require("./operation/validateMetadata.js");
40
+ const {
41
+ Success,
42
+ Failure,
43
+ clearResults,
44
+ WriteResults,
45
+ } = require("./util/resultsReport");
26
46
 
27
47
  function setStudyData(studyData) {
28
48
  this.studyData = studyData;
29
49
  }
30
50
 
31
- function internalGenerateImage(originalImageFrame, dataset, metadata, transferSyntaxUid, doneCallback) {
32
- decodeImage(originalImageFrame, dataset, transferSyntaxUid)
33
- .then((decodeResult = {}) => {
34
- if (isVideo(transferSyntaxUid)) {
35
- console.log("Video data - no thumbnail generator yet");
36
- } else {
37
- const { imageFrame, imageInfo } = decodeResult;
38
- const pixelData = dicomCodec.getPixelData(imageFrame, imageInfo, transferSyntaxUid);
39
- staticCS.getRenderedBuffer(transferSyntaxUid, pixelData, metadata, doneCallback);
40
- }
41
- })
42
- .catch((error) => {
43
- console.log(`Error while generating thumbnail:: ${error}`);
44
- });
51
+ async function cs3dThumbnail(
52
+ originalImageFrame,
53
+ dataset,
54
+ metadata,
55
+ transferSyntaxUid,
56
+ doneCallback,
57
+ ) {
58
+ if (!originalImageFrame) {
59
+ throw new Error(`No originalImageFrame data available`);
60
+ }
61
+ if (!dataset && !metadata) {
62
+ throw new Error(
63
+ `Neither dataset ${!!dataset} nor metadata ${!!metadata} available.`,
64
+ );
65
+ }
66
+ if (isVideo(transferSyntaxUid)) {
67
+ console.log("Video data - no thumbnail generator yet");
68
+ return;
69
+ }
70
+
71
+ let decodeResult;
72
+ try {
73
+ decodeResult = await decodeImage(
74
+ originalImageFrame,
75
+ dataset,
76
+ metadata,
77
+ transferSyntaxUid,
78
+ );
79
+ } catch (error) {
80
+ console.log("Error while decoding image:", error);
81
+ return;
82
+ }
83
+
84
+ if (!decodeResult) {
85
+ console.warn("No decode result, can't create thumbnail");
86
+ return;
87
+ }
88
+
89
+ const { imageFrame, imageInfo } = decodeResult;
90
+ const pixelData = dicomCodec.getPixelData(
91
+ imageFrame,
92
+ imageInfo,
93
+ transferSyntaxUid,
94
+ );
95
+ return staticCS.getRenderedBuffer(
96
+ transferSyntaxUid,
97
+ pixelData,
98
+ metadata,
99
+ doneCallback,
100
+ );
45
101
  }
46
102
 
47
103
  class StaticWado {
48
104
  constructor(configuration) {
49
- const { rootDir = "~/dicomweb", pathDeduplicated = "deduplicated", pathInstances = "instances", verbose } = configuration;
105
+ const {
106
+ rootDir = "~/dicomweb",
107
+ pathDeduplicated = "deduplicated",
108
+ pathInstances = "instances",
109
+ verbose,
110
+ quiet = false,
111
+ multipart = false,
112
+ showProgress = !quiet && !multipart && !verbose,
113
+ } = configuration;
50
114
 
51
115
  dicomCodec.setConfig({ verbose });
52
116
  const directoryName = handleHomeRelative(rootDir);
117
+ this.showProgress = showProgress;
118
+ this.processedFiles = 0;
119
+ this.totalFiles = 0;
53
120
 
54
121
  this.options = {
55
122
  ...configuration,
@@ -66,15 +133,22 @@ class StaticWado {
66
133
  videoWriter: VideoWriter(this.options),
67
134
  thumbWriter: ThumbnailWriter(this.options),
68
135
  completeStudy: CompleteStudyWriter(this.options),
136
+ validateMetadata: validateMetadata(this.options),
69
137
  metadata: InstanceDeduplicate(this.options),
70
138
  deduplicated: DeduplicateWriter(this.options),
71
139
  scanStudy: ScanStudy(this.options),
72
140
  reject: RejectInstance(this.options),
73
141
  delete: DeleteStudy(this.options),
142
+ success: Success(this.options),
143
+ failure: Failure(this.options),
144
+ writeResults: WriteResults(this.options),
145
+ clearResults,
74
146
  setStudyData,
75
147
  rawDicomWriter: RawDicomWriter(this.options),
76
- notificationService: new NotificationService(this.options.notificationDir),
77
- internalGenerateImage,
148
+ notificationService: new NotificationService(
149
+ this.options.notificationDir,
150
+ ),
151
+ internalGenerateImage: cs3dThumbnail,
78
152
  };
79
153
  }
80
154
 
@@ -106,20 +180,87 @@ class StaticWado {
106
180
  * @param {*} callback
107
181
  * @param {*} params
108
182
  */
183
+ updateProgress() {
184
+ if (!this.showProgress) return;
185
+ this.processedFiles++;
186
+ const percentage = Math.round(
187
+ (this.processedFiles / this.totalFiles) * 100,
188
+ );
189
+ const progressBar =
190
+ "=".repeat(Math.floor(percentage / 4)) +
191
+ "-".repeat(25 - Math.floor(percentage / 4));
192
+ process.stdout.write(
193
+ `\r[${progressBar}] ${percentage}% | ${this.processedFiles}/${this.totalFiles} files`,
194
+ );
195
+ }
196
+
109
197
  async processFiles(files, params) {
110
- return dirScanner(files, {
198
+ if (this.showProgress) {
199
+ // Count total files first
200
+ for (const file of files) {
201
+ try {
202
+ if (fs.statSync(file).isDirectory()) {
203
+ const dirFiles = fs.readdirSync(file, { recursive: true });
204
+ this.totalFiles += dirFiles.filter(
205
+ (f) => !fs.statSync(path.join(file, f)).isDirectory(),
206
+ ).length;
207
+ } else {
208
+ this.totalFiles++;
209
+ }
210
+ } catch (e) {
211
+ this.callback.failure(
212
+ "File not DICOM",
213
+ {
214
+ FailedSOPSequence: [
215
+ { FailureReason: 0xd000, TextValue: "File not DICOM" },
216
+ ],
217
+ },
218
+ file,
219
+ );
220
+ }
221
+ }
222
+ console.noQuiet(`\nProcessing ${this.totalFiles} DICOM files...\n`);
223
+ }
224
+
225
+ let filesProcessed = 0;
226
+ const result = await dirScanner(files, {
111
227
  ...params,
112
228
  callback: async (file) => {
113
229
  try {
114
230
  const dicomp10stream = fs.createReadStream(file);
115
231
  await this.importBinaryDicom(dicomp10stream, { ...params, file });
232
+ filesProcessed++;
116
233
  Stats.StudyStats.add("DICOM P10", "Parse DICOM P10 file");
234
+ this.updateProgress();
117
235
  } catch (e) {
118
- console.error("Couldn't process", file);
119
- console.verbose("Error", e);
236
+ this.updateProgress();
237
+ const message = {
238
+ FailedSOPSequence: [
239
+ {
240
+ FailureReason: 0xd000,
241
+ TextValue: "File not DICOM",
242
+ StorageURL: file,
243
+ },
244
+ ],
245
+ };
246
+ this.callback.failure("Not DICOM", message, file, e);
120
247
  }
121
248
  },
122
249
  });
250
+
251
+ if (!filesProcessed) {
252
+ const message = {
253
+ action: "metadata",
254
+ count: 0,
255
+ status: -1,
256
+ };
257
+ this.callback.failure("No files processed", message);
258
+ }
259
+
260
+ if (this.showProgress) {
261
+ console.log("\n"); // Move to next line after progress bar
262
+ }
263
+ return result;
123
264
  }
124
265
 
125
266
  /**
@@ -144,7 +285,7 @@ class StaticWado {
144
285
  const studyInstanceUid = dataSet.string("x0020000d");
145
286
 
146
287
  if (!studyInstanceUid) {
147
- console.log("No study UID, can't import file", params.file, dataSet.elements);
288
+ console.log("No study UID, can't import file", params.file);
148
289
  return undefined;
149
290
  }
150
291
 
@@ -156,10 +297,14 @@ class StaticWado {
156
297
  sopInstanceUid: dataSet.string("x00080018"),
157
298
  transferSyntaxUid: dataSet.string("x00020010"),
158
299
  },
159
- params.file
300
+ params.file,
160
301
  );
161
302
 
162
- const targetId = transcodeId(id, this.options, dataSet.uint16(Tags.RawSamplesPerPixel));
303
+ const targetId = transcodeId(
304
+ id,
305
+ this.options,
306
+ dataSet.uint16(Tags.RawSamplesPerPixel),
307
+ );
163
308
 
164
309
  let bulkDataIndex = 0;
165
310
  let imageFrameIndex = 0;
@@ -172,17 +317,36 @@ class StaticWado {
172
317
  // TODO - handle other types here too as single part rendered
173
318
  if (options?.mimeType === "application/pdf") {
174
319
  console.log("Writing rendered mimeType", options.mimeType);
175
- const writeStream = WriteStream(id.sopInstanceRootPath, "rendered.pdf", {
176
- gzip: false,
177
- mkdir: true,
178
- });
320
+ const writeStream = WriteStream(
321
+ id.sopInstanceRootPath,
322
+ "rendered.pdf",
323
+ {
324
+ gzip: false,
325
+ mkdir: true,
326
+ },
327
+ );
179
328
  await writeStream.write(bulkData);
180
329
  await writeStream.close();
181
330
  }
182
- return this.callback.bulkdata(targetId, _bulkDataIndex, bulkData, options);
331
+ return this.callback.bulkdata(
332
+ targetId,
333
+ _bulkDataIndex,
334
+ bulkData,
335
+ options,
336
+ );
183
337
  },
184
338
  imageFrame: async (originalImageFrame) => {
185
- const { imageFrame: transcodedImageFrame, decoded, id: transcodedId } = await transcodeImageFrame(id, targetId, originalImageFrame, dataSet, this.options);
339
+ const {
340
+ imageFrame: transcodedImageFrame,
341
+ decoded,
342
+ id: transcodedId,
343
+ } = await transcodeImageFrame(
344
+ id,
345
+ targetId,
346
+ originalImageFrame,
347
+ dataSet,
348
+ this.options,
349
+ );
186
350
 
187
351
  const lossyImage = await generateLossyImage(id, decoded, this.options);
188
352
 
@@ -190,7 +354,11 @@ class StaticWado {
190
354
  imageFrameIndex += 1;
191
355
 
192
356
  if (lossyImage) {
193
- await this.callback.imageFrame(lossyImage.id, currentImageFrameIndex, lossyImage.imageFrame);
357
+ await this.callback.imageFrame(
358
+ lossyImage.id,
359
+ currentImageFrameIndex,
360
+ lossyImage.imageFrame,
361
+ );
194
362
  }
195
363
 
196
364
  thumbnailService.queueThumbnail(
@@ -201,10 +369,14 @@ class StaticWado {
201
369
  id,
202
370
  frameIndex: currentImageFrameIndex,
203
371
  },
204
- this.options
372
+ this.options,
205
373
  );
206
374
 
207
- return this.callback.imageFrame(transcodedId, currentImageFrameIndex, transcodedImageFrame);
375
+ return this.callback.imageFrame(
376
+ transcodedId,
377
+ currentImageFrameIndex,
378
+ transcodedImageFrame,
379
+ );
208
380
  },
209
381
  videoWriter: async (_dataSet) => this.callback.videoWriter(id, _dataSet),
210
382
  };
@@ -212,11 +384,25 @@ class StaticWado {
212
384
  // convert to DICOMweb MetaData and BulkData
213
385
  const result = await getDataSet(dataSet, generator, this.options);
214
386
 
387
+ await this.callback.validateMetadata?.(id, result, buffer);
388
+
215
389
  await this.callback.rawDicomWriter?.(id, result, buffer);
216
390
 
217
391
  const transcodedMeta = transcodeMetadata(result.metadata, id, this.options);
218
- await thumbnailService.generateThumbnails(id, dataSet, transcodedMeta, this.callback, this.options);
219
- await thumbnailService.generateRendered(id, dataSet, transcodedMeta, this.callback, this.options);
392
+ await thumbnailService.generateThumbnails(
393
+ id,
394
+ dataSet,
395
+ transcodedMeta,
396
+ this.callback,
397
+ this.options,
398
+ );
399
+ await thumbnailService.generateRendered(
400
+ id,
401
+ dataSet,
402
+ transcodedMeta,
403
+ this.callback,
404
+ this.options,
405
+ );
220
406
  await this.callback.metadata(targetId, transcodedMeta);
221
407
 
222
408
  // resolve promise with statistics
@@ -227,8 +413,20 @@ class StaticWado {
227
413
  return getDataSet(dataSet, generator, params);
228
414
  }
229
415
 
230
- static internalGenerateImage(originalImageFrame, dataSet, metadata, transferSyntaxUid, doneCallback) {
231
- return internalGenerateImage(originalImageFrame, dataSet, metadata, transferSyntaxUid, doneCallback);
416
+ static internalGenerateImage(
417
+ originalImageFrame,
418
+ dataSet,
419
+ metadata,
420
+ transferSyntaxUid,
421
+ doneCallback,
422
+ ) {
423
+ return cs3dThumbnail(
424
+ originalImageFrame,
425
+ dataSet,
426
+ metadata,
427
+ transferSyntaxUid,
428
+ doneCallback,
429
+ );
232
430
  }
233
431
 
234
432
  /**
@@ -237,15 +435,17 @@ class StaticWado {
237
435
  * data in memory by default on a study level, which avoids needing to run the load process.
238
436
  */
239
437
  async executeCommand(input) {
438
+ this.callback.clearResults();
240
439
  if (this.options.scanStudies) {
241
440
  console.log("Scanning study dir", input);
242
441
  // Scan one of the study directories - in this case, files is a set of study directories
243
442
  await this.processStudyDir(input, this.options);
244
443
  } else {
245
- console.log("Scanning files", input);
444
+ console.noQuiet("Scanning files", input);
246
445
  await this.processFiles(input, this.options);
247
446
  }
248
447
  await this.close();
448
+ this.callback.writeResults();
249
449
  }
250
450
 
251
451
  async close() {
@@ -261,7 +461,11 @@ class StaticWado {
261
461
  const dirs = await fs.promises.readdir(studiesDir);
262
462
  const studies = [];
263
463
  for (const dir of dirs) {
264
- const study = await JSONReader(`${studiesDir}/${dir}`, "index.json.gz", null);
464
+ const study = await JSONReader(
465
+ `${studiesDir}/${dir}`,
466
+ "index.json.gz",
467
+ null,
468
+ );
265
469
  if (study === null) {
266
470
  console.log("No study found in", dir);
267
471
  continue;
@@ -1,4 +1,3 @@
1
- /* eslint-disable no-param-reassign */
2
1
  const { Tags, readBulkData } = require("@radicalimaging/static-wado-util");
3
2
  const dcmjs = require("dcmjs");
4
3
 
@@ -14,8 +13,13 @@ fileMetaInformationVersionArray[1] = 1;
14
13
 
15
14
  const createFmi = (instance) => {
16
15
  // Assume the TSUID is in the value 0
17
- const TransferSyntaxUID = Tags.getValue(instance, Tags.AvailableTransferSyntaxUID) || UncompressedLEIExplicit;
18
- const MediaStorageSOPClassUID = Tags.getValue(instance, Tags.MediaStorageSOPClassUID);
16
+ const TransferSyntaxUID =
17
+ Tags.getValue(instance, Tags.AvailableTransferSyntaxUID) ||
18
+ UncompressedLEIExplicit;
19
+ const MediaStorageSOPClassUID = Tags.getValue(
20
+ instance,
21
+ Tags.MediaStorageSOPClassUID,
22
+ );
19
23
  const SOPInstanceUID = Tags.getValue(instance, Tags.SOPInstanceUID);
20
24
  const naturalFmi = {
21
25
  MediaStorageSOPClassUID,
@@ -103,7 +107,7 @@ module.exports = async function createMain(options, program) {
103
107
  const instance = await study.recombine(sop);
104
108
  const fmi = createFmi(instance);
105
109
 
106
- console.log("Processing sop", sop);
110
+ console.noQuiet("Processing sop", sop);
107
111
  const dicomDict = new DicomDict(fmi);
108
112
  await readBinaryData(dir, instance);
109
113
  dicomDict.dict = instance;
@@ -0,0 +1,181 @@
1
+ const {
2
+ Tags,
3
+ readBulkData,
4
+ handleHomeRelative,
5
+ } = require("@radicalimaging/static-wado-util");
6
+ const dcmjs = require("dcmjs");
7
+
8
+ const StaticWado = require("./StaticWado");
9
+ const adaptProgramOpts = require("./util/adaptProgramOpts");
10
+ const WriteStream = require("./writer/WriteStream");
11
+
12
+ const UncompressedLEIExplicit = "1.2.840.10008.1.2.1";
13
+ const { DicomDict, DicomMetaDictionary } = dcmjs.data;
14
+
15
+ const fileMetaInformationVersionArray = new Uint8Array(2);
16
+ fileMetaInformationVersionArray[1] = 1;
17
+
18
+ const readBulkDataValue = async (studyDir, instance, value, options) => {
19
+ const { BulkDataURI } = value;
20
+ value.vr = "OB";
21
+ const seriesUid = Tags.getValue(instance, Tags.SeriesInstanceUID);
22
+ const numberOfFrames = Tags.getValue(instance, Tags.NumberOfFrames) || 1;
23
+ if (BulkDataURI.indexOf("/frames") !== -1) {
24
+ const seriesDir = `${studyDir}/series/${seriesUid}`;
25
+ if (options?.frame === false) {
26
+ return;
27
+ }
28
+ // Really only support a limited number of frames based on memory size
29
+ value.Value = [];
30
+ if (typeof options?.frame === "number") {
31
+ console.noQuiet("Reading frame", options.frame);
32
+ const bulk = await readBulkData(seriesDir, BulkDataURI, options.frame);
33
+ if (!bulk) {
34
+ return;
35
+ }
36
+ value.Value[options.frame - 1] = bulk.binaryData;
37
+ value.transferSyntaxUid = bulk.transferSyntaxUid;
38
+ value.contentType = bulk.contentType;
39
+ return;
40
+ }
41
+ console.noQuiet("Reading frames", 1, "...", numberOfFrames);
42
+ for (let frame = 1; frame <= numberOfFrames; frame++) {
43
+ const bulk = await readBulkData(seriesDir, BulkDataURI, frame);
44
+ if (!bulk) break;
45
+ value.Value.push(bulk.binaryData);
46
+ value.transferSyntaxUid = bulk.transferSyntaxUid;
47
+ value.contentType = bulk.contentType;
48
+ }
49
+ } else {
50
+ const bulk = await readBulkData(studyDir, BulkDataURI);
51
+ value.Value = [new ArrayBuffer(bulk.binaryData)];
52
+ value.contentType = bulk.contentType;
53
+ }
54
+ };
55
+
56
+ const readBinaryData = async (dir, instance, options = { frame: true }) => {
57
+ for (const tag of Object.keys(instance)) {
58
+ const v = instance[tag];
59
+ if (v.BulkDataURI) {
60
+ await readBulkDataValue(dir, instance, v, options);
61
+ continue;
62
+ }
63
+ if (!v.vr) {
64
+ const value0 = v.Value?.[0];
65
+ if (typeof value0 === "string") {
66
+ v.vr = "LT";
67
+ } else {
68
+ console.log("Deleting", tag, v.Value, v);
69
+ delete instance[tag];
70
+ }
71
+ continue;
72
+ }
73
+ if (v.vr === "SQ" && v.Values?.length) {
74
+ await Promise.all(v.Values.map(readBinaryData));
75
+ continue;
76
+ }
77
+ }
78
+ };
79
+
80
+ module.exports = async function createThumbnail(options, program) {
81
+ const finalOptions = adaptProgramOpts(options, {
82
+ ...this,
83
+ // Instance metadata is the instances/<sopUID>/metadata.gz files
84
+ isInstance: false,
85
+ // Deduplicated data is single instance deduplicated data
86
+ isDeduplicate: false,
87
+ // Group data is the group file directories
88
+ isGroup: false,
89
+ isStudyData: false,
90
+ isDeleteInstances: false,
91
+ });
92
+ const importer = new StaticWado(finalOptions);
93
+ const studyInstanceUid = program.args[0];
94
+ const dir = `${importer.options.rootDir}/studies/${studyInstanceUid}`;
95
+ console.noQuiet("Creating thumbnail for", studyInstanceUid, dir);
96
+ const study = await importer.callback.scanStudy(studyInstanceUid);
97
+ const outputPath = `./studies/${studyInstanceUid}`;
98
+ const { sopInstanceUid, seriesInstanceUid, seriesThumbnail, studyThumbnail } =
99
+ options;
100
+ const sops = sopInstanceUid ? new Set() : null;
101
+ if (sopInstanceUid) {
102
+ for (const sop of sopInstanceUid.split(",").map((s) => s.trim())) {
103
+ sops.add(sop);
104
+ }
105
+ }
106
+ const promises = [];
107
+
108
+ const writeThumbnail = async (id, buffer) => {
109
+ console.verbose("Write thumbnail", id, buffer);
110
+ await importer.callback.thumbWriter(
111
+ id.sopInstanceRootPath,
112
+ "thumbnail",
113
+ buffer,
114
+ );
115
+ if (seriesThumbnail) {
116
+ await importer.callback.thumbWriter(
117
+ id.seriesRootPath,
118
+ "thumbnail",
119
+ buffer,
120
+ );
121
+ }
122
+ if (studyThumbnail) {
123
+ await importer.callback.thumbWriter(id.studyPath, "thumbnail", buffer);
124
+ }
125
+ };
126
+
127
+ // TODO - choose middle frame of multiframe and choose middle image of
128
+ // series for series thumbnail, and first series for study thumbnail
129
+ const codecOptions = { frame: 1 };
130
+
131
+ for (const sop of study.getSopUids()) {
132
+ if (sops && !sops.has(sop)) {
133
+ continue;
134
+ }
135
+ const instance = await study.recombine(sop);
136
+
137
+ if (
138
+ seriesInstanceUid &&
139
+ Tags.getValue(instance, Tags.SeriesInstanceUID) !== seriesInstanceUid
140
+ ) {
141
+ continue;
142
+ }
143
+
144
+ await readBinaryData(dir, instance, codecOptions);
145
+
146
+ const availableTransferSyntaxUID = Tags.getValue(
147
+ instance,
148
+ Tags.AvailableTransferSyntaxUID,
149
+ );
150
+ const pixelData = Tags.getValue(instance, Tags.PixelData);
151
+ if (!pixelData) {
152
+ console.warn("No pixel data found in instance");
153
+ continue;
154
+ }
155
+ const transferSyntaxUid =
156
+ instance[Tags.PixelData].transferSyntaxUid || availableTransferSyntaxUID;
157
+ const frame = Array.isArray(pixelData.Value)
158
+ ? pixelData.Value[0]
159
+ : pixelData;
160
+ console.verbose("Use transfer syntax uid:", transferSyntaxUid);
161
+ const id = importer.callback.uids({
162
+ studyInstanceUid,
163
+ seriesInstanceUid: Tags.getValue(instance, Tags.SeriesInstanceUID),
164
+ sopInstanceUid: sop,
165
+ transferSyntaxUid,
166
+ });
167
+
168
+ const promise = importer.callback.internalGenerateImage(
169
+ frame,
170
+ null,
171
+ instance,
172
+ id.transferSyntaxUid,
173
+ writeThumbnail.bind(null, id),
174
+ );
175
+ promises.push(promise);
176
+ if (seriesThumbnail) {
177
+ break;
178
+ }
179
+ }
180
+ await Promise.all(promises);
181
+ };
package/lib/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  const mkdicomwebConfig = require("./mkdicomwebConfig");
2
2
  const StaticWado = require("./StaticWado");
3
+ const { configureProgram } = require("./program/index.js");
3
4
 
4
5
  StaticWado.mkdicomwebConfig = mkdicomwebConfig;
5
6
  StaticWado.createMain = require("./createMain");
6
7
  StaticWado.deleteMain = require("./deleteMain");
7
8
  StaticWado.adaptProgramOpts = require("./util/adaptProgramOpts");
9
+ StaticWado.configureProgram = configureProgram;
8
10
 
9
11
  module.exports = StaticWado;