@file-viewer/renderer-dicom 3.0.0 → 3.0.2
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 +1 -1
- package/README.md +1 -1
- package/THIRD_PARTY_LICENSES.json +21 -7
- package/THIRD_PARTY_NOTICES.md +6 -5
- package/dist/dicom.js +57 -14
- package/dist/index.js +1 -1
- package/dist/inspect.d.ts +4 -2
- package/dist/inspect.js +94 -4
- package/package.json +5 -3
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.
|
|
5
|
+
"packageVersion": "3.0.2",
|
|
6
6
|
"packageManager": "pnpm",
|
|
7
7
|
"command": "pnpm --filter @file-viewer/renderer-dicom list --prod --depth Infinity --json"
|
|
8
8
|
},
|
|
@@ -236,19 +236,19 @@
|
|
|
236
236
|
},
|
|
237
237
|
{
|
|
238
238
|
"name": "@file-viewer/core",
|
|
239
|
-
"version": "3.0.
|
|
239
|
+
"version": "3.0.2",
|
|
240
240
|
"license": "Apache-2.0",
|
|
241
241
|
"direct": true,
|
|
242
242
|
"optional": false,
|
|
243
243
|
"firstParty": true,
|
|
244
244
|
"author": "Yu Wang <admin@flyfish.dev>",
|
|
245
|
-
"repository": "https://github.com/flyfish-dev/file-viewer
|
|
245
|
+
"repository": "https://github.com/flyfish-dev/file-viewer",
|
|
246
246
|
"licenseFiles": [],
|
|
247
247
|
"noticeFiles": []
|
|
248
248
|
},
|
|
249
249
|
{
|
|
250
250
|
"name": "@file-viewer/renderer-dicom",
|
|
251
|
-
"version": "3.0.
|
|
251
|
+
"version": "3.0.2",
|
|
252
252
|
"license": "Apache-2.0",
|
|
253
253
|
"direct": false,
|
|
254
254
|
"optional": false,
|
|
@@ -470,7 +470,7 @@
|
|
|
470
470
|
},
|
|
471
471
|
{
|
|
472
472
|
"name": "browserslist",
|
|
473
|
-
"version": "4.28.
|
|
473
|
+
"version": "4.28.7",
|
|
474
474
|
"license": "MIT",
|
|
475
475
|
"direct": false,
|
|
476
476
|
"optional": false,
|
|
@@ -680,7 +680,7 @@
|
|
|
680
680
|
},
|
|
681
681
|
{
|
|
682
682
|
"name": "dompurify",
|
|
683
|
-
"version": "3.4.
|
|
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,
|
|
@@ -752,7 +752,7 @@
|
|
|
752
752
|
},
|
|
753
753
|
{
|
|
754
754
|
"name": "fflate",
|
|
755
|
-
"version": "0.7.
|
|
755
|
+
"version": "0.7.5",
|
|
756
756
|
"license": "MIT",
|
|
757
757
|
"direct": false,
|
|
758
758
|
"optional": false,
|
|
@@ -1114,6 +1114,20 @@
|
|
|
1114
1114
|
],
|
|
1115
1115
|
"noticeFiles": []
|
|
1116
1116
|
},
|
|
1117
|
+
{
|
|
1118
|
+
"name": "pako",
|
|
1119
|
+
"version": "2.2.0",
|
|
1120
|
+
"license": "(MIT AND Zlib)",
|
|
1121
|
+
"direct": true,
|
|
1122
|
+
"optional": false,
|
|
1123
|
+
"firstParty": false,
|
|
1124
|
+
"author": "",
|
|
1125
|
+
"repository": "https://github.com/nodeca/pako",
|
|
1126
|
+
"licenseFiles": [
|
|
1127
|
+
"LICENSE"
|
|
1128
|
+
],
|
|
1129
|
+
"noticeFiles": []
|
|
1130
|
+
},
|
|
1117
1131
|
{
|
|
1118
1132
|
"name": "path-is-absolute",
|
|
1119
1133
|
"version": "1.0.1",
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -7,10 +7,10 @@ The DICOM renderer is not part of any standard/full package or preset. These dep
|
|
|
7
7
|
## Required attribution
|
|
8
8
|
|
|
9
9
|
- `caniuse-lite@1.0.30001810` data is by Ben Briggs and contributors, from <https://github.com/browserslist/caniuse-lite>, licensed under CC-BY-4.0. The renderer does not modify that upstream data. The complete CC-BY-4.0 text is retained as `caniuse-lite/LICENSE` in the installed dependency.
|
|
10
|
-
|
|
10
|
+
`pako@1.0.11`, `pako@2.1.0`, `pako@2.2.0` contain zlib-derived code by Jean-loup Gailly and Mark Adler under `(MIT AND Zlib)`; their installed source retains the zlib notices and license terms.
|
|
11
11
|
- `spark-md5@3.0.2` is available under `(WTFPL OR MIT)` as declared by the package. Its installed package retains the upstream license file.
|
|
12
12
|
- `argparse@2.0.1` is licensed under Python-2.0 and retains the complete Python Software Foundation license in its installed `LICENSE` file.
|
|
13
|
-
|
|
13
|
+
`dompurify@3.4.14` is dual-licensed as `(MPL-2.0 OR Apache-2.0)`. File Viewer elects Apache-2.0, and the installed `LICENSE` file retains the complete Apache-2.0 text.
|
|
14
14
|
|
|
15
15
|
### Native libraries statically linked into codec WebAssembly
|
|
16
16
|
|
|
@@ -33,6 +33,7 @@ None of the Apache-2.0 dependencies in this closure publishes a top-level `NOTIC
|
|
|
33
33
|
|
|
34
34
|
- `pako@1.0.11` — https://github.com/nodeca/pako
|
|
35
35
|
- `pako@2.1.0` — https://github.com/nodeca/pako
|
|
36
|
+
- `pako@2.2.0` — https://github.com/nodeca/pako
|
|
36
37
|
|
|
37
38
|
### (WTFPL OR MIT)
|
|
38
39
|
|
|
@@ -42,7 +43,7 @@ None of the Apache-2.0 dependencies in this closure publishes a top-level `NOTIC
|
|
|
42
43
|
|
|
43
44
|
- `baseline-browser-mapping@2.11.19` — https://github.com/web-platform-dx/baseline-browser-mapping
|
|
44
45
|
- `comlink@4.4.2` — https://github.com/GoogleChromeLabs/comlink
|
|
45
|
-
- `dompurify@3.4.
|
|
46
|
+
- `dompurify@3.4.14` — https://github.com/cure53/DOMPurify
|
|
46
47
|
|
|
47
48
|
### BSD-3-Clause
|
|
48
49
|
|
|
@@ -99,7 +100,7 @@ None of the Apache-2.0 dependencies in this closure publishes a top-level `NOTIC
|
|
|
99
100
|
- `autoprefixer@10.5.4` — https://github.com/postcss/autoprefixer
|
|
100
101
|
- `balanced-match@1.0.0` — https://github.com/juliangruber/balanced-match
|
|
101
102
|
- `brace-expansion@1.1.18` — https://github.com/juliangruber/brace-expansion
|
|
102
|
-
- `browserslist@4.28.
|
|
103
|
+
- `browserslist@4.28.7` — https://github.com/browserslist/browserslist
|
|
103
104
|
- `commander@9.2.0` — https://github.com/tj/commander.js
|
|
104
105
|
- `concat-map@0.0.1` — https://github.com/substack/node-concat-map
|
|
105
106
|
- `core-js-pure@3.50.0` — https://github.com/zloirock/core-js
|
|
@@ -108,7 +109,7 @@ None of the Apache-2.0 dependencies in this closure publishes a top-level `NOTIC
|
|
|
108
109
|
- `es-errors@1.3.0` — https://github.com/ljharb/es-errors
|
|
109
110
|
- `escalade@3.2.0` — https://github.com/lukeed/escalade
|
|
110
111
|
- `fast-deep-equal@3.1.3` — https://github.com/epoberezkin/fast-deep-equal
|
|
111
|
-
- `fflate@0.7.
|
|
112
|
+
- `fflate@0.7.5` — https://github.com/101arrowz/fflate
|
|
112
113
|
- `fraction.js@5.3.4` — https://github.com/rawify/Fraction.js
|
|
113
114
|
- `function-bind@1.1.2` — https://github.com/Raynos/function-bind
|
|
114
115
|
- `gl-matrix@3.4.3` — https://github.com/toji/gl-matrix
|
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',
|
|
178
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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([
|
|
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
|
-
|
|
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), {
|
|
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
|
-
|
|
93
|
-
|
|
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.
|
|
3
|
+
"version": "3.0.2",
|
|
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.
|
|
73
|
-
"dicom-parser": "1.8.21"
|
|
72
|
+
"@file-viewer/core": "3.0.2",
|
|
73
|
+
"dicom-parser": "1.8.21",
|
|
74
|
+
"pako": "2.2.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",
|