Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Transfer Syntax is the language used in DICOM to describe the DICOM file format
- \*\*\* - Unlike all other DICOM transfer syntaxes, the deflate transfer syntaxes compress the whole of the DICOM data (tags, lengths, VR etc.) rather than just the pixel data - this is done using the standard “deflate” mechanism as used in gzip etc.) It is therefore most suitable for non-pixel objects such as structured reports, presentation states etc.

- 5: [JS Decoder](https://github.com/cornerstonejs/cornerstoneWADOImageLoader/blob/4bfa04759412d58647cc5d6bd0204aa37e4542e3/src/shared/decoders/decodeRLE.js)
- 57 & 70: [JS Decoder](https://github.com/cornerstonejs/cornerstoneWADOImageLoader/blob/4bfa04759412d58647cc5d6bd0204aa37e4542e3/codecs/jpegLossless.js)
- 57 & 70: [JS Decoder](https://github.com/cornerstonejs/JPEGLosslessDecoderJS) — used as the published [`@cornerstonejs/jpeg-lossless-decoder-js`](https://www.npmjs.com/package/@cornerstonejs/jpeg-lossless-decoder-js), a fork that carries the end-of-scan fix the unscoped `jpeg-lossless-decoder-js` still lacks
- 1.2 & 2.1 & 99: [JS Decoder](https://github.com/cornerstonejs/cornerstoneWADOImageLoader/blob/4bfa04759412d58647cc5d6bd0204aa37e4542e3/src/shared/decoders/decodeLittleEndian.js)
- 2.2: [JS Decoder](https://github.com/cornerstonejs/cornerstoneWADOImageLoader/blob/4bfa04759412d58647cc5d6bd0204aa37e4542e3/src/shared/decoders/decodeBigEndian.js)

Expand Down
4 changes: 2 additions & 2 deletions packages/dicom-codec/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"@cornerstonejs/codec-little-endian": "^0.0.9",
"@cornerstonejs/codec-openjpeg": "^1.3.6",
"@cornerstonejs/codec-openjph": "^2.4.11",
"browser-or-node": "^2.0.0",
"jpeg-lossless-decoder-js": "^2.0.4"
"@cornerstonejs/jpeg-lossless-decoder-js": "^2.2.0",
"browser-or-node": "^2.0.0"
}
}
5 changes: 4 additions & 1 deletion packages/dicom-codec/src/codecs/jpegLossless.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const codecModule = require("jpeg-lossless-decoder-js");
// @cornerstonejs/jpeg-lossless-decoder-js, not the unscoped
// jpeg-lossless-decoder-js: the published 2.1.2 of that package loses the last
// sample of any frame whose final Huffman code ends on a byte boundary.
const codecModule = require("@cornerstonejs/jpeg-lossless-decoder-js");
const codecFactory = require("./codecFactory");

/**
Expand Down
34 changes: 11 additions & 23 deletions packages/dicom-codec/test/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,8 @@ describe.skipIf(!ALL_BUILT)("dicom-codec integration", () => {

describe("JPEG Lossless (1.2.840.10008.1.2.4.57 / .70)", () => {
// These go through dicom-codec's internal jpegLosslessCodec
// (jpeg-lossless-decoder-js, pure JS — no separate wasm package). Both
// fixtures encode the same 512x512x16 CT slice; the reference
// (@cornerstonejs/jpeg-lossless-decoder-js, pure JS — no wasm package).
// Both fixtures encode the same 512x512x16 CT slice; the reference
// fixtures/raw/CT-512x512.raw was cross-validated three ways: the RLE
// decoder, the Process 14 path of jpeg-lossless-decoder-js and DCMTK's
// dcmdjpeg all produce these exact bytes.
Expand Down Expand Up @@ -258,33 +258,21 @@ describe.skipIf(!ALL_BUILT)("dicom-codec integration", () => {
expect(frameBytes(result.imageFrame).equals(ctRaw)).toBe(true)
})

// KNOWN UPSTREAM BUG (jpeg-lossless-decoder-js): the SV1 path decodes
// the final pixel of this fixture as 0 instead of -2000. DCMTK's
// dcmdjpeg confirms the fixture itself is correct (its decode matches
// CT-512x512.raw exactly, last pixel included), so the defect is in the
// JS decoder. The test below pins today's behavior: every sample except
// the last matches the reference. When the upstream bug is fixed, the
// paired `it.fails` test starts passing and vitest will flag it — then
// fold these two tests into a single exact comparison.
it("decodes Process 14 SV1 through the dispatcher (.70) — all but the last pixel match", async () => {
// This fixture is the regression case for the forked decoder: its scan
// ends in a long run of the image minimum whose zero-difference codes tile
// the final byte exactly, and published jpeg-lossless-decoder-js 2.1.2
// decoded that last sample as 0 instead of -2000 (it read the 0xFF
// introducing EOI as entropy coded data and stopped one sample early).
// DCMTK's dcmdjpeg always decoded this fixture to CT-512x512.raw exactly,
// last pixel included. An exact comparison here fails on any decoder
// without the fix.
it("decodes Process 14 SV1 through the dispatcher (.70) to the exact reference pixels", async () => {
const result = await dicomCodec.decode(
jpllProcess14Sv1,
ctImageInfo,
"1.2.840.10008.1.2.4.70"
)
expect(result.imageFrame.byteLength).toBe(ctRaw.length)

const actual = frameBytes(result.imageFrame)
// Everything up to the final 16-bit sample must match exactly.
expect(actual.subarray(0, ctRaw.length - 2).equals(ctRaw.subarray(0, ctRaw.length - 2))).toBe(true)
})

it.fails("decodes Process 14 SV1 (.70) to the exact reference pixels (known upstream last-pixel bug)", async () => {
const result = await dicomCodec.decode(
jpllProcess14Sv1,
ctImageInfo,
"1.2.840.10008.1.2.4.70"
)
expect(frameBytes(result.imageFrame).equals(ctRaw)).toBe(true)
})
})
Expand Down
27 changes: 8 additions & 19 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,20 @@ frozenLockfile: true
# local decision.
allowBuilds:
esbuild: true

# pnpm refuses a dependency version that the registry published very recently,
# which is a supply-chain measure: it buys time for a compromised release to be
# found and unpublished before this repo installs it. That measure is aimed at
# third-party code, and it cannot judge our own.
#
# @cornerstonejs/jpeg-lossless-decoder-js is published by this organisation from
# cornerstonejs/JPEGLosslessDecoderJS, and dicom-codec needs it for transfer
# syntaxes 1.2.840.10008.1.2.4.57 and .70. Waiting out the age window on our own
# release would block a decode fix behind a delay that protects nothing here.
#
# The entry names the package and no version on purpose. pnpm writes a
# version-pinned entry when it adds one itself, and that entry goes stale at
# every release of the package, which turns a routine version bump into an
# install failure. Keep this list to packages that this organisation publishes.
minimumReleaseAgeExclude:
- '@cornerstonejs/jpeg-lossless-decoder-js'
26 changes: 16 additions & 10 deletions tools/fixture-verification/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
Independent, from-scratch decoders used to verify that the `.raw`/`.RAW`
pixel references committed under `packages/*/test/fixtures` are correct.
They share **no code** with the codecs under test (the emscripten wasm
builds, `jpeg-lossless-decoder-js`, or dicom-codec's `rleLossless.js`), so
byte-exact agreement means two independent implementations produce the
same pixels from the same codestream.
builds, `@cornerstonejs/jpeg-lossless-decoder-js`, or dicom-codec's
`rleLossless.js`), so byte-exact agreement means two independent
implementations produce the same pixels from the same codestream.

```
node tools/fixture-verification/run-all.js
Expand Down Expand Up @@ -98,11 +98,17 @@ context, with CharLS and pylibjpeg agreeing with each other. The bug is in
remain valid (independently confirmed by DCMTK/RLE cross-checks). Fixing
the 12-bit run-interruption path here is a TODO.

## Bug found by this verification
## Bug found by this verification (now fixed)

`jpeg-lossless-decoder-js` (used by dicom-codec for transfer syntaxes
.57/.70) decodes the final pixel of the SV1 fixture as 0 instead of -2000.
Four independent decoders agree the fixture is correct: the from-scratch
`jpll.js` here, DCMTK's `dcmdjpeg`, the RLE decode of the same slice, and
the library's own Process-14 path. See the pinned `it.fails` test in
`packages/dicom-codec/test/integration.test.js`.
Published `jpeg-lossless-decoder-js` 2.1.2 (used by dicom-codec for transfer
syntaxes .57/.70) decoded the final pixel of the SV1 fixture as 0 instead of
-2000. Four independent decoders agreed the fixture was correct: the
from-scratch `jpll.js` here, DCMTK's `dcmdjpeg`, the RLE decode of the same
slice, and the library's own Process-14 path.

The cause was an off-by-one in the end-of-scan guards, which read the 0xFF
introducing EOI as entropy coded data whenever the last Huffman code ended
exactly on a byte boundary. dicom-codec now depends on
`@cornerstonejs/jpeg-lossless-decoder-js`, a fork of the library that carries
the fix, and `packages/dicom-codec/test/integration.test.js` compares both
fixtures byte-for-byte.
Loading