@radicalimaging/static-wado-creator 1.1.0 → 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,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 { Tags } = require("@radicalimaging/static-wado-util");
8
- const decodeImage = require("./adapter/decodeImage");
4
+ const { Tags, execSpawn, Stats } = require("@radicalimaging/static-wado-util");
9
5
  const { shouldThumbUseTranscoded } = require("./adapter/transcodeImage");
10
- const { isVideo } = require("../writer/VideoWriter");
11
6
 
12
7
  /**
13
8
  * Return the middle index of given list
@@ -17,21 +12,6 @@ const { isVideo } = require("../writer/VideoWriter");
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
 
@@ -2,6 +2,8 @@ const dicomCodec = require("@cornerstonejs/dicom-codec");
2
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,
@@ -37,6 +39,10 @@ const transcodeDestinationMap = {
37
39
  transferSyntaxUid: "3.2.840.10008.1.2.4.96",
38
40
  transcodeOp: transcodeOp.encode,
39
41
  },
42
+ jpeg: {
43
+ transferSyntaxUid: "1.2.840.10008.1.2.4.50",
44
+ transcodeOp: transcodeOp.encode,
45
+ },
40
46
  };
41
47
 
42
48
  const transcodeSourceMap = {
@@ -52,6 +58,10 @@ const transcodeSourceMap = {
52
58
  transcodeOp: transcodeOp.decode,
53
59
  alias: "uncompressed",
54
60
  },
61
+ "1.2.840.10008.1.2.4.50": {
62
+ transcodeOp: transcodeOp.decode,
63
+ alias: "jpeg",
64
+ },
55
65
  "1.2.840.10008.1.2.4.57": {
56
66
  transcodeOp: transcodeOp.decode,
57
67
  alias: "jls",
@@ -98,10 +108,8 @@ function getDestinationTranscoder(id) {
98
108
  * @param {*} transferSyntaxUid
99
109
  * @returns
100
110
  */
101
- function getTranscoder(transferSyntaxUid, { contentType, verbose }) {
102
- if (verbose) {
103
- console.log('getTranscoder for', transferSyntaxUid, 'source to', contentType);
104
- }
111
+ function getTranscoder(transferSyntaxUid, { contentType: greyContentType, colorContentType }, samplesPerPixel) {
112
+ const contentType = samplesPerPixel===3 ? colorContentType : greyContentType;
105
113
  const sourceTranscoder = transcodeSourceMap[transferSyntaxUid];
106
114
  const destinationTranscoder = getDestinationTranscoder(contentType);
107
115
  if (!sourceTranscoder || !destinationTranscoder) {
@@ -120,22 +128,23 @@ function getTranscoder(transferSyntaxUid, { contentType, verbose }) {
120
128
  * @param {*} id object containing transferSyntaxUid
121
129
  * @param {*} options runner options
122
130
  */
123
- function shouldTranscodeImageFrame(id, options) {
124
- const { recompress } = options;
131
+ function shouldTranscodeImageFrame(id, options, samplesPerPixel) {
132
+ const { recompress: recompressGrey, recompressColor } = options;
133
+ const recompress = samplesPerPixel === 3 ? recompressColor : recompressGrey;
125
134
  if (!recompress) {
126
- if( options.verbose ) console.log("Not transcoding because recompress not set");
135
+ console.verbose("Not transcoding because recompress not set");
127
136
  return false;
128
137
  }
129
138
 
130
139
  function isValidTranscoder() {
131
140
  const { transferSyntaxUid } = id;
132
- const transcoder = getTranscoder(transferSyntaxUid, options);
141
+ const transcoder = getTranscoder(transferSyntaxUid, options, samplesPerPixel);
133
142
  return transcoder && transcoder.transferSyntaxUid && (recompress.includes("true") || recompress.includes(transcoder.alias));
134
143
  }
135
144
 
136
- const ret = isValidTranscoder();
137
- if( !ret && options.verbose ) {
138
- console.log("Not transcoding");
145
+ const ret = isValidTranscoder();
146
+ if (!ret) {
147
+ console.verbose("Not transcoding");
139
148
  }
140
149
  return ret;
141
150
  }
@@ -154,7 +163,8 @@ function shouldThumbUseTranscoded(id, options) {
154
163
 
155
164
  function isValidTranscoder() {
156
165
  const { transferSyntaxUid } = id;
157
- const transcoder = getTranscoder(transferSyntaxUid, options);
166
+ // Ignore the samples per pixel for thumbnails
167
+ const transcoder = getTranscoder(transferSyntaxUid, options, "thumbnail");
158
168
  const result = transcoder && transcoder.transferSyntaxUid && options.recompress.includes(transcoder.alias) && options.recompressThumb.includes(transcoder.alias);
159
169
 
160
170
  return result;
@@ -183,8 +193,9 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
183
193
  let targetId = targetIdSrc;
184
194
  let result = {};
185
195
 
186
- if (!shouldTranscodeImageFrame(id, options)) {
187
- console.log("Shouldn't transcode");
196
+ const samplesPerPixel = dataSet.uint16(Tags.RawSamplesPerPixel);
197
+ if (!shouldTranscodeImageFrame(id, options, samplesPerPixel)) {
198
+ console.verbose("Shouldn't transcode");
188
199
  return {
189
200
  id,
190
201
  imageFrame,
@@ -192,11 +203,11 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
192
203
  };
193
204
  }
194
205
 
195
- const transcoder = getTranscoder(id.transferSyntaxUid, options);
206
+ const transcoder = getTranscoder(id.transferSyntaxUid, options, samplesPerPixel);
196
207
 
197
208
  // last chance to prevent transcoding
198
209
  if (targetId.transferSyntaxUid !== transcoder.transferSyntaxUid) {
199
- console.log("Image is already in", targetId.transferSyntaxUid, "not transcoding");
210
+ console.verbose("Image is already in", targetId.transferSyntaxUid, "not transcoding");
200
211
  return {
201
212
  id,
202
213
  imageFrame,
@@ -204,8 +215,8 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
204
215
  };
205
216
  }
206
217
 
207
- if( options.verbose ) console.log("Transcoding to", transcoder.transferSyntaxUid);
208
-
218
+ console.verbose("Transcoding to", transcoder.transferSyntaxUid);
219
+
209
220
  const imageInfo = getImageInfo(dataSet);
210
221
  let done = false;
211
222
  let processResultMsg = "";
@@ -269,13 +280,13 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
269
280
  * @param {*} options runner options.
270
281
  * @returns Transcoded id object
271
282
  */
272
- function transcodeId(id, options) {
273
- if (!shouldTranscodeImageFrame(id, options)) {
283
+ function transcodeId(id, options, samplesPerPixel) {
284
+ if (!shouldTranscodeImageFrame(id, options, samplesPerPixel)) {
274
285
  return id;
275
286
  }
276
287
 
277
288
  const targetId = { ...id };
278
- const { transferSyntaxUid } = getTranscoder(id.transferSyntaxUid, options);
289
+ const { transferSyntaxUid } = getTranscoder(id.transferSyntaxUid, options, samplesPerPixel);
279
290
 
280
291
  targetId.transferSyntaxUid = transferSyntaxUid;
281
292
 
@@ -294,19 +305,18 @@ function transcodeId(id, options) {
294
305
  * @returns Transcoded metadata.
295
306
  */
296
307
  function transcodeMetadata(metadata, id, options) {
297
- if (!shouldTranscodeImageFrame(id, options)) {
308
+ const samplesPerPixel = Tags.getValue(metadata, Tags.SamplesPerPixel);
309
+ if (!shouldTranscodeImageFrame(id, options, samplesPerPixel)) {
298
310
  return metadata;
299
311
  }
300
312
 
301
- const transcodedId = transcodeId(id, options);
313
+ const transcodedId = transcodeId(id, options, samplesPerPixel);
302
314
 
303
315
  const result = { ...metadata };
304
316
 
305
317
  if (result[Tags.AvailableTransferSyntaxUID]) {
306
318
  Tags.setValue(result, Tags.AvailableTransferSyntaxUID, transcodedId.transferSyntaxUid);
307
- if( this.verbose ) {
308
- console.log("Apply available tsuid", transcodeId.transferSyntaxUid);
309
- }
319
+ console.verbose("Apply available tsuid", transcodeId.transferSyntaxUid);
310
320
  }
311
321
 
312
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 });
@@ -13,8 +13,9 @@ const getValue = require("./getValue");
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,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");
package/lib/rejectMain.js CHANGED
@@ -1,12 +1,12 @@
1
1
  const StaticWado = require("./StaticWado");
2
2
  const adaptProgramOpts = require("./util/adaptProgramOpts");
3
3
 
4
- module.exports = function rejectMain(options, program) {
4
+ module.exports = function rejectMain(args, options) {
5
5
  const finalOptions = adaptProgramOpts(options, {
6
6
  ...this,
7
7
  isGroup: true,
8
8
  isStudyData: true,
9
9
  });
10
10
  const importer = new StaticWado(finalOptions);
11
- return importer.reject(program.args);
11
+ return importer.reject(args);
12
12
  };
@@ -10,51 +10,30 @@ const assignDefined = (dest, src) => {
10
10
 
11
11
  module.exports = function adaptProgramOpts(programOpts, defaults) {
12
12
  const {
13
- maximumInlinePublicLength,
14
- maximumInlinePrivateLength,
15
13
  group: isGroup,
16
14
  instances: isInstanceMetadata,
17
15
  deduplicate: isDeduplicate,
18
16
  study: isStudyData,
19
17
  clean: isClean,
20
- recompress,
21
- recompressThumb,
22
- contentType,
23
- colourContentType,
24
18
  dir: rootDir,
25
- pathDeduplicated,
26
- pathInstances,
27
- removeDeduplicatedInstances,
28
- storeMultipartBulkData,
29
- prependBulkDataUri,
30
- expandBulkDataUri,
31
- verbose,
32
- scpPort,
19
+ delete: deleteInstances,
20
+ singlePartImage,
21
+ encapsulatedImage,
33
22
  } = programOpts;
34
23
 
35
24
  return assignDefined(
36
25
  { ...defaults },
37
26
  {
38
- maximumInlinePublicLength,
39
- maximumInlinePrivateLength,
40
- isGroup,
41
- isInstanceMetadata,
42
- isDeduplicate,
27
+ ...programOpts,
43
28
  isStudyData,
44
29
  isClean,
45
- recompress,
46
- recompressThumb,
47
- contentType,
48
- colourContentType,
30
+ isInstanceMetadata,
31
+ isDeduplicate,
32
+ isGroup,
49
33
  rootDir,
50
- pathDeduplicated,
51
- pathInstances,
52
- removeDeduplicatedInstances,
53
- storeMultipartBulkData,
54
- scpPort,
55
- prependBulkDataUri,
56
- expandBulkDataUri,
57
- verbose,
34
+ encapsulatedImage: encapsulatedImage ?? singlePartImage !== true,
35
+ singlePartImage: singlePartImage ?? encapsulatedImage === false,
36
+ delete: deleteInstances,
58
37
  }
59
38
  );
60
39
  };
@@ -26,9 +26,12 @@ const CompleteStudyWriter = (options) => {
26
26
  } else {
27
27
  console.log("Not writing new deduplicated data because it is clean:", studyData.studyInstanceUid);
28
28
  }
29
+ await studyData.deleteInstancesReferenced();
29
30
  }
30
31
 
31
32
  if (!options.isStudyData) {
33
+ console.verbose("Not configured to write study metadata", studyData.studyInstanceUid);
34
+ if (options.notifications) this.notificationService.notifyStudy(studyData.studyInstanceUid);
32
35
  delete this.studyData;
33
36
  Stats.StudyStats.summarize();
34
37
  return;
@@ -36,11 +39,12 @@ const CompleteStudyWriter = (options) => {
36
39
 
37
40
  const isDirtyMetadata = await studyData.dirtyMetadata();
38
41
  if (!isDirtyMetadata) {
39
- console.log("Study metadata", studyData.studyInstanceUid, "has clean metadata, not writing");
42
+ console.log("Study metadata", studyData.studyInstanceUid, "is clean.");
40
43
  delete this.studyData;
41
44
  Stats.StudyStats.summarize(`Study metadata ${studyData.studyInstanceUid} has clean metadata, not writing`);
42
45
  return;
43
46
  }
47
+ console.log("Writing study metadata", studyData.studyInstanceUid);
44
48
 
45
49
  const studyQuery = await studyData.writeMetadata();
46
50
 
@@ -57,6 +61,7 @@ const CompleteStudyWriter = (options) => {
57
61
  allStudies[studyIndex] = studyQuery;
58
62
  }
59
63
  await JSONWriter(options.directoryName, "studies", allStudies);
64
+ if (options.notifications) this.notificationService.notifyStudy(studyData.studyInstanceUid);
60
65
  delete this.studyData;
61
66
  Stats.StudyStats.summarize(`Wrote study metadata/query files for ${studyData.studyInstanceUid}`);
62
67
  }
@@ -24,7 +24,10 @@ const DeduplicateWriter = (options) =>
24
24
  const studyData = await this.completeStudy.getCurrentStudyData(this, id);
25
25
 
26
26
  if (options.isDeduplicate) {
27
+ if (options.verbose) console.log("Writing single instance", id.studyInstanceUid);
27
28
  await perInstanceWriter(id, data);
29
+ } else if (options.verbose) {
30
+ console.log("Not writing single instance deduplicated");
28
31
  }
29
32
  studyData.addDeduplicated(data, id.filename);
30
33
  };
@@ -20,7 +20,6 @@ const HashDataWriter =
20
20
  (options) =>
21
21
  async (id, key, data, additionalOptions = {}) => {
22
22
  const isRaw = ArrayBuffer.isView(data);
23
- const { storeMultipartBulkData } = options;
24
23
  const { mimeType } = additionalOptions;
25
24
  // If the file has an extension, it should be directly accessible as that file type.
26
25
  const gzip = !isRaw || (data.length > 1024 && !mimeType);
@@ -31,7 +30,7 @@ const HashDataWriter =
31
30
  mkdir: true,
32
31
  gzip,
33
32
  });
34
- if (isRaw && storeMultipartBulkData) {
33
+ if (isRaw) {
35
34
  await WriteMultipart(writeStream, [new MultipartHeader("Content-Type", "application/octet-stream")], rawData);
36
35
  } else {
37
36
  await writeStream.write(rawData);
@@ -5,8 +5,7 @@ const ExpandUriPath = require("./ExpandUriPath");
5
5
  const { MultipartHeader, MultipartAttribute } = require("./MultipartHeader");
6
6
 
7
7
  const ImageFrameWriter = (options) => {
8
- const { verbose, encapsulatedImageFrame = true } = options;
9
-
8
+ const { encapsulatedImage, singlePartImage } = options;
10
9
  return async (id, index, imageFrame) => {
11
10
  const { transferSyntaxUid } = id;
12
11
  const type = uids[transferSyntaxUid] || uids.default;
@@ -18,7 +17,7 @@ const ImageFrameWriter = (options) => {
18
17
  content = Buffer.from(imageFrame.buffer);
19
18
  }
20
19
 
21
- if (encapsulatedImageFrame || !extension) {
20
+ if (encapsulatedImage || !extension) {
22
21
  const writeStream = WriteStream(id.imageFrameRootPath, `${1 + index}`, {
23
22
  gzip: type.gzip,
24
23
  mkdir: true,
@@ -28,19 +27,17 @@ const ImageFrameWriter = (options) => {
28
27
  [new MultipartHeader("Content-Type", type.contentType, [new MultipartAttribute("transfer-syntax", transferSyntaxUid)])],
29
28
  content
30
29
  );
31
- writeStream.close();
32
- if (verbose) {
33
- console.log("Wrote encapsulated image frame", id.sopInstanceUid, index + 1, type.contentType);
34
- }
30
+ await writeStream.close();
31
+ console.verbose("Wrote encapsulated image frame", id.sopInstanceUid, index + 1, type.contentType);
35
32
  }
36
- if (extension) {
33
+ if (extension && singlePartImage) {
37
34
  const writeStreamSingle = WriteStream(id.imageFrameRootPath, `${1 + index}${extension}`, {
38
35
  gzip: type.gzip,
39
36
  mkdir: true,
40
37
  });
41
38
  await writeStreamSingle.write(content);
42
- writeStreamSingle.close();
43
- if (verbose) console.log("Wrote single part image frame", id.sopInstanceUid, index + 1, extension);
39
+ await writeStreamSingle.close();
40
+ console.verbose("Wrote single part image frame", id.sopInstanceUid, index + 1, extension);
44
41
  }
45
42
  const includeSeries = true;
46
43
  return ExpandUriPath(id, `instances/${id.sopInstanceUid}/frames`, { includeSeries, ...options });
@@ -1,21 +1,14 @@
1
1
  const WriteStream = require("./WriteStream");
2
2
 
3
- const ThumbnailWriter = (options) => {
4
- const { verbose } = options;
3
+ const ThumbnailWriter = () => async (filePath, fileName, thumbBuffer) => {
4
+ const writeStream = WriteStream(filePath, fileName, {
5
+ mkdir: true,
6
+ });
5
7
 
6
- return async (filePath, fileName, thumbBuffer) => {
7
- const writeStream = WriteStream(filePath, fileName, {
8
- mkdir: true,
9
- });
8
+ await writeStream.write(thumbBuffer);
10
9
 
11
- await writeStream.write(thumbBuffer);
12
-
13
- await writeStream.close();
14
-
15
- if (verbose) {
16
- console.log("Wrote thumbnail frame", filePath, fileName);
17
- }
18
- };
10
+ console.verbose("Wrote thumbnail frame", filePath, fileName);
11
+ return writeStream.close();
19
12
  };
20
13
 
21
14
  module.exports = ThumbnailWriter;
@@ -2,6 +2,8 @@ const fs = require("fs");
2
2
  const zlib = require("zlib");
3
3
  const path = require("path");
4
4
 
5
+ let writeCount = 0;
6
+
5
7
  /** Create an optionally gzipped stream,
6
8
  * where the write operations are performed in order executed,
7
9
  * and don't require synchronization, only the 'close' operation
@@ -14,6 +16,14 @@ const WriteStream = (dir, nameSrc, options = {}) => {
14
16
 
15
17
  const tempName = path.join(dir, `tempFile-${Math.round(Math.random() * 1000000000)}`);
16
18
  const finalName = path.join(dir, name);
19
+ writeCount++;
20
+ if (writeCount > 10) {
21
+ console.log("Write count", tempName, finalName, writeCount);
22
+ if (writeCount > 100) {
23
+ console.error("Too many open writes", new Error());
24
+ throw new Error(`Write count too high ${writeCount} destination ${finalName}`);
25
+ }
26
+ }
17
27
  const rawStream = fs.createWriteStream(tempName);
18
28
  const closePromise = new Promise((resolve) => {
19
29
  rawStream.on("close", () => {
@@ -33,7 +43,8 @@ const WriteStream = (dir, nameSrc, options = {}) => {
33
43
  async function close() {
34
44
  await this.writeStream.end();
35
45
  await this.closePromise;
36
- await fs.rename(tempName, finalName, () => true); // console.log('Renamed', tempName,finalName));
46
+ await fs.rename(tempName, finalName, () => true);
47
+ writeCount--;
37
48
  }
38
49
 
39
50
  return {