Skip to content

Commit 15dba50

Browse files
MaykThewessenclaude
andcommitted
HEIF export: write HDR10 content light level (clli) for PQ
darktable's HEIF exporter already tags PQ/HLG Rec.2020 (and P3) output with the correct nclx colour information, but never wrote the HDR10 content-light-level box, which players and displays use to tone-map (and which platforms such as iOS/Instagram expect on HDR stills). 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 via heif_image_set_content_light_level(). HLG is relative, so skipped. This mirrors the equivalent AVIF change and rounds out HDR10 HEIF output. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b3c41a1 commit 15dba50

1 file changed

Lines changed: 58 additions & 0 deletions

File tree

src/imageio/format/heif.c

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
#include <glib/gstdio.h>
2828
#include <inttypes.h>
29+
#include <math.h>
2930
#include <stdio.h>
3031
#include <stdlib.h>
3132

@@ -117,6 +118,25 @@ void cleanup(dt_imageio_module_format_t *self)
117118
{
118119
}
119120

121+
/*
122+
* SMPTE ST 2084 (PQ) EOTF: map a normalized PQ-encoded value in [0, 1] to
123+
* absolute luminance in cd/m^2 (nits), peak white = 10000 nits.
124+
*/
125+
static float _pq_to_nits(const float e)
126+
{
127+
const float m1 = 0.1593017578125f; /* 2610 / 16384 */
128+
const float m2 = 78.84375f; /* 2523 / 4096 * 128 */
129+
const float c1 = 0.8359375f; /* 3424 / 4096 */
130+
const float c2 = 18.8515625f; /* 2413 / 4096 * 32 */
131+
const float c3 = 18.6875f; /* 2392 / 4096 * 32 */
132+
133+
const float ep = powf(CLAMP(e, 0.0f, 1.0f), 1.0f / m2);
134+
const float num = fmaxf(ep - c1, 0.0f);
135+
const float den = c2 - c3 * ep;
136+
if(den <= 0.0f) return 10000.0f;
137+
return 10000.0f * powf(num / den, 1.0f / m1);
138+
}
139+
120140
int write_image(dt_imageio_module_data_t *data,
121141
const char *filename,
122142
const void *in_tmp, // ptr to input image buf
@@ -324,6 +344,44 @@ int write_image(dt_imageio_module_data_t *data,
324344
}
325345
}
326346

347+
/*
348+
* HDR10 content light level (clli) metadata.
349+
*
350+
* For PQ (SMPTE ST 2084) output the float samples are absolute-luminance
351+
* encoded, so we can derive the real content light levels from the pixels:
352+
* - MaxCLL = the brightest single sample (max RGB component) in nits,
353+
* - MaxFALL = the mean over all samples of that per-sample peak.
354+
* Players and HDR displays use these to tone-map appropriately. We only write
355+
* them for PQ; HLG is relative and has no fixed nit scale.
356+
*/
357+
if(!need_to_embed_icc
358+
&& nclx_profile->transfer_characteristics == heif_transfer_characteristic_ITU_R_BT_2100_0_PQ)
359+
{
360+
const size_t npixels = width * height;
361+
float max_cll = 0.0f;
362+
double sum_fall = 0.0;
363+
364+
DT_OMP_FOR_SIMD(reduction(max : max_cll) reduction(+ : sum_fall))
365+
for(size_t k = 0; k < npixels; k++)
366+
{
367+
const float *const px = &in_data[4 * k];
368+
const float e_max = fmaxf(fmaxf(px[0], px[1]), px[2]);
369+
const float nits = _pq_to_nits(e_max);
370+
max_cll = fmaxf(max_cll, nits);
371+
sum_fall += nits;
372+
}
373+
const float max_fall = npixels ? (float)(sum_fall / (double)npixels) : 0.0f;
374+
375+
const struct heif_content_light_level cll = {
376+
.max_content_light_level = (uint16_t)CLAMP(roundf(max_cll), 0.0f, 65535.0f),
377+
.max_pic_average_light_level = (uint16_t)CLAMP(roundf(max_fall), 0.0f, 65535.0f),
378+
};
379+
heif_image_set_content_light_level(image, &cll);
380+
381+
dt_print(DT_DEBUG_IMAGEIO, "[heif HDR10 clli: MaxCLL=%u nits, MaxFALL=%u nits]",
382+
cll.max_content_light_level, cll.max_pic_average_light_level);
383+
}
384+
327385
struct heif_context* context = heif_context_alloc();
328386

329387
struct heif_encoder* encoder;

0 commit comments

Comments
 (0)