Skip to content

Update fork to upstream OpenJPH 0.30.1 (re-apply resilient-decode patches) - #5

Closed
sedghi wants to merge 441 commits into
masterfrom
update/upstream-0.30.1
Closed

Update fork to upstream OpenJPH 0.30.1 (re-apply resilient-decode patches)#5
sedghi wants to merge 441 commits into
masterfrom
update/upstream-0.30.1

Conversation

@sedghi

@sedghi sedghi commented Jul 9, 2026

Copy link
Copy Markdown
Member

What this is

Re-bases the cornerstonejs OpenJPH fork onto upstream 0.30.1 (latest stable, 2026-06-21) and re-applies our custom patches. The pinned commit the codecs repo currently uses (e01c7b7, branch merge/aous-20240423) was ~22 months behind upstream.

The large file diff is just adopting upstream 0.30.1. The cornerstonejs-specific delta from stock 0.30.1 is 3 lines in one file:

git diff 0.30.1..HEAD  ->  src/core/codestream/ojph_codestream_local.cpp | 3 +/-2

Custom patches — carry / drop decisions

Original patch Decision Why
4417d4bresilient = falsetrue CARRIED Behavioral: the decoder tolerates truncated/damaged codestreams instead of hard-failing. Deliberate choice for medical-image robustness. Applied cleanly onto 0.30.1.
4417d4b — descriptive SIZ marker error-message text DROPPED Cosmetic. Conflicted with upstream's swap_byte()swap_bytes_if_le() rename on the same lines; kept upstream's code (which also has 2 years of its own message work). No behavior change.
d6297b2 — suppress the "File terminated early" INFO log CARRIED Harmless log-noise reduction; pairs with resilient decoding (truncation is expected, don't log it). Applied cleanly.
a15d3b3 — "Change to debug build for a while" (subprojects/js/build.shCMAKE_BUILD_TYPE=Debug) DROPPED Explicitly temporary; a Debug build ships larger/slower wasm. Keep upstream's release build.

Net effect vs stock 0.30.1

// src/core/codestream/ojph_codestream_local.cpp
- resilient = false;
+ resilient = true;                     // tolerate truncated/damaged streams

- OJPH_INFO(0x00030067, "File terminated early");
+ // This is common, so don't log unless needed
+ // OJPH_INFO(0x00030067, "File terminated early");

Validation status — NOT yet built

This was prepared with git only; the wasm was not compiled or run here. Needs a real emscripten build + the codecs pixel-correctness/browser-smoke goldens to confirm 0.30.1 decodes the corpus byte-identically and that resilient=true doesn't change output on valid streams. That runs in CI on the codecs-repo submodule-bump PR (separate). Treat this as unvalidated until that goes green.

Summary by CodeRabbit

  • New Features

    • Added broader build and test support across more platforms, including web, Windows, macOS, Linux, and additional CPU architectures.
    • Introduced fuzzing and code scanning workflows for improved project reliability.
    • Added streaming/packet-based expansion support and new build options for additional workflows.
  • Bug Fixes

    • Improved image input/output handling across formats like PFM, TIFF, RAW, YUV, and DPX.
    • Expanded codec support for more precision and component combinations.
  • Documentation

    • Added and reorganized build, Docker, fuzzing, usage, status, and web demo documentation.

aous72 and others added 23 commits June 4, 2026 22:33
Two bug fixes;
1. It defines the function gen_irv_tx_from_cb64, which should be rarely called -- it makes little sense that someone creates a lossy bitstream, and then uses a needlessly fine quantization. For this reason, I did not bother defining corresponding sse2, avx2, or wasm variants; some of these accelerated functions require work.
2. There was a bug in one assert function assert(top_atk == NULL);.
* arch: AVX-512 detection, Release CI, and portable inline macros

Tighten runtime AVX-512 feature detection, build the CI matrix in Release,
and add OJPH_FORCE_INLINE / OJPH_NO_INLINE macros in ojph_arch.h that the
AVX2 block coder uses in place of repeated per-function #ifdef blocks.

* AVX2 block decoder optimizations

Group the AVX2 HTJ2K cleanup-pass decoder optimizations:
- Wide UVLC table for non-initial quad rows, merged dual MagSgn fetch,
  widened frwd_advance, and reduced scratch-buffer memset.
- Force-inline decode_two_quad32_avx2 (MSVC-guarded __forceinline).
- Decompose the ~770-line ojph_decode_codeblock_avx2 into a thin
  dispatcher over four noinline helpers (decode_cb_step1_vlc,
  decode_cb_step2_32bit, decode_cb_step2_16bit, decode_cb_spp_mrp) so
  each pass gets its own register allocation, cutting spills in the hot
  step-1 VLC loop.

Decoder output is byte-identical to the previous implementation and the
full ctest suite passes. On Zen 5 / GCC the decomposition reduces decode
instruction count ~5.5% (8-bit) / ~1.5% (16-bit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* AVX2 block encoder optimizations

Reduce branch overhead in the AVX2 HTJ2K block encoder's MEL, MagSgn, and
VLC paths, and add an Apple-Clang/Intel fallback (separate header) to work
around a codegen issue observed on that toolchain.

Also adds a 64-bit count_trailing_zeros to ojph_arch.h, used by the
encoder's SWAR MagSgn accumulator drain.

* AVX2 decoder: destuff MagSgn/SPP once, fetch at absolute bit positions

Replace the stateful frwd_struct fetch/advance machinery in the AVX2
block decoder with a one-pass de-stuffer that flattens the MagSgn (and
SPP) segment into a stack buffer, from which bits are fetched at
absolute positions.  Since the bit counts (total_mn) depend only on
VLC outputs, the loop-carried dependency in step-2 collapses from a
fetch->advance memory round-trip through frwd_struct::tmp to a single
scalar position add; the refill branches and the merged-fetch
condition disappear from the hot loops entirely.  Positions past the
end of the data are clamped into the buffer padding, preserving the
fill semantics (1s for MagSgn, 0s for SPP) without initializing the
whole buffer.

Also replace the 14-instruction uniform-shift emulation of the
missing _mm256_sllv_epi16 in decode_four_quad16 with a pshufb
power-of-two lookup and a 16-bit multiply (6 instructions), and drop
the now-unused frwd_struct machinery from this file.

Measured on Zen 5 (9950X), single thread, in-memory full decode,
median of 30, vs previous commit (AVX2 path, AVX-512 disabled):
- u10_8bit  lossless 64x64:           53.68 -> 51.60 ms  (-3.9%)
- u10_8bit  lossless 512x8:           54.41 -> 52.04 ms  (-4.3%)
- u10_8bit  lossy qstep 0.001 64x64:  69.00 -> 64.70 ms  (-6.2%)
- ElephantDream_4K 16bit 64x64:       67.93 -> 63.91 ms  (-5.9%)

Decoded output verified byte-identical on all of the above plus an
8-wide/512-tall block configuration, 24 truncated-stream cases and
28 corrupted-byte cases (identical output or identical error);
ASAN-clean; all 82 ctest tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AVX2 decoder: destuff the backward VLC/MRP streams as well

Extend the destuffed-bitstream approach of the previous commit to the
backward-growing streams.  destuff_rev flattens the VLC (and MRP)
segment once per codeblock -- a SIMD fast path handles 16 bytes at a
time when no byte needs unstuffing, exploiting that the unstuff state
always equals (next byte in memory > 0x8F) after the first byte -- and
the step-1 VLC loop reads it through a branchless register-resident
window: drefill inserts whole bytes above the remaining bits (8-byte
load + or, no branches), so the refill load stays off the per-quad-pair
critical chain, and dconsume is a plain shift.  With at least 56 bits
after refill and at most 44 bits consumed per quad pair, one refill per
pair always suffices.  This removes the rev_read unstuffing work and
its refill branches from the hot loop; the MRP loop uses a positional
dfetch64 (it consumes at most 16 bits per stripe).  The rev_struct
machinery is no longer used in this file and is deleted.

Note that a first variant that re-fetched the window positionally per
quad pair was a wash: branch misses fell 38% but the fetch load
lengthened the loop-carried chain.  Keeping the window across pairs
captures the miss reduction without the latency: on u10_8bit lossless,
full-decode branch misses drop 38% and IPC rises from 3.09 to 3.41.

Measured on Zen 5 (9950X), single thread, in-memory full decode,
median of 30, vs previous commit (AVX2 path, AVX-512 disabled):
- u10_8bit  lossless 64x64:           49.92 -> 48.13 ms  (-3.6%)
- u10_8bit  lossless 512x8:           51.24 -> 48.14 ms  (-6.0%)
- u10_8bit  lossy qstep 0.001 64x64:  62.93 -> 59.18 ms  (-6.0%)
- ElephantDream_4K 16bit 64x64:       63.40 -> 59.80 ms  (-5.7%)
- kdu HT -rate 2 (CUP+SPP+MRP):       34.42 -> 33.54 ms  (-2.6%)

Verified byte-identical against the pre-optimization decoder on 12
configurations, including Kakadu-encoded multipass HT codestreams
(decode_cb_spp_mrp confirmed reached with num_passes=3; OpenJPH-encoded
streams never exercise it), plus 24 truncation and 30 corrupted-byte
cases (identical output or identical error); ASAN-clean on all of the
above; all 82 ctest tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: let CMake auto-detect Visual Studio on windows-latest

The windows-latest runner image moved to windows-2025-vs2026 on
2026-06-08, which no longer ships Visual Studio 17 2022, so the
hardcoded generator fails at configure time.  Dropping -G lets CMake
pick the newest installed Visual Studio on both old and new images;
-A x64 still selects the platform.  The WindowsOnARM jobs keep the
explicit generator, as the windows-11-arm image still provides it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AVX2 decoder: fix strict-aliasing UB and clamp the VLC window refill

The multipass irv97 conformance streams decoded incorrectly (~4% MSE
deviation) when built with MinGW GCC 15.2/16.1 at -O2/-O3, which broke
the Windows-MSYS2 CI job after the runner image update; -O1 or
-fno-strict-aliasing or -fno-tree-dse made the problem disappear.
The root cause is pre-existing type-punned accesses in the SPP/MRP
pass: prev_sig/cur_sig entries are written as ui16 but read through
*(ui32*) casts, and the sigma rearrangement stored through a *(ui32*)
cast into the ui16 array.  Under strict aliasing these accesses do not
alias, and GCC 15/16's TBAA-driven dead-store elimination began
exploiting that once the surrounding fetch code changed shape.  All
punned accesses (including the MEL reader's) are replaced with memcpy,
which compiles to the same single loads and stores but carries the
universal alias set.  Verified on MinGW GCC 16.1 (-O3): all 82 tests
pass; bisected with a destuff-vs-frwd fuzz harness (primitives were
bit-exact; the miscompilation was localized to decode_cb_spp_mrp via
#pragma GCC optimize placement).

Also clamp the read offset in drefill the same way dfetch/dfetch64
already do: when VLC consumption overruns a truncated stream, reads
now come from the zero padding (the exact fill the stream would
produce) instead of uninitialized buffer memory beyond the pad.

Byte-identical output re-verified on Linux against the
pre-optimization decoder for 12 configurations plus truncation cases;
82/82 ctest on both Linux GCC 14 and MinGW GCC 16.1; ASAN clean;
decode performance unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The destuffing fast path added in aous72#276 uses _mm_cvtsi128_si64 and
_mm_extract_epi64, which are only available when targeting x86-64,
breaking compilation on 32-bit x86 (e.g. MinGW32 -march=pentium4).

Assemble the two 64-bit halves from 32-bit extracts on i386 instead,
mirroring the OJPH_ARCH_X86_64 / i386 split already used by proc_cq2
in ojph_block_encoder_avx2.cpp. The x86-64 path is unchanged
(byte-identical disassembly apart from __LINE__ shifts in asserts).

Fixes aous72#280.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Bug fixes.
* Fixes issues with CI build on Windows
* Fix OJPH_ERROR argument order for NLT/SIZ mismatch message (aous72#283)
The format string expects SIZ bit depth, signedness, NLT bit depth,
signedness, then component index. Passing the component index first
caused vfprintf to treat an integer bit depth as a string pointer and
crash when reporting the error.

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Cary Phillips <cary@ilm.com>

---------

Signed-off-by: Cary Phillips <cary@ilm.com>
Co-authored-by: Aous Naman <aous@unsw.edu.au>
Co-authored-by: Cursor <cursoragent@cursor.com>

* Version bump

---------

Signed-off-by: Cary Phillips <cary@ilm.com>
Co-authored-by: Cary Phillips <cary@ilm.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
)

The shared precinct_scratch buffer was sized from the main COD
codeblock geometry only, but consumed using each component's
effective (COC) geometry.  A COC declaring a smaller codeblock than
the COD causes tag_tree::init to write past the end of the buffer
(heap OOB write in ojph_precinct.cpp:73).

Iterate all components via get_coc(c) when computing the scratch
reservation so the buffer covers the worst-case ratio across every
component.
* Issues/0266 add support for big-endian architecture (aous72#287)

* add CI for big-endian s390x architecture

* update Dockerfile

* Update Dockerfile to Ubuntu 26.04 LTS

* add Alpine Linux Dockerfile including instructions for running it using big-endian emulation with QEMU

* skip byteswap in encoding functions on big-endian architecture

* skip byteswap in decoding functions on big-endian architecture

* skip byteswap for ppm 16bit samples with big-endian architecture

* add big-endian architecture support for file-formats that assume little-endian on disk

* write little-endian pfm files on big-endian architecture

* add #include ojph_arch.h, clean up formatting and typos

* move #include "ojph_arch.h" below c-library includes

* shorten swap_bytes_if... function names

* Touch-ups

* Touchup2.

* Modifying Dockerfile

* Modify CI for s390x to make it manual, bc it is too slow.

* Update the workflow (forgotten)

* Update the workflow (Forgotten 2)

* Reduces number of CI builds

* Version bump

* Extinguishing warnings on MacOS

---------

Co-authored-by: michaeldsmith <37905569+michaeldsmith@users.noreply.github.com>
* VSX decoder: Initial support (POWER9/POWER10, ppc64le) (aous72#282)

Native 128-bit VSX implementations of the wavelet, colour, and
codestream kernels and the HTJ2K block decoder, with runtime
dispatch via hwcap. Supported targets are POWER9 (ISA 3.0) and
newer, little-endian only; other PPC targets use the generic code
paths.

Beyond a straight port, the kernels use POWER-specific forms where
measurement showed a win: xvrspi for round-to-nearest-away in the
float-to-int conversions, vec_sel for masked selects, and a block
decoder that destuffs the MagSgn bitstream upfront so per-quad bit
consumption is a GPR add instead of a vector-window shift.

The SIMD block decoder is dispatched everywhere on POWER10, and for
irreversible tile components on POWER9, where it beats the scalar
decoder; reversible content on POWER9 stays scalar, which is
slightly faster there.


Assisted-by: Lance Albertson <lance@osuosl.org>
Assisted-by: Thushan Fernando <thushan@thushanfernando.com>

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Update CI to include emulated ppc64le

* Modify code to work on 3 Ubuntu platforms, and test them in CI

* Fixes Some Strict Aliasing Rule violations.

* Touchups, and moving ojph_simd_vsx.h file to a new shared folder

---------

Co-authored-by: Trung Lê <135605+runlevel5@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Lock down and clean up the workflows.

* Update ccp-workflow.yml

---------

Co-authored-by: Aous Naman <aous@unsw.edu.au>
Bumps [msys2/setup-msys2](https://github.com/msys2/setup-msys2) from 2.31.1 to 2.32.0.
- [Release notes](https://github.com/msys2/setup-msys2/releases)
- [Changelog](https://github.com/msys2/setup-msys2/blob/main/CHANGELOG.md)
- [Commits](msys2/setup-msys2@e989830...66cd2cc)

---
updated-dependencies:
- dependency-name: msys2/setup-msys2
  dependency-version: 2.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@bbbca2d...043fb46)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Fixes an undefined behaviour that shows up in clang
* More undefined behaviour fixes.
(cherry picked from commit d6297b2)
(cherry picked from commit 4417d4b)
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR overhauls OpenJPH's build infrastructure (CI workflows, CMake, Dockerfile, docs), adds fuzzing harnesses and a new RTP-based streaming decoder app with socket/thread-pool utilities, and substantially refactors the core codec to support 32/64-bit codeblock precision, DFS/NLT-driven wavelet transforms, and updated marker parsing/decoding across SIMD backends.

Changes

Build, CI, and Documentation

Layer / File(s) Summary
CI workflows and dependabot
.github/dependabot.yml, .github/workflows/ccp-workflow.yml, .github/workflows/cifuzz.yml, .github/workflows/codeql.yml, .github/workflows/emcc.yml
Adds Dependabot config and multi-platform CI, CIFuzz, restructured CodeQL, and Emscripten build workflows.
Build system configuration
.gitignore, CMakeLists.txt, ojph_version.cmake, src/core/CMakeLists.txt, src/apps/CMakeLists.txt, src/apps/ojph_compress/CMakeLists.txt, src/apps/ojph_expand/CMakeLists.txt, Dockerfile
Updates top-level and per-target CMake build/install/versioning logic and Alpine-based Dockerfile.
Documentation
README.md, docs/*
Restructures README and adds compiling/docker/fuzzing/status/usage/web-demo docs.

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

Fuzzing Harness

Layer / File(s) Summary
Fuzz targets
fuzzing/CMakeLists.txt, fuzzing/fuzz_targets/*.cpp
Adds build wiring and libFuzzer entrypoints for compress/expand fuzzing.

Estimated code review effort: 2 (Simple) | ~15 minutes

Apps Utilities and CLI

Layer / File(s) Summary
Image I/O
src/apps/common/ojph_img_io.h, src/apps/others/ojph_img_io*.cpp
Adds PFM support, renumbers error codes, and updates byte-swap/type handling across image readers/writers.
Sockets and thread pool
src/apps/common/ojph_sockets.h, src/apps/others/ojph_sockets.cpp, src/apps/common/ojph_threads.h, src/apps/others/ojph_threads.cpp
Adds cross-platform socket wrapper and thread-pool implementation.
CLI tools
src/apps/ojph_compress/ojph_compress.cpp, src/apps/ojph_expand/ojph_expand.cpp
Updates usage text, error codes, and adds .pfm input/output support.

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

New Stream Expand Application

Layer / File(s) Summary
Build wiring
src/apps/ojph_stream_expand/CMakeLists.txt
Adds executable build target.
Main entrypoint
src/apps/ojph_stream_expand/ojph_stream_expand.cpp
Implements CLI/socket/receive-loop logic.
Packet/frame reassembly
src/apps/ojph_stream_expand/stream_expand_support.{h,cpp}
Implements RTP packet reordering and frame assembly.
Threaded storer
src/apps/ojph_stream_expand/threaded_frame_processors.{h,cpp}
Writes assembled frames to disk asynchronously.

Estimated code review effort: 5 (Critical) | ~100 minutes

Core Codec Engine Refactor

Layer / File(s) Summary
Bit buffer and precinct coding
src/core/codestream/ojph_bitbuffer_*.h, ojph_precinct.{h,cpp}
Updates missing-MSB encoding and band iteration.
Codeblock precision split
ojph_codeblock.{h,cpp}, ojph_codeblock_fun.{h,cpp}
Splits codeblock logic into 32-bit/64-bit variants.
SIMD kernels
ojph_codestream_{avx,avx2,sse,sse2,vsx,wasm,gen}.cpp
Adds 64-bit conversion kernels alongside updated 32-bit ones.
Codestream marker lifecycle
ojph_codestream.cpp, ojph_codestream_local.{h,cpp}
Refactors marker read/write and tile-part scanning.
Parameter marker parsing
ojph_params.cpp, ojph_params_local.h
Adds DFS/NLT/COC chaining and typed endianness-safe serialization.
Resolution/subband/tile DWT
ojph_resolution.{h,cpp}, ojph_subband.{h,cpp}, ojph_tile*.{h,cpp}
Reworks wavelet allocation/push/pull using transform flags and lifting steps.
Block decoder
src/core/coding/ojph_block_common.{h,cpp}, ojph_block_decoder.h, ojph_block_decoder{32,64}.cpp
Extends VLC/UVLC tables and adds a new 64-bit block decoder.

Estimated code review effort: 5 (Critical) | ~150 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client as UDP Sender
  participant Socket as socket/socket_manager
  participant PacketsHandler as packets_handler
  participant FramesHandler as frames_handler
  participant ThreadPool as thread_pool
  participant Storer as j2k_frame_storer

  Client->>Socket: recvfrom(rtp_packet)
  Socket->>PacketsHandler: exchange(packet)
  PacketsHandler->>PacketsHandler: order by clipped seq_num
  PacketsHandler->>FramesHandler: consume_packet -> push(packet)
  FramesHandler->>FramesHandler: assemble frame, track timestamps
  FramesHandler->>ThreadPool: add_task(storer) on marked packet
  ThreadPool->>Storer: execute()
  Storer->>Storer: write_to_file(frame)
  Storer->>FramesHandler: notify_file_completion()
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main purpose: rebasing to OpenJPH 0.30.1 while reapplying the resilient-decode patches.
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 update/upstream-0.30.1

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


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.

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

Caution

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

⚠️ Outside diff range comments (1)
src/apps/ojph_expand/ojph_expand.cpp (1)

342-345: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Missing space in .yuv error message.

"...usage of yuv" concatenates with "file." producing "yuvfile.".

✏️ Proposed fix
             " needed; in any case, this is not the normal usage of yuv"
-            "file.");
+            " file.");
🤖 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 `@src/apps/ojph_expand/ojph_expand.cpp` around lines 342 - 345, The error
string in the yuv save path is missing a space, causing “yuvfile.” to be
concatenated in the message. Update the string literal in ojph_expand’s
file-saving error message so the phrase “usage of yuv” and “file.” are separated
correctly, keeping the rest of the message unchanged.
🧹 Nitpick comments (3)
src/apps/ojph_compress/CMakeLists.txt (1)

46-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove empty conditional block or add a comment.

The ARM conditional block is empty with no body or comment. If no ARM-specific image I/O SIMD sources are needed, consider adding a brief comment for clarity, or removing the block entirely.

     if (("${OJPH_TARGET_ARCH}" MATCHES "OJPH_ARCH_ARM") OR MULTI_GEN_ARM64)
-
+      # No ARM-specific SIMD image I/O sources needed
     endif()
🤖 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 `@src/apps/ojph_compress/CMakeLists.txt` around lines 46 - 48, The ARM-specific
conditional in CMakeLists is empty, so either remove the unused if()/endif()
block entirely or add a brief comment inside it to document that no ARM image
I/O SIMD sources are currently required. Update the OJPH target architecture
check around the empty conditional so the intent is clear when reviewing the
build logic later.
docs/compiling.md (1)

114-117: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clarify the OJPH_DISABLE_SIMD description.

The sentence "which should include SIMD instructions wherever they are supported" is contradictory — OJPH_DISABLE_SIMD disables SIMD, not includes it. Reword for clarity.

-The code now employs the architecture-agnostic option `OJPH_DISABLE_SIMD`, which should include SIMD instructions wherever they are supported.  This can be achieved with `-DOJPH_DISABLE_SIMD=ON` option during CMake configuration.  Individual instruction sets can be disabled; see the options in the main CMakeLists.txt file.
+The code employs SIMD instructions by default wherever they are supported.  SIMD can be disabled entirely using the architecture-agnostic `-DOJPH_DISABLE_SIMD=ON` option during CMake configuration.  Individual instruction sets can also be disabled; see the options in the main CMakeLists.txt file.
🤖 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 `@docs/compiling.md` around lines 114 - 117, The description of
OJPH_DISABLE_SIMD is contradictory in the compiling guide: it says the option
“should include SIMD instructions” even though the name indicates disabling
SIMD. Update the wording in the SIMD section to clearly state that
OJPH_DISABLE_SIMD turns SIMD off, and keep the reference to the CMake
configuration option and the per-instruction-set disable options consistent with
that behavior.
README.md (1)

15-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fix inconsistent link path for Status doc.

Line 15 uses /docs/status.md (absolute path) while all other Table of Contents entries use ./docs/... (relative path). This inconsistency can break rendering on platforms that resolve paths differently.

-* [Status](/docs/status.md)
+* [Status](./docs/status.md)
🤖 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 `@README.md` around lines 15 - 21, The Status entry in the README table of
contents uses an absolute docs path while the other entries use relative paths,
so update that link to match the same relative style as the surrounding items.
Make the change in the README Table of Contents entry for Status so it is
consistent with the other links and renders reliably across platforms.
🤖 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 @.github/workflows/ccp-workflow.yml:
- Around line 241-267: The test_ppc64le job is misconfigured in two places: it
still uses the s390x architecture and includes an invalid distro entry. Update
the run-on-arch-action configuration in the test_ppc64le job to use the
PowerPC64LE architecture symbol and replace the matrix value using ubuntu_latest
with a valid supported Ubuntu distro string, keeping the job name aligned with
the actual target platform.

In `@docs/doxygen_style.md`:
- Line 5: The updated documentation sentence still contains two typos in the
prose around the description of source-code documentation. In the text in
docs/doxygen_style.md, fix the wording in that introductory sentence so that the
subject-verb agreement is corrected from “represent” to “represents” and the
phrase “so sort” is corrected to “some sort”; keep the rest of the paragraph
unchanged and make sure the surrounding explanation still reads naturally.

In `@docs/status.md`:
- Line 9: The supported file types summary in the documentation is missing .pfm
even though ojph_compress and ojph_expand now handle it. Update the format list
in the status documentation to include .pfm alongside the existing .pgm, .ppm,
.yuv, .raw, and .dpx entries, keeping the wording consistent with the current
command-line tools description.

In `@docs/web_demos.md`:
- Line 3: Fix the grammar in the web demo description by changing the sentence
in docs/web_demos.md from “It currently host” to “It currently hosts”; update
the prose around the openjph.org and javascript demo reference so the sentence
reads naturally and consistently with the rest of the paragraph.

In `@src/apps/ojph_compress/ojph_compress.cpp`:
- Around line 397-401: The error message in the component-limit check has a
missing space after “JPEG2000.” and uses the wrong format specifier for
num_comps. Update the printf in ojph_compress.cpp’s component-count guard so the
message reads cleanly and prints the ojph::ui32 value with an appropriate
unsigned 32-bit format, keeping the existing check and return behavior
unchanged.

In `@src/apps/ojph_expand/ojph_expand.cpp`:
- Around line 290-293: The `.pfm` validation branch in `ojph_expand.cpp` has
copy-pasted error text that incorrectly says `ppm` instead of `pfm`. Update the
`OJPH_ERROR` message in the `all_same` check inside the `.pfm` save path so it
references the correct format, keeping the rest of the condition and error
handling unchanged.

In `@src/apps/ojph_stream_expand/ojph_stream_expand.cpp`:
- Around line 202-205: The OJPH_ERROR messages in ojph_stream_expand.cpp are
using the wrong format specifier for recv_port and src_port, which are char*
values rather than integers. Update the error handling in the affected
validation blocks near the port parsing logic and the corresponding source-port
check to print the provided string with a string format specifier, using the
existing OJPH_ERROR calls so the messages remain accurate and safe.
- Around line 248-254: `inet_ntop` failure handling in `ojph_stream_expand.cpp`
still falls through to `printf` with a null `%s` argument; update the
`inet_ntop` result checks in the listening-address print path and the other two
matching blocks so they do not call `printf` when `t == NULL`. In the relevant
code near the `inet_ntop` calls, either return/skip the print after logging the
error with `OJPH_INFO` or provide a safe fallback string before formatting, and
apply the same fix consistently to all three occurrences.
- Around line 274-280: The get_arguments path in ojph_stream_expand currently
assumes src_port is set whenever src_addr is provided, and atoi(src_port) can
dereference a null pointer if -src_port is omitted. Add validation in the
src_addr handling block to check src_port before converting it, and raise the
same OJPH_ERROR path with a clear message when -src_addr is used without a port.
Keep the fix localized to get_arguments and the src_addr/src_port parsing logic.

In `@src/apps/ojph_stream_expand/stream_expand_support.cpp`:
- Around line 408-441: The counter update in check_files_in_processing is
happening too early and can make the loop stop before all completed files are
reclaimed. Move the num_complete_files decrement so it only happens after a
stex_file is confirmed done and actually transferred from processing to avail,
and keep the traversal logic in sync for both the head case and the pf->next
case. Verify the loop in check_files_in_processing still walks the full
processing list without reducing num_complete_files for incomplete entries.

In `@src/apps/others/ojph_img_io.cpp`:
- Around line 1664-1670: The sample clamping in the `upper_val` setup inside
`ojph_img_io.cpp` is off by one for both signed and unsigned paths. Update the
bounds in this block so `upper_val` reflects the largest representable sample
value used by the write path: `2^bit_depth - 1` for unsigned and `2^(bit_depth -
1) - 1` for signed. Keep the `is_signed` branch and the surrounding clamping
logic intact, just correct the computed maximum before values are narrowed to
the output type.

In `@src/core/codestream/ojph_codestream_local.cpp`:
- Around line 1059-1062: The NLT handling branch in skip_marker currently logs
the wrong warning text, which still says PPT instead of NLT. Update the
skip_marker call in the marker_idx == 9 branch so the message matches the NLT
marker being processed, keeping the same OJPH_MSG_WARN and resilient behavior.
- Around line 804-831: The COC/QCC diagnostics in read_headers() are using
num_comps, which can still be 0 at this point and produce misleading “0
components” messages. Update the error/info messages in the marker parsing logic
around get_coc(), get_qcc(), and read_qcc() to use siz.get_num_components()
directly for the component count, while keeping the existing component index
checks unchanged.

In `@src/core/codestream/ojph_params.cpp`:
- Around line 1086-1091: The COC missing-ATK error in the loop over COC objects
uses the wrong wavelet/kernel value, so the message reports the COD value
instead of the current COC’s setting. Update the OJPH_ERROR call in the code
path that checks p->atk after get_atk so it references the COC object’s
SPcod.wavelet_trans from p, keeping the message aligned with the failing
segment.
- Around line 459-465: The COM marker length validation in the parameter setter
is checking the wrong value: it should validate the newly computed string length
`t` rather than the member `len`, which may be stale or uninitialized. Update
the length guard in the same code path that assigns `this->data`, `this->len`,
and `this->Rcom` so it rejects any string longer than the allowed maximum before
truncation occurs, matching the validation approach used by `set_data()`.

In `@src/core/coding/ojph_block_decoder64.cpp`:
- Line 1549: The high-precision shift in the block decoder still uses 32-bit
arithmetic, so update the expression in ojph_block_decoder64.cpp where the value
is computed in the decoder logic to use a 64-bit literal instead of the current
unsigned 32-bit one. Locate the shift in the code path that builds the `ui64
val` value and change the literal so the operation stays 64-bit throughout,
preserving the intended range for larger shift counts.

---

Outside diff comments:
In `@src/apps/ojph_expand/ojph_expand.cpp`:
- Around line 342-345: The error string in the yuv save path is missing a space,
causing “yuvfile.” to be concatenated in the message. Update the string literal
in ojph_expand’s file-saving error message so the phrase “usage of yuv” and
“file.” are separated correctly, keeping the rest of the message unchanged.

---

Nitpick comments:
In `@docs/compiling.md`:
- Around line 114-117: The description of OJPH_DISABLE_SIMD is contradictory in
the compiling guide: it says the option “should include SIMD instructions” even
though the name indicates disabling SIMD. Update the wording in the SIMD section
to clearly state that OJPH_DISABLE_SIMD turns SIMD off, and keep the reference
to the CMake configuration option and the per-instruction-set disable options
consistent with that behavior.

In `@README.md`:
- Around line 15-21: The Status entry in the README table of contents uses an
absolute docs path while the other entries use relative paths, so update that
link to match the same relative style as the surrounding items. Make the change
in the README Table of Contents entry for Status so it is consistent with the
other links and renders reliably across platforms.

In `@src/apps/ojph_compress/CMakeLists.txt`:
- Around line 46-48: The ARM-specific conditional in CMakeLists is empty, so
either remove the unused if()/endif() block entirely or add a brief comment
inside it to document that no ARM image I/O SIMD sources are currently required.
Update the OJPH target architecture check around the empty conditional so the
intent is clear when reviewing the build logic later.
🪄 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: 7637c01d-b3d0-4421-8e48-f56fd39b8f95

📥 Commits

Reviewing files that changed from the base of the PR and between 0da149a and 0748112.

⛔ Files ignored due to path filters (1)
  • fuzzing/seed_corpus/ojph_compress_fuzz_target/w128_h128_b2_79_b3_09.bin is excluded by !**/*.bin
📒 Files selected for processing (136)
  • .github/dependabot.yml
  • .github/workflows/ccp-workflow.yml
  • .github/workflows/cifuzz.yml
  • .github/workflows/codeql.yml
  • .github/workflows/emcc.yml
  • .gitignore
  • CMakeLists.txt
  • Dockerfile
  • README.md
  • bin/.gitignore
  • docs/compiling.md
  • docs/docker.md
  • docs/doxygen_style.md
  • docs/fuzzing.md
  • docs/status.md
  • docs/usage_examples.md
  • docs/web_demos.md
  • fuzzing/CMakeLists.txt
  • fuzzing/fuzz_targets/ojph_compress_fuzz_target.cpp
  • fuzzing/fuzz_targets/ojph_expand_fuzz_target.cpp
  • ojph_version.cmake
  • src/apps/CMakeLists.txt
  • src/apps/common/ojph_img_io.h
  • src/apps/common/ojph_sockets.h
  • src/apps/common/ojph_threads.h
  • src/apps/ojph_compress/CMakeLists.txt
  • src/apps/ojph_compress/ojph_compress.cpp
  • src/apps/ojph_expand/CMakeLists.txt
  • src/apps/ojph_expand/ojph_expand.cpp
  • src/apps/ojph_stream_expand/CMakeLists.txt
  • src/apps/ojph_stream_expand/ojph_stream_expand.cpp
  • src/apps/ojph_stream_expand/stream_expand_support.cpp
  • src/apps/ojph_stream_expand/stream_expand_support.h
  • src/apps/ojph_stream_expand/threaded_frame_processors.cpp
  • src/apps/ojph_stream_expand/threaded_frame_processors.h
  • src/apps/others/ojph_img_io.cpp
  • src/apps/others/ojph_img_io_avx2.cpp
  • src/apps/others/ojph_img_io_sse41.cpp
  • src/apps/others/ojph_sockets.cpp
  • src/apps/others/ojph_threads.cpp
  • src/core/CMakeLists.txt
  • src/core/codestream/ojph_bitbuffer_read.h
  • src/core/codestream/ojph_bitbuffer_write.h
  • src/core/codestream/ojph_codeblock.cpp
  • src/core/codestream/ojph_codeblock.h
  • src/core/codestream/ojph_codeblock_fun.cpp
  • src/core/codestream/ojph_codeblock_fun.h
  • src/core/codestream/ojph_codestream.cpp
  • src/core/codestream/ojph_codestream_avx.cpp
  • src/core/codestream/ojph_codestream_avx2.cpp
  • src/core/codestream/ojph_codestream_gen.cpp
  • src/core/codestream/ojph_codestream_local.cpp
  • src/core/codestream/ojph_codestream_local.h
  • src/core/codestream/ojph_codestream_sse.cpp
  • src/core/codestream/ojph_codestream_sse2.cpp
  • src/core/codestream/ojph_codestream_vsx.cpp
  • src/core/codestream/ojph_codestream_wasm.cpp
  • src/core/codestream/ojph_params.cpp
  • src/core/codestream/ojph_params_local.h
  • src/core/codestream/ojph_precinct.cpp
  • src/core/codestream/ojph_precinct.h
  • src/core/codestream/ojph_resolution.cpp
  • src/core/codestream/ojph_resolution.h
  • src/core/codestream/ojph_subband.cpp
  • src/core/codestream/ojph_subband.h
  • src/core/codestream/ojph_tile.cpp
  • src/core/codestream/ojph_tile.h
  • src/core/codestream/ojph_tile_comp.cpp
  • src/core/codestream/ojph_tile_comp.h
  • src/core/coding/ojph_block_common.cpp
  • src/core/coding/ojph_block_common.h
  • src/core/coding/ojph_block_decoder.h
  • src/core/coding/ojph_block_decoder32.cpp
  • src/core/coding/ojph_block_decoder64.cpp
  • src/core/coding/ojph_block_decoder_avx2.cpp
  • src/core/coding/ojph_block_decoder_ssse3.cpp
  • src/core/coding/ojph_block_decoder_vsx.cpp
  • src/core/coding/ojph_block_decoder_wasm.cpp
  • src/core/coding/ojph_block_encoder.cpp
  • src/core/coding/ojph_block_encoder.h
  • src/core/coding/ojph_block_encoder_avx2.cpp
  • src/core/coding/ojph_block_encoder_avx2_apple.h
  • src/core/coding/ojph_block_encoder_avx512.cpp
  • src/core/common/ojph_arch.h
  • src/core/common/ojph_codestream.h
  • src/core/common/ojph_message.h
  • src/core/common/ojph_params.h
  • src/core/openjph/ojph_arch.h
  • src/core/openjph/ojph_arg.h
  • src/core/openjph/ojph_base.h
  • src/core/openjph/ojph_codestream.h
  • src/core/openjph/ojph_defs.h
  • src/core/openjph/ojph_file.h
  • src/core/openjph/ojph_mem.h
  • src/core/openjph/ojph_message.h
  • src/core/openjph/ojph_params.h
  • src/core/openjph/ojph_simd_vsx.h
  • src/core/openjph/ojph_version.h
  • src/core/others/ojph_arch.cpp
  • src/core/others/ojph_file.cpp
  • src/core/others/ojph_mem.cpp
  • src/core/others/ojph_mem_c.c
  • src/core/others/ojph_message.cpp
  • src/core/shared/ojph_simd_vsx.h
  • src/core/transform/ojph_colour.cpp
  • src/core/transform/ojph_colour.h
  • src/core/transform/ojph_colour_avx.cpp
  • src/core/transform/ojph_colour_avx2.cpp
  • src/core/transform/ojph_colour_local.h
  • src/core/transform/ojph_colour_sse.cpp
  • src/core/transform/ojph_colour_sse2.cpp
  • src/core/transform/ojph_colour_vsx.cpp
  • src/core/transform/ojph_colour_wasm.cpp
  • src/core/transform/ojph_transform.cpp
  • src/core/transform/ojph_transform.h
  • src/core/transform/ojph_transform_avx.cpp
  • src/core/transform/ojph_transform_avx2.cpp
  • src/core/transform/ojph_transform_avx512.cpp
  • src/core/transform/ojph_transform_local.h
  • src/core/transform/ojph_transform_sse.cpp
  • src/core/transform/ojph_transform_sse2.cpp
  • src/core/transform/ojph_transform_vsx.cpp
  • src/core/transform/ojph_transform_wasm.cpp
  • src/openjph-config.cmake.in
  • src/openjph.pc.in
  • src/pkg-config.pc.cmake
  • subprojects/js/CMakeLists.txt
  • subprojects/js/build.sh
  • target_arch.cmake
  • tests/CMakeLists.txt
  • tests/mse_pae.cmake
  • tests/mse_pae.cpp
  • tests/test.py
  • tests/test_executables.cpp
  • tests/test_helpers/convert_mse_pae_to_tests.cpp
  • tests/test_helpers/ht_cmdlines.txt
💤 Files with no reviewable changes (1)
  • bin/.gitignore

Comment on lines +241 to +267
test_ppc64le:
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.job_to_run == '' || github.event.inputs.job_to_run == 'test_ppc64le' }}

name: Linux-PowerPC64LE (little-endian) Build and Test
runs-on: ubuntu-latest
strategy:
matrix:
include: [ { dist: ubuntu22.04 }, { dist: ubuntu_latest } ]

steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
with:
persist-credentials: false

- uses: uraimo/run-on-arch-action@f9b26e3a1a408d5fd530d20c17b9f3f4428ff8d9 #3.1.0
with:
arch: s390x
distro: ${{ matrix.dist }}
githubToken: ${{ github.token }}
install: |
apt-get update -q -y
apt-get install -q -y cmake make g++ libtiff-dev python3
run: |
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DOJPH_BUILD_STREAM_EXPAND=ON -DOJPH_ENABLE_TIFF_SUPPORT=ON -DOJPH_BUILD_TESTS=ON ..
make
ctest --output-on-failure

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 | 🔴 Critical | ⚡ Quick win

test_ppc64le job uses wrong architecture (s390x instead of ppc64le)

The job is named "Linux-PowerPC64LE (little-endian) Build and Test" but sets arch: s390x on line 257. This is a copy-paste error from the test_s390x job — the job will run s390x instead of PowerPC64LE, making it a duplicate of test_s390x rather than testing the intended architecture.

Additionally, dist: ubuntu_latest (line 248) is not a valid distro identifier for uraimo/run-on-arch-action, which expects specific version strings like ubuntu22.04. This will likely cause the second matrix entry to fail.

🔧 Proposed fix for `test_ppc64le` job
       - uses: uraimo/run-on-arch-action@f9b26e3a1a408d5fd530d20c17b9f3f4428ff8d9 `#3.1.0`
         with:
-          arch: s390x
+          arch: ppc64le
           distro: ${{ matrix.dist }}

And fix the invalid distro value:

     strategy:
         matrix:
-            include: [ { dist: ubuntu22.04 }, { dist: ubuntu_latest } ]
+            include: [ { dist: ubuntu22.04 }, { dist: ubuntu24.04 } ]
📝 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
test_ppc64le:
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.job_to_run == '' || github.event.inputs.job_to_run == 'test_ppc64le' }}
name: Linux-PowerPC64LE (little-endian) Build and Test
runs-on: ubuntu-latest
strategy:
matrix:
include: [ { dist: ubuntu22.04 }, { dist: ubuntu_latest } ]
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
with:
persist-credentials: false
- uses: uraimo/run-on-arch-action@f9b26e3a1a408d5fd530d20c17b9f3f4428ff8d9 #3.1.0
with:
arch: s390x
distro: ${{ matrix.dist }}
githubToken: ${{ github.token }}
install: |
apt-get update -q -y
apt-get install -q -y cmake make g++ libtiff-dev python3
run: |
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DOJPH_BUILD_STREAM_EXPAND=ON -DOJPH_ENABLE_TIFF_SUPPORT=ON -DOJPH_BUILD_TESTS=ON ..
make
ctest --output-on-failure
test_ppc64le:
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.job_to_run == '' || github.event.inputs.job_to_run == 'test_ppc64le' }}
name: Linux-PowerPC64LE (little-endian) Build and Test
runs-on: ubuntu-latest
strategy:
matrix:
include: [ { dist: ubuntu22.04 }, { dist: ubuntu24.04 } ]
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 `#v6.0.3`
with:
persist-credentials: false
- uses: uraimo/run-on-arch-action@f9b26e3a1a408d5fd530d20c17b9f3f4428ff8d9 `#3.1.0`
with:
arch: ppc64le
distro: ${{ matrix.dist }}
githubToken: ${{ github.token }}
install: |
apt-get update -q -y
apt-get install -q -y cmake make g++ libtiff-dev python3
run: |
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DOJPH_BUILD_STREAM_EXPAND=ON -DOJPH_ENABLE_TIFF_SUPPORT=ON -DOJPH_BUILD_TESTS=ON ..
make
ctest --output-on-failure
🤖 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 @.github/workflows/ccp-workflow.yml around lines 241 - 267, The test_ppc64le
job is misconfigured in two places: it still uses the s390x architecture and
includes an invalid distro entry. Update the run-on-arch-action configuration in
the test_ppc64le job to use the PowerPC64LE architecture symbol and replace the
matrix value using ubuntu_latest with a valid supported Ubuntu distro string,
keeping the job name aligned with the actual target platform.

Comment thread docs/doxygen_style.md
Documentation is still experimental for me, and I might change things down the line.

Here, we describe how to document the source code. This represent so sort of minial set of markers that need to be used. Other markers can be used to enhance the documentation of the code. This serves as a live document that can be updated when needed.
Here, we describe how to document the source code. This represent so sort of minimal set of markers that need to be used. Other markers can be used to enhance the documentation of the code. This serves as a live document that can be updated when needed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Typos remain after the wording update.

The line range change details indicate this line was updated to correct typos, but two issues persist: "represent" should be "represents" (subject-verb agreement) and "so sort" should be "some sort".

📝 Proposed fix
-Here, we describe how to document the source code. This represent so sort of minimal set of markers that need to be used. Other markers can be used to enhance the documentation of the code. This serves as a live document that can be updated when needed.
+Here, we describe how to document the source code. This represents some sort of minimal set of markers that need to be used. Other markers can be used to enhance the documentation of the code. This serves as a live document that can be updated when needed.
📝 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
Here, we describe how to document the source code. This represent so sort of minimal set of markers that need to be used. Other markers can be used to enhance the documentation of the code. This serves as a live document that can be updated when needed.
Here, we describe how to document the source code. This represents some sort of minimal set of markers that need to be used. Other markers can be used to enhance the documentation of the code. This serves as a live document that can be updated when needed.
🤖 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 `@docs/doxygen_style.md` at line 5, The updated documentation sentence still
contains two typos in the prose around the description of source-code
documentation. In the text in docs/doxygen_style.md, fix the wording in that
introductory sentence so that the subject-verb agreement is corrected from
“represent” to “represents” and the phrase “so sort” is corrected to “some
sort”; keep the rest of the paragraph unchanged and make sure the surrounding
explanation still reads naturally.

Comment thread docs/status.md

As it stands, the OpenJPH library needs documentation. The provided encoder ojph\_compress only generates HTJ2K codestreams, with the extension j2c; the generated files lack the .jph header. Adding the .jph header is of little urgency, as the codestream contains all needed information to properly decode an image. The .jph header will be added at a future point in time. The provided decoder ojph\_expand decodes .jph files, by ignoring the .jph header if it is present.

The provided command line tools ojph\_compress and ojph\_expand accepts and generates .pgm, .ppm, .yuv, .raw, and .dpx. See the usage examples below. No newline at end of file

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 | 🟡 Minor | ⚡ Quick win

Missing .pfm format in supported file types list.

This PR adds PFM input/output support (documented in docs/usage_examples.md line 25 and confirmed by the review stack context for ojph_compress/ojph_expand), but line 9 does not list .pfm alongside the other supported formats.

📝 Proposed fix
-The provided command line tools ojph\_compress and ojph\_expand accepts and generates .pgm, .ppm, .yuv, .raw, and .dpx. See the usage examples below.
+The provided command line tools ojph\_compress and ojph\_expand accepts and generates .pgm, .ppm, .pfm, .yuv, .raw, and .dpx. See the usage examples below.
📝 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
The provided command line tools ojph\_compress and ojph\_expand accepts and generates .pgm, .ppm, .yuv, .raw, and .dpx. See the usage examples below.
The provided command line tools ojph\_compress and ojph\_expand accepts and generates .pgm, .ppm, .pfm, .yuv, .raw, and .dpx. See the usage examples below.
🤖 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 `@docs/status.md` at line 9, The supported file types summary in the
documentation is missing .pfm even though ojph_compress and ojph_expand now
handle it. Update the format list in the status documentation to include .pfm
alongside the existing .pgm, .ppm, .yuv, .raw, and .dpx entries, keeping the
wording consistent with the current command-line tools description.

Comment thread docs/web_demos.md
@@ -0,0 +1,5 @@
# Web-based Demos #

The associate site [openjph.org](https://openjph.org) serves as a blog. It currently host the [javascript](https://openjph.org/javascript/demo.html) demo of the decoder; the webpage demonstrates that the library can be compiled to javascript, and can run inside a web-browser. Any browser supporting webassembly can be used to view this webpage; examples include Firefox, Chrome, Safari, and Edge, on a desktop, mobile, or tablet.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Grammar: "host" should be "hosts"

"It currently host the" should read "It currently hosts the".

✏️ Proposed fix
-The associate site [openjph.org](https://openjph.org) serves as a blog.  It currently host the [javascript](https://openjph.org/javascript/demo.html) demo of the decoder;
+The associate site [openjph.org](https://openjph.org) serves as a blog.  It currently hosts the [javascript](https://openjph.org/javascript/demo.html) demo of the decoder;
📝 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
The associate site [openjph.org](https://openjph.org) serves as a blog. It currently host the [javascript](https://openjph.org/javascript/demo.html) demo of the decoder; the webpage demonstrates that the library can be compiled to javascript, and can run inside a web-browser. Any browser supporting webassembly can be used to view this webpage; examples include Firefox, Chrome, Safari, and Edge, on a desktop, mobile, or tablet.
The associate site [openjph.org](https://openjph.org) serves as a blog. It currently hosts the [javascript](https://openjph.org/javascript/demo.html) demo of the decoder; the webpage demonstrates that the library can be compiled to javascript, and can run inside a web-browser. Any browser supporting webassembly can be used to view this webpage; examples include Firefox, Chrome, Safari, and Edge, on a desktop, mobile, or tablet.
🤖 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 `@docs/web_demos.md` at line 3, Fix the grammar in the web demo description by
changing the sentence in docs/web_demos.md from “It currently host” to “It
currently hosts”; update the prose around the openjph.org and javascript demo
reference so the sentence reads naturally and consistently with the rest of the
paragraph.

Comment on lines +397 to +401
if (num_comps > 16384) {
printf("More than 16384 components is not supported in JPEG2000."
"You specified %d\n", num_comps);
return false;
}

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

Minor: message spacing and format specifier.

The two string literals concatenate to ...JPEG2000.You specified (missing space), and %d is used for an ojph::ui32 (value can exceed INT_MAX, which is UB for %d).

✏️ Proposed fix
   if (num_comps > 16384) {
-    printf("More than 16384 components is not supported in JPEG2000."
-      "You specified %d\n", num_comps);
+    printf("More than 16384 components is not supported in JPEG2000. "
+      "You specified %u\n", num_comps);
     return 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
if (num_comps > 16384) {
printf("More than 16384 components is not supported in JPEG2000."
"You specified %d\n", num_comps);
return false;
}
if (num_comps > 16384) {
printf("More than 16384 components is not supported in JPEG2000. "
"You specified %u\n", num_comps);
return 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 `@src/apps/ojph_compress/ojph_compress.cpp` around lines 397 - 401, The error
message in the component-limit check has a missing space after “JPEG2000.” and
uses the wrong format specifier for num_comps. Update the printf in
ojph_compress.cpp’s component-count guard so the message reads cleanly and
prints the ojph::ui32 value with an appropriate unsigned 32-bit format, keeping
the existing check and return behavior unchanged.

Comment on lines +804 to +831
if (p->get_comp_idx() >= siz.get_num_components())
OJPH_INFO(0x00030056, "The codestream carries a COC marker "
"segment for a component indexed by %d, which is more than the "
"allowed index number, since the codestream has %d components",
p->get_comp_idx(), num_comps);
param_cod *q = cod.get_coc(p->get_comp_idx());
if (p != q && p->get_comp_idx() == q->get_comp_idx())
OJPH_ERROR(0x00030057, "The codestream has two COC marker "
"segments for one component of index %d", p->get_comp_idx());
}
else if (marker_idx == 5)
{ qcd.read(file); received_markers |= 2; }
{
qcd.read(file);
received_markers |= 2;
}
else if (marker_idx == 6)
{
ui32 num_comps = siz.get_num_components();
if (qcc == qcc_store &&
num_comps * sizeof(param_qcc) > sizeof(qcc_store))
{
qcc = new param_qcc[num_comps];
}
qcc[used_qcc_fields++].read(file, num_comps);
}
{
param_qcd* p = qcd.add_qcc_object(param_qcd::OJPH_QCD_UNKNOWN);
p->read_qcc(file, siz.get_num_components());
if (p->get_comp_idx() >= siz.get_num_components())
OJPH_ERROR(0x00030054, "The codestream carries a QCC marker "
"segment for a component indexed by %d, which is more than the "
"allowed index number, since the codestream has %d components",
p->get_comp_idx(), num_comps);
param_qcd *q = qcd.get_qcc(p->get_comp_idx());
if (p != q && p->get_comp_idx() == q->get_comp_idx())
OJPH_ERROR(0x00030055, "The codestream has two QCC marker "
"segments for one component of index %d", p->get_comp_idx());

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

Use siz.get_num_components() in these diagnostics
num_comps is still 0 while read_headers() parses COC/QCC, so these messages can print “0 components”. Use the already-checked siz.get_num_components() 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 `@src/core/codestream/ojph_codestream_local.cpp` around lines 804 - 831, The
COC/QCC diagnostics in read_headers() are using num_comps, which can still be 0
at this point and produce misleading “0 components” messages. Update the
error/info messages in the marker parsing logic around get_coc(), get_qcc(), and
read_qcc() to use siz.get_num_components() directly for the component count,
while keeping the existing component index checks unchanged.

Comment on lines +1059 to +1062
else if (marker_idx == 9)
result = skip_marker(infile, "NLT",
"PPT marker segment in a tile is not supported yet",
OJPH_MSG_WARN, resilient);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Wrong marker name in the NLT warning text.

This branch handles the NLT marker but the message says "PPT marker segment in a tile is not supported yet". Copy/paste from the PPT branch.

✏️ Proposed fix
                 else if (marker_idx == 9)
                   result = skip_marker(infile, "NLT",
-                    "PPT marker segment in a tile is not supported yet",
+                    "NLT marker segment in a tile is not supported yet",
                     OJPH_MSG_WARN, resilient);
📝 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
else if (marker_idx == 9)
result = skip_marker(infile, "NLT",
"PPT marker segment in a tile is not supported yet",
OJPH_MSG_WARN, resilient);
else if (marker_idx == 9)
result = skip_marker(infile, "NLT",
"NLT marker segment in a tile is not supported yet",
OJPH_MSG_WARN, resilient);
🤖 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 `@src/core/codestream/ojph_codestream_local.cpp` around lines 1059 - 1062, The
NLT handling branch in skip_marker currently logs the wrong warning text, which
still says PPT instead of NLT. Update the skip_marker call in the marker_idx ==
9 branch so the message matches the NLT marker being processed, keeping the same
OJPH_MSG_WARN and resilient behavior.

Comment on lines 459 to 465
size_t t = strlen(str);
if (len > 65531)
OJPH_ERROR(0x000500C1,
OJPH_ERROR(0x000500C1,
"COM marker string length cannot be larger than 65531");
this->data = str;
this->data = str;
this->len = (ui16)t;
this->Rcom = 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 | 🟠 Major | ⚡ Quick win

Length guard checks the wrong variable.

t holds the new string length, but the guard tests the (pre-existing/uninitialized) member len. An over-long string bypasses the check and then gets truncated by this->len = (ui16)t, producing a malformed COM segment length. Compare with set_data(), which correctly validates its len parameter.

🐛 Proposed fix
     size_t t = strlen(str);
-    if (len > 65531)
+    if (t > 65531)
       OJPH_ERROR(0x000500C1,
         "COM marker string length cannot be larger than 65531");
📝 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
size_t t = strlen(str);
if (len > 65531)
OJPH_ERROR(0x000500C1,
OJPH_ERROR(0x000500C1,
"COM marker string length cannot be larger than 65531");
this->data = str;
this->data = str;
this->len = (ui16)t;
this->Rcom = 1;
size_t t = strlen(str);
if (t > 65531)
OJPH_ERROR(0x000500C1,
"COM marker string length cannot be larger than 65531");
this->data = str;
this->len = (ui16)t;
this->Rcom = 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 `@src/core/codestream/ojph_params.cpp` around lines 459 - 465, The COM marker
length validation in the parameter setter is checking the wrong value: it should
validate the newly computed string length `t` rather than the member `len`,
which may be stale or uninitialized. Update the length guard in the same code
path that assigns `this->data`, `this->len`, and `this->Rcom` so it rejects any
string longer than the allowed maximum before truncation occurs, matching the
validation approach used by `set_data()`.

Comment on lines +1086 to +1091
p->atk = atk->get_atk(p->SPcod.wavelet_trans);
if (p->atk == NULL)
OJPH_ERROR(0x00050132, "A COC segment employs the DWT kernel "
"atk = %d, but a corresponding ATK segment cannot be found",
SPcod.wavelet_trans);
p = p->next;

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

COC error reports the COD's kernel, not the COC's.

Inside the loop over COC objects (p), the "corresponding ATK segment cannot be found" message interpolates SPcod.wavelet_trans (this == COD_MAIN) instead of p->SPcod.wavelet_trans, so it will report the wrong ATK index for a failing COC.

🤖 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 `@src/core/codestream/ojph_params.cpp` around lines 1086 - 1091, The COC
missing-ATK error in the loop over COC objects uses the wrong wavelet/kernel
value, so the message reports the COD value instead of the current COC’s
setting. Update the OJPH_ERROR call in the code path that checks p->atk after
get_atk so it references the COC object’s SPcod.wavelet_trans from p, keeping
the message aligned with the failing segment.

// new_sig has newly-discovered sig. samples during SPP
// find the signs and update decoded_data
ui64 *dp = dpp + x;
ui64 val = 3u << (p - 2);

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the local context around the suspected line.
sed -n '1528,1562p' src/core/coding/ojph_block_decoder64.cpp

# Find other similar magnitude-shift literals in this file for context.
rg -n "3u <<|3ull <<|1ULL <<|v_n \+ 2" src/core/coding/ojph_block_decoder64.cpp

Repository: cornerstonejs/OpenJPH

Length of output: 1965


🌐 Web query:

OpenJPH 0.30.1 ojph_block_decoder64.cpp 3u << (p - 2) 3ull

💡 Result:

In the OpenJPH library, specifically within the block decoder implementations (such as ojph_block_decoder64.cpp), the code patterns 3u << (p - 2) and 3ull are typically used for bitwise operations related to HTJ2K coefficient decoding [1][2]. 1. 3u << (p - 2): This expression is used to generate a bitmask or shift a value by a dynamic amount, where p relates to the bit-plane index or precision within the HTJ2K decoding process [1][2]. The u suffix specifies that the literal 3 is treated as an unsigned int. 2. 3ull: This is an unsigned long long literal, used to ensure 64-bit precision during bitwise arithmetic to prevent overflow or truncation when working with 64-bit codeblock data [1][2]. Technical Context: These operations appear within the core decoding logic, which handles High-Throughput JPEG2000 (HTJ2K) coefficient reconstruction [1][3]. OpenJPH employs various SIMD and scalar optimizations to accelerate this process [2][4]. Recently, developers have addressed strict-aliasing and undefined behavior issues in these decoder files (e.g., ojph_block_decoder32.cpp and ojph_block_decoder64.cpp) by replacing type-punned pointer accesses (like *(ui32*)) with safer memcpy operations to prevent miscompilation by modern compilers such as GCC 15 [5][4].

Citations:


🌐 Web query:

OpenJPH 0.30.1 raw ojph_block_decoder64.cpp "3u << (p - 2)" OR "3ull << (p - 2)"

💡 Result:

In OpenJPH version 0.30.1, the code in ojph_block_decoder64.cpp utilizes bitwise shift expressions such as 3u << (p - 2) and 3ull << (p - 2). This distinction relates to C++ type handling and potential integer overflow issues, particularly when compiling with newer compilers like GCC 15 [1]. The suffix u denotes an unsigned int (typically 32-bit), whereas ull denotes an unsigned long long (typically 64-bit). In the context of the ojph_block_decoder64.cpp file, these expressions are used for bit-shifting operations to process JPEG2000 Part-15 (HTJ2K) code streams. Using 3u can lead to undefined behavior or truncation if the shift operation results in a value that exceeds the width of a 32-bit integer [1]. The transition to 3ull ensures that the calculation is performed using 64-bit precision, which is necessary to accommodate larger shift amounts without overflowing the underlying type [1]. This specific issue has been tracked in relation to build failures with GCC 15, where stricter type checking and warning levels may flag these expressions as potential overflows [1]. Using the ull suffix correctly promotes the constant to a 64-bit type, ensuring safety and portability across different architectures and compiler versions.

Citations:


Use a 64-bit literal here
3u << (p - 2) is still 32-bit arithmetic, so shifts of 32+ are undefined and the high-precision path loses the intended range. Switch this to 3ull << (p - 2).

🤖 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 `@src/core/coding/ojph_block_decoder64.cpp` at line 1549, The high-precision
shift in the block decoder still uses 32-bit arithmetic, so update the
expression in ojph_block_decoder64.cpp where the value is computed in the
decoder logic to use a 64-bit literal instead of the current unsigned 32-bit
one. Locate the shift in the code path that builds the `ui64 val` value and
change the literal so the operation stays 64-bit throughout, preserving the
intended range for larger shift counts.

master holds the old (~0.21-era) fork state whose custom patches have been
re-applied on top of upstream 0.30.1 in this branch, so its content is fully
superseded. Recorded as a merge parent (strategy=ours) purely to make the PR
fast-forward-mergeable; the tree stays 0.30.1 + the carried resilient-decode
patches.
wayfarer3130 added a commit to cornerstonejs/codecs that referenced this pull request Aug 27, 2026
…ersedes #68) (#76)

codspeed regression is due to parallelization not being consistent in the simulation, not due to code speed changes

* chore(openjphjs): bump extern/openjph submodule to upstream 0.30.1

Points the openjph submodule at cornerstonejs/OpenJPH#5, which rebases the
fork onto upstream OpenJPH 0.30.1 (was ~22 months behind) and re-applies our
custom patches. Net cornerstonejs delta from stock 0.30.1 is 3 lines in
ojph_codestream_local.cpp: resilient=true (tolerate truncated streams) +
suppressed 'File terminated early' log. Dropped the cosmetic SIZ-marker
message renames (conflicted with upstream's swap_byte rename) and the
temporary debug-build toggle.

CI is the first real build/validation of this bump (not built locally).
After OpenJPH#5 merges, re-point this submodule at the merge commit.

* fix(openjphjs): add OpenJPH 0.30.1 header dirs to the wasm target include path

0.30.1 relocated its public headers under src/core/openjph (+ src/core/shared);
our glue's bare <ojph_arch.h> include no longer resolved and the openjphjs wasm
build failed with 'ojph_arch.h file not found'. Add both 0.30.1 header roots to
the openjphjs target's include path.

* fix(openjphjs): link the OpenJPH 0.30.1 'openjph' target (was renamed from openjphsimd)

0.30.1 builds a single architecture-agnostic 'openjph' library; the old
'openjphsimd' target no longer exists, so wasm-ld failed with
'unable to find library -lopenjphsimd'. Link 'openjph', matching upstream's
own wasm wrapper (subprojects/js).

* fix(openjphjs): keep WASM SIMD enabled for OpenJPH 0.30.1

0.30.1 deprecated OJPH_DISABLE_INTEL_SIMD and bridges it onto the new
OJPH_DISABLE_SIMD; our old 'OJPH_DISABLE_INTEL_SIMD=ON' therefore disabled ALL
SIMD (OJPH_ENABLE_WASM_SIMD=OFF), shipping a scalar wasm ~2x slower on decode/
encode. Stop setting the deprecated option and force OJPH_DISABLE_SIMD=OFF so
0.30.1's Emscripten path builds the WASM SIMD kernels (-msimd128).

* fix(openjphjs): build the wasm in Release, not Debug

build.sh forced CMAKE_BUILD_TYPE=Debug, so the shipped openjph wasm was built
-O0 with unoptimized SIMD intrinsics — the reason decode/encode benched far
slower (SIMD-on was even slower than scalar under -O0) and the binary was
oversized. Release (-O3) is the correct artifact and is what makes the 0.30.1
SIMD kernels fast and the wasm small.

* ci: squash in the pnpm migration from #87

Single squashed commit of ci/pnpm-trusted-publishing (#87), on the assumption
that #87 lands on main before this PR. Purpose is measurement: the pnpm
migration shifts CodSpeed's baseline on its own, and the HTJ2K work in this
branch shifts it again, so carrying both here lets one report show the combined
effect instead of attributing the sum to whichever merges second.

Expect this commit to become a no-op the moment #87 merges -- it should then
either drop out of the diff or merge cleanly against itself. It is NOT a second
copy of that work to review; review it in #87.

Merged with no conflicts. Two things worth noting about the overlap:

  - The submodule gitlink stayed at this branch's 0748112b rather than taking
    #87's e01c7b7, because #87 only reverted its own accidental bump back to
    the value main already had. Updated separately in the next commit.

  - dicom-codec/src/codecs/codecFactory.js is touched by both branches and did
    not conflict: #87 changes initialize() (routing emscripten's print through
    the logger) while this branch's carried work from #68 changes decode()
    (decoder reuse). They are independent edits to the same file.

Includes the pnpm.overrides pinning esbuild/rollup/webpack/terser to the
versions yarn.lock resolved, so build output does not drift across the
migration -- relevant here because this PR is measured against those baselines.

* build(openjphjs): track upstream OpenJPH master for the streaming fix

Moves extern/openjph from 0.30.1 + carried patches to cornerstonejs/OpenJPH#6,
which merges upstream master (6f3caf3) with ZERO fork delta.

Why master rather than a release: the streaming/truncated-decode fix landed
upstream as 638ccb4 "Cs3d/truncated decode graceful 0.30.1 (aous72#331)" on
2026-08-08, and the newest upstream release 0.31.0 was published 2026-07-27 --
twelve days earlier. `git tag --contains 638ccb4` is empty, so no tagged release
carries it yet. master is four commits past the fix (three dependabot codeql
bumps and a warning fix). Re-pin to a tag once upstream cuts one with #331.

Both patches the fork used to carry are gone, replaced by public API:

  resilient = true
      codestream::enable_resilience() -- already called by HTJ2KDecoder on main
      (line 270), so this patch was redundant before this PR.

  commented-out OJPH_INFO "File terminated early"
      ojph::set_message_level(OJPH_MSG_WARN), set here in jslib.cpp.

The message level is worth its own note. OpenJPH INFO goes to stdout, which
emscripten forwards to console.log, and there were two sources of per-decode
noise: HTJ2KDecoder's constructor banner ("v06 HTJ2K Decoder") on every
construction, and "File terminated early" on every resilient decode of a
truncated stream -- which with streaming support is the normal case. Raising the
threshold to WARN drops both and keeps warnings and errors, so it replaces a
source patch with a supported call and makes future upstream bumps
fast-forwards.

Verified: OJPH_DISABLE_SIMD still exists upstream (this branch's FORCE OFF is
still correct), and every header HTJ2KDecoder.hpp includes is present under
src/core/openjph on master.

* fix(openjphjs): decode truncated HTJ2K streams without a known full length

Carried from #68, which this PR supersedes. Previously the decoder could handle
a partial HTJ2K stream only if the caller already knew the full length; now a
truncated buffer decodes as far as its data allows.

readHeader, decode and decodeSubResolution wrap their codestream work in
try/catch and report instead of propagating, so resilient mode's throw at the
end of the available data yields a partial image rather than a failed decode.
frameInfo_ keeps whatever the header established, so dimensions survive.

Two deliberate changes from #68's version:

  - The diagnostics are OJPH_WARN, not OJPH_INFO. jslib.cpp raises OpenJPH's
    threshold to WARN to kill the per-construction banner, so INFO here would be
    dropped exactly when a decode failed.

  - DISABLE_EXCEPTION_CATCHING flips 1 -> 0 (double negative: catching ENABLED).
    This is required, not stylistic: with catching disabled emscripten compiles
    the handlers out and the throw terminates the module instead of being
    caught. It costs wasm size, so dist-size may need re-baselining.

test/truncated.test.js covers truncated and lossy decodes, and decoder reuse
across 500 decodes. NOTE: its performance assertions are wall-clock
(reused-faster-than-fresh, and a min/max ratio across milestones), so they are
inherently softer than the pixel-exactness tests and may prove flaky on shared
CI runners. Worth watching, and worth converting to a looser bound or dropping
if they turn noisy.

The core-side work is upstream as of aous72#331, so this is only the emscripten
wrapper plus tests -- the corresponding fork patches are gone.

* perf(dicom-codec): reuse the HTJ2K decoder instead of one per frame

Carried from #68. codecFactory.decode gains an opt-in reuseDecoder option: the
decoder is held on codecConfig (the per-codec singleton the wrapper modules
already share) and not deleted after each call. htj2k.js opts in; every other
codec keeps the construct-and-delete behaviour.

This is very likely the bulk of #68's measured 3.5x speedup on the dicom-codec
dispatch bench for HTJ2K -- CodSpeed reported 141.4ms -> 40.6ms there, and 40.6ms
is about what openjphjs' own direct decode benches cost, i.e. reuse closes the
gap between dispatching through this factory and calling the codec directly.
Constructing a wasm decoder per frame allocates heap and registers embind
bindings each time; for openjph it also ran the constructor banner through the
console on every frame.

Opt-in rather than default on purpose: a decoder that carries state between
decodes, or whose retained buffers grow without bound, must not enable it.
openjphjs' reuse test covers the consequence that matters for HTJ2K -- 500
successive decodes on one instance without progressive slowdown.

Independent of #87's change to the same file: that one routes emscripten's print
through the logger in initialize(); this one changes decode(). They merged with
no conflict.

* test(openjphjs): make the decoder-reuse perf assertion measure something real

Built openjphjs locally via tools/docker/build.sh and ran the suite, which is
how this surfaced: "reused decoder is faster than instantiate+decode+destroy per
frame" FAILED locally (3.34 ms vs 2.72 ms) while passing CI by 5% (2.38 vs 2.50).

The original had a structural flaw, not bad luck. It took ONE sample per path
with no warmup, and measured the reused path FIRST -- so V8's JIT warmup was
charged to exactly the side the assertion expects to win. Construction costs well
under a millisecond against a ~2.5 ms decode, so a single cold sample measures
warmup rather than the difference under test.

Fixed the measurement: warm both paths, then compare medians of 25 iterations.
That removed the order bias but showed the assertion itself is not sound at this
granularity -- warmed, construct+decode+destroy costs about the same as decode
alone (~1.6 ms each), so the medians sit inside each other's noise. Eight
observed runs produced two failures on unchanged code.

So the assertion is now a bound in the useful direction: reuse must not be
materially SLOWER (the real risk, e.g. retained state degrading each decode)
rather than provably faster. The medians are still logged.

This does not weaken the perf claim, it relocates it to the tool that can
actually measure it. CodSpeed on this branch reports the dispatch bench
141.5 ms -> 24.1 ms and instantiate+destroy HTJ2KDecoder x50 2315 us -> 458 us,
because Simulation counts instructions where wall-clock at ~3% of a decode
cannot resolve it. The 500-decode stability test is untouched and still guards
the thing that matters for reuse: no progressive slowdown from retained buffers.

Verified: 5 consecutive local runs stable, full openjphjs suite 30 passed.

* fix(htj2k): own the decoded buffer and surface swallowed decode failures

Addresses the review findings on #76, all of which stem from the same two
changes in this PR: reusing one HTJ2K decoder across a series, and swallowing
OpenJPH's exceptions so a partial codestream degrades to a partial image.

Buffer ownership (the critical one). getDecodedBuffer()/getEncodedBuffer()
return an emscripten typed_memory_view -- a live window onto the wasm heap
owned by the codec instance. Returning it as `imageFrame` was wrong three ways:
delete() frees the memory it points at, the next decode on a reused instance
overwrites it, and heap growth detaches it outright. Frames 1..n of a series
all showed frame n. copyFromWasm() now copies on both the decode and encode
paths -- unconditionally, because the non-reuse path was already handing back
memory delete() had just freed. Measured side effect: the raw view's .buffer is
the whole 50 MB heap, so callers passing imageFrame.buffer to a worker were
transferring the heap rather than the frame.

Failure reporting. decode()/readHeader()/decodeSubResolution() returned
normally after swallowing an exception, so codecFactory reported success and
the OJPH_WARN went to a logger that is silent unless setVerbose. HTJ2KDecoder
now exposes getIsHeaderValid()/getLastErrorMessage(), reset per call, and
codecFactory throws on an invalid header while flagging processInfo.partial
otherwise. Verified: before this, garbage input on a reused decoder resolved
successfully with the previous slice's pixels under the new frame's metadata.

Stale pixels. decode_ used resize(), which only value-initialises NEW elements,
so anything the decoder did not write kept the previous frame's pixels. Now
assign(size, 0), which zero-fills without giving up the capacity reuse depends
on. readHeader_ likewise resets every header-derived field before parsing, and
the previously uninitialised members (numDecompositions_, numLayers_, ...) get
initialisers -- a failed parse on a fresh decoder was doing arithmetic on heap
garbage.

Two comments in this PR claimed things that turned out to be false, corrected
in place. Truncation does NOT throw into decode()'s catch: swept CT1.j2c at
every length from 60 bytes up plus 875 single-byte corruptions and not one
input aborts mid-decode -- resilient mode absorbs a short codestream as zero
coefficients and reports success. The reachable stale-buffer window is
restrict_input_resolution() throwing for a decomposition level the codestream
does not carry, which is what the new regression test uses (it fails against
resize() with 128 stale bytes; a truncation-based test passes either way and
tests nothing).

Also:
- unset(OJPH_DISABLE_INTEL_SIMD CACHE): deleting the option() line does not
  remove it from an existing CMakeCache.txt, and upstream's `DEFINED` bridge
  shadows the forced OJPH_DISABLE_SIMD=OFF, so incremental local builds kept
  shipping the scalar wasm this PR's own comment warns about.
- releaseDecoder()/htj2k.release()/dicomCodec.release(): a reused decoder held
  its largest frame's buffers for the module's lifetime with no way to free.
- dist-size baseline for openjphjs: stale at 2241 KiB against a 293 KiB
  artifact, because THIS PR switched the build Debug -> Release. The gate only
  fails on growth, so it would have tolerated a 7.7x regression.

Rebuilt with tools/docker/build.sh (emsdk 3.1.74, SIMD confirmed intact);
openjphjs 34 passed, dicom-codec 41 passed, other codec suites unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.