@file-viewer/renderer-dicom 3.0.0 → 3.0.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.
package/README.en.md CHANGED
@@ -14,7 +14,7 @@ const viewer = createViewer(container, {
14
14
  })
15
15
  ```
16
16
 
17
- The first scope accepts Implicit/Explicit VR Little Endian, JPEG Lossless Process 14 SV1, JPEG-LS Lossless, and JPEG 2000 Lossless Part 10 files covered by Chromium, Firefox, and WebKit regression (single-frame or multi-frame). It provides stack navigation, window width/center, zoom, pan, left/right 90° rotation, fit-to-view, and unified File Viewer view-state restoration. Other transfer syntaxes, PACS/DICOMweb, multi-file series assembly, annotations, MPR, segmentation, hanging protocols, and diagnostic use are outside this package.
17
+ The first scope accepts Implicit/Explicit/Deflated Explicit VR Little Endian, JPEG Lossless Process 14 SV1, JPEG-LS Lossless, and JPEG 2000 Lossless Part 10 files covered by Chromium, Firefox, and WebKit regression (single-frame or multi-frame). It provides stack navigation, window width/center, zoom, pan, left/right 90° rotation, fit-to-view, and unified File Viewer view-state restoration. Other transfer syntaxes, PACS/DICOMweb, multi-file series assembly, annotations, MPR, segmentation, hanging protocols, and diagnostic use are outside this package.
18
18
 
19
19
  The package initializes codec workers only after a DICOM file is selected. Defaults are limited to a 64 MiB source, 256 frames, 16 million decoded samples per frame, and 48 million decoded samples in total; applications can configure lower limits. Destroying the viewer removes only that instance's file-manager entry, viewport, rendering engine, listeners, metadata, and image cache. Cornerstone owns the shared worker pool, so destroying a File Viewer instance never terminates a host-owned worker.
20
20
 
package/README.md CHANGED
@@ -14,7 +14,7 @@ const viewer = createViewer(container, {
14
14
  })
15
15
  ```
16
16
 
17
- 第一阶段接受经过 Chromium、Firefox、WebKit 真实浏览器回归的 Implicit/Explicit VR Little Endian、JPEG Lossless Process 14 SV1、JPEG-LS Lossless 和 JPEG 2000 Lossless Part 10 文件(单帧或多帧),支持帧切换、窗宽/窗位、缩放、平移、左右 90° 旋转、适合视图及 File Viewer 统一 view-state 恢复。其他传输语法、PACS/DICOMweb、多文件序列组装、标注、MPR、分割和 hanging protocol 不在本包范围内。
17
+ 第一阶段接受经过 Chromium、Firefox、WebKit 真实浏览器回归的 Implicit/Explicit/Deflated Explicit VR Little Endian、JPEG Lossless Process 14 SV1、JPEG-LS Lossless 和 JPEG 2000 Lossless Part 10 文件(单帧或多帧),支持帧切换、窗宽/窗位、缩放、平移、左右 90° 旋转、适合视图及 File Viewer 统一 view-state 恢复。其他传输语法、PACS/DICOMweb、多文件序列组装、标注、MPR、分割和 hanging protocol 不在本包范围内。
18
18
 
19
19
  只有选中 DICOM 文件后才初始化解码 worker。默认限制为源文件 64 MiB、256 帧、单帧 1600 万解码采样点、累计 4800 万解码采样点,均可通过 renderer options 向下调整。销毁预览时会移除本实例的 file-manager 条目、viewport、rendering engine、事件监听、metadata 与图像缓存;Cornerstone 的共享 worker 池由宿主管理,不会因销毁某个 File Viewer 实例而被终止。
20
20
 
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "generatedFrom": {
4
4
  "packageName": "@file-viewer/renderer-dicom",
5
- "packageVersion": "3.0.0",
5
+ "packageVersion": "3.0.1",
6
6
  "packageManager": "pnpm",
7
7
  "command": "pnpm --filter @file-viewer/renderer-dicom list --prod --depth Infinity --json"
8
8
  },
@@ -236,7 +236,7 @@
236
236
  },
237
237
  {
238
238
  "name": "@file-viewer/core",
239
- "version": "3.0.0",
239
+ "version": "3.0.1",
240
240
  "license": "Apache-2.0",
241
241
  "direct": true,
242
242
  "optional": false,
@@ -248,7 +248,7 @@
248
248
  },
249
249
  {
250
250
  "name": "@file-viewer/renderer-dicom",
251
- "version": "3.0.0",
251
+ "version": "3.0.1",
252
252
  "license": "Apache-2.0",
253
253
  "direct": false,
254
254
  "optional": false,
@@ -680,7 +680,7 @@
680
680
  },
681
681
  {
682
682
  "name": "dompurify",
683
- "version": "3.4.13",
683
+ "version": "3.4.14",
684
684
  "license": "Apache-2.0",
685
685
  "declaredLicense": "(MPL-2.0 OR Apache-2.0)",
686
686
  "direct": false,
@@ -1104,7 +1104,7 @@
1104
1104
  "name": "pako",
1105
1105
  "version": "2.1.0",
1106
1106
  "license": "(MIT AND Zlib)",
1107
- "direct": false,
1107
+ "direct": true,
1108
1108
  "optional": false,
1109
1109
  "firstParty": false,
1110
1110
  "author": "",
@@ -42,7 +42,7 @@ None of the Apache-2.0 dependencies in this closure publishes a top-level `NOTIC
42
42
 
43
43
  - `baseline-browser-mapping@2.11.19` — https://github.com/web-platform-dx/baseline-browser-mapping
44
44
  - `comlink@4.4.2` — https://github.com/GoogleChromeLabs/comlink
45
- - `dompurify@3.4.13` — https://github.com/cure53/DOMPurify
45
+ - `dompurify@3.4.14` — https://github.com/cure53/DOMPurify
46
46
 
47
47
  ### BSD-3-Clause
48
48
 
package/dist/dicom.js CHANGED
@@ -172,11 +172,16 @@ export default async function renderDicom(buffer, target, _type, context, option
172
172
  const documentRef = target.ownerDocument;
173
173
  const copy = resolveCopy(context);
174
174
  const root = createElement(documentRef, 'div', 'dicom-viewer');
175
+ const hasDicomWindow = inspected.windowWidth !== null && inspected.windowCenter !== null;
175
176
  Object.assign(root.dataset, {
176
177
  status: 'loading', renderer: 'dicom', rows: String(inspected.rows), columns: String(inspected.columns),
177
- frameCount: String(inspected.frameCount), currentFrame: '1', windowWidth: String(inspected.windowWidth),
178
- windowCenter: String(inspected.windowCenter), panX: '0', panY: '0', rotation: '0',
178
+ frameCount: String(inspected.frameCount), currentFrame: '1', windowSource: hasDicomWindow ? 'dicom' : 'auto',
179
+ panX: '0', panY: '0', rotation: '0',
179
180
  });
181
+ if (hasDicomWindow) {
182
+ root.dataset.windowWidth = String(inspected.windowWidth);
183
+ root.dataset.windowCenter = String(inspected.windowCenter);
184
+ }
180
185
  const toolbar = createElement(documentRef, 'div', 'dicom-toolbar');
181
186
  const navigation = createElement(documentRef, 'div', 'dicom-tool-group');
182
187
  const previousButton = createButton(documentRef, copy.previous, '‹');
@@ -199,7 +204,7 @@ export default async function renderDicom(buffer, target, _type, context, option
199
204
  widthInput.type = 'number';
200
205
  widthInput.min = '1';
201
206
  widthInput.step = '1';
202
- widthInput.value = String(Math.round(inspected.windowWidth));
207
+ widthInput.value = inspected.windowWidth === null ? '' : String(Math.round(inspected.windowWidth));
203
208
  widthInput.setAttribute('aria-label', copy.windowWidth);
204
209
  widthLabel.append(widthInput);
205
210
  const centerLabel = createElement(documentRef, 'label', undefined, 'WC');
@@ -207,7 +212,7 @@ export default async function renderDicom(buffer, target, _type, context, option
207
212
  const centerInput = createElement(documentRef, 'input');
208
213
  centerInput.type = 'number';
209
214
  centerInput.step = '1';
210
- centerInput.value = String(Math.round(inspected.windowCenter));
215
+ centerInput.value = inspected.windowCenter === null ? '' : String(Math.round(inspected.windowCenter));
211
216
  centerInput.setAttribute('aria-label', copy.windowCenter);
212
217
  centerLabel.append(centerInput);
213
218
  windowControls.append(widthLabel, centerLabel);
@@ -232,6 +237,7 @@ export default async function renderDicom(buffer, target, _type, context, option
232
237
  let frameQueue = Promise.resolve();
233
238
  let currentWindowWidth = inspected.windowWidth;
234
239
  let currentWindowCenter = inspected.windowCenter;
240
+ let currentWindowSource = hasDicomWindow ? 'dicom' : 'auto';
235
241
  let currentRotation = 0;
236
242
  let resizeObserver = null;
237
243
  let pointerId = null;
@@ -301,6 +307,36 @@ export default async function renderDicom(buffer, target, _type, context, option
301
307
  previousButton.disabled = currentFrame <= 0;
302
308
  nextButton.disabled = currentFrame >= inspected.frameCount - 1;
303
309
  };
310
+ const syncWindowUi = (width, center, source) => {
311
+ currentWindowWidth = width;
312
+ currentWindowCenter = center;
313
+ currentWindowSource = source;
314
+ widthInput.value = String(Math.round(width * 1000) / 1000);
315
+ centerInput.value = String(Math.round(center * 1000) / 1000);
316
+ root.dataset.windowWidth = String(width);
317
+ root.dataset.windowCenter = String(center);
318
+ root.dataset.windowSource = source;
319
+ };
320
+ const syncWindowFromViewport = () => {
321
+ var _a, _b;
322
+ if (!viewport)
323
+ return false;
324
+ let lower = finiteNumber((_a = viewport.getProperties().voiRange) === null || _a === void 0 ? void 0 : _a.lower, Number.NaN);
325
+ let upper = finiteNumber((_b = viewport.getProperties().voiRange) === null || _b === void 0 ? void 0 : _b.upper, Number.NaN);
326
+ if (!Number.isFinite(lower) || !Number.isFinite(upper) || upper <= lower) {
327
+ const image = viewport.getCornerstoneImage();
328
+ const slope = finiteNumber(image.slope, 1);
329
+ const intercept = finiteNumber(image.intercept, 0);
330
+ const first = finiteNumber(image.minPixelValue, Number.NaN) * slope + intercept;
331
+ const second = finiteNumber(image.maxPixelValue, Number.NaN) * slope + intercept;
332
+ lower = Math.min(first, second);
333
+ upper = Math.max(first, second);
334
+ }
335
+ if (!Number.isFinite(lower) || !Number.isFinite(upper) || upper <= lower)
336
+ return false;
337
+ syncWindowUi(upper - lower, (upper + lower) / 2, 'auto');
338
+ return true;
339
+ };
304
340
  const setFrame = (frame, source = 'user') => {
305
341
  const requested = clamp(Math.round(frame), 0, inspected.frameCount - 1);
306
342
  frameQueue = frameQueue.then(async () => {
@@ -310,6 +346,8 @@ export default async function renderDicom(buffer, target, _type, context, option
310
346
  if (destroyed)
311
347
  return;
312
348
  currentFrame = requested;
349
+ if (currentWindowSource === 'auto')
350
+ syncWindowFromViewport();
313
351
  syncFrameUi();
314
352
  viewport.render();
315
353
  emitViewState('page-change', source);
@@ -319,14 +357,11 @@ export default async function renderDicom(buffer, target, _type, context, option
319
357
  const applyWindow = () => {
320
358
  if (!viewport)
321
359
  return;
322
- const width = Math.max(1, finiteNumber(widthInput.value, currentWindowWidth));
323
- const center = finiteNumber(centerInput.value, currentWindowCenter);
324
- currentWindowWidth = width;
325
- currentWindowCenter = center;
326
- widthInput.value = String(Math.round(width * 1000) / 1000);
327
- centerInput.value = String(Math.round(center * 1000) / 1000);
328
- root.dataset.windowWidth = String(width);
329
- root.dataset.windowCenter = String(center);
360
+ const width = Math.max(1, finiteNumber(widthInput.value, currentWindowWidth !== null && currentWindowWidth !== void 0 ? currentWindowWidth : Number.NaN));
361
+ const center = finiteNumber(centerInput.value, currentWindowCenter !== null && currentWindowCenter !== void 0 ? currentWindowCenter : Number.NaN);
362
+ if (!Number.isFinite(width) || !Number.isFinite(center))
363
+ return;
364
+ syncWindowUi(width, center, 'user');
330
365
  viewport.setProperties({ voiRange: { lower: center - width / 2, upper: center + width / 2 } });
331
366
  viewport.render();
332
367
  emitViewState('window-change', 'user');
@@ -357,7 +392,7 @@ export default async function renderDicom(buffer, target, _type, context, option
357
392
  try {
358
393
  ensureDicomRuntime(options === null || options === void 0 ? void 0 : options.maxWebWorkers);
359
394
  const fileName = ((_b = context === null || context === void 0 ? void 0 : context.filename) === null || _b === void 0 ? void 0 : _b.trim()) || 'preview.dcm';
360
- baseImageId = wadouri.fileManager.add(new File([buffer], fileName, { type: 'application/dicom' }));
395
+ baseImageId = wadouri.fileManager.add(new File([inspected.loaderBuffer], fileName, { type: 'application/dicom' }));
361
396
  imageIds = makeImageIds(baseImageId, inspected.frameCount);
362
397
  root.dataset.dicomImageId = baseImageId;
363
398
  const renderingEngineId = `file-viewer-dicom-engine-${Date.now()}-${++renderingEngineSerial}`;
@@ -371,7 +406,15 @@ export default async function renderDicom(buffer, target, _type, context, option
371
406
  await viewport.setStack(imageIds, 0);
372
407
  if (destroyed || ((_c = context === null || context === void 0 ? void 0 : context.signal) === null || _c === void 0 ? void 0 : _c.aborted))
373
408
  throw ((_d = context === null || context === void 0 ? void 0 : context.signal) === null || _d === void 0 ? void 0 : _d.reason) || new DOMException('DICOM render aborted.', 'AbortError');
374
- viewport.setProperties({ voiRange: { lower: currentWindowCenter - currentWindowWidth / 2, upper: currentWindowCenter + currentWindowWidth / 2 } });
409
+ if (hasDicomWindow) {
410
+ const width = inspected.windowWidth;
411
+ const center = inspected.windowCenter;
412
+ syncWindowUi(width, center, 'dicom');
413
+ viewport.setProperties({ voiRange: { lower: center - width / 2, upper: center + width / 2 } });
414
+ }
415
+ else if (!syncWindowFromViewport()) {
416
+ throw new Error('Unable to determine a visible DICOM window from the decoded pixels.');
417
+ }
375
418
  viewport.resetCamera({ resetPan: true, resetZoom: true, resetToCenter: true });
376
419
  viewport.render();
377
420
  registerFileViewerZoomProvider(root, {
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ export const dicomRendererDefinition = {
9
9
  packageName: '@file-viewer/renderer-dicom',
10
10
  presets: [],
11
11
  knownLimits: [
12
- 'This MVP is tested with Implicit/Explicit VR Little Endian, JPEG Lossless Process 14 SV1, JPEG-LS Lossless, and JPEG 2000 Lossless Part 10 files; other transfer syntaxes and DICOMweb are not included.',
12
+ 'This MVP is tested with Implicit/Explicit/Deflated Explicit VR Little Endian, JPEG Lossless Process 14 SV1, JPEG-LS Lossless, and JPEG 2000 Lossless Part 10 files; other transfer syntaxes and DICOMweb are not included.',
13
13
  'The MVP renders one single-frame or multi-frame local file as a stack; series assembly, PACS/DICOMweb, MPR, and diagnostic use are not included.',
14
14
  ],
15
15
  capabilities: {
package/dist/inspect.d.ts CHANGED
@@ -11,7 +11,9 @@ export interface InspectedDicomPart10 {
11
11
  rows: number;
12
12
  samplesPerPixel: number;
13
13
  transferSyntax: string;
14
- windowCenter: number;
15
- windowWidth: number;
14
+ windowCenter: number | null;
15
+ windowWidth: number | null;
16
+ /** @internal Part 10 bytes normalized for the image loader when required. */
17
+ loaderBuffer: ArrayBuffer;
16
18
  }
17
19
  export declare const inspectDicomPart10: (buffer: ArrayBuffer, limitsInput?: FileViewerDicomLimits) => InspectedDicomPart10;
package/dist/inspect.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as dicomParser from 'dicom-parser';
2
+ import { Inflate } from 'pako';
2
3
  const MEBIBYTE = 1024 * 1024;
3
4
  export const DEFAULT_DICOM_LIMITS = Object.freeze({
4
5
  maxSourceBytes: 64 * MEBIBYTE,
@@ -10,6 +11,7 @@ export const DEFAULT_DICOM_LIMITS = Object.freeze({
10
11
  export const SUPPORTED_DICOM_TRANSFER_SYNTAXES = new Set([
11
12
  '1.2.840.10008.1.2',
12
13
  '1.2.840.10008.1.2.1',
14
+ '1.2.840.10008.1.2.1.99',
13
15
  '1.2.840.10008.1.2.4.70',
14
16
  '1.2.840.10008.1.2.4.80',
15
17
  '1.2.840.10008.1.2.4.90',
@@ -40,6 +42,75 @@ const hasPart10Signature = (buffer) => {
40
42
  const bytes = new Uint8Array(buffer, 128, 4);
41
43
  return bytes[0] === 0x44 && bytes[1] === 0x49 && bytes[2] === 0x43 && bytes[3] === 0x4d;
42
44
  };
45
+ class DicomInflatedSizeError extends Error {
46
+ }
47
+ const inflateDeflatedDataSet = (byteArray, position, maxInflatedBytes) => {
48
+ const chunks = [];
49
+ let inflatedBytes = 0;
50
+ const inflater = new Inflate({ raw: true, chunkSize: 64 * 1024 });
51
+ inflater.onData = chunk => {
52
+ const bytes = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
53
+ inflatedBytes += bytes.byteLength;
54
+ if (position + inflatedBytes > maxInflatedBytes) {
55
+ throw new DicomInflatedSizeError(`DICOM inflated size exceeds the configured ${maxInflatedBytes}-byte limit.`);
56
+ }
57
+ chunks.push(bytes.slice());
58
+ };
59
+ inflater.push(byteArray.subarray(position), true);
60
+ if (inflater.err)
61
+ throw new Error(inflater.msg || 'Unable to inflate the DICOM data set.');
62
+ const fullByteArray = new Uint8Array(position + inflatedBytes);
63
+ fullByteArray.set(byteArray.subarray(0, position), 0);
64
+ let outputOffset = position;
65
+ for (const chunk of chunks) {
66
+ fullByteArray.set(chunk, outputOffset);
67
+ outputOffset += chunk.byteLength;
68
+ }
69
+ return fullByteArray;
70
+ };
71
+ const normalizeDeflatedTransferSyntax = (dataSet) => {
72
+ const transferSyntaxElement = dataSet.elements.x00020010;
73
+ const groupLengthElement = dataSet.elements.x00020000;
74
+ if (!transferSyntaxElement || !groupLengthElement) {
75
+ throw new Error('The DICOM file meta information is incomplete.');
76
+ }
77
+ const explicitLittleEndianUid = new TextEncoder().encode('1.2.840.10008.1.2.1\0');
78
+ const removedBytes = transferSyntaxElement.length - explicitLittleEndianUid.byteLength;
79
+ if (removedBytes < 0)
80
+ throw new Error('Unable to normalize the DICOM transfer syntax.');
81
+ const source = dataSet.byteArray;
82
+ const output = new Uint8Array(source.byteLength - removedBytes);
83
+ const valueOffset = transferSyntaxElement.dataOffset;
84
+ output.set(source.subarray(0, valueOffset), 0);
85
+ new DataView(output.buffer).setUint16(valueOffset - 2, explicitLittleEndianUid.byteLength, true);
86
+ output.set(explicitLittleEndianUid, valueOffset);
87
+ output.set(source.subarray(valueOffset + transferSyntaxElement.length), valueOffset + explicitLittleEndianUid.byteLength);
88
+ const groupLengthOffset = groupLengthElement.dataOffset;
89
+ const sourceView = new DataView(source.buffer, source.byteOffset, source.byteLength);
90
+ const outputView = new DataView(output.buffer);
91
+ outputView.setUint32(groupLengthOffset, sourceView.getUint32(groupLengthOffset, true) - removedBytes, true);
92
+ return { buffer: output.buffer, removedBytes };
93
+ };
94
+ const normalizeLoaderBuffer = (dataSet, transferSyntax, sourceBuffer) => {
95
+ const specificCharacterSet = String(dataSet.string('x00080005') || '').trim().toUpperCase();
96
+ const normalizesLegacyUtf8 = specificCharacterSet === 'ISO_IR 196';
97
+ const normalizesTransferSyntax = transferSyntax === '1.2.840.10008.1.2.1.99';
98
+ if (!normalizesLegacyUtf8 && !normalizesTransferSyntax)
99
+ return sourceBuffer;
100
+ const normalizedTransferSyntax = normalizesTransferSyntax
101
+ ? normalizeDeflatedTransferSyntax(dataSet)
102
+ : { buffer: sourceBuffer.slice(0), removedBytes: 0 };
103
+ if (!normalizesLegacyUtf8)
104
+ return normalizedTransferSyntax.buffer;
105
+ const characterSetElement = dataSet.elements.x00080005;
106
+ if (!characterSetElement)
107
+ return normalizedTransferSyntax.buffer;
108
+ const output = new Uint8Array(normalizedTransferSyntax.buffer);
109
+ const replacement = new Uint8Array(characterSetElement.length).fill(0x20);
110
+ replacement.set(new TextEncoder().encode('ISO_IR 192'));
111
+ output.set(replacement, characterSetElement.dataOffset - normalizedTransferSyntax.removedBytes);
112
+ return output.buffer;
113
+ };
43
114
  export const inspectDicomPart10 = (buffer, limitsInput) => {
44
115
  const limits = resolveLimits(limitsInput);
45
116
  if (buffer.byteLength < 132 || buffer.byteLength > limits.maxSourceBytes) {
@@ -49,10 +120,19 @@ export const inspectDicomPart10 = (buffer, limitsInput) => {
49
120
  throw new Error('The selected file is not a DICOM Part 10 file (missing DICM signature).');
50
121
  }
51
122
  let dataSet;
123
+ const maxInflatedBytes = limits.maxSourceBytes + limits.maxTotalPixels * 4;
52
124
  try {
53
- dataSet = dicomParser.parseDicom(new Uint8Array(buffer), { untilTag: 'x7fe00010' });
125
+ dataSet = dicomParser.parseDicom(new Uint8Array(buffer), {
126
+ untilTag: 'x7fe00010',
127
+ // dicom-parser's Node branch assumes Buffer.copy() for deflated data.
128
+ // A deterministic Uint8Array inflater keeps direct inspection identical
129
+ // in Node, browsers, Workers, and host bundlers that polyfill `process`.
130
+ inflater: (byteArray, position) => inflateDeflatedDataSet(byteArray, position, maxInflatedBytes),
131
+ });
54
132
  }
55
- catch {
133
+ catch (error) {
134
+ if (error instanceof DicomInflatedSizeError)
135
+ throw error;
56
136
  throw new Error('Unable to parse the DICOM Part 10 header.');
57
137
  }
58
138
  const transferSyntax = String(dataSet.string('x00020010') || '').trim();
@@ -80,6 +160,9 @@ export const inspectDicomPart10 = (buffer, limitsInput) => {
80
160
  if (![1, 8, 16, 32].includes(bitsAllocated)) {
81
161
  throw new Error(`DICOM Bits Allocated value ${bitsAllocated} is not supported.`);
82
162
  }
163
+ const windowCenter = finiteNumber(dataSet.floatString('x00281050', 0), Number.NaN);
164
+ const windowWidth = finiteNumber(dataSet.floatString('x00281051', 0), Number.NaN);
165
+ const hasValidWindow = Number.isFinite(windowCenter) && Number.isFinite(windowWidth) && windowWidth > 0;
83
166
  return {
84
167
  bitsAllocated,
85
168
  columns,
@@ -89,7 +172,14 @@ export const inspectDicomPart10 = (buffer, limitsInput) => {
89
172
  rows,
90
173
  samplesPerPixel,
91
174
  transferSyntax,
92
- windowCenter: finiteNumber(dataSet.floatString('x00281050', 0), (2 ** bitsAllocated) / 2),
93
- windowWidth: Math.max(1, finiteNumber(dataSet.floatString('x00281051', 0), 2 ** bitsAllocated)),
175
+ // Window Center/Width are optional DICOM attributes. Keep their absence
176
+ // explicit so the renderer can use Cornerstone's pixel/rescale-aware VOI
177
+ // instead of inventing a raw unsigned range (which makes signed CT black).
178
+ windowCenter: hasValidWindow ? windowCenter : null,
179
+ windowWidth: hasValidWindow ? windowWidth : null,
180
+ // ISO_IR 196 was the original UTF-8 defined term from DICOM CP-107;
181
+ // normalize the loader-only copy to the current ISO_IR 192 spelling so
182
+ // dcmjs does not reject otherwise valid pixel data and UTF-8 metadata.
183
+ loaderBuffer: normalizeLoaderBuffer(dataSet, transferSyntax, buffer),
94
184
  };
95
185
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@file-viewer/renderer-dicom",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -69,10 +69,12 @@
69
69
  "@cornerstonejs/core": "5.8.2",
70
70
  "@cornerstonejs/dicom-image-loader": "5.8.2",
71
71
  "@cornerstonejs/metadata": "5.8.2",
72
- "@file-viewer/core": "3.0.0",
73
- "dicom-parser": "1.8.21"
72
+ "@file-viewer/core": "3.0.1",
73
+ "dicom-parser": "1.8.21",
74
+ "pako": "2.1.0"
74
75
  },
75
76
  "devDependencies": {
77
+ "@types/pako": "^2.0.4",
76
78
  "typescript": "^6.0.3"
77
79
  },
78
80
  "license": "Apache-2.0",