ohif-viewer/extensions/default/src/utils/dicomWriter.ts
Bill Wallace dbb8b1526e
feat: Add support for labelmap seg images in any supported tsuid (also for compressed bitmap) (#5806)
* feat: Update to use pnpm (#6031) [simulated squash-merge]

* feat: load DICOM SEG images via imageLoader

* feat: load DICOM SEG images via imageLoader

* PR review fixes

* Test fragment of compressed multiframes

* fix: Removed dependencies incorrectly

* Update frame as a targetted change to avoid stripping remaining params

* lock

* Update test to agree with the missing representation branch

* test(contour): contour color change coverage (#6042)

* test(contour): contour interactions delete segment (#6069)

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>

* chore(version): Update package versions to 3.13.0-beta.98 [skip ci]

* feat(testing): OHIF Test Agent Skills (#5993)

* chore(version): Update package versions to 3.13.0-beta.99 [skip ci]

* test(contour): Add the ContourSegmentToggleLock.spec.ts test file to test contour locking (#6072)

* chore(version): Update package versions to 3.13.0-beta.100 [skip ci]

* chore(testing): Flock lock for playwright tests; Pin node version for OHIF; add more workers (#6099)

* update Cypress apt deps for Ubuntu Noble (drop libgconf-2-4, libasound2→libasound2t64)

* chore(version): Update package versions to 3.13.0-beta.101 [skip ci]

* Debug fixes

* perf(seg): pass explicit frame decode concurrency (16) to SEG loader

Pass an explicit concurrency value (SEG_FRAME_DECODE_CONCURRENCY = 16) into
createFromDicomSegImageId rather than relying on the adapter default, so the
SEG frame fetch/decode parallelism is set at the call site and is ready to
become configurable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(behaviours): add behaviours section + multiframe Part 10 prefetch proposal

Start a "Behaviours" docs section for documenting how the system and UI behave
end-to-end (observed behaviours, design proposals, and failure modes), with an
index README and a Docusaurus category.

First entry is the proposal for loading a multiframe SEG as a single Part 10
instance: prefetch the whole instance (gated by loadMultiframeAsPart10RaceTimeMs),
parse it with dcmjs (handling multipart/related vs raw DICOM), and register the
per-frame compressed pixels into the Cornerstone3D core image cache (the single
uniform frame registry) so the per-frame load path is served locally while the
standard decode path is unchanged. Best-effort: falls back to per-frame fetches
on any failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Pre-cache the image data using a full part10 file for performance

* Add customizations to specify type of segmentation save

* Add clearcache of the cacheData

* Bump @cornerstonejs/* pins 5.1.3 -> 5.4.10 to match libs/@cornerstonejs base

libs/@cornerstonejs (fix/use-imageLoader-for-seg) is based on the released
cs3d 5.4.10 (merge-base with origin/main is the 5.4.10 version bump), so pin
OHIF to that release. The local branch changes still reach the app via the
cs3d:link symlinks; these pins keep the lockfile/npm fallback aligned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix seg OOM

* fix: file meta garbage element, bogus NumberOfFrames, unguarded source-map-loader

- dicomWriter: drop the naturalized meta.TransferSyntaxUID assignment that
  dcmjs wrote as a garbage (0000,0000) element into every saved file's meta
  group (download / clipboard / local wadouri blob / local store); keep the
  hex 00020010 assignment, which is required for string-form _meta fallbacks.
- registerNaturalizedDatasetForLocalWadouri: keep the computed frame count
  local so single-frame IODs (SR, RTSTRUCT) no longer gain a bogus
  NumberOfFrames element in their serialized form.
- rsbuild.config: resolve source-map-loader opportunistically (it is not a
  project dependency; the rule only serves the gitignored cs3d-link
  workflow) so fresh clones no longer crash at config load.
- Regression tests for both writer fixes (mutation-checked).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* PR review comment fixes

* Update versions

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: diattamo <mmddiatta@gmail.com>
Co-authored-by: ohif-bot <danny.ri.brown+ohif-bot@gmail.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2026-07-09 20:54:23 -04:00

111 lines
3.4 KiB
TypeScript

import dcmjs from 'dcmjs';
export const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1';
export const DICOM_WRITE_OPTIONS = {
allowInvalidVRLength: false,
// `fragmentMultiframe` only governs whether a SINGLE frame is split across
// multiple fragments (dcmjs splits frames larger than its 20KB fragment size).
// It does NOT merge frames: in an encapsulated (compressed) transfer syntax
// every frame is always written as its own fragment, preceded by the Basic
// Offset Table; in an uncompressed syntax pixel data is never fragmented at
// all. Keeping this `false` therefore yields exactly one fragment per frame
// for compressed SEG — the conformant layout — without splitting large frames.
fragmentMultiframe: false,
};
/** OHIF runtime fields — not DICOM tags; must not be enumerable for dcmjs datasetToDict. */
export const RUNTIME_INSTANCE_PROPERTY_KEYS = [
'url',
'wadorsuri',
'wadouri',
'wadoRoot',
'wadoUri',
'wadoUriRoot',
'imageRendering',
'imageId',
'_parentInstance',
'frameNumber',
] as const;
/**
* Attaches OHIF runtime data on an instance without enumerable keys (safe for dcmjs datasetToDict).
*/
export function setNonEnumerableInstanceProperty(
instance: Record<string, unknown>,
key: string,
value: unknown
) {
Object.defineProperty(instance, key, {
value,
enumerable: false,
writable: true,
configurable: true,
});
}
/**
* Re-defines any existing enumerable runtime properties as non-enumerable (keeps values).
*/
export function makeExistingPropertiesNonEnumerable(instance: Record<string, unknown>) {
for (const key of RUNTIME_INSTANCE_PROPERTY_KEYS) {
if (!Object.prototype.hasOwnProperty.call(instance, key)) {
continue;
}
const descriptor = Object.getOwnPropertyDescriptor(instance, key);
if (!descriptor || descriptor.enumerable === false) {
continue;
}
setNonEnumerableInstanceProperty(instance, key, descriptor.value);
}
}
export function getDatasetTransferSyntaxUID(dataset) {
const fromMeta = dataset?._meta?.TransferSyntaxUID;
if (typeof fromMeta === 'string') {
return fromMeta;
}
if (Array.isArray(fromMeta?.Value)) {
return fromMeta.Value[0];
}
if (typeof dataset?.TransferSyntaxUID === 'string') {
return dataset.TransferSyntaxUID;
}
return EXPLICIT_VR_LITTLE_ENDIAN;
}
function applyTransferSyntaxToFileMeta(dicomDict, transferSyntaxUID) {
if (!transferSyntaxUID || !dicomDict?.meta) {
return;
}
// Hex key only: DicomMessage.write treats every meta key as a tag string, so a
// naturalized key like 'TransferSyntaxUID' would be parsed via parseInt(..., 16)
// into tag (0000,0000) and written as a garbage element in group 2.
dicomDict.meta['00020010'] = { vr: 'UI', Value: [transferSyntaxUID] };
}
export function datasetToDicomPart10Buffer(dataset) {
makeExistingPropertiesNonEnumerable(dataset);
const transferSyntaxUID = getDatasetTransferSyntaxUID(dataset);
const dicomDict = dcmjs.data.datasetToDict(dataset);
applyTransferSyntaxToFileMeta(dicomDict, transferSyntaxUID);
return dicomDict.write(DICOM_WRITE_OPTIONS);
}
export function datasetToDicomBlob(dataset) {
const part10Buffer = datasetToDicomPart10Buffer(dataset);
return new Blob([part10Buffer], { type: 'application/dicom' });
}
export function writeDicomDictToPart10Buffer(dicomDict) {
return dicomDict.write(DICOM_WRITE_OPTIONS);
}