@cornerstonejs/core 1.42.0 → 1.42.1

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 (157) hide show
  1. package/dist/cjs/RenderingEngine/BaseVolumeViewport.js.map +1 -1
  2. package/dist/cjs/RenderingEngine/StackViewport.js +2 -1
  3. package/dist/cjs/RenderingEngine/StackViewport.js.map +1 -1
  4. package/dist/cjs/RenderingEngine/helpers/createVolumeActor.js +2 -1
  5. package/dist/cjs/RenderingEngine/helpers/createVolumeActor.js.map +1 -1
  6. package/dist/cjs/cache/cache.d.ts +6 -3
  7. package/dist/cjs/cache/cache.js +43 -6
  8. package/dist/cjs/cache/cache.js.map +1 -1
  9. package/dist/cjs/cache/classes/ImageVolume.d.ts +25 -5
  10. package/dist/cjs/cache/classes/ImageVolume.js +302 -13
  11. package/dist/cjs/cache/classes/ImageVolume.js.map +1 -1
  12. package/dist/cjs/eventTarget.d.ts +1 -0
  13. package/dist/cjs/eventTarget.js +13 -1
  14. package/dist/cjs/eventTarget.js.map +1 -1
  15. package/dist/cjs/init.js +2 -0
  16. package/dist/cjs/init.js.map +1 -1
  17. package/dist/cjs/loaders/imageLoader.js +6 -2
  18. package/dist/cjs/loaders/imageLoader.js.map +1 -1
  19. package/dist/cjs/loaders/volumeLoader.d.ts +12 -9
  20. package/dist/cjs/loaders/volumeLoader.js +50 -5
  21. package/dist/cjs/loaders/volumeLoader.js.map +1 -1
  22. package/dist/cjs/types/Cornerstone3DConfig.d.ts +1 -0
  23. package/dist/cjs/types/IDynamicImageVolume.d.ts +2 -2
  24. package/dist/cjs/types/IImage.d.ts +5 -0
  25. package/dist/cjs/types/IImageVolume.d.ts +7 -2
  26. package/dist/cjs/types/ILoadObject.d.ts +2 -2
  27. package/dist/cjs/types/IVolume.d.ts +3 -26
  28. package/dist/cjs/types/ImageVolumeProps.d.ts +6 -0
  29. package/dist/cjs/types/ImageVolumeProps.js +3 -0
  30. package/dist/cjs/types/ImageVolumeProps.js.map +1 -0
  31. package/dist/cjs/types/VolumeProps.d.ts +27 -0
  32. package/dist/cjs/types/VolumeProps.js +3 -0
  33. package/dist/cjs/types/VolumeProps.js.map +1 -0
  34. package/dist/cjs/types/index.d.ts +4 -2
  35. package/dist/cjs/utilities/VoxelManager.d.ts +2 -2
  36. package/dist/cjs/utilities/cacheUtils.d.ts +2 -0
  37. package/dist/cjs/utilities/cacheUtils.js +96 -0
  38. package/dist/cjs/utilities/cacheUtils.js.map +1 -0
  39. package/dist/cjs/utilities/convertStackToVolumeViewport.d.ts +12 -0
  40. package/dist/cjs/utilities/convertStackToVolumeViewport.js +61 -0
  41. package/dist/cjs/utilities/convertStackToVolumeViewport.js.map +1 -0
  42. package/dist/cjs/utilities/convertVolumeToStackViewport.d.ts +9 -0
  43. package/dist/cjs/utilities/convertVolumeToStackViewport.js +95 -0
  44. package/dist/cjs/utilities/convertVolumeToStackViewport.js.map +1 -0
  45. package/dist/cjs/utilities/generateVolumePropsFromImageIds.d.ts +3 -0
  46. package/dist/cjs/utilities/generateVolumePropsFromImageIds.js +124 -0
  47. package/dist/cjs/utilities/generateVolumePropsFromImageIds.js.map +1 -0
  48. package/dist/cjs/utilities/index.d.ts +6 -1
  49. package/dist/cjs/utilities/index.js +12 -1
  50. package/dist/cjs/utilities/index.js.map +1 -1
  51. package/dist/cjs/utilities/roundNumber.d.ts +4 -0
  52. package/dist/cjs/utilities/roundNumber.js +36 -0
  53. package/dist/cjs/utilities/roundNumber.js.map +1 -0
  54. package/dist/esm/RenderingEngine/BaseVolumeViewport.js.map +1 -1
  55. package/dist/esm/RenderingEngine/StackViewport.js +2 -1
  56. package/dist/esm/RenderingEngine/StackViewport.js.map +1 -1
  57. package/dist/esm/RenderingEngine/helpers/createVolumeActor.js +1 -1
  58. package/dist/esm/RenderingEngine/helpers/createVolumeActor.js.map +1 -1
  59. package/dist/esm/cache/cache.js +43 -6
  60. package/dist/esm/cache/cache.js.map +1 -1
  61. package/dist/esm/cache/classes/ImageVolume.js +279 -14
  62. package/dist/esm/cache/classes/ImageVolume.js.map +1 -1
  63. package/dist/esm/eventTarget.js +13 -1
  64. package/dist/esm/eventTarget.js.map +1 -1
  65. package/dist/esm/init.js +2 -0
  66. package/dist/esm/init.js.map +1 -1
  67. package/dist/esm/loaders/imageLoader.js +5 -2
  68. package/dist/esm/loaders/imageLoader.js.map +1 -1
  69. package/dist/esm/loaders/volumeLoader.js +50 -5
  70. package/dist/esm/loaders/volumeLoader.js.map +1 -1
  71. package/dist/esm/types/ImageVolumeProps.js +2 -0
  72. package/dist/esm/types/ImageVolumeProps.js.map +1 -0
  73. package/dist/esm/types/VolumeProps.js +2 -0
  74. package/dist/esm/types/VolumeProps.js.map +1 -0
  75. package/dist/esm/utilities/cacheUtils.js +65 -0
  76. package/dist/esm/utilities/cacheUtils.js.map +1 -0
  77. package/dist/esm/utilities/convertStackToVolumeViewport.js +49 -0
  78. package/dist/esm/utilities/convertStackToVolumeViewport.js.map +1 -0
  79. package/dist/esm/utilities/convertVolumeToStackViewport.js +58 -0
  80. package/dist/esm/utilities/convertVolumeToStackViewport.js.map +1 -0
  81. package/dist/esm/utilities/generateVolumePropsFromImageIds.js +118 -0
  82. package/dist/esm/utilities/generateVolumePropsFromImageIds.js.map +1 -0
  83. package/dist/esm/utilities/index.js +6 -1
  84. package/dist/esm/utilities/index.js.map +1 -1
  85. package/dist/esm/utilities/roundNumber.js +33 -0
  86. package/dist/esm/utilities/roundNumber.js.map +1 -0
  87. package/dist/types/RenderingEngine/BaseVolumeViewport.d.ts.map +1 -1
  88. package/dist/types/RenderingEngine/StackViewport.d.ts.map +1 -1
  89. package/dist/types/cache/cache.d.ts +6 -3
  90. package/dist/types/cache/cache.d.ts.map +1 -1
  91. package/dist/types/cache/classes/ImageVolume.d.ts +25 -5
  92. package/dist/types/cache/classes/ImageVolume.d.ts.map +1 -1
  93. package/dist/types/eventTarget.d.ts +1 -0
  94. package/dist/types/eventTarget.d.ts.map +1 -1
  95. package/dist/types/loaders/imageLoader.d.ts.map +1 -1
  96. package/dist/types/loaders/volumeLoader.d.ts +12 -9
  97. package/dist/types/loaders/volumeLoader.d.ts.map +1 -1
  98. package/dist/types/types/Cornerstone3DConfig.d.ts +1 -0
  99. package/dist/types/types/Cornerstone3DConfig.d.ts.map +1 -1
  100. package/dist/types/types/IDynamicImageVolume.d.ts +2 -2
  101. package/dist/types/types/IDynamicImageVolume.d.ts.map +1 -1
  102. package/dist/types/types/IImage.d.ts +5 -0
  103. package/dist/types/types/IImage.d.ts.map +1 -1
  104. package/dist/types/types/IImageVolume.d.ts +7 -2
  105. package/dist/types/types/IImageVolume.d.ts.map +1 -1
  106. package/dist/types/types/ILoadObject.d.ts +2 -2
  107. package/dist/types/types/ILoadObject.d.ts.map +1 -1
  108. package/dist/types/types/IVolume.d.ts +3 -26
  109. package/dist/types/types/IVolume.d.ts.map +1 -1
  110. package/dist/types/types/ImageVolumeProps.d.ts +7 -0
  111. package/dist/types/types/ImageVolumeProps.d.ts.map +1 -0
  112. package/dist/types/types/VolumeProps.d.ts +28 -0
  113. package/dist/types/types/VolumeProps.d.ts.map +1 -0
  114. package/dist/types/types/index.d.ts +4 -2
  115. package/dist/types/types/index.d.ts.map +1 -1
  116. package/dist/types/utilities/VoxelManager.d.ts +2 -2
  117. package/dist/types/utilities/VoxelManager.d.ts.map +1 -1
  118. package/dist/types/utilities/cacheUtils.d.ts +3 -0
  119. package/dist/types/utilities/cacheUtils.d.ts.map +1 -0
  120. package/dist/types/utilities/convertStackToVolumeViewport.d.ts +13 -0
  121. package/dist/types/utilities/convertStackToVolumeViewport.d.ts.map +1 -0
  122. package/dist/types/utilities/convertVolumeToStackViewport.d.ts +10 -0
  123. package/dist/types/utilities/convertVolumeToStackViewport.d.ts.map +1 -0
  124. package/dist/types/utilities/generateVolumePropsFromImageIds.d.ts +4 -0
  125. package/dist/types/utilities/generateVolumePropsFromImageIds.d.ts.map +1 -0
  126. package/dist/types/utilities/index.d.ts +6 -1
  127. package/dist/types/utilities/index.d.ts.map +1 -1
  128. package/dist/types/utilities/roundNumber.d.ts +5 -0
  129. package/dist/types/utilities/roundNumber.d.ts.map +1 -0
  130. package/dist/umd/index.js +1 -1
  131. package/dist/umd/index.js.map +1 -1
  132. package/package.json +2 -2
  133. package/src/RenderingEngine/BaseVolumeViewport.ts +0 -1
  134. package/src/RenderingEngine/StackViewport.ts +3 -1
  135. package/src/RenderingEngine/helpers/createVolumeActor.ts +1 -1
  136. package/src/cache/cache.ts +91 -7
  137. package/src/cache/classes/ImageVolume.ts +535 -21
  138. package/src/eventTarget.ts +19 -1
  139. package/src/init.ts +2 -2
  140. package/src/loaders/imageLoader.ts +6 -2
  141. package/src/loaders/volumeLoader.ts +118 -23
  142. package/src/types/Cornerstone3DConfig.ts +12 -0
  143. package/src/types/IDynamicImageVolume.ts +2 -2
  144. package/src/types/IImage.ts +6 -0
  145. package/src/types/IImageVolume.ts +14 -2
  146. package/src/types/ILoadObject.ts +2 -2
  147. package/src/types/IVolume.ts +4 -41
  148. package/src/types/ImageVolumeProps.ts +15 -0
  149. package/src/types/VolumeProps.ts +57 -0
  150. package/src/types/index.ts +5 -2
  151. package/src/utilities/VoxelManager.ts +2 -2
  152. package/src/utilities/cacheUtils.ts +121 -0
  153. package/src/utilities/convertStackToVolumeViewport.ts +115 -0
  154. package/src/utilities/convertVolumeToStackViewport.ts +125 -0
  155. package/src/utilities/generateVolumePropsFromImageIds.ts +183 -0
  156. package/src/utilities/index.ts +11 -0
  157. package/src/utilities/roundNumber.ts +56 -0
@@ -1,15 +1,24 @@
1
+ import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
2
+ import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
1
3
  import isTypedArray from '../../utilities/isTypedArray';
2
- import { imageIdToURI } from '../../utilities';
4
+ import {
5
+ genericMetadataProvider,
6
+ getMinMax,
7
+ imageIdToURI,
8
+ } from '../../utilities';
3
9
  import { vtkStreamingOpenGLTexture } from '../../RenderingEngine/vtkClasses';
4
- import type { vtkImageData } from '@kitware/vtk.js/Common/DataModel/ImageData';
5
10
  import {
6
- IVolume,
7
- VolumeScalarData,
8
11
  Metadata,
9
12
  Point3,
10
13
  IImageVolume,
11
14
  Mat3,
15
+ PixelDataTypedArray,
16
+ ImageVolumeProps,
17
+ IImage,
18
+ IImageLoadObject,
12
19
  } from '../../types';
20
+ import cache from '../cache';
21
+ import * as metaData from '../../metaData';
13
22
 
14
23
  /** The base class for volume data. It includes the volume metadata
15
24
  * and the volume data along with the loading status.
@@ -19,11 +28,16 @@ export class ImageVolume implements IImageVolume {
19
28
  private _imageIdsIndexMap = new Map();
20
29
  private _imageURIsIndexMap = new Map();
21
30
  /** volume scalar data 3D or 4D */
22
- protected scalarData: VolumeScalarData | Array<VolumeScalarData>;
31
+ protected scalarData: PixelDataTypedArray | Array<PixelDataTypedArray>;
32
+ protected numFrames: number;
33
+ protected totalNumFrames: number;
34
+ protected cornerstoneImageMetaData = null;
23
35
 
24
36
  /** Read-only unique identifier for the volume */
25
37
  readonly volumeId: string;
26
38
 
39
+ imageCacheOffsetMap = new Map();
40
+
27
41
  isPreScaled = false;
28
42
 
29
43
  /** Dimensions of the volume */
@@ -60,29 +74,82 @@ export class ImageVolume implements IImageVolume {
60
74
  loadStatus?: Record<string, any>;
61
75
  /** optional reference volume id if the volume is derived from another volume */
62
76
  referencedVolumeId?: string;
77
+ /** optional reference image ids if the volume is derived from a set of images in the image cache */
78
+ referencedImageIds?: Array<string>;
63
79
  /** whether the metadata for the pixel spacing is not undefined */
64
80
  hasPixelSpacing: boolean;
81
+ /** Property to store additional information */
82
+ additionalDetails?: Record<string, any>;
65
83
 
66
- constructor(props: IVolume) {
67
- this.volumeId = props.volumeId;
68
- this.metadata = props.metadata;
69
- this.dimensions = props.dimensions;
70
- this.spacing = props.spacing;
71
- this.origin = props.origin;
72
- this.direction = props.direction;
73
- this.imageData = props.imageData;
74
- this.scalarData = props.scalarData;
75
- this.sizeInBytes = props.sizeInBytes;
84
+ constructor(props: ImageVolumeProps) {
85
+ const {
86
+ imageIds,
87
+ scalarData,
88
+ scaling,
89
+ dimensions,
90
+ spacing,
91
+ origin,
92
+ direction,
93
+ volumeId,
94
+ referencedVolumeId,
95
+ sizeInBytes,
96
+ imageData,
97
+ metadata,
98
+ referencedImageIds,
99
+ additionalDetails,
100
+ } = props;
101
+
102
+ this.imageIds = imageIds;
103
+ this.volumeId = volumeId;
104
+ this.metadata = metadata;
105
+ this.dimensions = dimensions;
106
+ this.spacing = spacing;
107
+ this.origin = origin;
108
+ this.direction = direction;
109
+ this.scalarData = scalarData;
110
+ this.sizeInBytes = sizeInBytes;
76
111
  this.vtkOpenGLTexture = vtkStreamingOpenGLTexture.newInstance();
77
112
  this.numVoxels =
78
113
  this.dimensions[0] * this.dimensions[1] * this.dimensions[2];
79
114
 
80
- if (props.scaling) {
81
- this.scaling = props.scaling;
115
+ if (imageData) {
116
+ this.imageData = imageData;
117
+ } else {
118
+ const imageData = vtkImageData.newInstance();
119
+
120
+ const scalarArray = vtkDataArray.newInstance({
121
+ name: 'Pixels',
122
+ numberOfComponents: 1,
123
+ values: scalarData,
124
+ });
125
+
126
+ imageData.setDimensions(dimensions);
127
+ imageData.setSpacing(spacing);
128
+ imageData.setDirection(direction);
129
+ imageData.setOrigin(origin);
130
+ imageData.getPointData().setScalars(scalarArray);
131
+
132
+ this.imageData = imageData;
82
133
  }
83
134
 
84
- if (props.referencedVolumeId) {
85
- this.referencedVolumeId = props.referencedVolumeId;
135
+ this.numFrames = this._getNumFrames();
136
+ this._reprocessImageIds();
137
+ this._createCornerstoneImageMetaData();
138
+
139
+ if (scaling) {
140
+ this.scaling = scaling;
141
+ }
142
+
143
+ if (referencedVolumeId) {
144
+ this.referencedVolumeId = referencedVolumeId;
145
+ }
146
+
147
+ if (referencedImageIds) {
148
+ this.referencedImageIds = referencedImageIds;
149
+ }
150
+
151
+ if (additionalDetails) {
152
+ this.additionalDetails = additionalDetails;
86
153
  }
87
154
  }
88
155
 
@@ -120,9 +187,9 @@ export class ImageVolume implements IImageVolume {
120
187
  * Return the scalar data for 3D volumes or the active scalar data
121
188
  * (current time point) for 4D volumes
122
189
  */
123
- public getScalarData(): VolumeScalarData {
190
+ public getScalarData(): PixelDataTypedArray {
124
191
  if (isTypedArray(this.scalarData)) {
125
- return <VolumeScalarData>this.scalarData;
192
+ return <PixelDataTypedArray>this.scalarData;
126
193
  }
127
194
 
128
195
  throw new Error('Unknown scalar data type');
@@ -158,6 +225,453 @@ export class ImageVolume implements IImageVolume {
158
225
  this.vtkOpenGLTexture.releaseGraphicsResources();
159
226
  this.vtkOpenGLTexture.delete();
160
227
  }
228
+
229
+ /**
230
+ * Return all scalar data objects (buffers) which will be only one for
231
+ * 3D volumes and one per time point for 4D volumes
232
+ * images of each 3D volume is stored
233
+ * @returns scalar data array
234
+ */
235
+ public getScalarDataArrays(): PixelDataTypedArray[] {
236
+ return this.isDynamicVolume()
237
+ ? <PixelDataTypedArray[]>this.scalarData
238
+ : [<PixelDataTypedArray>this.scalarData];
239
+ }
240
+
241
+ /**
242
+ * If completelyRemove is true, remove the volume completely from the cache. Otherwise,
243
+ * convert the volume to cornerstone images (stack images) and store it in the cache
244
+ * @param completelyRemove - If true, the image will be removed from the
245
+ * cache completely.
246
+ */
247
+ public decache(completelyRemove = false): void | Array<string> {
248
+ if (completelyRemove) {
249
+ this.removeFromCache();
250
+ } else {
251
+ this.convertToImageSlicesAndCache();
252
+ }
253
+ }
254
+
255
+ public removeFromCache() {
256
+ cache.removeVolumeLoadObject(this.volumeId);
257
+ }
258
+
259
+ public getScalarDataLength(): number {
260
+ const { scalarData } = this;
261
+ return this.isDynamicVolume()
262
+ ? (<PixelDataTypedArray[]>scalarData)[0].length
263
+ : (<PixelDataTypedArray>scalarData).length;
264
+ }
265
+
266
+ /**
267
+ * Returns the number of frames stored in a scalarData object. The number of
268
+ * frames is equal to the number of images for 3D volumes or the number of
269
+ * frames per time poins for 4D volumes.
270
+ * @returns number of frames per volume
271
+ */
272
+ private _getNumFrames(): number {
273
+ const { imageIds, scalarData } = this;
274
+ const scalarDataCount = this.isDynamicVolume() ? scalarData.length : 1;
275
+
276
+ return imageIds.length / scalarDataCount;
277
+ }
278
+
279
+ private _getScalarDataLength(): number {
280
+ const { scalarData } = this;
281
+ return this.isDynamicVolume()
282
+ ? (<PixelDataTypedArray[]>scalarData)[0].length
283
+ : (<PixelDataTypedArray>scalarData).length;
284
+ }
285
+
286
+ /**
287
+ * Creates the metadata required for converting the volume to an cornerstoneImage
288
+ */
289
+ private _createCornerstoneImageMetaData() {
290
+ const { numFrames } = this;
291
+
292
+ if (numFrames === 0) {
293
+ return;
294
+ }
295
+
296
+ const bytesPerImage = this.sizeInBytes / numFrames;
297
+ const scalarDataLength = this._getScalarDataLength();
298
+ const numComponents = scalarDataLength / this.numVoxels;
299
+ const pixelsPerImage =
300
+ this.dimensions[0] * this.dimensions[1] * numComponents;
301
+
302
+ const { PhotometricInterpretation, voiLut, VOILUTFunction } = this.metadata;
303
+
304
+ let windowCenter = [];
305
+ let windowWidth = [];
306
+
307
+ if (voiLut && voiLut.length) {
308
+ windowCenter = voiLut.map((voi) => {
309
+ return voi.windowCenter;
310
+ });
311
+
312
+ windowWidth = voiLut.map((voi) => {
313
+ return voi.windowWidth;
314
+ });
315
+ }
316
+
317
+ const color = numComponents > 1 ? true : false; //todo: fix this
318
+
319
+ this.cornerstoneImageMetaData = {
320
+ bytesPerImage,
321
+ numComponents,
322
+ pixelsPerImage,
323
+ windowCenter,
324
+ windowWidth,
325
+ color,
326
+ // we use rgb (3 components) for the color volumes (and not rgba), and not rgba (which is used
327
+ // in some parts of the lib for stack viewing in CPU)
328
+ rgba: false,
329
+ spacing: this.spacing,
330
+ dimensions: this.dimensions,
331
+ photometricInterpretation: PhotometricInterpretation,
332
+ voiLUTFunction: VOILUTFunction,
333
+ invert: PhotometricInterpretation === 'MONOCHROME1',
334
+ };
335
+ }
336
+
337
+ protected getScalarDataByImageIdIndex(
338
+ imageIdIndex: number
339
+ ): PixelDataTypedArray {
340
+ if (imageIdIndex < 0 || imageIdIndex >= this.imageIds.length) {
341
+ throw new Error('imageIdIndex out of range');
342
+ }
343
+
344
+ const scalarDataArrays = this.getScalarDataArrays();
345
+ const scalarDataIndex = Math.floor(imageIdIndex / this.numFrames);
346
+
347
+ return scalarDataArrays[scalarDataIndex];
348
+ }
349
+
350
+ /**
351
+ * Converts the requested imageId inside the volume to a cornerstoneImage
352
+ * object. It uses the typedArray set method to copy the pixelData from the
353
+ * correct offset in the scalarData to a new array for the image
354
+ *
355
+ * @param imageId - the imageId of the image to be converted
356
+ * @param imageIdIndex - the index of the imageId in the imageIds array
357
+ * @returns image object containing the pixel data, metadata, and other information
358
+ */
359
+ public getCornerstoneImage(imageId: string, imageIdIndex: number): IImage {
360
+ const { imageIds } = this;
361
+ const frameIndex = this.imageIdIndexToFrameIndex(imageIdIndex);
362
+
363
+ const {
364
+ bytesPerImage,
365
+ pixelsPerImage,
366
+ windowCenter,
367
+ windowWidth,
368
+ numComponents,
369
+ color,
370
+ dimensions,
371
+ spacing,
372
+ invert,
373
+ voiLUTFunction,
374
+ photometricInterpretation,
375
+ } = this.cornerstoneImageMetaData;
376
+
377
+ // 1. Grab the buffer and it's type
378
+ const scalarData = this.getScalarDataByImageIdIndex(imageIdIndex);
379
+ const volumeBuffer = scalarData.buffer;
380
+ // (not sure if this actually works, TypeScript keeps complaining)
381
+ const TypedArray = scalarData.constructor;
382
+
383
+ // 2. Given the index of the image and frame length in bytes,
384
+ // create a view on the volume arraybuffer
385
+ const bytePerPixel = bytesPerImage / pixelsPerImage;
386
+
387
+ let byteOffset = bytesPerImage * frameIndex;
388
+
389
+ // If there is a discrepancy between the volume typed array
390
+ // and the bitsAllocated for the image. The reason is that VTK uses Float32
391
+ // on the GPU and if the type is not Float32, it will convert it. So for not
392
+ // having a performance issue, we convert all types initially to Float32 even
393
+ // if they are not Float32.
394
+ if (scalarData.BYTES_PER_ELEMENT !== bytePerPixel) {
395
+ byteOffset *= scalarData.BYTES_PER_ELEMENT / bytePerPixel;
396
+ }
397
+
398
+ // 3. Create a new TypedArray of the same type for the new
399
+ // Image that will be created
400
+ // @ts-ignore
401
+ const imageScalarData = new TypedArray(pixelsPerImage);
402
+ // @ts-ignore
403
+ const volumeBufferView = new TypedArray(
404
+ volumeBuffer,
405
+ byteOffset,
406
+ pixelsPerImage
407
+ );
408
+
409
+ // 4. Use e.g. TypedArray.set() to copy the data from the larger
410
+ // buffer's view into the smaller one
411
+ imageScalarData.set(volumeBufferView);
412
+
413
+ // 5. Create an Image Object from imageScalarData and put it into the Image cache
414
+ const volumeImageId = imageIds[imageIdIndex];
415
+ const modalityLutModule =
416
+ metaData.get('modalityLutModule', volumeImageId) || {};
417
+ const minMax = getMinMax(imageScalarData);
418
+ const intercept = modalityLutModule.rescaleIntercept
419
+ ? modalityLutModule.rescaleIntercept
420
+ : 0;
421
+
422
+ return {
423
+ imageId,
424
+ intercept,
425
+ windowCenter,
426
+ windowWidth,
427
+ voiLUTFunction,
428
+ color,
429
+ rgba: false,
430
+ numComps: numComponents,
431
+ // Note the dimensions were defined as [Columns, Rows, Frames]
432
+ rows: dimensions[1],
433
+ columns: dimensions[0],
434
+ sizeInBytes: imageScalarData.byteLength,
435
+ getPixelData: () => imageScalarData,
436
+ minPixelValue: minMax.min,
437
+ maxPixelValue: minMax.max,
438
+ slope: modalityLutModule.rescaleSlope
439
+ ? modalityLutModule.rescaleSlope
440
+ : 1,
441
+ getCanvas: undefined, // todo: which canvas?
442
+ height: dimensions[0],
443
+ width: dimensions[1],
444
+ columnPixelSpacing: spacing[0],
445
+ rowPixelSpacing: spacing[1],
446
+ invert,
447
+ photometricInterpretation,
448
+ };
449
+ }
450
+
451
+ /**
452
+ * Converts imageIdIndex into frameIndex which will be the same
453
+ * for 3D volumes but different for 4D volumes. The indices are 0 based.
454
+ */
455
+ protected imageIdIndexToFrameIndex(imageIdIndex: number): number {
456
+ return imageIdIndex % this.numFrames;
457
+ }
458
+
459
+ /**
460
+ * Converts the requested imageId inside the volume to a cornerstoneImage
461
+ * object. It uses the typedArray set method to copy the pixelData from the
462
+ * correct offset in the scalarData to a new array for the image
463
+ * Duplicate of getCornerstoneImageLoadObject for legacy reasons
464
+ *
465
+ * @param imageId - the imageId of the image to be converted
466
+ * @param imageIdIndex - the index of the imageId in the imageIds array
467
+ * @returns imageLoadObject containing the promise that resolves
468
+ * to the cornerstone image
469
+ */
470
+ public convertToCornerstoneImage(
471
+ imageId: string,
472
+ imageIdIndex: number
473
+ ): IImageLoadObject {
474
+ return this.getCornerstoneImageLoadObject(imageId, imageIdIndex);
475
+ }
476
+
477
+ /**
478
+ * Converts the requested imageId inside the volume to a cornerstoneImage
479
+ * object. It uses the typedArray set method to copy the pixelData from the
480
+ * correct offset in the scalarData to a new array for the image
481
+ *
482
+ * @param imageId - the imageId of the image to be converted
483
+ * @param imageIdIndex - the index of the imageId in the imageIds array
484
+ * @returns imageLoadObject containing the promise that resolves
485
+ * to the cornerstone image
486
+ */
487
+ public getCornerstoneImageLoadObject(
488
+ imageId: string,
489
+ imageIdIndex: number
490
+ ): IImageLoadObject {
491
+ const image = this.getCornerstoneImage(imageId, imageIdIndex);
492
+
493
+ const imageLoadObject = {
494
+ promise: Promise.resolve(image),
495
+ };
496
+
497
+ return imageLoadObject;
498
+ }
499
+
500
+ /**
501
+ * Returns an array of all the volume's images as Cornerstone images.
502
+ * It iterates over all the imageIds and converts them to Cornerstone images.
503
+ *
504
+ * @returns An array of Cornerstone images.
505
+ */
506
+ public getCornerstoneImages(): IImage[] {
507
+ const { imageIds } = this;
508
+
509
+ return imageIds.map((imageId, imageIdIndex) => {
510
+ return this.getCornerstoneImage(imageId, imageIdIndex);
511
+ });
512
+ }
513
+
514
+ /**
515
+ * Converts all the volume images (imageIds) to cornerstoneImages and caches them.
516
+ * It iterates over all the imageIds and convert them until there is no
517
+ * enough space left inside the imageCache. Finally it will decache the Volume.
518
+ *
519
+ */
520
+ public convertToImageSlicesAndCache() {
521
+ // 1. Try to decache images in the volatile Image Cache to provide
522
+ // enough space to store another entire copy of the volume (as Images).
523
+ // If we do not have enough, we will store as many images in the cache
524
+ // as possible, and the rest of the volume will be decached.
525
+ const byteLength = this.sizeInBytes;
526
+
527
+ if (!this.imageIds?.length) {
528
+ // generate random imageIds
529
+ // check if the referenced volume has imageIds to see how many
530
+ // images we need to generate
531
+ const referencedVolumeId = this.referencedVolumeId;
532
+ const referencedVolume = cache.getVolume(referencedVolumeId);
533
+
534
+ const numSlices =
535
+ referencedVolume?.imageIds?.length || this.dimensions[2];
536
+
537
+ this.imageIds = Array.from({ length: numSlices }, (_, i) => {
538
+ return `generated:${this.volumeId}:${i}`;
539
+ });
540
+
541
+ this._reprocessImageIds();
542
+ this.numFrames = this._getNumFrames();
543
+ this._createCornerstoneImageMetaData();
544
+ }
545
+
546
+ const numImages = this.imageIds.length;
547
+ const { bytesPerImage } = this.cornerstoneImageMetaData;
548
+ let bytesRemaining = cache.decacheIfNecessaryUntilBytesAvailable(
549
+ byteLength,
550
+ this.imageIds
551
+ );
552
+
553
+ for (let imageIdIndex = 0; imageIdIndex < numImages; imageIdIndex++) {
554
+ const imageId = this.imageIds[imageIdIndex];
555
+
556
+ bytesRemaining = bytesRemaining - bytesPerImage;
557
+
558
+ // 2. Convert each imageId to a cornerstone Image object which is
559
+ // resolved inside the promise of imageLoadObject
560
+ const image = this.getCornerstoneImage(imageId, imageIdIndex);
561
+
562
+ const imageLoadObject = {
563
+ promise: Promise.resolve(image),
564
+ };
565
+
566
+ // 3. Caching the image
567
+ if (!cache.getImageLoadObject(imageId)) {
568
+ cache.putImageLoadObject(imageId, imageLoadObject).catch((err) => {
569
+ console.error(err);
570
+ });
571
+ }
572
+
573
+ // 4. If we know we won't be able to add another Image to the cache
574
+ // without breaching the limit, stop here.
575
+ if (bytesRemaining <= bytesPerImage) {
576
+ break;
577
+ }
578
+
579
+ const imageOrientationPatient = [
580
+ this.direction[0],
581
+ this.direction[1],
582
+ this.direction[2],
583
+ this.direction[3],
584
+ this.direction[4],
585
+ this.direction[5],
586
+ ];
587
+
588
+ const precision = 6;
589
+ const imagePositionPatient = [
590
+ parseFloat(
591
+ (
592
+ this.origin[0] +
593
+ imageIdIndex * this.direction[6] * this.spacing[0]
594
+ ).toFixed(precision)
595
+ ),
596
+ parseFloat(
597
+ (
598
+ this.origin[1] +
599
+ imageIdIndex * this.direction[7] * this.spacing[1]
600
+ ).toFixed(precision)
601
+ ),
602
+ parseFloat(
603
+ (
604
+ this.origin[2] +
605
+ imageIdIndex * this.direction[8] * this.spacing[2]
606
+ ).toFixed(precision)
607
+ ),
608
+ ];
609
+
610
+ const pixelData = image.getPixelData();
611
+ const bitsAllocated = pixelData.BYTES_PER_ELEMENT * 8;
612
+
613
+ const imagePixelModule = {
614
+ // bitsStored: number;
615
+ // samplesPerPixel: number;
616
+ // highBit: number;
617
+ // pixelRepresentation: string;
618
+ // modality: string;
619
+ bitsAllocated,
620
+ photometricInterpretation: image.photometricInterpretation,
621
+ windowWidth: image.windowWidth,
622
+ windowCenter: image.windowCenter,
623
+ voiLUTFunction: image.voiLUTFunction,
624
+ };
625
+
626
+ const imagePlaneModule = {
627
+ rowCosines: [this.direction[0], this.direction[1], this.direction[2]],
628
+ columnCosines: [
629
+ this.direction[3],
630
+ this.direction[4],
631
+ this.direction[5],
632
+ ],
633
+ pixelSpacing: [this.spacing[0], this.spacing[1]],
634
+ // sliceLocation?: number;
635
+ // sliceThickness?: number;
636
+ // frameOfReferenceUID: string;
637
+ imageOrientationPatient: imageOrientationPatient,
638
+ imagePositionPatient: imagePositionPatient,
639
+ columnPixelSpacing: image.columnPixelSpacing,
640
+ rowPixelSpacing: image.rowPixelSpacing,
641
+ columns: image.columns,
642
+ rows: image.rows,
643
+ };
644
+
645
+ const generalSeriesModule = {
646
+ // modality: image.modality,
647
+ // seriesInstanceUID: string;
648
+ // seriesNumber: number;
649
+ // studyInstanceUID: string;
650
+ // seriesDate: DicomDateObject;
651
+ // seriesTime: DicomTimeObject;
652
+ };
653
+
654
+ const metadata = {
655
+ imagePixelModule,
656
+ imagePlaneModule,
657
+ generalSeriesModule,
658
+ };
659
+
660
+ ['imagePixelModule', 'imagePlaneModule', 'generalSeriesModule'].forEach(
661
+ (type) => {
662
+ genericMetadataProvider.add(imageId, {
663
+ type,
664
+ metadata: metadata[type],
665
+ });
666
+ }
667
+ );
668
+ }
669
+ // 5. When as much of the Volume is processed into Images as possible
670
+ // without breaching the cache limit, remove the Volume
671
+ this.removeFromCache();
672
+
673
+ return this.imageIds;
674
+ }
161
675
  }
162
676
 
163
677
  export default ImageVolume;
@@ -12,6 +12,20 @@ class CornerstoneEventTarget implements EventTarget {
12
12
  this.listeners = {};
13
13
  }
14
14
 
15
+ public addEventListenerOnce(type, callback) {
16
+ // Create a wrapper function to encapsulate the original callback
17
+ const onceWrapper = (event) => {
18
+ // Remove the listener after its first invocation
19
+ this.removeEventListener(type, onceWrapper);
20
+
21
+ // Call the original callback
22
+ callback.call(this, event);
23
+ };
24
+
25
+ // Add the wrapper as the listener
26
+ this.addEventListener(type, onceWrapper);
27
+ }
28
+
15
29
  public addEventListener(type, callback) {
16
30
  if (!this.listeners[type]) {
17
31
  this.listeners[type] = [];
@@ -52,7 +66,11 @@ class CornerstoneEventTarget implements EventTarget {
52
66
  const stackLength = stack.length;
53
67
 
54
68
  for (let i = 0; i < stackLength; i++) {
55
- stack[i].call(this, event);
69
+ try {
70
+ stack[i].call(this, event);
71
+ } catch (error) {
72
+ console.error(`error in event listener of type: ${event.type}`, error);
73
+ }
56
74
  }
57
75
 
58
76
  return !event.defaultPrevented;
package/src/init.ts CHANGED
@@ -21,7 +21,7 @@ const defaultConfig: Cornerstone3DConfig = {
21
21
  strictZSpacingForVolumeViewport: true,
22
22
  },
23
23
  // cache
24
- // ...
24
+ enableCacheOptimization: true,
25
25
  };
26
26
 
27
27
  let config: Cornerstone3DConfig = {
@@ -35,7 +35,7 @@ let config: Cornerstone3DConfig = {
35
35
  strictZSpacingForVolumeViewport: true,
36
36
  },
37
37
  // cache
38
- // ...
38
+ enableCacheOptimization: true,
39
39
  };
40
40
 
41
41
  let webWorkerManager = null;
@@ -1,4 +1,5 @@
1
1
  import cache from '../cache/cache';
2
+ import { ImageVolume } from '../cache';
2
3
  import Events from '../enums/Events';
3
4
  import eventTarget from '../eventTarget';
4
5
  import {
@@ -120,11 +121,14 @@ function loadImageFromCacheOrVolume(
120
121
  // 2. Check if there exists a volume in the cache containing the imageId,
121
122
  // we copy the pixelData over.
122
123
  const cachedVolumeInfo = cache.getVolumeContainingImageId(imageId);
123
- if (cachedVolumeInfo && cachedVolumeInfo.volume.loadStatus.loaded) {
124
+ if (cachedVolumeInfo?.volume?.loadStatus?.loaded) {
124
125
  // 2.1 Convert the volume at the specific slice to a cornerstoneImage object.
125
126
  // this will copy the pixel data over.
126
127
  const { volume, imageIdIndex } = cachedVolumeInfo;
127
- imageLoadObject = volume.convertToCornerstoneImage(imageId, imageIdIndex);
128
+
129
+ if (volume instanceof ImageVolume) {
130
+ imageLoadObject = volume.convertToCornerstoneImage(imageId, imageIdIndex);
131
+ }
128
132
  return imageLoadObject;
129
133
  }
130
134
  // 3. If no volume found, we search inside the imageCache for the imageId