@radicalimaging/static-wado-creator 1.7.3 → 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/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.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 -6
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const glob = require('glob');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { Tags, execSpawn, Stats } = require('@radicalimaging/static-wado-util');
|
|
5
|
+
const { shouldThumbUseTranscoded } = require('./adapter/transcodeImage');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Return the middle index of given list
|
|
9
|
+
* @param {*} listThumbs
|
|
10
|
+
* @returns
|
|
11
|
+
*/
|
|
12
|
+
function getThumbIndex(listThumbs) {
|
|
13
|
+
return Math.trunc(listThumbs / 2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ThumbObj wrapper containing both original and transcoded content.
|
|
18
|
+
*
|
|
19
|
+
* @typedef {Object} ThumbObjWrapper
|
|
20
|
+
* @property {Object} id object containing study properties such as seriesUid, instanceUid
|
|
21
|
+
* @property {*} transcodedId transcoded object containing study properties such as seriesUid, instanceUid (this might be the same as original id depending on options)
|
|
22
|
+
* @property {*} imageFrame original image frame
|
|
23
|
+
* @property {*} transcodedImageFrame transcoded image frame (this might be the same as original frame depending on options)
|
|
24
|
+
* @property {number} frameIndex
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* ThumbObj containing imageFrame, id to be used.
|
|
29
|
+
*
|
|
30
|
+
* @typedef {Object} ThumbObj
|
|
31
|
+
* @property {Object} id object containing study properties such as seriesUid, instanceUid
|
|
32
|
+
* @property {*} imageFrame
|
|
33
|
+
* @property {number} frameIndex
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Service to handle thumbnail creation.
|
|
38
|
+
* @class
|
|
39
|
+
* @constructor
|
|
40
|
+
* @property {Array<ThumbObj>} framesThumbnailObj Auxiliary list that contains all thumb obj.
|
|
41
|
+
* @property {ThumbObj} favoriteThumbnailObj Favorite thumb obj to be used on thumbnail creation
|
|
42
|
+
* @property {String} thumbFileName thumb file (with extension) to be used when creating the file.
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
class ThumbnailService {
|
|
46
|
+
constructor() {
|
|
47
|
+
this.framesThumbnailObj = [];
|
|
48
|
+
this.favoriteThumbnailObj = {};
|
|
49
|
+
this.thumbFileName = 'thumbnail';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Add thumbObj to list of possible thumbs.
|
|
54
|
+
* It might use original content or transcoded depending on options flags
|
|
55
|
+
* @param {ThumbObjWrapper} thumbObjWrapper
|
|
56
|
+
* @param {Object} programOpts
|
|
57
|
+
*/
|
|
58
|
+
queueThumbnail(thumbObjWrapper, programOpts) {
|
|
59
|
+
const { id, imageFrame, transcodedId, transcodedImageFrame, frameIndex } = thumbObjWrapper;
|
|
60
|
+
const getThumbContent = (originalContent, trancodedContent) =>
|
|
61
|
+
shouldThumbUseTranscoded(id, programOpts) ? trancodedContent : originalContent;
|
|
62
|
+
|
|
63
|
+
const thumbObj = {
|
|
64
|
+
imageFrame: getThumbContent(imageFrame, transcodedImageFrame),
|
|
65
|
+
id: getThumbContent(id, transcodedId),
|
|
66
|
+
frameIndex,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
this.framesThumbnailObj.push(thumbObj);
|
|
70
|
+
|
|
71
|
+
this.setFavoriteThumbnailObj();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setFavoriteThumbnailObj() {
|
|
75
|
+
const favIndex = getThumbIndex(this.framesThumbnailObj.length);
|
|
76
|
+
|
|
77
|
+
this.favoriteThumbnailObj = this.framesThumbnailObj[favIndex];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
ffmpeg(input, output) {
|
|
81
|
+
execSpawn(`ffmpeg -i "${input}" -y -f image2 -frames:v 1 -update true "${output}"`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
dcm2jpg(input, output, options) {
|
|
85
|
+
const script = ['dcm2jpg', `"${input}" "${output}"`];
|
|
86
|
+
if (options?.format) {
|
|
87
|
+
script.push('-F', options.format);
|
|
88
|
+
}
|
|
89
|
+
execSpawn(script.join(' '));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Generates a rendered copy of this, assuming --rendered is set.
|
|
93
|
+
* Requires dcm2jpg to be available.
|
|
94
|
+
*/
|
|
95
|
+
async generateRendered(itemid, dataSet, metadata, callback, options) {
|
|
96
|
+
if (!options.rendered) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const { id } = this.favoriteThumbnailObj;
|
|
100
|
+
const rendered = id.imageFrameRootPath.replace(/frames/, 'rendered');
|
|
101
|
+
return this.dcm2jpg(id.filename, rendered, { format: 'png' });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generates thumbnails for the levels: instances, series, study. This is asynchronous
|
|
106
|
+
*
|
|
107
|
+
* @param {*} dataSet
|
|
108
|
+
* @param {*} metadata
|
|
109
|
+
* @param {*} callback
|
|
110
|
+
*/
|
|
111
|
+
async generateThumbnails(itemId, dataSet, metadata, callback, options) {
|
|
112
|
+
const { imageFrame, id } = this.favoriteThumbnailObj;
|
|
113
|
+
|
|
114
|
+
// There are various reasons no thumbnails might be generated, so just return
|
|
115
|
+
if (!id) {
|
|
116
|
+
const pixelData = metadata[Tags.PixelData];
|
|
117
|
+
if (pixelData) {
|
|
118
|
+
const { BulkDataURI } = pixelData;
|
|
119
|
+
if (BulkDataURI?.indexOf('mp4')) {
|
|
120
|
+
fs.mkdirSync(`${itemId.sopInstanceRootPath}/rendered`, {
|
|
121
|
+
recursive: true,
|
|
122
|
+
});
|
|
123
|
+
const mp4Path = path.join(itemId.sopInstanceRootPath, 'rendered/index.mp4');
|
|
124
|
+
const thumbPath = path.join(itemId.sopInstanceRootPath, 'thumbnail');
|
|
125
|
+
console.verbose('MP4 - converting video format', mp4Path);
|
|
126
|
+
this.ffmpeg(mp4Path, thumbPath);
|
|
127
|
+
return thumbPath;
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
console.verbose('Series is of other type...', metadata[Tags.Modality]);
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!options.thumb) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (options.dcm2jpg) {
|
|
140
|
+
return this.dcm2jpg(id.filename, id.imageFrameRootPath.replace(/frames/, 'thumbnail'), {});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await callback.internalGenerateImage(
|
|
144
|
+
imageFrame,
|
|
145
|
+
dataSet,
|
|
146
|
+
metadata,
|
|
147
|
+
id.transferSyntaxUid,
|
|
148
|
+
async thumbBuffer => {
|
|
149
|
+
try {
|
|
150
|
+
if (thumbBuffer) {
|
|
151
|
+
await callback.thumbWriter(id.sopInstanceRootPath, this.thumbFileName, thumbBuffer);
|
|
152
|
+
|
|
153
|
+
this.copySyncThumbnail(id.sopInstanceRootPath, id.seriesRootPath);
|
|
154
|
+
this.copySyncThumbnail(id.seriesRootPath, id.studyPath);
|
|
155
|
+
Stats.StudyStats.add('Thumbnail Write', `Write thumbnail ${this.thumbFileName}`, 100);
|
|
156
|
+
}
|
|
157
|
+
return this.thumbFileName;
|
|
158
|
+
} catch (e) {
|
|
159
|
+
console.log("Couldn't generate thumbnail", this.thumbFileName, e);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Copy thumbnail from sourceFolder to targetFolderPath. It usually is used to copy from child to parent folder.
|
|
167
|
+
* If there is already some existing thumbnails under targetFolderPath it actually get thumb on the index defined by getThumbIndex.
|
|
168
|
+
* Copy is a syncrhonous process.
|
|
169
|
+
*
|
|
170
|
+
* @param {*} sourceFolderPath
|
|
171
|
+
* @param {*} targetFolderPath
|
|
172
|
+
* @returns
|
|
173
|
+
*/
|
|
174
|
+
async copySyncThumbnail(sourceFolderPath, targetFolderPath) {
|
|
175
|
+
const parentPathLevel = path.join(sourceFolderPath, '../');
|
|
176
|
+
const thumbFilesPath = glob.sync(`${parentPathLevel}*/${this.thumbFileName}`);
|
|
177
|
+
|
|
178
|
+
const thumbIndex = getThumbIndex(thumbFilesPath.length);
|
|
179
|
+
const thumbFilePath = thumbFilesPath[thumbIndex];
|
|
180
|
+
|
|
181
|
+
if (!fs.existsSync(thumbFilePath)) {
|
|
182
|
+
console.verbose('Thumbnail to copy does not exists');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
if (!fs.lstatSync(targetFolderPath).isDirectory()) {
|
|
188
|
+
throw new Error(`Target path: ${targetFolderPath} is not a directory`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fs.copyFileSync(thumbFilePath, `${targetFolderPath}/${this.thumbFileName}`);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
console.verbose('The file could not be copied', e);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = ThumbnailService;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const dicomCodec = require('@cornerstonejs/dicom-codec');
|
|
2
|
+
const getImageInfo = require('./getImageInfo');
|
|
3
|
+
|
|
4
|
+
function decodeImage(imageFrame, dataset, instance, transferSyntaxUid) {
|
|
5
|
+
const imageInfo = getImageInfo(dataset, instance);
|
|
6
|
+
if (imageFrame && !imageFrame.length) {
|
|
7
|
+
imageFrame = new Uint8Array(imageFrame);
|
|
8
|
+
}
|
|
9
|
+
return dicomCodec.decode(imageFrame, imageInfo, transferSyntaxUid);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = decodeImage;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const { Tags } = require('@radicalimaging/static-wado-util');
|
|
2
|
+
/**
|
|
3
|
+
* Minimum image info data to be used on transcode process by dicom-codec api.
|
|
4
|
+
*/
|
|
5
|
+
function getImageInfo(dataSet, instance) {
|
|
6
|
+
if (instance) {
|
|
7
|
+
const rows = Tags.getValue(instance, Tags.Rows);
|
|
8
|
+
const columns = Tags.getValue(instance, Tags.Columns);
|
|
9
|
+
const bitsAllocated = Tags.getValue(instance, Tags.BitsAllocated);
|
|
10
|
+
const samplesPerPixel = Tags.getValue(instance, Tags.SamplesPerPixel);
|
|
11
|
+
const pixelRepresentation = Tags.getValue(instance, Tags.PixelRepresentation) || 0; // not yet being used.
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
bitsAllocated,
|
|
15
|
+
samplesPerPixel,
|
|
16
|
+
rows, // Number with the image rows/height
|
|
17
|
+
columns, // Number with the image columns/width,
|
|
18
|
+
signed: pixelRepresentation === 1,
|
|
19
|
+
pixelRepresentation,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const rows = dataSet.uint16('x00280010');
|
|
23
|
+
const columns = dataSet.uint16('x00280011');
|
|
24
|
+
const bitsAllocated = dataSet.uint16('x00280100');
|
|
25
|
+
const samplesPerPixel = dataSet.uint16('x00280002');
|
|
26
|
+
const pixelRepresentation = dataSet.uint16('x00280103') || 0; // not yet being used.
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
bitsAllocated,
|
|
30
|
+
samplesPerPixel,
|
|
31
|
+
rows, // Number with the image rows/height
|
|
32
|
+
columns, // Number with the image columns/width,
|
|
33
|
+
signed: pixelRepresentation === 1,
|
|
34
|
+
pixelRepresentation,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = getImageInfo;
|