@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,112 @@
|
|
|
1
|
+
const { Stats } = require("@radicalimaging/static-wado-util");
|
|
2
|
+
/* eslint "no-param-reassign": "off" */
|
|
3
|
+
|
|
4
|
+
const handler = {
|
|
5
|
+
get(obj, key) {
|
|
6
|
+
const ikey = parseInt(key);
|
|
7
|
+
if (ikey == key) return obj.index_get(ikey);
|
|
8
|
+
const handlerFunc = handler[key];
|
|
9
|
+
if (handlerFunc && key != "get") return handlerFunc(obj, key);
|
|
10
|
+
if (!obj._keys[key]) {
|
|
11
|
+
console.log("New call to", key);
|
|
12
|
+
obj._keys[key] = true;
|
|
13
|
+
}
|
|
14
|
+
return obj[key];
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
length: (obj) => obj.combinedLength,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const StreamingFunctions = {
|
|
21
|
+
addChunks(chunks) {
|
|
22
|
+
if (!this.chunks) this.chunks = [];
|
|
23
|
+
chunks.forEach((chunk) => this.chunks.push(chunk));
|
|
24
|
+
this.combinedLength = 0;
|
|
25
|
+
let i = 0;
|
|
26
|
+
this.chunks.forEach((chunk) => {
|
|
27
|
+
chunk.start = this.combinedLength;
|
|
28
|
+
chunk.i = i;
|
|
29
|
+
i += 1;
|
|
30
|
+
this.combinedLength += chunk.length;
|
|
31
|
+
});
|
|
32
|
+
this.lastChunk = this.chunks[0];
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
index_get(ikey) {
|
|
36
|
+
const found = this.findChunk(ikey);
|
|
37
|
+
if (!found) throw Error(`index ${ikey} not found between 0..${this.combinedLength}`);
|
|
38
|
+
return found[ikey - found.start];
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
findChunk(ikey) {
|
|
42
|
+
let i = (this.lastChunk.start <= ikey && this.lastChunk.i) || 0;
|
|
43
|
+
while (i < this.chunks.length) {
|
|
44
|
+
const chunk = this.chunks[i];
|
|
45
|
+
if (ikey >= chunk.start && ikey < chunk.start + chunk.length) {
|
|
46
|
+
this.lastChunk = chunk;
|
|
47
|
+
return chunk;
|
|
48
|
+
}
|
|
49
|
+
i += 1;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
slice(start, end) {
|
|
55
|
+
const buflen = end - start;
|
|
56
|
+
const ret = Buffer.alloc(buflen);
|
|
57
|
+
let i = 0;
|
|
58
|
+
while (i < buflen) {
|
|
59
|
+
const chunk = this.findChunk(start + i);
|
|
60
|
+
const chunkI = start + i - chunk.start;
|
|
61
|
+
const useLen = Math.min(buflen, chunk.length - chunkI);
|
|
62
|
+
chunk.copy(ret, i, chunkI, chunkI + useLen);
|
|
63
|
+
i += useLen;
|
|
64
|
+
}
|
|
65
|
+
return ret;
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
hexSlice(start, end) {
|
|
69
|
+
return this.slice(start, end).hexSlice();
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/** The internal node copy function is a native that directly accesses internal class details, so over-ride it.
|
|
73
|
+
* TODO: Make this efficient by using the internal copy function when available rather than copy one at a time.
|
|
74
|
+
*/
|
|
75
|
+
copy(target, targetStart = 0, srcStart = 0, srcEnd = 0) {
|
|
76
|
+
const { length } = target;
|
|
77
|
+
const srcLength = (srcEnd === undefined && Math.min(this.length, srcEnd)) || this.length;
|
|
78
|
+
const copied = 0;
|
|
79
|
+
while (targetStart < length && srcStart < srcLength) {
|
|
80
|
+
target[targetStart] = this[srcStart];
|
|
81
|
+
targetStart += 1;
|
|
82
|
+
srcStart += 1;
|
|
83
|
+
}
|
|
84
|
+
return copied;
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
_keys: { then: true, lastChunk: true, chunks: true },
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
Object.keys(StreamingFunctions).forEach((key) => {
|
|
91
|
+
StreamingFunctions._keys[key] = true;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const StreamingBuffer = (chunks) => {
|
|
95
|
+
const buf = Buffer.from("NotUsed");
|
|
96
|
+
Object.assign(buf, StreamingFunctions);
|
|
97
|
+
buf.addChunks(chunks);
|
|
98
|
+
return new Proxy(buf, handler);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const asyncIteratorToBuffer = async (readable) => {
|
|
102
|
+
if (ArrayBuffer.isView(readable)) return readable;
|
|
103
|
+
const chunks = [];
|
|
104
|
+
for await (const chunk of readable) {
|
|
105
|
+
chunks.push(chunk);
|
|
106
|
+
Stats.BufferStats.add("Read Async", `Read async buffer ${chunks.length}`, 1024);
|
|
107
|
+
}
|
|
108
|
+
Stats.BufferStats.reset();
|
|
109
|
+
return StreamingBuffer(chunks);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
module.exports = asyncIteratorToBuffer;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const dicomCodec = require("@cornerstonejs/dicom-codec");
|
|
2
|
+
const getImageInfo = require("./getImageInfo");
|
|
3
|
+
|
|
4
|
+
async function decodeImage(imageFrame, dataset, transferSyntaxUid) {
|
|
5
|
+
const imageInfo = getImageInfo(dataset);
|
|
6
|
+
return dicomCodec.decode(imageFrame, imageInfo, transferSyntaxUid);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
module.exports = decodeImage;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimum image info data to be used on transcode process by dicom-codec api.
|
|
3
|
+
*/
|
|
4
|
+
function getImageInfo(dataSet) {
|
|
5
|
+
const rows = dataSet.uint16("x00280010");
|
|
6
|
+
const columns = dataSet.uint16("x00280011");
|
|
7
|
+
const bitsAllocated = dataSet.uint16("x00280100");
|
|
8
|
+
const samplesPerPixel = dataSet.uint16("x00280002");
|
|
9
|
+
const pixelRepresentation = dataSet.uint16("x00280103") || 0; // not yet being used.
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
bitsAllocated,
|
|
13
|
+
samplesPerPixel,
|
|
14
|
+
rows, // Number with the image rows/height
|
|
15
|
+
columns, // Number with the image columns/width,
|
|
16
|
+
signed: pixelRepresentation === 1,
|
|
17
|
+
pixelRepresentation,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = getImageInfo;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
const dicomCodec = require("@cornerstonejs/dicom-codec");
|
|
2
|
+
const Tags = require("../../dictionary/Tags");
|
|
3
|
+
const getImageInfo = require("./getImageInfo");
|
|
4
|
+
|
|
5
|
+
const transcodeOp = {
|
|
6
|
+
none: 0,
|
|
7
|
+
decode: 1,
|
|
8
|
+
encode: 2,
|
|
9
|
+
transcode: 3, // decode and then encode
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Static mapping for transcoding decoder destination names/uids - currently only lei, jls and jls-lossy appear to work.
|
|
14
|
+
*/
|
|
15
|
+
const transcodeDestinationMap = {
|
|
16
|
+
lei: {
|
|
17
|
+
transferSyntaxUid: "1.2.840.10008.1.2.1",
|
|
18
|
+
transcodeOp: transcodeOp.none
|
|
19
|
+
},
|
|
20
|
+
jls: {
|
|
21
|
+
transferSyntaxUid: "1.2.840.10008.1.2.4.80",
|
|
22
|
+
transcodeOp: transcodeOp.encode,
|
|
23
|
+
},
|
|
24
|
+
"jls-lossy": {
|
|
25
|
+
transferSyntaxUid: "1.2.840.10008.1.2.4.81",
|
|
26
|
+
transcodeOp: transcodeOp.encode,
|
|
27
|
+
},
|
|
28
|
+
jp2: {
|
|
29
|
+
transferSyntaxUid: "1.2.840.10008.1.2.4.90",
|
|
30
|
+
transcodeOp: transcodeOp.encode,
|
|
31
|
+
},
|
|
32
|
+
"jp2-lossy": {
|
|
33
|
+
transferSyntaxUid: "1.2.840.10008.1.2.4.91",
|
|
34
|
+
transcodeOp: transcodeOp.encode,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const transcodeSourceMap = {
|
|
39
|
+
"1.2.840.10008.1.2": {
|
|
40
|
+
transcodeOp: transcodeOp.decode,
|
|
41
|
+
alias: "uncompressed",
|
|
42
|
+
},
|
|
43
|
+
"1.2.840.10008.1.2.1": {
|
|
44
|
+
transcodeOp: transcodeOp.none,
|
|
45
|
+
alias: "uncompressed",
|
|
46
|
+
},
|
|
47
|
+
"1.2.840.10008.1.2.2": {
|
|
48
|
+
transcodeOp: transcodeOp.decode,
|
|
49
|
+
alias: "uncompressed",
|
|
50
|
+
},
|
|
51
|
+
"1.2.840.10008.1.2.4.57": {
|
|
52
|
+
transcodeOp: transcodeOp.decode,
|
|
53
|
+
alias: "jpeglossless",
|
|
54
|
+
},
|
|
55
|
+
"1.2.840.10008.1.2.4.70": {
|
|
56
|
+
transcodeOp: transcodeOp.decode,
|
|
57
|
+
alias: "jpeglossless",
|
|
58
|
+
},
|
|
59
|
+
"1.2.840.10008.1.2.4.90": {
|
|
60
|
+
transcodeOp: transcodeOp.decode,
|
|
61
|
+
alias: "jp2",
|
|
62
|
+
},
|
|
63
|
+
"1.2.840.10008.1.2.4.91": {
|
|
64
|
+
transcodeOp: transcodeOp.decode,
|
|
65
|
+
alias: "jp2",
|
|
66
|
+
},
|
|
67
|
+
"1.2.840.10008.1.2.5": {
|
|
68
|
+
transcodeOp: transcodeOp.decode,
|
|
69
|
+
alias: "rle",
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get an existing destination transcoder from destination map.
|
|
75
|
+
*
|
|
76
|
+
* @param {*} id Destination type to compress to
|
|
77
|
+
* @returns A partial transcoder definition. Otherwise it returns undefined.
|
|
78
|
+
*/
|
|
79
|
+
function getDestinationTranscoder(id) {
|
|
80
|
+
const destinationTranscoderEntry = Object.entries(transcodeDestinationMap).find(([key, value]) => key === id || value.transferSyntaxUid === id);
|
|
81
|
+
if (destinationTranscoderEntry) {
|
|
82
|
+
return destinationTranscoderEntry[1];
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Return a merged source and destination transcoder for the given transferSyntaxUid. Otherwise it returns undefined.
|
|
89
|
+
*
|
|
90
|
+
* @param {*} transferSyntaxUid
|
|
91
|
+
* @returns
|
|
92
|
+
*/
|
|
93
|
+
function getTranscoder(transferSyntaxUid, { contentType, verbose }) {
|
|
94
|
+
if (verbose) {
|
|
95
|
+
console.log(`getTranscoder for ${transferSyntaxUid}`);
|
|
96
|
+
}
|
|
97
|
+
const sourceTranscoder = transcodeSourceMap[transferSyntaxUid];
|
|
98
|
+
const destinationTranscoder = getDestinationTranscoder(contentType);
|
|
99
|
+
if (!sourceTranscoder || !destinationTranscoder) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
transferSyntaxUid: destinationTranscoder.transferSyntaxUid,
|
|
105
|
+
transcodeOp: sourceTranscoder.transcodeOp | destinationTranscoder.transcodeOp, // eslint-disable-line no-bitwise
|
|
106
|
+
alias: sourceTranscoder.alias,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Tell whether given id contain transferSyntaxUid which can be transcoded or not.
|
|
113
|
+
* @param {*} id object containing transferSyntaxUid
|
|
114
|
+
* @param {*} options runner options
|
|
115
|
+
*/
|
|
116
|
+
function shouldTranscodeImageFrame(id, options) {
|
|
117
|
+
if (!options.recompress) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isValidTranscoder() {
|
|
122
|
+
const { transferSyntaxUid } = id;
|
|
123
|
+
const transcoder = getTranscoder(transferSyntaxUid, options);
|
|
124
|
+
return transcoder && transcoder.transferSyntaxUid && options.recompress.includes(transcoder.alias);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return isValidTranscoder();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* It tells if we should use transcoded image to generate thumb or not.
|
|
132
|
+
*
|
|
133
|
+
* @param {*} id
|
|
134
|
+
* @param {*} options
|
|
135
|
+
* @returns True if transcoder.alias is present on intersection of recompressThumb and recompress options.
|
|
136
|
+
*/
|
|
137
|
+
function shouldThumbUseTranscoded(id, options) {
|
|
138
|
+
if (!options.recompressThumb) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isValidTranscoder() {
|
|
143
|
+
const { transferSyntaxUid } = id;
|
|
144
|
+
const transcoder = getTranscoder(transferSyntaxUid, options);
|
|
145
|
+
const result = transcoder && transcoder.transferSyntaxUid && options.recompress.includes(transcoder.alias) && options.recompressThumb.includes(transcoder.alias);
|
|
146
|
+
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return isValidTranscoder();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function transcodeLog(options, msg, error = "") {
|
|
154
|
+
if (options.verbose) {
|
|
155
|
+
console.log(`\x1b[34m${msg}\x1b[0m`, error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Transcode imageFrame. It uses transcodeMap to define the target encoding based on source encoding.
|
|
161
|
+
*
|
|
162
|
+
* @param {*} id object containing transferSyntaxUid
|
|
163
|
+
* @param {*} targetId target object containing transferSyntaxUid
|
|
164
|
+
* @param {*} imageFrame data to be transcoded
|
|
165
|
+
* @param {*} dataSet related image frame dataset
|
|
166
|
+
* @param {*} options runner options
|
|
167
|
+
* @returns object result for transcoding operation with id and image frame.
|
|
168
|
+
*/
|
|
169
|
+
async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options = {}) {
|
|
170
|
+
let targetId = targetIdSrc;
|
|
171
|
+
let result = {};
|
|
172
|
+
|
|
173
|
+
if (!shouldTranscodeImageFrame(id, options)) {
|
|
174
|
+
return {
|
|
175
|
+
id,
|
|
176
|
+
imageFrame,
|
|
177
|
+
done: false,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const transcoder = getTranscoder(id.transferSyntaxUid, options);
|
|
182
|
+
|
|
183
|
+
// last chance to prevent transcoding
|
|
184
|
+
if (targetId.transferSyntaxUid !== transcoder.transferSyntaxUid) {
|
|
185
|
+
return {
|
|
186
|
+
id,
|
|
187
|
+
imageFrame,
|
|
188
|
+
done: false,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (options.verbose) {
|
|
193
|
+
console.log("Transcoding to", transcoder.transferSyntaxUid);
|
|
194
|
+
}
|
|
195
|
+
const imageInfo = getImageInfo(dataSet);
|
|
196
|
+
let done = false;
|
|
197
|
+
let processResultMsg = "";
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
switch (transcoder.transcodeOp) {
|
|
201
|
+
case transcodeOp.transcode:
|
|
202
|
+
transcodeLog(options, `Full transcoding image from \x1b[43m${id.transferSyntaxUid}\x1b[0m to \x1b[43m${targetId.transferSyntaxUid}\x1b[0m`);
|
|
203
|
+
|
|
204
|
+
result = await dicomCodec.transcode(imageFrame, imageInfo, id.transferSyntaxUid, targetId.transferSyntaxUid);
|
|
205
|
+
|
|
206
|
+
processResultMsg = `Transcoding finished`;
|
|
207
|
+
break;
|
|
208
|
+
case transcodeOp.encode:
|
|
209
|
+
transcodeLog(options, `Encoding image to \x1b[43m${targetId.transferSyntaxUid}\x1b[0m`);
|
|
210
|
+
|
|
211
|
+
result = await dicomCodec.encode(imageFrame, imageInfo, targetId.transferSyntaxUid);
|
|
212
|
+
|
|
213
|
+
processResultMsg = `Encoding finished`;
|
|
214
|
+
break;
|
|
215
|
+
case transcodeOp.decode:
|
|
216
|
+
transcodeLog(options, `Decoding image from \x1b[43m${id.transferSyntaxUid}\x1b[0m`);
|
|
217
|
+
result = await dicomCodec.decode(imageFrame, imageInfo, id.transferSyntaxUid);
|
|
218
|
+
|
|
219
|
+
processResultMsg = `Decoding finished`;
|
|
220
|
+
break;
|
|
221
|
+
default:
|
|
222
|
+
processResultMsg = "";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
done = !!result.imageFrame;
|
|
226
|
+
} catch (e) {
|
|
227
|
+
transcodeLog(options, "Failed to transcode image", e);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// recover transfer syntax
|
|
231
|
+
if (!done) {
|
|
232
|
+
targetId = id;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// log final message only if resultMsg is present
|
|
236
|
+
if (processResultMsg) {
|
|
237
|
+
processResultMsg += done ? " successfully" : " unsuccessfully";
|
|
238
|
+
transcodeLog(options, processResultMsg);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const _imageFrame = result.imageFrame ?? imageFrame;
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
id: targetId,
|
|
245
|
+
imageFrame: _imageFrame,
|
|
246
|
+
done,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Returns transcoded id object based on the given id's transferSyntaxUid. If transcoding should not occur it will return original id value.
|
|
252
|
+
* It does not mutate id param.
|
|
253
|
+
*
|
|
254
|
+
* @param {*} id Object that contains (but not only) data transferSyntaxUid.
|
|
255
|
+
* @param {*} options runner options.
|
|
256
|
+
* @returns Transcoded id object
|
|
257
|
+
*/
|
|
258
|
+
function transcodeId(id, options) {
|
|
259
|
+
if (!shouldTranscodeImageFrame(id, options)) {
|
|
260
|
+
return id;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const targetId = { ...id };
|
|
264
|
+
const { transferSyntaxUid } = getTranscoder(id.transferSyntaxUid, options);
|
|
265
|
+
|
|
266
|
+
targetId.transferSyntaxUid = transferSyntaxUid;
|
|
267
|
+
|
|
268
|
+
return targetId;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Returns transcoded metadata based on the given id's transferSyntaxUid.
|
|
273
|
+
* Basically it changes the metadata AvailableTransferSyntaxUID tag's value to be transcodedId.transferSyntaxUid
|
|
274
|
+
* If transcoding should not occur it will return original metadata value.
|
|
275
|
+
*
|
|
276
|
+
* It does not mutate metadata param.
|
|
277
|
+
* @param {*} metadata Metadata object.
|
|
278
|
+
* @param {*} id Object that contains (but not only) data transferSyntaxUid.
|
|
279
|
+
* @param {*} options runner options.
|
|
280
|
+
* @returns Transcoded metadata.
|
|
281
|
+
*/
|
|
282
|
+
function transcodeMetadata(metadata, id, options) {
|
|
283
|
+
if (!shouldTranscodeImageFrame(id, options)) {
|
|
284
|
+
return metadata;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const transcodedId = transcodeId(id, options);
|
|
288
|
+
|
|
289
|
+
const result = { ...metadata };
|
|
290
|
+
|
|
291
|
+
if (result[Tags.AvailableTransferSyntaxUID]) {
|
|
292
|
+
Tags.setValue(result,Tags.AvailableTransferSyntaxUID, transcodedId.transferSyntaxUid);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
exports.shouldTranscodeImageFrame = shouldTranscodeImageFrame;
|
|
299
|
+
exports.shouldThumbUseTranscoded = shouldThumbUseTranscoded;
|
|
300
|
+
exports.transcodeId = transcodeId;
|
|
301
|
+
exports.getDestinationTranscoder = getDestinationTranscoder;
|
|
302
|
+
exports.transcodeImageFrame = transcodeImageFrame;
|
|
303
|
+
exports.transcodeMetadata = transcodeMetadata;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const getNumberOfFrames = require("./getNumberOfFrames");
|
|
2
|
+
const getUncompressedImageFrame = require("./getUncompressedImageFrame");
|
|
3
|
+
const getEncapsulatedImageFrame = require("./getEncapsulatedImageFrame");
|
|
4
|
+
const { isVideo } = require("../writer/VideoWriter");
|
|
5
|
+
|
|
6
|
+
const areFramesAreFragmented = (attr, numberOfFrames) => attr.encapsulatedPixelData && numberOfFrames != attr.fragments.length;
|
|
7
|
+
|
|
8
|
+
const getFrameSize = (dataSet) => {
|
|
9
|
+
const rows = dataSet.uint16("x00280010");
|
|
10
|
+
const columns = dataSet.uint16("x00280011");
|
|
11
|
+
const samplesPerPixel = dataSet.uint16("x00280002");
|
|
12
|
+
const bitsAllocated = dataSet.uint16("x00280100");
|
|
13
|
+
return (rows * columns * samplesPerPixel * bitsAllocated) / 8;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const extractImageFrames = async (dataSet, attr, vr, callback) => {
|
|
17
|
+
const numberOfFrames = getNumberOfFrames(dataSet);
|
|
18
|
+
const framesAreFragmented = areFramesAreFragmented(attr, numberOfFrames);
|
|
19
|
+
const uncompressedFrameSize = getFrameSize(dataSet);
|
|
20
|
+
|
|
21
|
+
const videoType = isVideo(dataSet);
|
|
22
|
+
if (videoType) {
|
|
23
|
+
// The top level function is asynchronous, so this one doesn't need an additional await
|
|
24
|
+
return callback.videoWriter(dataSet);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let BulkDataURI;
|
|
28
|
+
|
|
29
|
+
for (let frameIndex = 0; frameIndex < numberOfFrames; frameIndex++) {
|
|
30
|
+
if (attr.encapsulatedPixelData) {
|
|
31
|
+
const compressedFrame = getEncapsulatedImageFrame(dataSet, attr, frameIndex, framesAreFragmented);
|
|
32
|
+
BulkDataURI = await callback.imageFrame(compressedFrame, { dataSet });
|
|
33
|
+
} else {
|
|
34
|
+
const uncompressedFrame = getUncompressedImageFrame(dataSet, attr, frameIndex, uncompressedFrameSize);
|
|
35
|
+
BulkDataURI = await callback.imageFrame(uncompressedFrame, { dataSet });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return BulkDataURI;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
module.exports = extractImageFrames;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const getVR = require("./getVR");
|
|
2
|
+
const getValue = require("./getValue");
|
|
3
|
+
const Tags = require("../dictionary/Tags");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get dataset.
|
|
7
|
+
*
|
|
8
|
+
* ParentAttr is used to control root/non-root attr scenarios (such as pixel data).
|
|
9
|
+
*
|
|
10
|
+
* @param {*} dataSet
|
|
11
|
+
* @param {*} callback
|
|
12
|
+
* @param {*} options
|
|
13
|
+
* @param {*} parentAttr Parent reference for sequence element tags.
|
|
14
|
+
* @returns
|
|
15
|
+
*/
|
|
16
|
+
async function getDataSet(dataSet, callback, options, parentAttr = undefined) {
|
|
17
|
+
const metadata = {};
|
|
18
|
+
|
|
19
|
+
// iterate over dataSet attributes in order
|
|
20
|
+
for (const tag in dataSet.elements) {
|
|
21
|
+
// Raw versions have the x in front of them
|
|
22
|
+
if (tag != Tags.RawTransferSyntaxUID && tag >= Tags.RawMinTag && tag < Tags.RawFirstBodyTag) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const attr = dataSet.elements[tag];
|
|
26
|
+
/* eslint-disable-next-line no-use-before-define */
|
|
27
|
+
await attributeToJS(metadata, tag, dataSet, attr, callback, options, parentAttr);
|
|
28
|
+
}
|
|
29
|
+
if (metadata[Tags.TransferSyntaxUID]) {
|
|
30
|
+
// console.log(`Found tsuid ${JSON.stringify(metadata[Tags.TransferSyntaxUID])} assigning to ${Tags.AvailableTransferSyntaxUID}`)
|
|
31
|
+
metadata[Tags.AvailableTransferSyntaxUID] = metadata[Tags.TransferSyntaxUID];
|
|
32
|
+
delete metadata[Tags.TransferSyntaxUID];
|
|
33
|
+
}
|
|
34
|
+
return { metadata };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function attributeToJS(metadataSrc, tag, dataSet, attr, callback, options, parentAttr) {
|
|
38
|
+
const metadata = metadataSrc;
|
|
39
|
+
const vr = getVR(attr);
|
|
40
|
+
const value = await getValue(dataSet, attr, vr, getDataSet, callback, options, parentAttr);
|
|
41
|
+
const key = tag.substring(1).toUpperCase();
|
|
42
|
+
if (value === undefined || value === null || value.length === 0) {
|
|
43
|
+
if (!vr) return;
|
|
44
|
+
metadata[key] = {
|
|
45
|
+
vr,
|
|
46
|
+
};
|
|
47
|
+
} else if (value.InlineBinary || value.BulkDataURI) {
|
|
48
|
+
metadata[key] = {
|
|
49
|
+
vr,
|
|
50
|
+
...value,
|
|
51
|
+
};
|
|
52
|
+
} else {
|
|
53
|
+
metadata[key] = {
|
|
54
|
+
vr,
|
|
55
|
+
Value: value,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = getDataSet;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const dicomParser = require("dicom-parser");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Function to deal with extracting an image frame from an encapsulated data set.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const getEncapsulatedImageFrame = (dataSet, attr, frameIndex, framesAreFragmented) => {
|
|
8
|
+
if (dataSet.elements.x7fe00010 && dataSet.elements.x7fe00010.basicOffsetTable.length) {
|
|
9
|
+
// Basic Offset Table is not empty
|
|
10
|
+
return dicomParser.readEncapsulatedImageFrame(dataSet, dataSet.elements.x7fe00010, frameIndex);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Empty basic offset table
|
|
14
|
+
if (framesAreFragmented) {
|
|
15
|
+
const basicOffsetTable = dicomParser.createJPEGBasicOffsetTable(dataSet, dataSet.elements.x7fe00010);
|
|
16
|
+
|
|
17
|
+
return dicomParser.readEncapsulatedImageFrame(dataSet, dataSet.elements.x7fe00010, frameIndex, basicOffsetTable);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return dicomParser.readEncapsulatedPixelDataFromFragments(dataSet, dataSet.elements.x7fe00010, frameIndex);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
module.exports = getEncapsulatedImageFrame;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const getNumberOfFrames = (dataSet) => {
|
|
2
|
+
if (dataSet.elements.x00280008) {
|
|
3
|
+
const numberOfFramesString = dataSet.string("x00280008");
|
|
4
|
+
if (numberOfFramesString.length === 0) {
|
|
5
|
+
return 1;
|
|
6
|
+
}
|
|
7
|
+
const numberOfFrames = parseInt(numberOfFramesString);
|
|
8
|
+
return numberOfFrames;
|
|
9
|
+
}
|
|
10
|
+
return 1;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
module.exports = getNumberOfFrames;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
function getUncompressedImageFrame(dataSet, attr, frame, uncompressedFrameSize) {
|
|
2
|
+
const start = attr.dataOffset + frame * uncompressedFrameSize;
|
|
3
|
+
const binaryValue = dataSet.byteArray.slice(start, start + uncompressedFrameSize);
|
|
4
|
+
return binaryValue;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
module.exports = getUncompressedImageFrame;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const dataDictionary = require("../dictionary/dataDictionary");
|
|
2
|
+
|
|
3
|
+
const getVR = (attr) => {
|
|
4
|
+
if (attr.vr) {
|
|
5
|
+
return attr.vr;
|
|
6
|
+
}
|
|
7
|
+
// lookup the vr using the data dictionary
|
|
8
|
+
const tag = attr.tag.substring(1).toUpperCase();
|
|
9
|
+
const dataDictAttr = dataDictionary[tag];
|
|
10
|
+
if (dataDictAttr) {
|
|
11
|
+
return dataDictAttr.vr;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
module.exports = getVR;
|