Skip to content

chore(libjpeg-turbo): update submodule to upstream 3.2.0 (8-bit + 12-bit)#79

Open
sedghi wants to merge 3 commits into
mainfrom
chore/libjpeg-turbo-submodule-3.2.0
Open

chore(libjpeg-turbo): update submodule to upstream 3.2.0 (8-bit + 12-bit)#79
sedghi wants to merge 3 commits into
mainfrom
chore/libjpeg-turbo-submodule-3.2.0

Conversation

@sedghi

@sedghi sedghi commented Jul 9, 2026

Copy link
Copy Markdown
Member

Fourth and final codec submodule upgrade (after openjph #76, charls #77, openjpeg #78).

Bumps the shared extern/libjpeg-turbo submodule (both libjpeg-turbo-8bit and libjpeg-turbo-12bit) from dc4a93f (2.1.4-era, Dec 2022) to upstream 3.2.0 (2026-06-30). Fork PR: cornerstonejs/libjpeg-turbo#1.

  • No custom patches — clean version advance.
  • Major jump (2.x → 3.x) — the riskiest of the four. libjpeg-turbo 3.x unified 8/12/16-bit precision support, so I expect API/build drift against our glue (esp. the 12-bit path and its WITH_12BIT=1 flag). I'll iterate CI to green.
  • Also noted: libjpeg-turbo-12bit/build.sh forces CMAKE_BUILD_TYPE=Debug (same bug openjph had) — will address once it builds.

Not built locally; iterating on CI. Not for merge.

Summary by CodeRabbit

  • New Features

    • Improved JPEG decoding support for 12-bit grayscale images.
    • Updated the build process to use a two-stage libjpeg-turbo setup for better compatibility.
  • Bug Fixes

    • Added safeguards so configuration fails early if required build settings are missing.
    • Updated include and linking setup to match the newer libjpeg-turbo layout.

…am 3.2.0

Advances both the 8-bit and 12-bit packages' shared submodule from dc4a93f
(2.1.4-era, Dec 2022) to upstream 3.2.0 (2026-06-30). No custom fork patches
(clean version advance). Fork PR: cornerstonejs/libjpeg-turbo#1.

Major-version jump (2.x -> 3.x): CI is the first build of 3.2.0 against our
8-bit and 12-bit glue; API drift (incl. 3.x's unified precision handling vs
the old WITH_12BIT flag) is expected and will be iterated.
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Both libjpeg-turbo-12bit and libjpeg-turbo-8bit packages switch from in-tree submodule builds to a two-stage standalone build process using LIBJPEG_TURBO_BUILD_DIR, linking pre-built static libraries via imported CMake targets. The 12-bit decoder is rewritten to validate and decode only single-component 12-bit grayscale JPEGs. Submodule pointers are updated.

Changes

libjpeg-turbo-12bit standalone build and decoder update

Layer / File(s) Summary
Standalone build configuration and submodule update
packages/libjpeg-turbo-12bit/CMakeLists.txt, packages/libjpeg-turbo-12bit/build.sh, packages/libjpeg-turbo-12bit/src/CMakeLists.txt, packages/libjpeg-turbo-12bit/extern/libjpeg-turbo
CMake now requires LIBJPEG_TURBO_BUILD_DIR under Emscripten and fails otherwise; build.sh builds libjpeg-turbo 3.x standalone (static, no SIMD/SPNG) then configures the wrapper against it; src/CMakeLists.txt updates include paths and adds an imported jpeg-static target pointing to libjpeg.a; the extern submodule pointer is bumped.
12-bit grayscale decoder validation and decoding
packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp
Decode logic now validates single-component 12-bit precision input, throws std::runtime_error otherwise, forces grayscale output, adds dimension/buffer overflow checks, and decodes scanlines using the 12-bit libjpeg API instead of the prior 8-bit RGBA path.

libjpeg-turbo-8bit standalone build

Layer / File(s) Summary
Standalone build configuration and submodule update
packages/libjpeg-turbo-8bit/CMakeLists.txt, packages/libjpeg-turbo-8bit/build.sh, packages/libjpeg-turbo-8bit/src/CMakeLists.txt, packages/libjpeg-turbo-8bit/extern/libjpeg-turbo
CMake requires LIBJPEG_TURBO_BUILD_DIR under Emscripten with a fatal error otherwise; build.sh builds libjpeg-turbo 3.x standalone with TurboJPEG enabled then configures the wrapper against it; src/CMakeLists.txt updates include paths and adds an imported turbojpeg-static target pointing to libturbojpeg.a; the extern submodule pointer is bumped.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 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 clearly states the main change: updating the libjpeg-turbo submodule for both 8-bit and 12-bit packages.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/libjpeg-turbo-submodule-3.2.0

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.

libjpeg-turbo 3.x forbids add_subdirectory() integration, so build it
standalone (its own emscripten cmake) and link the produced libturbojpeg.a as
an IMPORTED target. Handles 3.x layout changes: headers moved under src/,
disable the new SPNG/ZLIB dep (WITH_SPNG=0). No glue changes — the legacy
TurboJPEG API our wrapper uses (tjInitDecompress/tjDecompress2/...) is still
present in 3.2.0. First blind cut; iterating on CI. 12-bit rework to follow.
@codspeed-hq

codspeed-hq Bot commented Jul 9, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 7.18%

⚡ 1 improved benchmark
✅ 11 untouched benchmarks
⏩ 107 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime instantiate+destroy JPEGDecoder x50 152.2 µs 142 µs +7.18%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing chore/libjpeg-turbo-submodule-3.2.0 (7c0c49a) with main (a88a461)2

Open in CodSpeed

Footnotes

  1. 107 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.

  2. No successful run was found on main (9c086c9) during the generation of this report, so a88a461 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

…recision API)

3.x forbids add_subdirectory() and removed WITH_12BIT (one build is now
multi-precision). Build libjpeg-turbo standalone and link libjpeg.a as an
IMPORTED target (two-phase build.sh), and rewrite the decoder for 3.x:
- decode grayscale 12-bit via jpeg12_read_scanlines + J12SAMPARRAY (the 3.x
  per-precision API) instead of jpeg_read_scanlines (the old WITH_12BIT model)
- guard on num_components==1 and data_precision==12; overflow-checked sizing
- correct single-component int16 output (no JCS_EXT_RGBA overflow)

3.x headers moved under src/. No dependency on #73 (left untouched); the
decode-correctness fix here mirrors #73's grayscale logic but on the 3.x API.

@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: 6

🧹 Nitpick comments (2)
packages/libjpeg-turbo-8bit/CMakeLists.txt (1)

38-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider validating that LIBJPEG_TURBO_BUILD_DIR actually exists.

The guard checks NOT DEFINED but not whether the directory exists or contains the expected libturbojpeg.a. A stale or empty directory would pass configuration and fail later at link time with a less clear error. Adding an existence check (e.g., NOT IS_DIRECTORY or NOT EXISTS "${LIBJPEG_TURBO_BUILD_DIR}/libturbojpeg.a") would surface the problem early.

This is low-priority since build.sh is the expected entry point and it creates the directory, but it would help when invoking cmake manually during debugging.

🤖 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/CMakeLists.txt` around lines 38 - 39, Add an
existence validation for LIBJPEG_TURBO_BUILD_DIR in the CMake guard so
configuration fails early if the path is stale, empty, or missing the expected
libturbojpeg.a. Update the existing EMSCRIPTEN check in CMakeLists.txt to verify
the directory/file before proceeding, and keep the fatal error message in the
same guard so manual cmake invocations surface a clear setup issue. Use the
LIBJPEG_TURBO_BUILD_DIR check as the main entry point for locating the fix.
packages/libjpeg-turbo-8bit/build.sh (1)

16-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop jpeg-static from packages/libjpeg-turbo-8bit/build.sh. The wrapper only links turbojpeg-static, so this extra target just adds build time.

🤖 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/build.sh` around lines 16 - 19, Remove the
unnecessary jpeg-static target from the libjpeg-turbo build step in build.sh,
since the wrapper only depends on turbojpeg-static. Update the emmake make
invocation in the libjpeg-turbo build block to build only the turbojpeg-static
target and keep the rest of the build flow unchanged.
🤖 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/build.sh`:
- Around line 18-19: The libjpeg-turbo build step in build.sh is not checked for
failure, so a failed emmake make can be masked and later surface as a misleading
imported library error. Add explicit error handling immediately after the build
stage in the script that runs the build-libjpeg/emmake make command so the
script exits with a clear libjpeg-turbo build failed message before continuing
to the wrapper CMake flow.
- Around line 13-19: The build configuration in build.sh is using the wrong
CMake flag for spng support; update the libjpeg-turbo cmake invocation in the
build-libjpeg setup to use WITH_SYSTEM_SPNG instead of WITH_SPNG. Keep the
existing jpeg-static target and libjpeg.a output path unchanged, and only adjust
the flag in the emcmake cmake command so the libjpeg-turbo 3.x build is
configured correctly.

In `@packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp`:
- Around line 147-150: The 12-bit grayscale metadata setup in JPEGDecoder should
also initialize frameInfo_.isSigned, since JPEGDecoder’s constructor does not
set it and downstream consumers read this field. Update the frameInfo_
population block where width, height, bitsPerSample, and componentCount are
assigned so isSigned is explicitly set for this path, keeping the FrameInfo
state fully defined.
- Around line 170-179: `getDecodedBuffer()` is still exposing the decoded 12-bit
samples as byte-sized data, which truncates values above 255. Update the
JS-facing buffer wrapping to use a 16-bit typed view (`Int16Array` or
`Uint16Array`) over `decoded_` so the `J12SAMPROW`/`J12SAMPLE` data from
`JPEGDecoder` is preserved end-to-end. Keep the change aligned with the
`decoded_` storage type and the decoding path in `jpeg12_read_scanlines`.
- Around line 123-145: The JPEGDecoder path still uses the default libjpeg error
handling, so fatal decode failures can abort the WASM instance instead of
surfacing as exceptions. In JPEGDecoder.hpp, add a custom jpeg_error_mgr for the
decode flow around jpeg_std_error and jpeg_create_decompress, wire in
setjmp/longjmp before any libjpeg calls that can fail, and convert libjpeg’s
reported message into a C++ exception in the same decode path that already
performs the component and precision checks.

In `@packages/libjpeg-turbo-8bit/build.sh`:
- Around line 3-5: The build script currently disables exit-on-error with set
+e, which lets later stages continue after an earlier failure and hides the real
root cause. Update build.sh so each stage (especially the libjpeg-turbo
configure/make work and the later packaging step) either runs under set -e or
explicitly checks command exit codes before proceeding, using the existing build
stage flow to stop immediately on failure and preserve the original error.

---

Nitpick comments:
In `@packages/libjpeg-turbo-8bit/build.sh`:
- Around line 16-19: Remove the unnecessary jpeg-static target from the
libjpeg-turbo build step in build.sh, since the wrapper only depends on
turbojpeg-static. Update the emmake make invocation in the libjpeg-turbo build
block to build only the turbojpeg-static target and keep the rest of the build
flow unchanged.

In `@packages/libjpeg-turbo-8bit/CMakeLists.txt`:
- Around line 38-39: Add an existence validation for LIBJPEG_TURBO_BUILD_DIR in
the CMake guard so configuration fails early if the path is stale, empty, or
missing the expected libturbojpeg.a. Update the existing EMSCRIPTEN check in
CMakeLists.txt to verify the directory/file before proceeding, and keep the
fatal error message in the same guard so manual cmake invocations surface a
clear setup issue. Use the LIBJPEG_TURBO_BUILD_DIR check as the main entry point
for locating the fix.
🪄 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: 6d518ffc-ca76-4d47-b5cf-8dbb283192ff

📥 Commits

Reviewing files that changed from the base of the PR and between 9c086c9 and 7c0c49a.

📒 Files selected for processing (9)
  • packages/libjpeg-turbo-12bit/CMakeLists.txt
  • packages/libjpeg-turbo-12bit/build.sh
  • packages/libjpeg-turbo-12bit/extern/libjpeg-turbo
  • packages/libjpeg-turbo-12bit/src/CMakeLists.txt
  • packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp
  • packages/libjpeg-turbo-8bit/CMakeLists.txt
  • packages/libjpeg-turbo-8bit/build.sh
  • packages/libjpeg-turbo-8bit/extern/libjpeg-turbo
  • packages/libjpeg-turbo-8bit/src/CMakeLists.txt

Comment on lines +13 to +19
(cd build-libjpeg && emcmake cmake -G"Unix Makefiles" \
-DCMAKE_BUILD_TYPE=Release \
-DENABLE_SHARED=0 -DENABLE_STATIC=1 \
-DWITH_SIMD=0 -DWITH_SPNG=0 \
../extern/libjpeg-turbo)
echo "~~~ MAKE libjpeg-turbo ~~~"
(cd build-libjpeg && emmake make VERBOSE=1 -j 16 jpeg-static)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify libjpeg-turbo 3.x CMake configuration: target names, options, and library output path.

SUBMODULE="packages/libjpeg-turbo-12bit/extern/libjpeg-turbo"

echo "=== Checking jpeg-static target definition ==="
rg -n 'add_library.*jpeg-static' "$SUBMODULE" --type cmake

echo "=== Checking CMAKE_ARCHIVE_OUTPUT_DIRECTORY ==="
rg -n 'CMAKE_ARCHIVE_OUTPUT_DIRECTORY' "$SUBMODULE" --type cmake

echo "=== Checking add_library location (root vs src/) ==="
rg -n 'add_library.*jpeg' "$SUBMODULE/CMakeLists.txt" 2>/dev/null
rg -n 'add_library.*jpeg' "$SUBMODULE/src/CMakeLists.txt" 2>/dev/null

echo "=== Checking WITH_SPNG option ==="
rg -n 'WITH_SPNG\|WITH_PNG' "$SUBMODULE" --type cmake

echo "=== Checking ENABLE_SHARED/ENABLE_STATIC options ==="
rg -n 'ENABLE_SHARED\|ENABLE_STATIC' "$SUBMODULE" --type cmake

echo "=== Checking jpeg12_ API symbols ==="
rg -n 'jpeg12_read_scanlines\|jpeg12_read_header' "$SUBMODULE" --type c --type cmake

Repository: cornerstonejs/codecs

Length of output: 430


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Files of interest ==="
git ls-files | rg 'packages/libjpeg-turbo-12bit/(build\.sh|src/CMakeLists\.txt|extern/libjpeg-turbo/.*CMakeLists\.txt|extern/libjpeg-turbo/.*cmake|extern/libjpeg-turbo/.*/CMakeLists\.txt)$'

echo "=== build.sh ==="
cat -n packages/libjpeg-turbo-12bit/build.sh

echo "=== top-level vendored CMake files (small only) ==="
for f in \
  packages/libjpeg-turbo-12bit/extern/libjpeg-turbo/CMakeLists.txt \
  packages/libjpeg-turbo-12bit/extern/libjpeg-turbo/src/CMakeLists.txt
do
  if [ -f "$f" ]; then
    echo "--- $f ($(wc -l < "$f") lines) ---"
    sed -n '1,260p' "$f"
  fi
done

echo "=== Search for relevant symbols/options/targets ==="
rg -n 'jpeg-static|WITH_SPNG|WITH_PNG|ENABLE_SHARED|ENABLE_STATIC|CMAKE_ARCHIVE_OUTPUT_DIRECTORY|add_library|OUTPUT_NAME|ARCHIVE_OUTPUT_DIRECTORY' \
  packages/libjpeg-turbo-12bit/extern/libjpeg-turbo

Repository: cornerstonejs/codecs

Length of output: 2052


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== packages/libjpeg-turbo-12bit/src/CMakeLists.txt ==="
cat -n packages/libjpeg-turbo-12bit/src/CMakeLists.txt

echo "=== references to libjpeg-turbo build dir / libjpeg.a / jpeg-static in wrapper files ==="
rg -n 'LIBJPEG_TURBO_BUILD_DIR|libjpeg\.a|jpeg-static|WITH_SPNG|WITH_PNG|ENABLE_SHARED|ENABLE_STATIC' \
  packages/libjpeg-turbo-12bit

Repository: cornerstonejs/codecs

Length of output: 5473


🌐 Web query:

libjpeg-turbo 3.x CMake WITH_SPNG jpeg-static libjpeg.a output directory target name

💡 Result:

In libjpeg-turbo 3.x, the CMake build system provides specific configurations and targets for handling static libraries and external dependencies like libspng. WITH_SPNG Option The build system uses the WITH_SYSTEM_SPNG option to determine how libspng is handled [1]. When set to OFF (the default), libjpeg-turbo uses its internal libspng source tree. When set to ON, it attempts to find and link against a system-supplied libspng library using pkg-config [1][2]. Static Library Target Names The CMake target name for the static libjpeg library is jpeg-static [3][4][5]. On non-MSVC platforms (e.g., Linux, macOS), the build system typically sets the output name property of this target to jpeg, resulting in a file named libjpeg.a [3][5]. On MSVC, it typically defaults to jpeg-static.lib [6][7]. Output Directory By default, the build artifacts are placed in the root of the binary directory (the directory from which CMake is executed) [6][7]. If a multi-configuration generator (like Visual Studio) is used, output files are often placed in subdirectories corresponding to the build configuration (e.g., Debug/ or Release/) [6][7]. Advanced Control If you require custom output naming for the static libraries, you can utilize the CMake cache variable JPEG_STATIC_OUTPUT_NAME (e.g., -DJPEG_STATIC_OUTPUT_NAME=jpeg) [8]. For general installation, the build system respects the CMAKE_INSTALL_PREFIX variable to determine the installation directory [9]. Top Results: [1][6][3][8][7]

Citations:


🌐 Web query:

libjpeg-turbo 3.x CMakeLists WITH_SYSTEM_SPNG WITH_SPNG jpeg-static libjpeg.a

💡 Result:

In libjpeg-turbo 3.x, the CMake build system provides options to control how libspng is handled during the build process [1][2]. Build Options for libspng: - WITH_SYSTEM_SPNG: This boolean CMake option (default: OFF) allows you to build libjpeg-turbo against a system-supplied libspng implementation instead of using the version included in the libjpeg-turbo source tree [1][2]. - When WITH_SYSTEM_SPNG is enabled (ON), the build system uses pkg-config to locate the system's libspng [1]. If disabled (OFF, default), it uses the internal implementation located in the src/spng subdirectory of the source tree [1][2]. Static Libraries and Dependencies: - The build process is designed to be self-contained [3]. When not using a system-supplied libspng or zlib, the build uses the internal, static versions provided within the source tree (e.g., spng-static, zlib-static) [1]. - The executables cjpeg and djpeg are configured to link against these static objects [4]. - Regarding libjpeg.a and jpeg-static: libjpeg-turbo provides the ability to build both shared and static libraries (governed by ENABLE_SHARED and ENABLE_STATIC options, default both TRUE) [5][6]. The static library is typically referred to as jpeg-static (the target name in CMake) [5][6]. Applications linking statically to libjpeg-turbo often link against this jpeg-static target [5]. In summary, the WITH_SYSTEM_SPNG option enables the use of an external libspng dependency, while the default behavior is to use a bundled version [1][2]. Static linking of the core libjpeg functionality is supported via the standard ENABLE_STATIC option [5][6].

Citations:


Use WITH_SYSTEM_SPNG here, not WITH_SPNG.
jpeg-static and ${LIBJPEG_TURBO_BUILD_DIR}/libjpeg.a already match libjpeg-turbo 3.x; only the spng CMake flag is off.

🤖 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/build.sh` around lines 13 - 19, The build
configuration in build.sh is using the wrong CMake flag for spng support; update
the libjpeg-turbo cmake invocation in the build-libjpeg setup to use
WITH_SYSTEM_SPNG instead of WITH_SPNG. Keep the existing jpeg-static target and
libjpeg.a output path unchanged, and only adjust the flag in the emcmake cmake
command so the libjpeg-turbo 3.x build is configured correctly.

Comment on lines +18 to +19
echo "~~~ MAKE libjpeg-turbo ~~~"
(cd build-libjpeg && emmake make VERBOSE=1 -j 16 jpeg-static)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add error checking after the libjpeg-turbo build stage.

Line 2 (set +e) disables exit-on-error, so if the first-stage emmake make fails, the script continues. LIBJPEG_TURBO_BUILD_DIR (line 22) will still be set because mkdir -p already created build-libjpeg, so the wrapper CMake guard passes — but the wrapper build then fails with a confusing "imported library not found" error instead of "libjpeg-turbo build failed." This is especially painful when iterating in CI as noted in the PR objectives.

🔧 Proposed fix
 echo "~~~ MAKE libjpeg-turbo ~~~"
-(cd build-libjpeg && emmake make VERBOSE=1 -j 16 jpeg-static)
+(cd build-libjpeg && emmake make VERBOSE=1 -j 16 jpeg-static) || {
+  echo "ERROR: libjpeg-turbo 3.x build failed — aborting" >&2
+  exit 1
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
echo "~~~ MAKE libjpeg-turbo ~~~"
(cd build-libjpeg && emmake make VERBOSE=1 -j 16 jpeg-static)
echo "~~~ MAKE libjpeg-turbo ~~~"
(cd build-libjpeg && emmake make VERBOSE=1 -j 16 jpeg-static) || {
echo "ERROR: libjpeg-turbo 3.x build failed — aborting" >&2
exit 1
}
🤖 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/build.sh` around lines 18 - 19, The
libjpeg-turbo build step in build.sh is not checked for failure, so a failed
emmake make can be masked and later surface as a misleading imported library
error. Add explicit error handling immediately after the build stage in the
script that runs the build-libjpeg/emmake make command so the script exits with
a clear libjpeg-turbo build failed message before continuing to the wrapper
CMake flow.

Comment on lines +123 to +145
// Read the header. In libjpeg-turbo 3.x this is precision-agnostic.
jpeg_read_header(&cinfo, TRUE);
// Force RGBA decoding, even for grayscale images
cinfo.out_color_space = JCS_EXT_RGBA;
jpeg_start_decompress(&cinfo);

// This codec handles single-component (grayscale) 12-bit JPEGs only. Fail
// closed on color input: forcing JCS_GRAYSCALE on a multi-component image
// would silently drop chroma and mis-report componentCount=1.
if (cinfo.num_components != 1) {
jpeg_destroy_decompress(&cinfo);
throw std::runtime_error(
"Unsupported 12-bit JPEG: expected 1 component (grayscale), got " +
std::to_string(cinfo.num_components));
}
// libjpeg-turbo 3.x is multi-precision in a single build; this codec only
// supports 12-bit samples. Reject other precisions rather than mis-decode.
if (cinfo.data_precision != 12) {
jpeg_destroy_decompress(&cinfo);
throw std::runtime_error(
"Unsupported JPEG precision: expected 12-bit, got " +
std::to_string(cinfo.data_precision));
}

cinfo.out_color_space = JCS_GRAYSCALE;
jpeg_start_decompress(&cinfo);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file first.
ast-grep outline packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp --view expanded || true

# Read the relevant slice with line numbers.
sed -n '1,240p' packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp | cat -n

# Search for libjpeg error handling patterns in this package.
rg -n "jpeg_std_error|error_exit|setjmp|longjmp|jpeg_destroy_decompress|jpeg12_read_scanlines|jpeg_read_header|jpeg_start_decompress" packages/libjpeg-turbo-12bit -S

Repository: cornerstonejs/codecs

Length of output: 9797


🌐 Web query:

libjpeg default error_exit exit process jpeg_std_error documentation

💡 Result:

In libjpeg, the default error_exit method provided by jpeg_std_error is designed to terminate the application upon encountering a fatal error [1][2][3]. When a fatal error occurs, the default implementation of error_exit performs the following steps: 1. Calls the output_message method to display the error message to stderr [2][3]. 2. Calls jpeg_destroy to clean up the JPEG object [2][3]. 3. Terminates the process by calling exit(EXIT_FAILURE) [2][3]. Because the library assumes that the error_exit method will not return to the caller, it effectively forces a process exit [4]. To prevent the application from terminating, you must override the error_exit method in the jpeg_error_mgr structure [2][5]. The standard approach for overriding this behavior is to use setjmp and longjmp to return control to a safe point in your application code [2][5]. After performing a longjmp, you are responsible for cleaning up the JPEG object using jpeg_destroy or jpeg_abort to return it to a known state [1][2][3].

Citations:


🌐 Web query:

libjpeg-turbo error_exit default behavior jpeg_std_error setjmp longjmp

💡 Result:

In libjpeg-turbo, the default behavior of the error_exit method, which is configured via jpeg_std_error, is to print the error message to stderr and terminate the application by calling exit(EXIT_FAILURE) [1][2][3]. Because this default behavior calls exit, it does not naturally support returning control to the caller when a fatal error occurs [4][5]. To change this, applications must override the default error_exit method [4][2][3]. The standard mechanism for overriding this behavior—and recovering from fatal errors without terminating the process—is to use the C setjmp and longjmp facilities [4][5]. The general implementation steps are as follows: 1. Create a custom error manager structure that extends the standard jpeg_error_mgr struct to include a jmp_buf field [4][5]. 2. Initialize this structure and point cinfo->err to it [1][4][6]. 3. Replace the default error_exit method with a custom function that performs a longjmp back to a location specified by a setjmp call in the main application logic [4][5]. 4. Before calling any libjpeg-turbo functions that might trigger a fatal error, perform a setjmp call to establish the recovery point [4][5]. When the custom error_exit method is triggered, it should perform any necessary cleanup, such as calling jpeg_destroy or jpeg_abort to return the JPEG object to a known state, before executing longjmp [1][2][3][6]. Attempting to use a JPEG object after a fatal error without first resetting its state with jpeg_destroy or jpeg_abort is unsafe [1][6][7].

Citations:


Install a custom libjpeg error handler
jpeg_std_error(&jerr) leaves the default error_exit in place, so any fatal decode error can terminate the whole WASM instance instead of throwing back to JS. Add a jpeg_error_mgr override with setjmp/longjmp before jpeg_create_decompress(), then convert libjpeg’s message into a C++ exception.

🤖 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 123 - 145, The
JPEGDecoder path still uses the default libjpeg error handling, so fatal decode
failures can abort the WASM instance instead of surfacing as exceptions. In
JPEGDecoder.hpp, add a custom jpeg_error_mgr for the decode flow around
jpeg_std_error and jpeg_create_decompress, wire in setjmp/longjmp before any
libjpeg calls that can fail, and convert libjpeg’s reported message into a C++
exception in the same decode path that already performs the component and
precision checks.

Comment on lines 147 to +150
frameInfo_.width = cinfo.output_width;
frameInfo_.height = cinfo.output_height;
frameInfo_.bitsPerSample = 8;
frameInfo_.componentCount = 1; //inColorspace == 2 ? 1 : 3;

// Prepare output buffer
// int pixelFormat = (frameInfo_.componentCount == 1) ? TJPF_GRAY : TJPF_RGB;

// const size_t destinationSize = frameInfo_.width * frameInfo_.height * tjPixelSize[pixelFormat];
int pixelFormat = 1;
size_t output_size = cinfo.output_width * cinfo.output_height * pixelFormat;

// std::vector<uint8_t> output_buffer(output_size);
frameInfo_.bitsPerSample = 12;
frameInfo_.componentCount = 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

frameInfo_.isSigned is left uninitialized.

The metadata block sets width, height, bitsPerSample, and componentCount, but not isSigned, and the constructor (Lines 38-40) doesn't initialize frameInfo_. Downstream consumers read this field (e.g. test/browser/index.html displays frameInfo.isSigned), so it currently reports an indeterminate value. Set it explicitly for the 12-bit grayscale path.

🩹 Set isSigned explicitly
     frameInfo_.bitsPerSample = 12;
     frameInfo_.componentCount = 1;
+    frameInfo_.isSigned = false;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
frameInfo_.width = cinfo.output_width;
frameInfo_.height = cinfo.output_height;
frameInfo_.bitsPerSample = 8;
frameInfo_.componentCount = 1; //inColorspace == 2 ? 1 : 3;
// Prepare output buffer
// int pixelFormat = (frameInfo_.componentCount == 1) ? TJPF_GRAY : TJPF_RGB;
// const size_t destinationSize = frameInfo_.width * frameInfo_.height * tjPixelSize[pixelFormat];
int pixelFormat = 1;
size_t output_size = cinfo.output_width * cinfo.output_height * pixelFormat;
// std::vector<uint8_t> output_buffer(output_size);
frameInfo_.bitsPerSample = 12;
frameInfo_.componentCount = 1;
frameInfo_.width = cinfo.output_width;
frameInfo_.height = cinfo.output_height;
frameInfo_.bitsPerSample = 12;
frameInfo_.componentCount = 1;
frameInfo_.isSigned = false;
🤖 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 147 - 150, The
12-bit grayscale metadata setup in JPEGDecoder should also initialize
frameInfo_.isSigned, since JPEGDecoder’s constructor does not set it and
downstream consumers read this field. Update the frameInfo_ population block
where width, height, bitsPerSample, and componentCount are assigned so isSigned
is explicitly set for this path, keeping the FrameInfo state fully defined.

Comment on lines 170 to +179
decoded_.resize(output_size);
const size_t stride = static_cast<size_t>(cinfo.output_width);

auto stride = cinfo.output_width * pixelFormat;

// Process data
// 12-bit precision decodes through jpeg12_read_scanlines with a
// J12SAMPARRAY (short-based) — the libjpeg-turbo 3.x per-precision API.
// decoded_ is std::vector<int16_t>, matching J12SAMPLE.
while (cinfo.output_scanline < cinfo.output_height) {
int16_t* output_data = &decoded_[stride * cinfo.output_scanline];
(void)jpeg_read_scanlines(&cinfo, &output_data, 1);
J12SAMPROW output_data =
reinterpret_cast<J12SAMPROW>(&decoded_[stride * cinfo.output_scanline]);
(void)jpeg12_read_scanlines(&cinfo, &output_data, 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files first
git ls-files 'packages/libjpeg-turbo-12bit/src/*' 'test/node/*' 'test/browser/*' | sed 's#^`#FILE` #'

echo
echo "=== Outline: JPEGDecoder.hpp ==="
ast-grep outline packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp --view expanded || true

echo
echo "=== Relevant getDecodedBuffer references ==="
rg -n "getDecodedBuffer|decodedBuffer|typed_memory_view|Uint8ClampedArray|Int16Array|Uint16Array" \
  packages/libjpeg-turbo-12bit/src test/node test/browser -S

echo
echo "=== Read JPEGDecoder.hpp around getDecodedBuffer and decode path ==="
sed -n '1,260p' packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp

echo
echo "=== Read JS consumer files ==="
sed -n '1,220p' test/node/index.js
echo '---'
sed -n '1,220p' test/browser/index.html

echo
echo "=== Small semantic probe: Uint8ClampedArray from Int16Array in JS ==="
node - <<'JS'
const src = Int16Array.from([0, 1, 255, 256, 4095, -1, 30000]);
const u8 = new Uint8ClampedArray(src);
console.log('src:', Array.from(src));
console.log('u8 :', Array.from(u8));
JS

Repository: cornerstonejs/codecs

Length of output: 1962


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== JPEGDecoder.hpp around getDecodedBuffer ==="
sed -n '1,120p' packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp

echo
echo "=== JPEGEncoder.hpp around getDecodedBuffer ==="
sed -n '1,110p' packages/libjpeg-turbo-12bit/src/JPEGEncoder.hpp

echo
echo "=== JS/HTML consumers in repo ==="
rg -n "getDecodedBuffer|decodedBuffer|decode\\(|Uint8ClampedArray|Int16Array|Uint16Array" . \
  -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**' -S

echo
echo "=== File list near tests/examples ==="
git ls-files | rg '(^|/)(test|tests|example|examples|demo|demos|browser|node)/'

echo
echo "=== Small runtime probe: copy/clamp behavior for Uint8ClampedArray ==="
node - <<'JS'
const src = new Int16Array([0, 1, 255, 256, 4095, -1]);
const dst = new Uint8ClampedArray(src);
console.log(JSON.stringify({src: Array.from(src), dst: Array.from(dst)}));
JS

Repository: cornerstonejs/codecs

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== JPEGDecoder.hpp getDecodedBuffer and decode tail ==="
sed -n '1,220p' packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp

echo
echo "=== libjpegTurbo12bit JS adapter ==="
sed -n '1,220p' packages/dicom-codec/src/codecs/libjpegTurbo12bit.js

echo
echo "=== libjpeg-turbo-12bit browser test around decoded buffer usage ==="
sed -n '130,390p' packages/libjpeg-turbo-12bit/test/browser/index.html

echo
echo "=== libjpeg-turbo-12bit node test around decoded buffer usage ==="
sed -n '1,170p' packages/libjpeg-turbo-12bit/test/node/index.js

echo
echo "=== libjpeg-turbo-12bit JS library bindings ==="
sed -n '1,120p' packages/libjpeg-turbo-12bit/src/jslib.cpp

Repository: cornerstonejs/codecs

Length of output: 24605


Expose 12-bit samples with a 16-bit typed array

getDecodedBuffer() is still wrapping decoded_ in Uint8ClampedArray, so the int16_t samples get copied and clamped to 0–255 before crossing into JS. That breaks the 12-bit contract: the browser/node consumers reinterpret the result as 16-bit pixel data, but this buffer now has byte-sized elements and loses every value above 255. Return an Int16Array or Uint16Array view instead.

🤖 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 170 - 179,
`getDecodedBuffer()` is still exposing the decoded 12-bit samples as byte-sized
data, which truncates values above 255. Update the JS-facing buffer wrapping to
use a 16-bit typed view (`Int16Array` or `Uint16Array`) over `decoded_` so the
`J12SAMPROW`/`J12SAMPLE` data from `JPEGDecoder` is preserved end-to-end. Keep
the change aligned with the `decoded_` storage type and the decoding path in
`jpeg12_read_scanlines`.

Comment on lines 3 to +5
set +e
mkdir -p build
mkdir -p dist
rm -rf build build-libjpeg
mkdir -p build build-libjpeg dist

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add error checking between build stages to avoid confusing CI failures.

set +e disables exit-on-error for the entire script. If stage 1 (libjpeg-turbo configure or make) fails, the script continues to stage 2, which will then fail with a confusing "missing libturbojpeg.a" error instead of the actual root cause. Since the PR notes this is being iterated in CI without local builds, this will waste CI cycles and make debugging harder.

Consider either switching to set -e (and selectively allowing expected failures) or adding explicit exit-code checks after each stage:

🛡️ Proposed fix: add stage-level error checking
 set +e
 rm -rf build build-libjpeg
 mkdir -p build build-libjpeg dist

 # ... stage 1 configure ...
 (cd build-libjpeg && emcmake cmake -G"Unix Makefiles" \
   -DCMAKE_BUILD_TYPE=Release \
   -DENABLE_SHARED=0 -DENABLE_STATIC=1 \
   -DWITH_SIMD=0 -DWITH_SPNG=0 -DWITH_TURBOJPEG=1 \
   ../extern/libjpeg-turbo)
+STAGE1_CFG=$?
+
 (cd build-libjpeg && emmake make VERBOSE=1 -j 16 turbojpeg-static jpeg-static)
+STAGE1_MAKE=$?
+
+if [ "$STAGE1_CFG" -ne 0 ] || [ "$STAGE1_MAKE" -ne 0 ]; then
+  echo "ERROR: libjpeg-turbo standalone build failed (cfg=$STAGE1_CFG, make=$STAGE1_MAKE)"
+  exit 1
+fi
🤖 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/build.sh` around lines 3 - 5, The build script
currently disables exit-on-error with set +e, which lets later stages continue
after an earlier failure and hides the real root cause. Update build.sh so each
stage (especially the libjpeg-turbo configure/make work and the later packaging
step) either runs under set -e or explicitly checks command exit codes before
proceeding, using the existing build stage flow to stop immediately on failure
and preserve the original error.

@sedghi

sedghi commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

Status: builds + tests + CodSpeed green; dist-size red pending a size decision

libjpeg-turbo 3.2.0 (from 2.1.4-era) — the major-version jump, for both the 8-bit and 12-bit packages. build (8-bit), build (12-bit), test, browser-smoke, and CodSpeed all pass.

What it took (a real rewrite, not just a bump): libjpeg-turbo 3.x forbids add_subdirectory() integration and removed the WITH_12BIT flag (one build is now multi-precision).

  • Build: both packages now build libjpeg-turbo as a separate project (two-phase build.sh) and link the produced static lib as an IMPORTED target; handled 3.x's header move to src/ and disabled the new SPNG/ZLIB dep.
  • 8-bit: no glue change — the legacy TurboJPEG API we use is still present in 3.2.0.
  • 12-bit: rewrote the decoder for 3.x's per-precision API — jpeg12_read_scanlines/J12SAMPARRAY gated on data_precision==12, single-component int16 grayscale with overflow guards. fix: consolidated codec correctness fixes (supersedes #71) #73 left untouched; this mirrors fix: consolidated codec correctness fixes (supersedes #71) #73's correctness logic on the 3.x API.

Size (dist-size, the only red):

  • 12-bit: −87% wasm (2186 → 272 KiB) — it was shipping Debug; 3.x + Release.
  • 8-bit: +24% wasm (438 → 542 KiB) — genuine 3.x cost (unified 8/12/16-bit + lossless code bundled into turbojpeg-static).

⚠️ Validation gap: the 12-bit pixel-correctness test lives in #73, not on this branch, so green here proves 12-bit compiles/links on 3.x but not decode-output correctness. That must be verified once combined with #73.

Fork PR: cornerstonejs/libjpeg-turbo#1 — MERGEABLE (clean advance to 3.2.0, no custom patches). (The 2.1.5.1 fork PR #2 was closed — marginal bump not worth it.)

Actions to merge (needs decisions)

  1. Decide the 8-bit +24% size: accept it / try to trim 3.x's unused precisions+lossless from the 8-bit build / keep 8-bit on 2.1.x. (Awaiting maintainer call.)
  2. Merge fix: consolidated codec correctness fixes (supersedes #71) #73 first, then reconcile 12-bit here (so the 3.x decoder stacks on fix: consolidated codec correctness fixes (supersedes #71) #73's corrected decoder + its pixel test validates decode).
  3. Re-baseline dist-size once (1) is decided.
  4. Merge fork libjpeg-turbo#1, then this PR.

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