@radicalimaging/static-wado-creator 1.1.0 → 1.2.0
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.js +1 -1
- package/lib/RejectInstance.js +18 -11
- package/lib/StaticWado.js +48 -19
- package/lib/createMain.js +6 -2
- package/lib/groupMain.js +3 -0
- package/lib/instanceMain.js +2 -0
- package/lib/metadataMain.js +3 -2
- package/lib/mkdicomwebConfig.js +36 -9
- package/lib/operation/InstanceDeduplicate.js +2 -2
- package/lib/operation/ScanStudy.js +4 -3
- package/lib/operation/StudyData.js +92 -43
- package/lib/operation/ThumbnailService.js +21 -42
- package/lib/operation/adapter/transcodeImage.js +36 -26
- package/lib/operation/extractImageFrames.js +3 -0
- package/lib/operation/getDataSet.js +6 -1
- package/lib/operation/getValue.js +22 -11
- package/lib/rejectMain.js +2 -2
- package/lib/util/adaptProgramOpts.js +10 -31
- package/lib/writer/CompleteStudyWriter.js +6 -1
- package/lib/writer/DeduplicateWriter.js +3 -0
- package/lib/writer/HashDataWriter.js +1 -2
- package/lib/writer/ImageFrameWriter.js +7 -10
- package/lib/writer/ThumbnailWriter.js +7 -14
- package/lib/writer/WriteStream.js +12 -1
- package/package.json +11 -15
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
2
|
const glob = require("glob");
|
|
3
|
-
const dicomCodec = require("@cornerstonejs/dicom-codec");
|
|
4
|
-
const staticCS = require("@radicalimaging/static-cs-lite");
|
|
5
3
|
const fs = require("fs");
|
|
6
|
-
const {
|
|
7
|
-
const { Tags } = require("@radicalimaging/static-wado-util");
|
|
8
|
-
const decodeImage = require("./adapter/decodeImage");
|
|
4
|
+
const { Tags, execSpawn, Stats } = require("@radicalimaging/static-wado-util");
|
|
9
5
|
const { shouldThumbUseTranscoded } = require("./adapter/transcodeImage");
|
|
10
|
-
const { isVideo } = require("../writer/VideoWriter");
|
|
11
6
|
|
|
12
7
|
/**
|
|
13
8
|
* Return the middle index of given list
|
|
@@ -17,21 +12,6 @@ const { isVideo } = require("../writer/VideoWriter");
|
|
|
17
12
|
function getThumbIndex(listThumbs) {
|
|
18
13
|
return Math.trunc(listThumbs / 2);
|
|
19
14
|
}
|
|
20
|
-
function internalGenerateThumbnail(originalImageFrame, dataset, metadata, transferSyntaxUid, doneCallback) {
|
|
21
|
-
decodeImage(originalImageFrame, dataset, transferSyntaxUid)
|
|
22
|
-
.then((decodeResult = {}) => {
|
|
23
|
-
if (isVideo(transferSyntaxUid)) {
|
|
24
|
-
console.log("Video data - no thumbnail generator yet");
|
|
25
|
-
} else {
|
|
26
|
-
const { imageFrame, imageInfo } = decodeResult;
|
|
27
|
-
const pixelData = dicomCodec.getPixelData(imageFrame, imageInfo, transferSyntaxUid);
|
|
28
|
-
staticCS.getRenderedBuffer(transferSyntaxUid, pixelData, metadata, doneCallback);
|
|
29
|
-
}
|
|
30
|
-
})
|
|
31
|
-
.catch((error) => {
|
|
32
|
-
console.log(`Error while generating thumbnail:: ${error}`);
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
15
|
|
|
36
16
|
/**
|
|
37
17
|
* ThumbObj wrapper containing both original and transcoded content.
|
|
@@ -97,21 +77,11 @@ class ThumbnailService {
|
|
|
97
77
|
}
|
|
98
78
|
|
|
99
79
|
ffmpeg(input, output) {
|
|
100
|
-
|
|
101
|
-
if (error) {
|
|
102
|
-
console.log(`error: ${error.message}`);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (stderr) {
|
|
106
|
-
console.log(`stderr: ${stderr}`);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
console.log(`stdout: ${stdout}`);
|
|
110
|
-
});
|
|
80
|
+
execSpawn(`ffmpeg -i "${input}" -vf "thumbnail,scale=640:360" -frames:v 1 -f singlejpeg "${output}"`);
|
|
111
81
|
}
|
|
112
82
|
|
|
113
83
|
/**
|
|
114
|
-
* Generates thumbnails for the levels: instances, series, study
|
|
84
|
+
* Generates thumbnails for the levels: instances, series, study. This is asynchronous
|
|
115
85
|
*
|
|
116
86
|
* @param {*} dataSet
|
|
117
87
|
* @param {*} metadata
|
|
@@ -127,7 +97,8 @@ class ThumbnailService {
|
|
|
127
97
|
const { BulkDataURI } = pixelData;
|
|
128
98
|
if (BulkDataURI?.indexOf("mp4")) {
|
|
129
99
|
const mp4Path = path.join(itemId.sopInstanceRootPath, "pixeldata.mp4");
|
|
130
|
-
|
|
100
|
+
// Generate as rendered, as more back ends support that.
|
|
101
|
+
const thumbPath = path.join(itemId.sopInstanceRootPath, "rendered");
|
|
131
102
|
console.log("MP4 - converting video format", mp4Path);
|
|
132
103
|
this.ffmpeg(mp4Path, thumbPath);
|
|
133
104
|
} else {
|
|
@@ -136,15 +107,23 @@ class ThumbnailService {
|
|
|
136
107
|
} else {
|
|
137
108
|
console.log("Series is of other type...", metadata[Tags.Modality]);
|
|
138
109
|
}
|
|
139
|
-
return;
|
|
110
|
+
return null;
|
|
140
111
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
callback.internalGenerateImage(imageFrame, dataSet, metadata, id.transferSyntaxUid, async (thumbBuffer) => {
|
|
114
|
+
try {
|
|
115
|
+
if (thumbBuffer) {
|
|
116
|
+
await callback.thumbWriter(id.sopInstanceRootPath, this.thumbFileName, thumbBuffer);
|
|
117
|
+
|
|
118
|
+
this.copySyncThumbnail(id.sopInstanceRootPath, id.seriesRootPath);
|
|
119
|
+
this.copySyncThumbnail(id.seriesRootPath, id.studyPath);
|
|
120
|
+
Stats.StudyStats.add("Thumbnail Write", `Write thumbnail ${this.thumbFileName}`, 100);
|
|
121
|
+
}
|
|
122
|
+
resolve(this.thumbFileName);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
reject(e);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
148
127
|
});
|
|
149
128
|
}
|
|
150
129
|
|
|
@@ -2,6 +2,8 @@ const dicomCodec = require("@cornerstonejs/dicom-codec");
|
|
|
2
2
|
const { Tags } = require("@radicalimaging/static-wado-util");
|
|
3
3
|
const getImageInfo = require("./getImageInfo");
|
|
4
4
|
|
|
5
|
+
dicomCodec.setConfig("verbose: false");
|
|
6
|
+
|
|
5
7
|
const transcodeOp = {
|
|
6
8
|
none: 0,
|
|
7
9
|
decode: 1,
|
|
@@ -37,6 +39,10 @@ const transcodeDestinationMap = {
|
|
|
37
39
|
transferSyntaxUid: "3.2.840.10008.1.2.4.96",
|
|
38
40
|
transcodeOp: transcodeOp.encode,
|
|
39
41
|
},
|
|
42
|
+
jpeg: {
|
|
43
|
+
transferSyntaxUid: "1.2.840.10008.1.2.4.50",
|
|
44
|
+
transcodeOp: transcodeOp.encode,
|
|
45
|
+
},
|
|
40
46
|
};
|
|
41
47
|
|
|
42
48
|
const transcodeSourceMap = {
|
|
@@ -52,6 +58,10 @@ const transcodeSourceMap = {
|
|
|
52
58
|
transcodeOp: transcodeOp.decode,
|
|
53
59
|
alias: "uncompressed",
|
|
54
60
|
},
|
|
61
|
+
"1.2.840.10008.1.2.4.50": {
|
|
62
|
+
transcodeOp: transcodeOp.decode,
|
|
63
|
+
alias: "jpeg",
|
|
64
|
+
},
|
|
55
65
|
"1.2.840.10008.1.2.4.57": {
|
|
56
66
|
transcodeOp: transcodeOp.decode,
|
|
57
67
|
alias: "jls",
|
|
@@ -98,10 +108,8 @@ function getDestinationTranscoder(id) {
|
|
|
98
108
|
* @param {*} transferSyntaxUid
|
|
99
109
|
* @returns
|
|
100
110
|
*/
|
|
101
|
-
function getTranscoder(transferSyntaxUid, { contentType,
|
|
102
|
-
|
|
103
|
-
console.log('getTranscoder for', transferSyntaxUid, 'source to', contentType);
|
|
104
|
-
}
|
|
111
|
+
function getTranscoder(transferSyntaxUid, { contentType: greyContentType, colorContentType }, samplesPerPixel) {
|
|
112
|
+
const contentType = samplesPerPixel===3 ? colorContentType : greyContentType;
|
|
105
113
|
const sourceTranscoder = transcodeSourceMap[transferSyntaxUid];
|
|
106
114
|
const destinationTranscoder = getDestinationTranscoder(contentType);
|
|
107
115
|
if (!sourceTranscoder || !destinationTranscoder) {
|
|
@@ -120,22 +128,23 @@ function getTranscoder(transferSyntaxUid, { contentType, verbose }) {
|
|
|
120
128
|
* @param {*} id object containing transferSyntaxUid
|
|
121
129
|
* @param {*} options runner options
|
|
122
130
|
*/
|
|
123
|
-
function shouldTranscodeImageFrame(id, options) {
|
|
124
|
-
const { recompress } = options;
|
|
131
|
+
function shouldTranscodeImageFrame(id, options, samplesPerPixel) {
|
|
132
|
+
const { recompress: recompressGrey, recompressColor } = options;
|
|
133
|
+
const recompress = samplesPerPixel === 3 ? recompressColor : recompressGrey;
|
|
125
134
|
if (!recompress) {
|
|
126
|
-
|
|
135
|
+
console.verbose("Not transcoding because recompress not set");
|
|
127
136
|
return false;
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
function isValidTranscoder() {
|
|
131
140
|
const { transferSyntaxUid } = id;
|
|
132
|
-
const transcoder = getTranscoder(transferSyntaxUid, options);
|
|
141
|
+
const transcoder = getTranscoder(transferSyntaxUid, options, samplesPerPixel);
|
|
133
142
|
return transcoder && transcoder.transferSyntaxUid && (recompress.includes("true") || recompress.includes(transcoder.alias));
|
|
134
143
|
}
|
|
135
144
|
|
|
136
|
-
const ret
|
|
137
|
-
if(
|
|
138
|
-
console.
|
|
145
|
+
const ret = isValidTranscoder();
|
|
146
|
+
if (!ret) {
|
|
147
|
+
console.verbose("Not transcoding");
|
|
139
148
|
}
|
|
140
149
|
return ret;
|
|
141
150
|
}
|
|
@@ -154,7 +163,8 @@ function shouldThumbUseTranscoded(id, options) {
|
|
|
154
163
|
|
|
155
164
|
function isValidTranscoder() {
|
|
156
165
|
const { transferSyntaxUid } = id;
|
|
157
|
-
|
|
166
|
+
// Ignore the samples per pixel for thumbnails
|
|
167
|
+
const transcoder = getTranscoder(transferSyntaxUid, options, "thumbnail");
|
|
158
168
|
const result = transcoder && transcoder.transferSyntaxUid && options.recompress.includes(transcoder.alias) && options.recompressThumb.includes(transcoder.alias);
|
|
159
169
|
|
|
160
170
|
return result;
|
|
@@ -183,8 +193,9 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
|
|
|
183
193
|
let targetId = targetIdSrc;
|
|
184
194
|
let result = {};
|
|
185
195
|
|
|
186
|
-
|
|
187
|
-
|
|
196
|
+
const samplesPerPixel = dataSet.uint16(Tags.RawSamplesPerPixel);
|
|
197
|
+
if (!shouldTranscodeImageFrame(id, options, samplesPerPixel)) {
|
|
198
|
+
console.verbose("Shouldn't transcode");
|
|
188
199
|
return {
|
|
189
200
|
id,
|
|
190
201
|
imageFrame,
|
|
@@ -192,11 +203,11 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
|
|
|
192
203
|
};
|
|
193
204
|
}
|
|
194
205
|
|
|
195
|
-
const transcoder = getTranscoder(id.transferSyntaxUid, options);
|
|
206
|
+
const transcoder = getTranscoder(id.transferSyntaxUid, options, samplesPerPixel);
|
|
196
207
|
|
|
197
208
|
// last chance to prevent transcoding
|
|
198
209
|
if (targetId.transferSyntaxUid !== transcoder.transferSyntaxUid) {
|
|
199
|
-
|
|
210
|
+
console.verbose("Image is already in", targetId.transferSyntaxUid, "not transcoding");
|
|
200
211
|
return {
|
|
201
212
|
id,
|
|
202
213
|
imageFrame,
|
|
@@ -204,8 +215,8 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
|
|
|
204
215
|
};
|
|
205
216
|
}
|
|
206
217
|
|
|
207
|
-
|
|
208
|
-
|
|
218
|
+
console.verbose("Transcoding to", transcoder.transferSyntaxUid);
|
|
219
|
+
|
|
209
220
|
const imageInfo = getImageInfo(dataSet);
|
|
210
221
|
let done = false;
|
|
211
222
|
let processResultMsg = "";
|
|
@@ -269,13 +280,13 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
|
|
|
269
280
|
* @param {*} options runner options.
|
|
270
281
|
* @returns Transcoded id object
|
|
271
282
|
*/
|
|
272
|
-
function transcodeId(id, options) {
|
|
273
|
-
if (!shouldTranscodeImageFrame(id, options)) {
|
|
283
|
+
function transcodeId(id, options, samplesPerPixel) {
|
|
284
|
+
if (!shouldTranscodeImageFrame(id, options, samplesPerPixel)) {
|
|
274
285
|
return id;
|
|
275
286
|
}
|
|
276
287
|
|
|
277
288
|
const targetId = { ...id };
|
|
278
|
-
const { transferSyntaxUid } = getTranscoder(id.transferSyntaxUid, options);
|
|
289
|
+
const { transferSyntaxUid } = getTranscoder(id.transferSyntaxUid, options, samplesPerPixel);
|
|
279
290
|
|
|
280
291
|
targetId.transferSyntaxUid = transferSyntaxUid;
|
|
281
292
|
|
|
@@ -294,19 +305,18 @@ function transcodeId(id, options) {
|
|
|
294
305
|
* @returns Transcoded metadata.
|
|
295
306
|
*/
|
|
296
307
|
function transcodeMetadata(metadata, id, options) {
|
|
297
|
-
|
|
308
|
+
const samplesPerPixel = Tags.getValue(metadata, Tags.SamplesPerPixel);
|
|
309
|
+
if (!shouldTranscodeImageFrame(id, options, samplesPerPixel)) {
|
|
298
310
|
return metadata;
|
|
299
311
|
}
|
|
300
312
|
|
|
301
|
-
const transcodedId = transcodeId(id, options);
|
|
313
|
+
const transcodedId = transcodeId(id, options, samplesPerPixel);
|
|
302
314
|
|
|
303
315
|
const result = { ...metadata };
|
|
304
316
|
|
|
305
317
|
if (result[Tags.AvailableTransferSyntaxUID]) {
|
|
306
318
|
Tags.setValue(result, Tags.AvailableTransferSyntaxUID, transcodedId.transferSyntaxUid);
|
|
307
|
-
|
|
308
|
-
console.log("Apply available tsuid", transcodeId.transferSyntaxUid);
|
|
309
|
-
}
|
|
319
|
+
console.verbose("Apply available tsuid", transcodeId.transferSyntaxUid);
|
|
310
320
|
}
|
|
311
321
|
|
|
312
322
|
return result;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { Stats } = require("@radicalimaging/static-wado-util");
|
|
2
|
+
|
|
1
3
|
const getNumberOfFrames = require("./getNumberOfFrames");
|
|
2
4
|
const getUncompressedImageFrame = require("./getUncompressedImageFrame");
|
|
3
5
|
const getEncapsulatedImageFrame = require("./getEncapsulatedImageFrame");
|
|
@@ -30,6 +32,7 @@ const extractImageFrames = async (dataSet, attr, vr, callback) => {
|
|
|
30
32
|
if (attr.encapsulatedPixelData) {
|
|
31
33
|
const compressedFrame = getEncapsulatedImageFrame(dataSet, attr, frameIndex, framesAreFragmented);
|
|
32
34
|
BulkDataURI = await callback.imageFrame(compressedFrame, { dataSet });
|
|
35
|
+
Stats.OverallStats.add("Image Write", `Write image frame ${frameIndex + 1}`, 5000);
|
|
33
36
|
} else {
|
|
34
37
|
const uncompressedFrame = getUncompressedImageFrame(dataSet, attr, frameIndex, uncompressedFrameSize);
|
|
35
38
|
BulkDataURI = await callback.imageFrame(uncompressedFrame, { dataSet });
|
|
@@ -13,8 +13,9 @@ const getValue = require("./getValue");
|
|
|
13
13
|
* @param {*} parentAttr Parent reference for sequence element tags.
|
|
14
14
|
* @returns
|
|
15
15
|
*/
|
|
16
|
-
async function getDataSet(dataSet, callback,
|
|
16
|
+
async function getDataSet(dataSet, callback, optionsOrig, parentAttr = undefined) {
|
|
17
17
|
const metadata = {};
|
|
18
|
+
let options = optionsOrig;
|
|
18
19
|
|
|
19
20
|
// iterate over dataSet attributes in order
|
|
20
21
|
for (const tag in dataSet.elements) {
|
|
@@ -22,6 +23,10 @@ async function getDataSet(dataSet, callback, options, parentAttr = undefined) {
|
|
|
22
23
|
if (tag != Tags.RawTransferSyntaxUID && tag >= Tags.RawMinTag && tag < Tags.RawFirstBodyTag) {
|
|
23
24
|
continue;
|
|
24
25
|
}
|
|
26
|
+
if (tag === Tags.RawSpecificCharacterSet) {
|
|
27
|
+
const SpecificCharacterSet = dataSet.string(tag);
|
|
28
|
+
options = { ...options, SpecificCharacterSet };
|
|
29
|
+
}
|
|
25
30
|
const attr = dataSet.elements[tag];
|
|
26
31
|
/* eslint-disable-next-line no-use-before-define */
|
|
27
32
|
await attributeToJS(metadata, tag, dataSet, attr, callback, options, parentAttr);
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
const extractImageFrames = require("./extractImageFrames");
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function decodeUtf8(s, options) {
|
|
4
|
+
if (options?.SpecificCharacterSet === "ISO_IR 192") {
|
|
5
|
+
return decodeURIComponent(escape(s));
|
|
6
|
+
}
|
|
7
|
+
return s;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Gets a value as a UTF-8 string - todo, make UTF-8 decode optional. */
|
|
11
|
+
const getValueInlineString = (dataSet, attr, options) => [decodeUtf8(dataSet.string(attr.tag), options)];
|
|
4
12
|
|
|
5
|
-
const getStrings = (dataSet, attr) => {
|
|
13
|
+
const getStrings = (dataSet, attr, options) => {
|
|
6
14
|
const ret = dataSet.string(attr.tag);
|
|
7
|
-
|
|
15
|
+
if (ret) {
|
|
16
|
+
return ret.split(/\\/).map(decodeUtf8, options);
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
8
19
|
};
|
|
9
20
|
|
|
10
21
|
const getValuePatientName = (dataSet, attr) => {
|
|
@@ -78,7 +89,7 @@ const getValueInlineAttributeTag = (dataSet, attr) => {
|
|
|
78
89
|
return groupHexStr + elementHexStr;
|
|
79
90
|
};
|
|
80
91
|
|
|
81
|
-
const getValueInline = (dataSet, attr, vr) => {
|
|
92
|
+
const getValueInline = (dataSet, attr, vr, options) => {
|
|
82
93
|
if (attr.length == 0) {
|
|
83
94
|
return [];
|
|
84
95
|
}
|
|
@@ -86,7 +97,7 @@ const getValueInline = (dataSet, attr, vr) => {
|
|
|
86
97
|
switch (vr) {
|
|
87
98
|
case "AE":
|
|
88
99
|
case "AS":
|
|
89
|
-
return getValueInlineString(dataSet, attr);
|
|
100
|
+
return getValueInlineString(dataSet, attr, options);
|
|
90
101
|
case "AT":
|
|
91
102
|
return getValueInlineAttributeTag(dataSet, attr);
|
|
92
103
|
case "DS":
|
|
@@ -94,7 +105,7 @@ const getValueInline = (dataSet, attr, vr) => {
|
|
|
94
105
|
case "CS":
|
|
95
106
|
case "DA":
|
|
96
107
|
case "DT":
|
|
97
|
-
return getValueInlineString(dataSet, attr);
|
|
108
|
+
return getValueInlineString(dataSet, attr, options);
|
|
98
109
|
case "FL":
|
|
99
110
|
return getValueInlineFloat(dataSet, attr);
|
|
100
111
|
case "FD":
|
|
@@ -103,7 +114,7 @@ const getValueInline = (dataSet, attr, vr) => {
|
|
|
103
114
|
return getValueInlineIntString(dataSet, attr);
|
|
104
115
|
case "LO":
|
|
105
116
|
case "LT":
|
|
106
|
-
return getValueInlineString(dataSet, attr);
|
|
117
|
+
return getValueInlineString(dataSet, attr, options);
|
|
107
118
|
case "OB":
|
|
108
119
|
case "OF":
|
|
109
120
|
case "OW":
|
|
@@ -111,7 +122,7 @@ const getValueInline = (dataSet, attr, vr) => {
|
|
|
111
122
|
case "PN":
|
|
112
123
|
return getValuePatientName(dataSet, attr);
|
|
113
124
|
case "SH":
|
|
114
|
-
return getValueInlineString(dataSet, attr);
|
|
125
|
+
return getValueInlineString(dataSet, attr, options);
|
|
115
126
|
case "SL":
|
|
116
127
|
return getValueInlineSignedLong(dataSet, attr);
|
|
117
128
|
case "SS":
|
|
@@ -119,7 +130,7 @@ const getValueInline = (dataSet, attr, vr) => {
|
|
|
119
130
|
case "ST":
|
|
120
131
|
case "TM":
|
|
121
132
|
case "UI":
|
|
122
|
-
return getValueInlineString(dataSet, attr);
|
|
133
|
+
return getValueInlineString(dataSet, attr, options);
|
|
123
134
|
case "UL":
|
|
124
135
|
return getValueInlineUnsignedLong(dataSet, attr);
|
|
125
136
|
case "UN":
|
|
@@ -127,7 +138,7 @@ const getValueInline = (dataSet, attr, vr) => {
|
|
|
127
138
|
case "US":
|
|
128
139
|
return getValueInlineUnsignedShort(dataSet, attr);
|
|
129
140
|
case "UT":
|
|
130
|
-
return getValueInlineString(dataSet, attr);
|
|
141
|
+
return getValueInlineString(dataSet, attr, options);
|
|
131
142
|
default:
|
|
132
143
|
return getValueInlineBinary(dataSet, attr);
|
|
133
144
|
}
|
|
@@ -183,7 +194,7 @@ const getValue = async (dataSet, attr, vr, getDataSet, callback, options, parent
|
|
|
183
194
|
}
|
|
184
195
|
// non sequence item
|
|
185
196
|
if (isValueInline(attr, options)) {
|
|
186
|
-
return getValueInline(dataSet, attr, vr);
|
|
197
|
+
return getValueInline(dataSet, attr, vr, options);
|
|
187
198
|
}
|
|
188
199
|
const binaryValue = dataSet.byteArray.slice(attr.dataOffset, attr.dataOffset + attr.length);
|
|
189
200
|
const mimeType = attr.tag == "x00420011" && dataSet.string("x00420012");
|
package/lib/rejectMain.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const StaticWado = require("./StaticWado");
|
|
2
2
|
const adaptProgramOpts = require("./util/adaptProgramOpts");
|
|
3
3
|
|
|
4
|
-
module.exports = function rejectMain(
|
|
4
|
+
module.exports = function rejectMain(args, options) {
|
|
5
5
|
const finalOptions = adaptProgramOpts(options, {
|
|
6
6
|
...this,
|
|
7
7
|
isGroup: true,
|
|
8
8
|
isStudyData: true,
|
|
9
9
|
});
|
|
10
10
|
const importer = new StaticWado(finalOptions);
|
|
11
|
-
return importer.reject(
|
|
11
|
+
return importer.reject(args);
|
|
12
12
|
};
|
|
@@ -10,51 +10,30 @@ const assignDefined = (dest, src) => {
|
|
|
10
10
|
|
|
11
11
|
module.exports = function adaptProgramOpts(programOpts, defaults) {
|
|
12
12
|
const {
|
|
13
|
-
maximumInlinePublicLength,
|
|
14
|
-
maximumInlinePrivateLength,
|
|
15
13
|
group: isGroup,
|
|
16
14
|
instances: isInstanceMetadata,
|
|
17
15
|
deduplicate: isDeduplicate,
|
|
18
16
|
study: isStudyData,
|
|
19
17
|
clean: isClean,
|
|
20
|
-
recompress,
|
|
21
|
-
recompressThumb,
|
|
22
|
-
contentType,
|
|
23
|
-
colourContentType,
|
|
24
18
|
dir: rootDir,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
storeMultipartBulkData,
|
|
29
|
-
prependBulkDataUri,
|
|
30
|
-
expandBulkDataUri,
|
|
31
|
-
verbose,
|
|
32
|
-
scpPort,
|
|
19
|
+
delete: deleteInstances,
|
|
20
|
+
singlePartImage,
|
|
21
|
+
encapsulatedImage,
|
|
33
22
|
} = programOpts;
|
|
34
23
|
|
|
35
24
|
return assignDefined(
|
|
36
25
|
{ ...defaults },
|
|
37
26
|
{
|
|
38
|
-
|
|
39
|
-
maximumInlinePrivateLength,
|
|
40
|
-
isGroup,
|
|
41
|
-
isInstanceMetadata,
|
|
42
|
-
isDeduplicate,
|
|
27
|
+
...programOpts,
|
|
43
28
|
isStudyData,
|
|
44
29
|
isClean,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
colourContentType,
|
|
30
|
+
isInstanceMetadata,
|
|
31
|
+
isDeduplicate,
|
|
32
|
+
isGroup,
|
|
49
33
|
rootDir,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
storeMultipartBulkData,
|
|
54
|
-
scpPort,
|
|
55
|
-
prependBulkDataUri,
|
|
56
|
-
expandBulkDataUri,
|
|
57
|
-
verbose,
|
|
34
|
+
encapsulatedImage: encapsulatedImage ?? singlePartImage !== true,
|
|
35
|
+
singlePartImage: singlePartImage ?? encapsulatedImage === false,
|
|
36
|
+
delete: deleteInstances,
|
|
58
37
|
}
|
|
59
38
|
);
|
|
60
39
|
};
|
|
@@ -26,9 +26,12 @@ const CompleteStudyWriter = (options) => {
|
|
|
26
26
|
} else {
|
|
27
27
|
console.log("Not writing new deduplicated data because it is clean:", studyData.studyInstanceUid);
|
|
28
28
|
}
|
|
29
|
+
await studyData.deleteInstancesReferenced();
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
if (!options.isStudyData) {
|
|
33
|
+
console.verbose("Not configured to write study metadata", studyData.studyInstanceUid);
|
|
34
|
+
if (options.notifications) this.notificationService.notifyStudy(studyData.studyInstanceUid);
|
|
32
35
|
delete this.studyData;
|
|
33
36
|
Stats.StudyStats.summarize();
|
|
34
37
|
return;
|
|
@@ -36,11 +39,12 @@ const CompleteStudyWriter = (options) => {
|
|
|
36
39
|
|
|
37
40
|
const isDirtyMetadata = await studyData.dirtyMetadata();
|
|
38
41
|
if (!isDirtyMetadata) {
|
|
39
|
-
console.log("Study metadata", studyData.studyInstanceUid, "
|
|
42
|
+
console.log("Study metadata", studyData.studyInstanceUid, "is clean.");
|
|
40
43
|
delete this.studyData;
|
|
41
44
|
Stats.StudyStats.summarize(`Study metadata ${studyData.studyInstanceUid} has clean metadata, not writing`);
|
|
42
45
|
return;
|
|
43
46
|
}
|
|
47
|
+
console.log("Writing study metadata", studyData.studyInstanceUid);
|
|
44
48
|
|
|
45
49
|
const studyQuery = await studyData.writeMetadata();
|
|
46
50
|
|
|
@@ -57,6 +61,7 @@ const CompleteStudyWriter = (options) => {
|
|
|
57
61
|
allStudies[studyIndex] = studyQuery;
|
|
58
62
|
}
|
|
59
63
|
await JSONWriter(options.directoryName, "studies", allStudies);
|
|
64
|
+
if (options.notifications) this.notificationService.notifyStudy(studyData.studyInstanceUid);
|
|
60
65
|
delete this.studyData;
|
|
61
66
|
Stats.StudyStats.summarize(`Wrote study metadata/query files for ${studyData.studyInstanceUid}`);
|
|
62
67
|
}
|
|
@@ -24,7 +24,10 @@ const DeduplicateWriter = (options) =>
|
|
|
24
24
|
const studyData = await this.completeStudy.getCurrentStudyData(this, id);
|
|
25
25
|
|
|
26
26
|
if (options.isDeduplicate) {
|
|
27
|
+
if (options.verbose) console.log("Writing single instance", id.studyInstanceUid);
|
|
27
28
|
await perInstanceWriter(id, data);
|
|
29
|
+
} else if (options.verbose) {
|
|
30
|
+
console.log("Not writing single instance deduplicated");
|
|
28
31
|
}
|
|
29
32
|
studyData.addDeduplicated(data, id.filename);
|
|
30
33
|
};
|
|
@@ -20,7 +20,6 @@ const HashDataWriter =
|
|
|
20
20
|
(options) =>
|
|
21
21
|
async (id, key, data, additionalOptions = {}) => {
|
|
22
22
|
const isRaw = ArrayBuffer.isView(data);
|
|
23
|
-
const { storeMultipartBulkData } = options;
|
|
24
23
|
const { mimeType } = additionalOptions;
|
|
25
24
|
// If the file has an extension, it should be directly accessible as that file type.
|
|
26
25
|
const gzip = !isRaw || (data.length > 1024 && !mimeType);
|
|
@@ -31,7 +30,7 @@ const HashDataWriter =
|
|
|
31
30
|
mkdir: true,
|
|
32
31
|
gzip,
|
|
33
32
|
});
|
|
34
|
-
if (isRaw
|
|
33
|
+
if (isRaw) {
|
|
35
34
|
await WriteMultipart(writeStream, [new MultipartHeader("Content-Type", "application/octet-stream")], rawData);
|
|
36
35
|
} else {
|
|
37
36
|
await writeStream.write(rawData);
|
|
@@ -5,8 +5,7 @@ const ExpandUriPath = require("./ExpandUriPath");
|
|
|
5
5
|
const { MultipartHeader, MultipartAttribute } = require("./MultipartHeader");
|
|
6
6
|
|
|
7
7
|
const ImageFrameWriter = (options) => {
|
|
8
|
-
const {
|
|
9
|
-
|
|
8
|
+
const { encapsulatedImage, singlePartImage } = options;
|
|
10
9
|
return async (id, index, imageFrame) => {
|
|
11
10
|
const { transferSyntaxUid } = id;
|
|
12
11
|
const type = uids[transferSyntaxUid] || uids.default;
|
|
@@ -18,7 +17,7 @@ const ImageFrameWriter = (options) => {
|
|
|
18
17
|
content = Buffer.from(imageFrame.buffer);
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
if (
|
|
20
|
+
if (encapsulatedImage || !extension) {
|
|
22
21
|
const writeStream = WriteStream(id.imageFrameRootPath, `${1 + index}`, {
|
|
23
22
|
gzip: type.gzip,
|
|
24
23
|
mkdir: true,
|
|
@@ -28,19 +27,17 @@ const ImageFrameWriter = (options) => {
|
|
|
28
27
|
[new MultipartHeader("Content-Type", type.contentType, [new MultipartAttribute("transfer-syntax", transferSyntaxUid)])],
|
|
29
28
|
content
|
|
30
29
|
);
|
|
31
|
-
writeStream.close();
|
|
32
|
-
|
|
33
|
-
console.log("Wrote encapsulated image frame", id.sopInstanceUid, index + 1, type.contentType);
|
|
34
|
-
}
|
|
30
|
+
await writeStream.close();
|
|
31
|
+
console.verbose("Wrote encapsulated image frame", id.sopInstanceUid, index + 1, type.contentType);
|
|
35
32
|
}
|
|
36
|
-
if (extension) {
|
|
33
|
+
if (extension && singlePartImage) {
|
|
37
34
|
const writeStreamSingle = WriteStream(id.imageFrameRootPath, `${1 + index}${extension}`, {
|
|
38
35
|
gzip: type.gzip,
|
|
39
36
|
mkdir: true,
|
|
40
37
|
});
|
|
41
38
|
await writeStreamSingle.write(content);
|
|
42
|
-
writeStreamSingle.close();
|
|
43
|
-
|
|
39
|
+
await writeStreamSingle.close();
|
|
40
|
+
console.verbose("Wrote single part image frame", id.sopInstanceUid, index + 1, extension);
|
|
44
41
|
}
|
|
45
42
|
const includeSeries = true;
|
|
46
43
|
return ExpandUriPath(id, `instances/${id.sopInstanceUid}/frames`, { includeSeries, ...options });
|
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
const WriteStream = require("./WriteStream");
|
|
2
2
|
|
|
3
|
-
const ThumbnailWriter = (
|
|
4
|
-
const
|
|
3
|
+
const ThumbnailWriter = () => async (filePath, fileName, thumbBuffer) => {
|
|
4
|
+
const writeStream = WriteStream(filePath, fileName, {
|
|
5
|
+
mkdir: true,
|
|
6
|
+
});
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
const writeStream = WriteStream(filePath, fileName, {
|
|
8
|
-
mkdir: true,
|
|
9
|
-
});
|
|
8
|
+
await writeStream.write(thumbBuffer);
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
await writeStream.close();
|
|
14
|
-
|
|
15
|
-
if (verbose) {
|
|
16
|
-
console.log("Wrote thumbnail frame", filePath, fileName);
|
|
17
|
-
}
|
|
18
|
-
};
|
|
10
|
+
console.verbose("Wrote thumbnail frame", filePath, fileName);
|
|
11
|
+
return writeStream.close();
|
|
19
12
|
};
|
|
20
13
|
|
|
21
14
|
module.exports = ThumbnailWriter;
|
|
@@ -2,6 +2,8 @@ const fs = require("fs");
|
|
|
2
2
|
const zlib = require("zlib");
|
|
3
3
|
const path = require("path");
|
|
4
4
|
|
|
5
|
+
let writeCount = 0;
|
|
6
|
+
|
|
5
7
|
/** Create an optionally gzipped stream,
|
|
6
8
|
* where the write operations are performed in order executed,
|
|
7
9
|
* and don't require synchronization, only the 'close' operation
|
|
@@ -14,6 +16,14 @@ const WriteStream = (dir, nameSrc, options = {}) => {
|
|
|
14
16
|
|
|
15
17
|
const tempName = path.join(dir, `tempFile-${Math.round(Math.random() * 1000000000)}`);
|
|
16
18
|
const finalName = path.join(dir, name);
|
|
19
|
+
writeCount++;
|
|
20
|
+
if (writeCount > 10) {
|
|
21
|
+
console.log("Write count", tempName, finalName, writeCount);
|
|
22
|
+
if (writeCount > 100) {
|
|
23
|
+
console.error("Too many open writes", new Error());
|
|
24
|
+
throw new Error(`Write count too high ${writeCount} destination ${finalName}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
17
27
|
const rawStream = fs.createWriteStream(tempName);
|
|
18
28
|
const closePromise = new Promise((resolve) => {
|
|
19
29
|
rawStream.on("close", () => {
|
|
@@ -33,7 +43,8 @@ const WriteStream = (dir, nameSrc, options = {}) => {
|
|
|
33
43
|
async function close() {
|
|
34
44
|
await this.writeStream.end();
|
|
35
45
|
await this.closePromise;
|
|
36
|
-
await fs.rename(tempName, finalName, () => true);
|
|
46
|
+
await fs.rename(tempName, finalName, () => true);
|
|
47
|
+
writeCount--;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
return {
|