Skip to content

UltraHDR (gain-map JPEG) HDR export via libultrahdr#21458

Draft
MaykThewessen wants to merge 1 commit into
darktable-org:masterfrom
MaykThewessen:upstream-ultrahdr
Draft

UltraHDR (gain-map JPEG) HDR export via libultrahdr#21458
MaykThewessen wants to merge 1 commit into
darktable-org:masterfrom
MaykThewessen:upstream-ultrahdr

Conversation

@MaykThewessen

@MaykThewessen MaykThewessen commented Jun 30, 2026

Copy link
Copy Markdown

What

Adds an optional imageio format module that exports UltraHDR / ISO 21496-1 gain-map JPEGs (Google's UltraHDR) through Google's libultrahdr. This is the one common HDR still-image format darktable cannot yet write; gain-map JPEGs render as HDR in Chrome and on Android, and (see the macOS note below) on Apple platforms too when libultrahdr is built to dual-encode, while degrading gracefully to the SDR base on viewers that ignore the gain map.

How

  • New file src/imageio/format/ultrahdr.c (modeled on avif.c), using libultrahdr's API-0 single-input encode path: only the HDR rendition is fed, and the library tone-maps the SDR base, computes the gain map, and muxes the base JPEG + gain map + MPF container itself.
  • The module receives darktable's interleaved float32 RGBA buffer (bpp 32). It requires a PQ output color profile (PQ Rec2020 or PQ P3): colorout has then already baked the SMPTE ST 2084 curve into the samples, so the floats are PQ-encoded and normalized to [0, 1]. They are packed to UHDR_IMG_FMT_32bppRGBA1010102 and tagged UHDR_CT_PQ with the matching gamut (UHDR_CG_BT_2100 for Rec2020, UHDR_CG_DISPLAY_P3 for P3). Any non-PQ profile logs a clear error and fails the export.
  • GUI exposes base quality, gain-map quality, and gain-map resolution (full/half/quarter); conf keys declared in darktableconfig.xml.in (defaults 95/95/full).

Build / optionality

  • Gated on libuhdr via pkg-config (USE_ULTRAHDR, default ON). NOTE: the pkg-config and link name is libuhdr, not libultrahdr.
  • When libuhdr is absent the module is simply not built, so the rest of darktable is unaffected. The cache flag is reset on every configure so toggling USE_ULTRAHDR off (or libuhdr disappearing) cannot leave a stale module target.

Status

Draft. Compiles and links libuhdr (1.4.0). Output is standards-conformant (verified with ultrahdr_app -m 1 and ImageMagick: a 2-image MPF container with an SDR base + ISO 21496-1 gain map) and renders as HDR in Chrome. Open items:

  • macOS rendering depends on how libultrahdr was built (root-caused and verified). With the common default build (UHDR_WRITE_XMP=OFF, ISO-21496-1 only, which is what current Homebrew ships), Apple's ImageIO cannot decode the file at all: CGImageSourceCreateImageAtIndex returns nil and Preview/Photos/Quick Look/Safari fail to open it. Root cause: in that mode libultrahdr embeds a ~30 KB Lab/CICP ICC profile in the gain-map sub-image that Apple's MPF parser rejects (it aborts the whole file rather than falling back to the primary). Building libultrahdr with UHDR_WRITE_XMP=ON (dual hdrgm-XMP + ISO, the same combination Android 15 and Apple's own camera write) fixes it: the identical darktable export then both decodes and renders as true HDR on macOS (independently verified at ~5.6 stops of headroom for a 0-4000 nit test pattern). The XMP flag both drops the offending gain-map ICC and adds the gain-map XMP Apple keys on (see libultrahdr lib/src/jpegr.cpp compressGainMap). No darktable code change is needed, the PQ RGBA1010102 packing is already correct; this is a libultrahdr packaging matter. When only the default ISO-only libultrahdr is available, darktable's AVIF/HEIF PQ exports remain the Apple-native HDR path.
  • The module uses the jpg extension, so darktable-cli selection by extension picks the core JPEG format; the GUI export dropdown selects "UltraHDR JPEG" explicitly. Happy to take guidance on the preferred disambiguation (a distinct extension, or a format selector).
  • A linear-f16 input path (UHDR_CT_LINEAR from a Linear Rec2020 export) could be offered as a higher-fidelity alternative to the 10-bit PQ pack; deferred for now.

Related HDR work

Part of a small HDR series: #21357 (AVIF HDR10 CLLI), #21365 (HEIF HDR10 CLLI), #21358 (optional external EDR darkroom viewer).

🤖 Generated with Claude Code

New optional imageio format module exporting ISO 21496-1 gain-map JPEGs
(Google UltraHDR) through libultrahdr's API-0 single-input encode path.

The module receives darktable's interleaved float32 RGBA buffer (bpp 32).
It requires a PQ output color profile (PQ Rec2020 or PQ P3): colorout has
then already baked the SMPTE ST 2084 curve into the samples, so the floats
are PQ-encoded and normalized to [0, 1]. They are packed to RGBA1010102 and
handed to libultrahdr as UHDR_CT_PQ with the matching color gamut
(UHDR_CG_BT_2100 for Rec2020, UHDR_CG_DISPLAY_P3 for P3). Any non-PQ profile
logs a clear error and fails the export.

With only the HDR rendition fed (API-0), libultrahdr internally tone-maps to
an SDR base, computes the gain map, and muxes the base JPEG, the ISO 21496-1
gain map, and the MPF container. The GUI exposes base quality, gain map
quality, and gain map resolution (full/half/quarter) sliders, round-tripped
through plugins/imageio/format/ultrahdr/* conf keys (declared in
darktableconfig.xml.in with defaults 95/95/full).

Build support is optional and gated on libuhdr being present via pkg-config
(USE_ULTRAHDR, default ON): when libuhdr is absent the module is simply not
built, so the rest of darktable is unaffected. The cache flag is reset on
every configure so toggling USE_ULTRAHDR off (or libuhdr disappearing)
cannot leave a stale module target.
@wpferguson

Copy link
Copy Markdown
Member

darktable can write ultrahdr images using the lua-script ultrahdr. I have no idea how much use the script gets, so I don't know how much interest there is.

@jenshannoschwalm

Copy link
Copy Markdown
Collaborator

darktable can write ultrahdr images using the lua-script ultrahdr.

Good. So let's stay with the lua addition i would think

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.

3 participants