@radicalimaging/static-wado-creator 1.5.0 → 1.6.11

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 (54) 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 +511433 -0
  6. package/lib/DeleteStudy.js +11 -9
  7. package/lib/RejectInstance.js +6 -5
  8. package/lib/StaticWado.js +222 -73
  9. package/lib/createMain.js +3 -3
  10. package/lib/createPart10.js +19 -19
  11. package/lib/createThumbnail.js +163 -0
  12. package/lib/deleteMain.js +2 -2
  13. package/lib/groupMain.js +2 -2
  14. package/lib/index.js +7 -5
  15. package/lib/indexMain.js +2 -2
  16. package/lib/instanceMain.js +3 -2
  17. package/lib/metadataMain.js +2 -2
  18. package/lib/mkdicomwebConfig.js +210 -115
  19. package/lib/model/TagLists.js +13 -4
  20. package/lib/model/uids.js +32 -28
  21. package/lib/operation/InstanceDeduplicate.js +18 -13
  22. package/lib/operation/ScanStudy.js +33 -7
  23. package/lib/operation/StudyData.js +122 -71
  24. package/lib/operation/ThumbnailService.js +44 -36
  25. package/lib/operation/adapter/decodeImage.js +7 -4
  26. package/lib/operation/adapter/getImageInfo.js +23 -6
  27. package/lib/operation/adapter/transcodeImage.js +157 -90
  28. package/lib/operation/extractImageFrames.js +25 -14
  29. package/lib/operation/getDataSet.js +4 -4
  30. package/lib/operation/getEncapsulatedImageFrame.js +16 -4
  31. package/lib/operation/getNumberOfFrames.js +2 -2
  32. package/lib/operation/getVR.js +12 -4
  33. package/lib/operation/getValue.js +42 -65
  34. package/lib/operation/validateMetadata.js +40 -0
  35. package/lib/program/index.js +2 -3
  36. package/lib/rejectMain.js +2 -2
  37. package/lib/serverMain.js +21 -0
  38. package/lib/util/IdCreator.js +25 -8
  39. package/lib/util/adaptProgramOpts.js +1 -1
  40. package/lib/util/compressionOptionParser.js +4 -4
  41. package/lib/util/resultsReport.ts +87 -0
  42. package/lib/writer/CompleteStudyWriter.js +42 -19
  43. package/lib/writer/DeduplicateWriter.js +7 -7
  44. package/lib/writer/ExpandUriPath.js +4 -3
  45. package/lib/writer/HashDataWriter.js +21 -17
  46. package/lib/writer/ImageFrameWriter.js +22 -10
  47. package/lib/writer/MultipartHeader.js +1 -1
  48. package/lib/writer/RawDicomWriter.js +18 -10
  49. package/lib/writer/ThumbnailWriter.js +2 -2
  50. package/lib/writer/VideoWriter.js +23 -34
  51. package/lib/writer/WriteMultipart.js +3 -3
  52. package/lib/writer/WriteStream.js +14 -12
  53. package/package.json +22 -18
  54. package/bin/mkdicomweb.js +0 -9
@@ -1,14 +1,40 @@
1
- const path = require("path");
1
+ const { getStudyUIDPathAndSubPath } = require('@radicalimaging/static-wado-util');
2
+
3
+ const path = require('path');
2
4
 
3
5
  function ScanStudy(options) {
4
- const { directoryName, deduplicatedRoot, deduplicatedInstancesRoot } = options;
6
+ const { directoryName, deduplicatedRoot, deduplicatedInstancesRoot, hashStudyUidPath } = options;
5
7
 
6
8
  return function scanStudy(studyInstanceUid) {
7
- console.verbose("scanStudy", studyInstanceUid);
8
- const studyPath = path.join(directoryName, "studies", studyInstanceUid);
9
- const deduplicatedInstancesPath = path.join(deduplicatedInstancesRoot, studyInstanceUid);
10
- const deduplicatedPath = path.join(deduplicatedRoot, studyInstanceUid);
11
- console.verbose("Importing", studyInstanceUid, studyPath, deduplicatedInstancesPath, deduplicatedPath);
9
+ const { path: hashPath = '', subpath: hashSubpath = '' } = hashStudyUidPath
10
+ ? getStudyUIDPathAndSubPath(studyInstanceUid)
11
+ : {};
12
+
13
+ console.verbose('scanStudy', studyInstanceUid);
14
+
15
+ if (hashPath && hashSubpath) {
16
+ console.verbose('hashPath', hashPath);
17
+ console.verbose('hashSubpath', hashSubpath);
18
+ }
19
+
20
+ const studySubDir = hashStudyUidPath
21
+ ? path.join(hashPath, hashSubpath, studyInstanceUid)
22
+ : studyInstanceUid;
23
+
24
+ const studyPath = path.join(directoryName, 'studies', studySubDir);
25
+ const deduplicatedInstancesPath = path.join(
26
+ deduplicatedInstancesRoot,
27
+ // studyInstanceUid,
28
+ studySubDir
29
+ );
30
+ const deduplicatedPath = path.join(deduplicatedRoot, studySubDir);
31
+ console.verbose(
32
+ 'Importing',
33
+ studyInstanceUid,
34
+ studyPath,
35
+ deduplicatedInstancesPath,
36
+ deduplicatedPath
37
+ );
12
38
  return this.completeStudy.getCurrentStudyData(this, {
13
39
  studyPath,
14
40
  deduplicatedPath,
@@ -1,14 +1,14 @@
1
- const path = require("path");
2
- const fs = require("fs");
3
- const hashFactory = require("node-object-hash");
4
- const { JSONReader, JSONWriter } = require("@radicalimaging/static-wado-util");
5
- const { Tags } = require("@radicalimaging/static-wado-util");
6
- const TagLists = require("../model/TagLists");
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const hashFactory = require('node-object-hash');
4
+ const { JSONReader, JSONWriter } = require('@radicalimaging/static-wado-util');
5
+ const { Tags } = require('@radicalimaging/static-wado-util');
6
+ const TagLists = require('../model/TagLists');
7
7
 
8
8
  const { getValue, setValue, getList, setList } = Tags;
9
9
  const hasher = hashFactory.hasher();
10
10
 
11
- const getSeriesInstanceUid = (seriesInstance) => getValue(seriesInstance, Tags.SeriesInstanceUID);
11
+ const getSeriesInstanceUid = seriesInstance => getValue(seriesInstance, Tags.SeriesInstanceUID);
12
12
 
13
13
  /**
14
14
  * StudyData contains information about the grouped study data. It is used to create
@@ -21,12 +21,16 @@ const getSeriesInstanceUid = (seriesInstance) => getValue(seriesInstance, Tags.S
21
21
  * level data multiple times when it already exists.
22
22
  */
23
23
  class StudyData {
24
- constructor({ studyInstanceUid, studyPath, deduplicatedPath, deduplicatedInstancesPath }, { isGroup, clean }) {
24
+ constructor(
25
+ { studyInstanceUid, studyPath, deduplicatedPath, deduplicatedInstancesPath },
26
+ { isGroup, clean, hashStudyUidPath }
27
+ ) {
25
28
  this.studyInstanceUid = studyInstanceUid;
26
29
  this.studyPath = studyPath;
27
30
  this.isGroup = isGroup;
28
31
  this.deduplicatedPath = deduplicatedPath;
29
32
  this.deduplicatedInstancesPath = deduplicatedInstancesPath;
33
+ this.hashStudyUidPath = hashStudyUidPath;
30
34
  this.clean = clean;
31
35
  this.clear();
32
36
  }
@@ -55,19 +59,35 @@ class StudyData {
55
59
  if (clean) {
56
60
  // Wipe out the study directory entirely, as well as the deduplicatedRoot and instancesRoot
57
61
  }
62
+
63
+ console.verbose('studyInstanceUid', this.studyInstanceUid);
64
+ console.verbose('studyPath', this.studyPath);
65
+ console.verbose('deduplicatedPath', this.deduplicatedPath);
66
+ console.verbose('deduplicatedInstancesPath', this.deduplicatedInstancesPath);
67
+
58
68
  this.groupFiles = 0;
59
- const studyDeduplicated = await JSONReader(this.studyPath, "deduplicated/index.json.gz", []);
69
+ const studyDeduplicated = await JSONReader(this.studyPath, 'deduplicated/index.json.gz', []);
60
70
  const info = studyDeduplicated[0];
61
71
  if (info) {
62
72
  const hash = getValue(info, Tags.DeduppedHash);
63
- console.log("Reading studies/<studyUID>/deduplicated/index.json.gz");
64
- this.readDeduplicatedData("index.json.gz", studyDeduplicated, hash);
73
+ if (this.hashStudyUidPath) {
74
+ console.verbose(
75
+ 'Reading studies/<studyUIDPath>/<studyUIDSubpath>/<studyUID>/deduplicated/index.json.gz'
76
+ );
77
+ } else {
78
+ console.verbose('Reading studies/<studyUID>/deduplicated/index.json.gz');
79
+ }
80
+ this.readDeduplicatedData('index.json.gz', studyDeduplicated, hash);
65
81
  } else {
66
- console.log("No deduplicated/index.json to read in", this.studyPath, "/deduplicated/index.json.gz");
82
+ console.log(
83
+ 'No deduplicated/index.json to read in',
84
+ this.studyPath,
85
+ '/deduplicated/index.json.gz'
86
+ );
67
87
  }
68
88
  if (this.deduplicatedPath) {
69
89
  this.groupFiles = await this.readDeduplicated(this.deduplicatedPath);
70
- console.verbose("Read groupFiles:", this.groupFiles);
90
+ console.verbose('Read groupFiles:', this.groupFiles);
71
91
  }
72
92
  if (this.deduplicatedInstancesPath) {
73
93
  this.instanceFiles = await this.readDeduplicated(this.deduplicatedInstancesPath);
@@ -89,26 +109,26 @@ class StudyData {
89
109
 
90
110
  async dirtyMetadata() {
91
111
  if (this.dirty) {
92
- console.verbose("dirtyMetadata::Group data is dirty");
112
+ console.verbose('dirtyMetadata::Group data is dirty');
93
113
  return true;
94
114
  }
95
115
  if (this.groupFiles > 0) {
96
116
  console.verbose("dirtyMetadata::Study level deduplicated doesn't match group files");
97
117
  }
98
118
  try {
99
- const studyFile = await JSONReader(this.studyPath, "index.json.gz", null);
119
+ const studyFile = await JSONReader(this.studyPath, 'index.json.gz', null);
100
120
  if (!studyFile) {
101
- console.verbose("dirtyMetadata::studyIndex");
121
+ console.verbose('dirtyMetadata::studyIndex');
102
122
  return true;
103
123
  }
104
124
  const hashValue = getValue(studyFile, Tags.DeduppedHash);
105
125
  if (this.existingFiles[0].indexOf(hashValue) == -1) {
106
126
  return false;
107
127
  }
108
- console.verbose("dirtyMetadata::Dedupped hash missing");
128
+ console.verbose('dirtyMetadata::Dedupped hash missing');
109
129
  return true;
110
130
  } catch (e) {
111
- console.verbose("dirtyMetadata::Exception, assume study metadata is dirty", e);
131
+ console.verbose('dirtyMetadata::Exception, assume study metadata is dirty', e);
112
132
  return true;
113
133
  }
114
134
  }
@@ -123,13 +143,16 @@ class StudyData {
123
143
  const reSop = getValue(recombined, Tags.SOPInstanceUID);
124
144
  if (!reSop) continue;
125
145
  if (sopInstanceUid && reSop !== sopInstanceUid) continue;
126
- setValue(this.deduplicated[i], Tags.DeduppedType, "deleted");
146
+ setValue(this.deduplicated[i], Tags.DeduppedType, 'deleted');
127
147
  }
128
148
  }
129
149
 
130
150
  async delete() {
131
151
  await fs.rmSync(this.studyPath, { recursive: true, force: true });
132
- await fs.rmSync(this.deduplicatedInstancesPath, { recursive: true, force: true });
152
+ await fs.rmSync(this.deduplicatedInstancesPath, {
153
+ recursive: true,
154
+ force: true,
155
+ });
133
156
  await fs.rmSync(this.deduplicatedPath, { recursive: true, force: true });
134
157
  this.clear();
135
158
  }
@@ -139,7 +162,7 @@ class StudyData {
139
162
  if (!item) {
140
163
  item = await JSONReader.readHashData(this.studyPath, hashKey);
141
164
  if (!item) {
142
- console.error("Unable to read hashdata", hashKey);
165
+ console.error('Unable to read hashdata', hashKey);
143
166
  return item;
144
167
  }
145
168
  this.extractData[hashKey] = item;
@@ -149,7 +172,7 @@ class StudyData {
149
172
 
150
173
  assignUndefined(ret, item) {
151
174
  if (!item) return;
152
- Object.keys(item).forEach((key) => {
175
+ Object.keys(item).forEach(key => {
153
176
  if (ret[key]) return;
154
177
  ret[key] = item[key];
155
178
  });
@@ -159,14 +182,14 @@ class StudyData {
159
182
  * Create a full study instance data for instance at index
160
183
  */
161
184
  async recombine(indexOrSop) {
162
- const index = typeof indexOrSop === "string" ? this.sopInstances[indexOrSop] : indexOrSop;
185
+ const index = typeof indexOrSop === 'string' ? this.sopInstances[indexOrSop] : indexOrSop;
163
186
  const deduplicated = this.deduplicated[index];
164
187
  if (index < 0 || index >= this.deduplicated.length) {
165
188
  throw new Error(`Can't read index ${index}, out of bounds [0..${this.deduplicated.length})`);
166
189
  }
167
190
  const refs = getList(deduplicated, Tags.DeduppedRef);
168
191
  if (!refs) {
169
- console.log("No refs for", deduplicated);
192
+ console.log('No refs for', deduplicated);
170
193
  return deduplicated;
171
194
  }
172
195
  const ret = { ...deduplicated };
@@ -179,7 +202,8 @@ class StudyData {
179
202
 
180
203
  async addExtracted(callback, hashKey, item) {
181
204
  if (this.extractData[hashKey]) {
182
- if (this.verbose) console.log("Already have extracted", hashKey, getValue(item, Tags.DeduppedType));
205
+ if (this.verbose)
206
+ console.log('Already have extracted', hashKey, getValue(item, Tags.DeduppedType));
183
207
  return;
184
208
  }
185
209
  await callback.bulkdata(this, hashKey, item);
@@ -200,7 +224,7 @@ class StudyData {
200
224
  }
201
225
 
202
226
  /** Add the instance if not already present */
203
- internalAddDeduplicated(data, filename = "internal") {
227
+ internalAddDeduplicated(data, filename = 'internal') {
204
228
  const hashValue = TagLists.addHash(data, Tags.InstanceType);
205
229
  if (!hashValue || this.deduplicatedHashes[hashValue]) {
206
230
  // console.log('Not adding', hashValue, 'because the hash exists');
@@ -209,13 +233,13 @@ class StudyData {
209
233
  const sopValue = getValue(data, Tags.SOPInstanceUID);
210
234
  const sopIndex = this.sopInstances[sopValue];
211
235
  if (!sopValue) {
212
- console.warn("No sop value in", filename, "reading", data);
236
+ console.warn('No sop value in', filename, 'reading', data);
213
237
  return false;
214
238
  }
215
239
  this.deduplicatedHashes[hashValue] = data;
216
240
  this.readHashes[hashValue] = filename;
217
241
  if (sopIndex !== undefined) {
218
- console.log("Replacing SOP", sopValue, "at index", sopIndex);
242
+ console.noQuiet('Replacing SOP', sopValue, 'at index', sopIndex);
219
243
  this.deduplicated[sopIndex] = data;
220
244
  } else {
221
245
  this.sopInstances[sopValue] = this.deduplicated.length;
@@ -228,9 +252,9 @@ class StudyData {
228
252
  async listJsonFiles(dir) {
229
253
  const names = await fs.promises.readdir(dir);
230
254
  if (!names || names.length == 0) return [];
231
- const gzNames = names.filter((name) => name.indexOf(".gz") > 0);
255
+ const gzNames = names.filter(name => name.indexOf('.gz') > 0);
232
256
  const statInfo = gzNames
233
- .map((name) => {
257
+ .map(name => {
234
258
  const stat = fs.statSync(path.join(dir, name));
235
259
  return {
236
260
  ...stat,
@@ -259,10 +283,10 @@ class StudyData {
259
283
  try {
260
284
  const files = await this.listJsonFiles(deduplicatedDirectory);
261
285
  if (!files || !files.length) {
262
- console.verbose("No deduplicated for", deduplicatedDirectory);
286
+ console.verbose('No deduplicated for', deduplicatedDirectory);
263
287
  return 0;
264
288
  }
265
- console.verbose("There are", files.length, "files to check");
289
+ console.verbose('There are', files.length, 'files to check');
266
290
  for (let i = 0; i < files.length; i++) {
267
291
  const stat = files[i];
268
292
  const { hash } = stat;
@@ -272,7 +296,7 @@ class StudyData {
272
296
  readCount += 1;
273
297
  await this.readDeduplicatedFile(deduplicatedDirectory, stat);
274
298
  }
275
- console.verbose("Done checking", deduplicatedDirectory);
299
+ console.verbose('Done checking', deduplicatedDirectory);
276
300
  } catch (e) {
277
301
  // No-op console.log(e);
278
302
  }
@@ -282,11 +306,11 @@ class StudyData {
282
306
  async readDeduplicatedFile(dir, stat) {
283
307
  const { hash, name } = stat;
284
308
  try {
285
- if (this.verbose) console.log("Reading deduplicated file", name);
309
+ console.verbose('Reading deduplicated file', name);
286
310
  const data = await JSONReader(dir, name);
287
311
  this.readDeduplicatedData(name, data, hash);
288
312
  } catch (e) {
289
- console.error("Unable to read", dir, name);
313
+ console.error('Unable to read', dir, name);
290
314
  }
291
315
  }
292
316
 
@@ -294,14 +318,14 @@ class StudyData {
294
318
  this.readHashes[hash] = name;
295
319
  this.existingFiles.push(name);
296
320
  const listData = (Array.isArray(data) && data) || [data];
297
- listData.forEach((item) => {
321
+ listData.forEach(item => {
298
322
  const type = getValue(item, Tags.DeduppedType);
299
323
  if (type === Tags.InstanceType || type === Tags.DeletedType) {
300
324
  this.internalAddDeduplicated(item, name);
301
- } else if (type == "info") {
325
+ } else if (type == 'info') {
302
326
  const refs = getList(item, Tags.DeduppedRef);
303
327
  if (refs) {
304
- refs.forEach((hashValue) => {
328
+ refs.forEach(hashValue => {
305
329
  this.readHashes[hashValue] = `${hashValue}.gz`;
306
330
  });
307
331
  }
@@ -321,18 +345,22 @@ class StudyData {
321
345
  for (let i = 0; i < this.numberOfInstances; i++) {
322
346
  const seriesInstance = await this.recombine(i);
323
347
  const type = getValue(seriesInstance, Tags.DeduppedType);
324
- if (type == "deleted") {
325
- console.log("Skipping deleted instance", type, getValue(seriesInstance, Tags.SeriesInstanceUID));
348
+ if (type == 'deleted') {
349
+ console.log(
350
+ 'Skipping deleted instance',
351
+ type,
352
+ getValue(seriesInstance, Tags.SeriesInstanceUID)
353
+ );
326
354
  continue;
327
355
  }
328
356
  const seriesInstanceUid = getSeriesInstanceUid(seriesInstance);
329
357
  if (!seriesInstanceUid) {
330
- console.log("Cant get seriesUid from", Tags.SeriesInstanceUID, seriesInstance);
358
+ console.log('Cant get seriesUid from', Tags.SeriesInstanceUID, seriesInstance);
331
359
  continue;
332
360
  }
333
361
  if (!series[seriesInstanceUid]) {
334
- const seriesQuery = TagLists.extract(seriesInstance, "series", TagLists.SeriesQuery);
335
- const seriesPath = path.join(this.studyPath, "series", seriesInstanceUid);
362
+ const seriesQuery = TagLists.extract(seriesInstance, 'series', TagLists.SeriesQuery);
363
+ const seriesPath = path.join(this.studyPath, 'series', seriesInstanceUid);
336
364
  series[seriesInstanceUid] = {
337
365
  seriesPath,
338
366
  seriesQuery,
@@ -341,18 +369,21 @@ class StudyData {
341
369
  };
342
370
  }
343
371
  series[seriesInstanceUid].instances.push(seriesInstance);
344
- series[seriesInstanceUid].instancesQuery.push(TagLists.extract(seriesInstance, "instance", TagLists.InstanceQuery));
372
+ series[seriesInstanceUid].instancesQuery.push(
373
+ TagLists.extract(seriesInstance, 'instance', TagLists.InstanceQuery)
374
+ );
345
375
  }
346
376
 
347
377
  const seriesList = [];
348
378
  const modalitiesInStudy = [];
349
379
  let numberOfInstances = 0;
350
380
  let numberOfSeries = 0;
381
+
351
382
  for (const seriesUid of Object.keys(series)) {
352
383
  const singleSeries = series[seriesUid];
353
384
  const { seriesQuery, seriesPath, instances, instancesQuery } = singleSeries;
354
385
  seriesQuery[Tags.NumberOfSeriesRelatedInstances] = {
355
- vr: "IS",
386
+ vr: 'IS',
356
387
  Value: [instances.length],
357
388
  };
358
389
  numberOfInstances += instances.length;
@@ -360,37 +391,51 @@ class StudyData {
360
391
  const modality = getValue(seriesQuery, Tags.Modality);
361
392
  seriesList.push(seriesQuery);
362
393
  if (modalitiesInStudy.indexOf(modality) == -1) modalitiesInStudy.push(modality);
363
- await JSONWriter(seriesPath, "metadata", instances, {
394
+ await JSONWriter(seriesPath, 'metadata', instances, {
364
395
  gzip: true,
365
396
  index: false,
366
397
  });
367
398
  // Write out a series singleton that has just the series response for a single series.
368
- await JSONWriter(seriesPath, "series-singleton.json", [seriesQuery], { gzip: true, index: false });
369
- await JSONWriter(seriesPath, "instances", instancesQuery);
399
+ await JSONWriter(seriesPath, 'series-singleton.json', [seriesQuery], {
400
+ gzip: true,
401
+ index: false,
402
+ });
403
+ await JSONWriter(seriesPath, 'instances', instancesQuery);
404
+ console.noQuiet('Wrote instances', seriesUid, 'with', instancesQuery.length);
370
405
  }
371
406
 
372
- await JSONWriter(this.studyPath, "series", seriesList);
373
- console.log("Wrote series with", seriesList.length);
407
+ await JSONWriter(this.studyPath, 'series', seriesList);
408
+ console.noQuiet('Wrote series', this.studyInstanceUid, 'with', seriesList.length);
374
409
 
375
- const studyQuery = TagLists.extract(anInstance, "study", TagLists.PatientStudyQuery);
376
- studyQuery[Tags.ModalitiesInStudy] = { Value: modalitiesInStudy, vr: "CS" };
410
+ const studyQuery = TagLists.extract(anInstance, 'study', TagLists.PatientStudyQuery);
411
+ studyQuery[Tags.ModalitiesInStudy] = { Value: modalitiesInStudy, vr: 'CS' };
377
412
  studyQuery[Tags.NumberOfStudyRelatedInstances] = {
378
413
  Value: [numberOfInstances],
379
- vr: "IS",
414
+ vr: 'IS',
380
415
  };
381
416
  studyQuery[Tags.NumberOfStudyRelatedSeries] = {
382
417
  Value: [numberOfSeries],
383
- vr: "IS",
418
+ vr: 'IS',
384
419
  };
385
420
  // Write the index.json directly in the studyPath directory.
386
- await JSONWriter(this.studyPath, "index.json", [studyQuery], {
421
+ await JSONWriter(this.studyPath, 'index.json', [studyQuery], {
387
422
  gzip: true,
388
423
  index: false,
389
424
  });
390
425
 
391
426
  const infoItem = this.createInfo();
392
- console.log("Writing deduplicated study data with", Object.values(this.extractData).length, "extract items and", this.deduplicated.length, "instance items");
393
- await JSONWriter(this.studyPath, "deduplicated", [infoItem, ...Object.values(this.extractData), ...this.deduplicated]);
427
+ console.noQuiet(
428
+ 'Writing deduplicated study data with',
429
+ Object.values(this.extractData).length,
430
+ 'extract items and',
431
+ this.deduplicated.length,
432
+ 'instance items'
433
+ );
434
+ await JSONWriter(this.studyPath, 'deduplicated', [
435
+ infoItem,
436
+ ...Object.values(this.extractData),
437
+ ...this.deduplicated,
438
+ ]);
394
439
 
395
440
  return studyQuery;
396
441
  }
@@ -402,38 +447,41 @@ class StudyData {
402
447
  const data = {};
403
448
  const hashValue = hasher.hash(this.deduplicated);
404
449
  setValue(data, Tags.DeduppedHash, hashValue);
405
- setValue(data, Tags.DeduppedType, "info");
450
+ setValue(data, Tags.DeduppedType, 'info');
406
451
  return data;
407
452
  }
408
453
 
409
- /* eslint-disable-next-line class-methods-use-this */
410
454
  removeGz(name) {
411
- const gzIndex = name.indexOf(".gz");
455
+ const gzIndex = name.indexOf('.gz');
412
456
  return (gzIndex > 0 && name.substring(0, gzIndex)) || name;
413
457
  }
414
458
 
415
459
  async deleteInstancesReferenced() {
416
460
  const deduplicatedDirectory = this.deduplicatedInstancesPath;
417
461
  if (!fs.existsSync(deduplicatedDirectory)) return;
418
- console.log("Deleting instances referenced in", this.studyInstanceUid, this.deduplicatedInstancesPath);
462
+ console.log(
463
+ 'Deleting instances referenced in',
464
+ this.studyInstanceUid,
465
+ this.deduplicatedInstancesPath
466
+ );
419
467
  const files = await this.listJsonFiles(deduplicatedDirectory);
420
- console.log("Deleting", files.length, "files");
468
+ console.noQuiet('Deleting', files.length, 'files');
421
469
  let deleteCount = 0;
422
470
  for (let i = 0; i < files.length; i++) {
423
471
  const stat = files[i];
424
472
  const { hash } = stat;
425
473
  if (this.readHashes[hash]) {
426
- console.log("Deleting", deduplicatedDirectory, stat.name);
474
+ console.noQuiet('Deleting', deduplicatedDirectory, stat.name);
427
475
  try {
428
476
  fs.unlinkSync(path.join(deduplicatedDirectory, stat.name));
429
477
  deleteCount += 1;
430
478
  } catch (e) {
431
- console.log("Delete failed", e);
479
+ console.log('Delete failed', e);
432
480
  }
433
481
  }
434
482
  }
435
483
  if (deleteCount === files.length) {
436
- console.log("Deleting instances directory", deduplicatedDirectory);
484
+ console.noQuiet('Deleting instances directory', deduplicatedDirectory);
437
485
  fs.rmdirSync(deduplicatedDirectory);
438
486
  }
439
487
  }
@@ -442,12 +490,12 @@ class StudyData {
442
490
  async writeDeduplicatedGroup() {
443
491
  const data = this.createInfo();
444
492
  const hashValue = getValue(data, Tags.DeduppedHash);
445
- console.log(
446
- "Writing deduplicated hash data set data with",
493
+ console.noQuiet(
494
+ 'Writing deduplicated hash data set data with',
447
495
  Object.values(this.extractData).length,
448
- "extract items and",
496
+ 'extract items and',
449
497
  this.deduplicated.length,
450
- "instance items"
498
+ 'instance items'
451
499
  );
452
500
  setList(
453
501
  data,
@@ -459,8 +507,11 @@ class StudyData {
459
507
  // // console.log("naturalList=", JSON.stringify(naturalList,null,2));
460
508
  // console.log("Going to write study data", naturalList.length);
461
509
  // await JSONWriter(this.deduplicatedPath, hashValue, naturalList, { gzip: true, index: false });
462
- await JSONWriter(this.deduplicatedPath, hashValue, deduplicatedList, { gzip: true, index: false });
463
- console.log("Wrote naturalized dataset");
510
+ await JSONWriter(this.deduplicatedPath, hashValue, deduplicatedList, {
511
+ gzip: true,
512
+ index: false,
513
+ });
514
+ console.noQuiet('Wrote naturalized dataset');
464
515
  }
465
516
 
466
517
  getSopUids() {
@@ -1,8 +1,8 @@
1
- const path = require("path");
2
- const glob = require("glob");
3
- const fs = require("fs");
4
- const { Tags, execSpawn, Stats } = require("@radicalimaging/static-wado-util");
5
- const { shouldThumbUseTranscoded } = require("./adapter/transcodeImage");
1
+ const path = require('path');
2
+ const glob = require('glob');
3
+ const fs = require('fs');
4
+ const { Tags, execSpawn, Stats } = require('@radicalimaging/static-wado-util');
5
+ const { shouldThumbUseTranscoded } = require('./adapter/transcodeImage');
6
6
 
7
7
  /**
8
8
  * Return the middle index of given list
@@ -46,7 +46,7 @@ class ThumbnailService {
46
46
  constructor() {
47
47
  this.framesThumbnailObj = [];
48
48
  this.favoriteThumbnailObj = {};
49
- this.thumbFileName = "thumbnail";
49
+ this.thumbFileName = 'thumbnail';
50
50
  }
51
51
 
52
52
  /**
@@ -57,7 +57,8 @@ class ThumbnailService {
57
57
  */
58
58
  queueThumbnail(thumbObjWrapper, programOpts) {
59
59
  const { id, imageFrame, transcodedId, transcodedImageFrame, frameIndex } = thumbObjWrapper;
60
- const getThumbContent = (originalContent, trancodedContent) => (shouldThumbUseTranscoded(id, programOpts) ? trancodedContent : originalContent);
60
+ const getThumbContent = (originalContent, trancodedContent) =>
61
+ shouldThumbUseTranscoded(id, programOpts) ? trancodedContent : originalContent;
61
62
 
62
63
  const thumbObj = {
63
64
  imageFrame: getThumbContent(imageFrame, transcodedImageFrame),
@@ -77,15 +78,15 @@ class ThumbnailService {
77
78
  }
78
79
 
79
80
  ffmpeg(input, output) {
80
- execSpawn(`ffmpeg -i "${input}" -vf "thumbnail,scale=640:360" -frames:v 1 -f singlejpeg "${output}"`);
81
+ execSpawn(`ffmpeg -i "${input}" -y -f image2 -frames:v 1 -update true "${output}"`);
81
82
  }
82
83
 
83
84
  dcm2jpg(input, output, options) {
84
- const script = ["dcm2jpg", `"${input}" "${output}"`];
85
+ const script = ['dcm2jpg', `"${input}" "${output}"`];
85
86
  if (options?.format) {
86
- script.push("-F", options.format);
87
+ script.push('-F', options.format);
87
88
  }
88
- execSpawn(script.join(" "));
89
+ execSpawn(script.join(' '));
89
90
  }
90
91
 
91
92
  /** Generates a rendered copy of this, assuming --rendered is set.
@@ -96,8 +97,8 @@ class ThumbnailService {
96
97
  return null;
97
98
  }
98
99
  const { id } = this.favoriteThumbnailObj;
99
- const rendered = id.imageFrameRootPath.replace(/frames/, "rendered");
100
- return this.dcm2jpg(id.filename, rendered, { format: "png" });
100
+ const rendered = id.imageFrameRootPath.replace(/frames/, 'rendered');
101
+ return this.dcm2jpg(id.filename, rendered, { format: 'png' });
101
102
  }
102
103
 
103
104
  /**
@@ -115,17 +116,18 @@ class ThumbnailService {
115
116
  const pixelData = metadata[Tags.PixelData];
116
117
  if (pixelData) {
117
118
  const { BulkDataURI } = pixelData;
118
- if (BulkDataURI?.indexOf("mp4")) {
119
- fs.mkdirSync(`${itemId.sopInstanceRootPath}/rendered`, { recursive: true });
120
- const mp4Path = path.join(itemId.sopInstanceRootPath, "rendered/index.mp4");
121
- // Generate as rendered, as more back ends support that.
122
- const thumbPath = path.join(itemId.sopInstanceRootPath, "rendered/1.jpg");
123
- console.log("MP4 - converting video format", mp4Path);
119
+ if (BulkDataURI?.indexOf('mp4')) {
120
+ fs.mkdirSync(`${itemId.sopInstanceRootPath}/rendered`, {
121
+ recursive: true,
122
+ });
123
+ const mp4Path = path.join(itemId.sopInstanceRootPath, 'rendered/index.mp4');
124
+ const thumbPath = path.join(itemId.sopInstanceRootPath, 'thumbnail');
125
+ console.verbose('MP4 - converting video format', mp4Path);
124
126
  this.ffmpeg(mp4Path, thumbPath);
125
127
  return thumbPath;
126
128
  }
127
129
  } else {
128
- console.log("Series is of other type...", metadata[Tags.Modality]);
130
+ console.verbose('Series is of other type...', metadata[Tags.Modality]);
129
131
  }
130
132
  return null;
131
133
  }
@@ -135,23 +137,29 @@ class ThumbnailService {
135
137
  }
136
138
 
137
139
  if (options.dcm2jpg) {
138
- return this.dcm2jpg(id.filename, id.imageFrameRootPath.replace(/frames/, "thumbnail"), {});
140
+ return this.dcm2jpg(id.filename, id.imageFrameRootPath.replace(/frames/, 'thumbnail'), {});
139
141
  }
140
142
 
141
- await callback.internalGenerateImage(imageFrame, dataSet, metadata, id.transferSyntaxUid, async (thumbBuffer) => {
142
- try {
143
- if (thumbBuffer) {
144
- await callback.thumbWriter(id.sopInstanceRootPath, this.thumbFileName, thumbBuffer);
145
-
146
- this.copySyncThumbnail(id.sopInstanceRootPath, id.seriesRootPath);
147
- this.copySyncThumbnail(id.seriesRootPath, id.studyPath);
148
- Stats.StudyStats.add("Thumbnail Write", `Write thumbnail ${this.thumbFileName}`, 100);
143
+ await callback.internalGenerateImage(
144
+ imageFrame,
145
+ dataSet,
146
+ metadata,
147
+ id.transferSyntaxUid,
148
+ async thumbBuffer => {
149
+ try {
150
+ if (thumbBuffer) {
151
+ await callback.thumbWriter(id.sopInstanceRootPath, this.thumbFileName, thumbBuffer);
152
+
153
+ this.copySyncThumbnail(id.sopInstanceRootPath, id.seriesRootPath);
154
+ this.copySyncThumbnail(id.seriesRootPath, id.studyPath);
155
+ Stats.StudyStats.add('Thumbnail Write', `Write thumbnail ${this.thumbFileName}`, 100);
156
+ }
157
+ return this.thumbFileName;
158
+ } catch (e) {
159
+ console.log("Couldn't generate thumbnail", this.thumbFileName, e);
149
160
  }
150
- return this.thumbFileName;
151
- } catch (e) {
152
- console.log("Couldn't generate thumbnail", this.thumbFileName, e);
153
161
  }
154
- });
162
+ );
155
163
  }
156
164
 
157
165
  /**
@@ -164,14 +172,14 @@ class ThumbnailService {
164
172
  * @returns
165
173
  */
166
174
  async copySyncThumbnail(sourceFolderPath, targetFolderPath) {
167
- const parentPathLevel = path.join(sourceFolderPath, "../");
175
+ const parentPathLevel = path.join(sourceFolderPath, '../');
168
176
  const thumbFilesPath = glob.sync(`${parentPathLevel}*/${this.thumbFileName}`);
169
177
 
170
178
  const thumbIndex = getThumbIndex(thumbFilesPath.length);
171
179
  const thumbFilePath = thumbFilesPath[thumbIndex];
172
180
 
173
181
  if (!fs.existsSync(thumbFilePath)) {
174
- console.log("Thumbnail to copy does not exists");
182
+ console.verbose('Thumbnail to copy does not exists');
175
183
  return;
176
184
  }
177
185
 
@@ -182,7 +190,7 @@ class ThumbnailService {
182
190
 
183
191
  fs.copyFileSync(thumbFilePath, `${targetFolderPath}/${this.thumbFileName}`);
184
192
  } catch (e) {
185
- console.log("The file could not be copied", e);
193
+ console.verbose('The file could not be copied', e);
186
194
  }
187
195
  }
188
196
  }