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