@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
package/dist/index.mjs ADDED
@@ -0,0 +1,18 @@
1
+ import mkdicomwebConfig from './mkdicomwebConfig.js';
2
+ import StaticWado from './StaticWado.js';
3
+ import programIndex from './program/index.js';
4
+ import createMain from './createMain.js';
5
+ import deleteMain from './deleteMain.js';
6
+ import adaptProgramOpts from './util/adaptProgramOpts.js';
7
+ import { uids } from '@radicalimaging/static-wado-util';
8
+
9
+ const { configureProgram } = programIndex;
10
+
11
+ StaticWado.mkdicomwebConfig = mkdicomwebConfig;
12
+ StaticWado.createMain = createMain;
13
+ StaticWado.deleteMain = deleteMain;
14
+ StaticWado.adaptProgramOpts = adaptProgramOpts;
15
+ StaticWado.configureProgram = configureProgram;
16
+
17
+ export default StaticWado;
18
+ export { uids, StaticWado };
@@ -0,0 +1,10 @@
1
+ const StaticWado = require('./StaticWado');
2
+ const adaptProgramOpts = require('./util/adaptProgramOpts');
3
+
4
+ module.exports = function createMain(options) {
5
+ const finalOptions = adaptProgramOpts(options, {
6
+ ...this,
7
+ });
8
+ const importer = new StaticWado(finalOptions);
9
+ return importer.reindex();
10
+ };
@@ -0,0 +1,16 @@
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
+ isInstance: true,
8
+ // Deduplicated data is single instance deduplicated data
9
+ isDeduplicate: true,
10
+ isGroup: false,
11
+ isStudyData: false,
12
+ isSkipStudyScan: true,
13
+ });
14
+ const importer = new StaticWado(finalOptions);
15
+ return importer.executeCommand(program.args);
16
+ };
@@ -0,0 +1,15 @@
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
+ isInstance: false,
8
+ isGroup: true,
9
+ isStudyData: true,
10
+ scanStudies: true,
11
+ });
12
+ const importer = new StaticWado(finalOptions);
13
+ // Either do a scan or on specified studies
14
+ return importer.executeCommand(program.args);
15
+ };
@@ -0,0 +1,342 @@
1
+ const ConfigPoint = require('config-point');
2
+ const { staticWadoConfig } = require('@radicalimaging/static-wado-util');
3
+ const createMain = require('./createMain');
4
+ const createPart10 = require('./createPart10');
5
+ const createThumbnail = require('./createThumbnail');
6
+ const deleteMain = require('./deleteMain');
7
+ const rejectMain = require('./rejectMain');
8
+ const serverMain = require('./serverMain');
9
+ const instanceMain = require('./instanceMain');
10
+ const indexMain = require('./indexMain');
11
+ const groupMain = require('./groupMain');
12
+ const metadataMain = require('./metadataMain');
13
+ const compressionOptionParser = require('./util/compressionOptionParser');
14
+
15
+ /**
16
+ * Defines the basic configuration values for the dicomwebserver component. See the README for more details.
17
+ * TODO - fix the default values so they come from the configuration file. In the meantime, leave the defaults blank.
18
+ */
19
+ const { mkdicomwebConfig } = ConfigPoint.register({
20
+ mkdicomwebConfig: {
21
+ // This declare the inheritted configuration, don't assume this is directly accessible
22
+ configBase: staticWadoConfig,
23
+ isStudyData: true,
24
+ isGroup: true,
25
+ options: [
26
+ {
27
+ key: '-c <configFile.json5>',
28
+ description: 'Use an alternate configuration file',
29
+ defaultValue: '~/static-wado.json5',
30
+ },
31
+ {
32
+ key: '--clean',
33
+ description: 'Clean the outputs before generating/starting to write new values.',
34
+ defaultValue: false,
35
+ },
36
+ {
37
+ key: '-d, --deployments <listvalue...>',
38
+ description: 'List of deployments from configuration to deploy to. Separated by space.',
39
+ defaultValue: undefined,
40
+ },
41
+ {
42
+ key: '--no-notifications',
43
+ description: 'Turns off notifications',
44
+ },
45
+ {
46
+ key: '--delete',
47
+ description: 'Delete the imported instances',
48
+ defaultValue: false,
49
+ },
50
+ {
51
+ key: '--lossy',
52
+ description: 'Generate lossy instances instead of lossless',
53
+ defaultValue: false,
54
+ },
55
+ {
56
+ key: '--alternate <type>',
57
+ description:
58
+ 'Generates an alternate representaton of the image generally in the /lossy sub-directory',
59
+ choices: ['jhc', 'jls', 'jhcLossless', 'jlsLossless'],
60
+ },
61
+ {
62
+ key: '--alternate-thumbnail',
63
+ description: 'Generates a thumbnail for the alternate representation',
64
+ },
65
+ {
66
+ key: '--alternate-name <dir>',
67
+ description: 'Uses a given sub directory name',
68
+ defaultValue: 'lossy',
69
+ },
70
+ {
71
+ key: '-v, --verbose',
72
+ description: 'Write verbose output',
73
+ defaultValue: false,
74
+ },
75
+ {
76
+ key: '--show-progress',
77
+ description: 'Show progress during DICOM file processing',
78
+ },
79
+ {
80
+ key: '-t, --content-type <type>',
81
+ description:
82
+ 'Destination type to compress to (choices: "jpeg", "jls", "lei", "jls-lossy", "jhc", "jxl" or DICOM Transfer Syntax UID - default: "jls")',
83
+ defaultValue: 'jls',
84
+ customParser: compressionOptionParser,
85
+ },
86
+ {
87
+ key: '--encapsulated-image',
88
+ description:
89
+ 'Avoid encapsulating the image frame. Writes with the extension and without multipart',
90
+ },
91
+ {
92
+ key: '-e, --no-encapsulated-image',
93
+ description:
94
+ 'Avoid encapsulating the image frame. Writes with the extension and without multipart',
95
+ },
96
+ {
97
+ key: '--single-part-image',
98
+ description: 'Writes single part image data',
99
+ },
100
+ {
101
+ key: '--no-single-part-image',
102
+ description: 'Writes single part image data',
103
+ },
104
+ {
105
+ key: '-m, --maximum-inline-public-length <value>',
106
+ description: 'Maximum length of public binary data',
107
+ defaultValue: 128 * 1024 + 2,
108
+ },
109
+ {
110
+ key: '-M, --maximum-inline-private-length <value>',
111
+ description: 'Maximum length of private binary data',
112
+ defaultValue: 64,
113
+ },
114
+ {
115
+ key: '-r, --recompress <listvalue...>',
116
+ description: 'List of types to recompress separated by space',
117
+ defaultValue: ['uncompressed', 'jp2', 'jls', 'jll'],
118
+ choices: [
119
+ 'uncompressed',
120
+ 'jp2',
121
+ 'jpeg',
122
+ 'jpeglossless',
123
+ 'rle',
124
+ 'jph',
125
+ 'jls',
126
+ 'true',
127
+ 'none',
128
+ ],
129
+ },
130
+ {
131
+ key: '--recompress-color <listvalue...>',
132
+ description: 'List of types to recompress for color images, separated by space',
133
+ defaultValue: ['uncompressed'],
134
+ choices: [
135
+ 'uncompressed',
136
+ 'jpeg',
137
+ 'jp2',
138
+ 'jpeglossless',
139
+ 'rle',
140
+ 'jph',
141
+ 'jls',
142
+ 'true',
143
+ 'none',
144
+ ],
145
+ },
146
+ {
147
+ key: '-f, --force',
148
+ description: 'Force the update even if the SOP exists',
149
+ defaultValue: false,
150
+ },
151
+ {
152
+ key: '--delete-input',
153
+ description: 'Deletes successful input files',
154
+ defaultValue: false,
155
+ },
156
+ {
157
+ key: '--delete-failed',
158
+ description: 'Deletes unsuccessful input files',
159
+ defaultValue: false,
160
+ },
161
+ {
162
+ key: '--recompress-thumb <listvalue...>',
163
+ description: 'List of types to recompress thumb separated by space',
164
+ defaultValue: ['uncompressed', 'jp2'],
165
+ choices: ['uncompressed', 'jp2', 'jpeglossless', 'rle', 'none'],
166
+ },
167
+ {
168
+ key: '--no-recompress',
169
+ description: 'Force no recompression',
170
+ defaultValue: false,
171
+ },
172
+ {
173
+ key: '--no-recompress-thumb',
174
+ description: 'Force no recompression thumbnail',
175
+ defaultValue: false,
176
+ },
177
+ {
178
+ key: '--no-thumb',
179
+ description: 'Skip generating thumbnails using CS3D internal render',
180
+ defaultValue: true,
181
+ },
182
+ {
183
+ key: '--dcm2jpg',
184
+ description: 'Use dcm2jpg for thumbnail image generation',
185
+ defaultValue: false,
186
+ },
187
+ {
188
+ key: '--rendered',
189
+ description: 'Use dcm2jpg to generate a rendered PNG image',
190
+ defaultValue: false,
191
+ },
192
+ {
193
+ key: '-T, --color-content-type <value>',
194
+ description: 'Colour content type',
195
+ defaultValue: 'jpeg',
196
+ },
197
+ {
198
+ key: '--path-deduplicated <path>',
199
+ description: 'Set the deduplicate data directory path (relative to dir)',
200
+ defaultValue: 'deduplicated',
201
+ },
202
+ {
203
+ key: '--path-instances <path>',
204
+ description: 'Set the instances directory path (relative to dir)',
205
+ defaultValue: 'instances',
206
+ },
207
+ {
208
+ key: '--store-multipart-bulk-data',
209
+ description: 'Use multipart encoding for non image frame bulkdata',
210
+ defaultValue: false,
211
+ },
212
+ {
213
+ key: '--prepend-bulk-data-uri <value>',
214
+ description:
215
+ 'Prepend bulkdata uri (ex. to use absolute Uri like http://host:3000/dicomweb)',
216
+ defaultValue: '',
217
+ },
218
+ {
219
+ key: '--expand-bulk-data-uri',
220
+ description:
221
+ 'expand bulkdata relative uri to use full relative path (should also be set when using --prepend-bulk-data-uri)',
222
+ defaultValue: false,
223
+ },
224
+ {
225
+ key: '--quiet',
226
+ description: 'Run quietly, minimizing extraneous output',
227
+ defaultValue: false,
228
+ },
229
+ {
230
+ key: '--multipart',
231
+ description: 'Add a multipart result output',
232
+ defaultValue: false,
233
+ },
234
+ {
235
+ key: '--no-verify',
236
+ description: 'Verify',
237
+ defaultValue: true,
238
+ },
239
+ {
240
+ key: '-o, --dir <value>',
241
+ description: 'Set output directory',
242
+ defaultValue: {
243
+ configOperation: 'reference',
244
+ source: 'staticWadoConfig',
245
+ reference: 'rootDir',
246
+ },
247
+ },
248
+ {
249
+ key: '--sop-instance-uid <value>',
250
+ description: 'Choose sop instances to run against (only for instance runs)',
251
+ },
252
+ {
253
+ key: '--series-instance-uid <value>',
254
+ description: 'Choose series instances to run against (only for instance runs)',
255
+ },
256
+
257
+ {
258
+ key: '--hash-study-uid-path',
259
+ description: 'Enable hashing of studyUID folder structure',
260
+ defaultValue: false,
261
+ },
262
+ {
263
+ key: '--no-index',
264
+ description:
265
+ 'Skip updating the shared studies index file in dicomweb/studies/index.json.gz',
266
+ defaultValue: true,
267
+ },
268
+ ],
269
+ programs: [
270
+ {
271
+ command: 'create',
272
+ isDefault: true,
273
+ arguments: ['input'],
274
+ main: createMain,
275
+ helpDescription:
276
+ 'Make DICOMweb query and metadata from binary Part 10 DICOM files. Does a full read\n' +
277
+ 'of deduplicated files each time a study instance UID is found, and only updates\n' +
278
+ 'those studies having at least one ',
279
+ },
280
+ {
281
+ command: 'server',
282
+ main: serverMain,
283
+ helpDescription: 'Run server deploy instances single threaded/separated exec process',
284
+ },
285
+ {
286
+ command: 'part10',
287
+ arguments: ['studyUID'],
288
+ main: createPart10,
289
+ helpDescription: 'Store the specified study as part 10 data',
290
+ },
291
+ {
292
+ command: 'index',
293
+ main: indexMain,
294
+ helpDescription: 'Recreate the index for the DICOMweb data.',
295
+ },
296
+ {
297
+ command: 'thumbnail',
298
+ main: createThumbnail,
299
+ helpDescription: 'Create a thumbnail for the specified image',
300
+ options: [
301
+ {
302
+ key: '--series-thumbnail',
303
+ description: 'Generate a series thumbnail instead of instance',
304
+ },
305
+ ],
306
+ },
307
+ {
308
+ command: 'instance',
309
+ arguments: ['input'],
310
+ main: instanceMain,
311
+ helpDescription:
312
+ "Make instance level DICOMweb metadata and bulkdata, but don't group or write series metadata",
313
+ },
314
+ {
315
+ command: 'group',
316
+ arguments: ['input'],
317
+ main: groupMain,
318
+ helpDescription:
319
+ 'Group instance level metadata into deduplicated data.\nDeletes instance level deduplicated information once it is confirmed written.',
320
+ },
321
+ {
322
+ command: 'metadata',
323
+ arguments: ['input'],
324
+ main: metadataMain,
325
+ helpDescription:
326
+ 'Write the metadata object (series and study details) from the grouped deduplicated data.',
327
+ },
328
+ {
329
+ command: 'delete',
330
+ main: deleteMain,
331
+ helpDescription: 'Delete the given study, series or instance (not yet implemented)',
332
+ },
333
+ {
334
+ command: 'reject <studyUID...>',
335
+ main: rejectMain,
336
+ helpDescription: 'Reject the specified series',
337
+ },
338
+ ],
339
+ },
340
+ });
341
+
342
+ module.exports = mkdicomwebConfig;
@@ -0,0 +1,103 @@
1
+ // TODO review returning type of some methods
2
+ const { JSONWriter, Tags, TagLists } = require('@radicalimaging/static-wado-util');
3
+
4
+ const { getValue, setValue } = Tags;
5
+
6
+ const extractors = {
7
+ patient: TagLists.PatientQuery,
8
+ study: TagLists.StudyQuery,
9
+ series: TagLists.SeriesExtract,
10
+ image: TagLists.ImageExtract,
11
+ };
12
+
13
+ /**
14
+ * This is an instance listener - the way this one works is that it listens for instance metadata.
15
+ * Every time an instance metadata event is fired, it deduplicates a bunch of data, firing deduplicate events,
16
+ * and then collects the deduplicate data into a list.
17
+ * Whenever the study UID changes, a new event is fired to indicate the deduplicated data is ready.
18
+ */
19
+ async function deduplicateSingleInstance(id, imageFrame, { force }) {
20
+ if (!imageFrame) return {};
21
+ const studyData = await this.completeStudy.getCurrentStudyData(this, id);
22
+ const seriesUID = getValue(imageFrame, Tags.SeriesInstanceUID);
23
+ const sopUID = getValue(imageFrame, Tags.SOPInstanceUID);
24
+ if (!sopUID) {
25
+ console.warn('No sop instance UID in', imageFrame);
26
+ return {};
27
+ }
28
+ if (!force && studyData.sopExists(sopUID)) {
29
+ if (this.verbose) console.log('SOP Instance UID', sopUID, 'already exists, skipping');
30
+ // TODO - allow replace as an option
31
+ // Null value means skip writing this instance
32
+ return null;
33
+ }
34
+
35
+ if (this.verbose) console.log('SOP Instance UID', sopUID, 'being added');
36
+ const deduplicated = { ...imageFrame };
37
+
38
+ if (!this.extractors) this.extractors = extractors;
39
+ for (const key of Object.keys(this.extractors)) {
40
+ const extracted = TagLists.extract(
41
+ deduplicated,
42
+ key,
43
+ this.extractors[key],
44
+ TagLists.RemoveExtract
45
+ );
46
+ const hashKey = getValue(extracted, Tags.DeduppedHash);
47
+ await studyData.addExtracted(this, hashKey, extracted);
48
+ }
49
+
50
+ // Restore the series and SOP UIDs
51
+ setValue(deduplicated, Tags.SeriesInstanceUID, seriesUID);
52
+ setValue(deduplicated, Tags.SOPInstanceUID, sopUID);
53
+ TagLists.addHash(deduplicated, Tags.InstanceType);
54
+
55
+ return deduplicated;
56
+ }
57
+
58
+ /** Canonicalize the JSON data, making Values always arrays, remote "undefined" from the tags etc */
59
+ const canonicalize = json => {
60
+ if (!json) return undefined;
61
+ if (Array.isArray(json)) {
62
+ return json.map(canonicalize);
63
+ }
64
+ Object.keys(json).forEach(tag => {
65
+ if (!tag || tag === 'undefined') {
66
+ // console.error('Got an undefined tag:', tag, typeof(tag));
67
+ // TODO check returning value
68
+ return;
69
+ }
70
+ const val = json[tag];
71
+ const { Value } = val || {};
72
+ if (Value && !Array.isArray(Value)) {
73
+ val.Value = [Value];
74
+ }
75
+ });
76
+ return json;
77
+ };
78
+
79
+ const InstanceDeduplicate = options =>
80
+ async function run(id, sourceImageFrame) {
81
+ // Notify the existing listeners, if any
82
+ const imageFrame = canonicalize(sourceImageFrame);
83
+ if (options.isInstance) {
84
+ await JSONWriter(id.sopInstanceRootPath, 'metadata', imageFrame);
85
+ console.verbose('Wrote instance metadata', id.sopInstanceRootPath, 'metadata');
86
+ }
87
+ if (!options.isDeduplicate && !options.isGroup) {
88
+ return;
89
+ }
90
+
91
+ if (!this.deduplicateSingleInstance) {
92
+ this.deduplicateSingleInstance = deduplicateSingleInstance;
93
+ this.verbose = options.verbose;
94
+ }
95
+
96
+ const deduppedInstance = await this.deduplicateSingleInstance(id, imageFrame, options);
97
+ if (deduppedInstance) {
98
+ // this refers to callee
99
+ await this.deduplicated(id, deduppedInstance);
100
+ }
101
+ };
102
+
103
+ module.exports = InstanceDeduplicate;
@@ -0,0 +1,47 @@
1
+ const { getStudyUIDPathAndSubPath } = require('@radicalimaging/static-wado-util');
2
+
3
+ const path = require('path');
4
+
5
+ function ScanStudy(options) {
6
+ const { directoryName, deduplicatedRoot, deduplicatedInstancesRoot, hashStudyUidPath } = options;
7
+
8
+ return function scanStudy(studyInstanceUid) {
9
+ const { path: hashPath = '', subpath: hashSubpath = '' } = hashStudyUidPath
10
+ ? getStudyUIDPathAndSubPath(studyInstanceUid)
11
+ : {};
12
+
13
+ console.verbose('scanStudy', studyInstanceUid);
14
+
15
+ if (hashPath && hashSubpath) {
16
+ console.verbose('hashPath', hashPath);
17
+ console.verbose('hashSubpath', hashSubpath);
18
+ }
19
+
20
+ const studySubDir = hashStudyUidPath
21
+ ? path.join(hashPath, hashSubpath, studyInstanceUid)
22
+ : studyInstanceUid;
23
+
24
+ const studyPath = path.join(directoryName, 'studies', studySubDir);
25
+ const deduplicatedInstancesPath = path.join(
26
+ deduplicatedInstancesRoot,
27
+ // studyInstanceUid,
28
+ studySubDir
29
+ );
30
+ const deduplicatedPath = path.join(deduplicatedRoot, studySubDir);
31
+ console.verbose(
32
+ 'Importing',
33
+ studyInstanceUid,
34
+ studyPath,
35
+ deduplicatedInstancesPath,
36
+ deduplicatedPath
37
+ );
38
+ return this.completeStudy.getCurrentStudyData(this, {
39
+ studyPath,
40
+ deduplicatedPath,
41
+ deduplicatedInstancesPath,
42
+ studyInstanceUid,
43
+ });
44
+ };
45
+ }
46
+
47
+ module.exports = ScanStudy;