* 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>
111 lines
3.4 KiB
TypeScript
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);
|
|
}
|