@radicalimaging/static-wado-creator 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +285 -0
  2. package/bin/mkdicomweb.js +9 -0
  3. package/lib/DeleteStudy.js +14 -0
  4. package/lib/RejectInstance.js +17 -0
  5. package/lib/StaticWado.js +206 -0
  6. package/lib/createMain.js +14 -0
  7. package/lib/deleteMain.js +12 -0
  8. package/lib/dictionary/Tags.js +111 -0
  9. package/lib/dictionary/dataDictionary.js +22268 -0
  10. package/lib/groupMain.js +14 -0
  11. package/lib/index.js +8 -0
  12. package/lib/instanceMain.js +13 -0
  13. package/lib/metadataMain.js +14 -0
  14. package/lib/mkdicomwebConfig.js +149 -0
  15. package/lib/model/TagLists.js +151 -0
  16. package/lib/model/uids.js +42 -0
  17. package/lib/operation/InstanceDeduplicate.js +100 -0
  18. package/lib/operation/ScanStudy.js +20 -0
  19. package/lib/operation/StudyData.js +416 -0
  20. package/lib/operation/ThumbnailService.js +184 -0
  21. package/lib/operation/adapter/asyncIterableToBuffer.js +112 -0
  22. package/lib/operation/adapter/decodeImage.js +9 -0
  23. package/lib/operation/adapter/getImageInfo.js +21 -0
  24. package/lib/operation/adapter/transcodeImage.js +303 -0
  25. package/lib/operation/extractImageFrames.js +41 -0
  26. package/lib/operation/getDataSet.js +60 -0
  27. package/lib/operation/getEncapsulatedImageFrame.js +23 -0
  28. package/lib/operation/getNumberOfFrames.js +13 -0
  29. package/lib/operation/getUncompressedImageFrame.js +7 -0
  30. package/lib/operation/getVR.js +16 -0
  31. package/lib/operation/getValue.js +193 -0
  32. package/lib/program/index.js +15 -0
  33. package/lib/rejectMain.js +12 -0
  34. package/lib/util/IdCreator.js +25 -0
  35. package/lib/util/adaptProgramOpts.js +58 -0
  36. package/lib/util/compressionOptionParser.js +13 -0
  37. package/lib/writer/CompleteStudyWriter.js +88 -0
  38. package/lib/writer/DeduplicateWriter.js +32 -0
  39. package/lib/writer/ExpandUriPath.js +15 -0
  40. package/lib/writer/HashDataWriter.js +59 -0
  41. package/lib/writer/ImageFrameWriter.js +31 -0
  42. package/lib/writer/JSONWriter.js +18 -0
  43. package/lib/writer/MultipartAttribute.js +8 -0
  44. package/lib/writer/MultipartHeader.js +14 -0
  45. package/lib/writer/ThumbnailWriter.js +21 -0
  46. package/lib/writer/VideoWriter.js +52 -0
  47. package/lib/writer/WriteMultipart.js +22 -0
  48. package/lib/writer/WriteStream.js +51 -0
  49. package/package.json +75 -0
@@ -0,0 +1,416 @@
1
+ const path = require("path");
2
+ const fs = require("fs");
3
+ const hashFactory = require("node-object-hash");
4
+ const { JSONReader } = require("@radicalimaging/static-wado-util");
5
+ const Tags = require("../dictionary/Tags");
6
+ const TagLists = require("../model/TagLists");
7
+ const JSONWriter = require("../writer/JSONWriter");
8
+
9
+ const {getValue, setValue, getList, setList} = Tags;
10
+ const hasher = hashFactory();
11
+
12
+ const getSeriesInstanceUid = (seriesInstance) =>
13
+ getValue(seriesInstance,Tags.SeriesInstanceUID);
14
+
15
+ /**
16
+ * StudyData contains information about the grouped study data. It is used to create
17
+ * study level information such as study or series level metadata, and to group things
18
+ * together during processing, such as handling the study level deduplicated group
19
+ * files.
20
+ *
21
+ * It also has methods on performing study level operations such as patient name updates
22
+ * or instance remove operations, and has methods to handle avoiding needing to store instance
23
+ * level data multiple times when it already exists.
24
+ */
25
+ class StudyData {
26
+ constructor({ studyInstanceUid, studyPath, deduplicatedPath, deduplicatedInstancesPath }, { isGroup, clean }) {
27
+ this.studyInstanceUid = studyInstanceUid;
28
+ this.studyPath = studyPath;
29
+ this.isGroup = isGroup;
30
+ this.deduplicatedPath = deduplicatedPath;
31
+ this.deduplicatedInstancesPath = deduplicatedInstancesPath;
32
+ this.clean = clean;
33
+ this.clear();
34
+ }
35
+
36
+ clear() {
37
+ this.deduplicated = [];
38
+ this.extractData = {};
39
+ // The list of already existing files read in to create this object
40
+ this.existingFiles = [];
41
+ // The read hashes is the hashes of the files that are read, both as groups and internally
42
+ this.readHashes = {};
43
+ // The deduplicated hashes are just the hashes for individual items
44
+ this.deduplicatedHashes = {};
45
+ this.sopInstances = {};
46
+
47
+ // Used to track if new instances have been added.
48
+ this.newInstancesAdded = 0;
49
+ }
50
+
51
+ /**
52
+ * Clean the directory, and/or read existing data in the isInstances or isGroup files.
53
+ * TODO - implement this.
54
+ */
55
+ async init({ clean }) {
56
+ if (clean) {
57
+ // Wipe out the study directory entirely, as well as the deduplicatedRoot and instancesRoot
58
+ }
59
+ if (this.deduplicatedPath) {
60
+ await this.readDeduplicated(this.deduplicatedPath);
61
+ }
62
+ if (this.deduplicatedInstancesPath) {
63
+ await this.readDeduplicated(this.deduplicatedInstancesPath);
64
+ }
65
+ }
66
+
67
+ get numberOfInstances() {
68
+ return this.deduplicated.length;
69
+ }
70
+
71
+ /**
72
+ * Indicate if this study is 'dirty', that is, has been updated.
73
+ * That is NOT to say that the metadata files generated from this study data are up to date, that is
74
+ * a separate type of check.
75
+ */
76
+ get dirty() {
77
+ return this.newInstancesAdded > 0 || this.existingFiles.length > 1;
78
+ }
79
+
80
+ async dirtyMetadata() {
81
+ if (this.dirty) {
82
+ // console.log("Study data is dirty - need to write updated file");
83
+ return true;
84
+ }
85
+ try {
86
+ const deduplicatedTopFile = await JSONReader(this.studyPath, "deduplicated.gz", null);
87
+ if (!deduplicatedTopFile) {
88
+ return true;
89
+ }
90
+ const info = deduplicatedTopFile[0];
91
+ if (!info || getValue(info,Tags.DeduppedHash) || getValue(info,Tags.DeduppedType) != "info") {
92
+ return true;
93
+ }
94
+ const hashValue = getValue(info,Tags.DeduppedHash);
95
+ return this.existingFiles[0].indexOf(hashValue) == -1;
96
+ } catch (e) {
97
+ console.log("Assume study metadata is dirty", e);
98
+ return true;
99
+ }
100
+ }
101
+
102
+ async reject(seriesInstanceUid, sopInstanceUid, reason) {
103
+ console.log("Rejecting series instance UID", seriesInstanceUid, "because", reason);
104
+ // TODO - actually add a reject note...
105
+ this.newInstancesAdded += 1;
106
+ for (let i = 0; i < this.deduplicated.length; i++) {
107
+ const recombined = await this.recombine(i);
108
+ const reSeriesUid = getValue(recombined,Tags.SeriesInstanceUID);
109
+ if (reSeriesUid !== seriesInstanceUid) continue;
110
+ const reSop = getValue(recombined,Tags.SOPInstanceUID);
111
+ if (!reSop) continue;
112
+ if (reSop !== sopInstanceUid) continue;
113
+ setValue(this.deduplicated[i], Tags.DeduppedType,"deleted");
114
+ }
115
+ }
116
+
117
+ async delete() {
118
+ await fs.rmSync(this.studyPath, { recursive: true, force: true });
119
+ await fs.rmSync(this.deduplicatedInstancesPath, { recursive: true, force: true });
120
+ await fs.rmSync(this.deduplicatedPath, { recursive: true, force: true });
121
+ this.clear();
122
+ }
123
+
124
+ async getOrLoadExtract(hashKey) {
125
+ let item = this.extractData[hashKey];
126
+ if (!item) {
127
+ item = await JSONReader.readHashData(this.studyPath, hashKey);
128
+ if (!item) {
129
+ console.error("Unable to read hashdata", hashKey);
130
+ return item;
131
+ }
132
+ this.extractData[hashKey] = item;
133
+ }
134
+ return item;
135
+ }
136
+
137
+ assignUndefined(ret, item) {
138
+ if (!item) return;
139
+ Object.keys(item).forEach((key) => {
140
+ if (ret[key]) return;
141
+ ret[key] = item[key];
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Create a full study instance data for instance at index
147
+ */
148
+ async recombine(index) {
149
+ const deduplicated = this.deduplicated[index];
150
+ if (index < 0 || index >= this.deduplicated.length) {
151
+ throw new Error(`Can't read index ${index}, out of bounds [0..${this.deduplicated.length})`);
152
+ }
153
+ const refs = getList(deduplicated,Tags.DeduppedRef);
154
+ if (!refs) {
155
+ console.log("No refs for", deduplicated);
156
+ return deduplicated;
157
+ }
158
+ const ret = { ...deduplicated };
159
+ for (const hashKey of refs) {
160
+ const item = await this.getOrLoadExtract(hashKey);
161
+ this.assignUndefined(ret, item);
162
+ }
163
+ return ret;
164
+ }
165
+
166
+ async addExtracted(callback, hashKey, item) {
167
+ if (this.extractData[hashKey]) {
168
+ if( this.verbose ) console.log("Already have extracted", hashKey, getValue(item,Tags.DeduppedType));
169
+ return;
170
+ }
171
+ await callback.bulkdata(this, hashKey, item);
172
+ this.extractData[hashKey] = item;
173
+ }
174
+
175
+ sopExists(sopUID) {
176
+ const sopValue = (sopUID && sopUID.Value && sopUID.Value[0]) || sopUID;
177
+ return sopValue && this.sopInstances[sopValue] !== undefined;
178
+ }
179
+
180
+ addDeduplicated(data, filename) {
181
+ if (!this.isGroup) return;
182
+ // TODO - check the hash code on the added data, if it has already been seen then ignore this item.
183
+ if (this.internalAddDeduplicated(data, filename)) {
184
+ this.newInstancesAdded += 1;
185
+ }
186
+ }
187
+
188
+ /** Add the instance if not already present */
189
+ internalAddDeduplicated(data, filename = "internal") {
190
+ const hashValue = TagLists.addHash(data, Tags.InstanceType);
191
+ if (!hashValue || this.deduplicatedHashes[hashValue]) {
192
+ // console.log('Not adding', hashValue, 'because the hash exists');
193
+ return false;
194
+ }
195
+ const sopValue = getValue(data,Tags.SOPInstanceUID);
196
+ const sopIndex = this.sopInstances[sopValue];
197
+ if (!sopValue) {
198
+ console.warn("No sop value in", filename, "reading", data);
199
+ return false;
200
+ }
201
+ this.deduplicatedHashes[hashValue] = data;
202
+ this.readHashes[hashValue] = filename;
203
+ if (sopIndex !== undefined) {
204
+ console.log("Replacing SOP", sopValue, "at index", sopIndex);
205
+ this.deduplicated[sopIndex] = data;
206
+ } else {
207
+ this.sopInstances[sopValue] = this.deduplicated.length;
208
+ this.deduplicated.push(data);
209
+ }
210
+
211
+ return !!hashValue;
212
+ }
213
+
214
+ async listJsonFiles(dir) {
215
+ const names = await fs.promises.readdir(dir);
216
+ if (!names || names.length == 0) return [];
217
+ const gzNames = names.filter((name) => name.indexOf(".gz") > 0);
218
+ const statInfo = gzNames
219
+ .map((name) => {
220
+ const stat = fs.statSync(path.join(dir, name));
221
+ return {
222
+ ...stat,
223
+ name,
224
+ hash: this.removeGz(name),
225
+ };
226
+ })
227
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
228
+ return statInfo;
229
+ }
230
+
231
+ /**
232
+ * Reads the deduplicated directory, finding the newest file in the directory of type JSON,
233
+ * and then reads it in. It then reads in all un-referenced files, and adds references to those to the
234
+ * current path, NOT changing the hash value of the top level element.
235
+ *
236
+ * Records the files read in referencedFiles list.
237
+ * Sets the dirty deduplicated flag if at least two instances were read.
238
+ *
239
+ * Does NOT call the readInstances, which is a separate step which can be executed after this one.
240
+ *
241
+ * @param {*} deduplicatedDirectory
242
+ */
243
+ async readDeduplicated(deduplicatedDirectory) {
244
+ try {
245
+ const files = await this.listJsonFiles(deduplicatedDirectory);
246
+ if (!files || !files.length) {
247
+ console.log("No deduplicated for", deduplicatedDirectory);
248
+ return;
249
+ }
250
+ console.log("There are", files.length, "files to check");
251
+ for (let i = 0; i < files.length; i++) {
252
+ const stat = files[i];
253
+ const { hash } = stat;
254
+ if (this.readHashes[hash]) {
255
+ continue;
256
+ }
257
+ await this.readDeduplicatedFile(deduplicatedDirectory, stat);
258
+ }
259
+ console.log("Done checking", deduplicatedDirectory);
260
+ } catch (e) {
261
+ // No-op console.log(e);
262
+ }
263
+ }
264
+
265
+ async readDeduplicatedFile(dir, stat) {
266
+ const { hash, name } = stat;
267
+ try {
268
+ if (this.verbose) console.log("Reading deduplicated file", name);
269
+ const data = await JSONReader(dir, name);
270
+ this.readHashes[hash] = name;
271
+ this.existingFiles.push(name);
272
+ const listData = (Array.isArray(data) && data) || [data];
273
+ listData.forEach((item) => {
274
+ const type = getValue(item, Tags.DeduppedType);
275
+ if (type == Tags.InstanceType) {
276
+ this.internalAddDeduplicated(item, name);
277
+ } else if (type == "info") {
278
+ const refs = getList(item,Tags.DeduppedRef);
279
+ if (refs) {
280
+ refs.forEach((hashValue) => {
281
+ this.readHashes[hashValue] = `${hashValue}.gz`;
282
+ });
283
+ }
284
+ } else {
285
+ const hashValue = getValue(item,Tags.DeduppedHash);
286
+ if (hashValue) {
287
+ this.extractData[hashValue] = item;
288
+ }
289
+ }
290
+ });
291
+ } catch (e) {
292
+ console.error("Unable to read", dir, name);
293
+ }
294
+ }
295
+
296
+ async writeMetadata() {
297
+ const anInstance = await this.recombine(0);
298
+ const series = {};
299
+
300
+ for (let i = 0; i < this.numberOfInstances; i++) {
301
+ const seriesInstance = await this.recombine(i);
302
+ const type = getValue(seriesInstance,Tags.DeduppedType);
303
+ if (type == "deleted") {
304
+ console.log("Skipping", type);
305
+ continue;
306
+ }
307
+ const seriesInstanceUid = getSeriesInstanceUid(seriesInstance);
308
+ if (!seriesInstanceUid) {
309
+ console.log("Cant get seriesUid from", Tags.SeriesInstanceUID, seriesInstance);
310
+ continue;
311
+ }
312
+ if (!series[seriesInstanceUid]) {
313
+ const seriesQuery = TagLists.extract(seriesInstance, "series", TagLists.SeriesQuery);
314
+ const seriesPath = path.join(this.studyPath, "series", seriesInstanceUid);
315
+ series[seriesInstanceUid] = {
316
+ seriesPath,
317
+ seriesQuery,
318
+ instances: [],
319
+ instancesQuery: [],
320
+ };
321
+ }
322
+ series[seriesInstanceUid].instances.push(seriesInstance);
323
+ series[seriesInstanceUid].instancesQuery.push(TagLists.extract(seriesInstance, "instance", TagLists.InstanceQuery));
324
+ }
325
+
326
+ const seriesList = [];
327
+ const modalitiesInStudy = [];
328
+ let numberOfInstances = 0;
329
+ let numberOfSeries = 0;
330
+ for (const seriesUid of Object.keys(series)) {
331
+ const singleSeries = series[seriesUid];
332
+ const { seriesQuery, seriesPath, instances, instancesQuery } = singleSeries;
333
+ seriesQuery[Tags.NumberOfSeriesRelatedInstances] = {
334
+ vr: "IS",
335
+ Value: [instances.length],
336
+ };
337
+ numberOfInstances += instances.length;
338
+ numberOfSeries += 1;
339
+ seriesList.push(seriesQuery);
340
+ const modality = getValue(seriesQuery,Tags.Modality);
341
+ if (modalitiesInStudy.indexOf(modality) == -1) modalitiesInStudy.push(modality);
342
+ await JSONWriter(seriesPath, "metadata", instances, {
343
+ gzip: true,
344
+ index: false,
345
+ });
346
+ await JSONWriter(seriesPath, "series", [seriesQuery]);
347
+ await JSONWriter(seriesPath, "instances", instancesQuery);
348
+ }
349
+
350
+ await JSONWriter(this.studyPath, "series", seriesList);
351
+ console.log("Wrote series with", seriesList.length);
352
+
353
+ const studyQuery = TagLists.extract(anInstance, "study", TagLists.PatientStudyQuery);
354
+ studyQuery[Tags.ModalitiesInStudy] = { Value: modalitiesInStudy, vr: "CS" };
355
+ studyQuery[Tags.NumberOfStudyRelatedInstances] = {
356
+ Value: [numberOfInstances],
357
+ vr: "IS",
358
+ };
359
+ studyQuery[Tags.NumberOfStudyRelatedSeries] = {
360
+ Value: [numberOfSeries],
361
+ vr: "IS",
362
+ };
363
+ // Write the index.json directly in the studyPath directory.
364
+ await JSONWriter(this.studyPath, "index.json", [studyQuery], {
365
+ gzip: true,
366
+ index: false,
367
+ });
368
+
369
+ const infoItem = this.createInfo();
370
+ console.log("Writing deduplicated study data with", Object.values(this.extractData).length, "extract items and", this.deduplicated.length, "instance items");
371
+ await JSONWriter(this.studyPath, "deduplicated", [infoItem, ...Object.values(this.extractData), ...this.deduplicated]);
372
+
373
+ return studyQuery;
374
+ }
375
+
376
+ /**
377
+ * Creates an information item containing the hash value of this item type.
378
+ */
379
+ createInfo() {
380
+ const data = {};
381
+ const hashValue = hasher.hash(this.deduplicated);
382
+ setValue(data,Tags.DeduppedHash,hashValue);
383
+ setValue(data,Tags.DeduppedType,"info");
384
+ return data;
385
+ }
386
+
387
+ /* eslint-disable-next-line class-methods-use-this */
388
+ removeGz(name) {
389
+ const gzIndex = name.indexOf(".gz");
390
+ return (gzIndex > 0 && name.substring(0, gzIndex)) || name;
391
+ }
392
+
393
+ /** Writes the deduplicated group */
394
+ async writeDeduplicatedGroup() {
395
+ const data = this.createInfo();
396
+ const hashValue = getValue(data,Tags.DeduppedHash);
397
+ console.log(
398
+ "Writing deduplicated hash data set data with",
399
+ Object.values(this.extractData).length,
400
+ "extract items and",
401
+ this.deduplicated.length,
402
+ "instance items"
403
+ );
404
+ setList(data,Tags.DeduppedRef,
405
+ Object.keys(this.readHashes).filter(() => this.deduplicatedHashes[hashValue] == undefined));
406
+ const deduplicatedList = [data, ...Object.values(this.extractData), ...this.deduplicated];
407
+ // const naturalList = deduplicatedList.map(Tags.naturalizeDataset);
408
+ // // console.log("naturalList=", JSON.stringify(naturalList,null,2));
409
+ // console.log("Going to write study data", naturalList.length);
410
+ // await JSONWriter(this.deduplicatedPath, hashValue, naturalList, { gzip: true, index: false });
411
+ await JSONWriter(this.deduplicatedPath, hashValue, deduplicatedList, { gzip: true, index: false });
412
+ console.log("Wrote naturalized dataset");
413
+ }
414
+ }
415
+
416
+ module.exports = StudyData;
@@ -0,0 +1,184 @@
1
+ const path = require("path");
2
+ const glob = require("glob");
3
+ const dicomCodec = require("@cornerstonejs/dicom-codec");
4
+ const staticCS = require("@radicalimaging/static-cs-lite");
5
+ const fs = require("fs");
6
+ const { exec } = require("child_process");
7
+ const decodeImage = require("./adapter/decodeImage");
8
+ const { shouldThumbUseTranscoded } = require("./adapter/transcodeImage");
9
+ const { isVideo } = require("../writer/VideoWriter");
10
+ const Tags = require("../dictionary/Tags");
11
+
12
+ /**
13
+ * Return the middle index of given list
14
+ * @param {*} listThumbs
15
+ * @returns
16
+ */
17
+ function getThumbIndex(listThumbs) {
18
+ return Math.trunc(listThumbs / 2);
19
+ }
20
+ function internalGenerateThumbnail(originalImageFrame, dataset, metadata, transferSyntaxUid, doneCallback) {
21
+ decodeImage(originalImageFrame, dataset, transferSyntaxUid)
22
+ .then((decodeResult = {}) => {
23
+ if (isVideo(transferSyntaxUid)) {
24
+ console.log("Video data - no thumbnail generator yet");
25
+ } else {
26
+ const { imageFrame, imageInfo } = decodeResult;
27
+ const pixelData = dicomCodec.getPixelData(imageFrame, imageInfo, transferSyntaxUid);
28
+ staticCS.getRenderedBuffer(transferSyntaxUid, pixelData, metadata, doneCallback);
29
+ }
30
+ })
31
+ .catch((error) => {
32
+ console.log(`Error while generating thumbnail:: ${error}`);
33
+ });
34
+ }
35
+
36
+ /**
37
+ * ThumbObj wrapper containing both original and transcoded content.
38
+ *
39
+ * @typedef {Object} ThumbObjWrapper
40
+ * @property {Object} id object containing study properties such as seriesUid, instanceUid
41
+ * @property {*} transcodedId transcoded object containing study properties such as seriesUid, instanceUid (this might be the same as original id depending on options)
42
+ * @property {*} imageFrame original image frame
43
+ * @property {*} transcodedImageFrame transcoded image frame (this might be the same as original frame depending on options)
44
+ * @property {number} frameIndex
45
+ */
46
+
47
+ /**
48
+ * ThumbObj containing imageFrame, id to be used.
49
+ *
50
+ * @typedef {Object} ThumbObj
51
+ * @property {Object} id object containing study properties such as seriesUid, instanceUid
52
+ * @property {*} imageFrame
53
+ * @property {number} frameIndex
54
+ */
55
+
56
+ /**
57
+ * Service to handle thumbnail creation.
58
+ * @class
59
+ * @constructor
60
+ * @property {Array<ThumbObj>} framesThumbnailObj Auxiliary list that contains all thumb obj.
61
+ * @property {ThumbObj} favoriteThumbnailObj Favorite thumb obj to be used on thumbnail creation
62
+ * @property {String} thumbFileName thumb file (with extension) to be used when creating the file.
63
+ * @public
64
+ */
65
+ class ThumbnailService {
66
+ constructor() {
67
+ this.framesThumbnailObj = [];
68
+ this.favoriteThumbnailObj = {};
69
+ this.thumbFileName = "thumbnail";
70
+ }
71
+
72
+ /**
73
+ * Add thumbObj to list of possible thumbs.
74
+ * It might use original content or transcoded depending on options flags
75
+ * @param {ThumbObjWrapper} thumbObjWrapper
76
+ * @param {Object} programOpts
77
+ */
78
+ queueThumbnail(thumbObjWrapper, programOpts) {
79
+ const { id, imageFrame, transcodedId, transcodedImageFrame, frameIndex } = thumbObjWrapper;
80
+ const getThumbContent = (originalContent, trancodedContent) => (shouldThumbUseTranscoded(id, programOpts) ? trancodedContent : originalContent);
81
+
82
+ const thumbObj = {
83
+ imageFrame: getThumbContent(imageFrame, transcodedImageFrame),
84
+ id: getThumbContent(id, transcodedId),
85
+ frameIndex,
86
+ };
87
+
88
+ this.framesThumbnailObj.push(thumbObj);
89
+
90
+ this.setFavoriteThumbnailObj();
91
+ }
92
+
93
+ setFavoriteThumbnailObj() {
94
+ const favIndex = getThumbIndex(this.framesThumbnailObj.length);
95
+
96
+ this.favoriteThumbnailObj = this.framesThumbnailObj[favIndex];
97
+ }
98
+
99
+ ffmpeg(input, output) {
100
+ exec(`ffmpeg -i "${input}" -vf "thumbnail,scale=640:360" -frames:v 1 -f singlejpeg "${output}"`, (error, stdout, stderr) => {
101
+ if (error) {
102
+ console.log(`error: ${error.message}`);
103
+ return;
104
+ }
105
+ if (stderr) {
106
+ console.log(`stderr: ${stderr}`);
107
+ return;
108
+ }
109
+ console.log(`stdout: ${stdout}`);
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Generates thumbnails for the levels: instances, series, study
115
+ *
116
+ * @param {*} dataSet
117
+ * @param {*} metadata
118
+ * @param {*} callback
119
+ */
120
+ generateThumbnails(itemId, dataSet, metadata, callback) {
121
+ const { imageFrame, id } = this.favoriteThumbnailObj;
122
+
123
+ // There are various reasons no thumbnails might be generated, so just return
124
+ if (!id) {
125
+ const pixelData = metadata[Tags.PixelData];
126
+ if (pixelData) {
127
+ const { BulkDataURI } = pixelData;
128
+ if (BulkDataURI?.indexOf("mp4")) {
129
+ const mp4Path = path.join(itemId.sopInstanceRootPath, "pixeldata.mp4");
130
+ const thumbPath = path.join(itemId.sopInstanceRootPath, "thumbnail");
131
+ console.log("MP4 - converting video format", mp4Path);
132
+ this.ffmpeg(mp4Path, thumbPath);
133
+ } else {
134
+ console.log("pixelData = ", pixelData, Tags.PixelData);
135
+ }
136
+ } else {
137
+ console.log("Series is of other type...", metadata[Tags.Modality]);
138
+ }
139
+ return;
140
+ }
141
+ internalGenerateThumbnail(imageFrame, dataSet, metadata, id.transferSyntaxUid, async (thumbBuffer) => {
142
+ if (thumbBuffer) {
143
+ await callback.thumbWriter(id.sopInstanceRootPath, this.thumbFileName, thumbBuffer);
144
+
145
+ this.copySyncThumbnail(id.sopInstanceRootPath, id.seriesRootPath);
146
+ this.copySyncThumbnail(id.seriesRootPath, id.studyPath);
147
+ }
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Copy thumbnail from sourceFolder to targetFolderPath. It usually is used to copy from child to parent folder.
153
+ * If there is already some existing thumbnails under targetFolderPath it actually get thumb on the index defined by getThumbIndex.
154
+ * Copy is a syncrhonous process.
155
+ *
156
+ * @param {*} sourceFolderPath
157
+ * @param {*} targetFolderPath
158
+ * @returns
159
+ */
160
+ async copySyncThumbnail(sourceFolderPath, targetFolderPath) {
161
+ const parentPathLevel = path.join(sourceFolderPath, "../");
162
+ const thumbFilesPath = glob.sync(`${parentPathLevel}*/${this.thumbFileName}`);
163
+
164
+ const thumbIndex = getThumbIndex(thumbFilesPath.length);
165
+ const thumbFilePath = thumbFilesPath[thumbIndex];
166
+
167
+ if (!fs.existsSync(thumbFilePath)) {
168
+ console.log("Thumbnail to copy does not exists");
169
+ return;
170
+ }
171
+
172
+ try {
173
+ if (!fs.lstatSync(targetFolderPath).isDirectory()) {
174
+ throw new Error(`Target path: ${targetFolderPath} is not a directory`);
175
+ }
176
+
177
+ fs.copyFileSync(thumbFilePath, `${targetFolderPath}/${this.thumbFileName}`);
178
+ } catch (e) {
179
+ console.log("The file could not be copied", e);
180
+ }
181
+ }
182
+ }
183
+
184
+ module.exports = ThumbnailService;