@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.
@@ -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("../dictionary/Tags");
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
- // console.log("Study data is dirty - need to write updated file");
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 deduplicatedTopFile = await JSONReader(this.studyPath, "deduplicated.gz", null);
86
- if (!deduplicatedTopFile) {
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 info = deduplicatedTopFile[0];
90
- if (!info || getValue(info, Tags.DeduppedHash) || getValue(info, Tags.DeduppedType) != "info") {
91
- return true;
104
+ const hashValue = getValue(studyFile, Tags.DeduppedHash);
105
+ if (this.existingFiles[0].indexOf(hashValue) == -1) {
106
+ return false;
92
107
  }
93
- const hashValue = getValue(info, Tags.DeduppedHash);
94
- return this.existingFiles[0].indexOf(hashValue) == -1;
108
+ console.verbose("dirtyMetadata::Dedupped hash missing");
109
+ return true;
95
110
  } catch (e) {
96
- console.log("Assume study metadata is dirty", e);
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.log("No deduplicated for", deduplicatedDirectory);
247
- return;
261
+ console.verbose("No deduplicated for", deduplicatedDirectory);
262
+ return 0;
248
263
  }
249
- console.log("There are", files.length, "files to check");
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.log("Done checking", deduplicatedDirectory);
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.readHashes[hash] = name;
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
- await JSONWriter(seriesPath, "series", [seriesQuery]);
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 { exec } = require("child_process");
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
- 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
- });
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
- const thumbPath = path.join(itemId.sopInstanceRootPath, "thumbnail");
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
- 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
- }
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("../../dictionary/Tags");
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: "jpeglossless",
67
+ alias: "jls",
54
68
  },
55
69
  "1.2.840.10008.1.2.4.70": {
56
70
  transcodeOp: transcodeOp.decode,
57
- alias: "jpeglossless",
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, verbose }) {
94
- if (verbose) {
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
- if (!options.recompress) {
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 && options.recompress.includes(transcoder.alias);
141
+ const transcoder = getTranscoder(transferSyntaxUid, options, samplesPerPixel);
142
+ return transcoder && transcoder.transferSyntaxUid && (recompress.includes("true") || recompress.includes(transcoder.alias));
124
143
  }
125
144
 
126
- return isValidTranscoder();
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
- const transcoder = getTranscoder(transferSyntaxUid, options);
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
- if (!shouldTranscodeImageFrame(id, options)) {
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
- if (options.verbose) {
192
- console.log("Transcoding to", transcoder.transferSyntaxUid);
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
- if (!shouldTranscodeImageFrame(id, options)) {
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, options, parentAttr = undefined) {
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);
@@ -1,4 +1,4 @@
1
- const dataDictionary = require("../dictionary/dataDictionary");
1
+ const { dataDictionary } = require("@radicalimaging/static-wado-util");
2
2
 
3
3
  const getVR = (attr) => {
4
4
  if (attr.vr) {
@@ -1,10 +1,21 @@
1
1
  const extractImageFrames = require("./extractImageFrames");
2
2
 
3
- const getValueInlineString = (dataSet, attr) => [dataSet.string(attr.tag)];
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
- return (ret && ret.split(/\\/)) || undefined;
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");