Skip to content

Releases: osamu620/OpenHTJ2K

v0.19.0

Choose a tag to compare

@osamu620 osamu620 released this 30 May 09:13

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
    --pprof flags.
  • 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_qfactor utility.
  • 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 to size_t before memset.
  • JPIP: highp floats 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_decode flag, and the lb_compare tool with its lbs_*
    streaming comparison tests.

Changed

  • Consolidated the browser/WASM tree under web/.
  • Relocated test/check tools from source/apps to tests/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

Choose a tag to compare

@osamu620 osamu620 released this 28 Apr 04:17

🔒 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 unchecked Isot (SOT tile index) in source/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 the tileSet allocation 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 in source/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 >= numTiles are 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).

⚠️ All users running JPIP servers or decoding untrusted JPEG 2000 content should upgrade to v0.18.5.

v0.18.4 - Security Fix: PPM Packet Header Heap Buffer Overflow

Choose a tag to compare

@osamu620 osamu620 released this 27 Apr 12:40

🔒 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 in j2k_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 = 128 constant matching the pass_length array 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.

⚠️ All users processing untrusted JPEG 2000 content or running JPIP servers should upgrade immediately. See v0.18.5 for an additional related security fix.

v0.18.3 — WASM decoder: SIMD pack paths and MT CLI freeze fix

Choose a tag to compare

@osamu620 osamu620 released this 22 Apr 21:44

[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-endian case (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 by subprojects/rtp_demo.html to
    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_simd target in subprojects/CMakeLists.txt
    missing OPENHTJ2K_ENABLE_WASM_SIMD. The target was being compiled
    with -msimd128 but without the macro, so every
    #if defined(OPENHTJ2K_ENABLE_WASM_SIMD) branch in wrapper.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 use std::optional.
  • Dropped redundant -s SHARED_MEMORY=1 from the MT link flags;
    -pthread implies it, and Emscripten 3.1.x rejected it as unknown.
  • New option OPENHTJ2K_WASM_PROFILE (default OFF) passes
    --profiling-funcs to the Wasm linker so node --cpu-prof and
    perf record --perf-basic-prof resolve 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.mjs now calls process.exit(0) once
    the output file has been written. On multi-threaded WASM builds
    (-num_threads > 1) Emscripten's pthreads layer keeps
    worker_threads workers 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-dir for 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 both node --cpu-prof for
    single-threaded variants and perf record --perf-basic-prof for the
    multi-threaded variants (where --cpu-prof silently produces no
    output because Emscripten's pthread teardown bypasses Node's
    profile finalizer). Linked from docs/README.md under a new
    "Developer tools" section.

v0.18.2 — JPIP gigapixel viewer: mid-decode paint on slow fetches

Choose a tag to compare

@osamu620 osamu620 released this 21 Apr 09:20

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_stream read loop, if ?midPaintMs (default 200 ms) has elapsed and no mid-paint has fired yet, the viewer runs jpip_end_frame_region + texSubImage2D against the partial DataBinSet. 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>ms when the branch fires.

New URL parameter

  • ?midPaintMs=N — wall-clock threshold in milliseconds; default 200, pass 0 to 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:

  1. v0.18.0 — LRU precinct cache. Second + later pans reuse precincts from previous pans; same viewport = 3-byte EOR.
  2. v0.18.1 — adjacent-viewport halo prefetch. After a pause, fetch precincts just outside the viewport so the next short pan hits the cache.
  3. 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

Choose a tag to compare

@osamu620 osamu620 released this 21 Apr 09:01

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 by PREFETCH_MARGIN_PX on 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 via AbortController; 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

Choose a tag to compare

@osamu620 osamu620 released this 21 Apr 08:38

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 its is_last flag so a subsequent server re-send can legitimately re-fill it. Primitive used by the viewer's LRU to keep DataBinSet consistent with the cache-model advertisement.
  • CacheModel::unmark(class_id, in_class_id) — complement to mark(); 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 from ctx->set so 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.html immediately after jpip_create_context().
  • New URL param ?precinctCacheMB=N (default 64, 0 disables).
  • 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_stream unchanged — 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 smaller fetch=...ms on repeat pans; the stats bar's bytes number 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

Choose a tag to compare

@osamu620 osamu620 released this 21 Apr 06:51

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).
  • --chunked opts in to Transfer-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++ JpipClient both do).
  • --no-chunked is 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

Choose a tag to compare

@osamu620 osamu620 released this 21 Apr 06:12

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 (default 1920x1080). 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 via GL_LINEAR. Pass ?maxSize=window to disable the cap.
  • ?fit=stretch|contain (default stretch). Companion knob: stretch CSS-scales the capped render target up to fill the window (v0.17.0 behaviour); contain shows 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 is P; the server + library have emitted / accepted P since v0.16.1. HTML now matches.
  • ?fit= help row gained a line break so stretch and contain don'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> for std::memchr (libc++ pulls it in transitively).
  • MSVC's <windows.h> defines a min macro that mangles std::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

Choose a tag to compare

@osamu620 osamu620 released this 21 Apr 03:46

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: chunked by default — every JPP message emitted by the new stream_jpp_response primitive is flushed to the socket as its own HTTP chunk. §C.6.1 len= rollback still works, inside a reusable scratch buffer. --no-chunked opts out for clients that can't parse chunked.
  • build_jpp_stream preserved 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 carries MessageHeaderContext across feed() calls and buffers the tail bytes of an incomplete message until the next chunk arrives.
  • parse_jpp_stream refactored to share a try_decode_one_message helper with the streaming parser — one decode path, no drift.
  • JpipClient::fetch_streaming(..., on_progress) (new): incremental HTTP chunked state machine feeds straight into StreamingJppParser. 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_end alongside the existing jpip_add_response (preserved for one-shot callers).

Browser demos

  • jpip_demo.html (foveated) and jpip_viewer.html (gigapixel) swap fetch().arrayBuffer() for response.body.getReader() + a _jpip_feed_stream loop.
  • Auto-prefix http:// on bare host:port server 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 identical DataBinSet to the one-shot parser.
  • jpip_chunked_roundtrip — end-to-end chunked responder drives fetch_streaming with a progress callback.
  • jpip_http_check extended with chunked formatter + decode_chunked_body round-trips and malformed-input rejection.
  • Node integration (tools/jpip_wasm_feed_test.mjs) asserts WASM streaming state matches _jpip_add_response state 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.