Add clli metadata read support (#1194)

* Add clli metadata read support

* Wrap lines to 100 chars and explain default value of clli box

* Formatting cleanup + don't print clli info if (0, 0)

* clli updates per Joe Drago for documentation and writing

Co-authored-by: Eric Chan <erichan@adobe.com>
diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c
index ebfca57..8c77d1c 100644
--- a/apps/shared/avifutil.c
+++ b/apps/shared/avifutil.c
@@ -121,6 +121,9 @@
         }
     }
     printf(" * Progressive    : %s\n", avifProgressiveStateToString(progressiveState));
+    if (avif->clli.maxCLL > 0 || avif->clli.maxPALL > 0) {
+        printf(" * CLLI           : %u, %u\n", (uint32_t)avif->clli.maxCLL, (uint32_t)avif->clli.maxPALL);
+    }
 }
 
 void avifImageDump(const avifImage * avif, uint32_t gridCols, uint32_t gridRows, avifProgressiveState progressiveState)
diff --git a/include/avif/avif.h b/include/avif/avif.h
index 3ff06ad..dd7dc31 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -425,6 +425,29 @@
                                                       avifDiagnostics * diag);
 
 // ---------------------------------------------------------------------------
+// avifContentLightLevelInformationBox
+
+typedef struct avifContentLightLevelInformationBox
+{
+    // 'clli' from ISO/IEC 23000-22:2019 (MIAF) 7.4.4.2.2. The SEI message semantics written above
+    //  each entry were originally described in ISO/IEC 23008-2.
+
+    // max_content_light_level, when not equal to 0, indicates an upper bound on the maximum light
+    // level among all individual samples in a 4:4:4 representation of red, green, and blue colour
+    // primary intensities (in the linear light domain) for the pictures of the CLVS, in units of
+    // candelas per square metre. When equal to 0, no such upper bound is indicated by
+    // max_content_light_level.
+    uint16_t maxCLL;
+
+    // max_pic_average_light_level, when not equal to 0, indicates an upper bound on the maximum
+    // average light level among the samples in a 4:4:4 representation of red, green, and blue
+    // colour primary intensities (in the linear light domain) for any individual picture of the
+    // CLVS, in units of candelas per square metre. When equal to 0, no such upper bound is
+    // indicated by max_pic_average_light_level.
+    uint16_t maxPALL;
+} avifContentLightLevelInformationBox;
+
+// ---------------------------------------------------------------------------
 // avifImage
 
 typedef struct avifImage
@@ -472,6 +495,13 @@
     avifImageRotation irot;
     avifImageMirror imir;
 
+    // CLLI information:
+    // Content Light Level Information. Used to represent maximum and average light level of an
+    // image. Useful for tone mapping HDR images, especially when using transfer characteristics
+    // SMPTE2084 (PQ). The default value of (0, 0) means the content light level information is
+    // unknown or unavailable, and will cause libavif to avoid writing a clli box for it.
+    avifContentLightLevelInformationBox clli;
+
     // Metadata - set with avifImageSetMetadata*() before write, check .size>0 for existence after read
     avifRWData exif;
     avifRWData xmp;
diff --git a/src/read.c b/src/read.c
index e45db4e..f5f303b 100644
--- a/src/read.c
+++ b/src/read.c
@@ -130,6 +130,7 @@
         avifOperatingPointSelectorProperty a1op;
         avifLayerSelectorProperty lsel;
         avifAV1LayeredImageIndexingProperty a1lx;
+        avifContentLightLevelInformationBox clli;
     } u;
 } avifProperty;
 AVIF_ARRAY_DECLARE(avifPropertyArray, avifProperty, prop);
@@ -1759,6 +1760,17 @@
     return AVIF_TRUE;
 }
 
+static avifBool avifParseContentLightLevelInformationBox(avifProperty * prop, const uint8_t * raw, size_t rawLen, avifDiagnostics * diag)
+{
+    BEGIN_STREAM(s, raw, rawLen, diag, "Box[clli]");
+
+    avifContentLightLevelInformationBox * clli = &prop->u.clli;
+
+    AVIF_CHECK(avifROStreamReadU16(&s, &clli->maxCLL));  // unsigned int(16) max_content_light_level
+    AVIF_CHECK(avifROStreamReadU16(&s, &clli->maxPALL)); // unsigned int(16) max_pic_average_light_level
+    return AVIF_TRUE;
+}
+
 static avifBool avifParseAV1CodecConfigurationBox(const uint8_t * raw, size_t rawLen, avifCodecConfigurationBox * av1C, avifDiagnostics * diag)
 {
     BEGIN_STREAM(s, raw, rawLen, diag, "Box[av1C]");
@@ -1956,6 +1968,8 @@
             AVIF_CHECK(avifParseLayerSelectorProperty(prop, avifROStreamCurrent(&s), header.size, diag));
         } else if (!memcmp(header.type, "a1lx", 4)) {
             AVIF_CHECK(avifParseAV1LayeredImageIndexingProperty(prop, avifROStreamCurrent(&s), header.size, diag));
+        } else if (!memcmp(header.type, "clli", 4)) {
+            AVIF_CHECK(avifParseContentLightLevelInformationBox(prop, avifROStreamCurrent(&s), header.size, diag));
         }
 
         AVIF_CHECK(avifROStreamSkip(&s, header.size));
@@ -2042,8 +2056,8 @@
             // Copy property to item
             const avifProperty * srcProp = &meta->properties.prop[propertyIndex];
 
-            static const char * supportedTypes[] = { "ispe", "auxC", "colr", "av1C", "pasp", "clap",
-                                                     "irot", "imir", "pixi", "a1op", "lsel", "a1lx" };
+            static const char * supportedTypes[] = { "ispe", "auxC", "colr", "av1C", "pasp", "clap", "irot",
+                                                     "imir", "pixi", "a1op", "lsel", "a1lx", "clli" };
             size_t supportedTypesCount = sizeof(supportedTypes) / sizeof(supportedTypes[0]);
             avifBool supportedType = AVIF_FALSE;
             for (size_t i = 0; i < supportedTypesCount; ++i) {
@@ -3760,6 +3774,11 @@
         return AVIF_RESULT_BMFF_PARSE_FAILED;
     }
 
+    const avifProperty * clliProp = avifPropertyArrayFind(colorProperties, "clli");
+    if (clliProp) {
+        decoder->image->clli = clliProp->u.clli;
+    }
+
     return avifDecoderFlush(decoder);
 }
 
diff --git a/src/write.c b/src/write.c
index 9576b77..bead4cc 100644
--- a/src/write.c
+++ b/src/write.c
@@ -527,6 +527,20 @@
         ipmaPush(ipma, avifItemPropertyDedupFinish(dedup, outputStream), AVIF_FALSE);
     }
 
+    // Write Content Light Level Information, if present
+    if (imageMetadata->clli.maxCLL || imageMetadata->clli.maxPALL) {
+        if (dedup) {
+            avifItemPropertyDedupStart(dedup);
+        }
+        avifBoxMarker clli = avifRWStreamWriteBox(s, "clli", AVIF_BOX_SIZE_TBD);
+        avifRWStreamWriteU16(s, imageMetadata->clli.maxCLL);  // unsigned int(16) max_content_light_level;
+        avifRWStreamWriteU16(s, imageMetadata->clli.maxPALL); // unsigned int(16) max_pic_average_light_level;
+        avifRWStreamFinishBox(s, clli);
+        if (dedup) {
+            ipmaPush(ipma, avifItemPropertyDedupFinish(dedup, outputStream), AVIF_FALSE);
+        }
+    }
+
     // Write (Optional) Transformations
     if (imageMetadata->transformFlags & AVIF_TRANSFORM_PASP) {
         if (dedup) {