@radicalimaging/static-wado-creator 1.7.4 → 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 (50) 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/canvas-xaqc8v6y.node +0 -0
  5. package/dist/createMain.js +18 -0
  6. package/dist/createPart10.js +113 -0
  7. package/dist/createThumbnail.js +163 -0
  8. package/dist/deleteMain.js +12 -0
  9. package/dist/groupMain.js +17 -0
  10. package/dist/index.js +500838 -0
  11. package/dist/index.mjs +18 -0
  12. package/dist/indexMain.js +10 -0
  13. package/dist/instanceMain.js +16 -0
  14. package/dist/metadataMain.js +15 -0
  15. package/dist/mkdicomwebConfig.js +342 -0
  16. package/dist/operation/InstanceDeduplicate.js +103 -0
  17. package/dist/operation/ScanStudy.js +47 -0
  18. package/dist/operation/StudyData.js +520 -0
  19. package/dist/operation/ThumbnailService.js +198 -0
  20. package/dist/operation/adapter/decodeImage.js +12 -0
  21. package/dist/operation/adapter/getImageInfo.js +38 -0
  22. package/dist/operation/adapter/transcodeImage.js +519 -0
  23. package/dist/operation/extractImageFrames.js +55 -0
  24. package/dist/operation/getDataSet.js +65 -0
  25. package/dist/operation/getEncapsulatedImageFrame.js +35 -0
  26. package/dist/operation/getNumberOfFrames.js +13 -0
  27. package/dist/operation/getUncompressedImageFrame.js +7 -0
  28. package/dist/operation/getVR.js +24 -0
  29. package/dist/operation/getValue.js +216 -0
  30. package/dist/operation/validateMetadata.js +40 -0
  31. package/dist/program/index.js +14 -0
  32. package/dist/rejectMain.js +12 -0
  33. package/dist/serverMain.js +21 -0
  34. package/dist/util/IdCreator.js +42 -0
  35. package/dist/util/adaptProgramOpts.js +39 -0
  36. package/dist/util/compressionOptionParser.js +13 -0
  37. package/dist/util/resultsReport.ts +87 -0
  38. package/dist/writer/CompleteStudyWriter.js +129 -0
  39. package/dist/writer/DeduplicateWriter.js +33 -0
  40. package/dist/writer/ExpandUriPath.js +16 -0
  41. package/dist/writer/HashDataWriter.js +74 -0
  42. package/dist/writer/ImageFrameWriter.js +59 -0
  43. package/dist/writer/MultipartAttribute.js +8 -0
  44. package/dist/writer/MultipartHeader.js +14 -0
  45. package/dist/writer/RawDicomWriter.js +44 -0
  46. package/dist/writer/ThumbnailWriter.js +14 -0
  47. package/dist/writer/VideoWriter.js +53 -0
  48. package/dist/writer/WriteMultipart.js +22 -0
  49. package/dist/writer/WriteStream.js +64 -0
  50. package/package.json +11 -2
@@ -0,0 +1,24 @@
1
+ const { dataDictionary } = require('@radicalimaging/static-wado-util');
2
+
3
+ const nullVrTags = new Set();
4
+ nullVrTags.add('FFFEE00D');
5
+
6
+ const getVR = attr => {
7
+ if (attr.vr) {
8
+ return attr.vr;
9
+ }
10
+ // lookup the vr using the data dictionary
11
+ const tag = attr.tag.substring(1).toUpperCase();
12
+ const dataDictAttr = dataDictionary[tag];
13
+ if (dataDictAttr?.vr) {
14
+ return dataDictAttr.vr;
15
+ }
16
+ if (nullVrTags.has(tag)) {
17
+ return null;
18
+ }
19
+
20
+ console.log('Unknown VR', tag, attr, dataDictAttr);
21
+ return 'UN';
22
+ };
23
+
24
+ module.exports = getVR;
@@ -0,0 +1,216 @@
1
+ const extractImageFrames = require('./extractImageFrames');
2
+
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) => [
12
+ decodeUtf8(dataSet.string(attr.tag), options),
13
+ ];
14
+
15
+ const getStrings = (dataSet, attr, options) => {
16
+ const ret = dataSet.string(attr.tag);
17
+ if (ret) {
18
+ return ret.split(/\\/).map(decodeUtf8, options);
19
+ }
20
+ return undefined;
21
+ };
22
+
23
+ const getValuePatientName = (dataSet, attr) => {
24
+ const strings = getStrings(dataSet, attr);
25
+ return (strings && strings.map(item => ({ Alphabetic: item }))) || undefined;
26
+ };
27
+
28
+ /** Gets either InlineBinary or BulkDataURI, if already defined */
29
+ const getValueInlineBinary = (dataSet, attr) => {
30
+ if (attr.BulkDataURI) return { BulkDataURI: attr.BulkDataURI };
31
+ const binaryValue = dataSet.byteArray.slice(attr.dataOffset, attr.dataOffset + attr.length);
32
+ return { InlineBinary: binaryValue.toString('base64') };
33
+ };
34
+
35
+ const getValueInlineSignedShort = (dataSet, attr) => {
36
+ const ret = [];
37
+ for (let i = 0; i < attr.length / 2; i++) {
38
+ ret.push(dataSet.int16(attr.tag, i));
39
+ }
40
+ return ret;
41
+ };
42
+
43
+ const getValueInlineUnsignedShort = (dataSet, attr) => {
44
+ const ret = [];
45
+ for (let i = 0; i < attr.length / 2; i++) {
46
+ ret.push(dataSet.uint16(attr.tag, i));
47
+ }
48
+ return ret;
49
+ };
50
+
51
+ const getValueInlineSignedLong = (dataSet, attr) => {
52
+ if (attr.length > 4) {
53
+ return getValueInlineBinary(dataSet, attr);
54
+ }
55
+ return [dataSet.int32(attr.tag)];
56
+ };
57
+
58
+ const getValueInlineUnsignedLong = (dataSet, attr) => {
59
+ if (attr.length > 4) {
60
+ return getValueInlineBinary(dataSet, attr);
61
+ }
62
+ return [dataSet.uint32(attr.tag)];
63
+ };
64
+
65
+ const getValueInlineFloat = (dataSet, attr) => {
66
+ if (attr.length > 65536 * 4) {
67
+ return getValueInlineBinary(dataSet, attr);
68
+ }
69
+ const ret = [];
70
+ for (let i = 0; i < attr.length / 4; i++) {
71
+ ret.push(dataSet.float(attr.tag, i));
72
+ }
73
+ return ret;
74
+ };
75
+
76
+ const getValueInlineIntString = (dataSet, attr) =>
77
+ getStrings(dataSet, attr).map(val => parseInt(val));
78
+
79
+ const getValueInlineFloatString = (dataSet, attr) =>
80
+ getStrings(dataSet, attr).map(val => parseFloat(val));
81
+
82
+ const getValueInlineFloatDouble = (dataSet, attr) => {
83
+ if (attr.length > 8) {
84
+ return getValueInlineBinary(dataSet, attr);
85
+ }
86
+ return [dataSet.double(attr.tag)];
87
+ };
88
+
89
+ const getValueInlineAttributeTag = (dataSet, attr) => {
90
+ const group = dataSet.uint16(attr.tag, 0);
91
+ const groupHexStr = `0000${group.toString(16)}`.substr(-4);
92
+ const element = dataSet.uint16(attr.tag, 1);
93
+ const elementHexStr = `0000${element.toString(16)}`.substr(-4);
94
+ return groupHexStr + elementHexStr;
95
+ };
96
+
97
+ const getValueInline = (dataSet, attr, vr, options) => {
98
+ if (attr.length == 0) {
99
+ return [];
100
+ }
101
+
102
+ switch (vr) {
103
+ case 'AE':
104
+ case 'AS':
105
+ return getValueInlineString(dataSet, attr, options);
106
+ case 'AT':
107
+ return getValueInlineAttributeTag(dataSet, attr);
108
+ case 'DS':
109
+ return getValueInlineFloatString(dataSet, attr);
110
+ case 'CS':
111
+ case 'DA':
112
+ case 'DT':
113
+ return getValueInlineString(dataSet, attr, options);
114
+ case 'FL':
115
+ return getValueInlineFloat(dataSet, attr);
116
+ case 'FD':
117
+ return getValueInlineFloatDouble(dataSet, attr);
118
+ case 'IS':
119
+ return getValueInlineIntString(dataSet, attr);
120
+ case 'LO':
121
+ case 'LT':
122
+ return getValueInlineString(dataSet, attr, options);
123
+ case 'OB':
124
+ case 'OF':
125
+ case 'OW':
126
+ return getValueInlineBinary(dataSet, attr);
127
+ case 'PN':
128
+ return getValuePatientName(dataSet, attr);
129
+ case 'SH':
130
+ return getValueInlineString(dataSet, attr, options);
131
+ case 'SL':
132
+ return getValueInlineSignedLong(dataSet, attr);
133
+ case 'SS':
134
+ return getValueInlineSignedShort(dataSet, attr);
135
+ case 'ST':
136
+ case 'TM':
137
+ case 'UI':
138
+ return getValueInlineString(dataSet, attr, options);
139
+ case 'UL':
140
+ return getValueInlineUnsignedLong(dataSet, attr);
141
+ case 'UN':
142
+ return getValueInlineBinary(dataSet, attr);
143
+ case 'US':
144
+ return getValueInlineUnsignedShort(dataSet, attr);
145
+ case 'UT':
146
+ return getValueInlineString(dataSet, attr, options);
147
+ default:
148
+ return getValueInlineBinary(dataSet, attr);
149
+ }
150
+ };
151
+
152
+ const isPrivate = attr => {
153
+ const { tag } = attr;
154
+ const ch = tag.substr(4, 1);
155
+ const chHex = parseInt(ch, 16);
156
+ return chHex % 2 == 1;
157
+ };
158
+
159
+ const RawEncapsulatedDocument = 'x00420011';
160
+
161
+ const isValueInline = (attr, options) => {
162
+ if (isPrivate(attr)) {
163
+ return attr.length <= options.maximumInlinePrivateLength;
164
+ }
165
+ // For encapsulated documents, almost always have them be external
166
+ if (attr.tag === RawEncapsulatedDocument && attr.length > 4) {
167
+ return false;
168
+ }
169
+ return !attr.length || attr.length <= options.maximumInlinePublicLength;
170
+ };
171
+
172
+ /**
173
+ * Get data value for the given attr.
174
+ * Attr can be a pixelData tag, sequence items, inline or bulkdata
175
+ *
176
+ * @param {*} dataSet
177
+ * @param {*} attr
178
+ * @param {*} vr
179
+ * @param {*} getDataSet
180
+ * @param {*} callback
181
+ * @param {*} options object containing program properties.
182
+ * @param {*} parentAttr attr's parent. If not present means attr is at root level.
183
+ * @returns
184
+ */
185
+ const getValue = async (dataSet, attr, vr, getDataSet, callback, options, parentAttr) => {
186
+ // It will only process pixelData tag if on metadata root. Otherwise it will be skiped.
187
+ if (attr.tag === 'x7fe00010' && !parentAttr) {
188
+ const BulkDataURI = await extractImageFrames(dataSet, attr, vr, callback, options);
189
+ return { BulkDataURI };
190
+ }
191
+ if (attr.tag === 'xfffee00d') return undefined;
192
+ // Group length
193
+ if (attr.tag.length == 9 && attr.tag.substring(5, 9) === '0000') return undefined;
194
+ if (attr.items) {
195
+ // sequences
196
+ const result = [];
197
+ for (const item of attr.items) {
198
+ const subResult = await getDataSet(item.dataSet, callback, options, attr);
199
+ if (subResult.metadata) result.push(subResult.metadata);
200
+ }
201
+ return result;
202
+ }
203
+ if (attr.Value) {
204
+ return (Array.isArray(attr.Value) && attr.Value) || [attr.Value];
205
+ }
206
+ // non sequence item
207
+ if (isValueInline(attr, options)) {
208
+ return getValueInline(dataSet, attr, vr, options);
209
+ }
210
+ const binaryValue = dataSet.byteArray.slice(attr.dataOffset, attr.dataOffset + attr.length);
211
+ const mimeType = attr.tag == 'x00420011' && dataSet.string('x00420012');
212
+
213
+ const BulkDataURI = await callback.bulkdata(binaryValue, { mimeType });
214
+ return { BulkDataURI, vr };
215
+ };
216
+ module.exports = getValue;
@@ -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;
@@ -0,0 +1,14 @@
1
+ const staticWadoUtil = require('@radicalimaging/static-wado-util');
2
+
3
+ /**
4
+ * Configure static-wado-creator commander program.
5
+ *
6
+ * @param {*} defaults Configuration caller level
7
+ * @returns Program object
8
+ */
9
+ async function configureProgram(defaults) {
10
+ const configurationFile = await staticWadoUtil.loadConfiguration(defaults, process.argv);
11
+ return staticWadoUtil.configureCommands(defaults);
12
+ }
13
+
14
+ exports.configureProgram = configureProgram;
@@ -0,0 +1,12 @@
1
+ const StaticWado = require('./StaticWado');
2
+ const adaptProgramOpts = require('./util/adaptProgramOpts');
3
+
4
+ module.exports = function rejectMain(args, options) {
5
+ const finalOptions = adaptProgramOpts(options, {
6
+ ...this,
7
+ isGroup: true,
8
+ isStudyData: true,
9
+ });
10
+ const importer = new StaticWado(finalOptions);
11
+ return importer.reject(args);
12
+ };
@@ -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
+ };
@@ -0,0 +1,42 @@
1
+ const path = require('path');
2
+ const { getStudyUIDPathAndSubPath } = require('@radicalimaging/static-wado-util');
3
+
4
+ function IdCreator({
5
+ directoryName,
6
+ deduplicatedRoot,
7
+ deduplicatedInstancesRoot,
8
+ hashStudyUidPath,
9
+ }) {
10
+ return (uids, filename) => {
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);
28
+
29
+ return {
30
+ ...uids,
31
+ studyPath,
32
+ deduplicatedPath,
33
+ deduplicatedInstancesPath,
34
+ seriesRootPath,
35
+ sopInstanceRootPath,
36
+ imageFrameRootPath,
37
+ filename,
38
+ };
39
+ };
40
+ }
41
+
42
+ module.exports = IdCreator;
@@ -0,0 +1,39 @@
1
+ const assignDefined = (dest, src) => {
2
+ Object.keys(src).forEach(key => {
3
+ const val = src[key];
4
+ if (val !== undefined) {
5
+ dest[key] = val;
6
+ }
7
+ });
8
+ return dest;
9
+ };
10
+
11
+ module.exports = function adaptProgramOpts(programOpts, defaults) {
12
+ const {
13
+ group: isGroup,
14
+ instances: isInstanceMetadata,
15
+ deduplicate: isDeduplicate,
16
+ study: isStudyData,
17
+ clean: isClean,
18
+ dir: rootDir,
19
+ delete: deleteInstances,
20
+ singlePartImage,
21
+ encapsulatedImage,
22
+ } = programOpts;
23
+
24
+ return assignDefined(
25
+ { ...defaults },
26
+ {
27
+ ...programOpts,
28
+ isStudyData,
29
+ isClean,
30
+ isInstanceMetadata,
31
+ isDeduplicate,
32
+ isGroup,
33
+ rootDir,
34
+ encapsulatedImage: encapsulatedImage ?? singlePartImage !== true,
35
+ singlePartImage: singlePartImage ?? encapsulatedImage === false,
36
+ delete: deleteInstances,
37
+ }
38
+ );
39
+ };
@@ -0,0 +1,13 @@
1
+ const { program } = require('@radicalimaging/static-wado-util');
2
+ const { getDestinationTranscoder } = require('../operation/adapter/transcodeImage');
3
+
4
+ module.exports = function compressionOptionParser(value) {
5
+ const destination = getDestinationTranscoder(value);
6
+ if (destination) {
7
+ console.log('Found destination transferSyntax ', destination.transferSyntaxUid, 'for ', value);
8
+ } else {
9
+ console.log('No transcoder destination for ', value);
10
+ throw new program.InvalidArgumentError(`No transcoder destination for ${value}`);
11
+ }
12
+ return destination.transferSyntaxUid;
13
+ };
@@ -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
+ }
@@ -0,0 +1,129 @@
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
+
5
+ /**
6
+ * CompleteStudyWriter takes the deduplicated data values, all loaded into the study parameter,
7
+ * and writes it out as various dataset types. The options parameters define what types it gets
8
+ * written out as.
9
+ * The studyData object is then removed, so that a new one can be created if required.
10
+ */
11
+ const CompleteStudyWriter = options => {
12
+ async function ret() {
13
+ const { studyData } = this;
14
+ if (!studyData) return;
15
+
16
+ if (!studyData.numberOfInstances) {
17
+ console.log('studyData.deduplicated is empty');
18
+ delete this.studyData;
19
+ return;
20
+ }
21
+
22
+ if (options.isGroup) {
23
+ if (studyData.dirty) {
24
+ await studyData.writeDeduplicatedGroup();
25
+ console.noQuiet('Wrote updated deduplicated data for study', studyData.studyInstanceUid);
26
+ } else {
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);
37
+ }
38
+ await studyData.deleteInstancesReferenced();
39
+ }
40
+
41
+ if (!options.isStudyData) {
42
+ if (options.notifications) this.notificationService.notifyStudy(studyData.studyInstanceUid);
43
+ delete this.studyData;
44
+ Stats.StudyStats.summarize();
45
+ return;
46
+ }
47
+
48
+ const isDirtyMetadata = await studyData.dirtyMetadata();
49
+ if (!isDirtyMetadata && !options.force) {
50
+ console.log('Study metadata', studyData.studyInstanceUid, 'is clean.');
51
+ delete this.studyData;
52
+ Stats.StudyStats.summarize(
53
+ `Study metadata ${studyData.studyInstanceUid} has clean metadata, not writing`
54
+ );
55
+ return;
56
+ }
57
+ console.noQuiet('Writing study metadata', studyData.studyInstanceUid);
58
+
59
+ const studyQuery = await studyData.writeMetadata();
60
+
61
+ if (options.index === false) {
62
+ if (options.notifications) this.notificationService.notifyStudy(studyData.studyInstanceUid);
63
+ const message = {
64
+ action: 'metadata',
65
+ status: 0,
66
+ studyInstanceUid: studyData.studyInstanceUid,
67
+ };
68
+ this.success('completeStudyWriter', message);
69
+ delete this.studyData;
70
+ Stats.StudyStats.summarize(
71
+ `Wrote study metadata/query files for ${studyData.studyInstanceUid} (index skipped)`
72
+ );
73
+ return;
74
+ }
75
+
76
+ const allStudies = await JSONReader(options.directoryName, 'studies/index.json.gz', []);
77
+ const studyUID = Tags.getValue(studyQuery, Tags.StudyInstanceUID);
78
+ if (!studyUID) {
79
+ console.error('studyQuery=', studyQuery);
80
+ throw new Error('Study query has null studyUID');
81
+ }
82
+ const studyIndex = allStudies.findIndex(
83
+ item => Tags.getValue(item, Tags.StudyInstanceUID) == studyUID
84
+ );
85
+ if (studyIndex == -1) {
86
+ allStudies.push(studyQuery);
87
+ } else {
88
+ allStudies[studyIndex] = studyQuery;
89
+ }
90
+ await JSONWriter(options.directoryName, 'studies', allStudies);
91
+ if (options.notifications) this.notificationService.notifyStudy(studyData.studyInstanceUid);
92
+ const message = {
93
+ action: 'metadata',
94
+ status: 0,
95
+ studyInstanceUid: studyData.studyInstanceUid,
96
+ };
97
+ this.success('completeStudyWriter', message);
98
+ delete this.studyData;
99
+ Stats.StudyStats.summarize(
100
+ `Wrote study metadata/query files for ${studyData.studyInstanceUid}`
101
+ );
102
+ }
103
+
104
+ /**
105
+ * Gets a current study data object, or completes the old one and generates a new one.
106
+ * async call as it may need to store the current study data value.
107
+ */
108
+ ret.getCurrentStudyData = async (callback, id) => {
109
+ const { studyData } = callback;
110
+ const { studyInstanceUid } = id;
111
+
112
+ if (studyData) {
113
+ if (studyData.studyInstanceUid == studyInstanceUid) {
114
+ return studyData;
115
+ }
116
+ await callback.completeStudy(studyData);
117
+ }
118
+ callback.setStudyData(new StudyData(id, options));
119
+ if (!options.isSkipStudyScan) {
120
+ console.verbose('Initiate StudyData');
121
+ await callback.studyData.init(options);
122
+ }
123
+ return callback.studyData;
124
+ };
125
+
126
+ return ret;
127
+ };
128
+
129
+ module.exports = CompleteStudyWriter;
@@ -0,0 +1,33 @@
1
+ const hashFactory = require('node-object-hash');
2
+
3
+ const hasher = hashFactory.hasher();
4
+ const { JSONWriter, Tags, TagLists } = require('@radicalimaging/static-wado-util');
5
+
6
+ async function writeDeduplicatedFile(dir, data, hashValueSrc) {
7
+ const hashValue = hashValueSrc || hasher.hash(data);
8
+ // Write it direct in the deduplicated directory, not as a sub-directory or index file
9
+ await JSONWriter(dir, hashValue, data, { gzip: true, index: false });
10
+ return hashValue;
11
+ }
12
+
13
+ const perInstanceWriter = async (id, data) => {
14
+ const { deduplicatedInstancesPath } = id;
15
+ const hashValue = TagLists.addHash(data, Tags.InstanceType);
16
+ return writeDeduplicatedFile(deduplicatedInstancesPath, [data], hashValue);
17
+ };
18
+
19
+ /** Writes out JSON files to the given file name. Automatically GZips them, and adds the extension */
20
+ const DeduplicateWriter = options =>
21
+ async function DeduplicateWriterInstance(id, data) {
22
+ const studyData = await this.completeStudy.getCurrentStudyData(this, id);
23
+
24
+ if (options.isDeduplicate) {
25
+ if (options.verbose) console.log('Writing single instance', id.studyInstanceUid);
26
+ await perInstanceWriter(id, data);
27
+ } else if (options.verbose) {
28
+ console.log('Not writing single instance deduplicated');
29
+ }
30
+ studyData.addDeduplicated(data, id.filename);
31
+ };
32
+
33
+ module.exports = DeduplicateWriter;
@@ -0,0 +1,16 @@
1
+ const ExpandUriPath = (id, path, options) => {
2
+ const { prependBulkDataUri, expandBulkDataUri, includeSeries } = options;
3
+ let expandedRelative = '';
4
+ if (expandBulkDataUri) {
5
+ let expandedRelativeSeries = '';
6
+ if (includeSeries) {
7
+ expandedRelativeSeries = `series/${id.seriesInstanceUid}/`;
8
+ }
9
+ expandedRelative = `studies/${id.studyInstanceUid}/${expandedRelativeSeries}`;
10
+ }
11
+ if (options.verbose)
12
+ console.log(`Expanded path returned: ${prependBulkDataUri}${expandedRelative}${path}`);
13
+ return `${prependBulkDataUri}${expandedRelative}${path}`;
14
+ };
15
+
16
+ module.exports = ExpandUriPath;