@radicalimaging/static-wado-creator 0.6.5 → 1.2.0
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/CHANGELOG.md +67 -0
- package/README.md +3 -1
- package/bin/mkdicomweb.js +1 -1
- package/lib/DeleteStudy.js +2 -2
- package/lib/RejectInstance.js +18 -11
- package/lib/StaticWado.js +52 -22
- package/lib/createMain.js +6 -2
- package/lib/groupMain.js +3 -0
- package/lib/index.js +1 -0
- package/lib/instanceMain.js +2 -0
- package/lib/metadataMain.js +3 -2
- package/lib/mkdicomwebConfig.js +44 -8
- package/lib/model/TagLists.js +1 -1
- package/lib/model/uids.js +9 -10
- package/lib/operation/InstanceDeduplicate.js +4 -4
- package/lib/operation/ScanStudy.js +4 -3
- package/lib/operation/StudyData.js +94 -46
- package/lib/operation/ThumbnailService.js +21 -42
- package/lib/operation/adapter/transcodeImage.js +51 -23
- package/lib/operation/extractImageFrames.js +3 -0
- package/lib/operation/getDataSet.js +7 -2
- package/lib/operation/getVR.js +1 -1
- package/lib/operation/getValue.js +22 -11
- package/lib/rejectMain.js +2 -2
- package/lib/util/adaptProgramOpts.js +10 -29
- package/lib/writer/CompleteStudyWriter.js +8 -5
- package/lib/writer/DeduplicateWriter.js +5 -2
- package/lib/writer/HashDataWriter.js +2 -3
- package/lib/writer/ImageFrameWriter.js +25 -9
- package/lib/writer/RawDicomWriter.js +36 -0
- package/lib/writer/ThumbnailWriter.js +7 -14
- package/lib/writer/VideoWriter.js +1 -1
- package/lib/writer/WriteStream.js +12 -1
- package/package.json +11 -15
- package/lib/dictionary/Tags.js +0 -119
- package/lib/dictionary/dataDictionary.js +0 -22268
- package/lib/writer/JSONWriter.js +0 -18
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const hashFactory = require("node-object-hash");
|
|
4
|
-
const { JSONReader } = require("@radicalimaging/static-wado-util");
|
|
5
|
-
const Tags = require("
|
|
4
|
+
const { JSONReader, JSONWriter } = require("@radicalimaging/static-wado-util");
|
|
5
|
+
const { Tags } = require("@radicalimaging/static-wado-util");
|
|
6
6
|
const TagLists = require("../model/TagLists");
|
|
7
|
-
const JSONWriter = require("../writer/JSONWriter");
|
|
8
7
|
|
|
9
8
|
const { getValue, setValue, getList, setList } = Tags;
|
|
10
9
|
const hasher = hashFactory();
|
|
@@ -42,6 +41,7 @@ class StudyData {
|
|
|
42
41
|
// The deduplicated hashes are just the hashes for individual items
|
|
43
42
|
this.deduplicatedHashes = {};
|
|
44
43
|
this.sopInstances = {};
|
|
44
|
+
this.instanceFiles = 0;
|
|
45
45
|
|
|
46
46
|
// Used to track if new instances have been added.
|
|
47
47
|
this.newInstancesAdded = 0;
|
|
@@ -55,11 +55,22 @@ class StudyData {
|
|
|
55
55
|
if (clean) {
|
|
56
56
|
// Wipe out the study directory entirely, as well as the deduplicatedRoot and instancesRoot
|
|
57
57
|
}
|
|
58
|
+
this.groupFiles = 0;
|
|
59
|
+
const studyDeduplicated = await JSONReader(this.studyPath, "deduplicated/index.json.gz", []);
|
|
60
|
+
const info = studyDeduplicated[0];
|
|
61
|
+
if (info) {
|
|
62
|
+
const hash = getValue(info, Tags.DeduppedHash);
|
|
63
|
+
console.log("Reading studies/<studyUID>/deduplicated/index.json.gz");
|
|
64
|
+
this.readDeduplicatedData("index.json.gz", studyDeduplicated, hash);
|
|
65
|
+
} else {
|
|
66
|
+
console.log("No deduplicated/index.json to read in", this.studyPath, "/deduplicated/index.json.gz");
|
|
67
|
+
}
|
|
58
68
|
if (this.deduplicatedPath) {
|
|
59
|
-
await this.readDeduplicated(this.deduplicatedPath);
|
|
69
|
+
this.groupFiles = await this.readDeduplicated(this.deduplicatedPath);
|
|
70
|
+
console.verbose("Read groupFiles:", this.groupFiles);
|
|
60
71
|
}
|
|
61
72
|
if (this.deduplicatedInstancesPath) {
|
|
62
|
-
await this.readDeduplicated(this.deduplicatedInstancesPath);
|
|
73
|
+
this.instanceFiles = await this.readDeduplicated(this.deduplicatedInstancesPath);
|
|
63
74
|
}
|
|
64
75
|
}
|
|
65
76
|
|
|
@@ -73,33 +84,36 @@ class StudyData {
|
|
|
73
84
|
* a separate type of check.
|
|
74
85
|
*/
|
|
75
86
|
get dirty() {
|
|
76
|
-
return this.newInstancesAdded > 0 || this.existingFiles.length > 1;
|
|
87
|
+
return this.newInstancesAdded > 0 || this.existingFiles.length > 1 || this.instanceFiles > 0;
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
async dirtyMetadata() {
|
|
80
91
|
if (this.dirty) {
|
|
81
|
-
|
|
92
|
+
console.verbose("dirtyMetadata::Group data is dirty");
|
|
82
93
|
return true;
|
|
83
94
|
}
|
|
95
|
+
if (this.groupFiles > 0) {
|
|
96
|
+
console.verbose("dirtyMetadata::Study level deduplicated doesn't match group files");
|
|
97
|
+
}
|
|
84
98
|
try {
|
|
85
|
-
const
|
|
86
|
-
if (!
|
|
99
|
+
const studyFile = await JSONReader(this.studyPath, "index.json.gz", null);
|
|
100
|
+
if (!studyFile) {
|
|
101
|
+
console.verbose("dirtyMetadata::studyIndex");
|
|
87
102
|
return true;
|
|
88
103
|
}
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
return
|
|
104
|
+
const hashValue = getValue(studyFile, Tags.DeduppedHash);
|
|
105
|
+
if (this.existingFiles[0].indexOf(hashValue) == -1) {
|
|
106
|
+
return false;
|
|
92
107
|
}
|
|
93
|
-
|
|
94
|
-
return
|
|
108
|
+
console.verbose("dirtyMetadata::Dedupped hash missing");
|
|
109
|
+
return true;
|
|
95
110
|
} catch (e) {
|
|
96
|
-
console.
|
|
111
|
+
console.verbose("dirtyMetadata::Exception, assume study metadata is dirty", e);
|
|
97
112
|
return true;
|
|
98
113
|
}
|
|
99
114
|
}
|
|
100
115
|
|
|
101
|
-
async reject(seriesInstanceUid, sopInstanceUid, reason) {
|
|
102
|
-
console.log("Rejecting series instance UID", seriesInstanceUid, "because", reason);
|
|
116
|
+
async reject(seriesInstanceUid, sopInstanceUid /* , reason */) {
|
|
103
117
|
// TODO - actually add a reject note...
|
|
104
118
|
this.newInstancesAdded += 1;
|
|
105
119
|
for (let i = 0; i < this.deduplicated.length; i++) {
|
|
@@ -108,7 +122,7 @@ class StudyData {
|
|
|
108
122
|
if (reSeriesUid !== seriesInstanceUid) continue;
|
|
109
123
|
const reSop = getValue(recombined, Tags.SOPInstanceUID);
|
|
110
124
|
if (!reSop) continue;
|
|
111
|
-
if (reSop !== sopInstanceUid) continue;
|
|
125
|
+
if (sopInstanceUid && reSop !== sopInstanceUid) continue;
|
|
112
126
|
setValue(this.deduplicated[i], Tags.DeduppedType, "deleted");
|
|
113
127
|
}
|
|
114
128
|
}
|
|
@@ -240,25 +254,28 @@ class StudyData {
|
|
|
240
254
|
* @param {*} deduplicatedDirectory
|
|
241
255
|
*/
|
|
242
256
|
async readDeduplicated(deduplicatedDirectory) {
|
|
257
|
+
let readCount = 0;
|
|
243
258
|
try {
|
|
244
259
|
const files = await this.listJsonFiles(deduplicatedDirectory);
|
|
245
260
|
if (!files || !files.length) {
|
|
246
|
-
console.
|
|
247
|
-
return;
|
|
261
|
+
console.verbose("No deduplicated for", deduplicatedDirectory);
|
|
262
|
+
return 0;
|
|
248
263
|
}
|
|
249
|
-
console.
|
|
264
|
+
console.verbose("There are", files.length, "files to check");
|
|
250
265
|
for (let i = 0; i < files.length; i++) {
|
|
251
266
|
const stat = files[i];
|
|
252
267
|
const { hash } = stat;
|
|
253
268
|
if (this.readHashes[hash]) {
|
|
254
269
|
continue;
|
|
255
270
|
}
|
|
271
|
+
readCount += 1;
|
|
256
272
|
await this.readDeduplicatedFile(deduplicatedDirectory, stat);
|
|
257
273
|
}
|
|
258
|
-
console.
|
|
274
|
+
console.verbose("Done checking", deduplicatedDirectory);
|
|
259
275
|
} catch (e) {
|
|
260
276
|
// No-op console.log(e);
|
|
261
277
|
}
|
|
278
|
+
return readCount;
|
|
262
279
|
}
|
|
263
280
|
|
|
264
281
|
async readDeduplicatedFile(dir, stat) {
|
|
@@ -266,32 +283,36 @@ class StudyData {
|
|
|
266
283
|
try {
|
|
267
284
|
if (this.verbose) console.log("Reading deduplicated file", name);
|
|
268
285
|
const data = await JSONReader(dir, name);
|
|
269
|
-
this.
|
|
270
|
-
this.existingFiles.push(name);
|
|
271
|
-
const listData = (Array.isArray(data) && data) || [data];
|
|
272
|
-
listData.forEach((item) => {
|
|
273
|
-
const type = getValue(item, Tags.DeduppedType);
|
|
274
|
-
if (type == Tags.InstanceType) {
|
|
275
|
-
this.internalAddDeduplicated(item, name);
|
|
276
|
-
} else if (type == "info") {
|
|
277
|
-
const refs = getList(item, Tags.DeduppedRef);
|
|
278
|
-
if (refs) {
|
|
279
|
-
refs.forEach((hashValue) => {
|
|
280
|
-
this.readHashes[hashValue] = `${hashValue}.gz`;
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
} else {
|
|
284
|
-
const hashValue = getValue(item, Tags.DeduppedHash);
|
|
285
|
-
if (hashValue) {
|
|
286
|
-
this.extractData[hashValue] = item;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
});
|
|
286
|
+
this.readDeduplicatedData(name, data, hash);
|
|
290
287
|
} catch (e) {
|
|
291
288
|
console.error("Unable to read", dir, name);
|
|
292
289
|
}
|
|
293
290
|
}
|
|
294
291
|
|
|
292
|
+
readDeduplicatedData(name, data, hash) {
|
|
293
|
+
this.readHashes[hash] = name;
|
|
294
|
+
this.existingFiles.push(name);
|
|
295
|
+
const listData = (Array.isArray(data) && data) || [data];
|
|
296
|
+
listData.forEach((item) => {
|
|
297
|
+
const type = getValue(item, Tags.DeduppedType);
|
|
298
|
+
if (type === Tags.InstanceType || type === Tags.DeletedType) {
|
|
299
|
+
this.internalAddDeduplicated(item, name);
|
|
300
|
+
} else if (type == "info") {
|
|
301
|
+
const refs = getList(item, Tags.DeduppedRef);
|
|
302
|
+
if (refs) {
|
|
303
|
+
refs.forEach((hashValue) => {
|
|
304
|
+
this.readHashes[hashValue] = `${hashValue}.gz`;
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
const hashValue = getValue(item, Tags.DeduppedHash);
|
|
309
|
+
if (hashValue) {
|
|
310
|
+
this.extractData[hashValue] = item;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
295
316
|
async writeMetadata() {
|
|
296
317
|
const anInstance = await this.recombine(0);
|
|
297
318
|
const series = {};
|
|
@@ -300,7 +321,7 @@ class StudyData {
|
|
|
300
321
|
const seriesInstance = await this.recombine(i);
|
|
301
322
|
const type = getValue(seriesInstance, Tags.DeduppedType);
|
|
302
323
|
if (type == "deleted") {
|
|
303
|
-
console.log("Skipping", type);
|
|
324
|
+
console.log("Skipping deleted instance", type, getValue(seriesInstance, Tags.SeriesInstanceUID));
|
|
304
325
|
continue;
|
|
305
326
|
}
|
|
306
327
|
const seriesInstanceUid = getSeriesInstanceUid(seriesInstance);
|
|
@@ -335,14 +356,15 @@ class StudyData {
|
|
|
335
356
|
};
|
|
336
357
|
numberOfInstances += instances.length;
|
|
337
358
|
numberOfSeries += 1;
|
|
338
|
-
seriesList.push(seriesQuery);
|
|
339
359
|
const modality = getValue(seriesQuery, Tags.Modality);
|
|
360
|
+
seriesList.push(seriesQuery);
|
|
340
361
|
if (modalitiesInStudy.indexOf(modality) == -1) modalitiesInStudy.push(modality);
|
|
341
362
|
await JSONWriter(seriesPath, "metadata", instances, {
|
|
342
363
|
gzip: true,
|
|
343
364
|
index: false,
|
|
344
365
|
});
|
|
345
|
-
|
|
366
|
+
// Write out a series singleton that has just the series response for a single series.
|
|
367
|
+
await JSONWriter(seriesPath, "series-singleton.json", [seriesQuery], {gzip:true, index:false});
|
|
346
368
|
await JSONWriter(seriesPath, "instances", instancesQuery);
|
|
347
369
|
}
|
|
348
370
|
|
|
@@ -389,6 +411,32 @@ class StudyData {
|
|
|
389
411
|
return (gzIndex > 0 && name.substring(0, gzIndex)) || name;
|
|
390
412
|
}
|
|
391
413
|
|
|
414
|
+
async deleteInstancesReferenced() {
|
|
415
|
+
const deduplicatedDirectory = this.deduplicatedInstancesPath;
|
|
416
|
+
if (!fs.existsSync(deduplicatedDirectory)) return;
|
|
417
|
+
console.log("Deleting instances referenced in", this.studyInstanceUid, this.deduplicatedInstancesPath);
|
|
418
|
+
const files = await this.listJsonFiles(deduplicatedDirectory);
|
|
419
|
+
console.log("Deleting", files.length, "files");
|
|
420
|
+
let deleteCount = 0;
|
|
421
|
+
for (let i = 0; i < files.length; i++) {
|
|
422
|
+
const stat = files[i];
|
|
423
|
+
const { hash } = stat;
|
|
424
|
+
if (this.readHashes[hash]) {
|
|
425
|
+
console.log("Deleting", deduplicatedDirectory, stat.name);
|
|
426
|
+
try {
|
|
427
|
+
fs.unlinkSync(path.join(deduplicatedDirectory, stat.name));
|
|
428
|
+
deleteCount += 1;
|
|
429
|
+
} catch (e) {
|
|
430
|
+
console.log("Delete failed", e);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (deleteCount === files.length) {
|
|
435
|
+
console.log("Deleting instances directory", deduplicatedDirectory);
|
|
436
|
+
fs.rmdirSync(deduplicatedDirectory);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
392
440
|
/** Writes the deduplicated group */
|
|
393
441
|
async writeDeduplicatedGroup() {
|
|
394
442
|
const data = this.createInfo();
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
2
|
const glob = require("glob");
|
|
3
|
-
const dicomCodec = require("@cornerstonejs/dicom-codec");
|
|
4
|
-
const staticCS = require("@radicalimaging/static-cs-lite");
|
|
5
3
|
const fs = require("fs");
|
|
6
|
-
const {
|
|
7
|
-
const decodeImage = require("./adapter/decodeImage");
|
|
4
|
+
const { Tags, execSpawn, Stats } = require("@radicalimaging/static-wado-util");
|
|
8
5
|
const { shouldThumbUseTranscoded } = require("./adapter/transcodeImage");
|
|
9
|
-
const { isVideo } = require("../writer/VideoWriter");
|
|
10
|
-
const Tags = require("../dictionary/Tags");
|
|
11
6
|
|
|
12
7
|
/**
|
|
13
8
|
* Return the middle index of given list
|
|
@@ -17,21 +12,6 @@ const Tags = require("../dictionary/Tags");
|
|
|
17
12
|
function getThumbIndex(listThumbs) {
|
|
18
13
|
return Math.trunc(listThumbs / 2);
|
|
19
14
|
}
|
|
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
15
|
|
|
36
16
|
/**
|
|
37
17
|
* ThumbObj wrapper containing both original and transcoded content.
|
|
@@ -97,21 +77,11 @@ class ThumbnailService {
|
|
|
97
77
|
}
|
|
98
78
|
|
|
99
79
|
ffmpeg(input, output) {
|
|
100
|
-
|
|
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
|
-
});
|
|
80
|
+
execSpawn(`ffmpeg -i "${input}" -vf "thumbnail,scale=640:360" -frames:v 1 -f singlejpeg "${output}"`);
|
|
111
81
|
}
|
|
112
82
|
|
|
113
83
|
/**
|
|
114
|
-
* Generates thumbnails for the levels: instances, series, study
|
|
84
|
+
* Generates thumbnails for the levels: instances, series, study. This is asynchronous
|
|
115
85
|
*
|
|
116
86
|
* @param {*} dataSet
|
|
117
87
|
* @param {*} metadata
|
|
@@ -127,7 +97,8 @@ class ThumbnailService {
|
|
|
127
97
|
const { BulkDataURI } = pixelData;
|
|
128
98
|
if (BulkDataURI?.indexOf("mp4")) {
|
|
129
99
|
const mp4Path = path.join(itemId.sopInstanceRootPath, "pixeldata.mp4");
|
|
130
|
-
|
|
100
|
+
// Generate as rendered, as more back ends support that.
|
|
101
|
+
const thumbPath = path.join(itemId.sopInstanceRootPath, "rendered");
|
|
131
102
|
console.log("MP4 - converting video format", mp4Path);
|
|
132
103
|
this.ffmpeg(mp4Path, thumbPath);
|
|
133
104
|
} else {
|
|
@@ -136,15 +107,23 @@ class ThumbnailService {
|
|
|
136
107
|
} else {
|
|
137
108
|
console.log("Series is of other type...", metadata[Tags.Modality]);
|
|
138
109
|
}
|
|
139
|
-
return;
|
|
110
|
+
return null;
|
|
140
111
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
callback.internalGenerateImage(imageFrame, dataSet, metadata, id.transferSyntaxUid, async (thumbBuffer) => {
|
|
114
|
+
try {
|
|
115
|
+
if (thumbBuffer) {
|
|
116
|
+
await callback.thumbWriter(id.sopInstanceRootPath, this.thumbFileName, thumbBuffer);
|
|
117
|
+
|
|
118
|
+
this.copySyncThumbnail(id.sopInstanceRootPath, id.seriesRootPath);
|
|
119
|
+
this.copySyncThumbnail(id.seriesRootPath, id.studyPath);
|
|
120
|
+
Stats.StudyStats.add("Thumbnail Write", `Write thumbnail ${this.thumbFileName}`, 100);
|
|
121
|
+
}
|
|
122
|
+
resolve(this.thumbFileName);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
reject(e);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
148
127
|
});
|
|
149
128
|
}
|
|
150
129
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const dicomCodec = require("@cornerstonejs/dicom-codec");
|
|
2
|
-
const Tags = require("
|
|
2
|
+
const { Tags } = require("@radicalimaging/static-wado-util");
|
|
3
3
|
const getImageInfo = require("./getImageInfo");
|
|
4
4
|
|
|
5
|
+
dicomCodec.setConfig("verbose: false");
|
|
6
|
+
|
|
5
7
|
const transcodeOp = {
|
|
6
8
|
none: 0,
|
|
7
9
|
decode: 1,
|
|
@@ -33,6 +35,14 @@ const transcodeDestinationMap = {
|
|
|
33
35
|
transferSyntaxUid: "1.2.840.10008.1.2.4.91",
|
|
34
36
|
transcodeOp: transcodeOp.encode,
|
|
35
37
|
},
|
|
38
|
+
jhc: {
|
|
39
|
+
transferSyntaxUid: "3.2.840.10008.1.2.4.96",
|
|
40
|
+
transcodeOp: transcodeOp.encode,
|
|
41
|
+
},
|
|
42
|
+
jpeg: {
|
|
43
|
+
transferSyntaxUid: "1.2.840.10008.1.2.4.50",
|
|
44
|
+
transcodeOp: transcodeOp.encode,
|
|
45
|
+
},
|
|
36
46
|
};
|
|
37
47
|
|
|
38
48
|
const transcodeSourceMap = {
|
|
@@ -48,13 +58,17 @@ const transcodeSourceMap = {
|
|
|
48
58
|
transcodeOp: transcodeOp.decode,
|
|
49
59
|
alias: "uncompressed",
|
|
50
60
|
},
|
|
61
|
+
"1.2.840.10008.1.2.4.50": {
|
|
62
|
+
transcodeOp: transcodeOp.decode,
|
|
63
|
+
alias: "jpeg",
|
|
64
|
+
},
|
|
51
65
|
"1.2.840.10008.1.2.4.57": {
|
|
52
66
|
transcodeOp: transcodeOp.decode,
|
|
53
|
-
alias: "
|
|
67
|
+
alias: "jls",
|
|
54
68
|
},
|
|
55
69
|
"1.2.840.10008.1.2.4.70": {
|
|
56
70
|
transcodeOp: transcodeOp.decode,
|
|
57
|
-
alias: "
|
|
71
|
+
alias: "jls",
|
|
58
72
|
},
|
|
59
73
|
"1.2.840.10008.1.2.4.90": {
|
|
60
74
|
transcodeOp: transcodeOp.decode,
|
|
@@ -64,6 +78,10 @@ const transcodeSourceMap = {
|
|
|
64
78
|
transcodeOp: transcodeOp.decode,
|
|
65
79
|
alias: "jp2",
|
|
66
80
|
},
|
|
81
|
+
"3.2.840.10008.1.2.4.96": {
|
|
82
|
+
transcodeOp: transcodeOp.decode,
|
|
83
|
+
alias: "jhc",
|
|
84
|
+
},
|
|
67
85
|
"1.2.840.10008.1.2.5": {
|
|
68
86
|
transcodeOp: transcodeOp.decode,
|
|
69
87
|
alias: "rle",
|
|
@@ -90,10 +108,8 @@ function getDestinationTranscoder(id) {
|
|
|
90
108
|
* @param {*} transferSyntaxUid
|
|
91
109
|
* @returns
|
|
92
110
|
*/
|
|
93
|
-
function getTranscoder(transferSyntaxUid, { contentType,
|
|
94
|
-
|
|
95
|
-
console.log(`getTranscoder for ${transferSyntaxUid}`);
|
|
96
|
-
}
|
|
111
|
+
function getTranscoder(transferSyntaxUid, { contentType: greyContentType, colorContentType }, samplesPerPixel) {
|
|
112
|
+
const contentType = samplesPerPixel===3 ? colorContentType : greyContentType;
|
|
97
113
|
const sourceTranscoder = transcodeSourceMap[transferSyntaxUid];
|
|
98
114
|
const destinationTranscoder = getDestinationTranscoder(contentType);
|
|
99
115
|
if (!sourceTranscoder || !destinationTranscoder) {
|
|
@@ -112,18 +128,25 @@ function getTranscoder(transferSyntaxUid, { contentType, verbose }) {
|
|
|
112
128
|
* @param {*} id object containing transferSyntaxUid
|
|
113
129
|
* @param {*} options runner options
|
|
114
130
|
*/
|
|
115
|
-
function shouldTranscodeImageFrame(id, options) {
|
|
116
|
-
|
|
131
|
+
function shouldTranscodeImageFrame(id, options, samplesPerPixel) {
|
|
132
|
+
const { recompress: recompressGrey, recompressColor } = options;
|
|
133
|
+
const recompress = samplesPerPixel === 3 ? recompressColor : recompressGrey;
|
|
134
|
+
if (!recompress) {
|
|
135
|
+
console.verbose("Not transcoding because recompress not set");
|
|
117
136
|
return false;
|
|
118
137
|
}
|
|
119
138
|
|
|
120
139
|
function isValidTranscoder() {
|
|
121
140
|
const { transferSyntaxUid } = id;
|
|
122
|
-
const transcoder = getTranscoder(transferSyntaxUid, options);
|
|
123
|
-
return transcoder && transcoder.transferSyntaxUid &&
|
|
141
|
+
const transcoder = getTranscoder(transferSyntaxUid, options, samplesPerPixel);
|
|
142
|
+
return transcoder && transcoder.transferSyntaxUid && (recompress.includes("true") || recompress.includes(transcoder.alias));
|
|
124
143
|
}
|
|
125
144
|
|
|
126
|
-
|
|
145
|
+
const ret = isValidTranscoder();
|
|
146
|
+
if (!ret) {
|
|
147
|
+
console.verbose("Not transcoding");
|
|
148
|
+
}
|
|
149
|
+
return ret;
|
|
127
150
|
}
|
|
128
151
|
|
|
129
152
|
/**
|
|
@@ -140,7 +163,8 @@ function shouldThumbUseTranscoded(id, options) {
|
|
|
140
163
|
|
|
141
164
|
function isValidTranscoder() {
|
|
142
165
|
const { transferSyntaxUid } = id;
|
|
143
|
-
|
|
166
|
+
// Ignore the samples per pixel for thumbnails
|
|
167
|
+
const transcoder = getTranscoder(transferSyntaxUid, options, "thumbnail");
|
|
144
168
|
const result = transcoder && transcoder.transferSyntaxUid && options.recompress.includes(transcoder.alias) && options.recompressThumb.includes(transcoder.alias);
|
|
145
169
|
|
|
146
170
|
return result;
|
|
@@ -169,7 +193,9 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
|
|
|
169
193
|
let targetId = targetIdSrc;
|
|
170
194
|
let result = {};
|
|
171
195
|
|
|
172
|
-
|
|
196
|
+
const samplesPerPixel = dataSet.uint16(Tags.RawSamplesPerPixel);
|
|
197
|
+
if (!shouldTranscodeImageFrame(id, options, samplesPerPixel)) {
|
|
198
|
+
console.verbose("Shouldn't transcode");
|
|
173
199
|
return {
|
|
174
200
|
id,
|
|
175
201
|
imageFrame,
|
|
@@ -177,10 +203,11 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
|
|
|
177
203
|
};
|
|
178
204
|
}
|
|
179
205
|
|
|
180
|
-
const transcoder = getTranscoder(id.transferSyntaxUid, options);
|
|
206
|
+
const transcoder = getTranscoder(id.transferSyntaxUid, options, samplesPerPixel);
|
|
181
207
|
|
|
182
208
|
// last chance to prevent transcoding
|
|
183
209
|
if (targetId.transferSyntaxUid !== transcoder.transferSyntaxUid) {
|
|
210
|
+
console.verbose("Image is already in", targetId.transferSyntaxUid, "not transcoding");
|
|
184
211
|
return {
|
|
185
212
|
id,
|
|
186
213
|
imageFrame,
|
|
@@ -188,9 +215,8 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
|
|
|
188
215
|
};
|
|
189
216
|
}
|
|
190
217
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
218
|
+
console.verbose("Transcoding to", transcoder.transferSyntaxUid);
|
|
219
|
+
|
|
194
220
|
const imageInfo = getImageInfo(dataSet);
|
|
195
221
|
let done = false;
|
|
196
222
|
let processResultMsg = "";
|
|
@@ -254,13 +280,13 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
|
|
|
254
280
|
* @param {*} options runner options.
|
|
255
281
|
* @returns Transcoded id object
|
|
256
282
|
*/
|
|
257
|
-
function transcodeId(id, options) {
|
|
258
|
-
if (!shouldTranscodeImageFrame(id, options)) {
|
|
283
|
+
function transcodeId(id, options, samplesPerPixel) {
|
|
284
|
+
if (!shouldTranscodeImageFrame(id, options, samplesPerPixel)) {
|
|
259
285
|
return id;
|
|
260
286
|
}
|
|
261
287
|
|
|
262
288
|
const targetId = { ...id };
|
|
263
|
-
const { transferSyntaxUid } = getTranscoder(id.transferSyntaxUid, options);
|
|
289
|
+
const { transferSyntaxUid } = getTranscoder(id.transferSyntaxUid, options, samplesPerPixel);
|
|
264
290
|
|
|
265
291
|
targetId.transferSyntaxUid = transferSyntaxUid;
|
|
266
292
|
|
|
@@ -279,16 +305,18 @@ function transcodeId(id, options) {
|
|
|
279
305
|
* @returns Transcoded metadata.
|
|
280
306
|
*/
|
|
281
307
|
function transcodeMetadata(metadata, id, options) {
|
|
282
|
-
|
|
308
|
+
const samplesPerPixel = Tags.getValue(metadata, Tags.SamplesPerPixel);
|
|
309
|
+
if (!shouldTranscodeImageFrame(id, options, samplesPerPixel)) {
|
|
283
310
|
return metadata;
|
|
284
311
|
}
|
|
285
312
|
|
|
286
|
-
const transcodedId = transcodeId(id, options);
|
|
313
|
+
const transcodedId = transcodeId(id, options, samplesPerPixel);
|
|
287
314
|
|
|
288
315
|
const result = { ...metadata };
|
|
289
316
|
|
|
290
317
|
if (result[Tags.AvailableTransferSyntaxUID]) {
|
|
291
318
|
Tags.setValue(result, Tags.AvailableTransferSyntaxUID, transcodedId.transferSyntaxUid);
|
|
319
|
+
console.verbose("Apply available tsuid", transcodeId.transferSyntaxUid);
|
|
292
320
|
}
|
|
293
321
|
|
|
294
322
|
return result;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { Stats } = require("@radicalimaging/static-wado-util");
|
|
2
|
+
|
|
1
3
|
const getNumberOfFrames = require("./getNumberOfFrames");
|
|
2
4
|
const getUncompressedImageFrame = require("./getUncompressedImageFrame");
|
|
3
5
|
const getEncapsulatedImageFrame = require("./getEncapsulatedImageFrame");
|
|
@@ -30,6 +32,7 @@ const extractImageFrames = async (dataSet, attr, vr, callback) => {
|
|
|
30
32
|
if (attr.encapsulatedPixelData) {
|
|
31
33
|
const compressedFrame = getEncapsulatedImageFrame(dataSet, attr, frameIndex, framesAreFragmented);
|
|
32
34
|
BulkDataURI = await callback.imageFrame(compressedFrame, { dataSet });
|
|
35
|
+
Stats.OverallStats.add("Image Write", `Write image frame ${frameIndex + 1}`, 5000);
|
|
33
36
|
} else {
|
|
34
37
|
const uncompressedFrame = getUncompressedImageFrame(dataSet, attr, frameIndex, uncompressedFrameSize);
|
|
35
38
|
BulkDataURI = await callback.imageFrame(uncompressedFrame, { dataSet });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
const { Tags } = require("@radicalimaging/static-wado-util");
|
|
1
2
|
const getVR = require("./getVR");
|
|
2
3
|
const getValue = require("./getValue");
|
|
3
|
-
const Tags = require("../dictionary/Tags");
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Get dataset.
|
|
@@ -13,8 +13,9 @@ const Tags = require("../dictionary/Tags");
|
|
|
13
13
|
* @param {*} parentAttr Parent reference for sequence element tags.
|
|
14
14
|
* @returns
|
|
15
15
|
*/
|
|
16
|
-
async function getDataSet(dataSet, callback,
|
|
16
|
+
async function getDataSet(dataSet, callback, optionsOrig, parentAttr = undefined) {
|
|
17
17
|
const metadata = {};
|
|
18
|
+
let options = optionsOrig;
|
|
18
19
|
|
|
19
20
|
// iterate over dataSet attributes in order
|
|
20
21
|
for (const tag in dataSet.elements) {
|
|
@@ -22,6 +23,10 @@ async function getDataSet(dataSet, callback, options, parentAttr = undefined) {
|
|
|
22
23
|
if (tag != Tags.RawTransferSyntaxUID && tag >= Tags.RawMinTag && tag < Tags.RawFirstBodyTag) {
|
|
23
24
|
continue;
|
|
24
25
|
}
|
|
26
|
+
if (tag === Tags.RawSpecificCharacterSet) {
|
|
27
|
+
const SpecificCharacterSet = dataSet.string(tag);
|
|
28
|
+
options = { ...options, SpecificCharacterSet };
|
|
29
|
+
}
|
|
25
30
|
const attr = dataSet.elements[tag];
|
|
26
31
|
/* eslint-disable-next-line no-use-before-define */
|
|
27
32
|
await attributeToJS(metadata, tag, dataSet, attr, callback, options, parentAttr);
|
package/lib/operation/getVR.js
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
const extractImageFrames = require("./extractImageFrames");
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function decodeUtf8(s, options) {
|
|
4
|
+
if (options?.SpecificCharacterSet === "ISO_IR 192") {
|
|
5
|
+
return decodeURIComponent(escape(s));
|
|
6
|
+
}
|
|
7
|
+
return s;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Gets a value as a UTF-8 string - todo, make UTF-8 decode optional. */
|
|
11
|
+
const getValueInlineString = (dataSet, attr, options) => [decodeUtf8(dataSet.string(attr.tag), options)];
|
|
4
12
|
|
|
5
|
-
const getStrings = (dataSet, attr) => {
|
|
13
|
+
const getStrings = (dataSet, attr, options) => {
|
|
6
14
|
const ret = dataSet.string(attr.tag);
|
|
7
|
-
|
|
15
|
+
if (ret) {
|
|
16
|
+
return ret.split(/\\/).map(decodeUtf8, options);
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
8
19
|
};
|
|
9
20
|
|
|
10
21
|
const getValuePatientName = (dataSet, attr) => {
|
|
@@ -78,7 +89,7 @@ const getValueInlineAttributeTag = (dataSet, attr) => {
|
|
|
78
89
|
return groupHexStr + elementHexStr;
|
|
79
90
|
};
|
|
80
91
|
|
|
81
|
-
const getValueInline = (dataSet, attr, vr) => {
|
|
92
|
+
const getValueInline = (dataSet, attr, vr, options) => {
|
|
82
93
|
if (attr.length == 0) {
|
|
83
94
|
return [];
|
|
84
95
|
}
|
|
@@ -86,7 +97,7 @@ const getValueInline = (dataSet, attr, vr) => {
|
|
|
86
97
|
switch (vr) {
|
|
87
98
|
case "AE":
|
|
88
99
|
case "AS":
|
|
89
|
-
return getValueInlineString(dataSet, attr);
|
|
100
|
+
return getValueInlineString(dataSet, attr, options);
|
|
90
101
|
case "AT":
|
|
91
102
|
return getValueInlineAttributeTag(dataSet, attr);
|
|
92
103
|
case "DS":
|
|
@@ -94,7 +105,7 @@ const getValueInline = (dataSet, attr, vr) => {
|
|
|
94
105
|
case "CS":
|
|
95
106
|
case "DA":
|
|
96
107
|
case "DT":
|
|
97
|
-
return getValueInlineString(dataSet, attr);
|
|
108
|
+
return getValueInlineString(dataSet, attr, options);
|
|
98
109
|
case "FL":
|
|
99
110
|
return getValueInlineFloat(dataSet, attr);
|
|
100
111
|
case "FD":
|
|
@@ -103,7 +114,7 @@ const getValueInline = (dataSet, attr, vr) => {
|
|
|
103
114
|
return getValueInlineIntString(dataSet, attr);
|
|
104
115
|
case "LO":
|
|
105
116
|
case "LT":
|
|
106
|
-
return getValueInlineString(dataSet, attr);
|
|
117
|
+
return getValueInlineString(dataSet, attr, options);
|
|
107
118
|
case "OB":
|
|
108
119
|
case "OF":
|
|
109
120
|
case "OW":
|
|
@@ -111,7 +122,7 @@ const getValueInline = (dataSet, attr, vr) => {
|
|
|
111
122
|
case "PN":
|
|
112
123
|
return getValuePatientName(dataSet, attr);
|
|
113
124
|
case "SH":
|
|
114
|
-
return getValueInlineString(dataSet, attr);
|
|
125
|
+
return getValueInlineString(dataSet, attr, options);
|
|
115
126
|
case "SL":
|
|
116
127
|
return getValueInlineSignedLong(dataSet, attr);
|
|
117
128
|
case "SS":
|
|
@@ -119,7 +130,7 @@ const getValueInline = (dataSet, attr, vr) => {
|
|
|
119
130
|
case "ST":
|
|
120
131
|
case "TM":
|
|
121
132
|
case "UI":
|
|
122
|
-
return getValueInlineString(dataSet, attr);
|
|
133
|
+
return getValueInlineString(dataSet, attr, options);
|
|
123
134
|
case "UL":
|
|
124
135
|
return getValueInlineUnsignedLong(dataSet, attr);
|
|
125
136
|
case "UN":
|
|
@@ -127,7 +138,7 @@ const getValueInline = (dataSet, attr, vr) => {
|
|
|
127
138
|
case "US":
|
|
128
139
|
return getValueInlineUnsignedShort(dataSet, attr);
|
|
129
140
|
case "UT":
|
|
130
|
-
return getValueInlineString(dataSet, attr);
|
|
141
|
+
return getValueInlineString(dataSet, attr, options);
|
|
131
142
|
default:
|
|
132
143
|
return getValueInlineBinary(dataSet, attr);
|
|
133
144
|
}
|
|
@@ -183,7 +194,7 @@ const getValue = async (dataSet, attr, vr, getDataSet, callback, options, parent
|
|
|
183
194
|
}
|
|
184
195
|
// non sequence item
|
|
185
196
|
if (isValueInline(attr, options)) {
|
|
186
|
-
return getValueInline(dataSet, attr, vr);
|
|
197
|
+
return getValueInline(dataSet, attr, vr, options);
|
|
187
198
|
}
|
|
188
199
|
const binaryValue = dataSet.byteArray.slice(attr.dataOffset, attr.dataOffset + attr.length);
|
|
189
200
|
const mimeType = attr.tag == "x00420011" && dataSet.string("x00420012");
|