@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.
- 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 +511433 -0
- package/lib/DeleteStudy.js +11 -9
- package/lib/RejectInstance.js +6 -5
- package/lib/StaticWado.js +222 -73
- package/lib/createMain.js +3 -3
- package/lib/createPart10.js +19 -19
- package/lib/createThumbnail.js +163 -0
- package/lib/deleteMain.js +2 -2
- package/lib/groupMain.js +2 -2
- package/lib/index.js +7 -5
- package/lib/indexMain.js +2 -2
- package/lib/instanceMain.js +3 -2
- package/lib/metadataMain.js +2 -2
- package/lib/mkdicomwebConfig.js +210 -115
- package/lib/model/TagLists.js +13 -4
- package/lib/model/uids.js +32 -28
- package/lib/operation/InstanceDeduplicate.js +18 -13
- package/lib/operation/ScanStudy.js +33 -7
- package/lib/operation/StudyData.js +122 -71
- package/lib/operation/ThumbnailService.js +44 -36
- package/lib/operation/adapter/decodeImage.js +7 -4
- package/lib/operation/adapter/getImageInfo.js +23 -6
- package/lib/operation/adapter/transcodeImage.js +157 -90
- package/lib/operation/extractImageFrames.js +25 -14
- package/lib/operation/getDataSet.js +4 -4
- package/lib/operation/getEncapsulatedImageFrame.js +16 -4
- package/lib/operation/getNumberOfFrames.js +2 -2
- package/lib/operation/getVR.js +12 -4
- package/lib/operation/getValue.js +42 -65
- package/lib/operation/validateMetadata.js +40 -0
- package/lib/program/index.js +2 -3
- package/lib/rejectMain.js +2 -2
- package/lib/serverMain.js +21 -0
- package/lib/util/IdCreator.js +25 -8
- package/lib/util/adaptProgramOpts.js +1 -1
- package/lib/util/compressionOptionParser.js +4 -4
- package/lib/util/resultsReport.ts +87 -0
- package/lib/writer/CompleteStudyWriter.js +42 -19
- package/lib/writer/DeduplicateWriter.js +7 -7
- package/lib/writer/ExpandUriPath.js +4 -3
- package/lib/writer/HashDataWriter.js +21 -17
- package/lib/writer/ImageFrameWriter.js +22 -10
- package/lib/writer/MultipartHeader.js +1 -1
- package/lib/writer/RawDicomWriter.js +18 -10
- package/lib/writer/ThumbnailWriter.js +2 -2
- package/lib/writer/VideoWriter.js +23 -34
- package/lib/writer/WriteMultipart.js +3 -3
- package/lib/writer/WriteStream.js +14 -12
- package/package.json +22 -18
- package/bin/mkdicomweb.js +0 -9
|
@@ -1,14 +1,40 @@
|
|
|
1
|
-
const
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
console.verbose(
|
|
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(
|
|
2
|
-
const fs = require(
|
|
3
|
-
const hashFactory = require(
|
|
4
|
-
const { JSONReader, JSONWriter } = require(
|
|
5
|
-
const { Tags } = require(
|
|
6
|
-
const TagLists = require(
|
|
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 =
|
|
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(
|
|
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,
|
|
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
|
-
|
|
64
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
119
|
+
const studyFile = await JSONReader(this.studyPath, 'index.json.gz', null);
|
|
100
120
|
if (!studyFile) {
|
|
101
|
-
console.verbose(
|
|
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(
|
|
128
|
+
console.verbose('dirtyMetadata::Dedupped hash missing');
|
|
109
129
|
return true;
|
|
110
130
|
} catch (e) {
|
|
111
|
-
console.verbose(
|
|
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,
|
|
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, {
|
|
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(
|
|
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(
|
|
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 ===
|
|
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(
|
|
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)
|
|
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 =
|
|
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(
|
|
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.
|
|
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(
|
|
255
|
+
const gzNames = names.filter(name => name.indexOf('.gz') > 0);
|
|
232
256
|
const statInfo = gzNames
|
|
233
|
-
.map(
|
|
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(
|
|
286
|
+
console.verbose('No deduplicated for', deduplicatedDirectory);
|
|
263
287
|
return 0;
|
|
264
288
|
}
|
|
265
|
-
console.verbose(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 ==
|
|
325
|
+
} else if (type == 'info') {
|
|
302
326
|
const refs = getList(item, Tags.DeduppedRef);
|
|
303
327
|
if (refs) {
|
|
304
|
-
refs.forEach(
|
|
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 ==
|
|
325
|
-
console.log(
|
|
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(
|
|
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,
|
|
335
|
-
const seriesPath = path.join(this.studyPath,
|
|
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(
|
|
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:
|
|
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,
|
|
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,
|
|
369
|
-
|
|
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,
|
|
373
|
-
console.
|
|
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,
|
|
376
|
-
studyQuery[Tags.ModalitiesInStudy] = { Value: modalitiesInStudy, vr:
|
|
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:
|
|
414
|
+
vr: 'IS',
|
|
380
415
|
};
|
|
381
416
|
studyQuery[Tags.NumberOfStudyRelatedSeries] = {
|
|
382
417
|
Value: [numberOfSeries],
|
|
383
|
-
vr:
|
|
418
|
+
vr: 'IS',
|
|
384
419
|
};
|
|
385
420
|
// Write the index.json directly in the studyPath directory.
|
|
386
|
-
await JSONWriter(this.studyPath,
|
|
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.
|
|
393
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
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(
|
|
479
|
+
console.log('Delete failed', e);
|
|
432
480
|
}
|
|
433
481
|
}
|
|
434
482
|
}
|
|
435
483
|
if (deleteCount === files.length) {
|
|
436
|
-
console.
|
|
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.
|
|
446
|
-
|
|
493
|
+
console.noQuiet(
|
|
494
|
+
'Writing deduplicated hash data set data with',
|
|
447
495
|
Object.values(this.extractData).length,
|
|
448
|
-
|
|
496
|
+
'extract items and',
|
|
449
497
|
this.deduplicated.length,
|
|
450
|
-
|
|
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, {
|
|
463
|
-
|
|
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(
|
|
2
|
-
const glob = require(
|
|
3
|
-
const fs = require(
|
|
4
|
-
const { Tags, execSpawn, Stats } = require(
|
|
5
|
-
const { shouldThumbUseTranscoded } = require(
|
|
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 =
|
|
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) =>
|
|
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}" -
|
|
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 = [
|
|
85
|
+
const script = ['dcm2jpg', `"${input}" "${output}"`];
|
|
85
86
|
if (options?.format) {
|
|
86
|
-
script.push(
|
|
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/,
|
|
100
|
-
return this.dcm2jpg(id.filename, rendered, { format:
|
|
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(
|
|
119
|
-
fs.mkdirSync(`${itemId.sopInstanceRootPath}/rendered`, {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
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.
|
|
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/,
|
|
140
|
+
return this.dcm2jpg(id.filename, id.imageFrameRootPath.replace(/frames/, 'thumbnail'), {});
|
|
139
141
|
}
|
|
140
142
|
|
|
141
|
-
await callback.internalGenerateImage(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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.
|
|
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.
|
|
193
|
+
console.verbose('The file could not be copied', e);
|
|
186
194
|
}
|
|
187
195
|
}
|
|
188
196
|
}
|