Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3cfbdd9
test: gate libjpeg-turbo-8bit suite on built dist so build-less runs …
sedghi Jul 7, 2026
fff5057
fix(dicom-codec): free wasm decoder/encoder instances on error path
sedghi Jul 7, 2026
1b72d13
fix(openjpeg): bounds-check BufferStream skip/write/seek callbacks
sedghi Jul 7, 2026
c293e39
fix(openjpeg): reject unsupported component counts, short buffers, an…
sedghi Jul 7, 2026
eab8b32
fix(codecs): overflow-check decoded-buffer sizing on wasm32 (openjph,…
sedghi Jul 7, 2026
0f609bd
fix(libjpeg-turbo-12bit): correct decode buffer sizing, wire dispatch…
sedghi Jul 7, 2026
ace5cf9
test(libjpeg-turbo-12bit): accept graceful recovery on truncated input
sedghi Jul 7, 2026
ffd155e
ci: pin codspeed bench runner and action version for stable baseline …
sedghi Jul 7, 2026
6943d76
fix(libjpeg-turbo-12bit): return 12-bit samples as Uint16Array, not c…
sedghi Jul 7, 2026
3179998
fix(openjpeg): free codec and stream handles on component-count rejec…
sedghi Jul 7, 2026
c9dc902
test: verify exact pixel output for every codec + fail CI on silently…
sedghi Jul 7, 2026
6b04753
tools: from-scratch decoders that verify all RAW pixel references
sedghi Jul 7, 2026
110fa1f
fix: fail closed on multi-component 12-bit JPEGs and failed J2K encodes
sedghi Jul 7, 2026
3280dcc
ci: single run per PR commit + CPU logging to tame CodSpeed env warnings
sedghi Jul 7, 2026
30d160a
ci: add walltime instrument on CodSpeed macro runners alongside simul…
sedghi Jul 7, 2026
80fbe1d
ci: provision node 22 in the emsdk build container
sedghi Jul 7, 2026
d93b80f
ci: serialize simulation benches; gate walltime job behind repo variable
sedghi Jul 7, 2026
c20d46f
ci: dist-size regression gate against committed baseline
sedghi Jul 7, 2026
d03c65c
ci: suppress vitest RPC-timeout exit noise in simulation benches
sedghi Jul 7, 2026
35e8074
ci: move simulation RPC-noise suppression into vitest configs
sedghi Jul 7, 2026
1b00da1
ci: match legacy 'instrumentation' runner-mode string for RPC-noise s…
sedghi Jul 8, 2026
c267e8c
test: color and bit-depth decode matrix across all codecs (plans 034/…
sedghi Jul 8, 2026
67a8e5b
ci: toolchain-only changes trigger the full pipeline (plan 033)
sedghi Jul 8, 2026
2e7b8d7
test: exercise the untested wasm and dispatcher API surface (plan 036)
sedghi Jul 8, 2026
e8d29cb
test: encoder quality pinning + bench coverage gaps (plan 037, absorb…
sedghi Jul 8, 2026
8794920
test: wasm heap-stability assertions per codec (plan 039)
sedghi Jul 8, 2026
d9ba7b9
test: browser smoke-decode for every wasm build variant (plan 038)
sedghi Jul 8, 2026
6769ccc
test: silence wasm stdout/stderr in decoder benches to improve measur…
sedghi Jul 8, 2026
9b968ea
ci(dist-size): print every tracked artifact, marking unchanged files …
sedghi Jul 8, 2026
6c46189
bench: batch microsecond-scale bench bodies to the millisecond range
sedghi Jul 8, 2026
538e335
fix: decode 32-bit pixel data as int per pixelRepresentation, float o…
sedghi Jul 8, 2026
ff46061
chore: remove accidentally committed profiling artifacts
sedghi Jul 8, 2026
472089f
test: keep this PR to tests that pass against unmodified sources
sedghi Jul 8, 2026
a53a394
test: measure the tests-only split against this PR's actual base (fix…
sedghi Jul 8, 2026
a8fbc9d
test: classify against main — all fix PRs consolidate into one follow-up
sedghi Jul 8, 2026
a325750
fix: consolidated codec correctness fixes (supersedes #71)
sedghi Jul 8, 2026
a778802
Merge remote-tracking branch 'origin/main' into codec-correctness-fixes
sedghi Jul 9, 2026
0e28946
Merge remote-tracking branch 'origin/main' into codec-correctness-fixes
sedghi Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 53 additions & 8 deletions packages/big-endian/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,29 @@
function swap16(val) {
return ((val & 0xff) << 8) | ((val >> 8) & 0xff);
}


function swap32(val) {
return (
((val & 0xff) << 24) |
((val & 0xff00) << 8) |
((val >> 8) & 0xff00) |
((val >> 24) & 0xff)
);
}

/**
* Decodes the provided pixelData and sets the `pixelData` property
* of the imageFrame object to the decoded representation.
*
* Set pixelData will be `Uint16Array` if `pixelRepresentation` is 0,
* otherwise it will be an `Int16Array`
*
*
* 16-bit and 32-bit data are byte-swapped and become unsigned
* (`pixelRepresentation` 0) or signed (`pixelRepresentation` 1) integer
* arrays. 32-bit data with no `pixelRepresentation` is treated as float
* (e.g. FloatPixelData), mirroring the little-endian package.
*
* @param {object} imageFrame
* @param {number} imageFrame.bitsAllocated - 16 or 8
* @param {number} imageFrame.bitsAllocated - 32, 16, 8 or 1
* @param {number} imageFrame.pixelRepresentation - 0 or 1
* @param {*} pixelData
* @param {*} pixelData
*/
function decode(imageFrame, pixelData) {
if (imageFrame.bitsAllocated === 16) {
Expand All @@ -38,8 +49,42 @@ function decode(imageFrame, pixelData) {
for (let i = 0; i < imageFrame.pixelData.length; i++) {
imageFrame.pixelData[i] = swap16(imageFrame.pixelData[i]);
}
} else if (imageFrame.bitsAllocated === 8) {
} else if (imageFrame.bitsAllocated === 8 || imageFrame.bitsAllocated === 1) {
// 1-bit data must already be extracted per frame by the caller:
// multi-frame 1-bit pixel data is bit-packed across frame boundaries,
// so frame extraction cannot happen at this level
imageFrame.pixelData = pixelData;
} else if (imageFrame.bitsAllocated === 32) {
let arrayBuffer = pixelData.buffer;

let offset = pixelData.byteOffset;
const length = pixelData.length;
// pixelData is typically a view into the full DICOM P10 buffer, so its
// byteOffset is even (DICOM guarantees even lengths) but not necessarily
// 4-byte aligned; 32-bit typed-array views require 4-byte alignment,
// so copy the bytes to a fresh, aligned buffer when needed
if (offset % 4) {
arrayBuffer = arrayBuffer.slice(offset);
offset = 0;
}

// The swap is a pure byte permutation, so it is done through a
// Uint32Array view regardless of how the result is interpreted below
const swapView = new Uint32Array(arrayBuffer, offset, length / 4);
for (let i = 0; i < swapView.length; i++) {
swapView[i] = swap32(swapView[i]);
}

// 32-bit PixelData is integer data (signed per pixelRepresentation);
// it is only float when pixelRepresentation is absent (e.g. the
// FloatPixelData element), matching cornerstone3D's decodeLittleEndian
if (imageFrame.pixelRepresentation === 0) {
imageFrame.pixelData = swapView;
} else if (imageFrame.pixelRepresentation === 1) {
imageFrame.pixelData = new Int32Array(arrayBuffer, offset, length / 4);
} else {
imageFrame.pixelData = new Float32Array(arrayBuffer, offset, length / 4);
}
}

return imageFrame;
Expand Down
63 changes: 63 additions & 0 deletions packages/big-endian/test/decode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,69 @@ describe("big-endian decode", () => {
expect(Array.from(imageFrame.pixelData)).toEqual([1, 2])
})

it("passes 1-bit pixel data through unchanged", () => {
const pixelData = new Uint8Array([0b10101010])
const imageFrame = { bitsAllocated: 1 }

decode(imageFrame, pixelData)

expect(imageFrame.pixelData).toBe(pixelData)
})

it("byte-swaps 32-bit unsigned pixel data into Uint32Array", () => {
const source = [1, 2, 0xdeadbeef]
// Build the big-endian byte stream for those values
const bigEndianBytes = new Uint8Array(source.length * 4)
const view = new DataView(bigEndianBytes.buffer)
source.forEach((value, i) => view.setUint32(i * 4, value, false))
const imageFrame = { bitsAllocated: 32, pixelRepresentation: 0 }

decode(imageFrame, bigEndianBytes)

expect(imageFrame.pixelData).toBeInstanceOf(Uint32Array)
expect(Array.from(imageFrame.pixelData)).toEqual([1, 2, 0xdeadbeef])
})

it("byte-swaps 32-bit signed pixel data into Int32Array", () => {
const source = [-1, 2, -100000]
const bigEndianBytes = new Uint8Array(source.length * 4)
const view = new DataView(bigEndianBytes.buffer)
source.forEach((value, i) => view.setInt32(i * 4, value, false))
const imageFrame = { bitsAllocated: 32, pixelRepresentation: 1 }

decode(imageFrame, bigEndianBytes)

expect(imageFrame.pixelData).toBeInstanceOf(Int32Array)
expect(Array.from(imageFrame.pixelData)).toEqual([-1, 2, -100000])
})

it("byte-swaps 32-bit pixel data into Float32Array when pixelRepresentation is absent", () => {
const source = new Float32Array([1.5, -2.25, 3.75])
const bigEndianBytes = new Uint8Array(source.length * 4)
const view = new DataView(bigEndianBytes.buffer)
source.forEach((value, i) => view.setFloat32(i * 4, value, false))
const imageFrame = { bitsAllocated: 32 }

decode(imageFrame, bigEndianBytes)

expect(imageFrame.pixelData).toBeInstanceOf(Float32Array)
expect(Array.from(imageFrame.pixelData)).toEqual([1.5, -2.25, 3.75])
})

it("realigns 32-bit pixel data when byteOffset is not 4-byte aligned", () => {
const source = new Float32Array([1.5, -2.25])
const padded = new Uint8Array(2 + source.length * 4)
const view = new DataView(padded.buffer)
source.forEach((value, i) => view.setFloat32(2 + i * 4, value, false))
const pixelData = new Uint8Array(padded.buffer, 2, source.length * 4)
const imageFrame = { bitsAllocated: 32 }

decode(imageFrame, pixelData)

expect(imageFrame.pixelData).toBeInstanceOf(Float32Array)
expect(Array.from(imageFrame.pixelData)).toEqual([1.5, -2.25])
})

it("returns the same imageFrame object", () => {
const imageFrame = { bitsAllocated: 8 }
const result = decode(imageFrame, new Uint8Array([0]))
Expand Down
116 changes: 62 additions & 54 deletions packages/dicom-codec/src/codecs/codecFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,38 +203,42 @@ function getImageFrame(typedArray) {
function encode(context, codecConfig, imageFrame, imageInfo, options = {}) {
const { iterations = 1 } = options;
const encoderInstance = new codecConfig.Encoder();
const decodedTypedArray = encoderInstance.getDecodedBuffer(imageInfo);
decodedTypedArray.set(imageFrame);
try {
const decodedTypedArray = encoderInstance.getDecodedBuffer(imageInfo);
decodedTypedArray.set(imageFrame);

const { beforeEncode = () => {} } = options;
const { beforeEncode = () => {} } = options;

beforeEncode(encoderInstance, codecConfig);
beforeEncode(encoderInstance, codecConfig);

context.timer.init("To encode length: " + imageFrame.length);
for (let i = 0; i < iterations; i++) {
encoderInstance.encode();
}
context.timer.init("To encode length: " + imageFrame.length);
for (let i = 0; i < iterations; i++) {
encoderInstance.encode();
}

context.timer.end();
context.timer.end();

const encodedTypedArray = encoderInstance.getEncodedBuffer();
context.logger.log("Encoded length:" + encodedTypedArray.length);
context.logger.log(
"Encoded is a Typed array of: " + encodedTypedArray.constructor.name
);
const encodedTypedArray = encoderInstance.getEncodedBuffer();
context.logger.log("Encoded length:" + encodedTypedArray.length);
context.logger.log(
"Encoded is a Typed array of: " + encodedTypedArray.constructor.name
);

// cleanup allocated memory
encoderInstance.delete();
const imageFrameOut = getImageFrame(encodedTypedArray);

const processInfo = {
duration: context.timer.getDuration(),
};
const processInfo = {
duration: context.timer.getDuration(),
};

return {
imageFrame: getImageFrame(encodedTypedArray),
imageInfo: getTargetImageInfo(imageInfo, imageInfo),
processInfo,
};
return {
imageFrame: imageFrameOut,
imageInfo: getTargetImageInfo(imageInfo, imageInfo),
processInfo,
};
} finally {
// cleanup allocated memory
encoderInstance.delete();
}
}

/**
Expand All @@ -255,40 +259,44 @@ function decode(context, codecConfig, imageFrame, imageInfo) {
}
const decoderInstance = new codecConfig.Decoder();

const { length } = imageFrame;
// get pointer to the source/encoded bit stream buffer in WASM memory
// that can hold the encoded bitstream
const encodedTypedArray = decoderInstance.getEncodedBuffer(length);

// copy the encoded bitstream into WASM memory buffer
encodedTypedArray.set(imageFrame);
context.timer.init("To decode length: " + length);
// decode it
decoderInstance.decode();
context.timer.end();
try {
const { length } = imageFrame;
// get pointer to the source/encoded bit stream buffer in WASM memory
// that can hold the encoded bitstream
const encodedTypedArray = decoderInstance.getEncodedBuffer(length);

// copy the encoded bitstream into WASM memory buffer
encodedTypedArray.set(imageFrame);
context.timer.init("To decode length: " + length);
// decode it
decoderInstance.decode();
context.timer.end();

const decodedTypedArray = decoderInstance.getDecodedBuffer();

context.logger.log("Decoded length:" + decodedTypedArray.length);
context.logger.log(
"Decoded is a Typed array of: " + decodedTypedArray.constructor.name
);

const decodedTypedArray = decoderInstance.getDecodedBuffer();
// get information about the decoded image
const decodedImageInfo = decoderInstance.getFrameInfo();

context.logger.log("Decoded length:" + decodedTypedArray.length);
context.logger.log(
"Decoded is a Typed array of: " + decodedTypedArray.constructor.name
);
const imageFrameOut = getImageFrame(decodedTypedArray);

// get information about the decoded image
const decodedImageInfo = decoderInstance.getFrameInfo();
const processInfo = {
duration: context.timer.getDuration(),
};

// cleanup allocated memory
decoderInstance.delete();

const processInfo = {
duration: context.timer.getDuration(),
};

return {
imageFrame: getImageFrame(decodedTypedArray),
imageInfo: getTargetImageInfo(imageInfo, decodedImageInfo),
processInfo,
};
return {
imageFrame: imageFrameOut,
imageInfo: getTargetImageInfo(imageInfo, decodedImageInfo),
processInfo,
};
} finally {
// cleanup allocated memory
decoderInstance.delete();
}
}

exports.runProcess = runProcess;
Expand Down
8 changes: 7 additions & 1 deletion packages/dicom-codec/src/codecs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,18 @@ function getCodec(transferSyntaxUID) {
* @returns {ExtendedImageInfo} Adapted imageInfo to all codecs.
*/
function adaptImageInfo(imageInfo) {
const { rows, columns, bitsAllocated, signed, samplesPerPixel, pixelRepresentation } = imageInfo;
const { rows, columns, bitsAllocated, signed, samplesPerPixel, pixelRepresentation, planarConfiguration } = imageInfo;

return {
pixelRepresentation,
bitsAllocated,
samplesPerPixel,
// Must survive adaptation: rleLossless dispatches between interleaved
// (decode8) and plane-sequential (decode8Planar) output on this flag.
// It was previously dropped here, which made decode8Planar unreachable
// through the public decode() API — PlanarConfiguration=1 datasets
// silently produced interleaved output.
planarConfiguration,
rows, // Number with the image rows/height
columns, // Number with the image columns/width
width: columns,
Expand Down
36 changes: 27 additions & 9 deletions packages/dicom-codec/src/codecs/libjpegTurbo12bit.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,53 @@
const codecModule = require("@cornerstonejs/codec-libjpeg-turbo-12bit");
const codecWasmModule = require("@cornerstonejs/codec-libjpeg-turbo-12bit/wasmjs");
const codecFactory = require("./codecFactory");

/**
* @type {CodecWrapper}
*/
const codecWrapper = {
// assign it and prevent initialization
codec: undefined,
Decoder: undefined,
Encoder: undefined,
decoderName: "codec libjpeg turbo 12bit",
encoderName: "codec libjpeg turbo 12bit",
encoderName: "JPEGEncoder",
decoderName: "JPEGDecoder",
};

/**
* Decode imageFrame using libjpegTurbo 12bit decoder.
*
* @param {TypedArray} imageFrame to decode.
* @param {ExtendedImageInfo} imageInfo image info options.
* @returns Object containing decoded image frame and imageInfo (current) data.
*/
async function decode(imageFrame, imageInfo) {
throw Error("Decoder not found for codec:" + codecWrapper.encoderName);
return codecFactory.runProcess(
codecWrapper,
codecModule,
codecWasmModule,
codecWrapper.decoderName,
(context) => {
return codecFactory.decode(context, codecWrapper, imageFrame, imageInfo);
}
);
}

/**
* <<Not available yet>> Encode imageFrame to libjpegTurbo 12bits format.
* <<Not available>> The libjpeg-turbo 12bit build does not expose an
* encoder (see src/jslib.cpp — the JPEGEncoder bindings are disabled), so
* encoding is not supported for this codec.
*
* @param {TypedArray} imageFrame to encode.
* @param {ExtendedImageInfo} imageInfo image info options.
* @param {Object} options encode option.
* @returns Object containing encoded image frame and imageInfo (current) data
*/
async function encode(imageFrame, imageInfo, options = {}) {
throw Error("Encoder not found for codec:" + codecWrapper.encoderName);
throw Error("Encoder not supported for codec: libjpeg-turbo 12bit");
}

function getPixelData(imageFrame, imageInfo) {
throw Error(
"GetPixel not found or not applied for codec:" + codecWrapper.encoderName
);
return codecFactory.getPixelData(imageFrame, imageInfo);
}

exports.decode = decode;
Expand Down
Loading
Loading