@radicalimaging/static-wado-creator 1.7.4 → 1.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/DeleteStudy.js +16 -0
  2. package/dist/RejectInstance.js +25 -0
  3. package/dist/StaticWado.js +419 -0
  4. package/dist/canvas-xaqc8v6y.node +0 -0
  5. package/dist/createMain.js +18 -0
  6. package/dist/createPart10.js +113 -0
  7. package/dist/createThumbnail.js +163 -0
  8. package/dist/deleteMain.js +12 -0
  9. package/dist/groupMain.js +17 -0
  10. package/dist/index.js +500838 -0
  11. package/dist/index.mjs +18 -0
  12. package/dist/indexMain.js +10 -0
  13. package/dist/instanceMain.js +16 -0
  14. package/dist/metadataMain.js +15 -0
  15. package/dist/mkdicomwebConfig.js +342 -0
  16. package/dist/operation/InstanceDeduplicate.js +103 -0
  17. package/dist/operation/ScanStudy.js +47 -0
  18. package/dist/operation/StudyData.js +520 -0
  19. package/dist/operation/ThumbnailService.js +198 -0
  20. package/dist/operation/adapter/decodeImage.js +12 -0
  21. package/dist/operation/adapter/getImageInfo.js +38 -0
  22. package/dist/operation/adapter/transcodeImage.js +519 -0
  23. package/dist/operation/extractImageFrames.js +55 -0
  24. package/dist/operation/getDataSet.js +65 -0
  25. package/dist/operation/getEncapsulatedImageFrame.js +35 -0
  26. package/dist/operation/getNumberOfFrames.js +13 -0
  27. package/dist/operation/getUncompressedImageFrame.js +7 -0
  28. package/dist/operation/getVR.js +24 -0
  29. package/dist/operation/getValue.js +216 -0
  30. package/dist/operation/validateMetadata.js +40 -0
  31. package/dist/program/index.js +14 -0
  32. package/dist/rejectMain.js +12 -0
  33. package/dist/serverMain.js +21 -0
  34. package/dist/util/IdCreator.js +42 -0
  35. package/dist/util/adaptProgramOpts.js +39 -0
  36. package/dist/util/compressionOptionParser.js +13 -0
  37. package/dist/util/resultsReport.ts +87 -0
  38. package/dist/writer/CompleteStudyWriter.js +129 -0
  39. package/dist/writer/DeduplicateWriter.js +33 -0
  40. package/dist/writer/ExpandUriPath.js +16 -0
  41. package/dist/writer/HashDataWriter.js +74 -0
  42. package/dist/writer/ImageFrameWriter.js +59 -0
  43. package/dist/writer/MultipartAttribute.js +8 -0
  44. package/dist/writer/MultipartHeader.js +14 -0
  45. package/dist/writer/RawDicomWriter.js +44 -0
  46. package/dist/writer/ThumbnailWriter.js +14 -0
  47. package/dist/writer/VideoWriter.js +53 -0
  48. package/dist/writer/WriteMultipart.js +22 -0
  49. package/dist/writer/WriteStream.js +64 -0
  50. package/package.json +11 -2
@@ -0,0 +1,520 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const hashFactory = require('node-object-hash');
4
+ const { JSONReader, JSONWriter, Tags, TagLists } = require('@radicalimaging/static-wado-util');
5
+
6
+ const { getValue, setValue, getList, setList } = Tags;
7
+ const hasher = hashFactory.hasher();
8
+
9
+ const getSeriesInstanceUid = seriesInstance => getValue(seriesInstance, Tags.SeriesInstanceUID);
10
+
11
+ /**
12
+ * StudyData contains information about the grouped study data. It is used to create
13
+ * study level information such as study or series level metadata, and to group things
14
+ * together during processing, such as handling the study level deduplicated group
15
+ * files.
16
+ *
17
+ * It also has methods on performing study level operations such as patient name updates
18
+ * or instance remove operations, and has methods to handle avoiding needing to store instance
19
+ * level data multiple times when it already exists.
20
+ */
21
+ class StudyData {
22
+ constructor(
23
+ { studyInstanceUid, studyPath, deduplicatedPath, deduplicatedInstancesPath },
24
+ { isGroup, clean, hashStudyUidPath }
25
+ ) {
26
+ this.studyInstanceUid = studyInstanceUid;
27
+ this.studyPath = studyPath;
28
+ this.isGroup = isGroup;
29
+ this.deduplicatedPath = deduplicatedPath;
30
+ this.deduplicatedInstancesPath = deduplicatedInstancesPath;
31
+ this.hashStudyUidPath = hashStudyUidPath;
32
+ this.clean = clean;
33
+ this.clear();
34
+ }
35
+
36
+ clear() {
37
+ this.deduplicated = [];
38
+ this.extractData = {};
39
+ // The list of already existing files read in to create this object
40
+ this.existingFiles = [];
41
+ // The read hashes is the hashes of the files that are read, both as groups and internally
42
+ this.readHashes = {};
43
+ // The deduplicated hashes are just the hashes for individual items
44
+ this.deduplicatedHashes = {};
45
+ this.sopInstances = {};
46
+ this.instanceFiles = 0;
47
+
48
+ // Used to track if new instances have been added.
49
+ this.newInstancesAdded = 0;
50
+ }
51
+
52
+ /**
53
+ * Clean the directory, and/or read existing data in the isInstances or isGroup files.
54
+ * TODO - implement this.
55
+ */
56
+ async init({ clean }) {
57
+ if (clean) {
58
+ // Wipe out the study directory entirely, as well as the deduplicatedRoot and instancesRoot
59
+ }
60
+
61
+ console.verbose('studyInstanceUid', this.studyInstanceUid);
62
+ console.verbose('studyPath', this.studyPath);
63
+ console.verbose('deduplicatedPath', this.deduplicatedPath);
64
+ console.verbose('deduplicatedInstancesPath', this.deduplicatedInstancesPath);
65
+
66
+ this.groupFiles = 0;
67
+ const studyDeduplicated = await JSONReader(this.studyPath, 'deduplicated/index.json.gz', []);
68
+ const info = studyDeduplicated[0];
69
+ if (info) {
70
+ const hash = getValue(info, Tags.DeduppedHash);
71
+ if (this.hashStudyUidPath) {
72
+ console.verbose(
73
+ 'Reading studies/<studyUIDPath>/<studyUIDSubpath>/<studyUID>/deduplicated/index.json.gz'
74
+ );
75
+ } else {
76
+ console.verbose('Reading studies/<studyUID>/deduplicated/index.json.gz');
77
+ }
78
+ this.readDeduplicatedData('index.json.gz', studyDeduplicated, hash);
79
+ } else {
80
+ console.log(
81
+ 'No deduplicated/index.json to read in',
82
+ this.studyPath,
83
+ '/deduplicated/index.json.gz'
84
+ );
85
+ }
86
+ if (this.deduplicatedPath) {
87
+ this.groupFiles = await this.readDeduplicated(this.deduplicatedPath);
88
+ console.verbose('Read groupFiles:', this.groupFiles);
89
+ }
90
+ if (this.deduplicatedInstancesPath) {
91
+ this.instanceFiles = await this.readDeduplicated(this.deduplicatedInstancesPath);
92
+ }
93
+ }
94
+
95
+ get numberOfInstances() {
96
+ return this.deduplicated.length;
97
+ }
98
+
99
+ /**
100
+ * Indicate if this study is 'dirty', that is, has been updated.
101
+ * That is NOT to say that the metadata files generated from this study data are up to date, that is
102
+ * a separate type of check.
103
+ */
104
+ get dirty() {
105
+ return this.newInstancesAdded > 0 || this.existingFiles.length > 1 || this.instanceFiles > 0;
106
+ }
107
+
108
+ async dirtyMetadata() {
109
+ if (this.dirty) {
110
+ console.verbose('dirtyMetadata::Group data is dirty');
111
+ return true;
112
+ }
113
+ if (this.groupFiles > 0) {
114
+ console.verbose("dirtyMetadata::Study level deduplicated doesn't match group files");
115
+ }
116
+ try {
117
+ const studyFile = await JSONReader(this.studyPath, 'index.json.gz', null);
118
+ if (!studyFile) {
119
+ console.verbose('dirtyMetadata::studyIndex');
120
+ return true;
121
+ }
122
+ const hashValue = getValue(studyFile, Tags.DeduppedHash);
123
+ if (this.existingFiles[0].indexOf(hashValue) == -1) {
124
+ return false;
125
+ }
126
+ console.verbose('dirtyMetadata::Dedupped hash missing');
127
+ return true;
128
+ } catch (e) {
129
+ console.verbose('dirtyMetadata::Exception, assume study metadata is dirty', e);
130
+ return true;
131
+ }
132
+ }
133
+
134
+ async reject(seriesInstanceUid, sopInstanceUid /* , reason */) {
135
+ // TODO - actually add a reject note...
136
+ this.newInstancesAdded += 1;
137
+ for (let i = 0; i < this.deduplicated.length; i++) {
138
+ const recombined = await this.recombine(i);
139
+ const reSeriesUid = getValue(recombined, Tags.SeriesInstanceUID);
140
+ if (reSeriesUid !== seriesInstanceUid) continue;
141
+ const reSop = getValue(recombined, Tags.SOPInstanceUID);
142
+ if (!reSop) continue;
143
+ if (sopInstanceUid && reSop !== sopInstanceUid) continue;
144
+ setValue(this.deduplicated[i], Tags.DeduppedType, 'deleted');
145
+ }
146
+ }
147
+
148
+ async delete() {
149
+ await fs.rmSync(this.studyPath, { recursive: true, force: true });
150
+ await fs.rmSync(this.deduplicatedInstancesPath, {
151
+ recursive: true,
152
+ force: true,
153
+ });
154
+ await fs.rmSync(this.deduplicatedPath, { recursive: true, force: true });
155
+ this.clear();
156
+ }
157
+
158
+ async getOrLoadExtract(hashKey) {
159
+ let item = this.extractData[hashKey];
160
+ if (!item) {
161
+ item = await JSONReader.readHashData(this.studyPath, hashKey);
162
+ if (!item) {
163
+ console.error('Unable to read hashdata', hashKey);
164
+ return item;
165
+ }
166
+ this.extractData[hashKey] = item;
167
+ }
168
+ return item;
169
+ }
170
+
171
+ assignUndefined(ret, item) {
172
+ if (!item) return;
173
+ Object.keys(item).forEach(key => {
174
+ if (ret[key]) return;
175
+ ret[key] = item[key];
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Create a full study instance data for instance at index
181
+ */
182
+ async recombine(indexOrSop) {
183
+ const index = typeof indexOrSop === 'string' ? this.sopInstances[indexOrSop] : indexOrSop;
184
+ const deduplicated = this.deduplicated[index];
185
+ if (index < 0 || index >= this.deduplicated.length) {
186
+ throw new Error(`Can't read index ${index}, out of bounds [0..${this.deduplicated.length})`);
187
+ }
188
+ const refs = getList(deduplicated, Tags.DeduppedRef);
189
+ if (!refs) {
190
+ console.log('No refs for', deduplicated);
191
+ return deduplicated;
192
+ }
193
+ const ret = { ...deduplicated };
194
+ for (const hashKey of refs) {
195
+ const item = await this.getOrLoadExtract(hashKey);
196
+ this.assignUndefined(ret, item);
197
+ }
198
+ return ret;
199
+ }
200
+
201
+ async addExtracted(callback, hashKey, item) {
202
+ if (this.extractData[hashKey]) {
203
+ if (this.verbose)
204
+ console.log('Already have extracted', hashKey, getValue(item, Tags.DeduppedType));
205
+ return;
206
+ }
207
+ await callback.bulkdata(this, hashKey, item);
208
+ this.extractData[hashKey] = item;
209
+ }
210
+
211
+ sopExists(sopUID) {
212
+ const sopValue = (sopUID && sopUID.Value && sopUID.Value[0]) || sopUID;
213
+ return sopValue && this.sopInstances[sopValue] !== undefined;
214
+ }
215
+
216
+ addDeduplicated(data, filename) {
217
+ if (!this.isGroup) return;
218
+ // TODO - check the hash code on the added data, if it has already been seen then ignore this item.
219
+ if (this.internalAddDeduplicated(data, filename)) {
220
+ this.newInstancesAdded += 1;
221
+ }
222
+ }
223
+
224
+ /** Add the instance if not already present */
225
+ internalAddDeduplicated(data, filename = 'internal') {
226
+ const hashValue = TagLists.addHash(data, Tags.InstanceType);
227
+ if (!hashValue || this.deduplicatedHashes[hashValue]) {
228
+ // console.log('Not adding', hashValue, 'because the hash exists');
229
+ return false;
230
+ }
231
+ const sopValue = getValue(data, Tags.SOPInstanceUID);
232
+ const sopIndex = this.sopInstances[sopValue];
233
+ if (!sopValue) {
234
+ console.warn('No sop value in', filename, 'reading', data);
235
+ return false;
236
+ }
237
+ this.deduplicatedHashes[hashValue] = data;
238
+ this.readHashes[hashValue] = filename;
239
+ if (sopIndex !== undefined) {
240
+ console.noQuiet('Replacing SOP', sopValue, 'at index', sopIndex);
241
+ this.deduplicated[sopIndex] = data;
242
+ } else {
243
+ this.sopInstances[sopValue] = this.deduplicated.length;
244
+ this.deduplicated.push(data);
245
+ }
246
+
247
+ return !!hashValue;
248
+ }
249
+
250
+ async listJsonFiles(dir) {
251
+ const names = await fs.promises.readdir(dir);
252
+ if (!names || names.length == 0) return [];
253
+ const gzNames = names.filter(name => name.indexOf('.gz') > 0);
254
+ const statInfo = gzNames
255
+ .map(name => {
256
+ const stat = fs.statSync(path.join(dir, name));
257
+ return {
258
+ ...stat,
259
+ name,
260
+ hash: this.removeGz(name),
261
+ };
262
+ })
263
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
264
+ return statInfo;
265
+ }
266
+
267
+ /**
268
+ * Reads the deduplicated directory, finding the newest file in the directory of type JSON,
269
+ * and then reads it in. It then reads in all un-referenced files, and adds references to those to the
270
+ * current path, NOT changing the hash value of the top level element.
271
+ *
272
+ * Records the files read in referencedFiles list.
273
+ * Sets the dirty deduplicated flag if at least two instances were read.
274
+ *
275
+ * Does NOT call the readInstances, which is a separate step which can be executed after this one.
276
+ *
277
+ * @param {*} deduplicatedDirectory
278
+ */
279
+ async readDeduplicated(deduplicatedDirectory) {
280
+ let readCount = 0;
281
+ try {
282
+ const files = await this.listJsonFiles(deduplicatedDirectory);
283
+ if (!files || !files.length) {
284
+ console.verbose('No deduplicated for', deduplicatedDirectory);
285
+ return 0;
286
+ }
287
+ console.verbose('There are', files.length, 'files to check');
288
+ for (let i = 0; i < files.length; i++) {
289
+ const stat = files[i];
290
+ const { hash } = stat;
291
+ if (this.readHashes[hash]) {
292
+ continue;
293
+ }
294
+ readCount += 1;
295
+ await this.readDeduplicatedFile(deduplicatedDirectory, stat);
296
+ }
297
+ console.verbose('Done checking', deduplicatedDirectory);
298
+ } catch (e) {
299
+ // No-op console.log(e);
300
+ }
301
+ return readCount;
302
+ }
303
+
304
+ async readDeduplicatedFile(dir, stat) {
305
+ const { hash, name } = stat;
306
+ try {
307
+ console.verbose('Reading deduplicated file', name);
308
+ const data = await JSONReader(dir, name);
309
+ this.readDeduplicatedData(name, data, hash);
310
+ } catch (e) {
311
+ console.error('Unable to read', dir, name);
312
+ }
313
+ }
314
+
315
+ readDeduplicatedData(name, data, hash) {
316
+ this.readHashes[hash] = name;
317
+ this.existingFiles.push(name);
318
+ const listData = (Array.isArray(data) && data) || [data];
319
+ listData.forEach(item => {
320
+ const type = getValue(item, Tags.DeduppedType);
321
+ if (type === Tags.InstanceType || type === Tags.DeletedType) {
322
+ this.internalAddDeduplicated(item, name);
323
+ } else if (type == 'info') {
324
+ const refs = getList(item, Tags.DeduppedRef);
325
+ if (refs) {
326
+ refs.forEach(hashValue => {
327
+ this.readHashes[hashValue] = `${hashValue}.gz`;
328
+ });
329
+ }
330
+ } else {
331
+ const hashValue = getValue(item, Tags.DeduppedHash);
332
+ if (hashValue) {
333
+ this.extractData[hashValue] = item;
334
+ }
335
+ }
336
+ });
337
+ }
338
+
339
+ async writeMetadata() {
340
+ const anInstance = await this.recombine(0);
341
+ const series = {};
342
+
343
+ for (let i = 0; i < this.numberOfInstances; i++) {
344
+ const seriesInstance = await this.recombine(i);
345
+ const type = getValue(seriesInstance, Tags.DeduppedType);
346
+ if (type == 'deleted') {
347
+ console.log(
348
+ 'Skipping deleted instance',
349
+ type,
350
+ getValue(seriesInstance, Tags.SeriesInstanceUID)
351
+ );
352
+ continue;
353
+ }
354
+ const seriesInstanceUid = getSeriesInstanceUid(seriesInstance);
355
+ if (!seriesInstanceUid) {
356
+ console.log('Cant get seriesUid from', Tags.SeriesInstanceUID, seriesInstance);
357
+ continue;
358
+ }
359
+ if (!series[seriesInstanceUid]) {
360
+ const seriesQuery = TagLists.extract(seriesInstance, 'series', TagLists.SeriesQuery);
361
+ const seriesPath = path.join(this.studyPath, 'series', seriesInstanceUid);
362
+ series[seriesInstanceUid] = {
363
+ seriesPath,
364
+ seriesQuery,
365
+ instances: [],
366
+ instancesQuery: [],
367
+ };
368
+ }
369
+ series[seriesInstanceUid].instances.push(seriesInstance);
370
+ series[seriesInstanceUid].instancesQuery.push(
371
+ TagLists.extract(seriesInstance, 'instance', TagLists.InstanceQuery)
372
+ );
373
+ }
374
+
375
+ const seriesList = [];
376
+ const modalitiesInStudy = [];
377
+ let numberOfInstances = 0;
378
+ let numberOfSeries = 0;
379
+
380
+ for (const seriesUid of Object.keys(series)) {
381
+ const singleSeries = series[seriesUid];
382
+ const { seriesQuery, seriesPath, instances, instancesQuery } = singleSeries;
383
+ seriesQuery[Tags.NumberOfSeriesRelatedInstances] = {
384
+ vr: 'IS',
385
+ Value: [instances.length],
386
+ };
387
+ numberOfInstances += instances.length;
388
+ numberOfSeries += 1;
389
+ const modality = getValue(seriesQuery, Tags.Modality);
390
+ seriesList.push(seriesQuery);
391
+ if (modalitiesInStudy.indexOf(modality) == -1) modalitiesInStudy.push(modality);
392
+ await JSONWriter(seriesPath, 'metadata', instances, {
393
+ gzip: true,
394
+ index: false,
395
+ });
396
+ // Write out a series singleton that has just the series response for a single series.
397
+ await JSONWriter(seriesPath, 'series-singleton.json', [seriesQuery], {
398
+ gzip: true,
399
+ index: false,
400
+ });
401
+ await JSONWriter(seriesPath, 'instances', instancesQuery);
402
+ console.noQuiet('Wrote instances', seriesUid, 'with', instancesQuery.length);
403
+ }
404
+
405
+ await JSONWriter(this.studyPath, 'series', seriesList);
406
+ console.noQuiet('Wrote series', this.studyInstanceUid, 'with', seriesList.length);
407
+
408
+ const studyQuery = TagLists.extract(anInstance, 'study', TagLists.PatientStudyQuery);
409
+ studyQuery[Tags.ModalitiesInStudy] = { Value: modalitiesInStudy, vr: 'CS' };
410
+ studyQuery[Tags.NumberOfStudyRelatedInstances] = {
411
+ Value: [numberOfInstances],
412
+ vr: 'IS',
413
+ };
414
+ studyQuery[Tags.NumberOfStudyRelatedSeries] = {
415
+ Value: [numberOfSeries],
416
+ vr: 'IS',
417
+ };
418
+ // Write the index.json directly in the studyPath directory.
419
+ await JSONWriter(this.studyPath, 'index.json', [studyQuery], {
420
+ gzip: true,
421
+ index: false,
422
+ });
423
+
424
+ const infoItem = this.createInfo();
425
+ console.noQuiet(
426
+ 'Writing deduplicated study data with',
427
+ Object.values(this.extractData).length,
428
+ 'extract items and',
429
+ this.deduplicated.length,
430
+ 'instance items'
431
+ );
432
+ await JSONWriter(this.studyPath, 'deduplicated', [
433
+ infoItem,
434
+ ...Object.values(this.extractData),
435
+ ...this.deduplicated,
436
+ ]);
437
+
438
+ return studyQuery;
439
+ }
440
+
441
+ /**
442
+ * Creates an information item containing the hash value of this item type.
443
+ */
444
+ createInfo() {
445
+ const data = {};
446
+ const hashValue = hasher.hash(this.deduplicated);
447
+ setValue(data, Tags.DeduppedHash, hashValue);
448
+ setValue(data, Tags.DeduppedType, 'info');
449
+ return data;
450
+ }
451
+
452
+ removeGz(name) {
453
+ const gzIndex = name.indexOf('.gz');
454
+ return (gzIndex > 0 && name.substring(0, gzIndex)) || name;
455
+ }
456
+
457
+ async deleteInstancesReferenced() {
458
+ const deduplicatedDirectory = this.deduplicatedInstancesPath;
459
+ if (!fs.existsSync(deduplicatedDirectory)) return;
460
+ console.log(
461
+ 'Deleting instances referenced in',
462
+ this.studyInstanceUid,
463
+ this.deduplicatedInstancesPath
464
+ );
465
+ const files = await this.listJsonFiles(deduplicatedDirectory);
466
+ console.noQuiet('Deleting', files.length, 'files');
467
+ let deleteCount = 0;
468
+ for (let i = 0; i < files.length; i++) {
469
+ const stat = files[i];
470
+ const { hash } = stat;
471
+ if (this.readHashes[hash]) {
472
+ console.noQuiet('Deleting', deduplicatedDirectory, stat.name);
473
+ try {
474
+ fs.unlinkSync(path.join(deduplicatedDirectory, stat.name));
475
+ deleteCount += 1;
476
+ } catch (e) {
477
+ console.log('Delete failed', e);
478
+ }
479
+ }
480
+ }
481
+ if (deleteCount === files.length) {
482
+ console.noQuiet('Deleting instances directory', deduplicatedDirectory);
483
+ fs.rmdirSync(deduplicatedDirectory);
484
+ }
485
+ }
486
+
487
+ /** Writes the deduplicated group */
488
+ async writeDeduplicatedGroup() {
489
+ const data = this.createInfo();
490
+ const hashValue = getValue(data, Tags.DeduppedHash);
491
+ console.noQuiet(
492
+ 'Writing deduplicated hash data set data with',
493
+ Object.values(this.extractData).length,
494
+ 'extract items and',
495
+ this.deduplicated.length,
496
+ 'instance items'
497
+ );
498
+ setList(
499
+ data,
500
+ Tags.DeduppedRef,
501
+ Object.keys(this.readHashes).filter(() => this.deduplicatedHashes[hashValue] == undefined)
502
+ );
503
+ const deduplicatedList = [data, ...Object.values(this.extractData), ...this.deduplicated];
504
+ // const naturalList = deduplicatedList.map(Tags.naturalizeDataset);
505
+ // // console.log("naturalList=", JSON.stringify(naturalList,null,2));
506
+ // console.log("Going to write study data", naturalList.length);
507
+ // await JSONWriter(this.deduplicatedPath, hashValue, naturalList, { gzip: true, index: false });
508
+ await JSONWriter(this.deduplicatedPath, hashValue, deduplicatedList, {
509
+ gzip: true,
510
+ index: false,
511
+ });
512
+ console.noQuiet('Wrote naturalized dataset');
513
+ }
514
+
515
+ getSopUids() {
516
+ return Object.keys(this.sopInstances);
517
+ }
518
+ }
519
+
520
+ module.exports = StudyData;