Skip to content

AVIF export: write HDR10 content light level (MaxCLL/MaxFALL) for PQ#21357

Open
MaykThewessen wants to merge 2 commits into
darktable-org:masterfrom
MaykThewessen:upstream-avif-hdr10-clli
Open

AVIF export: write HDR10 content light level (MaxCLL/MaxFALL) for PQ#21357
MaykThewessen wants to merge 2 commits into
darktable-org:masterfrom
MaykThewessen:upstream-avif-hdr10-clli

Conversation

@MaykThewessen

Copy link
Copy Markdown

PR: AVIF export — write HDR10 content light level (MaxCLL/MaxFALL) for PQ

Branch: upstream-avif-hdr10-clli (1 commit, src/imageio/format/avif.c, +56 / -0)
Base: darktable-org/darktable:master
Refs: part of #18078


What this does

darktable already tags AVIF exports with the correct nclx color information
(colorPrimaries / transferCharacteristics / matrixCoefficients) for PQ and
HLG Rec.2020 and P3 output profiles. However, it never writes the HDR10
content light level box (clli: MaxCLL and MaxFALL). Many players, OSes and
HDR displays use those values to tone-map, and several platforms (e.g.
iOS/Instagram) expect them on HDR stills.

This PR computes and writes clli for PQ output.

How

For PQ (SMPTE ST 2084) the exported float samples are absolute-luminance
encoded, so the real content light levels can be derived from the pixels via the
PQ EOTF:

  • MaxCLL = the brightest single sample (max RGB component), in cd/m².
  • MaxFALL = the mean over all samples of that per-sample peak light level
    (the frame-average light level; for a still, the whole image is the frame).

These are written into image->clli (maxCLL / maxPALL). A debug line is
emitted under DT_DEBUG_IMAGEIO.

HLG is not handled here: it is a relative system with no fixed nit scale, so
clli is not meaningful and is left unset.

Scope / notes

  • Only active when the output is PQ and nclx is being written natively.
  • No new dependencies; the PQ EOTF is a small static helper.
  • The computation reuses the same (const float *)in, 4-channel layout the rest
    of write_image() already uses.

Testing

  • Built on macOS (Apple Silicon) against libavif.
  • Export an image through a PQ Rec.2020 output profile and inspect the file,
    e.g. avifdec --info out.avif or exiftool out.avif, and confirm the
    MaxCLL / MaxFALL values are present and sane (debug log prints the values when
    run with -d imageio).

@MaykThewessen

Copy link
Copy Markdown
Author

Tested end-to-end

Exported a test image through the PQ Rec.2020 output profile to 12-bit AVIF
(darktable-cli ... --icc-type PQ_REC2020) on macOS / Apple Silicon, and verified
the bitstream with two independent tools.

avifdec --info:

Bit Depth      : 12
Color Primaries: 9     (BT.2020)
Transfer Char. : 16    (SMPTE ST 2084 / PQ)
Matrix Coeffs. : 9     (BT.2020 non-constant luminance)
CLLI           : 8970, 4615   (MaxCLL / MaxFALL, nits)

ffprobe:

color_primaries=bt2020
color_transfer=smpte2084
color_space=bt2020nc
pix_fmt=yuv422p12le
Content light level metadata: max_content=8970, max_average=4615

With this PR the clli box is present and the values track the actual pixel
luminance via the PQ EOTF; without it the box was absent. (The high numbers here
are because the synthetic test image is fully saturated with no filmic roll-off;
a normally graded photo lands much lower.)

darktable already tags AVIF exports with the correct nclx colorPrimaries /
transferCharacteristics / matrixCoefficients for PQ/HLG Rec.2020 and P3 output
profiles, but never wrote the HDR10 content-light-level box, which players and
displays use to tone-map (and which platforms like iOS/Instagram expect).

For PQ (SMPTE ST 2084) output the float samples are absolute-luminance encoded,
so derive the values from the pixels via the PQ EOTF:
  - MaxCLL  = brightest single sample (max RGB component), in nits,
  - MaxFALL = mean per-sample peak light level, in nits.
Written into image->clli. HLG is relative, so it is skipped there.

Implements the core of feature request darktable-org#18078.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MaykThewessen MaykThewessen force-pushed the upstream-avif-hdr10-clli branch from 95f62bd to 88b3428 Compare June 21, 2026 20:30
A non-finite float sample poisoned the MaxFALL sum and made the final 16-bit
clli cast undefined. Treat non-finite per-pixel nits as 0 in the accumulation.

Co-Authored-By: Claude Opus 4.8 <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.

1 participant