@@ -39,6 +39,10 @@ typedef struct
3939 int transferCharacteristics ;
4040 int matrixCoefficients ;
4141
42+ // HDR CLLI box (max content/picture-average light level, in cd/m^2).
43+ int maxCLL ;
44+ int maxPALL ;
45+
4246} InputParams ;
4347
4448typedef struct
@@ -123,6 +127,8 @@ InputMessage parse_input_message(const char *line)
123127 msg .data .params .colorPrimaries = (int )json_object_get_number (params_object , "colorPrimaries" );
124128 msg .data .params .transferCharacteristics = (int )json_object_get_number (params_object , "transferCharacteristics" );
125129 msg .data .params .matrixCoefficients = (int )json_object_get_number (params_object , "matrixCoefficients" );
130+ msg .data .params .maxCLL = (int )json_object_get_number (params_object , "maxCLL" );
131+ msg .data .params .maxPALL = (int )json_object_get_number (params_object , "maxPALL" );
126132 JSON_Array * durations_array = json_object_get_array (params_object , "frameDurations" );
127133 if (durations_array != NULL )
128134 {
@@ -209,6 +215,8 @@ void write_output_message(const OutputMessage *msg)
209215 json_object_set_number (params_object , "colorPrimaries" , msg -> data .params .colorPrimaries );
210216 json_object_set_number (params_object , "transferCharacteristics" , msg -> data .params .transferCharacteristics );
211217 json_object_set_number (params_object , "matrixCoefficients" , msg -> data .params .matrixCoefficients );
218+ json_object_set_number (params_object , "maxCLL" , msg -> data .params .maxCLL );
219+ json_object_set_number (params_object , "maxPALL" , msg -> data .params .maxPALL );
212220 if (msg -> data .params .frameDurations != NULL )
213221 {
214222 JSON_Value * durations_value = json_value_init_array ();
@@ -292,6 +300,8 @@ void handle_commands(FILE *stream)
292300 decoder -> ignoreExif = AVIF_TRUE ;
293301 decoder -> ignoreXMP = AVIF_TRUE ;
294302 decoder -> maxThreads = 1 ;
303+ // Request gain map pixels when present (e.g. Lightroom HDR exports).
304+ decoder -> imageContentToDecode = AVIF_IMAGE_CONTENT_ALL ;
295305
296306 avifResult result = avifDecoderSetIOMemory (decoder , blob_data , blob_size );
297307 if (result != AVIF_RESULT_OK )
@@ -311,6 +321,84 @@ void handle_commands(FILE *stream)
311321 goto cleanup ;
312322 }
313323
324+ // If a gain map is present (e.g. Adobe-style SDR+gainmap HDR from Lightroom),
325+ // bake it into a single true-HDR image in BT.2020/PQ at 10-bit.
326+ // The downstream pipeline then sees a normal HDR AVIF; SDR clients/displays
327+ // tone-map automatically.
328+ avifBool hasGainMap = (decoder -> image -> gainMap != NULL && decoder -> image -> gainMap -> image != NULL );
329+ if (hasGainMap )
330+ {
331+ // Need a full frame to apply the gain map.
332+ result = avifDecoderNextImage (decoder );
333+ if (result != AVIF_RESULT_OK )
334+ {
335+ snprintf (output .header .err , sizeof (output .header .err ), "Failed to decode AVIF for gain map: %s" , avifResultToString (result ));
336+ avifDecoderDestroy (decoder );
337+ write_output_message (& output );
338+ goto cleanup ;
339+ }
340+
341+ avifRGBImage outRGB ;
342+ memset (& outRGB , 0 , sizeof (outRGB ));
343+ avifRGBImageSetDefaults (& outRGB , decoder -> image );
344+ outRGB .format = AVIF_RGB_FORMAT_RGBA ;
345+ outRGB .depth = 16 ;
346+ if (avifRGBImageAllocatePixels (& outRGB ) != AVIF_RESULT_OK )
347+ {
348+ snprintf (output .header .err , sizeof (output .header .err ), "Failed to allocate RGB pixels for gain map apply" );
349+ avifDecoderDestroy (decoder );
350+ write_output_message (& output );
351+ goto cleanup ;
352+ }
353+
354+ // Target the alternate (full-HDR) endpoint: alternateHdrHeadroom = log2(HDR/SDR).
355+ float hdrHeadroom = 0.0f ;
356+ const avifUnsignedFraction * h = & decoder -> image -> gainMap -> alternateHdrHeadroom ;
357+ if (h -> d != 0 )
358+ {
359+ hdrHeadroom = (float )h -> n / (float )h -> d ;
360+ }
361+
362+ avifContentLightLevelInformationBox outCLLI = {0 };
363+ avifResult applyResult = avifImageApplyGainMap (
364+ decoder -> image ,
365+ decoder -> image -> gainMap ,
366+ hdrHeadroom ,
367+ AVIF_COLOR_PRIMARIES_BT2020 ,
368+ AVIF_TRANSFER_CHARACTERISTICS_PQ ,
369+ & outRGB ,
370+ & outCLLI ,
371+ NULL );
372+ if (applyResult != AVIF_RESULT_OK )
373+ {
374+ snprintf (output .header .err , sizeof (output .header .err ), "Failed to apply gain map: %s" , avifResultToString (applyResult ));
375+ avifRGBImageFreePixels (& outRGB );
376+ avifDecoderDestroy (decoder );
377+ write_output_message (& output );
378+ goto cleanup ;
379+ }
380+
381+ output .data .params .width = outRGB .width ;
382+ output .data .params .height = outRGB .height ;
383+ // Tell the Go side we're 10-bit HDR so it wraps in NRGBA64 and re-encodes as HDR.
384+ output .data .params .depth = 10 ;
385+ output .data .params .stride = outRGB .rowBytes ;
386+ output .data .params .frameCount = 1 ;
387+ output .data .params .colorPrimaries = AVIF_COLOR_PRIMARIES_BT2020 ;
388+ output .data .params .transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_PQ ;
389+ output .data .params .matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT2020_NCL ;
390+ output .data .params .maxCLL = outCLLI .maxCLL ;
391+ output .data .params .maxPALL = outCLLI .maxPALL ;
392+
393+ size_t blob_out_size = (size_t )outRGB .rowBytes * outRGB .height ;
394+ write_output_message (& output );
395+ write_blob (output .header .id , outRGB .pixels , blob_out_size );
396+
397+ avifRGBImageFreePixels (& outRGB );
398+ avifDecoderDestroy (decoder );
399+ goto cleanup ;
400+ }
401+
314402 // YUV420 (4:2:0) and YUV422 (4:2:2) chroma subsampling cause issues in WASM.
315403 // Only YUV444 (4:4:4) is currently supported.
316404 if (decoder -> image -> yuvFormat == AVIF_PIXEL_FORMAT_YUV420 ||
@@ -511,13 +599,19 @@ void handle_commands(FILE *stream)
511599 }
512600 image -> yuvRange = AVIF_RANGE_FULL ;
513601
602+ // CLLI carries HDR peak/avg light levels from a baked gain map.
603+ if (input .data .params .maxCLL > 0 || input .data .params .maxPALL > 0 ) {
604+ image -> clli .maxCLL = (uint16_t )input .data .params .maxCLL ;
605+ image -> clli .maxPALL = (uint16_t )input .data .params .maxPALL ;
606+ }
607+
514608 avifRGBImage rgb ;
515609 avifRGBImageSetDefaults (& rgb , image );
516610 rgb .format = AVIF_RGB_FORMAT_RGBA ;
517611 rgb .depth = rgb_depth ;
518612 rgb .pixels = (uint8_t * )blob_data ;
519613 rgb .rowBytes = stride ;
520-
614+
521615 avifResult result = avifImageRGBToYUV (image , & rgb );
522616 if (result != AVIF_RESULT_OK ) {
523617 snprintf (output .header .err , sizeof (output .header .err ), "encodeNRGBA: Failed to convert to YUV: %s" , avifResultToString (result ));
@@ -625,6 +719,11 @@ void handle_commands(FILE *stream)
625719 }
626720 image -> yuvRange = AVIF_RANGE_FULL ;
627721
722+ if (input .data .params .maxCLL > 0 || input .data .params .maxPALL > 0 ) {
723+ image -> clli .maxCLL = (uint16_t )input .data .params .maxCLL ;
724+ image -> clli .maxPALL = (uint16_t )input .data .params .maxPALL ;
725+ }
726+
628727 avifResult alloc_result = avifImageAllocatePlanes (image , AVIF_PLANES_YUV );
629728 if (alloc_result != AVIF_RESULT_OK ) {
630729 snprintf (output .header .err , sizeof (output .header .err ), "encodeGray: Failed to allocate planes: %s" , avifResultToString (alloc_result ));
0 commit comments