Implement the HDR part of MinimizedImageBox (#2440)

Write and parse tone mapping and gain map elements in SlimHEIF.
diff --git a/src/read.c b/src/read.c
index d711cb9..5c8b68a 100644
--- a/src/read.c
+++ b/src/read.c
@@ -2252,6 +2252,109 @@
     return AVIF_RESULT_OK;
 }
 
+#if defined(AVIF_ENABLE_EXPERIMENTAL_MINI) && defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+static avifResult avifSkipMasteringDisplayColourVolume(avifROStream * s)
+{
+    for (int c = 0; c < 3; c++) {
+        AVIF_CHECKERR(avifROStreamSkipBits(s, 16), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(16) display_primaries_x;
+        AVIF_CHECKERR(avifROStreamSkipBits(s, 16), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(16) display_primaries_y;
+    }
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 16), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(16) white_point_x;
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 16), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(16) white_point_y;
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 32), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(32) max_display_mastering_luminance;
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 32), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(32) min_display_mastering_luminance;
+    return AVIF_RESULT_OK;
+}
+
+static avifResult avifSkipContentColourVolume(avifROStream * s)
+{
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) reserved = 0; // ccv_cancel_flag
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) reserved = 0; // ccv_persistence_flag
+    uint8_t ccvPrimariesPresent;
+    AVIF_CHECKERR(avifROStreamReadBitsU8(s, &ccvPrimariesPresent, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) ccv_primaries_present_flag;
+    uint8_t ccvMinLuminanceValuePresent, ccvMaxLuminanceValuePresent, ccvAvgLuminanceValuePresent;
+    AVIF_CHECKERR(avifROStreamReadBitsU8(s, &ccvMinLuminanceValuePresent, 1),
+                  AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) ccv_min_luminance_value_present_flag;
+    AVIF_CHECKERR(avifROStreamReadBitsU8(s, &ccvMaxLuminanceValuePresent, 1),
+                  AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) ccv_max_luminance_value_present_flag;
+    AVIF_CHECKERR(avifROStreamReadBitsU8(s, &ccvAvgLuminanceValuePresent, 1),
+                  AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) ccv_avg_luminance_value_present_flag;
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 2), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(2) reserved = 0;
+
+    if (ccvPrimariesPresent) {
+        for (int c = 0; c < 3; c++) {
+            AVIF_CHECKERR(avifROStreamSkipBits(s, 32), AVIF_RESULT_BMFF_PARSE_FAILED); // signed int(32) ccv_primaries_x[[c]];
+            AVIF_CHECKERR(avifROStreamSkipBits(s, 32), AVIF_RESULT_BMFF_PARSE_FAILED); // signed int(32) ccv_primaries_y[[c]];
+        }
+    }
+    if (ccvMinLuminanceValuePresent) {
+        AVIF_CHECKERR(avifROStreamSkipBits(s, 32), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(32) ccv_min_luminance_value;
+    }
+    if (ccvMaxLuminanceValuePresent) {
+        AVIF_CHECKERR(avifROStreamSkipBits(s, 32), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(32) ccv_max_luminance_value;
+    }
+    if (ccvAvgLuminanceValuePresent) {
+        AVIF_CHECKERR(avifROStreamSkipBits(s, 32), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(32) ccv_avg_luminance_value;
+    }
+    return AVIF_RESULT_OK;
+}
+
+static avifResult avifSkipAmbientViewingEnvironment(avifROStream * s)
+{
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 32), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(32) ambient_illuminance;
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 16), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(16) ambient_light_x;
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 16), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(16) ambient_light_y;
+    return AVIF_RESULT_OK;
+}
+
+static avifResult avifSkipReferenceViewingEnvironment(avifROStream * s)
+{
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 32), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(32) surround_luminance;
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 16), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(16) surround_light_x;
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 16), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(16) surround_light_y;
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 32), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(32) periphery_luminance;
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 16), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(16) periphery_light_x;
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 16), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(16) periphery_light_y;
+    return AVIF_RESULT_OK;
+}
+
+static avifResult avifSkipNominalDiffuseWhite(avifROStream * s)
+{
+    AVIF_CHECKERR(avifROStreamSkipBits(s, 32), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(32) diffuse_white_luminance;
+    return AVIF_RESULT_OK;
+}
+
+static avifResult avifParseMiniHDRProperties(avifROStream * s, uint32_t * hasClli, avifContentLightLevelInformationBox * clli)
+{
+    AVIF_CHECKERR(avifROStreamReadBitsU32(s, hasClli, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(1) clli_flag;
+    uint32_t hasMdcv, hasCclv, hasAmve, hasReve, hasNdwt;
+    AVIF_CHECKERR(avifROStreamReadBitsU32(s, &hasMdcv, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(1) mdcv_flag;
+    AVIF_CHECKERR(avifROStreamReadBitsU32(s, &hasCclv, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(1) cclv_flag;
+    AVIF_CHECKERR(avifROStreamReadBitsU32(s, &hasAmve, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(1) amve_flag;
+    AVIF_CHECKERR(avifROStreamReadBitsU32(s, &hasReve, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(1) reve_flag;
+    AVIF_CHECKERR(avifROStreamReadBitsU32(s, &hasNdwt, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(1) ndwt_flag;
+    if (*hasClli) {
+        AVIF_CHECKRES(avifParseContentLightLevelInformation(s, clli)); // ContentLightLevel clli;
+    }
+    if (hasMdcv) {
+        AVIF_CHECKRES(avifSkipMasteringDisplayColourVolume(s)); // MasteringDisplayColourVolume mdcv;
+    }
+    if (hasCclv) {
+        AVIF_CHECKRES(avifSkipContentColourVolume(s)); // ContentColourVolume cclv;
+    }
+    if (hasAmve) {
+        AVIF_CHECKRES(avifSkipAmbientViewingEnvironment(s)); // AmbientViewingEnvironment amve;
+    }
+    if (hasReve) {
+        AVIF_CHECKRES(avifSkipReferenceViewingEnvironment(s)); // ReferenceViewingEnvironment reve;
+    }
+    if (hasNdwt) {
+        AVIF_CHECKRES(avifSkipNominalDiffuseWhite(s)); // NominalDiffuseWhite ndwt;
+    }
+    return AVIF_RESULT_OK;
+}
+#endif // AVIF_ENABLE_EXPERIMENTAL_MINI && AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP
+
 // Implementation of section 2.3.3 of AV1 Codec ISO Media File Format Binding specification v1.2.0.
 // See https://aomediacodec.github.io/av1-isobmff/v1.2.0.html#av1codecconfigurationbox-syntax.
 static avifBool avifParseCodecConfiguration(avifROStream * s, avifCodecConfigurationBox * config, const char * configPropName, avifDiagnostics * diag)
@@ -3570,13 +3673,14 @@
     return itemProperty;
 }
 
-static avifResult avifParseMinimizedImageBox(avifMeta * meta,
+static avifResult avifParseMinimizedImageBox(avifDecoderData * data,
                                              uint64_t rawOffset,
                                              const uint8_t * raw,
                                              size_t rawLen,
                                              avifBool isAvifAccordingToMinorVersion,
                                              avifDiagnostics * diag)
 {
+    avifMeta * meta = data->meta;
     BEGIN_STREAM(s, raw, rawLen, diag, "Box[mini]");
 
     meta->fromMiniBox = AVIF_TRUE;
@@ -3688,80 +3792,89 @@
 
     // High Dynamic Range properties
     uint32_t hasGainmap = AVIF_FALSE;
+    uint32_t tmapHasIcc = AVIF_FALSE;
+    uint32_t gainmapWidth = 0, gainmapHeight = 0;
+    uint8_t gainmapMatrixCoefficients = 0;
+    uint32_t gainmapFullRange = 0;
+    uint32_t gainmapChromaSubsampling = 0;
+    uint32_t gainmapBitDepth = 0;
+    uint32_t tmapHasExplicitCicp = AVIF_FALSE;
+    uint8_t tmapColorPrimaries = AVIF_COLOR_PRIMARIES_UNKNOWN;
+    uint8_t tmapTransferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNKNOWN;
+    uint8_t tmapMatrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY;
+    uint32_t tmapFullRange = AVIF_FALSE;
+    uint32_t hasClli = AVIF_FALSE, tmapHasClli = AVIF_FALSE;
+    avifContentLightLevelInformationBox clli = {}, tmapClli = {};
     if (hasHdr) {
-        // bit(1) gainmap_flag;
-        // if (gainmap_flag) {
-        //     unsigned int(small_dimensions_flag ? 7 : 15) gainmap_width_minus1;
-        //     unsigned int(small_dimensions_flag ? 7 : 15) gainmap_height_minus1;
-        //     bit(8) gainmap_matrix_coefficients;
-        //     bit(1) gainmap_full_range_flag;
-        //     bit(2) gainmap_chroma_subsampling;
-        //     if (gainmap_chroma_subsampling == 1 || gainmap_chroma_subsampling == 2)
-        //         bit(1) gainmap_chroma_is_horizontally_centered;
-        //     if (gainmap_chroma_subsampling == 1)
-        //         bit(1) gainmap_chroma_is_vertically_centered;
-        //     bit(1) gainmap_float_flag;
-        //     if (gainmap_float_flag)
-        //         bit(2) gainmap_bit_depth_log2_minus4;
-        //     else {
-        //         bit(1) gainmap_high_bit_depth_flag;
-        //         if (gainmap_high_bit_depth_flag)
-        //             bit(3) gainmap_bit_depth_minus9;
-        //     }
-        //     bit(1) tmap_icc_flag;
-        //     bit(1) tmap_explicit_cicp_flag;
-        //     if (tmap_explicit_cicp_flag) {
-        //         bit(8) tmap_colour_primaries;
-        //         bit(8) tmap_transfer_characteristics;
-        //         bit(8) tmap_matrix_coefficients;
-        //         bit(1) tmap_full_range_flag;
-        //     }
-        //     else {
-        //         tmap_colour_primaries = 1;
-        //         tmap_transfer_characteristics = 13;
-        //         tmap_matrix_coefficients = 6;
-        //         tmap_full_range_flag = 1;
-        //     }
-        // }
-        // bit(1) clli_flag;
-        // bit(1) mdcv_flag;
-        // bit(1) cclv_flag;
-        // bit(1) amve_flag;
-        // bit(1) reve_flag;
-        // bit(1) ndwt_flag;
-        // if (clli_flag)
-        //     ContentLightLevel clli;
-        // if (mdcv_flag)
-        //     MasteringDisplayColourVolume mdcv;
-        // if (cclv_flag)
-        //     ContentColourVolume cclv;
-        // if (amve_flag)
-        //     AmbientViewingEnvironment amve;
-        // if (reve_flag)
-        //     ReferenceViewingEnvironment reve;
-        // if (ndwt_flag)
-        //     NominalDiffuseWhite ndwt;
-        // if (gainmap_flag) {
-        //     bit(1) tmap_clli_flag;
-        //     bit(1) tmap_mdcv_flag;
-        //     bit(1) tmap_cclv_flag;
-        //     bit(1) tmap_amve_flag;
-        //     bit(1) tmap_reve_flag;
-        //     bit(1) tmap_ndwt_flag;
-        //     if (tmap_clli_flag)
-        //         ContentLightLevel tmap_clli;
-        //     if (tmap_mdcv_flag)
-        //         MasteringDisplayColourVolume tmap_mdcv;
-        //     if (tmap_cclv_flag)
-        //         ContentColourVolume tmap_cclv;
-        //     if (tmap_amve_flag)
-        //         AmbientViewingEnvironment tmap_amve;
-        //     if (tmap_reve_flag)
-        //         ReferenceViewingEnvironment tmap_reve;
-        //     if (tmap_ndwt_flag)
-        //         NominalDiffuseWhite tmap_ndwt;
-        // }
+#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+        AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &hasGainmap, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(1) gainmap_flag;
+        if (hasGainmap) {
+            // avifDecoderReset() requires the 'tmap' brand to be registered for the tone mapping derived image item to be parsed.
+            if (data->compatibleBrands.capacity == 0) {
+                AVIF_CHECKERR(avifArrayCreate(&data->compatibleBrands, sizeof(avifBrand), 1), AVIF_RESULT_OUT_OF_MEMORY);
+            }
+            avifBrand * brand = avifArrayPush(&data->compatibleBrands);
+            AVIF_CHECKERR(brand != NULL, AVIF_RESULT_OUT_OF_MEMORY);
+            memcpy(brand, "tmap", sizeof(avifBrand));
+
+            AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &gainmapWidth, smallDimensionsFlag ? 7 : 15),
+                          AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(small_dimensions_flag ? 7 : 15) gainmap_width_minus1;
+            ++gainmapWidth;
+            AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &gainmapHeight, smallDimensionsFlag ? 7 : 15),
+                          AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(small_dimensions_flag ? 7 : 15) gainmap_height_minus1;
+            ++gainmapHeight;
+            AVIF_CHECKERR(avifROStreamReadBitsU8(&s, &gainmapMatrixCoefficients, 8), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(8) gainmap_matrix_coefficients;
+            AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &gainmapFullRange, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(1) gainmap_full_range_flag;
+
+            AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &gainmapChromaSubsampling, 2), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(2) gainmap_chroma_subsampling;
+            uint32_t gainmapChromaIsHorizontallyCentered = 0, gainmapChromaIsVerticallyCentered = 0;
+            if (gainmapChromaSubsampling == 1 || gainmapChromaSubsampling == 2) {
+                AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &gainmapChromaIsHorizontallyCentered, 1),
+                              AVIF_RESULT_BMFF_PARSE_FAILED); // bit(1) gainmap_chroma_is_horizontally_centered;
+            }
+            if (gainmapChromaSubsampling == 1) {
+                AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &gainmapChromaIsVerticallyCentered, 1),
+                              AVIF_RESULT_BMFF_PARSE_FAILED); // bit(1) gainmap_chroma_is_vertically_centered;
+            }
+
+            uint32_t gainmapFloatFlag;
+            AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &gainmapFloatFlag, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(1) gainmap_float_flag;
+            if (gainmapFloatFlag) {
+                // bit(2) gainmap_bit_depth_log2_minus4;
+                return AVIF_RESULT_BMFF_PARSE_FAILED; // Either invalid AVIF or unsupported non-AVIF.
+            } else {
+                uint32_t gainmapHighBitDepthFlag;
+                AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &gainmapHighBitDepthFlag, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(1) gainmap_high_bit_depth_flag;
+                if (gainmapHighBitDepthFlag) {
+                    AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &gainmapBitDepth, 3), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(3) gainmap_bit_depth_minus9;
+                    gainmapBitDepth += 9;
+                } else {
+                    gainmapBitDepth = 8;
+                }
+            }
+
+            AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &tmapHasIcc, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(1) tmap_icc_flag;
+            AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &tmapHasExplicitCicp, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(1) tmap_explicit_cicp_flag;
+            if (tmapHasExplicitCicp) {
+                AVIF_CHECKERR(avifROStreamReadBitsU8(&s, &tmapColorPrimaries, 8), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(8) tmap_colour_primaries;
+                AVIF_CHECKERR(avifROStreamReadBitsU8(&s, &tmapTransferCharacteristics, 8),
+                              AVIF_RESULT_BMFF_PARSE_FAILED); // bit(8) tmap_transfer_characteristics;
+                AVIF_CHECKERR(avifROStreamReadBitsU8(&s, &tmapMatrixCoefficients, 8), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(8) tmap_matrix_coefficients;
+                AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &tmapFullRange, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // bit(1) tmap_full_range_flag;
+            } else {
+                tmapColorPrimaries = AVIF_COLOR_PRIMARIES_BT709;                  // 1
+                tmapTransferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; // 13
+                tmapMatrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601;          // 6
+                tmapFullRange = 1;
+            }
+        }
+        AVIF_CHECKRES(avifParseMiniHDRProperties(&s, &hasClli, &clli));
+        if (hasGainmap) {
+            AVIF_CHECKRES(avifParseMiniHDRProperties(&s, &tmapHasClli, &tmapClli));
+        }
+#else
         return AVIF_RESULT_NOT_IMPLEMENTED;
+#endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP
     }
 
     // Chunk sizes
@@ -3778,15 +3891,24 @@
                       AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(few_metadata_bytes_flag ? 10 : 20) icc_data_size_minus1;
         ++iccDataSize;
     }
-    // if (hdr_flag && gainmap_flag && tmap_icc_flag)
-    //     unsigned int(few_metadata_bytes_flag ? 10 : 20) tmap_icc_data_size_minus1;
+    uint32_t tmapIccDataSize = 0;
+    if (hasHdr && hasGainmap && tmapHasIcc) {
+        AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &tmapIccDataSize, fewMetadataBytesFlag ? 10 : 20),
+                      AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(few_metadata_bytes_flag ? 10 : 20) tmap_icc_data_size_minus1;
+        ++tmapIccDataSize;
+    }
 
-    // if (hdr_flag && gainmap_flag)
-    //     unsigned int(few_metadata_bytes_flag ? 10 : 20) gainmap_metadata_size;
-    // if (hdr_flag && gainmap_flag)
-    //     unsigned int(few_item_data_bytes_flag ? 15 : 28) gainmap_item_data_size;
-    // if (hdr_flag && gainmap_flag && gainmap_item_data_size > 0)
-    //     unsigned int(few_codec_config_bytes_flag ? 3 : 12) gainmap_item_codec_config_size;
+    uint32_t gainmapMetadataSize = 0, gainmapItemDataSize = 0, gainmapItemCodecConfigSize = 0;
+    if (hasHdr && hasGainmap) {
+        AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &gainmapMetadataSize, fewMetadataBytesFlag ? 10 : 20),
+                      AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(few_metadata_bytes_flag ? 10 : 20) gainmap_metadata_size;
+        AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &gainmapItemDataSize, fewItemDataBytesFlag ? 15 : 28),
+                      AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(few_item_data_bytes_flag ? 15 : 28) gainmap_item_data_size;
+        if (gainmapItemDataSize > 0) {
+            AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &gainmapItemCodecConfigSize, fewCodecConfigBytesFlag ? 3 : 12),
+                          AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(few_codec_config_bytes_flag ? 3 : 12) gainmap_item_codec_config_size;
+        }
+    }
 
     uint32_t mainItemCodecConfigSize, mainItemDataSize;
     AVIF_CHECKERR(avifROStreamReadBitsU32(&s, &mainItemCodecConfigSize, fewCodecConfigBytesFlag ? 3 : 12),
@@ -3825,18 +3947,20 @@
         AVIF_CHECKERR(padding == 0, AVIF_RESULT_BMFF_PARSE_FAILED); // Only accept zeros as padding.
     }
 
-    // Chunks
+    // Codec configuration ('av1C' always uses 4 bytes)
     avifCodecConfigurationBox alphaItemCodecConfig = { 0 };
     if (hasAlpha && alphaItemDataSize != 0 && alphaItemCodecConfigSize != 0) {
-        // 'av1C' always uses 4 bytes.
         AVIF_CHECKERR(alphaItemCodecConfigSize == 4, AVIF_RESULT_BMFF_PARSE_FAILED);
         AVIF_CHECKERR(avifParseCodecConfiguration(&s, &alphaItemCodecConfig, (const char *)codecConfigType, diag),
                       AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(8) alpha_item_codec_config[alpha_item_codec_config_size];
     }
-    // if (hdr_flag && gainmap_flag && gainmap_item_codec_config_size > 0)
-    //     unsigned int(8) gainmap_item_codec_config[gainmap_item_codec_config_size];
+    avifCodecConfigurationBox gainmapItemCodecConfig = { 0 };
+    if (hasHdr && hasGainmap && gainmapItemCodecConfigSize != 0) {
+        AVIF_CHECKERR(gainmapItemCodecConfigSize == 4, AVIF_RESULT_BMFF_PARSE_FAILED);
+        AVIF_CHECKERR(avifParseCodecConfiguration(&s, &gainmapItemCodecConfig, (const char *)codecConfigType, diag),
+                      AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(8) gainmap_item_codec_config[gainmap_item_codec_config_size];
+    }
     avifCodecConfigurationBox mainItemCodecConfig;
-    // 'av1C' always uses 4 bytes.
     AVIF_CHECKERR(mainItemCodecConfigSize == 4, AVIF_RESULT_BMFF_PARSE_FAILED);
     AVIF_CHECKERR(avifParseCodecConfiguration(&s, &mainItemCodecConfig, (const char *)codecConfigType, diag),
                   AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(8) main_item_codec_config[main_item_codec_config_size];
@@ -3844,7 +3968,8 @@
     // Make sure all metadata and coded chunks fit into the 'meta' box whose size is rawLen.
     // There should be no missing nor unused byte.
 
-    AVIF_CHECKERR(avifROStreamRemainingBytes(&s) == (uint64_t)iccDataSize + alphaItemDataSize + mainItemDataSize + exifDataSize + xmpDataSize,
+    AVIF_CHECKERR(avifROStreamRemainingBytes(&s) == (uint64_t)iccDataSize + tmapIccDataSize + gainmapMetadataSize + alphaItemDataSize +
+                                                        gainmapItemDataSize + mainItemDataSize + exifDataSize + xmpDataSize,
                   AVIF_RESULT_BMFF_PARSE_FAILED);
 
     // Create the items and properties generated by the MinimizedImageBox.
@@ -3894,6 +4019,23 @@
         alphaItem->miniBoxChromaSamplePosition = AVIF_CHROMA_SAMPLE_POSITION_UNKNOWN;
     }
 
+    avifDecoderItem * tmapItem = NULL;
+    if (hasGainmap) {
+        AVIF_CHECKRES(avifMetaFindOrCreateItem(meta, /*itemID=*/3, &tmapItem));
+        memcpy(tmapItem->type, "tmap", 4);
+        colorItem->dimgForID = tmapItem->id;
+        colorItem->dimgIdx = 0;
+    }
+    avifDecoderItem * gainmapItem = NULL;
+    if (gainmapItemDataSize != 0) {
+        AVIF_CHECKRES(avifMetaFindOrCreateItem(meta, /*itemID=*/4, &gainmapItem));
+        memcpy(gainmapItem->type, infeType, 4);
+        gainmapItem->width = gainmapWidth;
+        gainmapItem->height = gainmapHeight;
+        gainmapItem->dimgForID = tmapItem->id;
+        gainmapItem->dimgIdx = 1;
+    }
+
     // Property with fixed index 1.
     avifProperty * colorCodecConfigProp = avifMetaCreateProperty(meta, (const char *)codecConfigType);
     AVIF_CHECKERR(colorCodecConfigProp, AVIF_RESULT_OUT_OF_MEMORY);
@@ -3927,7 +4069,7 @@
     AVIF_CHECKERR(avifDecoderItemAddProperty(colorItem, colrPropNCLX), AVIF_RESULT_OUT_OF_MEMORY);
 
     // Property with fixed index 5.
-    if (iccDataSize) {
+    if (iccDataSize != 0) {
         avifProperty * colrPropICC = avifMetaCreateProperty(meta, "colr");
         AVIF_CHECKERR(colrPropICC, AVIF_RESULT_OUT_OF_MEMORY);
         colrPropICC->u.colr.hasICC = AVIF_TRUE; // colour_type "rICC" or "prof"
@@ -3939,11 +4081,6 @@
         AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY); // Placeholder.
     }
 
-    // if (hdr_flag && gainmap_flag && tmap_icc_flag)
-    //     unsigned int(8) tmap_icc_data[tmap_icc_data_size_minus1 + 1];
-    // if (hdr_flag && gainmap_flag && gainmap_metadata_size > 0)
-    //     unsigned int(8) gainmap_metadata[gainmap_metadata_size];
-
     if (hasAlpha) {
         // Property with fixed index 6.
         avifProperty * alphaCodecConfigProp = avifMetaCreateProperty(meta, (const char *)codecConfigType);
@@ -3996,59 +4133,178 @@
         AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY); // Placeholder.
     }
 
-    // HDR placeholders (unnecessary but added for specification matching).
-    for (int i = 11; i <= 29; ++i) {
+    if (hasClli) {
+        // Property with fixed index 11.
+        avifProperty * clliProp = avifMetaCreateProperty(meta, "clli");
+        AVIF_CHECKERR(clliProp, AVIF_RESULT_OUT_OF_MEMORY);
+        clliProp->u.clli = clli;
+        AVIF_CHECKERR(avifDecoderItemAddProperty(colorItem, clliProp), AVIF_RESULT_OUT_OF_MEMORY);
+    } else {
+        AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY); // Placeholder.
+    }
+    // Properties with fixed indices 12 to 16 are ignored by libavif (mdcv, cclv, amve, reve and ndwt).
+    for (int i = 12; i <= 16; ++i) {
+        AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY); // Placeholder.
+    }
+
+    if (gainmapItemCodecConfigSize != 0) {
+        // Property with fixed index 17.
+        avifProperty * gainmapCodecConfigProp = avifMetaCreateProperty(meta, (const char *)codecConfigType);
+        AVIF_CHECKERR(gainmapCodecConfigProp, AVIF_RESULT_OUT_OF_MEMORY);
+        gainmapCodecConfigProp->u.av1C = gainmapItemCodecConfig;
+        AVIF_CHECKERR(avifDecoderItemAddProperty(gainmapItem, gainmapCodecConfigProp), AVIF_RESULT_OUT_OF_MEMORY);
+    } else {
+        AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY); // Placeholder.
+    }
+
+    if (gainmapItemDataSize != 0) {
+        // Property with fixed index 18.
+        avifProperty * gainmapIspeProp = avifMetaCreateProperty(meta, "ispe");
+        AVIF_CHECKERR(gainmapIspeProp, AVIF_RESULT_OUT_OF_MEMORY);
+        gainmapIspeProp->u.ispe.width = gainmapWidth;
+        gainmapIspeProp->u.ispe.height = gainmapHeight;
+        AVIF_CHECKERR(avifDecoderItemAddProperty(gainmapItem, gainmapIspeProp), AVIF_RESULT_OUT_OF_MEMORY);
+
+        // Property with fixed index 19.
+        avifProperty * gainmapPixiProp = avifMetaCreateProperty(meta, "pixi");
+        AVIF_CHECKERR(gainmapPixiProp, AVIF_RESULT_OUT_OF_MEMORY);
+        memcpy(gainmapPixiProp->type, "pixi", 4);
+        gainmapPixiProp->u.pixi.planeCount = gainmapChromaSubsampling == 0 ? 1 : 3;
+        for (uint8_t plane = 0; plane < gainmapPixiProp->u.pixi.planeCount; ++plane) {
+            gainmapPixiProp->u.pixi.planeDepths[plane] = (uint8_t)gainmapBitDepth;
+        }
+        AVIF_CHECKERR(avifDecoderItemAddProperty(gainmapItem, gainmapPixiProp), AVIF_RESULT_OUT_OF_MEMORY);
+
+        // Property with fixed index 20.
+        avifProperty * gainmapColrPropNCLX = avifMetaCreateProperty(meta, "colr");
+        AVIF_CHECKERR(gainmapColrPropNCLX, AVIF_RESULT_OUT_OF_MEMORY);
+        gainmapColrPropNCLX->u.colr.hasNCLX = AVIF_TRUE;                                                 // colour_type "nclx"
+        gainmapColrPropNCLX->u.colr.colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED;                   // 2
+        gainmapColrPropNCLX->u.colr.transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; // 2
+        gainmapColrPropNCLX->u.colr.matrixCoefficients = (avifMatrixCoefficients)gainmapMatrixCoefficients;
+        gainmapColrPropNCLX->u.colr.range = gainmapFullRange ? AVIF_RANGE_FULL : AVIF_RANGE_LIMITED;
+        AVIF_CHECKERR(avifDecoderItemAddProperty(gainmapItem, gainmapColrPropNCLX), AVIF_RESULT_OUT_OF_MEMORY);
+    } else {
+        // Placeholders 18, 19 and 20.
+        AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY);
+        AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY);
         AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY);
     }
 
+    if (hasGainmap) {
+        // Property with fixed index 21.
+        avifProperty * tmapIspeProp = avifMetaCreateProperty(meta, "ispe");
+        AVIF_CHECKERR(tmapIspeProp, AVIF_RESULT_OUT_OF_MEMORY);
+        tmapIspeProp->u.ispe.width = orientation <= 4 ? width : height;
+        tmapIspeProp->u.ispe.height = orientation <= 4 ? height : width;
+        AVIF_CHECKERR(avifDecoderItemAddProperty(tmapItem, tmapIspeProp), AVIF_RESULT_OUT_OF_MEMORY);
+    } else {
+        AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY); // Placeholder.
+    }
+
+    if (hasGainmap && (tmapHasExplicitCicp || !tmapHasIcc)) {
+        // Property with fixed index 22.
+        avifProperty * tmapColrPropNCLX = avifMetaCreateProperty(meta, "colr");
+        AVIF_CHECKERR(tmapColrPropNCLX, AVIF_RESULT_OUT_OF_MEMORY);
+        tmapColrPropNCLX->u.colr.hasNCLX = AVIF_TRUE; // colour_type "nclx"
+        tmapColrPropNCLX->u.colr.colorPrimaries = (avifColorPrimaries)tmapColorPrimaries;
+        tmapColrPropNCLX->u.colr.transferCharacteristics = (avifTransferCharacteristics)tmapTransferCharacteristics;
+        tmapColrPropNCLX->u.colr.matrixCoefficients = (avifMatrixCoefficients)tmapMatrixCoefficients;
+        tmapColrPropNCLX->u.colr.range = tmapFullRange ? AVIF_RANGE_FULL : AVIF_RANGE_LIMITED;
+        AVIF_CHECKERR(avifDecoderItemAddProperty(tmapItem, tmapColrPropNCLX), AVIF_RESULT_OUT_OF_MEMORY);
+    } else {
+        AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY); // Placeholder.
+    }
+
+    if (tmapIccDataSize != 0) {
+        // Property with fixed index 23.
+        avifProperty * tmapColrPropICC = avifMetaCreateProperty(meta, "colr");
+        AVIF_CHECKERR(tmapColrPropICC, AVIF_RESULT_OUT_OF_MEMORY);
+        tmapColrPropICC->u.colr.hasICC = AVIF_TRUE; // colour_type "rICC" or "prof"
+        tmapColrPropICC->u.colr.iccOffset = rawOffset + avifROStreamOffset(&s);
+        tmapColrPropICC->u.colr.iccSize = tmapIccDataSize;
+        AVIF_CHECKERR(avifROStreamSkip(&s, tmapColrPropICC->u.colr.iccSize), AVIF_RESULT_BMFF_PARSE_FAILED);
+        AVIF_CHECKERR(avifDecoderItemAddProperty(colorItem, tmapColrPropICC), AVIF_RESULT_OUT_OF_MEMORY);
+    } else {
+        AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY); // Placeholder.
+    }
+
+    if (tmapHasClli) {
+        // Property with fixed index 24.
+        avifProperty * tmapClliProp = avifMetaCreateProperty(meta, "clli");
+        AVIF_CHECKERR(tmapClliProp, AVIF_RESULT_OUT_OF_MEMORY);
+        tmapClliProp->u.clli = tmapClli;
+        AVIF_CHECKERR(avifDecoderItemAddProperty(tmapItem, tmapClliProp), AVIF_RESULT_OUT_OF_MEMORY);
+    } else {
+        AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY); // Placeholder.
+    }
+    // Properties with fixed indices 25 to 29 are ignored by libavif (mdcv, cclv, amve, reve and ndwt).
+    for (int i = 25; i <= 29; ++i) {
+        AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY); // Placeholder.
+    }
+    AVIF_ASSERT_OR_RETURN(meta->properties.count == 29);
+
     // Extents.
 
+    if (gainmapMetadataSize != 0) {
+        // Prepend the version field to the GainMapMetadata to form the ToneMapImage syntax.
+        tmapItem->size = gainmapMetadataSize + 1;
+        AVIF_CHECKRES(avifRWDataRealloc(&tmapItem->mergedExtents, tmapItem->size));
+        tmapItem->ownsMergedExtents = AVIF_TRUE;
+        tmapItem->mergedExtents.data[0] = 0; // unsigned int(8) version = 0;
+        AVIF_CHECKERR(avifROStreamRead(&s, tmapItem->mergedExtents.data + 1, gainmapMetadataSize), AVIF_RESULT_BMFF_PARSE_FAILED);
+    }
+
     if (hasAlpha) {
         avifExtent * alphaExtent = (avifExtent *)avifArrayPush(&alphaItem->extents);
         AVIF_CHECKERR(alphaExtent, AVIF_RESULT_OUT_OF_MEMORY);
         alphaExtent->offset = rawOffset + avifROStreamOffset(&s);
-        alphaExtent->size = (size_t)alphaItemDataSize;
+        alphaExtent->size = alphaItemDataSize;
         AVIF_CHECKERR(avifROStreamSkip(&s, alphaExtent->size), AVIF_RESULT_BMFF_PARSE_FAILED);
         alphaItem->size = alphaExtent->size;
     }
 
-    // if (hdr_flag && gainmap_flag && gainmap_item_data_size > 0)
-    //     unsigned int(8) gainmap_item_data[gainmap_item_data_size];
+    if (gainmapItemDataSize != 0) {
+        avifExtent * gainmapExtent = (avifExtent *)avifArrayPush(&gainmapItem->extents);
+        AVIF_CHECKERR(gainmapExtent, AVIF_RESULT_OUT_OF_MEMORY);
+        gainmapExtent->offset = rawOffset + avifROStreamOffset(&s);
+        gainmapExtent->size = gainmapItemDataSize;
+        AVIF_CHECKERR(avifROStreamSkip(&s, gainmapExtent->size), AVIF_RESULT_BMFF_PARSE_FAILED);
+        gainmapItem->size = gainmapExtent->size;
+    }
 
     avifExtent * colorExtent = (avifExtent *)avifArrayPush(&colorItem->extents);
     AVIF_CHECKERR(colorExtent, AVIF_RESULT_OUT_OF_MEMORY);
     colorExtent->offset = rawOffset + avifROStreamOffset(&s);
-    colorExtent->size = (size_t)mainItemDataSize;
+    colorExtent->size = mainItemDataSize;
     AVIF_CHECKERR(avifROStreamSkip(&s, colorExtent->size), AVIF_RESULT_BMFF_PARSE_FAILED);
     colorItem->size = colorExtent->size;
 
     if (hasExif) {
         avifDecoderItem * exifItem;
-        AVIF_CHECKRES(avifMetaFindOrCreateItem(meta, /*itemID=*/3, &exifItem));
+        AVIF_CHECKRES(avifMetaFindOrCreateItem(meta, /*itemID=*/6, &exifItem));
         memcpy(exifItem->type, "Exif", 4);
-        exifItem->descForID = colorItem->id;
-        colorItem->premByID = alphaIsPremultiplied;
+        exifItem->descForID = colorItem->id; // 'cdsc'
 
         avifExtent * exifExtent = (avifExtent *)avifArrayPush(&exifItem->extents);
         AVIF_CHECKERR(exifExtent, AVIF_RESULT_OUT_OF_MEMORY);
         exifExtent->offset = rawOffset + avifROStreamOffset(&s);
-        exifExtent->size = (size_t)exifDataSize; // Does not include unsigned int(32) exif_tiff_header_offset;
+        exifExtent->size = exifDataSize; // Does not include unsigned int(32) exif_tiff_header_offset;
         AVIF_CHECKERR(avifROStreamSkip(&s, exifExtent->size), AVIF_RESULT_BMFF_PARSE_FAILED);
         exifItem->size = exifExtent->size;
     }
 
     if (hasXmp) {
         avifDecoderItem * xmpItem;
-        AVIF_CHECKRES(avifMetaFindOrCreateItem(meta, /*itemID=*/4, &xmpItem));
+        AVIF_CHECKRES(avifMetaFindOrCreateItem(meta, /*itemID=*/7, &xmpItem));
         memcpy(xmpItem->type, "mime", 4);
         memcpy(xmpItem->contentType.contentType, AVIF_CONTENT_TYPE_XMP, sizeof(AVIF_CONTENT_TYPE_XMP));
-        xmpItem->descForID = colorItem->id;
-        colorItem->premByID = alphaIsPremultiplied;
+        xmpItem->descForID = colorItem->id; // 'cdsc'
 
         avifExtent * xmpExtent = (avifExtent *)avifArrayPush(&xmpItem->extents);
         AVIF_CHECKERR(xmpExtent, AVIF_RESULT_OUT_OF_MEMORY);
         xmpExtent->offset = rawOffset + avifROStreamOffset(&s);
-        xmpExtent->size = (size_t)xmpDataSize;
+        xmpExtent->size = xmpDataSize;
         AVIF_CHECKERR(avifROStreamSkip(&s, xmpExtent->size), AVIF_RESULT_BMFF_PARSE_FAILED);
         xmpItem->size = xmpExtent->size;
     }
@@ -4247,7 +4503,7 @@
             AVIF_CHECKERR(!miniSeen, AVIF_RESULT_BMFF_PARSE_FAILED);
             const avifBool isAvifAccordingToMinorVersion = !memcmp(ftyp.minorVersion, "avif", 4);
             AVIF_CHECKRES(
-                avifParseMinimizedImageBox(data->meta, boxOffset, boxContents.data, boxContents.size, isAvifAccordingToMinorVersion, data->diag));
+                avifParseMinimizedImageBox(data, boxOffset, boxContents.data, boxContents.size, isAvifAccordingToMinorVersion, data->diag));
             miniSeen = AVIF_TRUE;
 #endif
         } else if (isMoov) {
diff --git a/src/write.c b/src/write.c
index 1587586..7e48280 100644
--- a/src/write.c
+++ b/src/write.c
@@ -676,6 +676,14 @@
     return avifEncoderWriteExtendedColorProperties(dedupStream, outputStream, imageMetadata, ipma, dedup);
 }
 
+static avifResult avifEncoderWriteContentLightLevelInformation(avifRWStream * outputStream,
+                                                               const avifContentLightLevelInformationBox * clli)
+{
+    AVIF_CHECKRES(avifRWStreamWriteBits(outputStream, clli->maxCLL, 16));  // unsigned int(16) max_content_light_level;
+    AVIF_CHECKRES(avifRWStreamWriteBits(outputStream, clli->maxPALL, 16)); // unsigned int(16) max_pic_average_light_level;
+    return AVIF_RESULT_OK;
+}
+
 // Same as 'avifEncoderWriteColorProperties' but for properties related to High Dynamic Range only.
 static avifResult avifEncoderWriteHDRProperties(avifRWStream * dedupStream,
                                                 avifRWStream * outputStream,
@@ -690,19 +698,57 @@
         }
         avifBoxMarker clli;
         AVIF_CHECKRES(avifRWStreamWriteBox(dedupStream, "clli", AVIF_BOX_SIZE_TBD, &clli));
-        AVIF_CHECKRES(avifRWStreamWriteU16(dedupStream, imageMetadata->clli.maxCLL)); // unsigned int(16) max_content_light_level;
-        AVIF_CHECKRES(avifRWStreamWriteU16(dedupStream, imageMetadata->clli.maxPALL)); // unsigned int(16) max_pic_average_light_level;
+        AVIF_CHECKRES(avifEncoderWriteContentLightLevelInformation(dedupStream, &imageMetadata->clli));
         avifRWStreamFinishBox(dedupStream, clli);
         if (dedup) {
             AVIF_CHECKRES(avifItemPropertyDedupFinish(dedup, outputStream, ipma, AVIF_FALSE));
         }
     }
 
-    // TODO(maryla): add other HDR boxes: mdcv, cclv, etc.
+    // TODO(maryla): add other HDR boxes: mdcv, cclv, etc. (in avifEncoderWriteMiniHDRProperties() too)
 
     return AVIF_RESULT_OK;
 }
 
+#if defined(AVIF_ENABLE_EXPERIMENTAL_MINI) && defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+static avifResult avifEncoderWriteMiniHDRProperties(avifRWStream * outputStream, const avifImage * imageMetadata)
+{
+    const avifBool hasClli = imageMetadata->clli.maxCLL != 0 || imageMetadata->clli.maxPALL != 0;
+    const avifBool hasMdcv = AVIF_FALSE;
+    const avifBool hasCclv = AVIF_FALSE;
+    const avifBool hasAmve = AVIF_FALSE;
+    const avifBool hasReve = AVIF_FALSE;
+    const avifBool hasNdwt = AVIF_FALSE;
+    AVIF_CHECKRES(avifRWStreamWriteBits(outputStream, hasClli, 1)); // bit(1) clli_flag;
+    AVIF_CHECKRES(avifRWStreamWriteBits(outputStream, hasMdcv, 1)); // bit(1) mdcv_flag;
+    AVIF_CHECKRES(avifRWStreamWriteBits(outputStream, hasCclv, 1)); // bit(1) cclv_flag;
+    AVIF_CHECKRES(avifRWStreamWriteBits(outputStream, hasAmve, 1)); // bit(1) amve_flag;
+    AVIF_CHECKRES(avifRWStreamWriteBits(outputStream, hasReve, 1)); // bit(1) reve_flag;
+    AVIF_CHECKRES(avifRWStreamWriteBits(outputStream, hasNdwt, 1)); // bit(1) ndwt_flag;
+
+    if (hasClli) {
+        // ContentLightLevel clli;
+        AVIF_CHECKRES(avifEncoderWriteContentLightLevelInformation(outputStream, &imageMetadata->clli));
+    }
+    if (hasMdcv) {
+        // MasteringDisplayColourVolume mdcv;
+    }
+    if (hasCclv) {
+        // ContentColourVolume cclv;
+    }
+    if (hasAmve) {
+        // AmbientViewingEnvironment amve;
+    }
+    if (hasReve) {
+        // ReferenceViewingEnvironment reve;
+    }
+    if (hasNdwt) {
+        // NominalDiffuseWhite ndwt;
+    }
+    return AVIF_RESULT_OK;
+}
+#endif // AVIF_ENABLE_EXPERIMENTAL_MINI && AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP
+
 static avifResult avifEncoderWriteExtendedColorProperties(avifRWStream * dedupStream,
                                                           avifRWStream * outputStream,
                                                           const avifImage * imageMetadata,
@@ -1262,6 +1308,7 @@
     sampleTransformItem->itemCategory = AVIF_ITEM_SAMPLE_TRANSFORM;
     uint16_t sampleTransformItemID = sampleTransformItem->id;
     // 'altr' group
+    AVIF_ASSERT_OR_RETURN(encoder->data->alternativeItemIDs.count == 0);
     uint16_t * alternativeItemID = (uint16_t *)avifArrayPush(&encoder->data->alternativeItemIDs);
     AVIF_CHECKERR(alternativeItemID != NULL, AVIF_RESULT_OUT_OF_MEMORY);
     *alternativeItemID = sampleTransformItem->id;
@@ -2330,26 +2377,72 @@
     }
 #endif
 
+    // Check for maximum field values and maximum chunk sizes.
+
+    // width_minus1 and height_minus1
     if (encoder->data->imageMetadata->width > (1 << 15) || encoder->data->imageMetadata->height > (1 << 15)) {
         return AVIF_FALSE;
     }
+    // icc_data_size_minus1, exif_data_size_minus1 and xmp_data_size_minus1
     if (encoder->data->imageMetadata->icc.size > (1 << 20) || encoder->data->imageMetadata->exif.size > (1 << 20) ||
         encoder->data->imageMetadata->xmp.size > (1 << 20)) {
         return AVIF_FALSE;
     }
+#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+    // gainmap_width_minus1 and gainmap_height_minus1
+    if (encoder->data->imageMetadata->gainMap != NULL && encoder->data->imageMetadata->gainMap->image != NULL &&
+        (encoder->data->imageMetadata->gainMap->image->width > (1 << 15) ||
+         encoder->data->imageMetadata->gainMap->image->height > (1 << 15))) {
+        return AVIF_FALSE;
+    }
+    // tmap_icc_data_size_minus1
+    if (encoder->data->altImageMetadata->icc.size > (1 << 20)) {
+        return AVIF_FALSE;
+    }
+    // gainmap_metadata_size
+    if (encoder->data->imageMetadata->gainMap != NULL && avifGainMapMetadataSize(encoder->data->imageMetadata->gainMap) >= (1 << 20)) {
+        return AVIF_FALSE;
+    }
+#endif
 
     // 4:4:4, 4:2:2, 4:2:0 and 4:0:0 are supported by a MinimizedImageBox.
+    // chroma_subsampling
     if (encoder->data->imageMetadata->yuvFormat != AVIF_PIXEL_FORMAT_YUV444 &&
         encoder->data->imageMetadata->yuvFormat != AVIF_PIXEL_FORMAT_YUV422 &&
         encoder->data->imageMetadata->yuvFormat != AVIF_PIXEL_FORMAT_YUV420 &&
         encoder->data->imageMetadata->yuvFormat != AVIF_PIXEL_FORMAT_YUV400) {
         return AVIF_FALSE;
     }
+#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+    // gainmap_chroma_subsampling
+    if (encoder->data->imageMetadata->gainMap != NULL && encoder->data->imageMetadata->gainMap->image != NULL &&
+        (encoder->data->imageMetadata->gainMap->image->yuvFormat != AVIF_PIXEL_FORMAT_YUV444 &&
+         encoder->data->imageMetadata->gainMap->image->yuvFormat != AVIF_PIXEL_FORMAT_YUV422 &&
+         encoder->data->imageMetadata->gainMap->image->yuvFormat != AVIF_PIXEL_FORMAT_YUV420 &&
+         encoder->data->imageMetadata->gainMap->image->yuvFormat != AVIF_PIXEL_FORMAT_YUV400)) {
+        return AVIF_FALSE;
+    }
+#endif
 
+    // colour_primaries, transfer_characteristics and matrix_coefficients
     if (encoder->data->imageMetadata->colorPrimaries > 255 || encoder->data->imageMetadata->transferCharacteristics > 255 ||
         encoder->data->imageMetadata->matrixCoefficients > 255) {
         return AVIF_FALSE;
     }
+#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+    // gainmap_colour_primaries, gainmap_transfer_characteristics and gainmap_matrix_coefficients
+    if (encoder->data->imageMetadata->gainMap != NULL && encoder->data->imageMetadata->gainMap->image != NULL &&
+        (encoder->data->imageMetadata->gainMap->image->colorPrimaries > 255 ||
+         encoder->data->imageMetadata->gainMap->image->transferCharacteristics > 255 ||
+         encoder->data->imageMetadata->gainMap->image->matrixCoefficients > 255)) {
+        return AVIF_FALSE;
+    }
+    // tmap_colour_primaries, tmap_transfer_characteristics and tmap_matrix_coefficients
+    if (encoder->data->altImageMetadata->colorPrimaries > 255 || encoder->data->altImageMetadata->transferCharacteristics > 255 ||
+        encoder->data->altImageMetadata->matrixCoefficients > 255) {
+        return AVIF_FALSE;
+    }
+#endif
 
     const avifEncoderItem * colorItem = NULL;
     for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) {
@@ -2363,19 +2456,32 @@
         if (item->id == encoder->data->primaryItemID) {
             assert(!colorItem);
             colorItem = item;
-            // main_item_data_size_minus_one so 2^28 inclusive.
+            // main_item_data_size_minus1
             if (item->encodeOutput->samples.count != 1 || item->encodeOutput->samples.sample[0].data.size > (1 << 28)) {
                 return AVIF_FALSE;
             }
             continue; // The primary item can be stored in the MinimizedImageBox.
         }
         if (item->itemCategory == AVIF_ITEM_ALPHA && item->irefToID == encoder->data->primaryItemID) {
-            // alpha_item_data_size so 2^28 exclusive.
+            // alpha_item_data_size
             if (item->encodeOutput->samples.count != 1 || item->encodeOutput->samples.sample[0].data.size >= (1 << 28)) {
                 return AVIF_FALSE;
             }
             continue; // The alpha auxiliary item can be stored in the MinimizedImageBox.
         }
+#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+        if (item->itemCategory == AVIF_ITEM_GAIN_MAP) {
+            // gainmap_item_data_size
+            if (item->encodeOutput->samples.count != 1 || item->encodeOutput->samples.sample[0].data.size >= (1 << 28)) {
+                return AVIF_FALSE;
+            }
+            continue; // The gainmap input image item can be stored in the MinimizedImageBox.
+        }
+        if (!memcmp(item->type, "tmap", 4)) {
+            assert(item->itemCategory == AVIF_ITEM_COLOR); // Cannot be differentiated from the primary item by its itemCategory.
+            continue; // The tone mapping derived image item can be represented in the MinimizedImageBox.
+        }
+#endif
         if (!memcmp(item->type, "mime", 4) && !memcmp(item->infeName, "XMP", item->infeNameSize)) {
             assert(item->metadataPayload.size == encoder->data->imageMetadata->xmp.size);
             continue; // XMP metadata can be stored in the MinimizedImageBox.
@@ -2389,7 +2495,7 @@
             continue; // Exif metadata can be stored in the MinimizedImageBox if exif_tiff_header_offset is 0.
         }
 
-        // Items besides the colorItem, the alphaItem and Exif/XMP/ICC
+        // Items besides the colorItem, the alphaItem, the gainmap item and Exif/XMP/ICC/HDR
         // metadata are not directly supported by the MinimizedImageBox.
         return AVIF_FALSE;
     }
@@ -2424,6 +2530,7 @@
 {
     const avifEncoderItem * colorItem = NULL;
     const avifEncoderItem * alphaItem = NULL;
+    const avifEncoderItem * gainmapItem = NULL;
     for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) {
         avifEncoderItem * item = &encoder->data->items.item[itemIndex];
         if (item->id == encoder->data->primaryItemID) {
@@ -2433,18 +2540,25 @@
             AVIF_ASSERT_OR_RETURN(!alphaItem);
             alphaItem = item;
         }
+#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+        if (item->itemCategory == AVIF_ITEM_GAIN_MAP) {
+            AVIF_ASSERT_OR_RETURN(!gainmapItem);
+            gainmapItem = item;
+        }
+#endif
     }
 
     AVIF_ASSERT_OR_RETURN(colorItem);
     const avifRWData * colorData = &colorItem->encodeOutput->samples.sample[0].data;
     const avifRWData * alphaData = alphaItem ? &alphaItem->encodeOutput->samples.sample[0].data : NULL;
+    const avifRWData * gainmapData = gainmapItem ? &gainmapItem->encodeOutput->samples.sample[0].data : NULL;
 
     const avifImage * const image = encoder->data->imageMetadata;
 
     const avifBool hasAlpha = alphaItem != NULL;
     const avifBool alphaIsPremultiplied = encoder->data->imageMetadata->alphaPremultiplied;
-    const avifBool hasHdr = AVIF_FALSE;     // Not implemented.
-    const avifBool hasGainmap = AVIF_FALSE; // Not implemented.
+    const avifBool hasGainmap = gainmapItem != NULL;
+    const avifBool hasHdr = hasGainmap; // libavif only supports gainmap-based HDR encoding for now.
     const avifBool hasIcc = image->icc.size != 0;
     const uint32_t chromaSubsampling = image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400   ? 0
                                        : image->yuvFormat == AVIF_PIXEL_FORMAT_YUV420 ? 1
@@ -2483,11 +2597,25 @@
 
     const avifBool hasExplicitCodecTypes = AVIF_FALSE; // 'av01' and 'av1C' known from 'avif' minor_version field of FileTypeBox.
 
-    const uint32_t smallDimensionsFlag = image->width <= (1 << 7) && image->height <= (1 << 7);
+    uint32_t smallDimensionsFlag = image->width <= (1 << 7) && image->height <= (1 << 7);
     const uint32_t codecConfigSize = 4; // 'av1C' always uses 4 bytes.
+    uint32_t gainmapMetadataSize = 0;
     const uint32_t fewCodecConfigBytesFlag = codecConfigSize < (1 << 3);
-    const uint32_t fewItemDataBytesFlag = colorData->size <= (1 << 15) && (!alphaData || alphaData->size < (1 << 15));
-    const uint32_t fewMetadataBytesFlag = image->icc.size <= (1 << 10) && image->exif.size <= (1 << 10) && image->xmp.size <= (1 << 10);
+    uint32_t fewItemDataBytesFlag = colorData->size <= (1 << 15) && (!alphaData || alphaData->size < (1 << 15));
+    uint32_t fewMetadataBytesFlag = image->icc.size <= (1 << 10) && image->exif.size <= (1 << 10) && image->xmp.size <= (1 << 10);
+
+#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+    if (hasGainmap) {
+        AVIF_ASSERT_OR_RETURN(image->gainMap != NULL && image->gainMap->image != NULL);
+        gainmapMetadataSize = avifGainMapMetadataSize(image->gainMap);
+        AVIF_ASSERT_OR_RETURN(gainmapData != NULL);
+
+        smallDimensionsFlag &= image->gainMap->image->width <= (1 << 7) && image->gainMap->image->height <= (1 << 7);
+        fewItemDataBytesFlag &= gainmapData->size < (1 << 15);
+        fewMetadataBytesFlag &= encoder->data->altImageMetadata->icc.size <= (1 << 10) && gainmapMetadataSize <= (1 << 10);
+        // image->gainMap->image->icc is ignored.
+    }
+#endif
 
     avifBoxMarker mini;
     AVIF_CHECKRES(avifRWStreamWriteBox(s, "mini", AVIF_BOX_SIZE_TBD, &mini));
@@ -2552,80 +2680,69 @@
     }
 
     // High Dynamic Range properties
+    size_t tmapIccSize = 0;
     if (hasHdr) {
-        // bit(1) gainmap_flag;
-        // if (gainmap_flag) {
-        //     unsigned int(small_dimensions_flag ? 7 : 15) gainmap_width_minus1;
-        //     unsigned int(small_dimensions_flag ? 7 : 15) gainmap_height_minus1;
-        //     bit(8) gainmap_matrix_coefficients;
-        //     bit(1) gainmap_full_range_flag;
-        //     bit(2) gainmap_chroma_subsampling;
-        //     if (gainmap_chroma_subsampling == 1 || gainmap_chroma_subsampling == 2)
-        //         bit(1) gainmap_chroma_is_horizontally_centered;
-        //     if (gainmap_chroma_subsampling == 1)
-        //         bit(1) gainmap_chroma_is_vertically_centered;
-        //     bit(1) gainmap_float_flag;
-        //     if (gainmap_float_flag)
-        //         bit(2) gainmap_bit_depth_log2_minus4;
-        //     else {
-        //         bit(1) gainmap_high_bit_depth_flag;
-        //         if (gainmap_high_bit_depth_flag)
-        //             bit(3) gainmap_bit_depth_minus9;
-        //     }
-        //     bit(1) tmap_icc_flag;
-        //     bit(1) tmap_explicit_cicp_flag;
-        //     if (tmap_explicit_cicp_flag) {
-        //         bit(8) tmap_colour_primaries;
-        //         bit(8) tmap_transfer_characteristics;
-        //         bit(8) tmap_matrix_coefficients;
-        //         bit(1) tmap_full_range_flag;
-        //     }
-        //     else {
-        //         tmap_colour_primaries = 1;
-        //         tmap_transfer_characteristics = 13;
-        //         tmap_matrix_coefficients = 6;
-        //         tmap_full_range_flag = 1;
-        //     }
-        // }
-        // bit(1) clli_flag;
-        // bit(1) mdcv_flag;
-        // bit(1) cclv_flag;
-        // bit(1) amve_flag;
-        // bit(1) reve_flag;
-        // bit(1) ndwt_flag;
-        // if (clli_flag)
-        //     ContentLightLevel clli;
-        // if (mdcv_flag)
-        //     MasteringDisplayColourVolume mdcv;
-        // if (cclv_flag)
-        //     ContentColourVolume cclv;
-        // if (amve_flag)
-        //     AmbientViewingEnvironment amve;
-        // if (reve_flag)
-        //     ReferenceViewingEnvironment reve;
-        // if (ndwt_flag)
-        //     NominalDiffuseWhite ndwt;
-        // if (gainmap_flag) {
-        //     bit(1) tmap_clli_flag;
-        //     bit(1) tmap_mdcv_flag;
-        //     bit(1) tmap_cclv_flag;
-        //     bit(1) tmap_amve_flag;
-        //     bit(1) tmap_reve_flag;
-        //     bit(1) tmap_ndwt_flag;
-        //     if (tmap_clli_flag)
-        //         ContentLightLevel tmap_clli;
-        //     if (tmap_mdcv_flag)
-        //         MasteringDisplayColourVolume tmap_mdcv;
-        //     if (tmap_cclv_flag)
-        //         ContentColourVolume tmap_cclv;
-        //     if (tmap_amve_flag)
-        //         AmbientViewingEnvironment tmap_amve;
-        //     if (tmap_reve_flag)
-        //         ReferenceViewingEnvironment tmap_reve;
-        //     if (tmap_ndwt_flag)
-        //         NominalDiffuseWhite tmap_ndwt;
-        // }
-        return AVIF_RESULT_NOT_IMPLEMENTED;
+#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+        AVIF_CHECKRES(avifRWStreamWriteBits(s, hasGainmap, 1)); // bit(1) gainmap_flag;
+        if (hasGainmap) {
+            const avifImage * tmap = encoder->data->altImageMetadata;
+            const avifImage * gainmap = image->gainMap->image;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, gainmap->width - 1, smallDimensionsFlag ? 7 : 15)); // unsigned int(small_dimensions_flag ? 7 : 15) gainmap_width_minus1;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, gainmap->height - 1, smallDimensionsFlag ? 7 : 15)); // unsigned int(small_dimensions_flag ? 7 : 15) gainmap_height_minus1;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, gainmap->matrixCoefficients, 8)); // bit(8) gainmap_matrix_coefficients;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, gainmap->yuvRange == AVIF_RANGE_FULL, 1)); // bit(1) gainmap_full_range_flag;
+            const uint32_t gainmapChromaSubsampling = gainmap->yuvFormat == AVIF_PIXEL_FORMAT_YUV400   ? 0
+                                                      : gainmap->yuvFormat == AVIF_PIXEL_FORMAT_YUV420 ? 1
+                                                      : gainmap->yuvFormat == AVIF_PIXEL_FORMAT_YUV422 ? 2
+                                                                                                       : 3;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, gainmapChromaSubsampling, 2)); // bit(1) gainmap_chroma_subsampling;
+            if (gainmapChromaSubsampling == 1 || gainmapChromaSubsampling == 2) {
+                AVIF_CHECKRES(avifRWStreamWriteBits(s,
+                                                    gainmap->yuvFormat == AVIF_PIXEL_FORMAT_YUV420 &&
+                                                        gainmap->yuvChromaSamplePosition != AVIF_CHROMA_SAMPLE_POSITION_VERTICAL &&
+                                                        gainmap->yuvChromaSamplePosition != AVIF_CHROMA_SAMPLE_POSITION_COLOCATED,
+                                                    1)); // bit(1) gainmap_chroma_is_horizontally_centered;
+            }
+            if (gainmapChromaSubsampling == 1) {
+                AVIF_CHECKRES(avifRWStreamWriteBits(s,
+                                                    gainmap->yuvFormat == AVIF_PIXEL_FORMAT_YUV420 &&
+                                                        gainmap->yuvChromaSamplePosition != AVIF_CHROMA_SAMPLE_POSITION_COLOCATED,
+                                                    1)); // bit(1) gainmap_chroma_is_vertically_centered;
+            }
+
+            const avifBool gainmapFloatFlag = AVIF_FALSE;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, gainmapFloatFlag, 1)); // bit(1) gainmap_float_flag;
+            if (gainmapFloatFlag) {
+                // bit(2) gainmap_bit_depth_log2_minus4;
+                AVIF_ASSERT_OR_RETURN(AVIF_FALSE);
+            } else {
+                AVIF_CHECKRES(avifRWStreamWriteBits(s, gainmap->depth > 8, 1)); // bit(1) gainmap_high_bit_depth_flag;
+                if (gainmap->depth > 8) {
+                    AVIF_CHECKRES(avifRWStreamWriteBits(s, gainmap->depth - 9, 3)); // bit(3) gainmap_bit_depth_minus9;
+                }
+            }
+
+            tmapIccSize = encoder->data->altImageMetadata->icc.size;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, tmapIccSize != 0, 1)); // bit(1) tmap_icc_flag;
+            const avifBool tmapHasExplicitCicp = tmap->colorPrimaries != AVIF_COLOR_PRIMARIES_BT709 ||
+                                                 tmap->transferCharacteristics != AVIF_TRANSFER_CHARACTERISTICS_SRGB ||
+                                                 tmap->matrixCoefficients != AVIF_MATRIX_COEFFICIENTS_BT601 ||
+                                                 tmap->yuvRange != AVIF_RANGE_FULL;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, tmapHasExplicitCicp, 1)); // bit(1) tmap_explicit_cicp_flag;
+            if (tmapHasExplicitCicp) {
+                AVIF_CHECKRES(avifRWStreamWriteBits(s, tmap->colorPrimaries, 8));          // bit(8) tmap_colour_primaries;
+                AVIF_CHECKRES(avifRWStreamWriteBits(s, tmap->transferCharacteristics, 8)); // bit(8) tmap_transfer_characteristics;
+                AVIF_CHECKRES(avifRWStreamWriteBits(s, tmap->matrixCoefficients, 8));      // bit(8) tmap_matrix_coefficients;
+                AVIF_CHECKRES(avifRWStreamWriteBits(s, tmap->yuvRange == AVIF_RANGE_FULL, 1)); // bit(8) tmap_full_range_flag;
+            }
+            // gainmap->icc is ignored.
+        }
+
+        AVIF_CHECKRES(avifEncoderWriteMiniHDRProperties(s, image));
+        if (hasGainmap) {
+            AVIF_CHECKRES(avifEncoderWriteMiniHDRProperties(s, encoder->data->altImageMetadata));
+        }
+#endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP
     }
 
     // Chunk sizes
@@ -2638,15 +2755,21 @@
     if (hasIcc) {
         AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)image->icc.size - 1, fewMetadataBytesFlag ? 10 : 20)); // unsigned int(few_metadata_bytes_flag ? 10 : 20) icc_data_size_minus1;
     }
-    // if (hdr_flag && gainmap_flag && tmap_icc_flag)
-    //     unsigned int(few_metadata_bytes_flag ? 10 : 20) tmap_icc_data_size_minus1;
+#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+    if (hasHdr && hasGainmap && tmapIccSize != 0) {
+        AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)tmapIccSize - 1, fewMetadataBytesFlag ? 10 : 20)); // unsigned int(few_metadata_bytes_flag ? 10 : 20) tmap_icc_data_size_minus1;
+    }
 
-    // if (hdr_flag && gainmap_flag)
-    //     unsigned int(few_metadata_bytes_flag ? 10 : 20) gainmap_metadata_size;
-    // if (hdr_flag && gainmap_flag)
-    //     unsigned int(few_item_data_bytes_flag ? 15 : 28) gainmap_item_data_size;
-    // if (hdr_flag && gainmap_flag && gainmap_item_data_size > 0)
-    //     unsigned int(few_codec_config_bytes_flag ? 3 : 12) gainmap_item_codec_config_size;
+    if (hasHdr && hasGainmap) {
+        AVIF_CHECKRES(avifRWStreamWriteBits(s, gainmapMetadataSize, fewMetadataBytesFlag ? 10 : 20)); // unsigned int(few_metadata_bytes_flag ? 10 : 20) gainmap_metadata_size;
+    }
+    if (hasHdr && hasGainmap) {
+        AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)gainmapData->size, fewItemDataBytesFlag ? 15 : 28)); // unsigned int(few_item_data_bytes_flag ? 15 : 28) gainmap_item_data_size;
+    }
+    if (hasHdr && hasGainmap && gainmapData->size != 0) {
+        AVIF_CHECKRES(avifRWStreamWriteBits(s, codecConfigSize, fewCodecConfigBytesFlag ? 3 : 12)); // unsigned int(few_codec_config_bytes_flag ? 3 : 12) gainmap_item_codec_config_size;
+    }
+#endif
 
     AVIF_CHECKRES(avifRWStreamWriteBits(s, codecConfigSize, fewCodecConfigBytesFlag ? 3 : 12)); // unsigned int(few_codec_config_bytes_flag ? 3 : 12) main_item_codec_config_size;
     AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)colorData->size - 1, fewItemDataBytesFlag ? 15 : 28)); // unsigned int(few_item_data_bytes_flag ? 15 : 28) main_item_data_size_minus1;
@@ -2669,14 +2792,15 @@
     if (s->numUsedBitsInPartialByte != 0) {
         AVIF_CHECKRES(avifRWStreamWriteBits(s, 0, 8 - s->numUsedBitsInPartialByte));
     }
-    const size_t headerSize = avifRWStreamOffset(s);
+    const size_t headerBytes = avifRWStreamOffset(s);
 
     // Chunks
     if (hasAlpha && alphaData->size != 0 && codecConfigSize != 0) {
         AVIF_CHECKRES(writeCodecConfig(s, &alphaItem->av1C)); // unsigned int(8) alpha_item_codec_config[alpha_item_codec_config_size];
     }
-    // if (hdr_flag && gainmap_flag && gainmap_item_codec_config_size > 0)
-    //     unsigned int(8) gainmap_item_codec_config[gainmap_item_codec_config_size];
+    if (hasHdr && hasGainmap && codecConfigSize != 0) {
+        AVIF_CHECKRES(writeCodecConfig(s, &gainmapItem->av1C)); // unsigned int(8) gainmap_item_codec_config[gainmap_item_codec_config_size];
+    }
     if (codecConfigSize > 0) {
         AVIF_CHECKRES(writeCodecConfig(s, &colorItem->av1C)); // unsigned int(8) main_item_codec_config[main_item_codec_config_size];
     }
@@ -2684,16 +2808,21 @@
     if (hasIcc) {
         AVIF_CHECKRES(avifRWStreamWrite(s, image->icc.data, image->icc.size)); // unsigned int(8) icc_data[icc_data_size_minus1 + 1];
     }
-    // if (hdr_flag && gainmap_flag && tmap_icc_flag)
-    //     unsigned int(8) tmap_icc_data[tmap_icc_data_size_minus1 + 1];
-    // if (hdr_flag && gainmap_flag && gainmap_metadata_size > 0)
-    //     unsigned int(8) gainmap_metadata[gainmap_metadata_size];
+#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+    if (hasHdr && hasGainmap && tmapIccSize != 0) {
+        AVIF_CHECKRES(avifRWStreamWrite(s, encoder->data->altImageMetadata->icc.data, tmapIccSize)); // unsigned int(8) tmap_icc_data[tmap_icc_data_size_minus1 + 1];
+    }
+    if (hasHdr && hasGainmap && gainmapMetadataSize != 0) {
+        AVIF_CHECKRES(avifWriteGainmapMetadata(s, image->gainMap, &encoder->diag)); // unsigned int(8) gainmap_metadata[gainmap_metadata_size];
+    }
+#endif
 
     if (hasAlpha && alphaData->size != 0) {
         AVIF_CHECKRES(avifRWStreamWrite(s, alphaData->data, alphaData->size)); // unsigned int(8) alpha_item_data[alpha_item_data_size];
     }
-    // if (hdr_flag && gainmap_flag && gainmap_item_data_size > 0)
-    //     unsigned int(8) gainmap_item_data[gainmap_item_data_size];
+    if (hasHdr && hasGainmap && gainmapData->size != 0) {
+        AVIF_CHECKRES(avifRWStreamWrite(s, gainmapData->data, gainmapData->size)); // unsigned int(8) gainmap_item_data[gainmap_item_data_size];
+    }
 
     AVIF_CHECKRES(avifRWStreamWrite(s, colorData->data, colorData->size)); // unsigned int(8) main_item_data[main_item_data_size_minus1 + 1];
 
@@ -2704,9 +2833,11 @@
         AVIF_CHECKRES(avifRWStreamWrite(s, image->xmp.data, image->xmp.size)); // unsigned int(8) xmp_data[xmp_data_size_minus1 + 1];
     }
 
-    AVIF_ASSERT_OR_RETURN(avifRWStreamOffset(s) - headerSize == (hasAlpha ? codecConfigSize : 0) + codecConfigSize +
-                                                                    image->icc.size + (hasAlpha ? alphaData->size : 0) +
-                                                                    colorData->size + image->exif.size + image->xmp.size);
+    const size_t expectedChunkBytes = (hasAlpha ? codecConfigSize : 0) + (hasGainmap ? codecConfigSize : 0) + codecConfigSize +
+                                      image->icc.size + (hasGainmap ? tmapIccSize : 0) + (hasGainmap ? gainmapMetadataSize : 0) +
+                                      (hasAlpha ? alphaData->size : 0) + (hasGainmap ? gainmapData->size : 0) + colorData->size +
+                                      image->exif.size + image->xmp.size;
+    AVIF_ASSERT_OR_RETURN(avifRWStreamOffset(s) == headerBytes + expectedChunkBytes);
     avifRWStreamFinishBox(s, mini);
     return AVIF_RESULT_OK;
 }
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index c77b329..702739c 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -131,7 +131,7 @@
     add_avif_gtest_with_data(aviflosslesstest)
     add_avif_gtest_with_data(avifmetadatatest)
 
-    if(AVIF_ENABLE_EXPERIMENTAL_MINI)
+    if(AVIF_ENABLE_EXPERIMENTAL_MINI AND AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
         add_avif_gtest(avifminitest)
     endif()
 
@@ -368,7 +368,7 @@
                 PROPERTIES DISABLED True
             )
 
-            if(AVIF_ENABLE_EXPERIMENTAL_MINI)
+            if(AVIF_ENABLE_EXPERIMENTAL_MINI AND AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
                 set_tests_properties(avifminitest PROPERTIES DISABLED True)
             endif()
             if(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
diff --git a/tests/gtest/avifminitest.cc b/tests/gtest/avifminitest.cc
index 1bddea3..4d9f3fe 100644
--- a/tests/gtest/avifminitest.cc
+++ b/tests/gtest/avifminitest.cc
@@ -17,9 +17,9 @@
     : public testing::TestWithParam<std::tuple<
           /*width=*/int, /*height=*/int, /*depth=*/int, avifPixelFormat,
           avifPlanesFlags, avifRange, /*create_icc=*/bool, /*create_exif=*/bool,
-          /*create_xmp=*/bool, avifTransformFlags>> {};
+          /*create_xmp=*/bool, avifTransformFlags, /*create_hdr=*/bool>> {};
 
-TEST_P(AvifMinimizedImageBoxTest, SimpleOpaque) {
+TEST_P(AvifMinimizedImageBoxTest, All) {
   const int width = std::get<0>(GetParam());
   const int height = std::get<1>(GetParam());
   const int depth = std::get<2>(GetParam());
@@ -30,6 +30,7 @@
   const bool create_exif = std::get<7>(GetParam());
   const bool create_xmp = std::get<8>(GetParam());
   const avifTransformFlags create_transform_flags = std::get<9>(GetParam());
+  const bool create_hdr = std::get<10>(GetParam());
 
   ImagePtr image =
       testutil::CreateImage(width, height, depth, format, planes, range);
@@ -64,6 +65,17 @@
   if (create_transform_flags & AVIF_TRANSFORM_IMIR) {
     image->imir.axis = 0;
   }
+  if (create_hdr) {
+    image->gainMap = avifGainMapCreate();
+    ASSERT_NE(image->gainMap, nullptr);
+    image->gainMap->image =
+        testutil::CreateImage(width, height, /*depth=*/8,
+                              AVIF_PIXEL_FORMAT_YUV400, AVIF_PLANES_YUV,
+                              AVIF_RANGE_FULL)
+            .release();
+    ASSERT_NE(image->gainMap->image, nullptr);
+    testutil::FillImageGradient(image->gainMap->image);
+  }
 
   // Encode.
   testutil::AvifRwData encoded_mini;
@@ -75,9 +87,15 @@
             AVIF_RESULT_OK);
 
   // Decode.
-  const ImagePtr decoded_mini =
-      testutil::Decode(encoded_mini.data, encoded_mini.size);
+  ImagePtr decoded_mini(avifImageCreateEmpty());
   ASSERT_NE(decoded_mini, nullptr);
+  DecoderPtr decoder_mini(avifDecoderCreate());
+  ASSERT_NE(decoder_mini, nullptr);
+  decoder_mini->enableParsingGainMapMetadata = AVIF_TRUE;
+  decoder_mini->enableDecodingGainMap = AVIF_TRUE;
+  ASSERT_EQ(avifDecoderReadMemory(decoder_mini.get(), decoded_mini.get(),
+                                  encoded_mini.data, encoded_mini.size),
+            AVIF_RESULT_OK);
 
   // Compare.
   testutil::AvifRwData encoded_meta =
@@ -86,14 +104,30 @@
   // At least 200 bytes should be saved.
   EXPECT_LT(encoded_mini.size, encoded_meta.size - 200);
 
-  const ImagePtr decoded_meta =
-      testutil::Decode(encoded_meta.data, encoded_meta.size);
+  ImagePtr decoded_meta(avifImageCreateEmpty());
   ASSERT_NE(decoded_meta, nullptr);
+  DecoderPtr decoder_meta(avifDecoderCreate());
+  ASSERT_NE(decoder_meta, nullptr);
+  decoder_meta->enableParsingGainMapMetadata = AVIF_TRUE;
+  decoder_meta->enableDecodingGainMap = AVIF_TRUE;
+  ASSERT_EQ(avifDecoderReadMemory(decoder_meta.get(), decoded_meta.get(),
+                                  encoded_meta.data, encoded_meta.size),
+            AVIF_RESULT_OK);
+  EXPECT_EQ(decoder_meta->gainMapPresent, decoder_mini->gainMapPresent);
 
   // Only the container changed. The pixels, features and metadata should be
   // identical.
   EXPECT_TRUE(
       testutil::AreImagesEqual(*decoded_meta.get(), *decoded_mini.get()));
+  EXPECT_EQ(decoded_meta->gainMap != nullptr, decoded_mini->gainMap != nullptr);
+  if (create_hdr) {
+    ASSERT_NE(decoded_meta->gainMap, nullptr);
+    ASSERT_NE(decoded_mini->gainMap, nullptr);
+    ASSERT_NE(decoded_meta->gainMap->image, nullptr);
+    ASSERT_NE(decoded_mini->gainMap->image, nullptr);
+    EXPECT_TRUE(testutil::AreImagesEqual(*decoded_meta->gainMap->image,
+                                         *decoded_mini->gainMap->image));
+  }
 }
 
 INSTANTIATE_TEST_SUITE_P(OnePixel, AvifMinimizedImageBoxTest,
@@ -105,7 +139,8 @@
                                  /*create_icc=*/Values(false, true),
                                  /*create_exif=*/Values(false, true),
                                  /*create_xmp=*/Values(false, true),
-                                 Values(AVIF_TRANSFORM_NONE)));
+                                 Values(AVIF_TRANSFORM_NONE),
+                                 /*create_hdr=*/Values(false)));
 
 INSTANTIATE_TEST_SUITE_P(
     DepthsSubsamplings, AvifMinimizedImageBoxTest,
@@ -115,7 +150,8 @@
                    AVIF_PIXEL_FORMAT_YUV420, AVIF_PIXEL_FORMAT_YUV400),
             Values(AVIF_PLANES_ALL), Values(AVIF_RANGE_FULL),
             /*create_icc=*/Values(false), /*create_exif=*/Values(false),
-            /*create_xmp=*/Values(false), Values(AVIF_TRANSFORM_NONE)));
+            /*create_xmp=*/Values(false), Values(AVIF_TRANSFORM_NONE),
+            /*create_hdr=*/Values(false)));
 
 INSTANTIATE_TEST_SUITE_P(
     Dimensions, AvifMinimizedImageBoxTest,
@@ -123,7 +159,7 @@
             Values(AVIF_PIXEL_FORMAT_YUV444), Values(AVIF_PLANES_ALL),
             Values(AVIF_RANGE_FULL), /*create_icc=*/Values(true),
             /*create_exif=*/Values(true), /*create_xmp=*/Values(true),
-            Values(AVIF_TRANSFORM_NONE)));
+            Values(AVIF_TRANSFORM_NONE), /*create_hdr=*/Values(false)));
 
 INSTANTIATE_TEST_SUITE_P(
     Orientation, AvifMinimizedImageBoxTest,
@@ -133,7 +169,17 @@
             /*create_exif=*/Values(true), /*create_xmp=*/Values(true),
             Values(AVIF_TRANSFORM_NONE, AVIF_TRANSFORM_IROT,
                    AVIF_TRANSFORM_IMIR,
-                   AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR)));
+                   AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR),
+            /*create_hdr=*/Values(false)));
+
+INSTANTIATE_TEST_SUITE_P(
+    Hdr, AvifMinimizedImageBoxTest,
+    Combine(/*width=*/Values(8), /*height=*/Values(10), /*depth=*/Values(10),
+            Values(AVIF_PIXEL_FORMAT_YUV420),
+            Values(AVIF_PLANES_YUV, AVIF_PLANES_ALL), Values(AVIF_RANGE_FULL),
+            /*create_icc=*/Values(false),
+            /*create_exif=*/Values(false), /*create_xmp=*/Values(false),
+            Values(AVIF_TRANSFORM_NONE), /*create_hdr=*/Values(true)));
 
 //------------------------------------------------------------------------------