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