Skip to content
Open
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 .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ jobs:
ci_touched=true ;;
esac
case "$f" in
.github/workflows/*|package.json|yarn.lock|vitest.workspace.mjs|babel.config.json|lerna.json|tools/ci/*|tools/dist-size/*|tools/browser-smoke/*|tools/fixture-verification/*)
.github/workflows/*|package.json|yarn.lock|vitest.workspace.mjs|babel.config.json|lerna.json|tools/ci/*|tools/csp/*|tools/dist-size/*|tools/browser-smoke/*|tools/fixture-verification/*)
toolchain_touched=true ;;
packages/*)
pkg=${f#packages/}; pkg=${pkg%%/*}
Expand Down
23 changes: 17 additions & 6 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@ jobs:
REF: ${{ github.ref }}
run: |
set -e
ALL=(charls libjpeg-turbo-8bit libjpeg-turbo-12bit openjpeg openjphjs little-endian big-endian dicom-codec)
ALL=(charls libjpeg-turbo-8bit libjpeg-turbo-12bit libjxl openjpeg openjphjs little-endian big-endian dicom-codec)
BENCHABLE=(charls libjpeg-turbo-8bit libjpeg-turbo-12bit openjpeg openjphjs little-endian big-endian dicom-codec)
ALL_JSON=$(printf '%s\n' "${ALL[@]}" | jq -R . | jq -s -c .)
BENCHABLE_JSON=$(printf '%s\n' "${BENCHABLE[@]}" | jq -R . | jq -s -c .)

# Baseline runs: on a manual dispatch or any commit landing on
# main, build/test/bench every package. The "diff vs main" trick
Expand All @@ -90,7 +92,7 @@ jobs:
if [ "$EVENT_NAME" = "workflow_dispatch" ] || [ "$REF" = "refs/heads/main" ]; then
echo "Baseline run ($EVENT_NAME on $REF): forcing all packages"
echo "packages=$ALL_JSON" >> "$GITHUB_OUTPUT"
echo "bench=$ALL_JSON" >> "$GITHUB_OUTPUT"
echo "bench=$BENCHABLE_JSON" >> "$GITHUB_OUTPUT"
echo "any=true" >> "$GITHUB_OUTPUT"
exit 0
fi
Expand Down Expand Up @@ -127,16 +129,20 @@ jobs:
if ! git diff --quiet "$BASE"..HEAD -- "$p"; then
echo "Toolchain change detected in $p: forcing all packages (build/test/bench)"
echo "packages=$ALL_JSON" >> "$GITHUB_OUTPUT"
echo "bench=$ALL_JSON" >> "$GITHUB_OUTPUT"
echo "bench=$BENCHABLE_JSON" >> "$GITHUB_OUTPUT"
echo "any=true" >> "$GITHUB_OUTPUT"
exit 0
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
done

changed=()
bench_changed=()
for pkg in "${ALL[@]}"; do
if ! git diff --quiet "$BASE"..HEAD -- "packages/$pkg/"; then
changed+=("$pkg")
for known in "${BENCHABLE[@]}"; do
[ "$pkg" = "$known" ] && bench_changed+=("$pkg") && break
done
fi
done
if [ ${#changed[@]} -eq 0 ]; then
Expand All @@ -145,8 +151,13 @@ jobs:
echo 'bench=[]' >> "$GITHUB_OUTPUT"
echo "any=false" >> "$GITHUB_OUTPUT"
else
bench_json=$(printf '%s\n' "${changed[@]}" | jq -R . | jq -s -c .)
echo "Changed: $bench_json — building all packages, benching changed only"
changed_json=$(printf '%s\n' "${changed[@]}" | jq -R . | jq -s -c .)
if [ ${#bench_changed[@]} -gt 0 ]; then
bench_json=$(printf '%s\n' "${bench_changed[@]}" | jq -R . | jq -s -c .)
else
bench_json='[]'
fi
echo "Changed: $changed_json — building all packages, benching supported changes only"
echo "packages=$ALL_JSON" >> "$GITHUB_OUTPUT"
echo "bench=$bench_json" >> "$GITHUB_OUTPUT"
echo "any=true" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -347,7 +358,7 @@ jobs:
# time. Enable macro runners for the org on app.codspeed.io first
# (the repo is public — also make sure the runner group allows public
# repositories), then: gh variable set CODSPEED_MACRO_ENABLED --body true
if: needs.detect-changes.outputs.any == 'true' && vars.CODSPEED_MACRO_ENABLED == 'true'
if: needs.detect-changes.outputs.any == 'true' && needs.detect-changes.outputs.bench != '[]' && vars.CODSPEED_MACRO_ENABLED == 'true'
# Advisory instrument: real wall-clock numbers (V8 JIT active, real
# cache/branch behavior) that complement the simulation gate (bench.yml) —
# simulation catches small algorithmic slips deterministically,
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
[submodule "packages/charls/extern/charls"]
path = packages/charls/extern/charls
url = https://github.com/cornerstonejs/charls.git
[submodule "packages/libjxl/extern/libjxl"]
path = packages/libjxl/extern/libjxl
url = https://github.com/libjxl/libjxl.git
14 changes: 13 additions & 1 deletion packages/dicom-codec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ DICOM codecs for JavaScript, with browser and Node.js support.
| JpegLossless | 1.2.840.10008.1.2.4.57 1.2.840.10008.1.2.4.70 | X | - | X | - |
| Jpegls | 1.2.840.10008.1.2.4.80 1.2.840.10008.1.2.4.81 | X | X | X | X |
| Jpeg2000 | 1.2.840.10008.1.2.4.90 1.2.840.10008.1.2.4.91 | X | X | X | X |
| JpegXL | 1.2.840.10008.1.2.4.110 1.2.840.10008.1.2.4.111 1.2.840.10008.1.2.4.112 | X | X except .111 | X | X |
| RleLossless | 1.2.840.10008.1.2.5 | X | - | - | - |
| HTJ2K | 1.2.840.10008.1.2.202 (201,203 as well) | X | X | X | X |

Expand Down Expand Up @@ -177,7 +178,18 @@ Parameters (**It does not mutate any param** ):

## TransferSyntax Specific Encoding Options

TBD
### JPEG XL

Transfer syntax `1.2.840.10008.1.2.4.110` always uses lossless encoding.
Transfer syntax `1.2.840.10008.1.2.4.111` supports decoding only because the
pixel encoder does not create JPEG reconstruction data.

Transfer syntax `1.2.840.10008.1.2.4.112` accepts these encoding options:

- `lossless`: Boolean, defaults to `true`.
- `distance`: Number from 0 to 25 for lossy encoding.
- `effort`: Integer from 1 to 9.
- `decodingSpeed`: Integer from 0 to 4.

## Testing

Expand Down
1 change: 1 addition & 0 deletions packages/dicom-codec/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@cornerstonejs/codec-big-endian": "^0.1.1",
"@cornerstonejs/codec-charls": "^1.2.5",
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.4",
"@cornerstonejs/codec-libjxl": "^1.0.0",
"@cornerstonejs/codec-little-endian": "^0.0.7",
"@cornerstonejs/codec-openjpeg": "^1.3.2",
"@cornerstonejs/codec-openjph": "^2.4.9",
Expand Down
7 changes: 5 additions & 2 deletions packages/dicom-codec/src/codecs/codecFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ function encode(context, codecConfig, imageFrame, imageInfo, options = {}) {
"Encoded is a Typed array of: " + encodedTypedArray.constructor.name
);

const encodedImageFrame = getImageFrame(encodedTypedArray).slice();

// cleanup allocated memory
encoderInstance.delete();

Expand All @@ -231,7 +233,7 @@ function encode(context, codecConfig, imageFrame, imageInfo, options = {}) {
};

return {
imageFrame: getImageFrame(encodedTypedArray),
imageFrame: encodedImageFrame,
imageInfo: getTargetImageInfo(imageInfo, imageInfo),
processInfo,
};
Expand Down Expand Up @@ -276,6 +278,7 @@ function decode(context, codecConfig, imageFrame, imageInfo) {

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

// cleanup allocated memory
decoderInstance.delete();
Expand All @@ -285,7 +288,7 @@ function decode(context, codecConfig, imageFrame, imageInfo) {
};

return {
imageFrame: getImageFrame(decodedTypedArray),
imageFrame: decodedImageFrame,
imageInfo: getTargetImageInfo(imageInfo, decodedImageInfo),
processInfo,
};
Expand Down
11 changes: 11 additions & 0 deletions packages/dicom-codec/src/codecs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ const rleLosslessCodec = require("./rleLossless");
const bigEndianCodec = require("./bigEndian");
const libjpegTurbo8BitCodec = require("./libjpegTurbo8bit");
const libjpegTurbo12BitCodec = require("./libjpegTurbo12bit");
const jpegxlCodec = require("./jpegxl");

const jpegxlLosslessCodec = Object.assign({}, jpegxlCodec, {
encode: jpegxlCodec.encodeLossless,
});
const jpegxlJpegRecompressionCodec = Object.assign({}, jpegxlCodec, {
encode: jpegxlCodec.encodeJpegRecompression,
});

/**
* Wrapper to codec. It holds current codec, encoder, decoder, name for each.
Expand Down Expand Up @@ -42,6 +50,9 @@ const codecsMap = {
"1.2.840.10008.1.2.4.81": jpeglsCodec,
"1.2.840.10008.1.2.4.90": jpeg2000Codec,
"1.2.840.10008.1.2.4.91": jpeg2000Codec,
"1.2.840.10008.1.2.4.110": jpegxlLosslessCodec,
"1.2.840.10008.1.2.4.111": jpegxlJpegRecompressionCodec,
"1.2.840.10008.1.2.4.112": jpegxlCodec,
// Private Transfer Syntax - update to final ID when released by WG-06
"3.2.840.10008.1.2.4.96": htj2kCodec,
// The three official HTJ2K transfer syntaxes
Expand Down
127 changes: 127 additions & 0 deletions packages/dicom-codec/src/codecs/jpegxl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
const codecFactory = require("./codecFactory");
const { isNode } = require("browser-or-node");

const decoderWrapper = {
codec: undefined,
Decoder: undefined,
Encoder: undefined,
encoderName: "",
decoderName: "JpegXLDecoder",
};

const encoderWrapper = {
codec: undefined,
Decoder: undefined,
Encoder: undefined,
encoderName: "JpegXLEncoder",
decoderName: "",
};

async function loadDecoder() {
const module = await import("@cornerstonejs/codec-libjxl/decodewasmjs");
return module.default(getModuleOptions("@cornerstonejs/codec-libjxl/decodewasm"));
}

async function loadEncoder() {
const module = await import("@cornerstonejs/codec-libjxl/encodewasmjs");
return module.default(getModuleOptions("@cornerstonejs/codec-libjxl/encodewasm"));
}

function getModuleOptions(wasmModule) {
if (!isNode) {
return {};
}

const nodeRequire = eval("require");
const fs = nodeRequire("fs");
return { wasmBinary: fs.readFileSync(nodeRequire.resolve(wasmModule)) };
}

async function decode(imageFrame, imageInfo) {
return codecFactory.runProcess(
decoderWrapper,
loadDecoder,
null,
decoderWrapper.decoderName,
(context) => {
const result = codecFactory.decode(
context,
decoderWrapper,
imageFrame,
imageInfo
);

result.imageInfo.signed = imageInfo.signed;
result.imageInfo.isSigned = imageInfo.isSigned;
result.imageInfo.pixelRepresentation = imageInfo.pixelRepresentation;
return result;
}
);
}

async function encode(imageFrame, imageInfo, options = {}) {
return codecFactory.runProcess(
encoderWrapper,
loadEncoder,
null,
encoderWrapper.encoderName,
(context) => {
function beforeEncode(encoderInstance) {
const {
lossless = true,
distance,
effort,
decodingSpeed,
} = options;

encoderInstance.setLossless(lossless);
if (distance !== undefined) {
encoderInstance.setDistance(distance);
}
if (effort !== undefined) {
encoderInstance.setEffort(effort);
}
if (decodingSpeed !== undefined) {
encoderInstance.setDecodingSpeed(decodingSpeed);
}
}

const codecImageInfo = Object.assign({}, imageInfo, { isSigned: false });
const result = codecFactory.encode(
context,
encoderWrapper,
imageFrame,
codecImageInfo,
Object.assign({}, options, { beforeEncode })
);

result.imageInfo.signed = imageInfo.signed;
result.imageInfo.isSigned = imageInfo.isSigned;
return result;
}
);
}

function encodeLossless(imageFrame, imageInfo, options = {}) {
return encode(
imageFrame,
imageInfo,
Object.assign({}, options, { lossless: true })
);
}

async function encodeJpegRecompression() {
throw new Error(
"JPEG XL JPEG Recompression encoding is not supported by the pixel encoder"
);
}

function getPixelData(imageFrame, imageInfo) {
return codecFactory.getPixelData(imageFrame, imageInfo);
}

exports.decode = decode;
exports.encode = encode;
exports.encodeLossless = encodeLossless;
exports.encodeJpegRecompression = encodeJpegRecompression;
exports.getPixelData = getPixelData;
5 changes: 5 additions & 0 deletions packages/dicom-codec/test/dispatch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const REQUIRED_BUILDS = [
"libjpeg-turbo-8bit/dist/libjpegturbojs.js",
"openjpeg/dist/openjpegjs.js",
"openjphjs/dist/openjphjs.js",
"libjxl/dist/jpegxlwasm_decode.js",
"libjxl/dist/jpegxlwasm_encode.js",
"little-endian/dist/index.js",
"big-endian/dist/index.js",
]
Expand All @@ -30,6 +32,9 @@ const SUPPORTED_UIDS = [
"1.2.840.10008.1.2.4.81",
"1.2.840.10008.1.2.4.90",
"1.2.840.10008.1.2.4.91",
"1.2.840.10008.1.2.4.110",
"1.2.840.10008.1.2.4.111",
"1.2.840.10008.1.2.4.112",
"1.2.840.10008.1.2.4.201",
"1.2.840.10008.1.2.4.202",
"1.2.840.10008.1.2.4.203",
Expand Down
26 changes: 26 additions & 0 deletions packages/dicom-codec/test/transcode-and-pixeldata.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const packagesRoot = resolve(__dirname, "../..")

const REQUIRED = [
"charls/dist/charlsjs.js",
"libjxl/dist/jpegxlwasm_decode.js",
"libjxl/dist/jpegxlwasm_encode.js",
]
const ALL_BUILT = REQUIRED.every((p) => existsSync(resolve(packagesRoot, p)))

Expand Down Expand Up @@ -41,6 +43,30 @@ describe.skipIf(!ALL_BUILT)("dicom-codec encode", () => {
const decoded = await dicomCodec.decode(encoded.imageFrame, ctImageInfo, "1.2.840.10008.1.2.4.80")
expect(frameBytes(decoded.imageFrame).equals(ct1Raw)).toBe(true)
})

it("encode() to JPEG XL Lossless (.110) round-trips byte-exact", async () => {
const encoded = await dicomCodec.encode(
new Uint8Array(ct1Raw),
ctImageInfo,
"1.2.840.10008.1.2.4.110"
)
const decoded = await dicomCodec.decode(
encoded.imageFrame,
ctImageInfo,
"1.2.840.10008.1.2.4.110"
)
expect(frameBytes(decoded.imageFrame).equals(ct1Raw)).toBe(true)
})

it("rejects JPEG XL JPEG Recompression encoding (.111)", async () => {
await expect(
dicomCodec.encode(
new Uint8Array(ct1Raw),
ctImageInfo,
"1.2.840.10008.1.2.4.111"
)
).rejects.toThrow(/JPEG Recompression encoding is not supported/)
})
})

describe.skipIf(!ALL_BUILT)("dicom-codec getPixelData typed-array contract", () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/libjxl/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build/
!dist/
!dist/**
Loading