Summary
J2KDecoder::decode() throws a WebAssembly RuntimeError: memory access out of bounds when decoding a J2K/J2C codestream whose SIZ marker declares component subsampling (XRsiz/YRsiz > 1). This affects real-world digital mammography (MG) images, which commonly use XRsiz=YRsiz=2.
Sample base64-encoded j2c MG image: mg-j2c-base64.txt
Package version: @cornerstonejs/codec-openjpeg v1.3.0
Affected file: packages/openjpeg/src/J2KDecoder.hpp
Root cause
After opj_decode() returns, decode_i() populates frameInfo_ from the raw SIZ reference-grid fields:
// J2KDecoder.hpp (current)
frameInfo_.width = image->x1; // Xsiz — reference-grid extent
frameInfo_.height = image->y1; // Ysiz — reference-grid extent
For images with component subsampling, image->x1 / image->y1 are the reference-grid dimensions (e.g. 4095×5625), not the actual decoded pixel dimensions. OpenJPEG already computes the correct component dimensions in image->comps[0].w / .h = ceil((x1−x0) / XRsiz) (e.g. 2048×2813 for XRsiz=YRsiz=2).
Using the wrong (oversized) dimensions causes two bugs:
- getFrameInfo() returns incorrect dimensions — callers receive the reference-grid extent instead of the actual pixel count, so they misinterpret the decoded buffer.
- Heap out-of-bounds write / WASM crash — calculateSizeAtDecompositionLevel() uses frameInfo_.width to compute destinationSize, allocating a buffer sized for x1 × y1 pixels (e.g. ~44 MB). The subsequent pixel-copy loop then indexes image->comps[0].data[y * x1], which runs far past the end of the component data (only comps [0].w × comps[0].h int32 values were allocated by OpenJPEG). In the WASM build this is caught by linear-memory bounds checking and throws:
RuntimeError: memory access out of bounds
Reproduction
Any J2K codestream with XRsiz=YRsiz=2 in the SIZ marker triggers the crash. A minimal example with the digital mammography image format:
const decoder = new codec.J2KDecoder();
decoder.getEncodedBuffer(mg_j2k_bytes.length).set(mg_j2k_bytes);
decoder.decode(); // ← throws "memory access out of bounds"
The SIZ marker of such a codestream looks like:
Xsiz = 4095 (reference-grid width)
Ysiz = 5625 (reference-grid height)
XRsiz = 2 (component subsampling factor)
YRsiz = 2
→ actual component pixels: ceil(4095/2) × ceil(5625/2) = 2048 × 2813
Fix
Replace image->x1 / image->y1 with image->comps[0].w / image->comps[0].h:
// J2KDecoder.hpp — proposed fix
frameInfo_.width = image->comps[0].w; // ceil((x1 - x0) / XRsiz)
frameInfo_.height = image->comps[0].h; // ceil((y1 - y0) / YRsiz)
image->comps[i].w and .h are set correctly by OpenJPEG during opj_decode() per ISO 15444-1 §B.2, and already account for the subsampling factor. This makes getFrameInfo() return the actual decoded pixel dimensions and fixes the out-of-bounds access.
The one-line change has been tested against:
• A real MG J2K with XRsiz=YRsiz=2 (reference grid 4095×5625, component 2048×2813): decodes correctly, frameInfo.width=2048, frameInfo.height=2813.
• Existing CT, MR, and other images with XRsiz=YRsiz=1: no regression.
References
• ISO 15444-1 §B.2: component sample count = ceil((Xsiz − XOsiz) / XRsiz)
• OpenJPEG sets image->comps[i].w correctly in opj_j2k_update_image_dimensions()
Summary
J2KDecoder::decode()throws a WebAssemblyRuntimeError: memory access out of boundswhen decoding a J2K/J2C codestream whose SIZ marker declares component subsampling (XRsiz/YRsiz > 1). This affects real-world digital mammography (MG) images, which commonly use XRsiz=YRsiz=2.Sample base64-encoded j2c MG image: mg-j2c-base64.txt
Package version:
@cornerstonejs/codec-openjpegv1.3.0Affected file:
packages/openjpeg/src/J2KDecoder.hppRoot cause
After
opj_decode()returns,decode_i()populatesframeInfo_from the raw SIZ reference-grid fields:For images with component subsampling, image->x1 / image->y1 are the reference-grid dimensions (e.g. 4095×5625), not the actual decoded pixel dimensions. OpenJPEG already computes the correct component dimensions in image->comps[0].w / .h = ceil((x1−x0) / XRsiz) (e.g. 2048×2813 for XRsiz=YRsiz=2).
Using the wrong (oversized) dimensions causes two bugs:
RuntimeError: memory access out of bounds
Reproduction
Any J2K codestream with XRsiz=YRsiz=2 in the SIZ marker triggers the crash. A minimal example with the digital mammography image format:
const decoder = new codec.J2KDecoder();
decoder.getEncodedBuffer(mg_j2k_bytes.length).set(mg_j2k_bytes);
decoder.decode(); // ← throws "memory access out of bounds"
The SIZ marker of such a codestream looks like:
Xsiz = 4095 (reference-grid width)
Ysiz = 5625 (reference-grid height)
XRsiz = 2 (component subsampling factor)
YRsiz = 2
→ actual component pixels: ceil(4095/2) × ceil(5625/2) = 2048 × 2813
Fix
Replace image->x1 / image->y1 with image->comps[0].w / image->comps[0].h:
// J2KDecoder.hpp — proposed fix
frameInfo_.width = image->comps[0].w; // ceil((x1 - x0) / XRsiz)
frameInfo_.height = image->comps[0].h; // ceil((y1 - y0) / YRsiz)
image->comps[i].w and .h are set correctly by OpenJPEG during opj_decode() per ISO 15444-1 §B.2, and already account for the subsampling factor. This makes getFrameInfo() return the actual decoded pixel dimensions and fixes the out-of-bounds access.
The one-line change has been tested against:
• A real MG J2K with XRsiz=YRsiz=2 (reference grid 4095×5625, component 2048×2813): decodes correctly, frameInfo.width=2048, frameInfo.height=2813.
• Existing CT, MR, and other images with XRsiz=YRsiz=1: no regression.
References
• ISO 15444-1 §B.2: component sample count = ceil((Xsiz − XOsiz) / XRsiz)
• OpenJPEG sets image->comps[i].w correctly in opj_j2k_update_image_dimensions()