@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.
- package/CHANGELOG.md +234 -0
- package/README.md +1 -1
- package/bin/mkdicomweb.mjs +8 -0
- package/dist/canvas-xaqc8v6y.node +0 -0
- package/dist/index.js +481518 -0
- package/lib/DeleteStudy.js +9 -2
- package/lib/RejectInstance.js +15 -3
- package/lib/StaticWado.js +245 -41
- package/lib/createPart10.js +8 -4
- package/lib/createThumbnail.js +181 -0
- package/lib/index.js +2 -0
- package/lib/instanceMain.js +1 -0
- package/lib/mkdicomwebConfig.js +122 -19
- package/lib/model/TagLists.js +24 -3
- package/lib/model/uids.js +5 -1
- package/lib/operation/InstanceDeduplicate.js +14 -4
- package/lib/operation/ScanStudy.js +38 -5
- package/lib/operation/StudyData.js +158 -38
- package/lib/operation/ThumbnailService.js +65 -31
- package/lib/operation/adapter/decodeImage.js +5 -2
- package/lib/operation/adapter/getImageInfo.js +19 -1
- package/lib/operation/adapter/transcodeImage.js +174 -52
- package/lib/operation/extractImageFrames.js +19 -4
- package/lib/operation/getDataSet.js +41 -7
- package/lib/operation/getEncapsulatedImageFrame.js +30 -6
- package/lib/operation/getUncompressedImageFrame.js +10 -2
- package/lib/operation/getVR.js +10 -2
- package/lib/operation/getValue.js +40 -12
- package/lib/operation/validateMetadata.js +40 -0
- package/lib/program/index.js +5 -3
- package/lib/serverMain.js +27 -0
- package/lib/util/IdCreator.js +32 -6
- package/lib/util/adaptProgramOpts.js +1 -1
- package/lib/util/compressionOptionParser.js +12 -3
- package/lib/util/resultsReport.ts +88 -0
- package/lib/writer/CompleteStudyWriter.js +49 -13
- package/lib/writer/DeduplicateWriter.js +2 -1
- package/lib/writer/ExpandUriPath.js +4 -1
- package/lib/writer/HashDataWriter.js +5 -1
- package/lib/writer/ImageFrameWriter.js +38 -13
- package/lib/writer/RawDicomWriter.js +11 -2
- package/lib/writer/VideoWriter.js +19 -7
- package/lib/writer/WriteMultipart.js +3 -1
- package/lib/writer/WriteStream.js +12 -4
- package/lib/writer/static-dicomweb.code-workspace +8 -0
- package/package.json +23 -19
- package/bin/mkdicomweb.js +0 -9
- package/license.md +0 -21
|
@@ -8,7 +8,8 @@ const TagLists = require("../model/TagLists");
|
|
|
8
8
|
const { getValue, setValue, getList, setList } = Tags;
|
|
9
9
|
const hasher = hashFactory.hasher();
|
|
10
10
|
|
|
11
|
-
const getSeriesInstanceUid = (seriesInstance) =>
|
|
11
|
+
const getSeriesInstanceUid = (seriesInstance) =>
|
|
12
|
+
getValue(seriesInstance, Tags.SeriesInstanceUID);
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* StudyData contains information about the grouped study data. It is used to create
|
|
@@ -21,12 +22,21 @@ const getSeriesInstanceUid = (seriesInstance) => getValue(seriesInstance, Tags.S
|
|
|
21
22
|
* level data multiple times when it already exists.
|
|
22
23
|
*/
|
|
23
24
|
class StudyData {
|
|
24
|
-
constructor(
|
|
25
|
+
constructor(
|
|
26
|
+
{
|
|
27
|
+
studyInstanceUid,
|
|
28
|
+
studyPath,
|
|
29
|
+
deduplicatedPath,
|
|
30
|
+
deduplicatedInstancesPath,
|
|
31
|
+
},
|
|
32
|
+
{ isGroup, clean, hashStudyUidPath },
|
|
33
|
+
) {
|
|
25
34
|
this.studyInstanceUid = studyInstanceUid;
|
|
26
35
|
this.studyPath = studyPath;
|
|
27
36
|
this.isGroup = isGroup;
|
|
28
37
|
this.deduplicatedPath = deduplicatedPath;
|
|
29
38
|
this.deduplicatedInstancesPath = deduplicatedInstancesPath;
|
|
39
|
+
this.hashStudyUidPath = hashStudyUidPath;
|
|
30
40
|
this.clean = clean;
|
|
31
41
|
this.clear();
|
|
32
42
|
}
|
|
@@ -55,22 +65,49 @@ class StudyData {
|
|
|
55
65
|
if (clean) {
|
|
56
66
|
// Wipe out the study directory entirely, as well as the deduplicatedRoot and instancesRoot
|
|
57
67
|
}
|
|
68
|
+
|
|
69
|
+
console.verbose("studyInstanceUid", this.studyInstanceUid);
|
|
70
|
+
console.verbose("studyPath", this.studyPath);
|
|
71
|
+
console.verbose("deduplicatedPath", this.deduplicatedPath);
|
|
72
|
+
console.verbose(
|
|
73
|
+
"deduplicatedInstancesPath",
|
|
74
|
+
this.deduplicatedInstancesPath,
|
|
75
|
+
);
|
|
76
|
+
|
|
58
77
|
this.groupFiles = 0;
|
|
59
|
-
const studyDeduplicated = await JSONReader(
|
|
78
|
+
const studyDeduplicated = await JSONReader(
|
|
79
|
+
this.studyPath,
|
|
80
|
+
"deduplicated/index.json.gz",
|
|
81
|
+
[],
|
|
82
|
+
);
|
|
60
83
|
const info = studyDeduplicated[0];
|
|
61
84
|
if (info) {
|
|
62
85
|
const hash = getValue(info, Tags.DeduppedHash);
|
|
63
|
-
|
|
86
|
+
if (this.hashStudyUidPath) {
|
|
87
|
+
console.verbose(
|
|
88
|
+
"Reading studies/<studyUIDPath>/<studyUIDSubpath>/<studyUID>/deduplicated/index.json.gz",
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
console.verbose(
|
|
92
|
+
"Reading studies/<studyUID>/deduplicated/index.json.gz",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
64
95
|
this.readDeduplicatedData("index.json.gz", studyDeduplicated, hash);
|
|
65
96
|
} else {
|
|
66
|
-
console.log(
|
|
97
|
+
console.log(
|
|
98
|
+
"No deduplicated/index.json to read in",
|
|
99
|
+
this.studyPath,
|
|
100
|
+
"/deduplicated/index.json.gz",
|
|
101
|
+
);
|
|
67
102
|
}
|
|
68
103
|
if (this.deduplicatedPath) {
|
|
69
104
|
this.groupFiles = await this.readDeduplicated(this.deduplicatedPath);
|
|
70
105
|
console.verbose("Read groupFiles:", this.groupFiles);
|
|
71
106
|
}
|
|
72
107
|
if (this.deduplicatedInstancesPath) {
|
|
73
|
-
this.instanceFiles = await this.readDeduplicated(
|
|
108
|
+
this.instanceFiles = await this.readDeduplicated(
|
|
109
|
+
this.deduplicatedInstancesPath,
|
|
110
|
+
);
|
|
74
111
|
}
|
|
75
112
|
}
|
|
76
113
|
|
|
@@ -84,7 +121,11 @@ class StudyData {
|
|
|
84
121
|
* a separate type of check.
|
|
85
122
|
*/
|
|
86
123
|
get dirty() {
|
|
87
|
-
return
|
|
124
|
+
return (
|
|
125
|
+
this.newInstancesAdded > 0 ||
|
|
126
|
+
this.existingFiles.length > 1 ||
|
|
127
|
+
this.instanceFiles > 0
|
|
128
|
+
);
|
|
88
129
|
}
|
|
89
130
|
|
|
90
131
|
async dirtyMetadata() {
|
|
@@ -93,7 +134,9 @@ class StudyData {
|
|
|
93
134
|
return true;
|
|
94
135
|
}
|
|
95
136
|
if (this.groupFiles > 0) {
|
|
96
|
-
console.verbose(
|
|
137
|
+
console.verbose(
|
|
138
|
+
"dirtyMetadata::Study level deduplicated doesn't match group files",
|
|
139
|
+
);
|
|
97
140
|
}
|
|
98
141
|
try {
|
|
99
142
|
const studyFile = await JSONReader(this.studyPath, "index.json.gz", null);
|
|
@@ -108,7 +151,10 @@ class StudyData {
|
|
|
108
151
|
console.verbose("dirtyMetadata::Dedupped hash missing");
|
|
109
152
|
return true;
|
|
110
153
|
} catch (e) {
|
|
111
|
-
console.verbose(
|
|
154
|
+
console.verbose(
|
|
155
|
+
"dirtyMetadata::Exception, assume study metadata is dirty",
|
|
156
|
+
e,
|
|
157
|
+
);
|
|
112
158
|
return true;
|
|
113
159
|
}
|
|
114
160
|
}
|
|
@@ -129,7 +175,10 @@ class StudyData {
|
|
|
129
175
|
|
|
130
176
|
async delete() {
|
|
131
177
|
await fs.rmSync(this.studyPath, { recursive: true, force: true });
|
|
132
|
-
await fs.rmSync(this.deduplicatedInstancesPath, {
|
|
178
|
+
await fs.rmSync(this.deduplicatedInstancesPath, {
|
|
179
|
+
recursive: true,
|
|
180
|
+
force: true,
|
|
181
|
+
});
|
|
133
182
|
await fs.rmSync(this.deduplicatedPath, { recursive: true, force: true });
|
|
134
183
|
this.clear();
|
|
135
184
|
}
|
|
@@ -159,10 +208,15 @@ class StudyData {
|
|
|
159
208
|
* Create a full study instance data for instance at index
|
|
160
209
|
*/
|
|
161
210
|
async recombine(indexOrSop) {
|
|
162
|
-
const index =
|
|
211
|
+
const index =
|
|
212
|
+
typeof indexOrSop === "string"
|
|
213
|
+
? this.sopInstances[indexOrSop]
|
|
214
|
+
: indexOrSop;
|
|
163
215
|
const deduplicated = this.deduplicated[index];
|
|
164
216
|
if (index < 0 || index >= this.deduplicated.length) {
|
|
165
|
-
throw new Error(
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Can't read index ${index}, out of bounds [0..${this.deduplicated.length})`,
|
|
219
|
+
);
|
|
166
220
|
}
|
|
167
221
|
const refs = getList(deduplicated, Tags.DeduppedRef);
|
|
168
222
|
if (!refs) {
|
|
@@ -179,7 +233,12 @@ class StudyData {
|
|
|
179
233
|
|
|
180
234
|
async addExtracted(callback, hashKey, item) {
|
|
181
235
|
if (this.extractData[hashKey]) {
|
|
182
|
-
if (this.verbose)
|
|
236
|
+
if (this.verbose)
|
|
237
|
+
console.log(
|
|
238
|
+
"Already have extracted",
|
|
239
|
+
hashKey,
|
|
240
|
+
getValue(item, Tags.DeduppedType),
|
|
241
|
+
);
|
|
183
242
|
return;
|
|
184
243
|
}
|
|
185
244
|
await callback.bulkdata(this, hashKey, item);
|
|
@@ -215,7 +274,7 @@ class StudyData {
|
|
|
215
274
|
this.deduplicatedHashes[hashValue] = data;
|
|
216
275
|
this.readHashes[hashValue] = filename;
|
|
217
276
|
if (sopIndex !== undefined) {
|
|
218
|
-
console.
|
|
277
|
+
console.noQuiet("Replacing SOP", sopValue, "at index", sopIndex);
|
|
219
278
|
this.deduplicated[sopIndex] = data;
|
|
220
279
|
} else {
|
|
221
280
|
this.sopInstances[sopValue] = this.deduplicated.length;
|
|
@@ -282,7 +341,7 @@ class StudyData {
|
|
|
282
341
|
async readDeduplicatedFile(dir, stat) {
|
|
283
342
|
const { hash, name } = stat;
|
|
284
343
|
try {
|
|
285
|
-
|
|
344
|
+
console.verbose("Reading deduplicated file", name);
|
|
286
345
|
const data = await JSONReader(dir, name);
|
|
287
346
|
this.readDeduplicatedData(name, data, hash);
|
|
288
347
|
} catch (e) {
|
|
@@ -322,17 +381,33 @@ class StudyData {
|
|
|
322
381
|
const seriesInstance = await this.recombine(i);
|
|
323
382
|
const type = getValue(seriesInstance, Tags.DeduppedType);
|
|
324
383
|
if (type == "deleted") {
|
|
325
|
-
console.log(
|
|
384
|
+
console.log(
|
|
385
|
+
"Skipping deleted instance",
|
|
386
|
+
type,
|
|
387
|
+
getValue(seriesInstance, Tags.SeriesInstanceUID),
|
|
388
|
+
);
|
|
326
389
|
continue;
|
|
327
390
|
}
|
|
328
391
|
const seriesInstanceUid = getSeriesInstanceUid(seriesInstance);
|
|
329
392
|
if (!seriesInstanceUid) {
|
|
330
|
-
console.log(
|
|
393
|
+
console.log(
|
|
394
|
+
"Cant get seriesUid from",
|
|
395
|
+
Tags.SeriesInstanceUID,
|
|
396
|
+
seriesInstance,
|
|
397
|
+
);
|
|
331
398
|
continue;
|
|
332
399
|
}
|
|
333
400
|
if (!series[seriesInstanceUid]) {
|
|
334
|
-
const seriesQuery = TagLists.extract(
|
|
335
|
-
|
|
401
|
+
const seriesQuery = TagLists.extract(
|
|
402
|
+
seriesInstance,
|
|
403
|
+
"series",
|
|
404
|
+
TagLists.SeriesQuery,
|
|
405
|
+
);
|
|
406
|
+
const seriesPath = path.join(
|
|
407
|
+
this.studyPath,
|
|
408
|
+
"series",
|
|
409
|
+
seriesInstanceUid,
|
|
410
|
+
);
|
|
336
411
|
series[seriesInstanceUid] = {
|
|
337
412
|
seriesPath,
|
|
338
413
|
seriesQuery,
|
|
@@ -341,16 +416,20 @@ class StudyData {
|
|
|
341
416
|
};
|
|
342
417
|
}
|
|
343
418
|
series[seriesInstanceUid].instances.push(seriesInstance);
|
|
344
|
-
series[seriesInstanceUid].instancesQuery.push(
|
|
419
|
+
series[seriesInstanceUid].instancesQuery.push(
|
|
420
|
+
TagLists.extract(seriesInstance, "instance", TagLists.InstanceQuery),
|
|
421
|
+
);
|
|
345
422
|
}
|
|
346
423
|
|
|
347
424
|
const seriesList = [];
|
|
348
425
|
const modalitiesInStudy = [];
|
|
349
426
|
let numberOfInstances = 0;
|
|
350
427
|
let numberOfSeries = 0;
|
|
428
|
+
|
|
351
429
|
for (const seriesUid of Object.keys(series)) {
|
|
352
430
|
const singleSeries = series[seriesUid];
|
|
353
|
-
const { seriesQuery, seriesPath, instances, instancesQuery } =
|
|
431
|
+
const { seriesQuery, seriesPath, instances, instancesQuery } =
|
|
432
|
+
singleSeries;
|
|
354
433
|
seriesQuery[Tags.NumberOfSeriesRelatedInstances] = {
|
|
355
434
|
vr: "IS",
|
|
356
435
|
Value: [instances.length],
|
|
@@ -359,20 +438,39 @@ class StudyData {
|
|
|
359
438
|
numberOfSeries += 1;
|
|
360
439
|
const modality = getValue(seriesQuery, Tags.Modality);
|
|
361
440
|
seriesList.push(seriesQuery);
|
|
362
|
-
if (modalitiesInStudy.indexOf(modality) == -1)
|
|
441
|
+
if (modalitiesInStudy.indexOf(modality) == -1)
|
|
442
|
+
modalitiesInStudy.push(modality);
|
|
363
443
|
await JSONWriter(seriesPath, "metadata", instances, {
|
|
364
444
|
gzip: true,
|
|
365
445
|
index: false,
|
|
366
446
|
});
|
|
367
447
|
// Write out a series singleton that has just the series response for a single series.
|
|
368
|
-
await JSONWriter(seriesPath, "series-singleton.json", [seriesQuery], {
|
|
448
|
+
await JSONWriter(seriesPath, "series-singleton.json", [seriesQuery], {
|
|
449
|
+
gzip: true,
|
|
450
|
+
index: false,
|
|
451
|
+
});
|
|
369
452
|
await JSONWriter(seriesPath, "instances", instancesQuery);
|
|
453
|
+
console.noQuiet(
|
|
454
|
+
"Wrote instances",
|
|
455
|
+
seriesUid,
|
|
456
|
+
"with",
|
|
457
|
+
instancesQuery.length,
|
|
458
|
+
);
|
|
370
459
|
}
|
|
371
460
|
|
|
372
461
|
await JSONWriter(this.studyPath, "series", seriesList);
|
|
373
|
-
console.
|
|
462
|
+
console.noQuiet(
|
|
463
|
+
"Wrote series",
|
|
464
|
+
this.studyInstanceUid,
|
|
465
|
+
"with",
|
|
466
|
+
seriesList.length,
|
|
467
|
+
);
|
|
374
468
|
|
|
375
|
-
const studyQuery = TagLists.extract(
|
|
469
|
+
const studyQuery = TagLists.extract(
|
|
470
|
+
anInstance,
|
|
471
|
+
"study",
|
|
472
|
+
TagLists.PatientStudyQuery,
|
|
473
|
+
);
|
|
376
474
|
studyQuery[Tags.ModalitiesInStudy] = { Value: modalitiesInStudy, vr: "CS" };
|
|
377
475
|
studyQuery[Tags.NumberOfStudyRelatedInstances] = {
|
|
378
476
|
Value: [numberOfInstances],
|
|
@@ -389,8 +487,18 @@ class StudyData {
|
|
|
389
487
|
});
|
|
390
488
|
|
|
391
489
|
const infoItem = this.createInfo();
|
|
392
|
-
console.
|
|
393
|
-
|
|
490
|
+
console.noQuiet(
|
|
491
|
+
"Writing deduplicated study data with",
|
|
492
|
+
Object.values(this.extractData).length,
|
|
493
|
+
"extract items and",
|
|
494
|
+
this.deduplicated.length,
|
|
495
|
+
"instance items",
|
|
496
|
+
);
|
|
497
|
+
await JSONWriter(this.studyPath, "deduplicated", [
|
|
498
|
+
infoItem,
|
|
499
|
+
...Object.values(this.extractData),
|
|
500
|
+
...this.deduplicated,
|
|
501
|
+
]);
|
|
394
502
|
|
|
395
503
|
return studyQuery;
|
|
396
504
|
}
|
|
@@ -406,7 +514,6 @@ class StudyData {
|
|
|
406
514
|
return data;
|
|
407
515
|
}
|
|
408
516
|
|
|
409
|
-
/* eslint-disable-next-line class-methods-use-this */
|
|
410
517
|
removeGz(name) {
|
|
411
518
|
const gzIndex = name.indexOf(".gz");
|
|
412
519
|
return (gzIndex > 0 && name.substring(0, gzIndex)) || name;
|
|
@@ -415,15 +522,19 @@ class StudyData {
|
|
|
415
522
|
async deleteInstancesReferenced() {
|
|
416
523
|
const deduplicatedDirectory = this.deduplicatedInstancesPath;
|
|
417
524
|
if (!fs.existsSync(deduplicatedDirectory)) return;
|
|
418
|
-
console.log(
|
|
525
|
+
console.log(
|
|
526
|
+
"Deleting instances referenced in",
|
|
527
|
+
this.studyInstanceUid,
|
|
528
|
+
this.deduplicatedInstancesPath,
|
|
529
|
+
);
|
|
419
530
|
const files = await this.listJsonFiles(deduplicatedDirectory);
|
|
420
|
-
console.
|
|
531
|
+
console.noQuiet("Deleting", files.length, "files");
|
|
421
532
|
let deleteCount = 0;
|
|
422
533
|
for (let i = 0; i < files.length; i++) {
|
|
423
534
|
const stat = files[i];
|
|
424
535
|
const { hash } = stat;
|
|
425
536
|
if (this.readHashes[hash]) {
|
|
426
|
-
console.
|
|
537
|
+
console.noQuiet("Deleting", deduplicatedDirectory, stat.name);
|
|
427
538
|
try {
|
|
428
539
|
fs.unlinkSync(path.join(deduplicatedDirectory, stat.name));
|
|
429
540
|
deleteCount += 1;
|
|
@@ -433,7 +544,7 @@ class StudyData {
|
|
|
433
544
|
}
|
|
434
545
|
}
|
|
435
546
|
if (deleteCount === files.length) {
|
|
436
|
-
console.
|
|
547
|
+
console.noQuiet("Deleting instances directory", deduplicatedDirectory);
|
|
437
548
|
fs.rmdirSync(deduplicatedDirectory);
|
|
438
549
|
}
|
|
439
550
|
}
|
|
@@ -442,25 +553,34 @@ class StudyData {
|
|
|
442
553
|
async writeDeduplicatedGroup() {
|
|
443
554
|
const data = this.createInfo();
|
|
444
555
|
const hashValue = getValue(data, Tags.DeduppedHash);
|
|
445
|
-
console.
|
|
556
|
+
console.noQuiet(
|
|
446
557
|
"Writing deduplicated hash data set data with",
|
|
447
558
|
Object.values(this.extractData).length,
|
|
448
559
|
"extract items and",
|
|
449
560
|
this.deduplicated.length,
|
|
450
|
-
"instance items"
|
|
561
|
+
"instance items",
|
|
451
562
|
);
|
|
452
563
|
setList(
|
|
453
564
|
data,
|
|
454
565
|
Tags.DeduppedRef,
|
|
455
|
-
Object.keys(this.readHashes).filter(
|
|
566
|
+
Object.keys(this.readHashes).filter(
|
|
567
|
+
() => this.deduplicatedHashes[hashValue] == undefined,
|
|
568
|
+
),
|
|
456
569
|
);
|
|
457
|
-
const deduplicatedList = [
|
|
570
|
+
const deduplicatedList = [
|
|
571
|
+
data,
|
|
572
|
+
...Object.values(this.extractData),
|
|
573
|
+
...this.deduplicated,
|
|
574
|
+
];
|
|
458
575
|
// const naturalList = deduplicatedList.map(Tags.naturalizeDataset);
|
|
459
576
|
// // console.log("naturalList=", JSON.stringify(naturalList,null,2));
|
|
460
577
|
// console.log("Going to write study data", naturalList.length);
|
|
461
578
|
// await JSONWriter(this.deduplicatedPath, hashValue, naturalList, { gzip: true, index: false });
|
|
462
|
-
await JSONWriter(this.deduplicatedPath, hashValue, deduplicatedList, {
|
|
463
|
-
|
|
579
|
+
await JSONWriter(this.deduplicatedPath, hashValue, deduplicatedList, {
|
|
580
|
+
gzip: true,
|
|
581
|
+
index: false,
|
|
582
|
+
});
|
|
583
|
+
console.noQuiet("Wrote naturalized dataset");
|
|
464
584
|
}
|
|
465
585
|
|
|
466
586
|
getSopUids() {
|
|
@@ -56,8 +56,12 @@ class ThumbnailService {
|
|
|
56
56
|
* @param {Object} programOpts
|
|
57
57
|
*/
|
|
58
58
|
queueThumbnail(thumbObjWrapper, programOpts) {
|
|
59
|
-
const { id, imageFrame, transcodedId, transcodedImageFrame, frameIndex } =
|
|
60
|
-
|
|
59
|
+
const { id, imageFrame, transcodedId, transcodedImageFrame, frameIndex } =
|
|
60
|
+
thumbObjWrapper;
|
|
61
|
+
const getThumbContent = (originalContent, trancodedContent) =>
|
|
62
|
+
shouldThumbUseTranscoded(id, programOpts)
|
|
63
|
+
? trancodedContent
|
|
64
|
+
: originalContent;
|
|
61
65
|
|
|
62
66
|
const thumbObj = {
|
|
63
67
|
imageFrame: getThumbContent(imageFrame, transcodedImageFrame),
|
|
@@ -77,7 +81,9 @@ class ThumbnailService {
|
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
ffmpeg(input, output) {
|
|
80
|
-
execSpawn(
|
|
84
|
+
execSpawn(
|
|
85
|
+
`ffmpeg -i "${input}" -y -f image2 -frames:v 1 -update true "${output}"`,
|
|
86
|
+
);
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
dcm2jpg(input, output, options) {
|
|
@@ -110,47 +116,70 @@ class ThumbnailService {
|
|
|
110
116
|
async generateThumbnails(itemId, dataSet, metadata, callback, options) {
|
|
111
117
|
const { imageFrame, id } = this.favoriteThumbnailObj;
|
|
112
118
|
|
|
113
|
-
if (!options.thumb) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
119
|
// There are various reasons no thumbnails might be generated, so just return
|
|
118
120
|
if (!id) {
|
|
119
121
|
const pixelData = metadata[Tags.PixelData];
|
|
120
122
|
if (pixelData) {
|
|
121
123
|
const { BulkDataURI } = pixelData;
|
|
122
124
|
if (BulkDataURI?.indexOf("mp4")) {
|
|
123
|
-
fs.mkdirSync(`${itemId.sopInstanceRootPath}/rendered`, {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
125
|
+
fs.mkdirSync(`${itemId.sopInstanceRootPath}/rendered`, {
|
|
126
|
+
recursive: true,
|
|
127
|
+
});
|
|
128
|
+
const mp4Path = path.join(
|
|
129
|
+
itemId.sopInstanceRootPath,
|
|
130
|
+
"rendered/index.mp4",
|
|
131
|
+
);
|
|
132
|
+
const thumbPath = path.join(itemId.sopInstanceRootPath, "thumbnail");
|
|
133
|
+
console.verbose("MP4 - converting video format", mp4Path);
|
|
128
134
|
this.ffmpeg(mp4Path, thumbPath);
|
|
129
135
|
return thumbPath;
|
|
130
136
|
}
|
|
131
137
|
} else {
|
|
132
|
-
console.
|
|
138
|
+
console.verbose("Series is of other type...", metadata[Tags.Modality]);
|
|
133
139
|
}
|
|
134
140
|
return null;
|
|
135
141
|
}
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
|
|
143
|
+
if (!options.thumb) {
|
|
144
|
+
return null;
|
|
138
145
|
}
|
|
139
146
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
if (options.dcm2jpg) {
|
|
148
|
+
return this.dcm2jpg(
|
|
149
|
+
id.filename,
|
|
150
|
+
id.imageFrameRootPath.replace(/frames/, "thumbnail"),
|
|
151
|
+
{},
|
|
152
|
+
);
|
|
153
|
+
}
|
|
144
154
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
155
|
+
await callback.internalGenerateImage(
|
|
156
|
+
imageFrame,
|
|
157
|
+
dataSet,
|
|
158
|
+
metadata,
|
|
159
|
+
id.transferSyntaxUid,
|
|
160
|
+
async (thumbBuffer) => {
|
|
161
|
+
try {
|
|
162
|
+
if (thumbBuffer) {
|
|
163
|
+
await callback.thumbWriter(
|
|
164
|
+
id.sopInstanceRootPath,
|
|
165
|
+
this.thumbFileName,
|
|
166
|
+
thumbBuffer,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
this.copySyncThumbnail(id.sopInstanceRootPath, id.seriesRootPath);
|
|
170
|
+
this.copySyncThumbnail(id.seriesRootPath, id.studyPath);
|
|
171
|
+
Stats.StudyStats.add(
|
|
172
|
+
"Thumbnail Write",
|
|
173
|
+
`Write thumbnail ${this.thumbFileName}`,
|
|
174
|
+
100,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return this.thumbFileName;
|
|
178
|
+
} catch (e) {
|
|
179
|
+
console.log("Couldn't generate thumbnail", this.thumbFileName, e);
|
|
148
180
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
console.log("Couldn't generate thumbnail", this.thumbFileName, e);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
181
|
+
},
|
|
182
|
+
);
|
|
154
183
|
}
|
|
155
184
|
|
|
156
185
|
/**
|
|
@@ -164,13 +193,15 @@ class ThumbnailService {
|
|
|
164
193
|
*/
|
|
165
194
|
async copySyncThumbnail(sourceFolderPath, targetFolderPath) {
|
|
166
195
|
const parentPathLevel = path.join(sourceFolderPath, "../");
|
|
167
|
-
const thumbFilesPath = glob.sync(
|
|
196
|
+
const thumbFilesPath = glob.sync(
|
|
197
|
+
`${parentPathLevel}*/${this.thumbFileName}`,
|
|
198
|
+
);
|
|
168
199
|
|
|
169
200
|
const thumbIndex = getThumbIndex(thumbFilesPath.length);
|
|
170
201
|
const thumbFilePath = thumbFilesPath[thumbIndex];
|
|
171
202
|
|
|
172
203
|
if (!fs.existsSync(thumbFilePath)) {
|
|
173
|
-
console.
|
|
204
|
+
console.verbose("Thumbnail to copy does not exists");
|
|
174
205
|
return;
|
|
175
206
|
}
|
|
176
207
|
|
|
@@ -179,9 +210,12 @@ class ThumbnailService {
|
|
|
179
210
|
throw new Error(`Target path: ${targetFolderPath} is not a directory`);
|
|
180
211
|
}
|
|
181
212
|
|
|
182
|
-
fs.copyFileSync(
|
|
213
|
+
fs.copyFileSync(
|
|
214
|
+
thumbFilePath,
|
|
215
|
+
`${targetFolderPath}/${this.thumbFileName}`,
|
|
216
|
+
);
|
|
183
217
|
} catch (e) {
|
|
184
|
-
console.
|
|
218
|
+
console.verbose("The file could not be copied", e);
|
|
185
219
|
}
|
|
186
220
|
}
|
|
187
221
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
const dicomCodec = require("@cornerstonejs/dicom-codec");
|
|
2
2
|
const getImageInfo = require("./getImageInfo");
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
const imageInfo = getImageInfo(dataset);
|
|
4
|
+
function decodeImage(imageFrame, dataset, instance, transferSyntaxUid) {
|
|
5
|
+
const imageInfo = getImageInfo(dataset, instance);
|
|
6
|
+
if (imageFrame && !imageFrame.length) {
|
|
7
|
+
imageFrame = new Uint8Array(imageFrame);
|
|
8
|
+
}
|
|
6
9
|
return dicomCodec.decode(imageFrame, imageInfo, transferSyntaxUid);
|
|
7
10
|
}
|
|
8
11
|
|
|
@@ -1,7 +1,25 @@
|
|
|
1
|
+
const { Tags } = require("@radicalimaging/static-wado-util");
|
|
1
2
|
/**
|
|
2
3
|
* Minimum image info data to be used on transcode process by dicom-codec api.
|
|
3
4
|
*/
|
|
4
|
-
function getImageInfo(dataSet) {
|
|
5
|
+
function getImageInfo(dataSet, instance) {
|
|
6
|
+
if (instance) {
|
|
7
|
+
const rows = Tags.getValue(instance, Tags.Rows);
|
|
8
|
+
const columns = Tags.getValue(instance, Tags.Columns);
|
|
9
|
+
const bitsAllocated = Tags.getValue(instance, Tags.BitsAllocated);
|
|
10
|
+
const samplesPerPixel = Tags.getValue(instance, Tags.SamplesPerPixel);
|
|
11
|
+
const pixelRepresentation =
|
|
12
|
+
Tags.getValue(instance, Tags.PixelRepresentation) || 0; // not yet being used.
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
bitsAllocated,
|
|
16
|
+
samplesPerPixel,
|
|
17
|
+
rows, // Number with the image rows/height
|
|
18
|
+
columns, // Number with the image columns/width,
|
|
19
|
+
signed: pixelRepresentation === 1,
|
|
20
|
+
pixelRepresentation,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
5
23
|
const rows = dataSet.uint16("x00280010");
|
|
6
24
|
const columns = dataSet.uint16("x00280011");
|
|
7
25
|
const bitsAllocated = dataSet.uint16("x00280100");
|