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