Releases: osamu620/OpenHTJ2K
Release list
v0.19.0
First feature release since the 0.18.x security line (148 commits since
0.18.5). Headlined by a new WebTransport browser viewer for live RFC 9828
streams, substantial encoder and decoder performance gains, and broad
WASM and JPIP-viewer improvements.
Added
WebTransport browser viewer
- New browser-native viewer (
web/wt_viewer) that decodes live RFC 9828
(video/jpeg2000-scl) streams in WebAssembly and renders them through a
WebGL2 pipeline. Includes overload and RTP-clock-drift telemetry, an
opt-in RTP-timestamp pacer (?pace=rtp), and automatic reconnect on
WebTransport session loss. - New
wt_bridge: a Go UDP→WebTransport relay that forwards live RFC 9828
RTP to browser clients over QUIC. Ships with LAN and split-host launch
scripts, dev-certificate SAN handling, and--initial-mtu,--log, and
--pprofflags. - Shared decoder Web Worker used by both the WebTransport viewer and the
RTP demo, with batched packet delivery to reduce postMessage overhead.
WASM decoder
- SharedArrayBuffer zero-copy plane delivery.
- Optional skip of the CPU color transform when a GPU shader performs the
YCbCr→RGB conversion. - Configurable decode-ring depth, drop/underrun statistics, pre-decode
skip, and a network-underrun ("Low bandwidth") indicator that rebuffers
on recovery.
JPIP viewer
- Touch support for iPad / iPhone, including coherent two-finger trackpad
scrolling. - Carry-forward of the previous frame during pan/zoom behind a latency
gate, for smoother interaction under load. - Scrubber-style drag on the thumbnail overview.
- GPU YCbCr→RGB color conversion for the JPIP demos.
Tools
estimate_qfactorutility.- Shared image-I/O library (
openhtj2k_imgio) for PNM/PGX/TIFF readers,
plus streaming TIFF input.
Performance
Encoder
- HT cleanup encoder reworked around a two-pass architecture with deferred
MagSgn emission and batched VLC/UVLC emission: full two-pass paths on
AVX2 and AVX-512, plus deferred-MagSgn (emitFlat) emission on NEON. - Quantization fused into the DWT sink, bypassing the intermediate sample
buffer; int32 5/3 reversible FDWT wired into the streaming encoder. - Streaming path rebuilt around a per-strip ring scratch that drops
intermediate buffers, roughly halving peak resident memory (~120 MB less
on a 4K frame). - TLM measurement pass skipped for single-tile codestreams; thread-local
output and scratch reused across encodes. - Measured against 0.18.5 on a 4K test image (x86-64): encoder wall time
down roughly 20% across repeated-encode runs (scratch reuse) and peak RSS
roughly halved, with bit-identical output verified by the conformance
suite.
Decoder
- int32 reversible 5/3 IDWT pipeline.
- Below-viewport fast-forward and a skipped row-all-zero scan when the
source zero-fills.
Fixed
- NEON: correct fused MCT+finalize (
vfmaq/vfmsq), shared IDWT helpers
across batch and stream paths, and scalar-tail correctness in dequant
and ICT. - Coding: height-parity guards on the SIMD fused-dequant paths; 32-bit
multiplications widened tosize_tbeforememset. - JPIP:
highpfloats in fragment shaders for Nvidia/Linux, R8 texture
upload alignment and init-race fixes, and MCT-flag-based YCbCr detection. - WASM: cwrap guarded against a stale module cache; adaptive pace-drop with
event-driven backpressure and timeline-rebasing fixes. - Web: stale service-worker cleanup, pthread-pool sizing for HTTP/1.1
connection limits, and HTTPS-over-HTTP/2 static serving. - Resolved all compiler warnings in Release builds.
Removed
- Batch encode/decode mode, the dead predecoded decoder path and its
bypass_decodeflag, and thelb_comparetool with itslbs_*
streaming comparison tests.
Changed
- Consolidated the browser/WASM tree under
web/. - Relocated test/check tools from
source/appstotests/tools. - CLI decoder default thread count capped at 8.
Full changelog: v0.18.5...v0.19.0
v0.18.5 - Security Fix: SOT Out-of-Bounds Tile Index Heap Info Leak
🔒 Critical Security Release
This release addresses a heap out-of-bounds access in tile-part dispatch, responsibly disclosed by Korean security researchers as a follow-up to v0.18.4.
Security Impact
- CVE: CVE-2026-TBD (pending MITRE assignment)
- Severity: High — heap OOB read/write with confirmed server-side heap information leak
- Component:
j2k_tile::add_tile_part()reached via uncheckedIsot(SOT tile index) insource/core/interface/decoder.cpp - Attack vectors: Crafted J2K/JP2 files; reachable in the JPIP server's startup codestream load (
PacketLocator::build) and in every public decoder entry point (invoke(),invoke_line_based(),invoke_line_based_stream(),invoke_line_based_predecoded()) - Confirmed primitive: server-surviving heap information leak — heap pointer values written by
add_tile_part()into memory adjacent to thetileSetallocation can be reflected back to the client via subsequent JPIP tile-header data-bin responses
Fix Details
- Added
tile_index >= tileSet.size()validation at all four SOT-dispatch sites insource/core/interface/decoder.cpp - Out-of-range tile indices now throw
std::exception()with a diagnostic before any write through the offending pointer - Verified under AddressSanitizer that the published PoCs (
poc_01_server_surviving_heap_info_leak_*.j2k,poc_02_server_heap_info_leak_*.j2k) no longer trigger any heap-buffer-overflow
Migration Notes
- No API changes. All valid inputs are unaffected.
- Malformed inputs with
Isot >= numTilesare now rejected at parse time instead of silently corrupting heap memory.
Credit
Vulnerability discovered and responsibly disclosed by KANG DAEUN (SANGMYUNG UNIVERSITY) and OH HAN GUEL (SANGMYUNG UNIVERSITY, @5asever40-a11y).
v0.18.4 - Security Fix: PPM Packet Header Heap Buffer Overflow
🔒 Critical Security Release
This release addresses a heap buffer overflow vulnerability in PPM packet header parsing, responsibly disclosed by Korean security researchers.
Security Impact
- CVE: CVE-2026-TBD (pending MITRE assignment)
- Severity: High — heap buffer overflow write, potential for crash or exploitation
- Component:
j2k_codeblock::pass_length[128]array inj2k_precinct_subband::parse_packet_header() - Attack vectors: Malicious JPIP server responses, crafted J2K/JP2 files processed by decoder or JPIP server
Fix Details
- Added
kMaxCodingPasses = 128constant matching thepass_lengtharray size - Bounds-checked in three vulnerable code paths before updating
num_passes:- General packet header parsing
- HT mode with secondary passes
- Bypass mode with multiple segments
- Fail-fast: throws
std::exception()when bounds would be exceeded — prevents heap corruption entirely
Migration Notes
- No API changes. All valid inputs are unaffected.
- Malformed inputs are now cleanly rejected instead of causing undefined behaviour.
Credit
Vulnerability discovered and responsibly disclosed by KANG DAEUN (SANGMYUNG UNIVERSITY) and OH HAN GUEL (SANGMYUNG UNIVERSITY, @5asever40-a11y).
Credit corrected on 2026-04-28 per the reporters' follow-up request — the original release notes attributed the disclosure incorrectly.
v0.18.3 — WASM decoder: SIMD pack paths and MT CLI freeze fix
[0.18.3] - 2026-04-22
WASM decoder — SIMD pack paths for 16-bit and planar-u8 output
Two WASM-SIMD patches to the decoder wrapper, plus a CMake bug fix that
was causing the non-threaded libopen_htj2k_simd variant to silently
run scalar pack code. Output is byte-exact with the previous builds
on every file we tested; the change is pure perf.
Measured on a 3840×2160 12-bit YCbCr codestream, Ryzen 9 9950X / Node 24:
| Config | Before | After | Speedup |
|---|---|---|---|
libopen_htj2k_simd, PPM output |
105.0 ms | 81.0 ms | 1.30× |
libopen_htj2k_mt_simd 2T, PPM output |
63.8 ms | 42.1 ms | 1.52× |
libopen_htj2k_mt_simd 4T, PPM output |
64.9 ms | 36.9 ms | 1.76× |
libopen_htj2k_simd, planar-u8 output |
94.3 ms | 85.2 ms | 1.11× |
libopen_htj2k_mt_simd 2T, planar-u8 output |
52.1 ms | 45.2 ms | 1.15× |
libopen_htj2k_mt_simd 4T, planar-u8 output |
50.0 ms | 41.5 ms | 1.20× |
WASM
invoke_decoder_stream— new WASM-SIMD pack block for the
nc==3 && !subsampled && 16-bit big-endiancase (12–16 bpc RGB
PPM output). Processes 8 samples per iteration across R/G/B,
writing 48 output bytes per three v128 stores via an i32→u16 narrow,
per-pair byte-swap, and 3-way shuffle interleave. The previous
code took the scalar fallback here even on the simd variant, so
this is the single biggest line-item in the stream-mode speedup.invoke_decoder_planar_u8— new WASM-SIMD rescale for the
per-component inner loop (add DC-offset bias, right-shift for
depths ≥ 8, left-shift for depths < 8, clamp to [0,255], narrow
i32→u8, 16-byte store). Used bysubprojects/rtp_demo.htmlto
upload three R8 textures per frame; previously scalar.- Byte-exact against pre-patch output on all 137
wasm_*
conformance tests, plus explicit plane-level md5 checks on
8-bit 4:4:4, 12-bit 4:4:4, 12-bit 4:2:2, and 4-bit grayscale
fixtures for the planar-u8 path (which has no conformance-test
coverage).
Build
- Fixed
libopen_htj2k_simdtarget insubprojects/CMakeLists.txt
missingOPENHTJ2K_ENABLE_WASM_SIMD. The target was being compiled
with-msimd128but without the macro, so every
#if defined(OPENHTJ2K_ENABLE_WASM_SIMD)branch inwrapper.cpp
(grayscale pack, 8-bit RGB pack, YCbCr→RGBA) was compiled out and
the variant silently ran scalar wrapper code. The mt_simd target
had the define and was unaffected. - C++17 required for the subproject build — rtp_reassembly
object libraries usestd::optional. - Dropped redundant
-s SHARED_MEMORY=1from the MT link flags;
-pthreadimplies it, and Emscripten 3.1.x rejected it as unknown. - New option
OPENHTJ2K_WASM_PROFILE(defaultOFF) passes
--profiling-funcsto the Wasm linker sonode --cpu-profand
perf record --perf-basic-profresolve function names. ~15% .wasm
size overhead, zero runtime cost (verified: release vs profile
builds are within run-to-run noise on both simd 1T and mt_simd 2T).
CLI
subprojects/open_htj2k_dec.mjsnow callsprocess.exit(0)once
the output file has been written. On multi-threaded WASM builds
(-num_threads > 1) Emscripten's pthreads layer keeps
worker_threadsworkers parked waiting for a teardown signal that
never comes, so the CLI appeared to hang indefinitely after a
successful decode. The decoded output was always correct; only the
process lifetime was wrong.
Tools
subprojects/wasm_bench.mjs— new iteration-loop bench harness.
Loads a chosen WASM variant once, runs N decode iterations, reports
min/median/p95/mean wall-clock plus Msamples/s and fps. Supports
--mode stream|planar_u8,--dump-planes <prefix>for byte-exact
regression-checking, and--build-dirfor A/B between two build
outputs.
Documentation
docs/wasm_bench.md— prerequisites, synopsis, full option
table, worked examples (baseline throughput, thread sweep,
RTP-demo-shaped planar-u8 path, byte-exact regression check), and a
profiling section that covers bothnode --cpu-proffor
single-threaded variants andperf record --perf-basic-proffor the
multi-threaded variants (where--cpu-profsilently produces no
output because Emscripten's pthread teardown bypasses Node's
profile finalizer). Linked fromdocs/README.mdunder a new
"Developer tools" section.
v0.18.2 — JPIP gigapixel viewer: mid-decode paint on slow fetches
Third of three small latency wins queued after v0.18.0's client-side precinct cache: on a slow fetch, show the user a partial render (whatever precincts have already arrived) before the final byte lands. Purely client-side — no server / WASM / library changes.
Gated on wall-clock elapsed so fast fetches (LAN, cache hits) never pay the extra-decode cost.
Browser viewer
- Extracted
paintDecodedRegion()helper so the mid-paint and final paint share one code path. - Inside the
_jpip_feed_streamread loop, if?midPaintMs(default 200 ms) has elapsed and no mid-paint has fired yet, the viewer runsjpip_end_frame_region+texSubImage2Dagainst the partialDataBinSet. One mid-paint per response at most. - Final paint at end-of-response always runs and re-decodes with the complete
DataBinSet, so the on-screen result is correct. - Stats bar now reports
mid-paint=<N>mswhen the branch fires.
New URL parameter
?midPaintMs=N— wall-clock threshold in milliseconds; default200, pass0to disable. Default chosen so a typical 1920 full-res LAN response (~50 ms) never triggers, but a multi-MB first-pan response over home broadband does.
Cost model
| Scenario | Overhead |
|---|---|
| Fetch < 200 ms (LAN, cache hit) | None — branch not taken |
| Fetch > 200 ms (slow link, first pan into a fresh region) | One extra jpip_end_frame_region (~20–50 ms CPU on a 1920 viewport) |
Trade: see a partial image 150–500 ms earlier on slow links, at the cost of one redundant decoder run when the threshold is crossed.
Arc recap — v0.18.0 → v0.18.2
Three composable client-side latency wins for the gigapixel viewer, each independently toggleable:
- v0.18.0 — LRU precinct cache. Second + later pans reuse precincts from previous pans; same viewport = 3-byte EOR.
- v0.18.1 — adjacent-viewport halo prefetch. After a pause, fetch precincts just outside the viewport so the next short pan hits the cache.
- v0.18.2 — mid-decode paint on slow fetches. Feedback on first-time loads while the full response is still arriving.
Verified: 616/616 ctests on all runners (macOS clang + intel, Ubuntu x86_64/arm64 gcc + clang, Windows-latest/arm, WASM).
See CHANGELOG for the full 0.18.2 entry.
v0.18.1 — JPIP gigapixel viewer: adjacent-viewport halo prefetch
Builds on v0.18.0's LRU precinct cache: after each successful render, if the user pauses for a short idle period, prefetch a halo of precincts just outside the viewport so a follow-up pan hits in the cache and the per-pan query collapses to the 3-byte EOR marker.
Purely client-side — no server, WASM, or library changes. Gated on a user-driven idle period so continuous panning never triggers the prefetch; bandwidth cost is paid only when the user actually pauses to examine the viewport.
Measured (1920×1920 asset, live server)
| Scenario | Response bytes |
|---|---|
| Main fetch, 320×320, no cache | 28,863 |
| Halo fetch, 576×576 (main + 128 px on each side), no cache | 37,206 |
| Halo fetch, main's precincts already cached | ~10 KB halo-exclusive |
| Halo fetch, everything cached | 3 |
So a typical pause pays about 1/3 of a viewport-worth of bandwidth to make the next short pan free.
Browser viewer
- After each successful render, arms a
setTimeout(prefetchAdjacent, PREFETCH_DELAY_MS)(default 150 ms). On fire, issues a&rsiz=<halo>&model=<cache-model>fetch expanded byPREFETCH_MARGIN_PXon each side of the just-rendered region. - Response is piped through
_jpip_feed_stream_begin / _feed / _end— same WASM path as the main fetch — but skips_jpip_end_frame_region(nothing to paint, just fill the cache). scheduleFetch()(called on every pan / zoom / key event) clears the timer and aborts the in-flight halo viaAbortController; bytes already landed in the LRU stay useful, anything past the abort is dropped.
New URL parameters (viewer only)
| Param | Default | Meaning |
|---|---|---|
?prefetchMargin=N |
128 |
Halo width in canvas pixels, applied to all four sides. 0 disables. |
?prefetchDelayMs=N |
150 |
Idle delay (ms) before the halo fires. |
Migrating
Existing deployments get the feature for free after upgrading — opt out with ?prefetchMargin=0. The foveation demo (jpip_demo.html) is untouched: it has its own fetch logic and doesn't use the viewer's halo prefetch.
Verified: 616/616 ctests on all runners (macOS clang + intel, Ubuntu x86_64/arm64 gcc + clang, Windows-latest/arm, WASM). Live smoke test confirms the halo query format is correct and collapses to a 3-byte EOR when everything is already cached.
See CHANGELOG for the full 0.18.1 entry.
v0.18.0 — JPIP gigapixel viewer: client-side LRU precinct cache
Biggest single latency win available for the pan-and-zoom viewer: stop refetching precincts the client already has.
The viewer's WASM side already accumulates precincts in its DataBinSet across frames, but previous versions deliberately skipped class 0 / 1 in the §C.9 &model= advertisement because the foveation demo wanted its periphery to decay each frame. Wrong default for the gigapixel viewer — pans into an overlapping region should be near-free.
Opt-in LRU precinct cache flips the behaviour for the viewer while leaving the foveation demo alone. Wire-bytes reduction is proportional to precinct overlap between pans; on a fully-cached view-window the response collapses to a 3-byte EOR marker.
Measured (1920×1920 asset, live server)
| Request | Cache advertised | Response bytes |
|---|---|---|
| 1 | none | 28863 |
| 2 | headers only | 28630 |
| 3 | headers + all precincts | 3 |
Library
DataBinSet::erase(class_id, in_class_id)— drops a bin's bytes and itsis_lastflag so a subsequent server re-send can legitimately re-fill it. Primitive used by the viewer's LRU to keepDataBinSetconsistent with the cache-model advertisement.CacheModel::unmark(class_id, in_class_id)— complement tomark(); removes a bin so the next&model=no longer claims it.
WASM
Three new exports in subprojects/src/jpip_wrapper.cpp:
jpip_track_precincts_in_cache(ctx, enabled)— opt-in flag. When flipped on, back-fills the LRU fromctx->setso the first&model=after enable is accurate.jpip_set_precinct_cache_budget(ctx, lo, hi)— 64-bit budget split into two 32-bit halves for Emscripten's i32 calling convention. Default 64 MB.jpip_get_precinct_cache_count(ctx)— diagnostic.
LRU semantics: insertion = touch = push-back; eviction = pop-front; re-hits move to back. Eviction drops the bin from ctx->set, client_cache, and the LRU list + index in one step so DataBinSet::append() never rejects a re-send of an evicted bin.
Browser viewer
- Enabled in
jpip_viewer.htmlimmediately afterjpip_create_context(). - New URL param
?precinctCacheMB=N(default64,0disables). CacheModel::format()already range-compresses (P0-99,P101,P103-200) so&model=stays short even on 40k-precinct assets.
Migrating from v0.17.x
- External callers of
jpip_add_response/jpip_feed_streamunchanged — precinct tracking stays off until the caller opts in. jpip_demo.html(foveation) unchanged — periphery still decays per frame because it intentionally does not opt in.jpip_viewer.html(gigapixel) sees progressively smallerfetch=...mson repeat pans; the stats bar'sbytesnumber drops proportional to viewport overlap.
Verified: 616/616 ctests on macOS (clang + intel), Ubuntu x86_64/arm64 (gcc + clang), Windows-latest/arm (cl), WASM. WASM build produces all four variants.
See CHANGELOG for the full 0.18.0 entry.
v0.17.2 — JPIP server: default to Content-Length; chunked opt-in
Interop fix: some reference interactive JPIP clients do not support HTTP/1.1 Transfer-Encoding: chunked parsing and report "connection closed unexpectedly" after the first response because they size their receive buffer by the absent Content-Length header. Making chunked the default in v0.17.0 was a regression against those clients.
Server
- Default wire format is now Content-Length (was chunked since v0.17.0).
--chunkedopts in toTransfer-Encoding: chunked. Flushes each JPP-stream message to the socket as it is produced; keeps the ~17× loopback TTFB win on large responses for clients that handle chunked transfer (our browser demos + C++JpipClientboth do).--no-chunkedis retained as a recognised flag (now a no-op, since that's the default) so existing scripts / deploy recipes continue to work unchanged.
No wire / library / test changes
The stream_jpp_response primitive, StreamingJppParser, parse_http_chunked / decode_chunked_body in jpip_response.{hpp,cpp}, JpipClient::fetch_streaming, and the WASM jpip_feed_stream_* entry points are all unchanged — they are simply no longer exercised unless the operator passes --chunked. 616/616 ctests pass, both modes emit byte-identical JPP-stream bodies.
Browser demos
Unchanged JS code. fetch().body.getReader() + _jpip_feed_stream* works identically on Content-Length responses (the browser surfaces fewer/larger chunks). Comments in both jpip_demo.html and jpip_viewer.html that previously claimed chunked was the default are softened to describe the capability.
Migrating from v0.17.0 / v0.17.1
If you've been deploying the server as-is: expect Content-Length responses after upgrading. Add --chunked to the command line if your clients support it and you want the progressive-delivery TTFB win.
If you've been passing --no-chunked: no change — the flag is now a no-op that still produces Content-Length.
See CHANGELOG for the full 0.17.2 entry.
v0.17.1 — JPIP gigapixel viewer render-target cap + layout polish
Patch release refining the v0.17.0 browser demos based on real-display testing. No wire-format, library, or server changes — purely JPIP gigapixel viewer UX + documentation fixes.
Gigapixel viewer (subprojects/jpip_viewer.html)
?maxSize=WxH(default1920x1080). Caps the WebGL render target + server-side precinct fetch so per-frame decode cost on 4K / ultrawide displays doesn't scale with the window. The GPU upsamples the capped texture viaGL_LINEAR. Pass?maxSize=windowto disable the cap.?fit=stretch|contain(defaultstretch). Companion knob:stretchCSS-scales the capped render target up to fill the window (v0.17.0 behaviour);containshows the canvas at native pixel scale centered in the window — matching how the foveation demo frames its fixed-size output. Pixel-accurate, at the cost of unused screen real estate around the canvas on displays larger than the cap.- Letterbox styling in
fit=contain. Diagonal dark-stripe pattern for the body background + thin accent-blue outline + soft drop shadow on the canvas, so the centered rectangle is visible against the dark UI theme. - Window-resize handler (100 ms debounced). Drag-resize now re-centers the canvas, resets
gl.viewport, clamps pan, and refetches. Pure CSS-only resizes (render target unchanged) skip the refetch.
Demo help table fixes
- Both demos' cache-model tables documented the precinct data-bin prefix as
Hp(pre-v0.16.1). Per §C.9.3.1 it isP; the server + library have emitted / acceptedPsince v0.16.1. HTML now matches. ?fit=help row gained a line break sostretchandcontaindon't run together visually.
Noted retroactively in CHANGELOG
The v0.17.0 tag commit (e70e432) carried two portability fixes that the release notes didn't call out:
- libstdc++ needs an explicit
#include <cstring>forstd::memchr(libc++ pulls it in transitively). - MSVC's
<windows.h>defines aminmacro that manglesstd::min(a, b); wrapping as(std::min)(a, b)suppresses the macro expansion.
Both already shipped in v0.17.0; noting here for bisect purposes.
Verified: 616/616 ctests on macOS (clang + intel), Ubuntu x86_64/arm64 (gcc + clang), Windows-latest/arm (cl), WASM. Both browser demos verified on a real browser.
See CHANGELOG for the full 0.17.1 entry.
v0.17.0 — progressive HTTP-chunked JPIP delivery end-to-end
Stateless JPIP server + every in-repo client now pipeline JPP-stream responses on the wire and through the parser, so large view-window responses start landing in the decoder before the server has finished building them. Closes #297.
Loopback TTFB for a 24 MB full-canvas response drops from 7.4 ms → 0.44 ms (~17×). HTTP/1.1 bodies remain byte-identical between the chunked and Content-Length paths.
Server
- HTTP/1.1
Transfer-Encoding: chunkedby default — every JPP message emitted by the newstream_jpp_responseprimitive is flushed to the socket as its own HTTP chunk. §C.6.1len=rollback still works, inside a reusable scratch buffer.--no-chunkedopts out for clients that can't parse chunked. build_jpp_streampreserved as a thin wrapper — benchmarks + tests that want a buffered response are unchanged.- HTTP/3 path stays buffered (progressive DATA-frame delivery would need an nghttp3 data-reader refactor; documented inline, not in this release).
Library
StreamingJppParser(new): resumable JPP parser that carriesMessageHeaderContextacrossfeed()calls and buffers the tail bytes of an incomplete message until the next chunk arrives.parse_jpp_streamrefactored to share atry_decode_one_messagehelper with the streaming parser — one decode path, no drift.JpipClient::fetch_streaming(..., on_progress)(new): incremental HTTP chunked state machine feeds straight intoStreamingJppParser.JpipClient::fetch(...)now delegates to it, so every existing caller gets streaming for free.
WASM bindings
- New exports
jpip_feed_stream_begin / _feed_stream / _feed_stream_endalongside the existingjpip_add_response(preserved for one-shot callers).
Browser demos
jpip_demo.html(foveated) andjpip_viewer.html(gigapixel) swapfetch().arrayBuffer()forresponse.body.getReader()+ a_jpip_feed_streamloop.- Auto-prefix
http://on barehost:portserver inputs (fixes a pre-existing "URL scheme 'localhost' is not supported" fetch error).
Test coverage
jpip_streaming_parser_{p0_04,ht_01}— every two-chunk split offset on real JPP-streams, plus byte-at-a-time feeds, must reconstruct an identicalDataBinSetto the one-shot parser.jpip_chunked_roundtrip— end-to-end chunked responder drivesfetch_streamingwith a progress callback.jpip_http_checkextended with chunked formatter +decode_chunked_bodyround-trips and malformed-input rejection.- Node integration (
tools/jpip_wasm_feed_test.mjs) asserts WASM streaming state matches_jpip_add_responsestate across chunk sizes {1, 7, 100, 1024, full}.
Verified: 616/616 ctests on macOS (clang + intel), Ubuntu x86_64/arm64 (gcc + clang), Windows-latest/arm (cl), WASM. Both browser demos verified against a live chunked server.
See CHANGELOG for the full set of changes.