@radicalimaging/static-wado-creator 1.5.0 → 1.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +234 -0
- package/README.md +1 -1
- package/bin/mkdicomweb.mjs +8 -0
- package/dist/canvas-xaqc8v6y.node +0 -0
- package/dist/index.js +511433 -0
- package/lib/DeleteStudy.js +11 -9
- package/lib/RejectInstance.js +6 -5
- package/lib/StaticWado.js +222 -73
- package/lib/createMain.js +3 -3
- package/lib/createPart10.js +19 -19
- package/lib/createThumbnail.js +163 -0
- package/lib/deleteMain.js +2 -2
- package/lib/groupMain.js +2 -2
- package/lib/index.js +7 -5
- package/lib/indexMain.js +2 -2
- package/lib/instanceMain.js +3 -2
- package/lib/metadataMain.js +2 -2
- package/lib/mkdicomwebConfig.js +210 -115
- package/lib/model/TagLists.js +13 -4
- package/lib/model/uids.js +32 -28
- package/lib/operation/InstanceDeduplicate.js +18 -13
- package/lib/operation/ScanStudy.js +33 -7
- package/lib/operation/StudyData.js +122 -71
- package/lib/operation/ThumbnailService.js +44 -36
- package/lib/operation/adapter/decodeImage.js +7 -4
- package/lib/operation/adapter/getImageInfo.js +23 -6
- package/lib/operation/adapter/transcodeImage.js +157 -90
- package/lib/operation/extractImageFrames.js +25 -14
- package/lib/operation/getDataSet.js +4 -4
- package/lib/operation/getEncapsulatedImageFrame.js +16 -4
- package/lib/operation/getNumberOfFrames.js +2 -2
- package/lib/operation/getVR.js +12 -4
- package/lib/operation/getValue.js +42 -65
- package/lib/operation/validateMetadata.js +40 -0
- package/lib/program/index.js +2 -3
- package/lib/rejectMain.js +2 -2
- package/lib/serverMain.js +21 -0
- package/lib/util/IdCreator.js +25 -8
- package/lib/util/adaptProgramOpts.js +1 -1
- package/lib/util/compressionOptionParser.js +4 -4
- package/lib/util/resultsReport.ts +87 -0
- package/lib/writer/CompleteStudyWriter.js +42 -19
- package/lib/writer/DeduplicateWriter.js +7 -7
- package/lib/writer/ExpandUriPath.js +4 -3
- package/lib/writer/HashDataWriter.js +21 -17
- package/lib/writer/ImageFrameWriter.js +22 -10
- package/lib/writer/MultipartHeader.js +1 -1
- package/lib/writer/RawDicomWriter.js +18 -10
- package/lib/writer/ThumbnailWriter.js +2 -2
- package/lib/writer/VideoWriter.js +23 -34
- package/lib/writer/WriteMultipart.js +3 -3
- package/lib/writer/WriteStream.js +14 -12
- package/package.json +22 -18
- package/bin/mkdicomweb.js +0 -9
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const extractImageFrames = require(
|
|
1
|
+
const extractImageFrames = require('./extractImageFrames');
|
|
2
2
|
|
|
3
3
|
function decodeUtf8(s, options) {
|
|
4
|
-
if (options?.SpecificCharacterSet ===
|
|
4
|
+
if (options?.SpecificCharacterSet === 'ISO_IR 192') {
|
|
5
5
|
return decodeURIComponent(escape(s));
|
|
6
6
|
}
|
|
7
7
|
return s;
|
|
@@ -22,19 +22,14 @@ const getStrings = (dataSet, attr, options) => {
|
|
|
22
22
|
|
|
23
23
|
const getValuePatientName = (dataSet, attr) => {
|
|
24
24
|
const strings = getStrings(dataSet, attr);
|
|
25
|
-
return (
|
|
26
|
-
(strings && strings.map((item) => ({ Alphabetic: item }))) || undefined
|
|
27
|
-
);
|
|
25
|
+
return (strings && strings.map(item => ({ Alphabetic: item }))) || undefined;
|
|
28
26
|
};
|
|
29
27
|
|
|
30
28
|
/** Gets either InlineBinary or BulkDataURI, if already defined */
|
|
31
29
|
const getValueInlineBinary = (dataSet, attr) => {
|
|
32
30
|
if (attr.BulkDataURI) return { BulkDataURI: attr.BulkDataURI };
|
|
33
|
-
const binaryValue = dataSet.byteArray.slice(
|
|
34
|
-
|
|
35
|
-
attr.dataOffset + attr.length
|
|
36
|
-
);
|
|
37
|
-
return { InlineBinary: binaryValue.toString("base64") };
|
|
31
|
+
const binaryValue = dataSet.byteArray.slice(attr.dataOffset, attr.dataOffset + attr.length);
|
|
32
|
+
return { InlineBinary: binaryValue.toString('base64') };
|
|
38
33
|
};
|
|
39
34
|
|
|
40
35
|
const getValueInlineSignedShort = (dataSet, attr) => {
|
|
@@ -79,10 +74,10 @@ const getValueInlineFloat = (dataSet, attr) => {
|
|
|
79
74
|
};
|
|
80
75
|
|
|
81
76
|
const getValueInlineIntString = (dataSet, attr) =>
|
|
82
|
-
getStrings(dataSet, attr).map(
|
|
77
|
+
getStrings(dataSet, attr).map(val => parseInt(val));
|
|
83
78
|
|
|
84
79
|
const getValueInlineFloatString = (dataSet, attr) =>
|
|
85
|
-
getStrings(dataSet, attr).map(
|
|
80
|
+
getStrings(dataSet, attr).map(val => parseFloat(val));
|
|
86
81
|
|
|
87
82
|
const getValueInlineFloatDouble = (dataSet, attr) => {
|
|
88
83
|
if (attr.length > 8) {
|
|
@@ -105,63 +100,63 @@ const getValueInline = (dataSet, attr, vr, options) => {
|
|
|
105
100
|
}
|
|
106
101
|
|
|
107
102
|
switch (vr) {
|
|
108
|
-
case
|
|
109
|
-
case
|
|
103
|
+
case 'AE':
|
|
104
|
+
case 'AS':
|
|
110
105
|
return getValueInlineString(dataSet, attr, options);
|
|
111
|
-
case
|
|
106
|
+
case 'AT':
|
|
112
107
|
return getValueInlineAttributeTag(dataSet, attr);
|
|
113
|
-
case
|
|
108
|
+
case 'DS':
|
|
114
109
|
return getValueInlineFloatString(dataSet, attr);
|
|
115
|
-
case
|
|
116
|
-
case
|
|
117
|
-
case
|
|
110
|
+
case 'CS':
|
|
111
|
+
case 'DA':
|
|
112
|
+
case 'DT':
|
|
118
113
|
return getValueInlineString(dataSet, attr, options);
|
|
119
|
-
case
|
|
114
|
+
case 'FL':
|
|
120
115
|
return getValueInlineFloat(dataSet, attr);
|
|
121
|
-
case
|
|
116
|
+
case 'FD':
|
|
122
117
|
return getValueInlineFloatDouble(dataSet, attr);
|
|
123
|
-
case
|
|
118
|
+
case 'IS':
|
|
124
119
|
return getValueInlineIntString(dataSet, attr);
|
|
125
|
-
case
|
|
126
|
-
case
|
|
120
|
+
case 'LO':
|
|
121
|
+
case 'LT':
|
|
127
122
|
return getValueInlineString(dataSet, attr, options);
|
|
128
|
-
case
|
|
129
|
-
case
|
|
130
|
-
case
|
|
123
|
+
case 'OB':
|
|
124
|
+
case 'OF':
|
|
125
|
+
case 'OW':
|
|
131
126
|
return getValueInlineBinary(dataSet, attr);
|
|
132
|
-
case
|
|
127
|
+
case 'PN':
|
|
133
128
|
return getValuePatientName(dataSet, attr);
|
|
134
|
-
case
|
|
129
|
+
case 'SH':
|
|
135
130
|
return getValueInlineString(dataSet, attr, options);
|
|
136
|
-
case
|
|
131
|
+
case 'SL':
|
|
137
132
|
return getValueInlineSignedLong(dataSet, attr);
|
|
138
|
-
case
|
|
133
|
+
case 'SS':
|
|
139
134
|
return getValueInlineSignedShort(dataSet, attr);
|
|
140
|
-
case
|
|
141
|
-
case
|
|
142
|
-
case
|
|
135
|
+
case 'ST':
|
|
136
|
+
case 'TM':
|
|
137
|
+
case 'UI':
|
|
143
138
|
return getValueInlineString(dataSet, attr, options);
|
|
144
|
-
case
|
|
139
|
+
case 'UL':
|
|
145
140
|
return getValueInlineUnsignedLong(dataSet, attr);
|
|
146
|
-
case
|
|
141
|
+
case 'UN':
|
|
147
142
|
return getValueInlineBinary(dataSet, attr);
|
|
148
|
-
case
|
|
143
|
+
case 'US':
|
|
149
144
|
return getValueInlineUnsignedShort(dataSet, attr);
|
|
150
|
-
case
|
|
145
|
+
case 'UT':
|
|
151
146
|
return getValueInlineString(dataSet, attr, options);
|
|
152
147
|
default:
|
|
153
148
|
return getValueInlineBinary(dataSet, attr);
|
|
154
149
|
}
|
|
155
150
|
};
|
|
156
151
|
|
|
157
|
-
const isPrivate =
|
|
152
|
+
const isPrivate = attr => {
|
|
158
153
|
const { tag } = attr;
|
|
159
154
|
const ch = tag.substr(4, 1);
|
|
160
155
|
const chHex = parseInt(ch, 16);
|
|
161
156
|
return chHex % 2 == 1;
|
|
162
157
|
};
|
|
163
158
|
|
|
164
|
-
const RawEncapsulatedDocument =
|
|
159
|
+
const RawEncapsulatedDocument = 'x00420011';
|
|
165
160
|
|
|
166
161
|
const isValueInline = (attr, options) => {
|
|
167
162
|
if (isPrivate(attr)) {
|
|
@@ -187,30 +182,15 @@ const isValueInline = (attr, options) => {
|
|
|
187
182
|
* @param {*} parentAttr attr's parent. If not present means attr is at root level.
|
|
188
183
|
* @returns
|
|
189
184
|
*/
|
|
190
|
-
const getValue = async (
|
|
191
|
-
dataSet,
|
|
192
|
-
attr,
|
|
193
|
-
vr,
|
|
194
|
-
getDataSet,
|
|
195
|
-
callback,
|
|
196
|
-
options,
|
|
197
|
-
parentAttr
|
|
198
|
-
) => {
|
|
185
|
+
const getValue = async (dataSet, attr, vr, getDataSet, callback, options, parentAttr) => {
|
|
199
186
|
// It will only process pixelData tag if on metadata root. Otherwise it will be skiped.
|
|
200
|
-
if (attr.tag ===
|
|
201
|
-
const BulkDataURI = await extractImageFrames(
|
|
202
|
-
dataSet,
|
|
203
|
-
attr,
|
|
204
|
-
vr,
|
|
205
|
-
callback,
|
|
206
|
-
options
|
|
207
|
-
);
|
|
187
|
+
if (attr.tag === 'x7fe00010' && !parentAttr) {
|
|
188
|
+
const BulkDataURI = await extractImageFrames(dataSet, attr, vr, callback, options);
|
|
208
189
|
return { BulkDataURI };
|
|
209
190
|
}
|
|
210
|
-
if (attr.tag ===
|
|
191
|
+
if (attr.tag === 'xfffee00d') return undefined;
|
|
211
192
|
// Group length
|
|
212
|
-
if (attr.tag.length == 9 && attr.tag.substring(5, 9) ===
|
|
213
|
-
return undefined;
|
|
193
|
+
if (attr.tag.length == 9 && attr.tag.substring(5, 9) === '0000') return undefined;
|
|
214
194
|
if (attr.items) {
|
|
215
195
|
// sequences
|
|
216
196
|
const result = [];
|
|
@@ -227,11 +207,8 @@ const getValue = async (
|
|
|
227
207
|
if (isValueInline(attr, options)) {
|
|
228
208
|
return getValueInline(dataSet, attr, vr, options);
|
|
229
209
|
}
|
|
230
|
-
const binaryValue = dataSet.byteArray.slice(
|
|
231
|
-
|
|
232
|
-
attr.dataOffset + attr.length
|
|
233
|
-
);
|
|
234
|
-
const mimeType = attr.tag == "x00420011" && dataSet.string("x00420012");
|
|
210
|
+
const binaryValue = dataSet.byteArray.slice(attr.dataOffset, attr.dataOffset + attr.length);
|
|
211
|
+
const mimeType = attr.tag == 'x00420011' && dataSet.string('x00420012');
|
|
235
212
|
|
|
236
213
|
const BulkDataURI = await callback.bulkdata(binaryValue, { mimeType });
|
|
237
214
|
return { BulkDataURI, vr };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { Tags } = require('@radicalimaging/static-wado-util');
|
|
2
|
+
|
|
3
|
+
function validateMetadata(options) {
|
|
4
|
+
const serviceUrl = '';
|
|
5
|
+
const { verify, multipart: stowResponse } = options;
|
|
6
|
+
|
|
7
|
+
return function (id, result) {
|
|
8
|
+
const getValue = Tags.getValue.bind(null, result.metadata);
|
|
9
|
+
if (verify) {
|
|
10
|
+
console.verbose('Validating metadata', id);
|
|
11
|
+
if (id.studyInstanceUid.length > 64) {
|
|
12
|
+
throw new Error(`StudyInstanceUID too long: ${id.studyInstanceUid}`);
|
|
13
|
+
}
|
|
14
|
+
if (id.seriesInstanceUid.length > 64) {
|
|
15
|
+
throw new Error(`SeriesInstanceUID too long: ${id.studyInstanceUid}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (stowResponse) {
|
|
19
|
+
const RetrieveURL = `${serviceUrl}${id.studyInstanceUid}/series/${id.seriesInstanceUid}/instances/${id.sopInstanceUid}`;
|
|
20
|
+
|
|
21
|
+
// TODO - add check here from plugin to add remote query checks, eg kheops or similar
|
|
22
|
+
const { filename } = id;
|
|
23
|
+
const successMessage = {
|
|
24
|
+
ReferencedSOPSequence: [
|
|
25
|
+
{
|
|
26
|
+
StudyInstanceUID: id.studyInstanceUid,
|
|
27
|
+
SeriesInstanceUID: id.seriesInstanceUid,
|
|
28
|
+
ReferencedSOPClassUID: getValue(Tags.SOPClassUID),
|
|
29
|
+
ReferencedSOPInstanceUID: id.sopInstanceUid,
|
|
30
|
+
RetrieveURL,
|
|
31
|
+
StorageURL: filename,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
this.success('instanceStored', successMessage, filename);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = validateMetadata;
|
package/lib/program/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const staticWadoUtil = require(
|
|
1
|
+
const staticWadoUtil = require('@radicalimaging/static-wado-util');
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Configure static-wado-creator commander program.
|
|
@@ -8,8 +8,7 @@ const staticWadoUtil = require("@radicalimaging/static-wado-util");
|
|
|
8
8
|
*/
|
|
9
9
|
async function configureProgram(defaults) {
|
|
10
10
|
const configurationFile = await staticWadoUtil.loadConfiguration(defaults, process.argv);
|
|
11
|
-
|
|
12
|
-
staticWadoUtil.configureCommands(defaults);
|
|
11
|
+
return staticWadoUtil.configureCommands(defaults);
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
exports.configureProgram = configureProgram;
|
package/lib/rejectMain.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const StaticWado = require(
|
|
2
|
-
const adaptProgramOpts = require(
|
|
1
|
+
const StaticWado = require('./StaticWado');
|
|
2
|
+
const adaptProgramOpts = require('./util/adaptProgramOpts');
|
|
3
3
|
|
|
4
4
|
module.exports = function rejectMain(args, options) {
|
|
5
5
|
const finalOptions = adaptProgramOpts(options, {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const readline = require('node:readline');
|
|
2
|
+
const { argv, argv0, exit, stdin: input, stdout: output } = require('node:process');
|
|
3
|
+
|
|
4
|
+
const StaticWado = require('./StaticWado');
|
|
5
|
+
const adaptProgramOpts = require('./util/adaptProgramOpts');
|
|
6
|
+
|
|
7
|
+
module.exports = async function createMain(options, program) {
|
|
8
|
+
const rl = readline.createInterface({ input, output });
|
|
9
|
+
for await (const line of rl) {
|
|
10
|
+
if (!line) continue;
|
|
11
|
+
if (line.startsWith('exit')) {
|
|
12
|
+
exit(0);
|
|
13
|
+
}
|
|
14
|
+
argv.splice(2, argv.length - 2, ...line.split(' '));
|
|
15
|
+
console.verbose('exec mkdicomweb', JSON.stringify(argv));
|
|
16
|
+
|
|
17
|
+
// Configure program commander
|
|
18
|
+
await StaticWado.configureProgram(StaticWado.mkdicomwebConfig);
|
|
19
|
+
output.write('mkdicomweb server -->\n');
|
|
20
|
+
}
|
|
21
|
+
};
|
package/lib/util/IdCreator.js
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
|
-
const path = require(
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { getStudyUIDPathAndSubPath } = require('@radicalimaging/static-wado-util');
|
|
2
3
|
|
|
3
|
-
function IdCreator({
|
|
4
|
+
function IdCreator({
|
|
5
|
+
directoryName,
|
|
6
|
+
deduplicatedRoot,
|
|
7
|
+
deduplicatedInstancesRoot,
|
|
8
|
+
hashStudyUidPath,
|
|
9
|
+
}) {
|
|
4
10
|
return (uids, filename) => {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
const { studyInstanceUid, seriesInstanceUid, sopInstanceUid } = uids;
|
|
12
|
+
|
|
13
|
+
const { path: hashPath = '', subpath: hashSubpath = '' } = hashStudyUidPath
|
|
14
|
+
? getStudyUIDPathAndSubPath(studyInstanceUid)
|
|
15
|
+
: {};
|
|
16
|
+
|
|
17
|
+
const studySubDir = hashStudyUidPath
|
|
18
|
+
? path.join(hashPath, hashSubpath, studyInstanceUid)
|
|
19
|
+
: studyInstanceUid;
|
|
20
|
+
|
|
21
|
+
const studyPath = path.join(directoryName, 'studies', studySubDir);
|
|
22
|
+
const seriesRootPath = path.join(studyPath, 'series', seriesInstanceUid);
|
|
23
|
+
const sopInstanceRootPath = path.join(seriesRootPath, 'instances', sopInstanceUid);
|
|
24
|
+
const imageFrameRootPath = path.join(sopInstanceRootPath, 'frames');
|
|
25
|
+
|
|
26
|
+
const deduplicatedPath = path.join(deduplicatedRoot, studySubDir);
|
|
27
|
+
const deduplicatedInstancesPath = path.join(deduplicatedInstancesRoot, studySubDir);
|
|
11
28
|
|
|
12
29
|
return {
|
|
13
30
|
...uids,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
const { program } = require(
|
|
2
|
-
const { getDestinationTranscoder } = require(
|
|
1
|
+
const { program } = require('@radicalimaging/static-wado-util');
|
|
2
|
+
const { getDestinationTranscoder } = require('../operation/adapter/transcodeImage');
|
|
3
3
|
|
|
4
4
|
module.exports = function compressionOptionParser(value) {
|
|
5
5
|
const destination = getDestinationTranscoder(value);
|
|
6
6
|
if (destination) {
|
|
7
|
-
console.log(
|
|
7
|
+
console.log('Found destination transferSyntax ', destination.transferSyntaxUid, 'for ', value);
|
|
8
8
|
} else {
|
|
9
|
-
console.log(
|
|
9
|
+
console.log('No transcoder destination for ', value);
|
|
10
10
|
throw new program.InvalidArgumentError(`No transcoder destination for ${value}`);
|
|
11
11
|
}
|
|
12
12
|
return destination.transferSyntaxUid;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
const results = [];
|
|
4
|
+
let sopResponse = null;
|
|
5
|
+
|
|
6
|
+
function ensureSOPResponse(data) {
|
|
7
|
+
if (!data.ReferencedSOPSequence && !data.FailedSOPSequence) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (!sopResponse) {
|
|
11
|
+
sopResponse = {
|
|
12
|
+
...data,
|
|
13
|
+
FailedSOPSequence: undefined,
|
|
14
|
+
ReferencedSOPSequence: undefined,
|
|
15
|
+
};
|
|
16
|
+
results.push(sopResponse);
|
|
17
|
+
}
|
|
18
|
+
if (data.ReferencedSOPSequence) {
|
|
19
|
+
sopResponse.ReferencedSOPSequence ||= [];
|
|
20
|
+
sopResponse.ReferencedSOPSequence.push(...data.ReferencedSOPSequence);
|
|
21
|
+
}
|
|
22
|
+
if (data.FailedSOPSequence) {
|
|
23
|
+
sopResponse.FailedSOPSequence ||= [];
|
|
24
|
+
sopResponse.FailedSOPSequence.push(...data.FailedSOPSequence);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function Success(options) {
|
|
28
|
+
const { deleteInput = false } = options;
|
|
29
|
+
|
|
30
|
+
return (_action, data, file) => {
|
|
31
|
+
if (data.ReferencedSOPSequence) {
|
|
32
|
+
ensureSOPResponse(data);
|
|
33
|
+
} else {
|
|
34
|
+
results.push(data);
|
|
35
|
+
}
|
|
36
|
+
if (file && deleteInput) {
|
|
37
|
+
console.noQuiet('Deleting', file);
|
|
38
|
+
fs.unlinkSync(file);
|
|
39
|
+
} else if (file) {
|
|
40
|
+
console.verbose('Not deleting', file);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function Failure(options) {
|
|
46
|
+
const { deleteFailed } = options;
|
|
47
|
+
|
|
48
|
+
return (_action, data, file) => {
|
|
49
|
+
if (data.FailedSOPSequence) {
|
|
50
|
+
ensureSOPResponse(data);
|
|
51
|
+
} else {
|
|
52
|
+
results.push(data);
|
|
53
|
+
}
|
|
54
|
+
if (file && deleteFailed) {
|
|
55
|
+
console.noQuiet('Deleting failure', file);
|
|
56
|
+
fs.unlinkSync(file);
|
|
57
|
+
} else if (file) {
|
|
58
|
+
console.verbose('Not deleting failure file', file);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function clearResults() {
|
|
64
|
+
results.splice(0, results.length);
|
|
65
|
+
sopResponse = null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function WriteResults(options) {
|
|
69
|
+
const { multipart } = options;
|
|
70
|
+
|
|
71
|
+
return function () {
|
|
72
|
+
if (multipart) {
|
|
73
|
+
const str =
|
|
74
|
+
results.length === 1
|
|
75
|
+
? JSON.stringify(results[0])
|
|
76
|
+
: `[\n${results.map(r => JSON.stringify(r, null, 2)).join(',\n')}\n]`;
|
|
77
|
+
console.log(
|
|
78
|
+
'\r\n--boundary-response\r\n' +
|
|
79
|
+
'content-type: application/json\r\n\r\n' +
|
|
80
|
+
str +
|
|
81
|
+
'\r\n--boundary-response--\r\n'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
clearResults();
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const { JSONReader, JSONWriter, Stats } = require(
|
|
2
|
-
const { Tags } = require(
|
|
3
|
-
const StudyData = require(
|
|
1
|
+
const { JSONReader, JSONWriter, Stats } = require('@radicalimaging/static-wado-util');
|
|
2
|
+
const { Tags } = require('@radicalimaging/static-wado-util');
|
|
3
|
+
const StudyData = require('../operation/StudyData');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* CompleteStudyWriter takes the deduplicated data values, all loaded into the study parameter,
|
|
@@ -8,13 +8,13 @@ const StudyData = require("../operation/StudyData");
|
|
|
8
8
|
* written out as.
|
|
9
9
|
* The studyData object is then removed, so that a new one can be created if required.
|
|
10
10
|
*/
|
|
11
|
-
const CompleteStudyWriter =
|
|
11
|
+
const CompleteStudyWriter = options => {
|
|
12
12
|
async function ret() {
|
|
13
13
|
const { studyData } = this;
|
|
14
14
|
if (!studyData) return;
|
|
15
15
|
|
|
16
16
|
if (!studyData.numberOfInstances) {
|
|
17
|
-
console.log(
|
|
17
|
+
console.log('studyData.deduplicated is empty');
|
|
18
18
|
delete this.studyData;
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
@@ -22,15 +22,23 @@ const CompleteStudyWriter = (options) => {
|
|
|
22
22
|
if (options.isGroup) {
|
|
23
23
|
if (studyData.dirty) {
|
|
24
24
|
await studyData.writeDeduplicatedGroup();
|
|
25
|
-
console.
|
|
25
|
+
console.noQuiet('Wrote updated deduplicated data for study', studyData.studyInstanceUid);
|
|
26
26
|
} else {
|
|
27
|
-
console.
|
|
27
|
+
console.noQuiet(
|
|
28
|
+
'Not writing new deduplicated data because it is clean:',
|
|
29
|
+
studyData.studyInstanceUid
|
|
30
|
+
);
|
|
31
|
+
const message = {
|
|
32
|
+
text: 'Metadata clean, not updating',
|
|
33
|
+
status: 0,
|
|
34
|
+
studyInstanceUid: studyData.studyInstanceUid,
|
|
35
|
+
};
|
|
36
|
+
this.success('metadataClean', message);
|
|
28
37
|
}
|
|
29
38
|
await studyData.deleteInstancesReferenced();
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
if (!options.isStudyData) {
|
|
33
|
-
console.verbose("Not configured to write study metadata", studyData.studyInstanceUid);
|
|
34
42
|
if (options.notifications) this.notificationService.notifyStudy(studyData.studyInstanceUid);
|
|
35
43
|
delete this.studyData;
|
|
36
44
|
Stats.StudyStats.summarize();
|
|
@@ -38,32 +46,44 @@ const CompleteStudyWriter = (options) => {
|
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
const isDirtyMetadata = await studyData.dirtyMetadata();
|
|
41
|
-
if (!isDirtyMetadata) {
|
|
42
|
-
console.log(
|
|
49
|
+
if (!isDirtyMetadata && !options.force) {
|
|
50
|
+
console.log('Study metadata', studyData.studyInstanceUid, 'is clean.');
|
|
43
51
|
delete this.studyData;
|
|
44
|
-
Stats.StudyStats.summarize(
|
|
52
|
+
Stats.StudyStats.summarize(
|
|
53
|
+
`Study metadata ${studyData.studyInstanceUid} has clean metadata, not writing`
|
|
54
|
+
);
|
|
45
55
|
return;
|
|
46
56
|
}
|
|
47
|
-
console.
|
|
57
|
+
console.noQuiet('Writing study metadata', studyData.studyInstanceUid);
|
|
48
58
|
|
|
49
59
|
const studyQuery = await studyData.writeMetadata();
|
|
50
60
|
|
|
51
|
-
const allStudies = await JSONReader(options.directoryName,
|
|
61
|
+
const allStudies = await JSONReader(options.directoryName, 'studies/index.json.gz', []);
|
|
52
62
|
const studyUID = Tags.getValue(studyQuery, Tags.StudyInstanceUID);
|
|
53
63
|
if (!studyUID) {
|
|
54
|
-
console.error(
|
|
55
|
-
throw new Error(
|
|
64
|
+
console.error('studyQuery=', studyQuery);
|
|
65
|
+
throw new Error('Study query has null studyUID');
|
|
56
66
|
}
|
|
57
|
-
const studyIndex = allStudies.findIndex(
|
|
67
|
+
const studyIndex = allStudies.findIndex(
|
|
68
|
+
item => Tags.getValue(item, Tags.StudyInstanceUID) == studyUID
|
|
69
|
+
);
|
|
58
70
|
if (studyIndex == -1) {
|
|
59
71
|
allStudies.push(studyQuery);
|
|
60
72
|
} else {
|
|
61
73
|
allStudies[studyIndex] = studyQuery;
|
|
62
74
|
}
|
|
63
|
-
await JSONWriter(options.directoryName,
|
|
75
|
+
await JSONWriter(options.directoryName, 'studies', allStudies);
|
|
64
76
|
if (options.notifications) this.notificationService.notifyStudy(studyData.studyInstanceUid);
|
|
77
|
+
const message = {
|
|
78
|
+
action: 'metadata',
|
|
79
|
+
status: 0,
|
|
80
|
+
studyInstanceUid: studyData.studyInstanceUid,
|
|
81
|
+
};
|
|
82
|
+
this.success('completeStudyWriter', message);
|
|
65
83
|
delete this.studyData;
|
|
66
|
-
Stats.StudyStats.summarize(
|
|
84
|
+
Stats.StudyStats.summarize(
|
|
85
|
+
`Wrote study metadata/query files for ${studyData.studyInstanceUid}`
|
|
86
|
+
);
|
|
67
87
|
}
|
|
68
88
|
|
|
69
89
|
/**
|
|
@@ -81,7 +101,10 @@ const CompleteStudyWriter = (options) => {
|
|
|
81
101
|
await callback.completeStudy(studyData);
|
|
82
102
|
}
|
|
83
103
|
callback.setStudyData(new StudyData(id, options));
|
|
84
|
-
|
|
104
|
+
if (!options.isSkipStudyScan) {
|
|
105
|
+
console.verbose('Initiate StudyData');
|
|
106
|
+
await callback.studyData.init(options);
|
|
107
|
+
}
|
|
85
108
|
return callback.studyData;
|
|
86
109
|
};
|
|
87
110
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
const hashFactory = require(
|
|
1
|
+
const hashFactory = require('node-object-hash');
|
|
2
2
|
|
|
3
3
|
const hasher = hashFactory.hasher();
|
|
4
|
-
const { JSONWriter } = require(
|
|
5
|
-
const { Tags } = require(
|
|
6
|
-
const TagLists = require(
|
|
4
|
+
const { JSONWriter } = require('@radicalimaging/static-wado-util');
|
|
5
|
+
const { Tags } = require('@radicalimaging/static-wado-util');
|
|
6
|
+
const TagLists = require('../model/TagLists');
|
|
7
7
|
|
|
8
8
|
async function writeDeduplicatedFile(dir, data, hashValueSrc) {
|
|
9
9
|
const hashValue = hashValueSrc || hasher.hash(data);
|
|
@@ -19,15 +19,15 @@ const perInstanceWriter = async (id, data) => {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
/** Writes out JSON files to the given file name. Automatically GZips them, and adds the extension */
|
|
22
|
-
const DeduplicateWriter =
|
|
22
|
+
const DeduplicateWriter = options =>
|
|
23
23
|
async function DeduplicateWriterInstance(id, data) {
|
|
24
24
|
const studyData = await this.completeStudy.getCurrentStudyData(this, id);
|
|
25
25
|
|
|
26
26
|
if (options.isDeduplicate) {
|
|
27
|
-
if (options.verbose) console.log(
|
|
27
|
+
if (options.verbose) console.log('Writing single instance', id.studyInstanceUid);
|
|
28
28
|
await perInstanceWriter(id, data);
|
|
29
29
|
} else if (options.verbose) {
|
|
30
|
-
console.log(
|
|
30
|
+
console.log('Not writing single instance deduplicated');
|
|
31
31
|
}
|
|
32
32
|
studyData.addDeduplicated(data, id.filename);
|
|
33
33
|
};
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
const ExpandUriPath = (id, path, options) => {
|
|
2
2
|
const { prependBulkDataUri, expandBulkDataUri, includeSeries } = options;
|
|
3
|
-
let expandedRelative =
|
|
3
|
+
let expandedRelative = '';
|
|
4
4
|
if (expandBulkDataUri) {
|
|
5
|
-
let expandedRelativeSeries =
|
|
5
|
+
let expandedRelativeSeries = '';
|
|
6
6
|
if (includeSeries) {
|
|
7
7
|
expandedRelativeSeries = `series/${id.seriesInstanceUid}/`;
|
|
8
8
|
}
|
|
9
9
|
expandedRelative = `studies/${id.studyInstanceUid}/${expandedRelativeSeries}`;
|
|
10
10
|
}
|
|
11
|
-
if (options.verbose)
|
|
11
|
+
if (options.verbose)
|
|
12
|
+
console.log(`Expanded path returned: ${prependBulkDataUri}${expandedRelative}${path}`);
|
|
12
13
|
return `${prependBulkDataUri}${expandedRelative}${path}`;
|
|
13
14
|
};
|
|
14
15
|
|