Skip to content

fixes: 12-bit wiring, buffer sizing, error-path cleanup, and test gating#71

Closed
sedghi wants to merge 10 commits into
mainfrom
fixes
Closed

fixes: 12-bit wiring, buffer sizing, error-path cleanup, and test gating#71
sedghi wants to merge 10 commits into
mainfrom
fixes

Conversation

@sedghi

@sedghi sedghi commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary

A batch of correctness, robustness, and test-infrastructure fixes across the codec packages. Each change is an independent commit.

Changes

  • libjpeg-turbo-12bit — implement the codec end to end. The 12-bit decoder produced a single-component 16-bit frame but was decoding into a mismatched color space and buffer size; this corrects the output format and buffer sizing so the decoded dimensions and sample count are right. The dicom-codec wrapper for transfer syntax 1.2.840.10008.1.2.4.51 (previously an unimplemented stub that always threw) is now wired to the real decoder, and the package main/exports are corrected to the filenames the build actually emits. Adds a decode test against a real 12-bit fixture.

  • Overflow-safe buffer sizing (openjpeg, openjph, libjpeg-8bit/12bit). The decoded-buffer size is width * height * components * bytesPerPixel, all taken from the encoded header. On the 32-bit wasm target that product can wrap around; the size math is now computed with a checked 64-bit multiply and a sane upper bound, so a malformed header can't produce an undersized allocation.

  • openjpeg decoder — reject inputs it can't handle. Bail out cleanly on unsupported component counts and on buffers too small to contain a header, instead of reading/writing past the intended bounds.

  • openjpeg BufferStream — bounds-check the callbacks. The skip/write/seek stream callbacks now clamp to the buffer extents rather than advancing past them.

  • dicom-codec — free wasm instances on the error path. decode/encode now release the decoder/encoder instance in a finally, so a failed decode no longer leaks the instance and its heap buffers.

  • Test infrastructure. The libjpeg-turbo-8bit suite is gated on a built dist/ so a build-less yarn test skips cleanly instead of failing with a module-not-found error; a root test script is added.

Verification

yarn test passes locally (13 passed | 75 skipped); the wasm-backed suites skip cleanly because the WebAssembly artifacts aren't built in a plain checkout. The wasm packages are compiled and their decode tests run in CI (the emsdk build matrix), which is the authoritative gate for the C++ changes here.

Summary by CodeRabbit

  • New Features

    • Added support for decoding 12-bit JPEG images, including build and package updates to expose the codec.
  • Bug Fixes

    • Improved cleanup during codec processing so resources are released even when decoding fails.
    • Added safer size checks and bounds handling to reduce crashes or invalid allocations when handling large or malformed images.
  • Tests

    • Added coverage for 12-bit JPEG decoding, error handling, and resource cleanup.
    • Updated test scripts to run with Vitest.

@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@sedghi sedghi changed the title Codec decode fixes: 12-bit wiring, buffer sizing, error-path cleanup, and test gating fixes: 12-bit wiring, buffer sizing, error-path cleanup, and test gating Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sedghi, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4ef2a382-b475-4b92-8b0f-0456ca490344

📥 Commits

Reviewing files that changed from the base of the PR and between 0f609bd and 3179998.

📒 Files selected for processing (4)
  • .github/workflows/pr-checks.yml
  • packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp
  • packages/libjpeg-turbo-12bit/test/decode.test.js
  • packages/openjpeg/src/J2KDecoder.hpp
📝 Walkthrough

Walkthrough

This PR enables the libjpeg-turbo 12-bit JPEG codec in the dispatcher, switches its decoder to grayscale 12-bit output, and ensures codecFactory always cleans up encoder/decoder instances via try/finally. It also adds overflow-checked buffer size computations across libjpeg-turbo 8-bit, openjpeg, and openjphjs decoders, plus new tests and Vitest configs.

Changes

12-bit JPEG codec enablement

Layer / File(s) Summary
codecFactory cleanup guarantees
packages/dicom-codec/src/codecs/codecFactory.js, packages/dicom-codec/test/dispatch.test.js
encode()/decode() move delete() calls into finally blocks so instances are cleaned up even on error; a new test verifies cleanup on decode failure.
Dispatcher wiring for 12-bit codec
packages/dicom-codec/src/codecs/libjpegTurbo12bit.js
Wires real decode via codecFactory.runProcess/decode, delegates getPixelData, keeps encode unsupported.
Grayscale 12-bit decode path
packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp
Switches decode to JCS_GRAYSCALE, updates frame info, adds overflow-checked/bounded buffer sizing, removes dead code.
Package config, integration and unit tests
package.json, packages/libjpeg-turbo-12bit/package.json, packages/libjpeg-turbo-12bit/vitest.config.mjs, packages/dicom-codec/test/integration.test.js, packages/libjpeg-turbo-12bit/test/decode.test.js
Adds Vitest scripts/config, updates package exports/main, gates integration test on 12-bit build artifact and adds a 12-bit decode test case, and adds a dedicated decoder test suite.

Decoder buffer-size overflow hardening

Layer / File(s) Summary
libjpeg-turbo 8-bit checked sizing
packages/libjpeg-turbo-8bit/src/JPEGDecoder.hpp, packages/libjpeg-turbo-8bit/test/decode.test.js
Adds checkedDecodedSize helper used in decode(); tests gate on build artifact existence.
OpenJPEG bounds checks and hardening
packages/openjpeg/src/BufferStream.hpp, packages/openjpeg/src/J2KDecoder.hpp, packages/openjpeg/src/J2KEncoder.hpp, packages/openjpeg/test/decode.test.js
Adds write/skip/seek bounds checks, decoded-size validation, component-count checks, encoder preallocation headroom, and robustness tests.
OpenJPH checked sizing
packages/openjphjs/src/HTJ2KDecoder.hpp
Adds checkedDecodedSize helper applied before resizing the decoded buffer.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Dispatcher
  participant codecFactory
  participant JPEGDecoder
  Dispatcher->>codecFactory: decode(imageFrame, imageInfo)
  codecFactory->>JPEGDecoder: decode() (via runProcess)
  JPEGDecoder->>JPEGDecoder: checkedDecodedSize / grayscale 12-bit decode
  alt decode succeeds
    JPEGDecoder-->>codecFactory: decoded buffer
    codecFactory->>JPEGDecoder: delete() (finally)
    codecFactory-->>Dispatcher: imageFrame, imageInfo, duration
  else decode throws
    JPEGDecoder-->>codecFactory: error
    codecFactory->>JPEGDecoder: delete() (finally)
    codecFactory-->>Dispatcher: rethrow error
  end
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: 12-bit codec wiring, safer buffer sizing, cleanup on error, and test gating.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codspeed-hq

codspeed-hq Bot commented Jul 7, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 29 untouched benchmarks
⏩ 19 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
decode jpeg400jfif.jpg (600x800x8bit) — warm 10 ms 10.9 ms -8.44%
encode CT1.RAW (HTJ2K lossless) — cold 36.7 ms 34.5 ms +6.21%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing fixes (3179998) with main (04c3e87)

Open in CodSpeed

Footnotes

  1. 19 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/libjpeg-turbo-8bit/src/JPEGDecoder.hpp (1)

118-141: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

tjInstance leaks if checkedDecodedSize throws.

checkedDecodedSize(...) at line 131 can throw std::runtime_error for an oversized/zero-sized frame, but tjInstance (created at line 120) is only destroyed on the tjDecompress2 failure path and at normal completion — not on this new throw path. Since this validation is exercised precisely on attacker-controlled/malformed image dimensions, this introduces a leak of the native decompressor handle on every such input.

🐛 Proposed fix
     int pixelFormat = (frameInfo_.componentCount == 1) ? TJPF_GRAY : TJPF_RGB;
 
-    const size_t destinationSize = checkedDecodedSize(frameInfo_.width, frameInfo_.height, 1, tjPixelSize[pixelFormat]);
+    size_t destinationSize;
+    try {
+      destinationSize = checkedDecodedSize(frameInfo_.width, frameInfo_.height, 1, tjPixelSize[pixelFormat]);
+    } catch (...) {
+      tjDestroy(tjInstance);
+      throw;
+    }
     decoded_.resize(destinationSize);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/libjpeg-turbo-8bit/src/JPEGDecoder.hpp` around lines 118 - 141,
`decode()` leaks the native `tjInstance` if `checkedDecodedSize(...)` throws
before the explicit `tjDestroy` calls. Update `JPEGDecoder::decode` to ensure
the decompressor handle is always released on every exit path, including
exceptions from `checkedDecodedSize`, by using an RAII guard or equivalent
cleanup tied to `tjInitDecompress()`/`tjDestroy()`. Keep the existing error
handling in `readHeader_i` and `tjDecompress2`, but make the cleanup automatic
so malformed image dimensions cannot leak the handle.
packages/openjpeg/src/J2KDecoder.hpp (1)

862-867: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

checkedDecodedSize throw leaks l_stream, l_codec, and image.

If the computed destination size is zero or exceeds the 512 MiB cap (attacker-influenced via decomposition-level/dimension fields), the exception unwinds past l_stream/l_codec/image without any of the three being released, unlike the other error paths in this function.

🐛 Proposed fix
       const size_t bytesPerPixel = (frameInfo_.bitsPerSample + 8 - 1) / 8;
-      const size_t destinationSize = checkedDecodedSize(sizeAtDecompositionLevel.width, sizeAtDecompositionLevel.height, frameInfo_.componentCount, bytesPerPixel);
+      size_t destinationSize;
+      try {
+        destinationSize = checkedDecodedSize(sizeAtDecompositionLevel.width, sizeAtDecompositionLevel.height, frameInfo_.componentCount, bytesPerPixel);
+      } catch (...) {
+        opj_stream_destroy(l_stream);
+        opj_destroy_codec(l_codec);
+        opj_image_destroy(image);
+        throw;
+      }
       decoded_.resize(destinationSize);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/openjpeg/src/J2KDecoder.hpp` around lines 862 - 867, The
destination-size computation in J2KDecoder’s decode path can throw from
checkedDecodedSize before l_stream, l_codec, and image are released, causing a
leak on invalid or oversized dimensions. Update the cleanup flow around the
sizeAtDecompositionLevel/decoded_.resize block so any exception from
checkedDecodedSize or later allocation still frees those resources, matching the
other error paths in this function; use the existing cleanup logic for l_stream,
l_codec, and image rather than letting the exception escape first.
packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp (1)

115-172: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Install a non-terminating libjpeg error handler
jpeg_std_error keeps libjpeg’s default error_exit, which aborts the runtime on fatal JPEG errors. Add a custom error manager with setjmp/longjmp so bad input can unwind as an exception instead of terminating the whole process/WASM instance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp` around lines 115 - 172, The
JPEGDecoder::decode path still uses jpeg_std_error with libjpeg’s default fatal
error_exit, so malformed input can terminate the process instead of unwinding
safely. Add a custom error manager for JPEGDecoder that installs a
non-terminating handler and uses setjmp/longjmp around jpeg_create_decompress,
jpeg_read_header, and jpeg_start_decompress so libjpeg errors are converted into
a C++ exception. Keep the existing cleanup via jpeg_destroy_decompress in the
error path and preserve the current buffer-size checks and grayscale decoding
setup.
🧹 Nitpick comments (3)
packages/dicom-codec/test/dispatch.test.js (1)

40-79: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a symmetric encoder cleanup test.

This test covers decode() cleanup on throw, but encode() got the same try/finally treatment in codecFactory.js and has no equivalent coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dicom-codec/test/dispatch.test.js` around lines 40 - 79, Add a
matching cleanup test for the encode path in codecFactory by extending the
existing codecFactory instance cleanup coverage: create a FakeEncoder with
encode() throwing and delete() setting a flag, then call codecFactory.encode
with a similar context/codecConfig setup and assert the thrown error is
preserved and delete() still runs. Place the new test alongside the current
decode() cleanup test so both try/finally paths in codecFactory are covered
symmetrically.
packages/libjpeg-turbo-12bit/test/decode.test.js (2)

9-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fixture read isn't gated, so a missing fixture crashes the whole file instead of skipping.

ct12bit is read at module scope for both build variants, unlike the isBuilt dist-artifact gating. If the fixture file is ever absent, every test (including build-variant-skipped ones) fails hard rather than skipping gracefully, which runs counter to this PR's stated test-infra goal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/libjpeg-turbo-12bit/test/decode.test.js` around lines 9 - 11, The
fixture load for ct12bit is happening at module scope, so a missing JPEG file
crashes the entire test file before any build-variant gating can skip tests.
Move the readFileSync/resolve lookup into the test setup or guard it with the
same conditional used by isBuilt so decode.test.js can skip gracefully when the
fixture is absent. Keep the change localized around ct12bit and the existing
built-vs-skipped test paths.

34-55: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Wrap decoder in try/finally to avoid leaking the WASM instance on assertion failure.

decoder.delete() only runs if every preceding expect() passes. A failing assertion leaves the WASM decoder instance undeleted for the rest of the test run.

♻️ Proposed fix
   it.skipIf(!isBuilt)(
     "decodes the CT-512x512 12-bit fixture and reports correct dimensions/format",
     () => {
       const decoder = new codec.JPEGDecoder()
-      const encodedBuffer = decoder.getEncodedBuffer(ct12bit.length)
-      encodedBuffer.set(ct12bit)
-
-      decoder.decode()
-
-      const frameInfo = decoder.getFrameInfo()
-      expect(frameInfo.width).toBe(512)
-      expect(frameInfo.height).toBe(512)
-      expect(frameInfo.bitsPerSample).toBe(12)
-      expect(frameInfo.componentCount).toBe(1)
-
-      const decoded = decoder.getDecodedBuffer()
-      // One 16-bit-wide sample per pixel (grayscale, 1 component/pixel).
-      expect(decoded.length).toBe(512 * 512)
-
-      decoder.delete()
+      try {
+        const encodedBuffer = decoder.getEncodedBuffer(ct12bit.length)
+        encodedBuffer.set(ct12bit)
+
+        decoder.decode()
+
+        const frameInfo = decoder.getFrameInfo()
+        expect(frameInfo.width).toBe(512)
+        expect(frameInfo.height).toBe(512)
+        expect(frameInfo.bitsPerSample).toBe(12)
+        expect(frameInfo.componentCount).toBe(1)
+
+        const decoded = decoder.getDecodedBuffer()
+        expect(decoded.length).toBe(512 * 512)
+      } finally {
+        decoder.delete()
+      }
     }
   )

Same pattern applies to the truncated-input test at Lines 68-77.

Also applies to: 68-77

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/libjpeg-turbo-12bit/test/decode.test.js` around lines 34 - 55, The
JPEGDecoder test cases in decode.test.js are leaking the WASM instance because
decoder.delete() is only reached after all expectations pass. Update the test
bodies that use codec.JPEGDecoder to wrap decode/getFrameInfo/getDecodedBuffer
assertions in a try/finally block so decoder.delete() always runs, including the
CT-512x512 case and the truncated-input test mentioned in the comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp`:
- Around line 168-172: The decoded buffer in getDecodedBuffer() is being exposed
through Uint8ClampedArray.new_(), which clamps away 12-bit sample values before
codecFactory.js can reconstruct them. Update the getDecodedBuffer() path in
JPEGDecoder.hpp to return the raw typed_memory_view(...) or another 16-bit typed
array so the original sample data is preserved; use the existing decoded_ buffer
and stride/output_size logic as the place to adjust the return type.

In `@packages/openjpeg/src/J2KDecoder.hpp`:
- Around line 835-841: The component-count validation in decode_i leaks native
OpenJPEG resources because the new unsupported count throw path only destroys
image and skips releasing l_codec and l_stream. Update this branch to mirror the
other failure paths in J2KDecoder::decode_i by cleaning up all allocated handles
before throwing, using the same teardown pattern already present for earlier
error cases.

---

Outside diff comments:
In `@packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp`:
- Around line 115-172: The JPEGDecoder::decode path still uses jpeg_std_error
with libjpeg’s default fatal error_exit, so malformed input can terminate the
process instead of unwinding safely. Add a custom error manager for JPEGDecoder
that installs a non-terminating handler and uses setjmp/longjmp around
jpeg_create_decompress, jpeg_read_header, and jpeg_start_decompress so libjpeg
errors are converted into a C++ exception. Keep the existing cleanup via
jpeg_destroy_decompress in the error path and preserve the current buffer-size
checks and grayscale decoding setup.

In `@packages/libjpeg-turbo-8bit/src/JPEGDecoder.hpp`:
- Around line 118-141: `decode()` leaks the native `tjInstance` if
`checkedDecodedSize(...)` throws before the explicit `tjDestroy` calls. Update
`JPEGDecoder::decode` to ensure the decompressor handle is always released on
every exit path, including exceptions from `checkedDecodedSize`, by using an
RAII guard or equivalent cleanup tied to `tjInitDecompress()`/`tjDestroy()`.
Keep the existing error handling in `readHeader_i` and `tjDecompress2`, but make
the cleanup automatic so malformed image dimensions cannot leak the handle.

In `@packages/openjpeg/src/J2KDecoder.hpp`:
- Around line 862-867: The destination-size computation in J2KDecoder’s decode
path can throw from checkedDecodedSize before l_stream, l_codec, and image are
released, causing a leak on invalid or oversized dimensions. Update the cleanup
flow around the sizeAtDecompositionLevel/decoded_.resize block so any exception
from checkedDecodedSize or later allocation still frees those resources,
matching the other error paths in this function; use the existing cleanup logic
for l_stream, l_codec, and image rather than letting the exception escape first.

---

Nitpick comments:
In `@packages/dicom-codec/test/dispatch.test.js`:
- Around line 40-79: Add a matching cleanup test for the encode path in
codecFactory by extending the existing codecFactory instance cleanup coverage:
create a FakeEncoder with encode() throwing and delete() setting a flag, then
call codecFactory.encode with a similar context/codecConfig setup and assert the
thrown error is preserved and delete() still runs. Place the new test alongside
the current decode() cleanup test so both try/finally paths in codecFactory are
covered symmetrically.

In `@packages/libjpeg-turbo-12bit/test/decode.test.js`:
- Around line 9-11: The fixture load for ct12bit is happening at module scope,
so a missing JPEG file crashes the entire test file before any build-variant
gating can skip tests. Move the readFileSync/resolve lookup into the test setup
or guard it with the same conditional used by isBuilt so decode.test.js can skip
gracefully when the fixture is absent. Keep the change localized around ct12bit
and the existing built-vs-skipped test paths.
- Around line 34-55: The JPEGDecoder test cases in decode.test.js are leaking
the WASM instance because decoder.delete() is only reached after all
expectations pass. Update the test bodies that use codec.JPEGDecoder to wrap
decode/getFrameInfo/getDecodedBuffer assertions in a try/finally block so
decoder.delete() always runs, including the CT-512x512 case and the
truncated-input test mentioned in the comment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a85cc5ec-e4ee-4c59-9e98-dbf54b33e9af

📥 Commits

Reviewing files that changed from the base of the PR and between 04c3e87 and 0f609bd.

📒 Files selected for processing (16)
  • package.json
  • packages/dicom-codec/src/codecs/codecFactory.js
  • packages/dicom-codec/src/codecs/libjpegTurbo12bit.js
  • packages/dicom-codec/test/dispatch.test.js
  • packages/dicom-codec/test/integration.test.js
  • packages/libjpeg-turbo-12bit/package.json
  • packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp
  • packages/libjpeg-turbo-12bit/test/decode.test.js
  • packages/libjpeg-turbo-12bit/vitest.config.mjs
  • packages/libjpeg-turbo-8bit/src/JPEGDecoder.hpp
  • packages/libjpeg-turbo-8bit/test/decode.test.js
  • packages/openjpeg/src/BufferStream.hpp
  • packages/openjpeg/src/J2KDecoder.hpp
  • packages/openjpeg/src/J2KEncoder.hpp
  • packages/openjpeg/test/decode.test.js
  • packages/openjphjs/src/HTJ2KDecoder.hpp

Comment thread packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp
Comment thread packages/openjpeg/src/J2KDecoder.hpp
@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

sedghi added a commit that referenced this pull request Jul 8, 2026
…es), not main

The previous split commit reverted sources to main, but this PR is
stacked on the fixes branch (#71) which already carries the first round
of codec fixes — so the diff showed reversions of #71's work
(codecFactory, 12-bit decoder, openjpeg guards, dispatcher wiring).

Sources are now pinned to the fixes tree (zero src delta in this PR)
and the classification was re-run empirically against wasm rebuilt from
the fixes branch's C++: 17 tests depend on fixes this branch itself
introduced and move to the follow-up PR; everything that #71's fixes
already satisfy stays here, including the 12-bit decode suite, the
dispatcher 12-bit integration test, the codecFactory cleanup test, the
openjpeg small-buffer guard tests and the 12-bit browser-smoke variants.

Moved out (fail without this branch's own fixes):
- big-endian 1-bit/32-bit tests, little-endian 32-bit typing/realign
- dicom-codec planar RLE and both J2K encode round-trips
- openjpeg encoder-failure throw and encode/delete heap stability
- openjphjs 12-bit encoder round-trip
- libjpeg-turbo-12bit multi-component rejection + bench

Full workspace green with CI=1 (177/177); dist-size and browser smoke
pass against dists built from the fixes branch's C++.
sedghi added a commit that referenced this pull request Jul 8, 2026
Second-round fixes found by the pixel-correctness suite (#72), split out
so that PR stays a pure test/CI change. First-round fixes (12-bit decode
path, codecFactory instance cleanup, openjpeg decoder guards and
BufferStream bounds) are already in #71 — this PR carries only what the
test branch itself introduced, each fix together with the tests that
fail without it (verified against wasm rebuilt from the fixes branch's
C++ with CI's emsdk 3.1.74 image):

- openjpeg: J2KEncoder throws on setup/compress failure instead of
  silently returning, with the encoded buffer zeroed (callers previously
  read back a garbage pre-sized allocation as a successful encode);
  frees codec/stream/image on every exit path (repeated encodes grew the
  wasm heap monotonically); sizes the output buffer with headroom so
  clamped writes surface as errors instead of truncation. Pinned by the
  encoder-failure-throw tests, the encode/delete heap-stability test,
  and both dicom-codec J2K round-trips (encode .90 and transcode
  .80->.90), which fail byte-exactness without this rework.
- openjphjs: HTJ2KEncoder rounds bytesPerPixel UP; bitsPerSample/8
  truncated to 1 for 9..15-bit samples, halving the row stride so every
  row after the first was read from the wrong offset (12-bit encodes
  corrupted). Pinned by the 12-bit encoder round-trip.
- libjpeg-turbo-12bit: fail closed on multi-component input — forcing
  JCS_GRAYSCALE on a color 12-bit JPEG silently discards chroma and
  reports componentCount=1. Pinned by the multi-component rejection
  test; also adds the CodSpeed bench and its package script.
- dicom-codec: adaptImageInfo preserves planarConfiguration
  (decode8Planar was unreachable; PlanarConfiguration=1 RLE silently
  produced interleaved output). Pinned by the planar RLE test.
- little-endian/big-endian: 32-bit pixel data decodes to
  Uint32Array/Int32Array per pixelRepresentation with Float32Array only
  as the no-pixelRepresentation fallback (review feedback from
  wayfarer3130, matching cornerstone3D's decodeLittleEndian); 32-bit
  views realign to 4-byte boundaries (the old offset % 2 check threw a
  RangeError at offset % 4 == 2); big-endian gains 1-bit passthrough
  and byte-swapped 32-bit support. Same typing fix applied to
  dicom-codec's littleEndian getPixelData.
sedghi added a commit that referenced this pull request Jul 8, 2026
Reverts the fixes-branch-relative classification: #71 is being closed in
favor of a single consolidated fixes PR stacked on this one, and this PR
retargets to main. With no fix PR below it, this branch must hold only
tests that pass against plain main sources — which is exactly the state
this restores (verified earlier against wasm rebuilt from main's C++
with CI's emsdk 3.1.74 image: 164/164 with CI=1, dist-size gate and
browser smoke green).
sedghi added a commit that referenced this pull request Jul 8, 2026
All source fixes for the codec packages in one PR, stacked on the
pixel-correctness test PR (#72): the first round formerly on the fixes
branch (#71, closed in favor of this) plus the second round found by
the new test suite. Each fix travels with the tests that fail without
it — classification was empirical, running the full workspace against
wasm rebuilt from unfixed C++ with CI's emsdk 3.1.74 image.

First round (formerly #71):
- libjpeg-turbo-12bit: decode as single-component grayscale into 16-bit
  output. Forcing JCS_EXT_RGBA sized the buffer for 1 sample/pixel
  while libjpeg wrote 4 (heap overflow), and Uint8ClampedArray
  flattened 12-bit samples to 255. Fix the package entry points
  (dist/libjpegturbo12js.js) so the package is requireable at all, and
  wire the decoder into dicom-codec's dispatcher (.51), which
  previously threw 'Decoder not found'.
- openjpeg: decoder rejects <4-byte input and unsupported component
  counts, frees handles on the rejection path; BufferStream
  write/skip/seek callbacks are bounds-checked.
- dicom-codec: codecFactory reads results before delete() and frees
  decoder/encoder instances in finally, so failures no longer leak
  wasm instances.
- overflow-checked decoded-buffer sizing on wasm32 (openjpeg,
  openjphjs, libjpeg-turbo-8bit).

Second round (found by the pixel-correctness suite):
- openjpeg: J2KEncoder throws on setup/compress failure instead of
  silently returning a garbage pre-sized buffer, frees
  codec/stream/image on every exit path (repeated encodes grew the
  wasm heap monotonically), and sizes the output buffer with headroom.
- openjphjs: HTJ2KEncoder rounds bytesPerPixel UP; bitsPerSample/8
  truncated to 1 for 9..15-bit samples, halving the row stride and
  corrupting every row after the first in 12-bit encodes.
- libjpeg-turbo-12bit: fail closed on multi-component input instead of
  silently discarding chroma; add the CodSpeed bench.
- dicom-codec: adaptImageInfo preserves planarConfiguration
  (decode8Planar was unreachable; PlanarConfiguration=1 RLE silently
  produced interleaved output).
- little-endian/big-endian: 32-bit pixel data decodes to
  Uint32Array/Int32Array per pixelRepresentation with Float32Array
  only as the no-pixelRepresentation fallback (review feedback from
  wayfarer3130, matching cornerstone3D's decodeLittleEndian); 32-bit
  views realign to 4-byte boundaries; big-endian gains 1-bit
  passthrough and byte-swapped 32-bit support. Same typing fix applied
  to dicom-codec's littleEndian getPixelData.
@sedghi

sedghi commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

Closing in favor of #73: every fix on this branch (12-bit decode path and package entry points, dispatcher wiring, codecFactory instance cleanup, openjpeg decoder guards and BufferStream bounds, wasm32 size-overflow checks) now lives there, consolidated with the second-round fixes found by the pixel-correctness suite, each paired with the tests that fail without it. Merge order: #72 (tests only, targets main) first, then #73.

@sedghi sedghi closed this Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant