@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
@@ -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
+ isStudyData: false,
10
+ });
11
+ const importer = new StaticWado(finalOptions);
12
+ // Either do a scan or on specified studies
13
+ return importer.executeCommand(program.args);
14
+ };
package/lib/index.js ADDED
@@ -0,0 +1,8 @@
1
+ const mkdicomwebConfig = require("./mkdicomwebConfig");
2
+ const StaticWado = require("./StaticWado");
3
+
4
+ StaticWado.mkdicomwebConfig = mkdicomwebConfig;
5
+ StaticWado.createMain = require("./createMain");
6
+ StaticWado.deleteMain = require("./deleteMain");
7
+
8
+ module.exports = StaticWado;
@@ -0,0 +1,13 @@
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
+ isGroup: false,
9
+ isStudyData: false,
10
+ });
11
+ const importer = new StaticWado(finalOptions);
12
+ return importer.executeCommand(program.args);
13
+ };
@@ -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: false,
9
+ isStudyData: false,
10
+ });
11
+ const importer = new StaticWado(finalOptions);
12
+ // Either do a scan or on specified studies
13
+ return importer.executeCommand(program.args);
14
+ };
@@ -0,0 +1,149 @@
1
+ const ConfigPoint = require("config-point");
2
+ const { staticWadoConfig } = require("@radicalimaging/static-wado-util");
3
+ const createMain = require("./createMain");
4
+ const deleteMain = require("./deleteMain");
5
+ const rejectMain = require("./rejectMain");
6
+ const instanceMain = require("./instanceMain");
7
+ const groupMain = require("./groupMain");
8
+ const metadataMain = require("./metadataMain");
9
+ const compressionOptionParser = require("./util/compressionOptionParser");
10
+
11
+ /**
12
+ * Defines the basic configuration values for the dicomwebserver component. See the README for more details.
13
+ * TODO - fix the default values so they come from the configuration file. In the meantime, leave the defaults blank.
14
+ */
15
+ const { mkdicomwebConfig } = ConfigPoint.register({
16
+ mkdicomwebConfig: {
17
+ // This declare the inheritted configuration, don't assume this is directly accessible
18
+ configBase: staticWadoConfig,
19
+ isStudyData: true,
20
+ isGroup: true,
21
+ options: [
22
+ {
23
+ key: "-c, --clean",
24
+ description: "Clean the outputs before generating/starting to write new values.",
25
+ defaultValue: false,
26
+ },
27
+ {
28
+ key: "-v, --verbose",
29
+ description: "Write verbose output",
30
+ defaultValue: false,
31
+ },
32
+ {
33
+ key: "-t, --content-type <type>",
34
+ description: 'Destination type to compress to (choices: "jls", "lei", "jls-lossy" or DICOM Transfer Syntax UID - default: "jls")',
35
+ defaultValue: "jls",
36
+ customParser: compressionOptionParser,
37
+ },
38
+ {
39
+ key: "-m, --maximum-inline-public-length <value>",
40
+ description: "Maximum length of public binary data",
41
+ defaultValue: 128 * 1024 + 2,
42
+ },
43
+ {
44
+ key: "-M, --maximum-inline-private-length <value>",
45
+ description: "Maximum length of private binary data",
46
+ defaultValue: 64,
47
+ },
48
+ {
49
+ key: "-r, --recompress <listvalue...>",
50
+ description: "List of types to recompress separated by space",
51
+ defaultValue: ["uncompressed", "jp2"],
52
+ choices: ["uncompressed", "jp2", "jpeglossless", "rle", "none"],
53
+ },
54
+ {
55
+ key: "--recompress-thumb <listvalue...>",
56
+ description: "List of types to recompress thumb separated by space",
57
+ defaultValue: ["uncompressed", "jp2"],
58
+ choices: ["uncompressed", "jp2", "jpeglossless", "rle", "none"],
59
+ },
60
+ {
61
+ key: "--no-recompress",
62
+ description: "Force no recompression",
63
+ defaultValue: false,
64
+ },
65
+ {
66
+ key: "--no-recompress-thumb",
67
+ description: "Force no recompression thumbnail",
68
+ defaultValue: false,
69
+ },
70
+ {
71
+ key: "-T, --colour-content-type <value>",
72
+ description: "Colour content type",
73
+ defaultValue: null,
74
+ },
75
+ {
76
+ key: "--path-deduplicated <path>",
77
+ description: "Set the deduplicate data directory path (relative to dir)",
78
+ defaultValue: "deduplicated",
79
+ },
80
+ {
81
+ key: "--path-instances <path>",
82
+ description: "Set the instances directory path (relative to dir)",
83
+ defaultValue: "instances",
84
+ },
85
+ {
86
+ key: "--store-multipart-bulk-data",
87
+ description: "Use multipart encoding for non image frame bulkdata",
88
+ defaultValue: false,
89
+ },
90
+ {
91
+ key: "--prepend-bulk-data-uri <value>",
92
+ description: "Prepend bulkdata uri (ex. to use absolute Uri like http://host:3000/dicomweb)",
93
+ defaultValue: "",
94
+ },
95
+ {
96
+ key: "--expand-bulk-data-uri",
97
+ description: "expand bulkdata relative uri to use full relative path (should also be set when using --prepend-bulk-data-uri)",
98
+ defaultValue: false,
99
+ },
100
+ {
101
+ key: "-o, --dir <value>",
102
+ description: "Set output directory",
103
+ defaultValue: { configOperation: "reference", source: "staticWadoConfig", reference: "rootDir" },
104
+ },
105
+ ],
106
+ programs: [
107
+ {
108
+ command: "create",
109
+ isDefault: true,
110
+ arguments: ["input"],
111
+ main: createMain,
112
+ helpDescription:
113
+ "Make DICOMweb query and metadata from binary Part 10 DICOM files. Does a full read of\n" +
114
+ "deduplicated files each time a study instance UID is found, and only updates those studies\n" +
115
+ "having at least one ",
116
+ },
117
+ {
118
+ command: "instance",
119
+ arguments: ["input"],
120
+ main: instanceMain,
121
+ helpDescription: "Make instance level DICOMweb metadata and bulkdata, but don't group or write series metadata",
122
+ },
123
+ {
124
+ command: "group",
125
+ arguments: ["input"],
126
+ main: groupMain,
127
+ helpDescription: "Group instance level metadata into deduplicated data.\nDeletes instance level deduplicated information once it is confirmed written.",
128
+ },
129
+ {
130
+ command: "metadata",
131
+ arguments: ["input"],
132
+ main: metadataMain,
133
+ helpDescription: "Write the metadata object (series and study details) from the deduplicated data.",
134
+ },
135
+ {
136
+ command: "delete",
137
+ main: deleteMain,
138
+ helpDescription: "Delete the given study, series or instance (not yet implemented)",
139
+ },
140
+ {
141
+ command: "reject",
142
+ main: rejectMain,
143
+ helpDescription: "Reject the specified series, specified as studies/<studyUID>/series/<seriesUID>",
144
+ },
145
+ ],
146
+ },
147
+ });
148
+
149
+ module.exports = mkdicomwebConfig;
@@ -0,0 +1,151 @@
1
+ const hashFactory = require("node-object-hash");
2
+ const Tags = require("../dictionary/Tags");
3
+
4
+ /* eslint "no-param-reassign": "off" */
5
+
6
+ const hasher = hashFactory();
7
+
8
+ const { PatientID, PatientName, IssuerOfPatientID } = Tags;
9
+ const { StudyDescription, AccessionNumber, StudyInstanceUID, StudyDate, StudyTime } = Tags;
10
+ const { SeriesDescription, SeriesNumber, SeriesInstanceUID, SeriesDate, SeriesTime } = Tags;
11
+
12
+ const { DeduppedHash, DeduppedRef, DeduppedType, } = Tags;
13
+
14
+ const { pushList, getList, setValue, getValue } = Tags;
15
+
16
+ const PatientQuery = [
17
+ PatientID,
18
+ Tags.OtherPatientIDsSequence,
19
+ PatientName,
20
+ IssuerOfPatientID,
21
+ Tags.PatientBirthDate,
22
+ Tags.PatientBirthTime,
23
+ Tags.PatientSex,
24
+ Tags.PatientIdentityRemoved,
25
+ Tags.DeidentificationMethodCodeSequence,
26
+ ];
27
+ const StudyQuery = [StudyDescription, AccessionNumber, StudyInstanceUID, StudyDate, StudyTime, Tags.StudyStatusID, Tags.StudyPriorityID, Tags.StudyID];
28
+
29
+ const PatientStudyQuery = [...PatientQuery, ...StudyQuery];
30
+
31
+ // The difference between the extracts and the query is that the query contains the parent query values
32
+ // TODO - make it into a self-cpontained object that can generate either
33
+ const SeriesExtract = [
34
+ SeriesDescription,
35
+ SeriesNumber,
36
+ SeriesInstanceUID,
37
+ Tags.Modality,
38
+ SeriesDate,
39
+ SeriesTime,
40
+ Tags.SpecificCharacterSet,
41
+ Tags.Manufacturer,
42
+ Tags.InstitutionName,
43
+ Tags.ReferringPhysicianName,
44
+ Tags.StationName,
45
+ Tags.OperatorsName,
46
+ Tags.ManufacturerModelName,
47
+ Tags.BodyPartExamined,
48
+ Tags.ProtocolName,
49
+ Tags.RequestedProcedureDescription,
50
+ ];
51
+ const SeriesQuery = [StudyInstanceUID, ...SeriesExtract];
52
+
53
+ const InstanceQuery = [
54
+ Tags.SOPInstanceUID,
55
+ Tags.InstanceNumber,
56
+ Tags.SeriesInstanceUID,
57
+ Tags.StudyInstanceUID,
58
+ Tags.ContentDate,
59
+ Tags.ContentTime,
60
+ Tags.AcquisitionDate,
61
+ Tags.AcquisitionTime,
62
+ Tags.AcquisitionNumber,
63
+ Tags.SamplesPerPixel,
64
+ Tags.PhotometricInterpretation,
65
+ Tags.PlanarConfiguration,
66
+ Tags.Rows,
67
+ Tags.Columns,
68
+ Tags.BitsAllocated,
69
+ Tags.BitsStored,
70
+ Tags.HighBit,
71
+ Tags.PixelRepresentation,
72
+ Tags.WindowCenter,
73
+ Tags.WindowWidth,
74
+ Tags.WindowCenterWidthExplanation,
75
+ Tags.InstanceCreationTime,
76
+ ];
77
+
78
+ const ImageExtract = [
79
+ Tags.ImageType,
80
+ Tags.InstanceCreationDate,
81
+ Tags.SOPClassUID,
82
+ Tags.ConversionType,
83
+ Tags.ManufacturerModelName,
84
+ Tags.ContentDate,
85
+ Tags.AcquisitionDate,
86
+ Tags.SecondaryCaptureDeviceID,
87
+ Tags.DateOfSecondaryCapture,
88
+ Tags.SecondaryCaptureDeviceManufacturer,
89
+ Tags.SecondaryCaptureDeviceManufacturerModelName,
90
+ Tags.SecondaryCaptureDeviceSoftwareVersions,
91
+ Tags.SoftwareVersions,
92
+ Tags.SamplesPerPixel,
93
+ Tags.PhotometricInterpretation,
94
+ Tags.PlanarConfiguration,
95
+ Tags.Rows,
96
+ Tags.Columns,
97
+ Tags.BitsAllocated,
98
+ Tags.BitsStored,
99
+ Tags.HighBit,
100
+ Tags.PixelRepresentation,
101
+ Tags.WindowCenter,
102
+ Tags.WindowWidth,
103
+ Tags.WindowCenterWidthExplanation,
104
+ Tags.PatientPosition,
105
+ Tags.PatientOrientation,
106
+ ];
107
+
108
+ const addHash = (data, type) => {
109
+ const existing = getValue(data,DeduppedHash);
110
+ if (existing) {
111
+ return existing;
112
+ }
113
+
114
+ const hashValue = hasher.hash(data);
115
+ setValue(data, DeduppedHash, hashValue );
116
+ setValue(data, DeduppedType, type );
117
+ return hashValue;
118
+ };
119
+
120
+ const TagSets = {
121
+ PatientQuery,
122
+ StudyQuery,
123
+ PatientStudyQuery,
124
+ SeriesQuery,
125
+ SeriesExtract,
126
+ InstanceQuery,
127
+ ImageExtract,
128
+
129
+ QueryExtract: { remove: false, hashRef: false },
130
+ RemoveExtract: { remove: true, hashRef: true },
131
+
132
+ extract: (data, type, tagSet, options) => {
133
+ const ret = {};
134
+ const { remove, hashRef } = options || {};
135
+ tagSet.forEach((tag) => {
136
+ const value = data[tag];
137
+ if (value) {
138
+ ret[tag] = value;
139
+ if (remove) delete data[tag];
140
+ }
141
+ });
142
+ const hashValue = addHash(ret, type);
143
+ if (hashRef) {
144
+ pushList(data,DeduppedRef,hashValue);
145
+ }
146
+ return ret;
147
+ },
148
+ addHash,
149
+ };
150
+
151
+ module.exports = TagSets;
@@ -0,0 +1,42 @@
1
+ const octetStream = "application/octet-stream";
2
+ const uncompressed = {
3
+ uncompressed: true,
4
+ gzip: true,
5
+ contentType: octetStream,
6
+ };
7
+
8
+ /**
9
+ * @see {@link https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_8.7.3.3.2.html}
10
+ */
11
+ const jpeg = "image/jpeg";
12
+ const jls = "image/jls";
13
+ const jll = "image/jll";
14
+ const jxl = "image/x-jxl";
15
+ const htj2k = "image/x-htj2k";
16
+ const jp2 = "image/jp2";
17
+ const rle = "image/dicom-rle";
18
+
19
+ const uids = {
20
+ "1.2.840.10008.1.2": uncompressed,
21
+ "1.2.840.10008.1.2.1": uncompressed,
22
+ "1.2.840.10008.1.2.1.99": uncompressed,
23
+ "1.2.840.10008.1.2.2": uncompressed,
24
+ "1.2.840.10008.1.2.4.50": { contentType: jpeg, lossy: true },
25
+ "1.2.840.10008.1.2.4.51": { contentType: jpeg, lossy: true },
26
+ "1.2.840.10008.1.2.4.57": { contentType: jpeg },
27
+ "1.2.840.10008.1.2.4.70": { contentType: jll },
28
+ "1.2.840.10008.1.2.4.140": { contentType: jxl },
29
+ "1.2.840.10008.1.2.4.141": { contentType: jxl, lossy: true },
30
+ "1.2.840.10008.1.2.4.80": { contentType: jls },
31
+ "1.2.840.10008.1.2.4.81": { contentType: jls, lossy: true },
32
+ "1.2.840.10008.1.2.4.90": { contentType: jp2 },
33
+ "1.2.840.10008.1.2.4.91": { contentType: jp2 },
34
+ // From JPEG original data, lossless to JXL
35
+ "1.2.840.10008.1.2.4.142": { contentType: jxl, lossy: true },
36
+ "1.2.840.10008.1.2.4.150": { contentType: htj2k },
37
+ // **
38
+ "1.2.840.10008.1.2.5": { contentType: rle },
39
+ default: {},
40
+ };
41
+
42
+ module.exports = uids;
@@ -0,0 +1,100 @@
1
+ // TODO review returning type of some methods
2
+ const JSONWriter = require("../writer/JSONWriter");
3
+ const TagLists = require("../model/TagLists");
4
+ const Tags = require("../dictionary/Tags");
5
+
6
+ const {getValue, setValue} = Tags;
7
+
8
+ const extractors = {
9
+ patient: TagLists.PatientQuery,
10
+ study: TagLists.StudyQuery,
11
+ series: TagLists.SeriesExtract,
12
+ image: TagLists.ImageExtract,
13
+ };
14
+
15
+ /**
16
+ * This is an instance listener - the way this one works is that it listens for instance metadata.
17
+ * Every time an instance metadata event is fired, it deduplicates a bunch of data, firing deduplicate events,
18
+ * and then collects the deduplicate data into a list.
19
+ * Whenever the study UID changes, a new event is fired to indicate the deduplicated data is ready.
20
+ */
21
+ async function deduplicateSingleInstance(id, imageFrame) {
22
+ if (!imageFrame) return {};
23
+ const studyData = await this.completeStudy.getCurrentStudyData(this, id);
24
+ const seriesUID = getValue(imageFrame,Tags.SeriesInstanceUID);
25
+ const sopUID = getValue(imageFrame,Tags.SOPInstanceUID);
26
+ if (!sopUID) {
27
+ console.warn("No sop instance UID in", imageFrame);
28
+ return {};
29
+ }
30
+ if (studyData.sopExists(sopUID)) {
31
+ if( this.verbose ) console.log('SOP Instance UID', sopUID, 'already exists, skipping');
32
+ // TODO - allow replace as an option
33
+ // Null value means skip writing this instance
34
+ return null;
35
+ }
36
+
37
+ if( this.verbose ) console.log('SOP Instance UID', sopUID, 'being added');
38
+ const deduplicated = { ...imageFrame };
39
+
40
+ if (!this.extractors) this.extractors = extractors;
41
+ for (const key of Object.keys(this.extractors)) {
42
+ const extracted = TagLists.extract(deduplicated, key, this.extractors[key], TagLists.RemoveExtract);
43
+ const hashKey = getValue(extracted,Tags.DeduppedHash);
44
+ await studyData.addExtracted(this, hashKey, extracted);
45
+ }
46
+
47
+ // Restore the series and SOP UIDs
48
+ setValue(deduplicated, Tags.SeriesInstanceUID, seriesUID);
49
+ setValue(deduplicated, Tags.SOPInstanceUID, sopUID);
50
+ TagLists.addHash(deduplicated, Tags.InstanceType);
51
+
52
+ return deduplicated;
53
+ }
54
+
55
+ /** Canonicalize the JSON data, making Values always arrays, remote "undefined" from the tags etc */
56
+ const canonicalize = (json) => {
57
+ if (!json) return undefined;
58
+ if (Array.isArray(json)) {
59
+ return json.map(canonicalize);
60
+ }
61
+ Object.keys(json).forEach((tag) => {
62
+ if (!tag || tag === "undefined") {
63
+ // console.error('Got an undefined tag:', tag, typeof(tag));
64
+ // TODO check returning value
65
+ return;
66
+ }
67
+ const val = json[tag];
68
+ const { Value } = val || {};
69
+ if (Value && !Array.isArray(Value)) {
70
+ val.Value = [Value];
71
+ }
72
+ });
73
+ return json;
74
+ };
75
+
76
+ const InstanceDeduplicate = (options) =>
77
+ async function run(id, sourceImageFrame) {
78
+ // Notify the existing listeners, if any
79
+ const imageFrame = canonicalize(sourceImageFrame);
80
+ if (options.isInstanceMetadata) {
81
+ // console.log("Writing instance metadata");
82
+ await JSONWriter(id.sopInstanceRootPath, "metadata", imageFrame);
83
+ }
84
+ if (!options.isDeduplicate && !options.isGroup) {
85
+ return;
86
+ }
87
+
88
+ if (!this.deduplicateSingleInstance) {
89
+ this.deduplicateSingleInstance = deduplicateSingleInstance;
90
+ this.verbose = options.verbose;
91
+ }
92
+
93
+ const deduppedInstance = await this.deduplicateSingleInstance(id, imageFrame);
94
+ if (deduppedInstance) {
95
+ // this refers to callee
96
+ await this.deduplicated(id, deduppedInstance);
97
+ }
98
+ };
99
+
100
+ module.exports = InstanceDeduplicate;
@@ -0,0 +1,20 @@
1
+ const path = require("path");
2
+
3
+ function ScanStudy(options) {
4
+ const { directoryName, deduplicatedRoot, deduplicatedInstancesRoot } = options;
5
+
6
+ return function run(dir, studyInstanceUid) {
7
+ const studyPath = path.join(directoryName, "studies", studyInstanceUid);
8
+ const deduplicatedPath = path.join(deduplicatedRoot, studyInstanceUid);
9
+ const deduplicatedInstancesPath = path.join(deduplicatedInstancesRoot, studyInstanceUid);
10
+ console.log("Scanning", dir, studyInstanceUid);
11
+ return this.completeStudy.getCurrentStudyData(this, {
12
+ studyPath,
13
+ deduplicatedPath,
14
+ deduplicatedInstancesPath,
15
+ studyInstanceUid,
16
+ });
17
+ };
18
+ }
19
+
20
+ module.exports = ScanStudy;