@radicalimaging/static-wado-creator 1.7.3 → 1.7.6

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.
Files changed (48) hide show
  1. package/dist/DeleteStudy.js +16 -0
  2. package/dist/RejectInstance.js +25 -0
  3. package/dist/StaticWado.js +419 -0
  4. package/dist/createMain.js +18 -0
  5. package/dist/createPart10.js +113 -0
  6. package/dist/createThumbnail.js +163 -0
  7. package/dist/deleteMain.js +12 -0
  8. package/dist/groupMain.js +17 -0
  9. package/dist/index.mjs +18 -0
  10. package/dist/indexMain.js +10 -0
  11. package/dist/instanceMain.js +16 -0
  12. package/dist/metadataMain.js +15 -0
  13. package/dist/mkdicomwebConfig.js +342 -0
  14. package/dist/operation/InstanceDeduplicate.js +103 -0
  15. package/dist/operation/ScanStudy.js +47 -0
  16. package/dist/operation/StudyData.js +520 -0
  17. package/dist/operation/ThumbnailService.js +198 -0
  18. package/dist/operation/adapter/decodeImage.js +12 -0
  19. package/dist/operation/adapter/getImageInfo.js +38 -0
  20. package/dist/operation/adapter/transcodeImage.js +519 -0
  21. package/dist/operation/extractImageFrames.js +55 -0
  22. package/dist/operation/getDataSet.js +65 -0
  23. package/dist/operation/getEncapsulatedImageFrame.js +35 -0
  24. package/dist/operation/getNumberOfFrames.js +13 -0
  25. package/dist/operation/getUncompressedImageFrame.js +7 -0
  26. package/dist/operation/getVR.js +24 -0
  27. package/dist/operation/getValue.js +216 -0
  28. package/dist/operation/validateMetadata.js +40 -0
  29. package/dist/program/index.js +14 -0
  30. package/dist/rejectMain.js +12 -0
  31. package/dist/serverMain.js +21 -0
  32. package/dist/util/IdCreator.js +42 -0
  33. package/dist/util/adaptProgramOpts.js +39 -0
  34. package/dist/util/compressionOptionParser.js +13 -0
  35. package/dist/util/resultsReport.ts +87 -0
  36. package/dist/writer/CompleteStudyWriter.js +129 -0
  37. package/dist/writer/DeduplicateWriter.js +33 -0
  38. package/dist/writer/ExpandUriPath.js +16 -0
  39. package/dist/writer/HashDataWriter.js +74 -0
  40. package/dist/writer/ImageFrameWriter.js +59 -0
  41. package/dist/writer/MultipartAttribute.js +8 -0
  42. package/dist/writer/MultipartHeader.js +14 -0
  43. package/dist/writer/RawDicomWriter.js +44 -0
  44. package/dist/writer/ThumbnailWriter.js +14 -0
  45. package/dist/writer/VideoWriter.js +53 -0
  46. package/dist/writer/WriteMultipart.js +22 -0
  47. package/dist/writer/WriteStream.js +64 -0
  48. package/package.json +11 -6
@@ -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
+ };