imazen / imazen/zenpixels

Epic: M×N HDR pipeline — gain-map/SDR → true HDR (JXL/AVIF/PNG) via an absolute-luminance hub

Open
#45 4 comments 0 reactions 1 assignee View on GitHub

@lilith is already working on this.

Since Jun 14, 2026.

Dominant language
Rust
Stars
1
Forks
0
PR merge metrics
No merged PRs in 30d

Description

Design of record for converting gain-map / SDR sources into true HDR (JXL / AVIF / PNG-3) via an absolute-luminance hub. Follows the prior-art survey (docs/hdr-design-survey-2026-06-13.md) and a verified 4-leg audit of the current matrix.

Headline: the HDR science is done — the gaps are four wiring points

A read across the stack (file:line below) found the hard parts already built and mutually consistent:

  • Luminance anchor is universal: 1.0 = 203 nits (BT.2408 diffuse white), SDR primaries — identical across ultrahdr-core (apply.rs:124, types.rs:93), heic, zenavif, and zenjxl's reconstruct path. peak = 203 × capacity_max.
  • Transfer ops: all present, SIMD — linear↔{sRGB,709,PQ,HLG,γ2.2}, u8/u16/f32 (zenpixels-convert/src/convert.rs:90-142).
  • Gamut ops: present and general — BT.709/P3/BT.2020/AdobeRGB, Bradford-adapted, chromaticity-derived 3×3 (registry.rs:203-226). SDR→BT.2020 works today.
  • Tone-mapping: exists in zentone (BT.2390 EETF, BT.2446 A/B/C, BT.2408 Hermite, ACES/Hable/AgX, HLG-OOTF, gamut soft-clip, gain-map split — all SIMD). zenpixels-convert does not depend on it yet.
  • JXL HDR engine (jxl-encoder): fully capable — float PQ/HLG input (api.rs:425-458), intensity_target genuinely plumbed PQ→10000/HLG→1000 → metadata → request (api.rs:9918-9984), native jhgm gain-map writer (hdr/from_sdr.rs).
  • M×N orchestrator (zenpipe/zencodecs): transcode() already does decode→adapt→re-encode with Metadata roundtrip (transcode.rs:197).
  • Carrier types: no duplication — zencodec re-exports zenpixels' Cicp/ContentLightLevel/MasteringDisplay. Metadata is #[non_exhaustive] (metadata.rs:46).
The four gaps (all plumbing, not new math)
  1. zencodecs::transcode does pixel-format adaptation only — no transfer/primaries/tonemap, and it drops the gain map (standing TODO transcode.rs:168).
  2. zenpixels-convert has no anchored tonemap step; PQ domain is hard-pinned to 1.0=10000 (every caller hand-prescales, e.g. quantize_to).
  3. The anchor isn't in any carrier — neither zencodec::Metadata nor ColorContext has a DiffuseWhite field.
  4. zenjxl's encode wrapper exposes only sRGB/linear descriptors and never forwards intensity_target — so via the trait you can decode HDR but not encode true-HDR JXL, though the engine can.

Confirmed architecture decisions

  • Anchor home: ColorContext + zencodec::Metadata (both, additive). Add diffuse_white: Option<DiffuseWhite> to the existing PixelBuffer.color: Arc<ColorContext> sidecar (propagates through every clone already) AND to Metadata/SourceColor (survives the codec boundary, feeds intensity_target). No new PixelDescriptor field (it's Copy/≤8B/frozen presets), no wrapper type. Caveat: ColorContext: Eq and f32 isn't Eq — bit-compare newtype or drop the derive (tolerated 0.2.x).
  • Gain-map → JXL: both paths, caller-selected, default reconstruct. Default = apply gain map → single linear HDR → native float/PQ JXL with intensity_target. Knob = preserve as native jhgm (SDR base + gain map) for backward-compat.

Slices (each ends in a demoable artifact)

  • S1 — Carrier: diffuse_white onto ColorContext (zenpixels) + Metadata/SourceColor (zencodec); populate it in the gain-map decode adapters (they already compute 203 × capacity_max). Demo: decode an UltraHDR JPEG, assert buffer + metadata carry the anchor.
  • S2 — Anchored hub ops: make PQ/HLG ConvertSteps read the anchor from ColorContext (scale by anchor/10000 internally, so convert_buffer needs no manual pre-scale); depend on zentone (verify no dep cycle) for EETF/BT.2446/HLG-OOTF/soft-clip; add a peak-adaptation step taking (content_nits, display_nits). quantize_to dissolves into convert_buffer; migrate ~/work/hdr-corpus-convert to it. Demo: convert_buffer linear@203→PQ16 matches the old encode_pq16 ±1; PQ→HLG peak-adapt round-trips.
  • S3 — JXL HDR encode wrapper: map PQ/HLG/float descriptors in zenjxl's encode wrapper; forward intensity_target from the anchor/peak; expose the jhgm path behind the knob. Demo: UltraHDR JPEG → true-HDR JXL; decode back, assert intensity_target + CICP.
  • S4 — M×N orchestration: insert the HDR hub step in zencodecs::transcode (reconstruct-or-carry gain maps per the knob, gamut + tonemap to the target's primaries/peak, set the anchor); wire the gain-map re-embed (the TODO). Demo: zcimg converts gain-map JPEG → JXL HDR and → AVIF HDR with correct signaling.

Relationship to PR #41

#41 lands the DiffuseWhite type + ContentLightLevel::measure + the HdrMetadata deprecation — the foundation S1 builds on. quantize_to stays the pub(crate) tested oracle until S2's anchored convert_buffer supersedes it (then deprecate + migrate hdr-corpus-convert). S1 depends on #41 landing first.

Cross-repo: zenpixels, zencodec, zenpixels-convert, zentone, zenjxl, zenpipe/zencodecs.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.