@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.
- package/README.md +285 -0
- package/bin/mkdicomweb.js +9 -0
- package/lib/DeleteStudy.js +14 -0
- package/lib/RejectInstance.js +17 -0
- package/lib/StaticWado.js +206 -0
- package/lib/createMain.js +14 -0
- package/lib/deleteMain.js +12 -0
- package/lib/dictionary/Tags.js +111 -0
- package/lib/dictionary/dataDictionary.js +22268 -0
- package/lib/groupMain.js +14 -0
- package/lib/index.js +8 -0
- package/lib/instanceMain.js +13 -0
- package/lib/metadataMain.js +14 -0
- package/lib/mkdicomwebConfig.js +149 -0
- package/lib/model/TagLists.js +151 -0
- package/lib/model/uids.js +42 -0
- package/lib/operation/InstanceDeduplicate.js +100 -0
- package/lib/operation/ScanStudy.js +20 -0
- package/lib/operation/StudyData.js +416 -0
- package/lib/operation/ThumbnailService.js +184 -0
- package/lib/operation/adapter/asyncIterableToBuffer.js +112 -0
- package/lib/operation/adapter/decodeImage.js +9 -0
- package/lib/operation/adapter/getImageInfo.js +21 -0
- package/lib/operation/adapter/transcodeImage.js +303 -0
- package/lib/operation/extractImageFrames.js +41 -0
- package/lib/operation/getDataSet.js +60 -0
- package/lib/operation/getEncapsulatedImageFrame.js +23 -0
- package/lib/operation/getNumberOfFrames.js +13 -0
- package/lib/operation/getUncompressedImageFrame.js +7 -0
- package/lib/operation/getVR.js +16 -0
- package/lib/operation/getValue.js +193 -0
- package/lib/program/index.js +15 -0
- package/lib/rejectMain.js +12 -0
- package/lib/util/IdCreator.js +25 -0
- package/lib/util/adaptProgramOpts.js +58 -0
- package/lib/util/compressionOptionParser.js +13 -0
- package/lib/writer/CompleteStudyWriter.js +88 -0
- package/lib/writer/DeduplicateWriter.js +32 -0
- package/lib/writer/ExpandUriPath.js +15 -0
- package/lib/writer/HashDataWriter.js +59 -0
- package/lib/writer/ImageFrameWriter.js +31 -0
- package/lib/writer/JSONWriter.js +18 -0
- package/lib/writer/MultipartAttribute.js +8 -0
- package/lib/writer/MultipartHeader.js +14 -0
- package/lib/writer/ThumbnailWriter.js +21 -0
- package/lib/writer/VideoWriter.js +52 -0
- package/lib/writer/WriteMultipart.js +22 -0
- package/lib/writer/WriteStream.js +51 -0
- package/package.json +75 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const extractImageFrames = require("./extractImageFrames");
|
|
2
|
+
|
|
3
|
+
const getValueInlineString = (dataSet, attr) => [dataSet.string(attr.tag)];
|
|
4
|
+
|
|
5
|
+
const getStrings = (dataSet, attr) => {
|
|
6
|
+
const ret = dataSet.string(attr.tag);
|
|
7
|
+
return (ret && ret.split(/\\/)) || undefined;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const getValuePatientName = (dataSet, attr) => {
|
|
11
|
+
const strings = getStrings(dataSet, attr);
|
|
12
|
+
return (strings && strings.map((item) => ({ Alphabetic: item }))) || undefined;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Gets either InlineBinary or BulkDataURI, if already defined */
|
|
16
|
+
const getValueInlineBinary = (dataSet, attr) => {
|
|
17
|
+
if (attr.BulkDataURI) return { BulkDataURI: attr.BulkDataURI };
|
|
18
|
+
const binaryValue = dataSet.byteArray.slice(attr.dataOffset, attr.dataOffset + attr.length);
|
|
19
|
+
return { InlineBinary: binaryValue.toString("base64") };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const getValueInlineSignedShort = (dataSet, attr) => {
|
|
23
|
+
if (attr.length > 2) {
|
|
24
|
+
return getValueInlineBinary(dataSet, attr);
|
|
25
|
+
}
|
|
26
|
+
return [dataSet.int16(attr.tag)];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const getValueInlineUnsignedShort = (dataSet, attr) => {
|
|
30
|
+
const ret = [];
|
|
31
|
+
for (let i = 0; i < attr.length / 2; i++) {
|
|
32
|
+
ret.push(dataSet.uint16(attr.tag, i));
|
|
33
|
+
}
|
|
34
|
+
return ret;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getValueInlineSignedLong = (dataSet, attr) => {
|
|
38
|
+
if (attr.length > 4) {
|
|
39
|
+
return getValueInlineBinary(dataSet, attr);
|
|
40
|
+
}
|
|
41
|
+
return [dataSet.int32(attr.tag)];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getValueInlineUnsignedLong = (dataSet, attr) => {
|
|
45
|
+
if (attr.length > 4) {
|
|
46
|
+
return getValueInlineBinary(dataSet, attr);
|
|
47
|
+
}
|
|
48
|
+
return [dataSet.uint32(attr.tag)];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getValueInlineFloat = (dataSet, attr) => {
|
|
52
|
+
if (attr.length > 65536 * 4) {
|
|
53
|
+
return getValueInlineBinary(dataSet, attr);
|
|
54
|
+
}
|
|
55
|
+
const ret = [];
|
|
56
|
+
for (let i = 0; i < attr.length / 4; i++) {
|
|
57
|
+
ret.push(dataSet.float(attr.tag, i));
|
|
58
|
+
}
|
|
59
|
+
return ret;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const getValueInlineIntString = (dataSet, attr) => getStrings(dataSet, attr).map((val) => parseInt(val));
|
|
63
|
+
|
|
64
|
+
const getValueInlineFloatString = (dataSet, attr) => getStrings(dataSet, attr).map((val) => parseFloat(val));
|
|
65
|
+
|
|
66
|
+
const getValueInlineFloatDouble = (dataSet, attr) => {
|
|
67
|
+
if (attr.length > 8) {
|
|
68
|
+
return getValueInlineBinary(dataSet, attr);
|
|
69
|
+
}
|
|
70
|
+
return [dataSet.double(attr.tag)];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const getValueInlineAttributeTag = (dataSet, attr) => {
|
|
74
|
+
const group = dataSet.uint16(attr.tag, 0);
|
|
75
|
+
const groupHexStr = `0000${group.toString(16)}`.substr(-4);
|
|
76
|
+
const element = dataSet.uint16(attr.tag, 1);
|
|
77
|
+
const elementHexStr = `0000${element.toString(16)}`.substr(-4);
|
|
78
|
+
return groupHexStr + elementHexStr;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const getValueInline = (dataSet, attr, vr) => {
|
|
82
|
+
if (attr.length == 0) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
switch (vr) {
|
|
87
|
+
case "AE":
|
|
88
|
+
case "AS":
|
|
89
|
+
return getValueInlineString(dataSet, attr);
|
|
90
|
+
case "AT":
|
|
91
|
+
return getValueInlineAttributeTag(dataSet, attr);
|
|
92
|
+
case "DS":
|
|
93
|
+
return getValueInlineFloatString(dataSet, attr);
|
|
94
|
+
case "CS":
|
|
95
|
+
case "DA":
|
|
96
|
+
case "DT":
|
|
97
|
+
return getValueInlineString(dataSet, attr);
|
|
98
|
+
case "FL":
|
|
99
|
+
return getValueInlineFloat(dataSet, attr);
|
|
100
|
+
case "FD":
|
|
101
|
+
return getValueInlineFloatDouble(dataSet, attr);
|
|
102
|
+
case "IS":
|
|
103
|
+
return getValueInlineIntString(dataSet, attr);
|
|
104
|
+
case "LO":
|
|
105
|
+
case "LT":
|
|
106
|
+
return getValueInlineString(dataSet, attr);
|
|
107
|
+
case "OB":
|
|
108
|
+
case "OF":
|
|
109
|
+
case "OW":
|
|
110
|
+
return getValueInlineBinary(dataSet, attr);
|
|
111
|
+
case "PN":
|
|
112
|
+
return getValuePatientName(dataSet, attr);
|
|
113
|
+
case "SH":
|
|
114
|
+
return getValueInlineString(dataSet, attr);
|
|
115
|
+
case "SL":
|
|
116
|
+
return getValueInlineSignedLong(dataSet, attr);
|
|
117
|
+
case "SS":
|
|
118
|
+
return getValueInlineSignedShort(dataSet, attr);
|
|
119
|
+
case "ST":
|
|
120
|
+
case "TM":
|
|
121
|
+
case "UI":
|
|
122
|
+
return getValueInlineString(dataSet, attr);
|
|
123
|
+
case "UL":
|
|
124
|
+
return getValueInlineUnsignedLong(dataSet, attr);
|
|
125
|
+
case "UN":
|
|
126
|
+
return getValueInlineBinary(dataSet, attr);
|
|
127
|
+
case "US":
|
|
128
|
+
return getValueInlineUnsignedShort(dataSet, attr);
|
|
129
|
+
case "UT":
|
|
130
|
+
return getValueInlineString(dataSet, attr);
|
|
131
|
+
default:
|
|
132
|
+
return getValueInlineBinary(dataSet, attr);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const isPrivate = (attr) => {
|
|
137
|
+
const { tag } = attr;
|
|
138
|
+
const ch = tag.substr(4, 1);
|
|
139
|
+
const chHex = parseInt(ch, 16);
|
|
140
|
+
return chHex % 2 == 1;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const isValueInline = (attr, options) => {
|
|
144
|
+
if (isPrivate(attr)) {
|
|
145
|
+
return attr.length <= options.maximumInlinePrivateLength;
|
|
146
|
+
}
|
|
147
|
+
return !attr.length || attr.length <= options.maximumInlinePublicLength;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get data value for the given attr.
|
|
152
|
+
* Attr can be a pixelData tag, sequence items, inline or bulkdata
|
|
153
|
+
*
|
|
154
|
+
* @param {*} dataSet
|
|
155
|
+
* @param {*} attr
|
|
156
|
+
* @param {*} vr
|
|
157
|
+
* @param {*} getDataSet
|
|
158
|
+
* @param {*} callback
|
|
159
|
+
* @param {*} options object containing program properties.
|
|
160
|
+
* @param {*} parentAttr attr's parent. If not present means attr is at root level.
|
|
161
|
+
* @returns
|
|
162
|
+
*/
|
|
163
|
+
const getValue = async (dataSet, attr, vr, getDataSet, callback, options, parentAttr) => {
|
|
164
|
+
// It will only process pixelData tag if on metadata root. Otherwise it will be skiped.
|
|
165
|
+
if (attr.tag === "x7fe00010" && !parentAttr) {
|
|
166
|
+
const BulkDataURI = await extractImageFrames(dataSet, attr, vr, callback, options);
|
|
167
|
+
return { BulkDataURI };
|
|
168
|
+
}
|
|
169
|
+
if (attr.tag === "xfffee00d") return undefined;
|
|
170
|
+
// Group length
|
|
171
|
+
if (attr.tag.length == 9 && attr.tag.substring(5, 9) === "0000") return undefined;
|
|
172
|
+
if (attr.items) {
|
|
173
|
+
// sequences
|
|
174
|
+
const result = [];
|
|
175
|
+
for (const item of attr.items) {
|
|
176
|
+
const subResult = await getDataSet(item.dataSet, callback, options, attr);
|
|
177
|
+
if (subResult.metadata) result.push(subResult.metadata);
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
if (attr.Value) {
|
|
182
|
+
return (Array.isArray(attr.Value) && attr.Value) || [attr.Value];
|
|
183
|
+
}
|
|
184
|
+
// non sequence item
|
|
185
|
+
if (isValueInline(attr, options)) {
|
|
186
|
+
return getValueInline(dataSet, attr, vr);
|
|
187
|
+
}
|
|
188
|
+
const binaryValue = dataSet.byteArray.slice(attr.dataOffset, attr.dataOffset + attr.length);
|
|
189
|
+
const mimeType = attr.tag == "x00420011" && dataSet.string("x00420012");
|
|
190
|
+
const BulkDataURI = await callback.bulkdata(binaryValue, { mimeType });
|
|
191
|
+
return { BulkDataURI, vr };
|
|
192
|
+
};
|
|
193
|
+
module.exports = getValue;
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
console.log("Loaded configuration from", configurationFile);
|
|
12
|
+
staticWadoUtil.configureCommands(defaults);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
exports.configureProgram = configureProgram;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const StaticWado = require("./StaticWado");
|
|
2
|
+
const adaptProgramOpts = require("./util/adaptProgramOpts");
|
|
3
|
+
|
|
4
|
+
module.exports = function rejectMain(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.reject(program.args);
|
|
12
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
function IdCreator({ directoryName, deduplicatedRoot, deduplicatedInstancesRoot }) {
|
|
4
|
+
return (uids, filename) => {
|
|
5
|
+
const studyPath = path.join(directoryName, "studies", uids.studyInstanceUid);
|
|
6
|
+
const seriesRootPath = path.join(studyPath, "series", uids.seriesInstanceUid);
|
|
7
|
+
const sopInstanceRootPath = path.join(studyPath, "series", uids.seriesInstanceUid, "instances", uids.sopInstanceUid);
|
|
8
|
+
const deduplicatedPath = path.join(deduplicatedRoot, uids.studyInstanceUid);
|
|
9
|
+
const deduplicatedInstancesPath = path.join(deduplicatedInstancesRoot, uids.studyInstanceUid);
|
|
10
|
+
const imageFrameRootPath = path.join(sopInstanceRootPath, "frames");
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
...uids,
|
|
14
|
+
studyPath,
|
|
15
|
+
deduplicatedPath,
|
|
16
|
+
deduplicatedInstancesPath,
|
|
17
|
+
seriesRootPath,
|
|
18
|
+
sopInstanceRootPath,
|
|
19
|
+
imageFrameRootPath,
|
|
20
|
+
filename,
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = IdCreator;
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
maximumInlinePublicLength,
|
|
14
|
+
maximumInlinePrivateLength,
|
|
15
|
+
group: isGroup,
|
|
16
|
+
instances: isInstanceMetadata,
|
|
17
|
+
deduplicate: isDeduplicate,
|
|
18
|
+
study: isStudyData,
|
|
19
|
+
clean: isClean,
|
|
20
|
+
recompress,
|
|
21
|
+
recompressThumb,
|
|
22
|
+
contentType,
|
|
23
|
+
colourContentType,
|
|
24
|
+
dir: rootDir,
|
|
25
|
+
pathDeduplicated,
|
|
26
|
+
pathInstances,
|
|
27
|
+
removeDeduplicatedInstances,
|
|
28
|
+
storeMultipartBulkData,
|
|
29
|
+
prependBulkDataUri,
|
|
30
|
+
expandBulkDataUri,
|
|
31
|
+
verbose,
|
|
32
|
+
} = programOpts;
|
|
33
|
+
|
|
34
|
+
return assignDefined(
|
|
35
|
+
{ ...defaults },
|
|
36
|
+
{
|
|
37
|
+
maximumInlinePublicLength,
|
|
38
|
+
maximumInlinePrivateLength,
|
|
39
|
+
isGroup,
|
|
40
|
+
isInstanceMetadata,
|
|
41
|
+
isDeduplicate,
|
|
42
|
+
isStudyData,
|
|
43
|
+
isClean,
|
|
44
|
+
recompress,
|
|
45
|
+
recompressThumb,
|
|
46
|
+
contentType,
|
|
47
|
+
colourContentType,
|
|
48
|
+
rootDir,
|
|
49
|
+
pathDeduplicated,
|
|
50
|
+
pathInstances,
|
|
51
|
+
removeDeduplicatedInstances,
|
|
52
|
+
storeMultipartBulkData,
|
|
53
|
+
prependBulkDataUri,
|
|
54
|
+
expandBulkDataUri,
|
|
55
|
+
verbose,
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
};
|
|
@@ -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,88 @@
|
|
|
1
|
+
const { Stats } = require("@radicalimaging/static-wado-util");
|
|
2
|
+
const { JSONReader } = require("@radicalimaging/static-wado-util");
|
|
3
|
+
const JSONWriter = require("./JSONWriter");
|
|
4
|
+
const StudyData = require("../operation/StudyData");
|
|
5
|
+
const Tags = require("../dictionary/Tags");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CompleteStudyWriter takes the deduplicated data values, all loaded into the study parameter,
|
|
9
|
+
* and writes it out as various dataset types. The options parameters define what types it gets
|
|
10
|
+
* written out as.
|
|
11
|
+
* The studyData object is then removed, so that a new one can be created if required.
|
|
12
|
+
*/
|
|
13
|
+
const CompleteStudyWriter = (options) => {
|
|
14
|
+
async function ret() {
|
|
15
|
+
const { studyData } = this;
|
|
16
|
+
if (!studyData) return;
|
|
17
|
+
|
|
18
|
+
if (!studyData.numberOfInstances) {
|
|
19
|
+
console.log("studyData.deduplicated is empty");
|
|
20
|
+
delete this.studyData;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (options.isGroup) {
|
|
25
|
+
if (studyData.dirty) {
|
|
26
|
+
await studyData.writeDeduplicatedGroup();
|
|
27
|
+
console.log("Wrote updated deduplicated data for study", studyData.studyInstanceUid);
|
|
28
|
+
} else {
|
|
29
|
+
console.log("Not writing new deduplicated data because it is clean:", studyData.studyInstanceUid);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!options.isStudyData) {
|
|
34
|
+
delete this.studyData;
|
|
35
|
+
Stats.StudyStats.summarize();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const isDirtyMetadata = await studyData.dirtyMetadata();
|
|
40
|
+
if (!isDirtyMetadata) {
|
|
41
|
+
console.log("Study metadata", studyData.studyInstanceUid, "has clean metadata, not writing");
|
|
42
|
+
delete this.studyData;
|
|
43
|
+
Stats.StudyStats.summarize(`Study metadata ${studyData.studyInstanceUid} has clean metadata, not writing`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const studyQuery = await studyData.writeMetadata();
|
|
48
|
+
|
|
49
|
+
const allStudies = await JSONReader(options.directoryName, "studies/index.json.gz", []);
|
|
50
|
+
const studyUID = Tags.getValue(studyQuery,Tags.StudyInstanceUID);
|
|
51
|
+
if (!studyUID) {
|
|
52
|
+
console.error("studyQuery=", studyQuery);
|
|
53
|
+
throw new Error("Study query has null studyUID");
|
|
54
|
+
}
|
|
55
|
+
const studyIndex = allStudies.findIndex((item) => Tags.getValue(item,Tags.StudyInstanceUID) == studyUID);
|
|
56
|
+
if (studyIndex == -1) {
|
|
57
|
+
allStudies.push(studyQuery);
|
|
58
|
+
} else {
|
|
59
|
+
allStudies[studyIndex] = studyQuery;
|
|
60
|
+
}
|
|
61
|
+
await JSONWriter(options.directoryName, "studies", allStudies);
|
|
62
|
+
delete this.studyData;
|
|
63
|
+
Stats.StudyStats.summarize(`Wrote study metadata/query files for ${studyData.studyInstanceUid}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Gets a current study data object, or completes the old one and generates a new one.
|
|
68
|
+
* async call as it may need to store the current study data value.
|
|
69
|
+
*/
|
|
70
|
+
ret.getCurrentStudyData = async (callback, id) => {
|
|
71
|
+
const { studyData } = callback;
|
|
72
|
+
const { studyInstanceUid } = id;
|
|
73
|
+
|
|
74
|
+
if (studyData) {
|
|
75
|
+
if (studyData.studyInstanceUid == studyInstanceUid) {
|
|
76
|
+
return studyData;
|
|
77
|
+
}
|
|
78
|
+
await callback.completeStudy(studyData);
|
|
79
|
+
}
|
|
80
|
+
callback.setStudyData(new StudyData(id, options));
|
|
81
|
+
await callback.studyData.init(options);
|
|
82
|
+
return callback.studyData;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return ret;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
module.exports = CompleteStudyWriter;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const hashFactory = require("node-object-hash");
|
|
2
|
+
|
|
3
|
+
const hasher = hashFactory();
|
|
4
|
+
const JSONWriter = require("./JSONWriter");
|
|
5
|
+
const Tags = require("../dictionary/Tags");
|
|
6
|
+
const TagLists = require("../model/TagLists");
|
|
7
|
+
|
|
8
|
+
async function writeDeduplicatedFile(dir, data, hashValueSrc) {
|
|
9
|
+
const hashValue = hashValueSrc || hasher.hash(data);
|
|
10
|
+
// Write it direct in the deduplicated directory, not as a sub-directory or index file
|
|
11
|
+
await JSONWriter(dir, hashValue, data, { gzip: true, index: false });
|
|
12
|
+
return hashValue;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const perInstanceWriter = async (id, data) => {
|
|
16
|
+
const { deduplicatedInstancesPath } = id;
|
|
17
|
+
const hashValue = TagLists.addHash(data, Tags.InstanceType);
|
|
18
|
+
return writeDeduplicatedFile(deduplicatedInstancesPath, [data], hashValue);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Writes out JSON files to the given file name. Automatically GZips them, and adds the extension */
|
|
22
|
+
const DeduplicateWriter = (options) =>
|
|
23
|
+
async function DeduplicateWriterInstance(id, data) {
|
|
24
|
+
const studyData = await this.completeStudy.getCurrentStudyData(this, id);
|
|
25
|
+
|
|
26
|
+
if (options.isDeduplicate) {
|
|
27
|
+
await perInstanceWriter(id, data);
|
|
28
|
+
}
|
|
29
|
+
studyData.addDeduplicated(data, id.filename);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
module.exports = DeduplicateWriter;
|
|
@@ -0,0 +1,15 @@
|
|
|
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) console.log(`Expanded path returned: ${prependBulkDataUri}${expandedRelative}${path}`);
|
|
12
|
+
return `${prependBulkDataUri}${expandedRelative}${path}`;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
module.exports = ExpandUriPath;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const hashFactory = require("node-object-hash");
|
|
2
|
+
|
|
3
|
+
const hasher = hashFactory();
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const Tags = require("../dictionary/Tags");
|
|
6
|
+
const WriteStream = require("./WriteStream");
|
|
7
|
+
const WriteMultipart = require("./WriteMultipart");
|
|
8
|
+
const ExpandUriPath = require("./ExpandUriPath");
|
|
9
|
+
const { MultipartHeader } = require("./MultipartHeader");
|
|
10
|
+
|
|
11
|
+
// Extensions for encapsulated content. Do NOT add any executable content extensions here.
|
|
12
|
+
const extensions = {
|
|
13
|
+
"application/pdf": ".pdf",
|
|
14
|
+
"text/json": ".json",
|
|
15
|
+
"application/xml+cda": ".cda.xml",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Writes out JSON files to the given file name. Automatically GZips them, and adds the extension */
|
|
19
|
+
const HashDataWriter =
|
|
20
|
+
(options) =>
|
|
21
|
+
async (id, key, data, additionalOptions = {}) => {
|
|
22
|
+
const isRaw = ArrayBuffer.isView(data);
|
|
23
|
+
const { storeMultipartBulkData } = options;
|
|
24
|
+
const { mimeType } = additionalOptions;
|
|
25
|
+
// If the file has an extension, it should be directly accessible as that file type.
|
|
26
|
+
const gzip = !isRaw || (data.length > 1024 && !mimeType);
|
|
27
|
+
const { dirName, fileName } = HashDataWriter.createHashPath(data, options);
|
|
28
|
+
const absolutePath = path.join(id.studyPath, dirName);
|
|
29
|
+
const rawData = isRaw ? data : JSON.stringify(data, null, 1);
|
|
30
|
+
const writeStream = WriteStream(absolutePath, fileName, {
|
|
31
|
+
mkdir: true,
|
|
32
|
+
gzip,
|
|
33
|
+
});
|
|
34
|
+
if (isRaw && storeMultipartBulkData) {
|
|
35
|
+
await WriteMultipart(writeStream, [new MultipartHeader("Content-Type", "application/octet-stream")], rawData);
|
|
36
|
+
} else {
|
|
37
|
+
await writeStream.write(rawData);
|
|
38
|
+
}
|
|
39
|
+
await writeStream.close();
|
|
40
|
+
return ExpandUriPath(id, `${dirName}/${fileName}`, options);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns a hash path relative to the objectUID directory.
|
|
45
|
+
*/
|
|
46
|
+
HashDataWriter.createHashPath = (data, options = {}) => {
|
|
47
|
+
const { mimeType } = options;
|
|
48
|
+
const isRaw = ArrayBuffer.isView(data);
|
|
49
|
+
const extension = isRaw ? (mimeType && extensions[mimeType]) || "" : ".json";
|
|
50
|
+
const hashValue = Tags.getValue(data,Tags.DeduppedHash) || hasher.hash(data);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
// Use string concat as this value is used for the BulkDataURI which needs forward slashes
|
|
54
|
+
dirName: `bulkdata/${hashValue.substring(0, 3)}/${hashValue.substring(3, 5)}`,
|
|
55
|
+
fileName: hashValue.substring(5) + extension,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
module.exports = HashDataWriter;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const uids = require("../model/uids");
|
|
2
|
+
const WriteStream = require("./WriteStream");
|
|
3
|
+
const WriteMultipart = require("./WriteMultipart");
|
|
4
|
+
const ExpandUriPath = require("./ExpandUriPath");
|
|
5
|
+
const { MultipartHeader, MultipartAttribute } = require("./MultipartHeader");
|
|
6
|
+
|
|
7
|
+
const ImageFrameWriter = (options) => {
|
|
8
|
+
const { verbose } = options;
|
|
9
|
+
|
|
10
|
+
return async (id, index, imageFrame) => {
|
|
11
|
+
const { transferSyntaxUid } = id;
|
|
12
|
+
const type = uids[transferSyntaxUid] || uids.default;
|
|
13
|
+
const writeStream = WriteStream(id.imageFrameRootPath, `${1 + index}`, {
|
|
14
|
+
gzip: type.gzip,
|
|
15
|
+
mkdir: true,
|
|
16
|
+
});
|
|
17
|
+
let content;
|
|
18
|
+
if (imageFrame instanceof Uint8Array) {
|
|
19
|
+
content = imageFrame;
|
|
20
|
+
} else {
|
|
21
|
+
content = Buffer.from(imageFrame.buffer);
|
|
22
|
+
}
|
|
23
|
+
await WriteMultipart(writeStream, [new MultipartHeader("Content-Type", type.contentType, [new MultipartAttribute("transfer-syntax", transferSyntaxUid)])], content);
|
|
24
|
+
await writeStream.close();
|
|
25
|
+
if (verbose) console.log("Wrote image frame", id.sopInstanceUid, index + 1);
|
|
26
|
+
const includeSeries = true;
|
|
27
|
+
return ExpandUriPath(id, `instances/${id.sopInstanceUid}/frames`, { includeSeries, ...options });
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
module.exports = ImageFrameWriter;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const { Stats } = require("@radicalimaging/static-wado-util");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const WriteStream = require("./WriteStream");
|
|
4
|
+
|
|
5
|
+
/** Writes out JSON files to the given file name. Automatically GZips them, and adds the extension */
|
|
6
|
+
const JSONWriter = async (dir, name, data, options = { gzip: true, brotli: false, index: true }) => {
|
|
7
|
+
const fileName = options.index ? "index.json.gz" : name + ((options.gzip && ".gz") || (options.brotli && ".br") || "");
|
|
8
|
+
const dirName = options.index ? path.join(dir, name) : dir;
|
|
9
|
+
const writeStream = WriteStream(dirName, fileName, {
|
|
10
|
+
...options,
|
|
11
|
+
mkdir: true,
|
|
12
|
+
});
|
|
13
|
+
await writeStream.write(JSON.stringify(data));
|
|
14
|
+
await writeStream.close();
|
|
15
|
+
Stats.StudyStats.add("Write JSON", `Write JSON file ${name}`, 1000);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = JSONWriter;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const MultipartAttribute = require("./MultipartAttribute");
|
|
2
|
+
|
|
3
|
+
class MultipartHeader {
|
|
4
|
+
constructor(headerName, headerValue, attributes = []) {
|
|
5
|
+
this.headerName = headerName;
|
|
6
|
+
this.headerValue = headerValue;
|
|
7
|
+
this.attributes = attributes;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
MultipartAttribute,
|
|
13
|
+
MultipartHeader,
|
|
14
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const WriteStream = require("./WriteStream");
|
|
2
|
+
|
|
3
|
+
const ThumbnailWriter = (options) => {
|
|
4
|
+
const { verbose } = options;
|
|
5
|
+
|
|
6
|
+
return async (filePath, fileName, thumbBuffer) => {
|
|
7
|
+
const writeStream = WriteStream(filePath, fileName, {
|
|
8
|
+
mkdir: true,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
await writeStream.write(thumbBuffer);
|
|
12
|
+
|
|
13
|
+
await writeStream.close();
|
|
14
|
+
|
|
15
|
+
if (verbose) {
|
|
16
|
+
console.log("Wrote thumbnail frame", filePath, fileName);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
module.exports = ThumbnailWriter;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Write raw video file */
|
|
2
|
+
const Tags = require("../dictionary/Tags");
|
|
3
|
+
const WriteStream = require("./WriteStream");
|
|
4
|
+
|
|
5
|
+
const MPEG2 = "mpg";
|
|
6
|
+
const H264 = "mp4";
|
|
7
|
+
const H265 = "h265";
|
|
8
|
+
|
|
9
|
+
const VIDEO_TYPES = {
|
|
10
|
+
"1.2.840.10008.1.2.4.100": MPEG2,
|
|
11
|
+
"1.2.840.10008.1.2.4.101": MPEG2,
|
|
12
|
+
"1.2.840.10008.1.2.4.102": H264,
|
|
13
|
+
"1.2.840.10008.1.2.4.103": H264,
|
|
14
|
+
"1.2.840.10008.1.2.4.104": H264,
|
|
15
|
+
"1.2.840.10008.1.2.4.105": H264,
|
|
16
|
+
"1.2.840.10008.1.2.4.106": H264,
|
|
17
|
+
"1.2.840.10008.1.2.4.107": H265,
|
|
18
|
+
"1.2.840.10008.1.2.4.108": H265,
|
|
19
|
+
// TODO - add the new multi-segment MPEG2 and H264 variants
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const isVideo = (value) => VIDEO_TYPES[value && value.string ? value.string(Tags.RawTransferSyntaxUID) : value];
|
|
23
|
+
|
|
24
|
+
const VideoWriter = () =>
|
|
25
|
+
async function run(id, dataSet) {
|
|
26
|
+
console.log(`Writing video ${id.sopInstanceUid}`);
|
|
27
|
+
const extension = VIDEO_TYPES[dataSet.string(Tags.RawTransferSyntaxUID)];
|
|
28
|
+
const filename = `pixeldata.${extension}`;
|
|
29
|
+
const writeStream = WriteStream(id.sopInstanceRootPath, filename, {
|
|
30
|
+
mkdir: true,
|
|
31
|
+
});
|
|
32
|
+
let length = 0;
|
|
33
|
+
const { fragments } = dataSet.elements.x7fe00010;
|
|
34
|
+
|
|
35
|
+
if (!fragments) {
|
|
36
|
+
console.warn("No video data");
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
// The zero position fragment isn't available, even though present in the original data
|
|
40
|
+
for (let i = 0; i < fragments.length; i++) {
|
|
41
|
+
const fragment = fragments[i];
|
|
42
|
+
const blob = dataSet.byteArray.slice(fragment.position, fragment.position + fragment.length);
|
|
43
|
+
length += blob.length;
|
|
44
|
+
await writeStream.write(blob);
|
|
45
|
+
}
|
|
46
|
+
await writeStream.close();
|
|
47
|
+
console.log(`Done video ${id.sopInstanceRootPath}\\${filename} of length ${length}`);
|
|
48
|
+
return `series/${id.seriesInstanceUid}/instances/${id.sopInstanceUid}/pixeldata.${extension}?length=${length}&offset=0`;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
module.exports = VideoWriter;
|
|
52
|
+
VideoWriter.isVideo = isVideo;
|