@radicalimaging/static-wado-creator 1.7.4 → 1.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DeleteStudy.js +16 -0
- package/dist/RejectInstance.js +25 -0
- package/dist/StaticWado.js +419 -0
- package/dist/canvas-xaqc8v6y.node +0 -0
- package/dist/createMain.js +18 -0
- package/dist/createPart10.js +113 -0
- package/dist/createThumbnail.js +163 -0
- package/dist/deleteMain.js +12 -0
- package/dist/groupMain.js +17 -0
- package/dist/index.js +500838 -0
- package/dist/index.mjs +18 -0
- package/dist/indexMain.js +10 -0
- package/dist/instanceMain.js +16 -0
- package/dist/metadataMain.js +15 -0
- package/dist/mkdicomwebConfig.js +342 -0
- package/dist/operation/InstanceDeduplicate.js +103 -0
- package/dist/operation/ScanStudy.js +47 -0
- package/dist/operation/StudyData.js +520 -0
- package/dist/operation/ThumbnailService.js +198 -0
- package/dist/operation/adapter/decodeImage.js +12 -0
- package/dist/operation/adapter/getImageInfo.js +38 -0
- package/dist/operation/adapter/transcodeImage.js +519 -0
- package/dist/operation/extractImageFrames.js +55 -0
- package/dist/operation/getDataSet.js +65 -0
- package/dist/operation/getEncapsulatedImageFrame.js +35 -0
- package/dist/operation/getNumberOfFrames.js +13 -0
- package/dist/operation/getUncompressedImageFrame.js +7 -0
- package/dist/operation/getVR.js +24 -0
- package/dist/operation/getValue.js +216 -0
- package/dist/operation/validateMetadata.js +40 -0
- package/dist/program/index.js +14 -0
- package/dist/rejectMain.js +12 -0
- package/dist/serverMain.js +21 -0
- package/dist/util/IdCreator.js +42 -0
- package/dist/util/adaptProgramOpts.js +39 -0
- package/dist/util/compressionOptionParser.js +13 -0
- package/dist/util/resultsReport.ts +87 -0
- package/dist/writer/CompleteStudyWriter.js +129 -0
- package/dist/writer/DeduplicateWriter.js +33 -0
- package/dist/writer/ExpandUriPath.js +16 -0
- package/dist/writer/HashDataWriter.js +74 -0
- package/dist/writer/ImageFrameWriter.js +59 -0
- package/dist/writer/MultipartAttribute.js +8 -0
- package/dist/writer/MultipartHeader.js +14 -0
- package/dist/writer/RawDicomWriter.js +44 -0
- package/dist/writer/ThumbnailWriter.js +14 -0
- package/dist/writer/VideoWriter.js +53 -0
- package/dist/writer/WriteMultipart.js +22 -0
- package/dist/writer/WriteStream.js +64 -0
- package/package.json +11 -2
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
const dicomCodec = require('@cornerstonejs/dicom-codec');
|
|
2
|
+
const { Tags, replicate } = require('@radicalimaging/static-wado-util');
|
|
3
|
+
const getImageInfo = require('./getImageInfo');
|
|
4
|
+
|
|
5
|
+
dicomCodec.setConfig('verbose: false');
|
|
6
|
+
|
|
7
|
+
const transcodeOp = {
|
|
8
|
+
none: 0,
|
|
9
|
+
decode: 1,
|
|
10
|
+
encode: 2,
|
|
11
|
+
transcode: 3, // decode and then encode
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const jpegBeforeEncode = (options, encoder) => {
|
|
15
|
+
const quality = options.quality ?? 99;
|
|
16
|
+
// First value is to encode as reversible lossless colour
|
|
17
|
+
return encoder => {
|
|
18
|
+
console.log('Calling setQuality', quality, !!encoder.setQuality);
|
|
19
|
+
encoder.setQuality(quality);
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const jlsBeforeEncode = options => {
|
|
24
|
+
const delta = (options.delta ?? lossy) ? 3 : 0;
|
|
25
|
+
return encoder => encoder.setNearLossless(delta);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const htj2kBeforeEncode = options => {
|
|
29
|
+
const { lossy } = options;
|
|
30
|
+
const quality = lossy === true ? 0.002 : -1;
|
|
31
|
+
return encoder => encoder.setQuality(!lossy, quality);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Static mapping for transcoding decoder destination names/uids - currently only lei, jls and jls-lossy appear to work.
|
|
36
|
+
*/
|
|
37
|
+
const transcodeDestinationMap = {
|
|
38
|
+
lei: {
|
|
39
|
+
transferSyntaxUid: '1.2.840.10008.1.2.1',
|
|
40
|
+
transcodeOp: transcodeOp.none,
|
|
41
|
+
},
|
|
42
|
+
jls: {
|
|
43
|
+
transferSyntaxUid: '1.2.840.10008.1.2.4.80',
|
|
44
|
+
transcodeOp: transcodeOp.encode,
|
|
45
|
+
beforeEncode: jlsBeforeEncode.bind(null, { lossy: false }),
|
|
46
|
+
},
|
|
47
|
+
'jls-lossy': {
|
|
48
|
+
transferSyntaxUid: '1.2.840.10008.1.2.4.81',
|
|
49
|
+
transcodeOp: transcodeOp.encode,
|
|
50
|
+
beforeEncode: jlsBeforeEncode.bind(null, { lossy: true, delta: 3 }),
|
|
51
|
+
},
|
|
52
|
+
jp2: {
|
|
53
|
+
transferSyntaxUid: '1.2.840.10008.1.2.4.90',
|
|
54
|
+
transcodeOp: transcodeOp.encode,
|
|
55
|
+
},
|
|
56
|
+
'jp2-lossy': {
|
|
57
|
+
transferSyntaxUid: '1.2.840.10008.1.2.4.91',
|
|
58
|
+
transcodeOp: transcodeOp.encode,
|
|
59
|
+
},
|
|
60
|
+
jhc: {
|
|
61
|
+
transferSyntaxUid: '1.2.840.10008.1.2.4.201',
|
|
62
|
+
transcodeOp: transcodeOp.encode,
|
|
63
|
+
beforeEncode: htj2kBeforeEncode({}),
|
|
64
|
+
},
|
|
65
|
+
'jhc-lossy': {
|
|
66
|
+
transferSyntaxUid: '1.2.840.10008.1.2.4.202',
|
|
67
|
+
transcodeOp: transcodeOp.encode,
|
|
68
|
+
beforeEncode: htj2kBeforeEncode({ lossy: true }),
|
|
69
|
+
},
|
|
70
|
+
jpeg: {
|
|
71
|
+
transferSyntaxUid: '1.2.840.10008.1.2.4.50',
|
|
72
|
+
transcodeOp: transcodeOp.encode,
|
|
73
|
+
// beforeEncode: jpegBeforeEncode({}),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const transcodeSourceMap = {
|
|
78
|
+
'1.2.840.10008.1.2': {
|
|
79
|
+
transcodeOp: transcodeOp.decode,
|
|
80
|
+
alias: 'uncompressed',
|
|
81
|
+
},
|
|
82
|
+
'1.2.840.10008.1.2.1': {
|
|
83
|
+
transcodeOp: transcodeOp.none,
|
|
84
|
+
alias: 'uncompressed',
|
|
85
|
+
},
|
|
86
|
+
'1.2.840.10008.1.2.2': {
|
|
87
|
+
transcodeOp: transcodeOp.decode,
|
|
88
|
+
alias: 'uncompressed',
|
|
89
|
+
},
|
|
90
|
+
'1.2.840.10008.1.2.4.50': {
|
|
91
|
+
transcodeOp: transcodeOp.decode,
|
|
92
|
+
alias: 'jpeg',
|
|
93
|
+
},
|
|
94
|
+
'1.2.840.10008.1.2.4.57': {
|
|
95
|
+
transcodeOp: transcodeOp.decode,
|
|
96
|
+
alias: 'jls',
|
|
97
|
+
},
|
|
98
|
+
'1.2.840.10008.1.2.4.70': {
|
|
99
|
+
transcodeOp: transcodeOp.decode,
|
|
100
|
+
alias: 'jpll',
|
|
101
|
+
},
|
|
102
|
+
'1.2.840.10008.1.2.4.80': {
|
|
103
|
+
transcodeOp: transcodeOp.decode,
|
|
104
|
+
alias: 'jls',
|
|
105
|
+
},
|
|
106
|
+
'1.2.840.10008.1.2.4.90': {
|
|
107
|
+
transcodeOp: transcodeOp.decode,
|
|
108
|
+
alias: 'jp2',
|
|
109
|
+
},
|
|
110
|
+
'1.2.840.10008.1.2.4.91': {
|
|
111
|
+
transcodeOp: transcodeOp.decode,
|
|
112
|
+
alias: 'jp2',
|
|
113
|
+
},
|
|
114
|
+
'3.2.840.10008.1.2.4.96': {
|
|
115
|
+
transcodeOp: transcodeOp.decode,
|
|
116
|
+
alias: 'jhc',
|
|
117
|
+
},
|
|
118
|
+
'1.2.840.10008.1.2.4.202': {
|
|
119
|
+
transcodeOp: transcodeOp.decode,
|
|
120
|
+
alias: 'jhc',
|
|
121
|
+
},
|
|
122
|
+
'1.2.840.10008.1.2.5': {
|
|
123
|
+
transcodeOp: transcodeOp.decode,
|
|
124
|
+
alias: 'rle',
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get an existing destination transcoder from destination map.
|
|
130
|
+
*
|
|
131
|
+
* @param {*} id Destination type to compress to
|
|
132
|
+
* @returns A partial transcoder definition. Otherwise it returns undefined.
|
|
133
|
+
*/
|
|
134
|
+
function getDestinationTranscoder(id) {
|
|
135
|
+
const destinationTranscoderEntry =
|
|
136
|
+
transcodeDestinationMap[id] ||
|
|
137
|
+
Object.values(transcodeDestinationMap).find(value => value.transferSyntaxUid === id);
|
|
138
|
+
return destinationTranscoderEntry;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Return a merged source and destination transcoder for the given transferSyntaxUid. Otherwise it returns undefined.
|
|
143
|
+
*
|
|
144
|
+
* @param {*} transferSyntaxUid
|
|
145
|
+
* @returns
|
|
146
|
+
*/
|
|
147
|
+
function getTranscoder(
|
|
148
|
+
transferSyntaxUid,
|
|
149
|
+
{ contentType: greyContentType, colorContentType },
|
|
150
|
+
samplesPerPixel
|
|
151
|
+
) {
|
|
152
|
+
const contentType = samplesPerPixel === 3 ? colorContentType : greyContentType;
|
|
153
|
+
const sourceTranscoder = transcodeSourceMap[transferSyntaxUid];
|
|
154
|
+
const destinationTranscoder = getDestinationTranscoder(contentType);
|
|
155
|
+
if (!sourceTranscoder || !destinationTranscoder) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
transferSyntaxUid: destinationTranscoder.transferSyntaxUid,
|
|
161
|
+
transcodeOp: sourceTranscoder.transcodeOp | destinationTranscoder.transcodeOp,
|
|
162
|
+
alias: sourceTranscoder.alias,
|
|
163
|
+
beforeEncode: destinationTranscoder.beforeEncode,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Tell whether given id contain transferSyntaxUid which can be transcoded or not.
|
|
169
|
+
* @param {*} id object containing transferSyntaxUid
|
|
170
|
+
* @param {*} options runner options
|
|
171
|
+
*/
|
|
172
|
+
function shouldTranscodeImageFrame(id, options, samplesPerPixel) {
|
|
173
|
+
const { recompress: recompressGrey, recompressColor } = options;
|
|
174
|
+
const recompress = samplesPerPixel === 3 ? recompressColor : recompressGrey;
|
|
175
|
+
if (!recompress) {
|
|
176
|
+
console.verbose('Not transcoding because recompress not set');
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const { transferSyntaxUid } = id;
|
|
181
|
+
const transcoder = getTranscoder(transferSyntaxUid, options, samplesPerPixel);
|
|
182
|
+
const validTranscoder = transcoder && transcoder.transferSyntaxUid;
|
|
183
|
+
if (!validTranscoder) {
|
|
184
|
+
console.verbose(
|
|
185
|
+
'Not transcoding because no decoder found for',
|
|
186
|
+
transferSyntaxUid,
|
|
187
|
+
samplesPerPixel
|
|
188
|
+
);
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const validRecompress = recompress.includes('true') || recompress.includes(transcoder.alias);
|
|
192
|
+
if (!validRecompress) {
|
|
193
|
+
console.verbose(
|
|
194
|
+
'Not transcoding because recompress',
|
|
195
|
+
recompress,
|
|
196
|
+
'does not include',
|
|
197
|
+
transcoder.alias
|
|
198
|
+
);
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* It tells if we should use transcoded image to generate thumb or not.
|
|
207
|
+
*
|
|
208
|
+
* @param {*} id
|
|
209
|
+
* @param {*} options
|
|
210
|
+
* @returns True if transcoder.alias is present on intersection of recompressThumb and recompress options.
|
|
211
|
+
*/
|
|
212
|
+
function shouldThumbUseTranscoded(id, options) {
|
|
213
|
+
if (!options.recompressThumb) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isValidTranscoder() {
|
|
218
|
+
const { transferSyntaxUid } = id;
|
|
219
|
+
// Ignore the samples per pixel for thumbnails
|
|
220
|
+
const transcoder = getTranscoder(transferSyntaxUid, options, 'thumbnail');
|
|
221
|
+
const result =
|
|
222
|
+
transcoder &&
|
|
223
|
+
transcoder.transferSyntaxUid &&
|
|
224
|
+
options.recompress.includes(transcoder.alias) &&
|
|
225
|
+
options.recompressThumb.includes(transcoder.alias);
|
|
226
|
+
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return isValidTranscoder();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function transcodeLog(options, msg, error = '') {
|
|
234
|
+
if (options.verbose) {
|
|
235
|
+
console.log(`\x1b[34m${msg}\x1b[0m`, error);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function scale(imageFrame, imageInfo) {
|
|
240
|
+
const { rows, columns, bitsPerPixel, pixelRepresentation, samplesPerPixel } = imageInfo;
|
|
241
|
+
let ArrayConstructor = Float32Array;
|
|
242
|
+
if (bitsPerPixel === 8) {
|
|
243
|
+
ArrayConstructor = pixelRepresentation ? Int8Array : Uint8Array;
|
|
244
|
+
} else if (bitsPerPixel > 8 && bitsPerPixel <= 16) {
|
|
245
|
+
ArrayConstructor = pixelRepresentation ? Int16Array : Uint16Array;
|
|
246
|
+
}
|
|
247
|
+
const src = {
|
|
248
|
+
pixelData: new ArrayConstructor(imageFrame.buffer),
|
|
249
|
+
rows,
|
|
250
|
+
columns,
|
|
251
|
+
samplesPerPixel,
|
|
252
|
+
};
|
|
253
|
+
const dest = {
|
|
254
|
+
rows: Math.round(rows / 4),
|
|
255
|
+
columns: Math.round(columns / 4),
|
|
256
|
+
samplesPerPixel,
|
|
257
|
+
};
|
|
258
|
+
dest.pixelData = new ArrayConstructor(dest.rows * dest.columns * samplesPerPixel);
|
|
259
|
+
replicate(src, dest);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
imageFrame: dest.pixelData,
|
|
263
|
+
imageInfo: {
|
|
264
|
+
...imageInfo,
|
|
265
|
+
rows: dest.rows,
|
|
266
|
+
columns: dest.columns,
|
|
267
|
+
width: dest.columns,
|
|
268
|
+
height: dest.rows,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Generates a lossy version of the image.
|
|
274
|
+
* Options for lossy are:
|
|
275
|
+
* * htj2k - generates a full resolution but high loss htj2k representation of the frames
|
|
276
|
+
* * jls - generates a +/- 2 value, maximum 128 on an edge JLS representation
|
|
277
|
+
*
|
|
278
|
+
* In either case, saves it to <sopUID>/fsiz/frameNo as multipart related
|
|
279
|
+
*/
|
|
280
|
+
async function generateLossyImage(id, decoded, options) {
|
|
281
|
+
if (!options.alternate) return;
|
|
282
|
+
if (!decoded?.imageFrame) return;
|
|
283
|
+
console.verbose('Writing alternate thumbnail');
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
let { imageFrame, imageInfo } = decoded;
|
|
287
|
+
const lossyId = {
|
|
288
|
+
...id,
|
|
289
|
+
imageFrameRootPath: id.imageFrameRootPath.replace('frames', options.alternateName),
|
|
290
|
+
transferSyntaxUid: transcodeDestinationMap.jhc.transferSyntaxUid,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
let lossy = true;
|
|
294
|
+
if (options.alternateThumbnail && imageInfo.rows >= 512) {
|
|
295
|
+
const scaled = scale(imageFrame, imageInfo);
|
|
296
|
+
if (!scaled) {
|
|
297
|
+
console.log("Couldn't scale");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
imageFrame = Buffer.from(scaled.imageFrame.buffer);
|
|
301
|
+
imageInfo = scaled.imageInfo;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (options.alternate === 'jls') {
|
|
305
|
+
lossyId.transferSyntaxUid = transcodeDestinationMap['jls-lossy'].transferSyntaxUid;
|
|
306
|
+
} else if (options.alternate === 'jlsLossless') {
|
|
307
|
+
lossyId.transferSyntaxUid = transcodeDestinationMap.jls.transferSyntaxUid;
|
|
308
|
+
lossy = false;
|
|
309
|
+
} else if (options.alternate === 'jhcLossless') {
|
|
310
|
+
lossy = false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const encodeOptions = {
|
|
314
|
+
beforeEncode: transcodeDestinationMap[options.alternate]?.beforeEncode,
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const lossyEncoding = await dicomCodec.encode(
|
|
318
|
+
imageFrame,
|
|
319
|
+
imageInfo,
|
|
320
|
+
lossyId.transferSyntaxUid,
|
|
321
|
+
encodeOptions
|
|
322
|
+
);
|
|
323
|
+
console.verbose(
|
|
324
|
+
'Encoded alternate',
|
|
325
|
+
lossyId.transferSyntaxUid,
|
|
326
|
+
'of size',
|
|
327
|
+
lossyEncoding.imageFrame.length
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
return { id: lossyId, imageFrame: lossyEncoding.imageFrame };
|
|
331
|
+
} catch (e) {
|
|
332
|
+
console.warn('Unable to create alternate:', e);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function isPalette(dataSet) {
|
|
337
|
+
return dataSet.string(Tags.RawPhotometricInterpretation) === 'PALETTE COLOR';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Transcode imageFrame. It uses transcodeMap to define the target encoding based on source encoding.
|
|
342
|
+
*
|
|
343
|
+
* @param {*} id object containing transferSyntaxUid
|
|
344
|
+
* @param {*} targetId target object containing transferSyntaxUid
|
|
345
|
+
* @param {*} imageFrame data to be transcoded
|
|
346
|
+
* @param {*} dataSet related image frame dataset
|
|
347
|
+
* @param {*} options runner options
|
|
348
|
+
* @returns object result for transcoding operation with id and image frame.
|
|
349
|
+
*/
|
|
350
|
+
async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options = {}) {
|
|
351
|
+
let targetId = targetIdSrc;
|
|
352
|
+
let result = {};
|
|
353
|
+
|
|
354
|
+
const samplesPerPixel = dataSet.uint16(Tags.RawSamplesPerPixel);
|
|
355
|
+
const planarConfiguration = dataSet.uint16('x00280006');
|
|
356
|
+
if (
|
|
357
|
+
!shouldTranscodeImageFrame(id, options, samplesPerPixel) ||
|
|
358
|
+
planarConfiguration === 1 ||
|
|
359
|
+
isPalette(dataSet)
|
|
360
|
+
) {
|
|
361
|
+
console.verbose("Shouldn't transcode");
|
|
362
|
+
return {
|
|
363
|
+
id,
|
|
364
|
+
imageFrame,
|
|
365
|
+
done: false,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const transcoder = getTranscoder(id.transferSyntaxUid, options, samplesPerPixel);
|
|
370
|
+
|
|
371
|
+
// Don't transcode if not required
|
|
372
|
+
if (targetId.transferSyntaxUid !== transcoder.transferSyntaxUid && !options.forceTranscode) {
|
|
373
|
+
console.verbose('Image is already in', targetId.transferSyntaxUid, 'not transcoding');
|
|
374
|
+
return {
|
|
375
|
+
id,
|
|
376
|
+
imageFrame,
|
|
377
|
+
done: false,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
console.verbose('Transcoding to', transcoder.transferSyntaxUid);
|
|
382
|
+
|
|
383
|
+
const imageInfo = getImageInfo(dataSet);
|
|
384
|
+
let done = false;
|
|
385
|
+
let processResultMsg = '';
|
|
386
|
+
const encodeOptions = {
|
|
387
|
+
beforeEncode: transcoder.beforeEncode,
|
|
388
|
+
};
|
|
389
|
+
let decoded;
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
switch (transcoder.transcodeOp) {
|
|
393
|
+
case transcodeOp.transcode:
|
|
394
|
+
transcodeLog(
|
|
395
|
+
options,
|
|
396
|
+
`Full transcoding image from \x1b[43m${id.transferSyntaxUid}\x1b[0m to \x1b[43m${targetId.transferSyntaxUid}\x1b[0m`
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
decoded = await dicomCodec.decode(imageFrame, imageInfo, id.transferSyntaxUid);
|
|
400
|
+
result = await dicomCodec.encode(
|
|
401
|
+
decoded.imageFrame,
|
|
402
|
+
decoded.imageInfo,
|
|
403
|
+
targetId.transferSyntaxUid,
|
|
404
|
+
encodeOptions
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
console.verbose(
|
|
408
|
+
'transcoded image to',
|
|
409
|
+
targetId.transferSyntaxUid,
|
|
410
|
+
'of size',
|
|
411
|
+
result.imageFrame.length
|
|
412
|
+
);
|
|
413
|
+
processResultMsg = `Transcoding finished`;
|
|
414
|
+
break;
|
|
415
|
+
case transcodeOp.encode:
|
|
416
|
+
transcodeLog(options, `Encoding image to \x1b[43m${targetId.transferSyntaxUid}\x1b[0m`);
|
|
417
|
+
|
|
418
|
+
result = await dicomCodec.encode(
|
|
419
|
+
imageFrame,
|
|
420
|
+
imageInfo,
|
|
421
|
+
targetId.transferSyntaxUid,
|
|
422
|
+
encodeOptions
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
processResultMsg = `Encoding finished`;
|
|
426
|
+
break;
|
|
427
|
+
case transcodeOp.decode:
|
|
428
|
+
transcodeLog(options, `Decoding image from \x1b[43m${id.transferSyntaxUid}\x1b[0m`);
|
|
429
|
+
result = await dicomCodec.decode(imageFrame, imageInfo, id.transferSyntaxUid);
|
|
430
|
+
|
|
431
|
+
processResultMsg = `Decoding finished`;
|
|
432
|
+
break;
|
|
433
|
+
default:
|
|
434
|
+
processResultMsg = '';
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
done = !!result.imageFrame;
|
|
438
|
+
} catch (e) {
|
|
439
|
+
transcodeLog(options, 'Failed to transcode image', e);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// recover transfer syntax
|
|
443
|
+
if (!done) {
|
|
444
|
+
targetId = id;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// log final message only if resultMsg is present
|
|
448
|
+
if (processResultMsg) {
|
|
449
|
+
processResultMsg += done ? ' successfully' : ' unsuccessfully';
|
|
450
|
+
transcodeLog(options, processResultMsg);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const _imageFrame = result.imageFrame ?? imageFrame;
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
id: targetId,
|
|
457
|
+
imageFrame: _imageFrame,
|
|
458
|
+
done,
|
|
459
|
+
decoded,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Returns transcoded id object based on the given id's transferSyntaxUid. If transcoding should not occur it will return original id value.
|
|
465
|
+
* It does not mutate id param.
|
|
466
|
+
*
|
|
467
|
+
* @param {*} id Object that contains (but not only) data transferSyntaxUid.
|
|
468
|
+
* @param {*} options runner options.
|
|
469
|
+
* @returns Transcoded id object
|
|
470
|
+
*/
|
|
471
|
+
function transcodeId(id, options, samplesPerPixel) {
|
|
472
|
+
if (!shouldTranscodeImageFrame(id, options, samplesPerPixel)) {
|
|
473
|
+
return id;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const targetId = { ...id };
|
|
477
|
+
const { transferSyntaxUid } = getTranscoder(id.transferSyntaxUid, options, samplesPerPixel);
|
|
478
|
+
|
|
479
|
+
targetId.transferSyntaxUid = transferSyntaxUid;
|
|
480
|
+
|
|
481
|
+
return targetId;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Returns transcoded metadata based on the given id's transferSyntaxUid.
|
|
486
|
+
* Basically it changes the metadata AvailableTransferSyntaxUID tag's value to be transcodedId.transferSyntaxUid
|
|
487
|
+
* If transcoding should not occur it will return original metadata value.
|
|
488
|
+
*
|
|
489
|
+
* It does not mutate metadata param.
|
|
490
|
+
* @param {*} metadata Metadata object.
|
|
491
|
+
* @param {*} id Object that contains (but not only) data transferSyntaxUid.
|
|
492
|
+
* @param {*} options runner options.
|
|
493
|
+
* @returns Transcoded metadata.
|
|
494
|
+
*/
|
|
495
|
+
function transcodeMetadata(metadata, id, options) {
|
|
496
|
+
const samplesPerPixel = Tags.getValue(metadata, Tags.SamplesPerPixel);
|
|
497
|
+
if (!shouldTranscodeImageFrame(id, options, samplesPerPixel)) {
|
|
498
|
+
return metadata;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const transcodedId = transcodeId(id, options, samplesPerPixel);
|
|
502
|
+
|
|
503
|
+
const result = { ...metadata };
|
|
504
|
+
|
|
505
|
+
if (result[Tags.AvailableTransferSyntaxUID]) {
|
|
506
|
+
Tags.setValue(result, Tags.AvailableTransferSyntaxUID, transcodedId.transferSyntaxUid);
|
|
507
|
+
console.verbose('Apply available tsuid', transcodeId.transferSyntaxUid);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
exports.shouldTranscodeImageFrame = shouldTranscodeImageFrame;
|
|
514
|
+
exports.shouldThumbUseTranscoded = shouldThumbUseTranscoded;
|
|
515
|
+
exports.transcodeId = transcodeId;
|
|
516
|
+
exports.getDestinationTranscoder = getDestinationTranscoder;
|
|
517
|
+
exports.transcodeImageFrame = transcodeImageFrame;
|
|
518
|
+
exports.transcodeMetadata = transcodeMetadata;
|
|
519
|
+
exports.generateLossyImage = generateLossyImage;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const { Stats } = require('@radicalimaging/static-wado-util');
|
|
2
|
+
|
|
3
|
+
const getNumberOfFrames = require('./getNumberOfFrames');
|
|
4
|
+
const getUncompressedImageFrame = require('./getUncompressedImageFrame');
|
|
5
|
+
const getEncapsulatedImageFrame = require('./getEncapsulatedImageFrame');
|
|
6
|
+
const { isVideo } = require('../writer/VideoWriter');
|
|
7
|
+
|
|
8
|
+
const areFramesAreFragmented = (attr, numberOfFrames) =>
|
|
9
|
+
attr.encapsulatedPixelData && numberOfFrames != attr.fragments.length;
|
|
10
|
+
|
|
11
|
+
const getFrameSize = dataSet => {
|
|
12
|
+
const rows = dataSet.uint16('x00280010');
|
|
13
|
+
const columns = dataSet.uint16('x00280011');
|
|
14
|
+
const samplesPerPixel = dataSet.uint16('x00280002');
|
|
15
|
+
const bitsAllocated = dataSet.uint16('x00280100');
|
|
16
|
+
return (rows * columns * samplesPerPixel * bitsAllocated) / 8;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const extractImageFrames = async (dataSet, attr, vr, callback) => {
|
|
20
|
+
const numberOfFrames = getNumberOfFrames(dataSet);
|
|
21
|
+
const framesAreFragmented = areFramesAreFragmented(attr, numberOfFrames);
|
|
22
|
+
const uncompressedFrameSize = getFrameSize(dataSet);
|
|
23
|
+
|
|
24
|
+
const videoType = isVideo(dataSet);
|
|
25
|
+
if (videoType) {
|
|
26
|
+
// The top level function is asynchronous, so this one doesn't need an additional await
|
|
27
|
+
return callback.videoWriter(dataSet);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let BulkDataURI;
|
|
31
|
+
|
|
32
|
+
for (let frameIndex = 0; frameIndex < numberOfFrames; frameIndex++) {
|
|
33
|
+
if (attr.encapsulatedPixelData) {
|
|
34
|
+
const compressedFrame = getEncapsulatedImageFrame(
|
|
35
|
+
dataSet,
|
|
36
|
+
attr,
|
|
37
|
+
frameIndex,
|
|
38
|
+
framesAreFragmented
|
|
39
|
+
);
|
|
40
|
+
BulkDataURI = await callback.imageFrame(compressedFrame, { dataSet });
|
|
41
|
+
Stats.OverallStats.add('Image Write', `Write image frame ${frameIndex + 1}`, 5000);
|
|
42
|
+
} else {
|
|
43
|
+
const uncompressedFrame = getUncompressedImageFrame(
|
|
44
|
+
dataSet,
|
|
45
|
+
attr,
|
|
46
|
+
frameIndex,
|
|
47
|
+
uncompressedFrameSize
|
|
48
|
+
);
|
|
49
|
+
BulkDataURI = await callback.imageFrame(uncompressedFrame, { dataSet });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return BulkDataURI;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
module.exports = extractImageFrames;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const { Tags } = require('@radicalimaging/static-wado-util');
|
|
2
|
+
const getVR = require('./getVR');
|
|
3
|
+
const getValue = require('./getValue');
|
|
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, optionsOrig, parentAttr = undefined) {
|
|
17
|
+
const metadata = {};
|
|
18
|
+
let options = optionsOrig;
|
|
19
|
+
|
|
20
|
+
// iterate over dataSet attributes in order
|
|
21
|
+
for (const tag in dataSet.elements) {
|
|
22
|
+
// Raw versions have the x in front of them
|
|
23
|
+
if (tag != Tags.RawTransferSyntaxUID && tag >= Tags.RawMinTag && tag < Tags.RawFirstBodyTag) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (tag === Tags.RawSpecificCharacterSet) {
|
|
27
|
+
const SpecificCharacterSet = dataSet.string(tag);
|
|
28
|
+
options = { ...options, SpecificCharacterSet };
|
|
29
|
+
}
|
|
30
|
+
const attr = dataSet.elements[tag];
|
|
31
|
+
|
|
32
|
+
await attributeToJS(metadata, tag, dataSet, attr, callback, options, parentAttr);
|
|
33
|
+
}
|
|
34
|
+
if (metadata[Tags.TransferSyntaxUID]) {
|
|
35
|
+
// console.log(`Found tsuid ${JSON.stringify(metadata[Tags.TransferSyntaxUID])} assigning to ${Tags.AvailableTransferSyntaxUID}`)
|
|
36
|
+
metadata[Tags.AvailableTransferSyntaxUID] = metadata[Tags.TransferSyntaxUID];
|
|
37
|
+
delete metadata[Tags.TransferSyntaxUID];
|
|
38
|
+
}
|
|
39
|
+
return { metadata };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function attributeToJS(metadataSrc, tag, dataSet, attr, callback, options, parentAttr) {
|
|
43
|
+
const metadata = metadataSrc;
|
|
44
|
+
const vr = getVR(attr);
|
|
45
|
+
const value = await getValue(dataSet, attr, vr, getDataSet, callback, options, parentAttr);
|
|
46
|
+
const key = tag.substring(1).toUpperCase();
|
|
47
|
+
if (value === undefined || value === null || value.length === 0) {
|
|
48
|
+
if (!vr) return;
|
|
49
|
+
metadata[key] = {
|
|
50
|
+
vr,
|
|
51
|
+
};
|
|
52
|
+
} else if (value.InlineBinary || value.BulkDataURI) {
|
|
53
|
+
metadata[key] = {
|
|
54
|
+
vr,
|
|
55
|
+
...value,
|
|
56
|
+
};
|
|
57
|
+
} else {
|
|
58
|
+
metadata[key] = {
|
|
59
|
+
vr,
|
|
60
|
+
Value: value,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = getDataSet;
|
|
@@ -0,0 +1,35 @@
|
|
|
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(
|
|
16
|
+
dataSet,
|
|
17
|
+
dataSet.elements.x7fe00010
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return dicomParser.readEncapsulatedImageFrame(
|
|
21
|
+
dataSet,
|
|
22
|
+
dataSet.elements.x7fe00010,
|
|
23
|
+
frameIndex,
|
|
24
|
+
basicOffsetTable
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return dicomParser.readEncapsulatedPixelDataFromFragments(
|
|
29
|
+
dataSet,
|
|
30
|
+
dataSet.elements.x7fe00010,
|
|
31
|
+
frameIndex
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
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;
|