@radicalimaging/static-wado-creator 0.6.4

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 (49) hide show
  1. package/README.md +285 -0
  2. package/bin/mkdicomweb.js +9 -0
  3. package/lib/DeleteStudy.js +14 -0
  4. package/lib/RejectInstance.js +17 -0
  5. package/lib/StaticWado.js +206 -0
  6. package/lib/createMain.js +14 -0
  7. package/lib/deleteMain.js +12 -0
  8. package/lib/dictionary/Tags.js +111 -0
  9. package/lib/dictionary/dataDictionary.js +22268 -0
  10. package/lib/groupMain.js +14 -0
  11. package/lib/index.js +8 -0
  12. package/lib/instanceMain.js +13 -0
  13. package/lib/metadataMain.js +14 -0
  14. package/lib/mkdicomwebConfig.js +149 -0
  15. package/lib/model/TagLists.js +151 -0
  16. package/lib/model/uids.js +42 -0
  17. package/lib/operation/InstanceDeduplicate.js +100 -0
  18. package/lib/operation/ScanStudy.js +20 -0
  19. package/lib/operation/StudyData.js +416 -0
  20. package/lib/operation/ThumbnailService.js +184 -0
  21. package/lib/operation/adapter/asyncIterableToBuffer.js +112 -0
  22. package/lib/operation/adapter/decodeImage.js +9 -0
  23. package/lib/operation/adapter/getImageInfo.js +21 -0
  24. package/lib/operation/adapter/transcodeImage.js +303 -0
  25. package/lib/operation/extractImageFrames.js +41 -0
  26. package/lib/operation/getDataSet.js +60 -0
  27. package/lib/operation/getEncapsulatedImageFrame.js +23 -0
  28. package/lib/operation/getNumberOfFrames.js +13 -0
  29. package/lib/operation/getUncompressedImageFrame.js +7 -0
  30. package/lib/operation/getVR.js +16 -0
  31. package/lib/operation/getValue.js +193 -0
  32. package/lib/program/index.js +15 -0
  33. package/lib/rejectMain.js +12 -0
  34. package/lib/util/IdCreator.js +25 -0
  35. package/lib/util/adaptProgramOpts.js +58 -0
  36. package/lib/util/compressionOptionParser.js +13 -0
  37. package/lib/writer/CompleteStudyWriter.js +88 -0
  38. package/lib/writer/DeduplicateWriter.js +32 -0
  39. package/lib/writer/ExpandUriPath.js +15 -0
  40. package/lib/writer/HashDataWriter.js +59 -0
  41. package/lib/writer/ImageFrameWriter.js +31 -0
  42. package/lib/writer/JSONWriter.js +18 -0
  43. package/lib/writer/MultipartAttribute.js +8 -0
  44. package/lib/writer/MultipartHeader.js +14 -0
  45. package/lib/writer/ThumbnailWriter.js +21 -0
  46. package/lib/writer/VideoWriter.js +52 -0
  47. package/lib/writer/WriteMultipart.js +22 -0
  48. package/lib/writer/WriteStream.js +51 -0
  49. package/package.json +75 -0
package/README.md ADDED
@@ -0,0 +1,285 @@
1
+ # `@radicalimaging/static-wado-creator`
2
+
3
+ # static-wado-js
4
+
5
+ Status: Beta - in development (as of Oct 25, 2021)
6
+
7
+ The scope of this project is to convert DICOM P10 into a format into a DICOMweb compatible format - specifically JSON metadata, frames and bulkdata. This library will enable the following:
8
+ * Building of DICOMweb compliant services
9
+ * Ability to stream the data for "on the fly" use cases
10
+ * Ability to pregenerate DICOMweb responses and store them so they can be served up with a standard HTTP server to implement a subset of DICOMweb WADO-RS, specifically:
11
+ * Retrieve instance metadata
12
+ * Retrieve series level metadata
13
+ * Retrieve frames in an instance
14
+ * Retrieve bulk data
15
+ * Retrieve study, series or instance query
16
+ * To explore an alternative archive format that is web friendly (no need for a DICOM parser)
17
+ * Should be able to recreate DICOM P10 file from this data (semantic equivalence or possibly bit for bit equivalent)
18
+ * Will make updates more efficient (e.g. updating patient name just requires updating metadata)
19
+ * More efficient image access (no need to scan through DICOM P10 to access frames, or store the offsets of each frame separately and seek to them)
20
+
21
+ The scope of this library is limited to:
22
+ * Taking one or more DICOM files as input, writing Bulkdata and Pixeldata
23
+ * Creating either deduplicated representations or instance level metadata
24
+ * Combining deduplicated representations into sets of deduplicated representations
25
+ * Using the deduplicated representations to generate study/series/instance query results and series metadata
26
+
27
+ View the [design rationale](docs/design.md) for more information on the scope
28
+
29
+ View [frequently asked questions](docs/faq.md)
30
+
31
+ View [specification](docs/spec.md)
32
+
33
+ ## Pre-requisites
34
+ View root pre-requisites section [pre-requisites](../../README.md#pre-requisites)
35
+
36
+ ## Development
37
+ View root development section [development](../../README.md#development)
38
+
39
+ ## Running the CLI
40
+ Install the CLI with either npm install -g . or npm install -g @radicalimaging/static-wado-js
41
+
42
+ ## Usage
43
+ Once you install the package the executables (i.e static-wado commands) are installed into to your PATH system, then you can use anywhere on your command line program.
44
+
45
+ ### Api
46
+ There are 5 different commands: mkdicomweb, mkdicomwebinstances, mkdicomwebstudy, mkdicomwebdeduplicated, mkdicomwebdeduplicatedgroup that can be used (see usage bellow). It can be run as:
47
+
48
+ ```
49
+ commnadName [options] [<input...>]
50
+ ```
51
+
52
+ Arguments are:
53
+
54
+ input
55
+ : List of directories/files separated by space.
56
+ Required for commands: **mkdicomweb**, **mkdicomwebdeduplicated**, **mkdicomwebinstances**. Optional for the others. This argument should be placed at end of command invoking.
57
+
58
+ The options are:
59
+
60
+ -c, --clean
61
+ : Clean the study output directory for these instances (default: false)
62
+
63
+ -C, --remove-deduplicated-instances
64
+ : Remove single instance deduplicated files after writing group files (default: false)
65
+
66
+ -d, --deduplicate
67
+ : Write deduplicate instance level data (default: false)
68
+
69
+ -g, --group
70
+ : Write combined deduplicate data (default: true)
71
+
72
+ -i, --instances
73
+ : Write instance metadata (default: false)
74
+
75
+ -m, --maximum-inline-public-length <value>
76
+ : Maximum length of public binary data (default: 131074)
77
+
78
+ -M, --maximum-inline-private-length <value>
79
+ : Maximum length of private binary data (default: 64)
80
+
81
+ --no-recompress
82
+ : Force no recompression
83
+ [see](#to-recompress)
84
+
85
+ --no-recompress-thumb
86
+ : Force no recompression for thumbnail
87
+ [see](#to-recompress-thumb)
88
+
89
+ -o, --dir <value>
90
+ : Set output directory (default: "~/dicomweb")
91
+
92
+ --path-deduplicated <path>
93
+ : Set the deduplicate data directory path (relative to dir) (default: "deduplicated")
94
+
95
+ --path-instances <path>
96
+ : Set the instances directory path (relative to dir) (default: "instances")
97
+
98
+ -r, --recompress <listvalue...>
99
+ : List of types to recompress separated by space (choices: "uncompressed", "jp2", "jpeglossless", "rle", default: "uncompressed jp2")
100
+ [see](#to-recompress)
101
+
102
+ --recompress-thumb <listvalue...>
103
+ : List of types to recompress thumb separated by space (choices: "uncompressed", "jp2", "jpeglossless", "rle", default: "uncompressed jp2")
104
+ [see](#to-recompress-thumb)
105
+
106
+ -s, --study
107
+ : Write study metadata - on provided instances only (TO FIX), (default: true)
108
+
109
+ -t, --content-type <type>
110
+ : Destination type to compress to (choices: "jls", "lei", "jls-lossy" or DICOM Transfer Syntax UID - default: "jls")
111
+
112
+ -T, --colour-content-type <type>
113
+ : Colour content type (default: null)
114
+
115
+ -v, --verbose
116
+ : Write verbose output (default: false)
117
+
118
+ -V, --version
119
+ : output the version number
120
+
121
+ ### Help command
122
+ You can output help information by:
123
+ ```
124
+ commandName help
125
+ ```
126
+
127
+ ```
128
+ commandName -h
129
+ ```
130
+
131
+ ```
132
+ commandName --help
133
+ ```
134
+
135
+ ### To create instances
136
+ Run the tool:
137
+ ```
138
+ mkdicomwebinstances <directoryOfP10Files>
139
+ ```
140
+
141
+ ### To create a full DICOMweb output structure
142
+ Run the tool:
143
+ ```
144
+ mkdicomweb <directoryOfP10Files>
145
+ ```
146
+
147
+ ### To run separated stages
148
+ The mkdicomweb tool runs the three stages all together, on just the studies references. This can instead be done on separate files by running:
149
+ ```
150
+ mkdicomwebdeduplicated <directoryOfP10Files>
151
+ mkdicomwebdeduplicatedgroup
152
+ mkdicomwebstudy
153
+ ```
154
+ which creates a full study directory. The first stage writes to ~/dicomweb/instances/<studyUID>/ data about each instance as it is read in. The second stage then groups these files for more efficient compression into the ~/dicomweb/deduplicated/<studyUID>/ The last stage then creates the actual DICOMweb files.
155
+
156
+ There is currently no notification of what studies have been updated between stages. The intent is to write notifications to ~/dicomweb/notifications/<studyUID> of what operations need to be applied/updated.
157
+
158
+ ### To Serve Instances As a Web Server
159
+ ```
160
+ cd ~/dicomweb
161
+ npx http-server -p 5000 -g --cors
162
+ ```
163
+
164
+ The -g option serves up compressed files ending in .gz as compressed http streams.
165
+
166
+ ### To Create DICOM part 10 from DICOMweb files
167
+ TODO
168
+
169
+ Run the tool mkdicomwebpart10 on the studyUID, and optionally on the series/instance UID's of interest to generate a local set of part 10 files.
170
+
171
+ ### To Update DICOM Metadata
172
+ TODO
173
+
174
+ Run the tool
175
+ ```
176
+ mkdicomwebupdate -<delete/anonymize/patient/study/series/instance> <studyInstanceUID> (tag=newValue)*
177
+ ```
178
+ to delete the given item or to update the specified attribute contained in the given level. Multiple mkdicomwebupdate commands may be run to perform updates on different attribute sets, or they may be grouped into a single file for bulk application.
179
+
180
+ ### To recompress
181
+ It allows commands to recompress data/metadata prior writing it to local. Currently, if recompress is active the designed data types will be transcoded [see](#recompress-aliases-and-input-transfer-syntaxes).
182
+
183
+ By default the recompression occurs for incoming types: uncompressed, jp2
184
+ ```
185
+ mkdicomweb ./folderName
186
+ ```
187
+ It will recompress any existing data whose transfer syntaxes are 1.2.840.10008.1.2.4.90, 1.2.840.10008.1.2.4.91, 1.2.840.10008.1.2, 1.2.840.10008.1.2.1 and 1.2.840.10008.1.2.2.
188
+
189
+ Define incoming types for recompression of: uncompressed,jp2,rle
190
+ ```
191
+ mkdicomweb -r uncompressed jp2 rle ./folderName
192
+ ```
193
+ It will recompress any existing data whose transfer syntaxes: 1.2.840.10008.1.2.4.90, 1.2.840.10008.1.2.4.91, 1.2.840.10008.1.2, 1.2.840.10008.1.2.1, 1.2.840.10008.1.2.2 and 1.2.840.10008.1.2.5 to transfer syntax 1.2.840.10008.1.2.4.80.
194
+
195
+ By default the destination compression type for recompression: jls
196
+ ```
197
+ mkdicomweb ./folderName
198
+ ```
199
+ It will recompress any existing data whose transfer syntaxes are 1.2.840.10008.1.2.4.90, 1.2.840.10008.1.2.4.91, 1.2.840.10008.1.2, 1.2.840.10008.1.2.1 and 1.2.840.10008.1.2.2 to jls (transfer syntax 1.2.840.10008.1.2.4.80)
200
+
201
+ Define destination type for uncompressed encoding: lei
202
+ ```
203
+ mkdicomweb -t lei ./folderName
204
+ ```
205
+ It will decode any existing data whose transfer syntaxes are 1.2.840.10008.1.2.4.90, 1.2.840.10008.1.2.4.91, 1.2.840.10008.1.2, 1.2.840.10008.1.2.1 and 1.2.840.10008.1.2.2 and store uncompressed as LEI (transfer syntax 1.2.840.10008.1.2.1)
206
+
207
+ Force no recompression
208
+ ```
209
+ mkdicomweb -r uncompressed jp2 rle --no-recompress ./folderName
210
+ ```
211
+ It will NOT recompress any existing data but instead it will keep them in the original encoding.
212
+
213
+ Force no recompression
214
+ ```
215
+ mkdicomweb --no-recompress ./folderName
216
+ ```
217
+ It will NOT recompress any existing data but instead it will keep them in the original encoding.
218
+
219
+ Obs: --no-recompress forces no compression at all. Use it if you want to disable even the default behavior.
220
+
221
+
222
+ ### Recompress aliases and input transfer syntaxes
223
+ The table below shows the support for transfer syntax recompression(i.e use "... -r jp2" if you want to recompress from 1.2.840.10008.1.2.4.90 to 1.2.840.10008.1.2.4.80)
224
+
225
+ | TransferSyntaxUID | Name | Recompress alias | Target TransferSyntaxUID |
226
+ |------------------------ |--------------------------------------------------------- |------------------ |-------------------------- |
227
+ | 1.2.840.10008.1.2 | Implicit VR Endian | uncompressed | 1.2.840.10008.1.2 |
228
+ | 1.2.840.10008.1.2.1 | Explicit VR Little Endian | uncompressed | 1.2.840.10008.1.2.1 |
229
+ | 1.2.840.10008.1.2.2 | Explicit VR Big Endian | uncompressed | 1.2.840.10008.1.2.1 |
230
+ | 1.2.840.10008.1.2.4.57 | JPEG Lossless, Nonhierarchical (Processes 14) | jpeglossless | 1.2.840.10008.1.2.4.80 |
231
+ | 1.2.840.10008.1.2.4.70 | JPEG Lossless, Nonhierarchical, First- Order Prediction | jpeglossless | 1.2.840.10008.1.2.4.80 |
232
+ | 1.2.840.10008.1.2.4.90 | JPEG 2000 Image Compression (Lossless Only) | jp2 | 1.2.840.10008.1.2.4.80 |
233
+ | 1.2.840.10008.1.2.4.91 | JPEG 2000 Image Compression | jp2 | 1.2.840.10008.1.2.4.80 |
234
+ | 1.2.840.10008.1.2.5 | RLE Lossless | rle | 1.2.840.10008.1.2.4.80 |
235
+
236
+
237
+ ### To recompress thumb
238
+ It tells thumbnail creation to use recompressed data or not.
239
+ The list of types MUST be necessarily a subset of recompress list.
240
+
241
+ Obs: --no-recompress-thumb forces to not use compression at all. Use it if you want to disable even the default behavior.
242
+
243
+ By default the recompression occurs for incoming types: uncompressed, jp2. This will recompress the types mentioned and will generate the thumbnails with the result of data recompression.
244
+
245
+ ```
246
+ mkdicomweb ./folderName
247
+ ```
248
+
249
+ Force recompression thumb. This will recompress the types: uncompressed jp2 rle, thumbnails creation will use original data for all types except jp2 (which will use recompress data).
250
+ ```
251
+ mkdicomweb -r uncompressed jp2 rle --recompress-thumb jp2 ./folderName
252
+ ```
253
+
254
+ Skipping not recompressed data. This will recompress the types: uncompressed jp2, thumbnails creation will use original data for all types (since recompress-thumb list does not intersect with recompress list).
255
+ ```
256
+ mkdicomweb -r uncompressed jp2 --recompress-thumb rle ./folderName
257
+ ```
258
+
259
+ Force no recompression thumb. This will recompress the types: uncompressed jp2 rle, but for thumbnails creation will use original data.
260
+ ```
261
+ mkdicomweb -r uncompressed jp2 rle --no-recompress-thumb ./folderName
262
+ ```
263
+
264
+
265
+
266
+
267
+ ## Development
268
+
269
+ ## TODO (Looking for help here!!)
270
+
271
+ * Create docker container to encapsulate build environment
272
+ * Create test data (DICOM P10 and expected DICOMweb results) - Jordan working on this
273
+ * Document API
274
+ * Create CI + Publish to NPM
275
+ * Fix Bugs
276
+ * Get bulkdata refs written properly
277
+ * Enhance cli
278
+ * Add support for specifying bulkDataMinSize
279
+ * Add support for writing out DICOM P10 file
280
+ * Enhance library
281
+ * Write out "info" file
282
+ * P10 Header
283
+ * Data needed to recreate original P10 instance
284
+ * Create DICOMweb -> DICOM P10 tool
285
+
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { mkdicomwebConfig } = require("../lib");
4
+ const { configureProgram } = require("../lib/program");
5
+
6
+ // Configure program commander
7
+ configureProgram(mkdicomwebConfig).then(() => {
8
+ console.log("done");
9
+ });
@@ -0,0 +1,14 @@
1
+ const { JSONReader } = require("@radicalimaging/static-wado-util");
2
+ const JSONWriter = require("./writer/JSONWriter");
3
+ const Tags = require("./dictionary/Tags");
4
+
5
+ module.exports = (options) =>
6
+ async function (studyInstanceUid) {
7
+ console.log("Delete Study", studyInstanceUid);
8
+ const studyData = await this.scanStudy("studies", studyInstanceUid);
9
+ studyData.delete();
10
+ const allStudies = await JSONReader(options.directoryName, "studies/index.json.gz", []);
11
+ const studiesWithoutDeleted = allStudies.filter((study) => studyInstanceUid != Tags.getValue(study,Tags.StudyInstanceUID));
12
+ await JSONWriter(options.directoryName, "studies", studiesWithoutDeleted);
13
+ delete this.studyData;
14
+ };
@@ -0,0 +1,17 @@
1
+ const dataExtractor = /^(.*studies)?[\\/]?([^/\\]+)[\\/](series[\\/])?([^/\\]+)([\\/]instances[\\/]([^/\\]*))?$/;
2
+
3
+ module.exports = (options) =>
4
+ async function (rejectPath, reason) {
5
+ console.log("Reject instance", rejectPath, reason);
6
+ const extracted = rejectPath.match(dataExtractor);
7
+ if (!extracted) {
8
+ console.log("Unable to reject", rejectPath, "because it isn't in the format studies/<studyUID>/series/<seriesUID>");
9
+ return;
10
+ }
11
+ const [, , studyInstanceUid, , seriesInstanceUid, , sopInstanceUid] = extracted;
12
+ const studyData = await this.scanStudy("studies", studyInstanceUid);
13
+ if (options.verbose) console.log("Trying to reject", seriesInstanceUid, "because", reason);
14
+ await studyData.reject(seriesInstanceUid, sopInstanceUid, reason);
15
+ await studyData.writeDeduplicatedGroup();
16
+ return studyData.writeMetadata();
17
+ };
@@ -0,0 +1,206 @@
1
+ const dicomCodec = require("@cornerstonejs/dicom-codec");
2
+ const { Stats, handleHomeRelative } = require("@radicalimaging/static-wado-util");
3
+ const dicomParser = require("dicom-parser");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const { dirScanner } = require("@radicalimaging/static-wado-util");
7
+ const asyncIterableToBuffer = require("./operation/adapter/asyncIterableToBuffer");
8
+ const getDataSet = require("./operation/getDataSet");
9
+ const InstanceDeduplicate = require("./operation/InstanceDeduplicate");
10
+ const DeduplicateWriter = require("./writer/DeduplicateWriter");
11
+ const ImageFrameWriter = require("./writer/ImageFrameWriter");
12
+ const CompleteStudyWriter = require("./writer/CompleteStudyWriter");
13
+ const IdCreator = require("./util/IdCreator");
14
+ const ScanStudy = require("./operation/ScanStudy");
15
+ const HashDataWriter = require("./writer/HashDataWriter");
16
+ const VideoWriter = require("./writer/VideoWriter");
17
+ const { transcodeImageFrame, transcodeId, transcodeMetadata } = require("./operation/adapter/transcodeImage");
18
+ const ThumbnailWriter = require("./writer/ThumbnailWriter");
19
+ const ThumbnailService = require("./operation/ThumbnailService");
20
+ const DeleteStudy = require("./DeleteStudy");
21
+ const RejectInstance = require("./RejectInstance");
22
+
23
+ function setStudyData(studyData) {
24
+ this.studyData = studyData;
25
+ }
26
+
27
+ class StaticWado {
28
+ constructor(configuration) {
29
+ const { rootDir = "~/dicomweb", pathDeduplicated = "deduplicated", pathInstances = "instances", verbose } = configuration;
30
+
31
+ dicomCodec.setConfig({ verbose });
32
+ const directoryName = handleHomeRelative(rootDir);
33
+
34
+ this.options = {
35
+ ...configuration,
36
+ directoryName,
37
+ deduplicatedRoot: path.join(directoryName, pathDeduplicated),
38
+ deduplicatedInstancesRoot: path.join(directoryName, pathInstances),
39
+ TransferSyntaxUID: "1.2.840.10008.1.2",
40
+ };
41
+ this.callback = {
42
+ uids: IdCreator(this.options),
43
+ bulkdata: HashDataWriter(this.options),
44
+ imageFrame: ImageFrameWriter(this.options),
45
+ videoWriter: VideoWriter(this.options),
46
+ thumbWriter: ThumbnailWriter(this.options),
47
+ completeStudy: CompleteStudyWriter(this.options),
48
+ metadata: InstanceDeduplicate(this.options),
49
+ deduplicated: DeduplicateWriter(this.options),
50
+ scanStudy: ScanStudy(this.options),
51
+ reject: RejectInstance(this.options),
52
+ delete: DeleteStudy(this.options),
53
+ setStudyData,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Rejects the given item, specified as studies/<studyUID>/series/<seriesUID>
59
+ * Does NOT remove any files, but adds a flag to the study indicating the deleted series and instances.
60
+ *
61
+ * @param {string} args listing the instances to remove (may remove entire series)
62
+ */
63
+ async reject(args) {
64
+ args.forEach((removal) => {
65
+ this.callback.reject(removal);
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Deletes a study, as specified by the study instance UID by removing instance, deduplicated and studies files,
71
+ * as well as removing the object from the studies list.
72
+ *
73
+ * @param {string[]} args listing the studies to remove
74
+ */
75
+ async delete(args) {
76
+ args.forEach((studyUID) => this.callback.delete(studyUID));
77
+ }
78
+
79
+ /**
80
+ * Processes a set of DICOM files, where the starting point is a list of directory names or file instances.
81
+ * This is used for importing DICOM files.
82
+ *
83
+ * @param {*} files
84
+ * @param {*} callback
85
+ * @param {*} params
86
+ */
87
+ async processFiles(files, params) {
88
+ return dirScanner(files, {
89
+ ...params,
90
+ callback: async (file) => {
91
+ try {
92
+ const dicomp10stream = fs.createReadStream(file);
93
+ await this.importBinaryDicom(dicomp10stream, { ...params, file });
94
+ Stats.StudyStats.add("DICOM P10", "Parse DICOM P10 file");
95
+ } catch (e) {
96
+ console.error("Couldn't process", file, e);
97
+ }
98
+ },
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Processes a study directory, matching up study instance UIDs. Either processes the
104
+ * deduplicated group directory or the instances directory, or the notifications directory.
105
+ * @param {*} params
106
+ */
107
+ async processStudyDir(studyUids, params) {
108
+ return dirScanner(params[params.scanStudies], {
109
+ ...params,
110
+ recursive: false,
111
+ callback: (dir, name) => this.callback.scanStudy(dir, name),
112
+ });
113
+ }
114
+
115
+ async importBinaryDicom(dicomp10stream, params) {
116
+ // Read dicomp10 stream into buffer
117
+ const buffer = await asyncIterableToBuffer(dicomp10stream);
118
+
119
+ // Parse it
120
+ const dataSet = dicomParser.parseDicom(buffer, params);
121
+
122
+ const studyInstanceUid = dataSet.string("x0020000d");
123
+
124
+ if (!studyInstanceUid) {
125
+ console.log(`Can't import file ${params.file}`);
126
+ return undefined;
127
+ }
128
+
129
+ // Extract uids
130
+ const id = this.callback.uids(
131
+ {
132
+ studyInstanceUid,
133
+ seriesInstanceUid: dataSet.string("x0020000e"),
134
+ sopInstanceUid: dataSet.string("x00080018"),
135
+ transferSyntaxUid: dataSet.string("x00020010"),
136
+ },
137
+ params.file
138
+ );
139
+
140
+ const targetId = transcodeId(id, this.options);
141
+
142
+ let bulkDataIndex = 0;
143
+ let imageFrameIndex = 0;
144
+ const thumbnailService = new ThumbnailService();
145
+
146
+ const generator = {
147
+ bulkdata: async (bulkData, options) => {
148
+ const _bulkDataIndex = bulkDataIndex;
149
+ bulkDataIndex += 1;
150
+ return this.callback.bulkdata(targetId, _bulkDataIndex, bulkData, options);
151
+ },
152
+ imageFrame: async (originalImageFrame) => {
153
+ const { imageFrame: transcodedImageFrame, id: transcodedId } = await transcodeImageFrame(id, targetId, originalImageFrame, dataSet, this.options);
154
+
155
+ const currentImageFrameIndex = imageFrameIndex;
156
+ imageFrameIndex += 1;
157
+
158
+ thumbnailService.queueThumbnail(
159
+ {
160
+ imageFrame: originalImageFrame,
161
+ transcodedImageFrame,
162
+ transcodedId,
163
+ id,
164
+ frameIndex: currentImageFrameIndex,
165
+ },
166
+ this.options
167
+ );
168
+
169
+ return this.callback.imageFrame(transcodedId, currentImageFrameIndex, transcodedImageFrame);
170
+ },
171
+ videoWriter: async (_dataSet) => this.callback.videoWriter(id, _dataSet),
172
+ };
173
+
174
+ // convert to DICOMweb MetaData and BulkData
175
+ const result = await getDataSet(dataSet, generator, this.options);
176
+
177
+ const transcodedMeta = transcodeMetadata(result.metadata, id, this.options);
178
+ thumbnailService.generateThumbnails(id, dataSet, transcodedMeta, this.callback);
179
+
180
+ await this.callback.metadata(targetId, transcodedMeta);
181
+
182
+ // resolve promise with statistics
183
+ return {};
184
+ }
185
+
186
+ /**
187
+ * The mkdicomweb command first runs mkdicomwebinstances, writing out the deduplicated data, and then runs the
188
+ * mkdicomwebstudy command, creating the deduplicated data set. This version, however, keeps the deduplicated
189
+ * data in memory by default on a study level, which avoids needing to run the load process.
190
+ */
191
+ async executeCommand(input) {
192
+ if (this.options.scanStudies) {
193
+ // Scan one of the study directories - in this case, files is a set of study directories
194
+ await this.processStudyDir(input, this.options);
195
+ } else {
196
+ await this.processFiles(input, this.options);
197
+ }
198
+ await this.close();
199
+ }
200
+
201
+ async close() {
202
+ await this.callback.completeStudy();
203
+ Stats.OverallStats.summarize("Completed Study Processing");
204
+ }
205
+ }
206
+ module.exports = StaticWado;
@@ -0,0 +1,14 @@
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
+ isDeduplicate: true,
10
+ isStudyData: true,
11
+ });
12
+ const importer = new StaticWado(finalOptions);
13
+ return importer.executeCommand(program.args);
14
+ };
@@ -0,0 +1,12 @@
1
+ const StaticWado = require("./StaticWado");
2
+ const adaptProgramOpts = require("./util/adaptProgramOpts");
3
+
4
+ module.exports = function deleteMain(options, program) {
5
+ const finalOptions = adaptProgramOpts(options, {
6
+ ...this,
7
+ isGroup: true,
8
+ isStudyData: true,
9
+ });
10
+ const importer = new StaticWado(finalOptions);
11
+ return importer.delete(program.args);
12
+ };
@@ -0,0 +1,111 @@
1
+ const dataDictionary = require("./dataDictionary");
2
+ const dcmjs = require("dcmjs");
3
+
4
+ const { naturalizeDataset, denaturalizeDataset} = dcmjs.data.DicomMetaDictionary
5
+
6
+ /** Find the actual tag for a private value */
7
+ const findPrivate = (item, tagObject, create) => {
8
+ if( typeof tagObject === "string" ) return tagObject;
9
+ if( typeof tagObject === "number" ) return `00000000${tagObject.toString(16)}`.slice(-8);
10
+ const {creator,tag} = tagObject;
11
+ if( !creator ) return tag;
12
+ const start = tag.substring(0,4);
13
+ const end = tag.substring(6,8);
14
+ // Should technically go all the way up, but for now only test 0x30 items
15
+ let assignPosition;
16
+ for(let offset = 0x10; offset < 0x40; offset++) {
17
+ const testTag = `${start}00${offset.toString(16)}`
18
+ const testCreator = item[testTag];
19
+ if( testCreator===undefined && assignPosition===undefined ) assignPosition = offset;
20
+ if( testCreator && testCreator.Value && testCreator.Value[0]===creator ) {
21
+ return `${start}${offset.toString(16)}${end}`
22
+ }
23
+ }
24
+ if( create ) {
25
+ if( !assignPosition ) throw new Error(`Couldn't find any assign positions for ${creator} ${tag} in ${item}`);
26
+ const creatorTag = `${start}00${assignPosition.toString(16)}`
27
+ item[creatorTag] = { Value: [creator], vr: 'CS' };
28
+ return `${start}${assignPosition.toString(16)}${end}`
29
+ }
30
+ };
31
+
32
+ const DeduppedCreator = "dedupped";
33
+
34
+ const Tags = {
35
+ // Raw tags have the x before them, not parsed yet
36
+ RawMinTag: "x00000000",
37
+ RawFirstBodyTag: "x00080000",
38
+ RawTransferSyntaxUID: "x00020010",
39
+
40
+ // This one isn't defined in the dataDictionary
41
+ AvailableTransferSyntaxUID: "00083002",
42
+
43
+ DeduppedCreator,
44
+
45
+ // The references to extract data included in this object, 1..n values
46
+ DeduppedRef: { creator: DeduppedCreator, tag: "00091010"},
47
+
48
+ // The hash value of THIS object
49
+ DeduppedHash: {creator: DeduppedCreator, tag: "00091011"},
50
+
51
+ // Type of hash instance
52
+ DeduppedType: {creator: DeduppedCreator, tag: "00091012"},
53
+ InstanceType: "instance",
54
+ InfoType: "info",
55
+
56
+ naturalizeDataset, denaturalizeDataset,
57
+
58
+ setValue: (item, tag, value) => {
59
+ const actualTag = findPrivate(item,tag,true);
60
+ item[actualTag] = { Value: Array.isArray(value) ? value : [value] };
61
+ return actualTag;
62
+ },
63
+
64
+ setList: (item, tag, value) => {
65
+ const actualTag = findPrivate(item,tag,true);
66
+ item[actualTag] = {Value: Array.isArray(value) ? value : [value]};
67
+ return actualTag;
68
+ },
69
+
70
+ pushList: (item, tag, value) => {
71
+ const actualTag = findPrivate(item,tag,true);
72
+ if( !item[actualTag] ) item[actualTag] = { Value: [] };
73
+ item[actualTag].Value.push(value);
74
+ return actualTag;
75
+ },
76
+
77
+ getValue: (item, tag) => {
78
+ const actualTag = findPrivate(item,tag);
79
+ if( !actualTag ) return undefined;
80
+ const value = item[actualTag];
81
+ if( value===undefined ) return undefined;
82
+ if( Array.isArray(value) ) return value.length==1 ? value[0] : (value.length===0 ? "" : value);
83
+ if( value.Value ) return value.Value.length==1 ? value.Value[0] : (value.Value.length===0 ? "" : value.Value);
84
+ return value;
85
+ },
86
+
87
+ getList: (item, tag) => {
88
+ const actualTag = findPrivate(item,tag);
89
+ if( !actualTag ) return undefined;
90
+ const value = item[actualTag];
91
+ if( value===undefined ) return undefined;
92
+ if( Array.isArray(value) ) return value;
93
+ if( value.Value ) return value.Value;
94
+ return [value];
95
+ },
96
+
97
+ removeValue: (item,tag) => {
98
+ const actualTag = findPrivate(item,tag);
99
+ if( actualTag ) delete item[actualTag];
100
+ // TODO, remove creator tag if not used any longer
101
+ },
102
+
103
+ findPrivate,
104
+ };
105
+
106
+ Object.keys(dataDictionary).forEach((key) => {
107
+ const value = dataDictionary[key];
108
+ Tags[value.name] = key;
109
+ });
110
+
111
+ module.exports = Tags;