Refactor gain map API (#2444)

Remove avifGainMapMetadata and avifGainMapMetadataDouble.
These structs were not extensible, i.e. new fields could not be added without
breaking ABI backward compatibility.
Instead, the fields of avifGainMapMetadata are added to avifGainMap directly
(which is an extensible struct since it uses Create()/Destroy() functions).
Expose avifDoubleTo(Un)SignedFraction to replace avifGainMapMetadataDoubleToFractions.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9101280..3263608 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,10 @@
 * Write an empty HandlerBox name field instead of "libavif" (saves 7 bytes).
 * Update aom.cmd/LocalAom.cmake: v3.10.0
 * Update svt.cmd/svt.sh/LocalSvt.cmake: v2.2.1
+* Change experimental gainmap API: remove avifGainMapMetadata and
+  avifGainMapMetadataDouble structs.
+* Add avif(Un)SignedFraction structs and avifDoubleTo(Un)SignedFraction
+  utility functions.
 
 ## [1.1.1] - 2024-07-30
 
diff --git a/apps/avifgainmaputil/convert_command.cc b/apps/avifgainmaputil/convert_command.cc
index ecb617d..47f149c 100644
--- a/apps/avifgainmaputil/convert_command.cc
+++ b/apps/avifgainmaputil/convert_command.cc
@@ -88,7 +88,7 @@
   if (arg_swap_base_) {
     int depth = arg_image_read_.depth;
     if (depth == 0) {
-      depth = image->gainMap->metadata.alternateHdrHeadroomN == 0 ? 8 : 10;
+      depth = image->gainMap->alternateHdrHeadroom.n == 0 ? 8 : 10;
     }
     ImagePtr new_base(avifImageCreateEmpty());
     if (new_base == nullptr) {
diff --git a/apps/avifgainmaputil/printmetadata_command.cc b/apps/avifgainmaputil/printmetadata_command.cc
index 010d566..782469a 100644
--- a/apps/avifgainmaputil/printmetadata_command.cc
+++ b/apps/avifgainmaputil/printmetadata_command.cc
@@ -12,23 +12,20 @@
 
 namespace {
 template <typename T>
-std::string FormatFraction(T numerator, uint32_t denominator) {
+std::string FormatFraction(T fraction) {
   std::stringstream stream;
-  stream << (denominator != 0 ? (double)numerator / denominator : 0)
-         << " (as fraction: " << numerator << "/" << denominator << ")";
+  stream << (fraction->d != 0 ? (double)fraction->n / fraction->d : 0)
+         << " (as fraction: " << fraction->n << "/" << fraction->d << ")";
   return stream.str();
 }
 
 template <typename T>
-std::string FormatFractions(const T numerator[3],
-                            const uint32_t denominator[3]) {
+std::string FormatFractions(const T fractions[3]) {
   std::stringstream stream;
   const int w = 40;
-  stream << "R " << std::left << std::setw(w)
-         << FormatFraction(numerator[0], denominator[0]) << " G " << std::left
-         << std::setw(w) << FormatFraction(numerator[1], denominator[1])
-         << " B " << std::left << std::setw(w)
-         << FormatFraction(numerator[2], denominator[2]);
+  stream << "R " << std::left << std::setw(w) << FormatFraction(fractions)
+         << " G " << std::left << std::setw(w) << FormatFraction(fractions)
+         << " B " << std::left << std::setw(w) << FormatFraction(fractions);
   return stream.str();
 }
 }  // namespace
@@ -65,34 +62,27 @@
   }
   assert(decoder->image->gainMap);
 
-  const avifGainMapMetadata& metadata = decoder->image->gainMap->metadata;
+  const avifGainMap& gainMap = *decoder->image->gainMap;
   const int w = 20;
-  std::cout << " * " << std::left << std::setw(w) << "Base headroom: "
-            << FormatFraction(metadata.baseHdrHeadroomN,
-                              metadata.baseHdrHeadroomD)
+  std::cout << " * " << std::left << std::setw(w)
+            << "Base headroom: " << FormatFraction(&gainMap.baseHdrHeadroom)
             << "\n";
   std::cout << " * " << std::left << std::setw(w) << "Alternate headroom: "
-            << FormatFraction(metadata.alternateHdrHeadroomN,
-                              metadata.alternateHdrHeadroomD)
+            << FormatFraction(&gainMap.alternateHdrHeadroom) << "\n";
+  std::cout << " * " << std::left << std::setw(w)
+            << "Gain Map Min: " << FormatFractions(gainMap.gainMapMin) << "\n";
+  std::cout << " * " << std::left << std::setw(w)
+            << "Gain Map Max: " << FormatFractions(gainMap.gainMapMax) << "\n";
+  std::cout << " * " << std::left << std::setw(w)
+            << "Base Offset: " << FormatFractions(gainMap.baseOffset) << "\n";
+  std::cout << " * " << std::left << std::setw(w)
+            << "Alternate Offset: " << FormatFractions(gainMap.alternateOffset)
             << "\n";
-  std::cout << " * " << std::left << std::setw(w) << "Gain Map Min: "
-            << FormatFractions(metadata.gainMapMinN, metadata.gainMapMinD)
-            << "\n";
-  std::cout << " * " << std::left << std::setw(w) << "Gain Map Max: "
-            << FormatFractions(metadata.gainMapMaxN, metadata.gainMapMaxD)
-            << "\n";
-  std::cout << " * " << std::left << std::setw(w) << "Base Offset: "
-            << FormatFractions(metadata.baseOffsetN, metadata.baseOffsetD)
-            << "\n";
-  std::cout << " * " << std::left << std::setw(w) << "Alternate Offset: "
-            << FormatFractions(metadata.alternateOffsetN,
-                               metadata.alternateOffsetD)
-            << "\n";
-  std::cout << " * " << std::left << std::setw(w) << "Gain Map Gamma: "
-            << FormatFractions(metadata.gainMapGammaN, metadata.gainMapGammaD)
+  std::cout << " * " << std::left << std::setw(w)
+            << "Gain Map Gamma: " << FormatFractions(gainMap.gainMapGamma)
             << "\n";
   std::cout << " * " << std::left << std::setw(w) << "Use Base Color Space: "
-            << (metadata.useBaseColorSpace ? "True" : "False") << "\n";
+            << (gainMap.useBaseColorSpace ? "True" : "False") << "\n";
 
   return AVIF_RESULT_OK;
 }
diff --git a/apps/avifgainmaputil/swapbase_command.cc b/apps/avifgainmaputil/swapbase_command.cc
index 8cb81b3..3b504ae 100644
--- a/apps/avifgainmaputil/swapbase_command.cc
+++ b/apps/avifgainmaputil/swapbase_command.cc
@@ -22,9 +22,12 @@
   swapped->depth = depth;
   swapped->yuvFormat = yuvFormat;
 
+  if (image.gainMap->alternateHdrHeadroom.d == 0) {
+    return AVIF_RESULT_INVALID_ARGUMENT;
+  }
   const float headroom =
-      static_cast<float>(image.gainMap->metadata.alternateHdrHeadroomN) /
-      image.gainMap->metadata.alternateHdrHeadroomD;
+      static_cast<float>(image.gainMap->alternateHdrHeadroom.n) /
+      image.gainMap->alternateHdrHeadroom.d;
   const bool tone_mapping_to_sdr = (headroom == 0.0f);
 
   swapped->colorPrimaries = image.gainMap->altColorPrimaries;
@@ -97,14 +100,12 @@
       (image.yuvFormat == AVIF_PIXEL_FORMAT_YUV400) ? 1 : 3;
   swapped->gainMap->altCLLI = image.clli;
 
-  // Swap base and alternate in the gain map metadata.
-  avifGainMapMetadata& metadata = swapped->gainMap->metadata;
-  metadata.useBaseColorSpace = !metadata.useBaseColorSpace;
-  std::swap(metadata.baseHdrHeadroomN, metadata.alternateHdrHeadroomN);
-  std::swap(metadata.baseHdrHeadroomD, metadata.alternateHdrHeadroomD);
+  // Swap base and alternate in the gain map
+  avifGainMap* gainMap = swapped->gainMap;
+  gainMap->useBaseColorSpace = !gainMap->useBaseColorSpace;
+  std::swap(gainMap->baseHdrHeadroom, gainMap->alternateHdrHeadroom);
   for (int c = 0; c < 3; ++c) {
-    std::swap(metadata.baseOffsetN, metadata.alternateOffsetN);
-    std::swap(metadata.baseOffsetD, metadata.alternateOffsetD);
+    std::swap(gainMap->baseOffset, gainMap->alternateOffset);
   }
 
   return AVIF_RESULT_OK;
diff --git a/apps/avifgainmaputil/tonemap_command.cc b/apps/avifgainmaputil/tonemap_command.cc
index 371b91c..d0c48e0 100644
--- a/apps/avifgainmaputil/tonemap_command.cc
+++ b/apps/avifgainmaputil/tonemap_command.cc
@@ -85,29 +85,31 @@
               << " does not contain a gain map\n";
     return AVIF_RESULT_INVALID_ARGUMENT;
   }
-
-  avifGainMapMetadataDouble metadata;
-  if (!avifGainMapMetadataFractionsToDouble(&metadata,
-                                            &image->gainMap->metadata)) {
-    std::cerr << "Input image " << arg_input_filename_
-              << " has invalid gain map metadata\n";
+  if (image->gainMap->baseHdrHeadroom.d == 0 ||
+      image->gainMap->alternateHdrHeadroom.d == 0) {
     return AVIF_RESULT_INVALID_ARGUMENT;
   }
 
+  const float base_hdr_hreadroom =
+      static_cast<float>(image->gainMap->baseHdrHeadroom.n) /
+      image->gainMap->baseHdrHeadroom.d;
+  const float alternate_hdr_hreadroom =
+      static_cast<float>(image->gainMap->alternateHdrHeadroom.n /
+                         image->gainMap->alternateHdrHeadroom.d);
   // We are either tone mapping to the base image (i.e. leaving it as is),
   // or tone mapping to the alternate image (i.e. fully applying the gain map),
   // or tone mapping in between (partially applying the gain map).
   const bool tone_mapping_to_base =
-      (headroom <= metadata.baseHdrHeadroom &&
-       metadata.baseHdrHeadroom <= metadata.alternateHdrHeadroom) ||
-      (headroom >= metadata.baseHdrHeadroom &&
-       metadata.baseHdrHeadroom >= metadata.alternateHdrHeadroom);
+      (headroom <= base_hdr_hreadroom &&
+       base_hdr_hreadroom <= alternate_hdr_hreadroom) ||
+      (headroom >= base_hdr_hreadroom &&
+       base_hdr_hreadroom >= alternate_hdr_hreadroom);
   const bool tone_mapping_to_alternate =
-      (headroom <= metadata.alternateHdrHeadroom &&
-       metadata.alternateHdrHeadroom <= metadata.baseHdrHeadroom) ||
-      (headroom >= metadata.alternateHdrHeadroom &&
-       metadata.alternateHdrHeadroom >= metadata.baseHdrHeadroom);
-  const bool base_is_hdr = (metadata.baseHdrHeadroom != 0.0f);
+      (headroom <= alternate_hdr_hreadroom &&
+       alternate_hdr_hreadroom <= base_hdr_hreadroom) ||
+      (headroom >= alternate_hdr_hreadroom &&
+       alternate_hdr_hreadroom >= base_hdr_hreadroom);
+  const bool base_is_hdr = (base_hdr_hreadroom != 0.0f);
 
   // Determine output CICP.
   CicpValues cicp;
diff --git a/apps/shared/avifjpeg.c b/apps/shared/avifjpeg.c
index d48beaa..43b008c 100644
--- a/apps/shared/avifjpeg.c
+++ b/apps/shared/avifjpeg.c
@@ -592,53 +592,47 @@
 // Parses gain map metadata from XMP.
 // See https://helpx.adobe.com/camera-raw/using/gain-map.html
 // Returns AVIF_TRUE if the gain map metadata was successfully read.
-static avifBool avifJPEGParseGainMapXMPProperties(const xmlNode * rootNode, avifGainMapMetadata * metadata)
+static avifBool avifJPEGParseGainMapXMPProperties(const xmlNode * rootNode, avifGainMap * gainMap)
 {
     const xmlNode * descNode = avifJPEGFindGainMapXMPNode(rootNode);
     if (descNode == NULL) {
         return AVIF_FALSE;
     }
 
-    avifGainMapMetadataDouble metadataDouble;
     // Set default values from Adobe's spec.
-    metadataDouble.baseHdrHeadroom = 0.0;
-    metadataDouble.alternateHdrHeadroom = 1.0;
-    for (int i = 0; i < 3; ++i) {
-        metadataDouble.gainMapMin[i] = 0.0;
-        metadataDouble.gainMapMax[i] = 1.0;
-        metadataDouble.baseOffset[i] = 1.0 / 64.0;
-        metadataDouble.alternateOffset[i] = 1.0 / 64.0;
-        metadataDouble.gainMapGamma[i] = 1.0;
-    }
-    // Not in Adobe's spec but both color spaces should be the same so this value doesn't matter.
-    metadataDouble.useBaseColorSpace = AVIF_TRUE;
-
-    AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "HDRCapacityMin", &metadataDouble.baseHdrHeadroom, /*numDoubles=*/1));
-    AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "HDRCapacityMax", &metadataDouble.alternateHdrHeadroom, /*numDoubles=*/1));
-    AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "OffsetSDR", metadataDouble.baseOffset, /*numDoubles=*/3));
-    AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "OffsetHDR", metadataDouble.alternateOffset, /*numDoubles=*/3));
-    AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "GainMapMin", metadataDouble.gainMapMin, /*numDoubles=*/3));
-    AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "GainMapMax", metadataDouble.gainMapMax, /*numDoubles=*/3));
-    AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "Gamma", metadataDouble.gainMapGamma, /*numDoubles=*/3));
+    double baseHdrHeadroom = 0.0;
+    double alternateHdrHeadroom = 1.0;
+    double gainMapMin[3] = { 0.0, 0.0, 0.0 };
+    double gainMapMax[3] = { 1.0, 1.0, 1.0 };
+    double gainMapGamma[3] = { 1.0, 1.0, 1.0 };
+    double baseOffset[3] = { 1.0 / 64.0, 1.0 / 64.0, 1.0 / 64.0 };
+    double alternateOffset[3] = { 1.0 / 64.0, 1.0 / 64.0, 1.0 / 64.0 };
+    AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "HDRCapacityMin", &baseHdrHeadroom, /*numDoubles=*/1));
+    AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "HDRCapacityMax", &alternateHdrHeadroom, /*numDoubles=*/1));
+    AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "OffsetSDR", baseOffset, /*numDoubles=*/3));
+    AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "OffsetHDR", alternateOffset, /*numDoubles=*/3));
+    AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "GainMapMin", gainMapMin, /*numDoubles=*/3));
+    AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "GainMapMax", gainMapMax, /*numDoubles=*/3));
+    AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "Gamma", gainMapGamma, /*numDoubles=*/3));
 
     // See inequality requirements in section 'XMP Representation of Gain Map Metadata' of Adobe's gain map specification
     // https://helpx.adobe.com/camera-raw/using/gain-map.html
-    AVIF_CHECK(metadataDouble.alternateHdrHeadroom > metadataDouble.baseHdrHeadroom);
-    AVIF_CHECK(metadataDouble.baseHdrHeadroom >= 0);
+    AVIF_CHECK(alternateHdrHeadroom > baseHdrHeadroom);
+    AVIF_CHECK(baseHdrHeadroom >= 0);
     for (int i = 0; i < 3; ++i) {
-        AVIF_CHECK(metadataDouble.gainMapMax[i] >= metadataDouble.gainMapMin[i]);
-        AVIF_CHECK(metadataDouble.baseOffset[i] >= 0.0);
-        AVIF_CHECK(metadataDouble.alternateOffset[i] >= 0.0);
-        AVIF_CHECK(metadataDouble.gainMapGamma[i] > 0.0);
+        AVIF_CHECK(gainMapMax[i] >= gainMapMin[i]);
+        AVIF_CHECK(baseOffset[i] >= 0.0);
+        AVIF_CHECK(alternateOffset[i] >= 0.0);
+        AVIF_CHECK(gainMapGamma[i] > 0.0);
     }
 
     uint32_t numValues;
     const char * baseRenditionIsHDR;
     if (avifJPEGFindGainMapProperty(descNode, "BaseRenditionIsHDR", /*maxValues=*/1, &baseRenditionIsHDR, &numValues)) {
         if (!strcmp(baseRenditionIsHDR, "True")) {
-            SwapDoubles(&metadataDouble.baseHdrHeadroom, &metadataDouble.alternateHdrHeadroom);
+            SwapDoubles(&baseHdrHeadroom, &alternateHdrHeadroom);
             for (int c = 0; c < 3; ++c) {
-                SwapDoubles(&metadataDouble.baseOffset[c], &metadataDouble.alternateOffset[c]);
+                SwapDoubles(&baseOffset[c], &alternateOffset[c]);
             }
         } else if (!strcmp(baseRenditionIsHDR, "False")) {
         } else {
@@ -646,21 +640,31 @@
         }
     }
 
-    AVIF_CHECK(avifGainMapMetadataDoubleToFractions(metadata, &metadataDouble));
+    for (int i = 0; i < 3; ++i) {
+        AVIF_CHECK(avifDoubleToSignedFraction(gainMapMin[i], &gainMap->gainMapMin[i]));
+        AVIF_CHECK(avifDoubleToSignedFraction(gainMapMax[i], &gainMap->gainMapMax[i]));
+        AVIF_CHECK(avifDoubleToUnsignedFraction(gainMapGamma[i], &gainMap->gainMapGamma[i]));
+        AVIF_CHECK(avifDoubleToSignedFraction(baseOffset[i], &gainMap->baseOffset[i]));
+        AVIF_CHECK(avifDoubleToSignedFraction(alternateOffset[i], &gainMap->alternateOffset[i]));
+    }
+    AVIF_CHECK(avifDoubleToUnsignedFraction(baseHdrHeadroom, &gainMap->baseHdrHeadroom));
+    AVIF_CHECK(avifDoubleToUnsignedFraction(alternateHdrHeadroom, &gainMap->alternateHdrHeadroom));
+    // Not in Adobe's spec but both color spaces should be the same so this value doesn't matter.
+    gainMap->useBaseColorSpace = AVIF_TRUE;
 
     return AVIF_TRUE;
 }
 
 // Parses gain map metadata from an XMP payload.
 // Returns AVIF_TRUE if the gain map metadata was successfully read.
-avifBool avifJPEGParseGainMapXMP(const uint8_t * xmpData, size_t xmpSize, avifGainMapMetadata * metadata)
+avifBool avifJPEGParseGainMapXMP(const uint8_t * xmpData, size_t xmpSize, avifGainMap * gainMap)
 {
     xmlDoc * document = xmlReadMemory((const char *)xmpData, (int)xmpSize, NULL, NULL, LIBXML2_XML_PARSING_FLAGS);
     if (document == NULL) {
         return AVIF_FALSE; // Probably an out of memory error.
     }
     xmlNode * rootNode = xmlDocGetRootElement(document);
-    const avifBool res = avifJPEGParseGainMapXMPProperties(rootNode, metadata);
+    const avifBool res = avifJPEGParseGainMapXMPProperties(rootNode, gainMap);
     xmlFreeDoc(document);
     return res;
 }
@@ -817,7 +821,7 @@
                 avifImageDestroy(image);
                 return AVIF_FALSE;
             }
-            if (!avifJPEGParseGainMapXMP(image->xmp.data, image->xmp.size, &gainMap->metadata)) {
+            if (!avifJPEGParseGainMapXMP(image->xmp.data, image->xmp.size, gainMap)) {
                 fprintf(stderr, "Warning: failed to parse gain map metadata\n");
                 avifImageDestroy(image);
                 return AVIF_FALSE;
diff --git a/apps/shared/avifjpeg.h b/apps/shared/avifjpeg.h
index 16032bc..ad2a3f0 100644
--- a/apps/shared/avifjpeg.h
+++ b/apps/shared/avifjpeg.h
@@ -31,7 +31,7 @@
 
 #if defined(AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION)
 // Parses XMP gain map metadata. Visible for testing.
-avifBool avifJPEGParseGainMapXMP(const uint8_t * xmpData, size_t xmpSize, avifGainMapMetadata * metadata);
+avifBool avifJPEGParseGainMapXMP(const uint8_t * xmpData, size_t xmpSize, avifGainMap * gainMap);
 #endif
 
 #ifdef __cplusplus
diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c
index 72edbca..8006ea4 100644
--- a/apps/shared/avifutil.c
+++ b/apps/shared/avifutil.c
@@ -141,7 +141,7 @@
                avifPixelFormatToString(gainMapImage->yuvFormat),
                (gainMapImage->yuvRange == AVIF_RANGE_FULL) ? "Full" : "Limited",
                gainMapImage->matrixCoefficients,
-               (avif->gainMap->metadata.baseHdrHeadroomN == 0) ? "SDR" : "HDR");
+               (avif->gainMap->baseHdrHeadroom.n == 0) ? "SDR" : "HDR");
         printf(" * Alternate image:\n");
         printf("    * Color Primaries: %u\n", avif->gainMap->altColorPrimaries);
         printf("    * Transfer Char. : %u\n", avif->gainMap->altTransferCharacteristics);
diff --git a/include/avif/avif.h b/include/avif/avif.h
index d9105bd..f5ee805 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -426,7 +426,7 @@
 AVIF_API void avifDiagnosticsClearError(avifDiagnostics * diag);
 
 // ---------------------------------------------------------------------------
-// Fraction utility
+// Fraction utilities
 
 typedef struct avifFraction
 {
@@ -434,6 +434,25 @@
     int32_t d;
 } avifFraction;
 
+typedef struct avifSignedFraction
+{
+    int32_t n;
+    uint32_t d;
+} avifSignedFraction;
+
+typedef struct avifUnsignedFraction
+{
+    uint32_t n;
+    uint32_t d;
+} avifUnsignedFraction;
+
+// Creates an int32/uint32 fraction that is approximately equal to 'v'.
+// Returns AVIF_FALSE if 'v' is NaN or abs(v) is > INT32_MAX.
+AVIF_API AVIF_NODISCARD avifBool avifDoubleToSignedFraction(double v, avifSignedFraction * fraction);
+// Creates a uint32/uint32 fraction that is approximately equal to 'v'.
+// Returns AVIF_FALSE if 'v' is < 0 or > UINT32_MAX or NaN.
+AVIF_API AVIF_NODISCARD avifBool avifDoubleToUnsignedFraction(double v, avifUnsignedFraction * fraction);
+
 // ---------------------------------------------------------------------------
 // Optional transformation structs
 
@@ -591,25 +610,32 @@
 
 struct avifImage;
 
-// Gain map metadata, to apply the gain map. Fully applying the gain map to the base
-// image results in the alternate image.
-// All field pairs ending with 'N' and 'D' are fractional values (numerator and denominator).
-typedef struct avifGainMapMetadata
+// Gain map image and associated metadata.
+// Must be allocated by calling avifGainMapCreate().
+typedef struct avifGainMap
 {
+    // Gain map pixels.
+    // Owned by the avifGainMap and gets freed when calling avifGainMapDestroy().
+    // Used fields: width, height, depth, yuvFormat, yuvRange,
+    // yuvChromaSamplePosition, yuvPlanes, yuvRowBytes, imageOwnsYUVPlanes,
+    // matrixCoefficients. The colorPrimaries and transferCharacteristics fields
+    // shall be 2. Other fields are ignored.
+    struct avifImage * image;
+
+    // Gain map metadata used to interpret and apply the gain map pixel data.
+    // When encoding an image grid, all metadata below shall be identical for all
+    // cells.
+
     // Parameters for converting the gain map from its image encoding to log2 space.
     // gainMapLog2 = lerp(gainMapMin, gainMapMax, pow(gainMapEncoded, gainMapGamma));
     // where 'lerp' is a linear interpolation function.
-
     // Minimum value in the gain map, log2-encoded, per RGB channel.
-    int32_t gainMapMinN[3];
-    uint32_t gainMapMinD[3];
+    avifSignedFraction gainMapMin[3];
     // Maximum value in the gain map, log2-encoded, per RGB channel.
-    int32_t gainMapMaxN[3];
-    uint32_t gainMapMaxD[3];
+    avifSignedFraction gainMapMax[3];
     // Gain map gamma value with which the gain map was encoded, per RGB channel.
     // For decoding, the inverse value (1/gamma) should be used.
-    uint32_t gainMapGammaN[3];
-    uint32_t gainMapGammaD[3];
+    avifUnsignedFraction gainMapGamma[3];
 
     // Parameters used in gain map computation/tone mapping to avoid numerical
     // instability.
@@ -618,16 +644,9 @@
     // (see below).
 
     // Offset constants for the base image, per RGB channel.
-    int32_t baseOffsetN[3];
-    uint32_t baseOffsetD[3];
+    avifSignedFraction baseOffset[3];
     // Offset constants for the alternate image, per RGB channel.
-    int32_t alternateOffsetN[3];
-    uint32_t alternateOffsetD[3];
-
-    // -----------------------------------------------------------------------
-
-    // Parameters below can be manually tuned after the gain map has been
-    // created.
+    avifSignedFraction alternateOffset[3];
 
     // Log2-encoded HDR headroom of the base and alternate images respectively.
     // If baseHdrHeadroom is < alternateHdrHeadroom, the result of tone mapping
@@ -648,34 +667,13 @@
     // f = clamp((H - baseHdrHeadroom) /
     //           (alternateHdrHeadroom - baseHdrHeadroom), 0, 1);
     // w = sign(alternateHdrHeadroom - baseHdrHeadroom) * f
-    uint32_t baseHdrHeadroomN;
-    uint32_t baseHdrHeadroomD;
-    uint32_t alternateHdrHeadroomN;
-    uint32_t alternateHdrHeadroomD;
+    avifUnsignedFraction baseHdrHeadroom;
+    avifUnsignedFraction alternateHdrHeadroom;
 
     // True if tone mapping should be performed in the color space of the
     // base image. If false, the color space of the alternate image should
     // be used.
     avifBool useBaseColorSpace;
-} avifGainMapMetadata;
-
-// Gain map image and associated metadata.
-// Must be allocated by calling avifGainMapCreate().
-typedef struct avifGainMap
-{
-    // Gain map pixels.
-    // Owned by the avifGainMap and gets freed when calling avifGainMapDestroy().
-    // Used fields: width, height, depth, yuvFormat, yuvRange,
-    // yuvChromaSamplePosition, yuvPlanes, yuvRowBytes, imageOwnsYUVPlanes,
-    // matrixCoefficients. The colorPrimaries and transferCharacteristics fields
-    // shall be 2. Other fields are ignored.
-    struct avifImage * image;
-
-    // When encoding an image grid, all metadata below shall be identical for all
-    // cells.
-
-    // Gain map metadata used to interpret and apply the gain map pixel data.
-    avifGainMapMetadata metadata;
 
     // Colorimetry of the alternate image (ICC profile and/or CICP information
     // of the alternate image that the gain map was created from).
@@ -702,29 +700,6 @@
 // Frees a gain map, including the 'image' field if non NULL.
 AVIF_API void avifGainMapDestroy(avifGainMap * gainMap);
 
-// Same as avifGainMapMetadata, but with fields of type double instead of uint32_t fractions.
-// Use avifGainMapMetadataDoubleToFractions() to convert this to a avifGainMapMetadata.
-// See avifGainMapMetadata for detailed descriptions of fields.
-typedef struct avifGainMapMetadataDouble
-{
-    double gainMapMin[3];
-    double gainMapMax[3];
-    double gainMapGamma[3];
-    double baseOffset[3];
-    double alternateOffset[3];
-    double baseHdrHeadroom;
-    double alternateHdrHeadroom;
-    avifBool useBaseColorSpace;
-} avifGainMapMetadataDouble;
-
-// Converts a avifGainMapMetadataDouble to avifGainMapMetadata by converting double values
-// to the closest uint32_t fractions.
-// Returns AVIF_FALSE if some field values are < 0 or > UINT32_MAX.
-AVIF_NODISCARD AVIF_API avifBool avifGainMapMetadataDoubleToFractions(avifGainMapMetadata * dst, const avifGainMapMetadataDouble * src);
-// Converts a avifGainMapMetadata to avifGainMapMetadataDouble by converting fractions to double values.
-// Returns AVIF_FALSE if some denominators are zero.
-AVIF_NODISCARD AVIF_API avifBool avifGainMapMetadataFractionsToDouble(avifGainMapMetadataDouble * dst, const avifGainMapMetadata * src);
-
 #endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP
 
 // ---------------------------------------------------------------------------
diff --git a/include/avif/avif_cxx.h b/include/avif/avif_cxx.h
index 8a9fd2b..176157c 100644
--- a/include/avif/avif_cxx.h
+++ b/include/avif/avif_cxx.h
@@ -21,6 +21,9 @@
     void operator()(avifEncoder * encoder) const { avifEncoderDestroy(encoder); }
     void operator()(avifDecoder * decoder) const { avifDecoderDestroy(decoder); }
     void operator()(avifImage * image) const { avifImageDestroy(image); }
+#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+    void operator()(avifGainMap * gainMap) const { avifGainMapDestroy(gainMap); }
+#endif
 };
 
 // Use these unique_ptr to ensure the structs are automatically destroyed.
@@ -28,6 +31,10 @@
 using DecoderPtr = std::unique_ptr<avifDecoder, UniquePtrDeleter>;
 using ImagePtr = std::unique_ptr<avifImage, UniquePtrDeleter>;
 
+#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+using GainMapPtr = std::unique_ptr<avifGainMap, UniquePtrDeleter>;
+#endif
+
 } // namespace avif
 
 #endif // AVIF_AVIF_CXX_H
diff --git a/include/avif/internal.h b/include/avif/internal.h
index b35c50c..c044716 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -139,13 +139,6 @@
 AVIF_NODISCARD avifBool avifFractionAdd(avifFraction a, avifFraction b, avifFraction * result);
 AVIF_NODISCARD avifBool avifFractionSub(avifFraction a, avifFraction b, avifFraction * result);
 
-// Creates an int32 fraction that is approximately equal to 'v'.
-// Returns AVIF_FALSE if 'v' is NaN or abs(v) is > INT32_MAX.
-AVIF_NODISCARD avifBool avifDoubleToSignedFraction(double v, int32_t * numerator, uint32_t * denominator);
-// Creates a uint32 fraction that is approximately equal to 'v'.
-// Returns AVIF_FALSE if 'v' is < 0 or > UINT32_MAX or NaN.
-AVIF_NODISCARD avifBool avifDoubleToUnsignedFraction(double v, uint32_t * numerator, uint32_t * denominator);
-
 void avifImageSetDefaults(avifImage * image);
 // Copies all fields that do not need to be freed/allocated from srcImage to dstImage.
 void avifImageCopyNoAlloc(avifImage * dstImage, const avifImage * srcImage);
@@ -786,7 +779,7 @@
 // Removing outliers helps with accuracy/compression.
 avifResult avifFindMinMaxWithoutOutliers(const float * gainMapF, int numPixels, float * rangeMin, float * rangeMax);
 
-avifResult avifGainMapMetadataValidate(const avifGainMapMetadata * metadata, avifDiagnostics * diag);
+avifResult avifGainMapValidateMetadata(const avifGainMap * gainMap, avifDiagnostics * diag);
 
 #endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP
 
diff --git a/src/avif.c b/src/avif.c
index defd78e..a818858 100644
--- a/src/avif.c
+++ b/src/avif.c
@@ -262,7 +262,16 @@
             dstImage->gainMap = avifGainMapCreate();
             AVIF_CHECKERR(dstImage->gainMap, AVIF_RESULT_OUT_OF_MEMORY);
         }
-        dstImage->gainMap->metadata = srcImage->gainMap->metadata;
+        for (int c = 0; c < 3; ++c) {
+            dstImage->gainMap->gainMapMin[c] = srcImage->gainMap->gainMapMin[c];
+            dstImage->gainMap->gainMapMax[c] = srcImage->gainMap->gainMapMax[c];
+            dstImage->gainMap->gainMapGamma[c] = srcImage->gainMap->gainMapGamma[c];
+            dstImage->gainMap->baseOffset[c] = srcImage->gainMap->baseOffset[c];
+            dstImage->gainMap->alternateOffset[c] = srcImage->gainMap->alternateOffset[c];
+        }
+        dstImage->gainMap->baseHdrHeadroom = srcImage->gainMap->baseHdrHeadroom;
+        dstImage->gainMap->alternateHdrHeadroom = srcImage->gainMap->alternateHdrHeadroom;
+        dstImage->gainMap->useBaseColorSpace = srcImage->gainMap->useBaseColorSpace;
         AVIF_CHECKRES(avifRWDataSet(&dstImage->gainMap->altICC, srcImage->gainMap->altICC.data, srcImage->gainMap->altICC.size));
         dstImage->gainMap->altColorPrimaries = srcImage->gainMap->altColorPrimaries;
         dstImage->gainMap->altTransferCharacteristics = srcImage->gainMap->altTransferCharacteristics;
@@ -1192,17 +1201,18 @@
     gainMap->altTransferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED;
     gainMap->altMatrixCoefficients = AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED;
     gainMap->altYUVRange = AVIF_RANGE_FULL;
-    gainMap->metadata.useBaseColorSpace = AVIF_TRUE;
+    gainMap->useBaseColorSpace = AVIF_TRUE;
     // Set all denominators to valid values (1).
     for (int i = 0; i < 3; ++i) {
-        gainMap->metadata.gainMapMinD[i] = 1;
-        gainMap->metadata.gainMapMaxD[i] = 1;
-        gainMap->metadata.gainMapGammaD[i] = 1;
-        gainMap->metadata.baseOffsetD[i] = 1;
-        gainMap->metadata.alternateOffsetD[i] = 1;
+        gainMap->gainMapMin[i].d = 1;
+        gainMap->gainMapMax[i].d = 1;
+        gainMap->gainMapGamma[i].n = 1;
+        gainMap->gainMapGamma[i].d = 1;
+        gainMap->baseOffset[i].d = 1;
+        gainMap->alternateOffset[i].d = 1;
     }
-    gainMap->metadata.baseHdrHeadroomD = 1;
-    gainMap->metadata.alternateHdrHeadroomD = 1;
+    gainMap->baseHdrHeadroom.d = 1;
+    gainMap->alternateHdrHeadroom.d = 1;
     return gainMap;
 }
 
diff --git a/src/gainmap.c b/src/gainmap.c
index d36fe4a..70c0217 100644
--- a/src/gainmap.c
+++ b/src/gainmap.c
@@ -9,71 +9,44 @@
 
 #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
 
-avifBool avifGainMapMetadataDoubleToFractions(avifGainMapMetadata * dst, const avifGainMapMetadataDouble * src)
+static void avifGainMapSetDefaults(avifGainMap * gainMap)
 {
-    AVIF_CHECK(dst != NULL && src != NULL);
-
     for (int i = 0; i < 3; ++i) {
-        AVIF_CHECK(avifDoubleToSignedFraction(src->gainMapMin[i], &dst->gainMapMinN[i], &dst->gainMapMinD[i]));
-        AVIF_CHECK(avifDoubleToSignedFraction(src->gainMapMax[i], &dst->gainMapMaxN[i], &dst->gainMapMaxD[i]));
-        AVIF_CHECK(avifDoubleToUnsignedFraction(src->gainMapGamma[i], &dst->gainMapGammaN[i], &dst->gainMapGammaD[i]));
-        AVIF_CHECK(avifDoubleToSignedFraction(src->baseOffset[i], &dst->baseOffsetN[i], &dst->baseOffsetD[i]));
-        AVIF_CHECK(avifDoubleToSignedFraction(src->alternateOffset[i], &dst->alternateOffsetN[i], &dst->alternateOffsetD[i]));
+        gainMap->gainMapMin[i] = (avifSignedFraction) { 1, 1 };
+        gainMap->gainMapMax[i] = (avifSignedFraction) { 1, 1 };
+        gainMap->baseOffset[i] = (avifSignedFraction) { 1, 64 };
+        gainMap->alternateOffset[i] = (avifSignedFraction) { 1, 64 };
+        gainMap->gainMapGamma[i] = (avifUnsignedFraction) { 1, 1 };
     }
-    AVIF_CHECK(avifDoubleToUnsignedFraction(src->baseHdrHeadroom, &dst->baseHdrHeadroomN, &dst->baseHdrHeadroomD));
-    AVIF_CHECK(avifDoubleToUnsignedFraction(src->alternateHdrHeadroom, &dst->alternateHdrHeadroomN, &dst->alternateHdrHeadroomD));
-    dst->useBaseColorSpace = src->useBaseColorSpace;
-    return AVIF_TRUE;
+    gainMap->baseHdrHeadroom = (avifUnsignedFraction) { 0, 1 };
+    gainMap->alternateHdrHeadroom = (avifUnsignedFraction) { 1, 1 };
+    gainMap->useBaseColorSpace = AVIF_TRUE;
 }
 
-avifBool avifGainMapMetadataFractionsToDouble(avifGainMapMetadataDouble * dst, const avifGainMapMetadata * src)
+static float avifSignedFractionToFloat(avifSignedFraction f)
 {
-    AVIF_CHECK(dst != NULL && src != NULL);
-
-    AVIF_CHECK(src->baseHdrHeadroomD != 0);
-    AVIF_CHECK(src->alternateHdrHeadroomD != 0);
-    for (int i = 0; i < 3; ++i) {
-        AVIF_CHECK(src->gainMapMaxD[i] != 0);
-        AVIF_CHECK(src->gainMapGammaD[i] != 0);
-        AVIF_CHECK(src->gainMapMinD[i] != 0);
-        AVIF_CHECK(src->baseOffsetD[i] != 0);
-        AVIF_CHECK(src->alternateOffsetD[i] != 0);
+    if (f.d == 0) {
+        return 0.0f;
     }
-
-    for (int i = 0; i < 3; ++i) {
-        dst->gainMapMin[i] = (double)src->gainMapMinN[i] / src->gainMapMinD[i];
-        dst->gainMapMax[i] = (double)src->gainMapMaxN[i] / src->gainMapMaxD[i];
-        dst->gainMapGamma[i] = (double)src->gainMapGammaN[i] / src->gainMapGammaD[i];
-        dst->baseOffset[i] = (double)src->baseOffsetN[i] / src->baseOffsetD[i];
-        dst->alternateOffset[i] = (double)src->alternateOffsetN[i] / src->alternateOffsetD[i];
-    }
-    dst->baseHdrHeadroom = (double)src->baseHdrHeadroomN / src->baseHdrHeadroomD;
-    dst->alternateHdrHeadroom = (double)src->alternateHdrHeadroomN / src->alternateHdrHeadroomD;
-    dst->useBaseColorSpace = src->useBaseColorSpace;
-    return AVIF_TRUE;
+    return (float)f.n / f.d;
 }
 
-static void avifGainMapMetadataDoubleSetDefaults(avifGainMapMetadataDouble * metadata)
+static float avifUnsignedFractionToFloat(avifUnsignedFraction f)
 {
-    memset(metadata, 0, sizeof(*metadata));
-    for (int i = 0; i < 3; ++i) {
-        metadata->baseOffset[i] = 0.015625;      // 1/64
-        metadata->alternateOffset[i] = 0.015625; // 1/64
-        metadata->gainMapGamma[i] = 1.0;
+    if (f.d == 0) {
+        return 0.0f;
     }
-    metadata->baseHdrHeadroom = 0.0;
-    metadata->alternateHdrHeadroom = 1.0;
-    metadata->useBaseColorSpace = AVIF_TRUE;
+    return (float)f.n / f.d;
 }
 
 // ---------------------------------------------------------------------------
 // Apply a gain map.
 
 // Returns a weight in [-1.0, 1.0] that represents how much the gain map should be applied.
-static float avifGetGainMapWeight(float hdrHeadroom, const avifGainMapMetadataDouble * metadata)
+static float avifGetGainMapWeight(float hdrHeadroom, const avifGainMap * gainMap)
 {
-    const float baseHdrHeadroom = (float)metadata->baseHdrHeadroom;
-    const float alternateHdrHeadroom = (float)metadata->alternateHdrHeadroom;
+    const float baseHdrHeadroom = avifUnsignedFractionToFloat(gainMap->baseHdrHeadroom);
+    const float alternateHdrHeadroom = avifUnsignedFractionToFloat(gainMap->alternateHdrHeadroom);
     if (baseHdrHeadroom == alternateHdrHeadroom) {
         // Do not apply the gain map if the HDR headroom is the same.
         // This case is not handled in the specification and does not make practical sense.
@@ -104,6 +77,8 @@
 {
     avifDiagnosticsClearError(diag);
 
+    AVIF_CHECKRES(avifGainMapValidateMetadata(gainMap, diag));
+
     if (hdrHeadroom < 0.0f) {
         avifDiagnosticsPrintf(diag, "hdrHeadroom should be >= 0, got %f", hdrHeadroom);
         return AVIF_RESULT_INVALID_ARGUMENT;
@@ -113,22 +88,10 @@
         return AVIF_RESULT_INVALID_ARGUMENT;
     }
 
-    avifGainMapMetadataDouble metadata;
-    if (!avifGainMapMetadataFractionsToDouble(&metadata, &gainMap->metadata)) {
-        avifDiagnosticsPrintf(diag, "Invalid gain map metadata, a denominator value is zero");
-        return AVIF_RESULT_INVALID_ARGUMENT;
-    }
-    for (int i = 0; i < 3; ++i) {
-        if (metadata.gainMapGamma[i] <= 0) {
-            avifDiagnosticsPrintf(diag, "Invalid gain map metadata, gamma should be strictly positive");
-            return AVIF_RESULT_INVALID_ARGUMENT;
-        }
-    }
-
     const uint32_t width = baseImage->width;
     const uint32_t height = baseImage->height;
 
-    const avifBool useBaseColorSpace = gainMap->metadata.useBaseColorSpace;
+    const avifBool useBaseColorSpace = gainMap->useBaseColorSpace;
     const avifColorPrimaries gainMapMathPrimaries =
         (useBaseColorSpace || (gainMap->altColorPrimaries == AVIF_COLOR_PRIMARIES_UNSPECIFIED)) ? baseColorPrimaries
                                                                                                 : gainMap->altColorPrimaries;
@@ -147,7 +110,7 @@
 
     // --- After this point, the function should exit with 'goto cleanup' to free allocated pixels.
 
-    const float weight = avifGetGainMapWeight(hdrHeadroom, &metadata);
+    const float weight = avifGetGainMapWeight(hdrHeadroom, gainMap);
 
     // Early exit if the gain map does not need to be applied and the pixel format is the same.
     if (weight == 0.0f && outputTransferCharacteristics == baseTransferCharacteristics &&
@@ -250,9 +213,22 @@
 
     float rgbMaxLinear = 0; // Max tone mapped pixel value across R, G and B channels.
     float rgbSumLinear = 0; // Sum of max(r, g, b) for mapped pixels.
-    const float gammaInv[3] = { 1.0f / (float)metadata.gainMapGamma[0],
-                                1.0f / (float)metadata.gainMapGamma[1],
-                                1.0f / (float)metadata.gainMapGamma[2] };
+    // The gain map metadata contains the encoding gamma, and 1/gamma should be used for decoding.
+    const float gammaInv[3] = { 1.0f / avifUnsignedFractionToFloat(gainMap->gainMapGamma[0]),
+                                1.0f / avifUnsignedFractionToFloat(gainMap->gainMapGamma[1]),
+                                1.0f / avifUnsignedFractionToFloat(gainMap->gainMapGamma[2]) };
+    const float gainMapMin[3] = { avifSignedFractionToFloat(gainMap->gainMapMin[0]),
+                                  avifSignedFractionToFloat(gainMap->gainMapMin[1]),
+                                  avifSignedFractionToFloat(gainMap->gainMapMin[2]) };
+    const float gainMapMax[3] = { avifSignedFractionToFloat(gainMap->gainMapMax[0]),
+                                  avifSignedFractionToFloat(gainMap->gainMapMax[1]),
+                                  avifSignedFractionToFloat(gainMap->gainMapMax[2]) };
+    const float baseOffset[3] = { avifSignedFractionToFloat(gainMap->baseOffset[0]),
+                                  avifSignedFractionToFloat(gainMap->baseOffset[1]),
+                                  avifSignedFractionToFloat(gainMap->baseOffset[2]) };
+    const float alternateOffset[3] = { avifSignedFractionToFloat(gainMap->alternateOffset[0]),
+                                       avifSignedFractionToFloat(gainMap->alternateOffset[1]),
+                                       avifSignedFractionToFloat(gainMap->alternateOffset[2]) };
     for (uint32_t j = 0; j < height; ++j) {
         for (uint32_t i = 0; i < width; ++i) {
             float basePixelRGBA[4];
@@ -278,10 +254,8 @@
                 const float gainMapValue = gainMapRGBA[c];
 
                 // Undo gamma & affine transform; the result is in log2 space.
-                const float gainMapLog2 =
-                    lerp((float)metadata.gainMapMin[c], (float)metadata.gainMapMax[c], powf(gainMapValue, gammaInv[c]));
-                const float toneMappedLinear = (baseLinear + (float)metadata.baseOffset[c]) * exp2f(gainMapLog2 * weight) -
-                                               (float)metadata.alternateOffset[c];
+                const float gainMapLog2 = lerp(gainMapMin[c], gainMapMax[c], powf(gainMapValue, gammaInv[c]));
+                const float toneMappedLinear = (baseLinear + baseOffset[c]) * exp2f(gainMapLog2 * weight) - alternateOffset[c];
 
                 if (toneMappedLinear > rgbMaxLinear) {
                     rgbMaxLinear = toneMappedLinear;
@@ -441,21 +415,25 @@
     return AVIF_RESULT_OK;
 }
 
-avifResult avifGainMapMetadataValidate(const avifGainMapMetadata * metadata, avifDiagnostics * diag)
+avifResult avifGainMapValidateMetadata(const avifGainMap * gainMap, avifDiagnostics * diag)
 {
     for (int i = 0; i < 3; ++i) {
-        if (metadata->gainMapMinD[i] == 0 || metadata->gainMapMaxD[i] == 0 || metadata->gainMapGammaD[i] == 0 ||
-            metadata->baseOffsetD[i] == 0 || metadata->alternateOffsetD[i] == 0) {
-            avifDiagnosticsPrintf(diag, "Per-channel denominator is 0 in avifGainMapMetadata");
+        if (gainMap->gainMapMin[i].d == 0 || gainMap->gainMapMax[i].d == 0 || gainMap->gainMapGamma[i].d == 0 ||
+            gainMap->baseOffset[i].d == 0 || gainMap->alternateOffset[i].d == 0) {
+            avifDiagnosticsPrintf(diag, "Per-channel denominator is 0 in gain map metadata");
+            return AVIF_RESULT_INVALID_ARGUMENT;
+        }
+        if (gainMap->gainMapGamma[i].n == 0) {
+            avifDiagnosticsPrintf(diag, "Per-channel gamma is 0 in gain map metadata");
             return AVIF_RESULT_INVALID_ARGUMENT;
         }
     }
-    if (metadata->baseHdrHeadroomD == 0 || metadata->alternateHdrHeadroomD == 0) {
-        avifDiagnosticsPrintf(diag, "Headroom denominator is 0 in avifGainMapMetadata");
+    if (gainMap->baseHdrHeadroom.d == 0 || gainMap->alternateHdrHeadroom.d == 0) {
+        avifDiagnosticsPrintf(diag, "Headroom denominator is 0 in gain map metadata");
         return AVIF_RESULT_INVALID_ARGUMENT;
     }
-    if (metadata->useBaseColorSpace != 0 && metadata->useBaseColorSpace != 1) {
-        avifDiagnosticsPrintf(diag, "useBaseColorSpace is %d in avifGainMapMetadata", metadata->useBaseColorSpace);
+    if (gainMap->useBaseColorSpace != 0 && gainMap->useBaseColorSpace != 1) {
+        avifDiagnosticsPrintf(diag, "useBaseColorSpace is %d in gain map metadata", gainMap->useBaseColorSpace);
         return AVIF_RESULT_INVALID_ARGUMENT;
     }
     return AVIF_RESULT_OK;
@@ -560,9 +538,8 @@
         }
     }
 
-    avifGainMapMetadataDouble gainMapMetadata;
-    avifGainMapMetadataDoubleSetDefaults(&gainMapMetadata);
-    gainMapMetadata.useBaseColorSpace = (gainMapMathPrimaries == baseColorPrimaries);
+    avifGainMapSetDefaults(gainMap);
+    gainMap->useBaseColorSpace = (gainMapMathPrimaries == baseColorPrimaries);
 
     float (*baseGammaToLinear)(float) = avifTransferCharacteristicsGetGammaToLinearFunction(baseTransferCharacteristics);
     float (*altGammaToLinear)(float) = avifTransferCharacteristicsGetGammaToLinearFunction(altTransferCharacteristics);
@@ -571,7 +548,7 @@
 
     double rgbConversionCoeffs[3][3];
     if (colorSpacesDiffer) {
-        if (gainMapMetadata.useBaseColorSpace) {
+        if (gainMap->useBaseColorSpace) {
             if (!avifColorPrimariesComputeRGBToRGBMatrix(altColorPrimaries, baseColorPrimaries, rgbConversionCoeffs)) {
                 avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion");
                 res = AVIF_RESULT_NOT_IMPLEMENTED;
@@ -586,24 +563,31 @@
         }
     }
 
+    float baseOffset[3] = { avifSignedFractionToFloat(gainMap->baseOffset[0]),
+                            avifSignedFractionToFloat(gainMap->baseOffset[1]),
+                            avifSignedFractionToFloat(gainMap->baseOffset[2]) };
+    float alternateOffset[3] = { avifSignedFractionToFloat(gainMap->alternateOffset[0]),
+                                 avifSignedFractionToFloat(gainMap->alternateOffset[1]),
+                                 avifSignedFractionToFloat(gainMap->alternateOffset[2]) };
+
     // If we are converting from one colorspace to another, some RGB values may be negative and an offset must be added to
     // avoid clamping (although the choice of color space to do the gain map computation with
     // avifChooseColorSpaceForGainMapMath() should mostly avoid this).
     if (colorSpacesDiffer) {
         // Color convert pure red, pure green and pure blue in turn and see if they result in negative values.
-        float rgba[4] = { 0 };
-        float channelMin[3] = { 0 };
+        float rgba[4] = { 0.0f };
+        float channelMin[3] = { 0.0f };
         for (int j = 0; j < height; ++j) {
             for (int i = 0; i < width; ++i) {
-                avifGetRGBAPixel(gainMapMetadata.useBaseColorSpace ? altRgbImage : baseRgbImage,
+                avifGetRGBAPixel(gainMap->useBaseColorSpace ? altRgbImage : baseRgbImage,
                                  i,
                                  j,
-                                 gainMapMetadata.useBaseColorSpace ? &altRGBInfo : &baseRGBInfo,
+                                 gainMap->useBaseColorSpace ? &altRGBInfo : &baseRGBInfo,
                                  rgba);
 
                 // Convert to linear.
                 for (int c = 0; c < 3; ++c) {
-                    if (gainMapMetadata.useBaseColorSpace) {
+                    if (gainMap->useBaseColorSpace) {
                         rgba[c] = altGammaToLinear(rgba[c]);
                     } else {
                         rgba[c] = baseGammaToLinear(rgba[c]);
@@ -622,10 +606,10 @@
             const float maxOffset = 0.1f;
             if (channelMin[c] < -kEpsilon) {
                 // Increase the offset to avoid negative values.
-                if (gainMapMetadata.useBaseColorSpace) {
-                    gainMapMetadata.alternateOffset[c] = AVIF_MIN(gainMapMetadata.alternateOffset[c] - channelMin[c], maxOffset);
+                if (gainMap->useBaseColorSpace) {
+                    alternateOffset[c] = AVIF_MIN(alternateOffset[c] - channelMin[c], maxOffset);
                 } else {
-                    gainMapMetadata.baseOffset[c] = AVIF_MIN(gainMapMetadata.baseOffset[c] - channelMin[c], maxOffset);
+                    baseOffset[c] = AVIF_MIN(baseOffset[c] - channelMin[c], maxOffset);
                 }
             }
         }
@@ -648,7 +632,7 @@
             }
 
             if (colorSpacesDiffer) {
-                if (gainMapMetadata.useBaseColorSpace) {
+                if (gainMap->useBaseColorSpace) {
                     // convert altRGBA to baseRGBA's color space
                     avifLinearRGBConvertColorSpace(altRGBA, rgbConversionCoeffs);
                 } else {
@@ -671,7 +655,7 @@
                 if (alt > altMax) {
                     altMax = alt;
                 }
-                const float ratio = (alt + (float)gainMapMetadata.alternateOffset[c]) / (base + (float)gainMapMetadata.baseOffset[c]);
+                const float ratio = (alt + alternateOffset[c]) / (base + baseOffset[c]);
                 const float ratioLog2 = log2f(AVIF_MAX(ratio, kEpsilon));
                 gainMapF[c][j * width + i] = ratioLog2;
             }
@@ -679,13 +663,18 @@
     }
 
     // Populate the gain map metadata's headrooms.
-    gainMapMetadata.baseHdrHeadroom = log2f(AVIF_MAX(baseMax, kEpsilon));
-    gainMapMetadata.alternateHdrHeadroom = log2f(AVIF_MAX(altMax, kEpsilon));
+    const double baseHeadroom = log2f(AVIF_MAX(baseMax, kEpsilon));
+    const double alternateHeadroom = log2f(AVIF_MAX(altMax, kEpsilon));
+    if (!avifDoubleToUnsignedFraction(baseHeadroom, &gainMap->baseHdrHeadroom) ||
+        !avifDoubleToUnsignedFraction(alternateHeadroom, &gainMap->alternateHdrHeadroom)) {
+        res = AVIF_RESULT_INVALID_ARGUMENT;
+        goto cleanup;
+    }
 
     // Multiply the gainmap by sign(alternateHdrHeadroom - baseHdrHeadroom), to
     // ensure that it stores the log-ratio of the HDR representation to the SDR
     // representation.
-    if (gainMapMetadata.alternateHdrHeadroom < gainMapMetadata.baseHdrHeadroom) {
+    if (alternateHeadroom < baseHeadroom) {
         for (int c = 0; c < numGainMapChannels; ++c) {
             for (int j = 0; j < height; ++j) {
                 for (int i = 0; i < width; ++i) {
@@ -707,15 +696,13 @@
 
     // Populate the gain map metadata's min and max values.
     for (int c = 0; c < 3; ++c) {
-        gainMapMetadata.gainMapMin[c] = gainMapMinLog2[singleChannel ? 0 : c];
-        gainMapMetadata.gainMapMax[c] = gainMapMaxLog2[singleChannel ? 0 : c];
-    }
-
-    // All of gainMapMetadata has been populated now (except for gamma which is left to the default
-    // value), so convert to the fraction form in which it will be stored.
-    if (!avifGainMapMetadataDoubleToFractions(&gainMap->metadata, &gainMapMetadata)) {
-        res = AVIF_RESULT_UNKNOWN_ERROR;
-        goto cleanup;
+        if (!avifDoubleToSignedFraction(gainMapMinLog2[singleChannel ? 0 : c], &gainMap->gainMapMin[c]) ||
+            !avifDoubleToSignedFraction(gainMapMaxLog2[singleChannel ? 0 : c], &gainMap->gainMapMax[c]) ||
+            !avifDoubleToSignedFraction(alternateOffset[c], &gainMap->alternateOffset[c]) ||
+            !avifDoubleToSignedFraction(baseOffset[c], &gainMap->baseOffset[c])) {
+            res = AVIF_RESULT_INVALID_ARGUMENT;
+            goto cleanup;
+        }
     }
 
     // Scale the gain map values to map [min, max] range to [0, 1].
@@ -724,12 +711,13 @@
         if (range <= 0.0f) {
             continue;
         }
+        const float gainMapGamma = avifUnsignedFractionToFloat(gainMap->gainMapGamma[c]);
 
         for (int j = 0; j < height; ++j) {
             for (int i = 0; i < width; ++i) {
                 // Remap [min; max] range to [0; 1]
                 const float v = AVIF_CLAMP(gainMapF[c][j * width + i], gainMapMinLog2[c], gainMapMaxLog2[c]);
-                gainMapF[c][j * width + i] = powf((v - gainMapMinLog2[c]) / range, (float)gainMapMetadata.gainMapGamma[c]);
+                gainMapF[c][j * width + i] = powf((v - gainMapMinLog2[c]) / range, gainMapGamma);
             }
         }
     }
diff --git a/src/read.c b/src/read.c
index 3afd4a5..d711cb9 100644
--- a/src/read.c
+++ b/src/read.c
@@ -1958,7 +1958,7 @@
 
 #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
 
-static avifBool avifParseGainMapMetadata(avifGainMapMetadata * metadata, avifROStream * s)
+static avifBool avifParseGainMapMetadata(avifGainMap * gainMap, avifROStream * s)
 {
     uint32_t isMultichannel;
     AVIF_CHECK(avifROStreamReadBitsU32(s, &isMultichannel, 1)); // unsigned int(1) is_multichannel;
@@ -1966,47 +1966,42 @@
 
     uint32_t useBaseColorSpace;
     AVIF_CHECK(avifROStreamReadBitsU32(s, &useBaseColorSpace, 1)); // unsigned int(1) use_base_colour_space;
-    metadata->useBaseColorSpace = useBaseColorSpace ? AVIF_TRUE : AVIF_FALSE;
+    gainMap->useBaseColorSpace = useBaseColorSpace ? AVIF_TRUE : AVIF_FALSE;
 
     uint32_t reserved;
     AVIF_CHECK(avifROStreamReadBitsU32(s, &reserved, 6)); // unsigned int(6) reserved;
 
-    AVIF_CHECK(avifROStreamReadU32(s, &metadata->baseHdrHeadroomN));      // unsigned int(32) base_hdr_headroom_numerator;
-    AVIF_CHECK(avifROStreamReadU32(s, &metadata->baseHdrHeadroomD));      // unsigned int(32) base_hdr_headroom_denominator;
-    AVIF_CHECK(avifROStreamReadU32(s, &metadata->alternateHdrHeadroomN)); // unsigned int(32) alternate_hdr_headroom_numerator;
-    AVIF_CHECK(avifROStreamReadU32(s, &metadata->alternateHdrHeadroomD)); // unsigned int(32) alternate_hdr_headroom_denominator;
+    AVIF_CHECK(avifROStreamReadU32(s, &gainMap->baseHdrHeadroom.n));      // unsigned int(32) base_hdr_headroom_numerator;
+    AVIF_CHECK(avifROStreamReadU32(s, &gainMap->baseHdrHeadroom.d));      // unsigned int(32) base_hdr_headroom_denominator;
+    AVIF_CHECK(avifROStreamReadU32(s, &gainMap->alternateHdrHeadroom.n)); // unsigned int(32) alternate_hdr_headroom_numerator;
+    AVIF_CHECK(avifROStreamReadU32(s, &gainMap->alternateHdrHeadroom.d)); // unsigned int(32) alternate_hdr_headroom_denominator;
 
     for (int c = 0; c < channelCount; ++c) {
-        AVIF_CHECK(avifROStreamReadU32(s, (uint32_t *)&metadata->gainMapMinN[c])); // int(32) gain_map_min_numerator;
-        AVIF_CHECK(avifROStreamReadU32(s, &metadata->gainMapMinD[c]));             // unsigned int(32) gain_map_min_denominator;
-        AVIF_CHECK(avifROStreamReadU32(s, (uint32_t *)&metadata->gainMapMaxN[c])); // int(32) gain_map_max_numerator;
-        AVIF_CHECK(avifROStreamReadU32(s, &metadata->gainMapMaxD[c]));             // unsigned int(32) gain_map_max_denominator;
-        AVIF_CHECK(avifROStreamReadU32(s, &metadata->gainMapGammaN[c]));           // unsigned int(32) gamma_numerator;
-        AVIF_CHECK(avifROStreamReadU32(s, &metadata->gainMapGammaD[c]));           // unsigned int(32) gamma_denominator;
-        AVIF_CHECK(avifROStreamReadU32(s, (uint32_t *)&metadata->baseOffsetN[c])); // int(32) base_offset_numerator;
-        AVIF_CHECK(avifROStreamReadU32(s, &metadata->baseOffsetD[c]));             // unsigned int(32) base_offset_denominator;
-        AVIF_CHECK(avifROStreamReadU32(s, (uint32_t *)&metadata->alternateOffsetN[c])); // int(32) alternate_offset_numerator;
-        AVIF_CHECK(avifROStreamReadU32(s, &metadata->alternateOffsetD[c])); // unsigned int(32) alternate_offset_denominator;
+        AVIF_CHECK(avifROStreamReadU32(s, (uint32_t *)&gainMap->gainMapMin[c].n)); // int(32) gain_map_min_numerator;
+        AVIF_CHECK(avifROStreamReadU32(s, &gainMap->gainMapMin[c].d));             // unsigned int(32) gain_map_min_denominator;
+        AVIF_CHECK(avifROStreamReadU32(s, (uint32_t *)&gainMap->gainMapMax[c].n)); // int(32) gain_map_max_numerator;
+        AVIF_CHECK(avifROStreamReadU32(s, &gainMap->gainMapMax[c].d));             // unsigned int(32) gain_map_max_denominator;
+        AVIF_CHECK(avifROStreamReadU32(s, &gainMap->gainMapGamma[c].n));           // unsigned int(32) gamma_numerator;
+        AVIF_CHECK(avifROStreamReadU32(s, &gainMap->gainMapGamma[c].d));           // unsigned int(32) gamma_denominator;
+        AVIF_CHECK(avifROStreamReadU32(s, (uint32_t *)&gainMap->baseOffset[c].n)); // int(32) base_offset_numerator;
+        AVIF_CHECK(avifROStreamReadU32(s, &gainMap->baseOffset[c].d));             // unsigned int(32) base_offset_denominator;
+        AVIF_CHECK(avifROStreamReadU32(s, (uint32_t *)&gainMap->alternateOffset[c].n)); // int(32) alternate_offset_numerator;
+        AVIF_CHECK(avifROStreamReadU32(s, &gainMap->alternateOffset[c].d)); // unsigned int(32) alternate_offset_denominator;
     }
 
     // Fill the remaining values by copying those from the first channel.
     for (int c = channelCount; c < 3; ++c) {
-        metadata->gainMapMinN[c] = metadata->gainMapMinN[0];
-        metadata->gainMapMinD[c] = metadata->gainMapMinD[0];
-        metadata->gainMapMaxN[c] = metadata->gainMapMaxN[0];
-        metadata->gainMapMaxD[c] = metadata->gainMapMaxD[0];
-        metadata->gainMapGammaN[c] = metadata->gainMapGammaN[0];
-        metadata->gainMapGammaD[c] = metadata->gainMapGammaD[0];
-        metadata->baseOffsetN[c] = metadata->baseOffsetN[0];
-        metadata->baseOffsetD[c] = metadata->baseOffsetD[0];
-        metadata->alternateOffsetN[c] = metadata->alternateOffsetN[0];
-        metadata->alternateOffsetD[c] = metadata->alternateOffsetD[0];
+        gainMap->gainMapMin[c] = gainMap->gainMapMin[0];
+        gainMap->gainMapMax[c] = gainMap->gainMapMax[0];
+        gainMap->gainMapGamma[c] = gainMap->gainMapGamma[0];
+        gainMap->baseOffset[c] = gainMap->baseOffset[0];
+        gainMap->alternateOffset[c] = gainMap->alternateOffset[0];
     }
     return AVIF_TRUE;
 }
 
 // If the gain map's version or minimum_version tag is not supported, returns AVIF_RESULT_NOT_IMPLEMENTED.
-static avifResult avifParseToneMappedImageBox(avifGainMapMetadata * metadata, const uint8_t * raw, size_t rawLen, avifDiagnostics * diag)
+static avifResult avifParseToneMappedImageBox(avifGainMap * gainMap, const uint8_t * raw, size_t rawLen, avifDiagnostics * diag)
 {
     BEGIN_STREAM(s, raw, rawLen, diag, "Box[tmap]");
 
@@ -2028,7 +2023,7 @@
     AVIF_CHECKERR(avifROStreamReadU16(&s, &writerVersion), AVIF_RESULT_INVALID_TONE_MAPPED_IMAGE); // unsigned int(16) writer_version;
     AVIF_CHECKERR(writerVersion >= minimumVersion, AVIF_RESULT_INVALID_TONE_MAPPED_IMAGE);
 
-    AVIF_CHECKERR(avifParseGainMapMetadata(metadata, &s), AVIF_RESULT_INVALID_TONE_MAPPED_IMAGE);
+    AVIF_CHECKERR(avifParseGainMapMetadata(gainMap, &s), AVIF_RESULT_INVALID_TONE_MAPPED_IMAGE);
 
     if (writerVersion <= supportedMetadataVersion) {
         AVIF_CHECKERR(avifROStreamRemainingBytes(&s) == 0, AVIF_RESULT_INVALID_TONE_MAPPED_IMAGE);
@@ -5487,7 +5482,7 @@
                 AVIF_CHECKRES(avifDecoderItemRead(toneMappedImageItem, decoder->io, &tmapData, 0, 0, data->diag));
                 AVIF_ASSERT_OR_RETURN(decoder->image->gainMap != NULL);
                 const avifResult tmapParsingRes =
-                    avifParseToneMappedImageBox(&decoder->image->gainMap->metadata, tmapData.data, tmapData.size, data->diag);
+                    avifParseToneMappedImageBox(decoder->image->gainMap, tmapData.data, tmapData.size, data->diag);
                 if (tmapParsingRes == AVIF_RESULT_NOT_IMPLEMENTED) {
                     // Unsupported gain map version. Simply ignore the gain map.
                     avifGainMapDestroy(decoder->image->gainMap);
diff --git a/src/utils.c b/src/utils.c
index bcb191a..8578d30 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -231,7 +231,7 @@
     }
 
     // Maximum denominator: makes sure that the numerator is <= maxNumerator and the denominator is <= UINT32_MAX.
-    const uint64_t maxD = (v <= 1) ? UINT32_MAX : (uint64_t)floor(maxNumerator / v);
+    const uint32_t maxD = (v <= 1) ? UINT32_MAX : (uint32_t)floor(maxNumerator / v);
 
     // Find the best approximation of v as a fraction using continued fractions, see
     // https://en.wikipedia.org/wiki/Continued_fraction
@@ -252,7 +252,7 @@
         }
         currentV = 1.0 / currentV;
         const double newD = previousD + floor(currentV) * (*denominator);
-        if (newD > maxD) {
+        if (newD > (double)maxD) {
             // This is the best we can do with a denominator <= max_d.
             return AVIF_TRUE;
         }
@@ -269,20 +269,20 @@
     return AVIF_TRUE;
 }
 
-avifBool avifDoubleToSignedFraction(double v, int32_t * numerator, uint32_t * denominator)
+avifBool avifDoubleToSignedFraction(double v, avifSignedFraction * fraction)
 {
     uint32_t positive_numerator;
-    if (!avifDoubleToUnsignedFractionImpl(fabs(v), INT32_MAX, &positive_numerator, denominator)) {
+    if (!avifDoubleToUnsignedFractionImpl(fabs(v), INT32_MAX, &positive_numerator, &fraction->d)) {
         return AVIF_FALSE;
     }
-    *numerator = (int32_t)positive_numerator;
+    fraction->n = (int32_t)positive_numerator;
     if (v < 0) {
-        *numerator *= -1;
+        fraction->n *= -1;
     }
     return AVIF_TRUE;
 }
 
-avifBool avifDoubleToUnsignedFraction(double v, uint32_t * numerator, uint32_t * denominator)
+avifBool avifDoubleToUnsignedFraction(double v, avifUnsignedFraction * fraction)
 {
-    return avifDoubleToUnsignedFractionImpl(v, UINT32_MAX, numerator, denominator);
+    return avifDoubleToUnsignedFractionImpl(v, UINT32_MAX, &fraction->n, &fraction->d);
 }
diff --git a/src/write.c b/src/write.c
index 92f8539..1587586 100644
--- a/src/write.c
+++ b/src/write.c
@@ -891,32 +891,32 @@
 
 #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
 
-static avifBool avifGainmapMetadataIdenticalChannels(const avifGainMapMetadata * metadata)
+static avifBool avifGainMapIdenticalChannels(const avifGainMap * gainMap)
 {
-    return metadata->gainMapMinN[0] == metadata->gainMapMinN[1] && metadata->gainMapMinN[0] == metadata->gainMapMinN[2] &&
-           metadata->gainMapMinD[0] == metadata->gainMapMinD[1] && metadata->gainMapMinD[0] == metadata->gainMapMinD[2] &&
-           metadata->gainMapMaxN[0] == metadata->gainMapMaxN[1] && metadata->gainMapMaxN[0] == metadata->gainMapMaxN[2] &&
-           metadata->gainMapMaxD[0] == metadata->gainMapMaxD[1] && metadata->gainMapMaxD[0] == metadata->gainMapMaxD[2] &&
-           metadata->gainMapGammaN[0] == metadata->gainMapGammaN[1] && metadata->gainMapGammaN[0] == metadata->gainMapGammaN[2] &&
-           metadata->gainMapGammaD[0] == metadata->gainMapGammaD[1] && metadata->gainMapGammaD[0] == metadata->gainMapGammaD[2] &&
-           metadata->baseOffsetN[0] == metadata->baseOffsetN[1] && metadata->baseOffsetN[0] == metadata->baseOffsetN[2] &&
-           metadata->baseOffsetD[0] == metadata->baseOffsetD[1] && metadata->baseOffsetD[0] == metadata->baseOffsetD[2] &&
-           metadata->alternateOffsetN[0] == metadata->alternateOffsetN[1] &&
-           metadata->alternateOffsetN[0] == metadata->alternateOffsetN[2] &&
-           metadata->alternateOffsetD[0] == metadata->alternateOffsetD[1] &&
-           metadata->alternateOffsetD[0] == metadata->alternateOffsetD[2];
+    return gainMap->gainMapMin[0].n == gainMap->gainMapMin[1].n && gainMap->gainMapMin[0].n == gainMap->gainMapMin[2].n &&
+           gainMap->gainMapMin[0].d == gainMap->gainMapMin[1].d && gainMap->gainMapMin[0].d == gainMap->gainMapMin[2].d &&
+           gainMap->gainMapMax[0].n == gainMap->gainMapMax[1].n && gainMap->gainMapMax[0].n == gainMap->gainMapMax[2].n &&
+           gainMap->gainMapMax[0].d == gainMap->gainMapMax[1].d && gainMap->gainMapMax[0].d == gainMap->gainMapMax[2].d &&
+           gainMap->gainMapGamma[0].n == gainMap->gainMapGamma[1].n && gainMap->gainMapGamma[0].n == gainMap->gainMapGamma[2].n &&
+           gainMap->gainMapGamma[0].d == gainMap->gainMapGamma[1].d && gainMap->gainMapGamma[0].d == gainMap->gainMapGamma[2].d &&
+           gainMap->baseOffset[0].n == gainMap->baseOffset[1].n && gainMap->baseOffset[0].n == gainMap->baseOffset[2].n &&
+           gainMap->baseOffset[0].d == gainMap->baseOffset[1].d && gainMap->baseOffset[0].d == gainMap->baseOffset[2].d &&
+           gainMap->alternateOffset[0].n == gainMap->alternateOffset[1].n &&
+           gainMap->alternateOffset[0].n == gainMap->alternateOffset[2].n &&
+           gainMap->alternateOffset[0].d == gainMap->alternateOffset[1].d &&
+           gainMap->alternateOffset[0].d == gainMap->alternateOffset[2].d;
 }
 
 // Returns the number of bytes written by avifWriteGainmapMetadata().
-static avifBool avifGainmapMetadataSize(const avifGainMapMetadata * metadata)
+static avifBool avifGainMapMetadataSize(const avifGainMap * gainMap)
 {
-    const uint8_t channelCount = avifGainmapMetadataIdenticalChannels(metadata) ? 1u : 3u;
+    const uint8_t channelCount = avifGainMapIdenticalChannels(gainMap) ? 1u : 3u;
     return sizeof(uint16_t) * 2 + sizeof(uint8_t) + sizeof(uint32_t) * 4 + channelCount * sizeof(uint32_t) * 10;
 }
 
-static avifResult avifWriteGainmapMetadata(avifRWStream * s, const avifGainMapMetadata * metadata, avifDiagnostics * diag)
+static avifResult avifWriteGainmapMetadata(avifRWStream * s, const avifGainMap * gainMap, avifDiagnostics * diag)
 {
-    AVIF_CHECKRES(avifGainMapMetadataValidate(metadata, diag));
+    AVIF_CHECKRES(avifGainMapValidateMetadata(gainMap, diag));
     const size_t offset = avifRWStreamOffset(s);
 
     // GainMapMetadata syntax as per clause C.2.2 of ISO 21496-1:
@@ -928,37 +928,37 @@
     AVIF_CHECKRES(avifRWStreamWriteBits(s, writerVersion, 16)); // unsigned int(16) writer_version;
 
     if (minimumVersion == 0) {
-        const uint8_t channelCount = avifGainmapMetadataIdenticalChannels(metadata) ? 1u : 3u;
-        AVIF_CHECKRES(avifRWStreamWriteBits(s, channelCount == 3, 1));           // unsigned int(1) is_multichannel;
-        AVIF_CHECKRES(avifRWStreamWriteBits(s, metadata->useBaseColorSpace, 1)); // unsigned int(1) use_base_colour_space;
-        AVIF_CHECKRES(avifRWStreamWriteBits(s, 0, 6));                           // unsigned int(6) reserved;
+        const uint8_t channelCount = avifGainMapIdenticalChannels(gainMap) ? 1u : 3u;
+        AVIF_CHECKRES(avifRWStreamWriteBits(s, channelCount == 3, 1));          // unsigned int(1) is_multichannel;
+        AVIF_CHECKRES(avifRWStreamWriteBits(s, gainMap->useBaseColorSpace, 1)); // unsigned int(1) use_base_colour_space;
+        AVIF_CHECKRES(avifRWStreamWriteBits(s, 0, 6));                          // unsigned int(6) reserved;
 
-        AVIF_CHECKRES(avifRWStreamWriteBits(s, metadata->baseHdrHeadroomN, 32)); // unsigned int(32) base_hdr_headroom_numerator;
-        AVIF_CHECKRES(avifRWStreamWriteBits(s, metadata->baseHdrHeadroomD, 32)); // unsigned int(32) base_hdr_headroom_denominator;
-        AVIF_CHECKRES(avifRWStreamWriteBits(s, metadata->alternateHdrHeadroomN, 32)); // unsigned int(32) alternate_hdr_headroom_numerator;
-        AVIF_CHECKRES(avifRWStreamWriteBits(s, metadata->alternateHdrHeadroomD, 32)); // unsigned int(32) alternate_hdr_headroom_denominator;
+        AVIF_CHECKRES(avifRWStreamWriteBits(s, gainMap->baseHdrHeadroom.n, 32)); // unsigned int(32) base_hdr_headroom_numerator;
+        AVIF_CHECKRES(avifRWStreamWriteBits(s, gainMap->baseHdrHeadroom.d, 32)); // unsigned int(32) base_hdr_headroom_denominator;
+        AVIF_CHECKRES(avifRWStreamWriteBits(s, gainMap->alternateHdrHeadroom.n, 32)); // unsigned int(32) alternate_hdr_headroom_numerator;
+        AVIF_CHECKRES(avifRWStreamWriteBits(s, gainMap->alternateHdrHeadroom.d, 32)); // unsigned int(32) alternate_hdr_headroom_denominator;
 
         // GainMapChannel channels[channel_count];
         for (int c = 0; c < channelCount; ++c) {
             // GainMapChannel syntax as per clause C.2.2 of ISO 21496-1:
-            AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)metadata->gainMapMinN[c], 32)); // int(32) gain_map_min_numerator;
-            AVIF_CHECKRES(avifRWStreamWriteBits(s, metadata->gainMapMinD[c], 32)); // unsigned int(32) gain_map_min_denominator;
-            AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)metadata->gainMapMaxN[c], 32)); // int(32) gain_map_max_numerator;
-            AVIF_CHECKRES(avifRWStreamWriteBits(s, metadata->gainMapMaxD[c], 32));   // unsigned int(32) gain_map_max_denominator;
-            AVIF_CHECKRES(avifRWStreamWriteBits(s, metadata->gainMapGammaN[c], 32)); // unsigned int(32) gamma_numerator;
-            AVIF_CHECKRES(avifRWStreamWriteBits(s, metadata->gainMapGammaD[c], 32)); // unsigned int(32) gamma_denominator;
-            AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)metadata->baseOffsetN[c], 32)); // int(32) base_offset_numerator;
-            AVIF_CHECKRES(avifRWStreamWriteBits(s, metadata->baseOffsetD[c], 32)); // unsigned int(32) base_offset_denominator;
-            AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)metadata->alternateOffsetN[c], 32)); // int(32) alternate_offset_numerator;
-            AVIF_CHECKRES(avifRWStreamWriteBits(s, metadata->alternateOffsetD[c], 32)); // unsigned int(32) alternate_offset_denominator;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)gainMap->gainMapMin[c].n, 32)); // int(32) gain_map_min_numerator;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, gainMap->gainMapMin[c].d, 32)); // unsigned int(32) gain_map_min_denominator;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)gainMap->gainMapMax[c].n, 32)); // int(32) gain_map_max_numerator;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, gainMap->gainMapMax[c].d, 32));   // unsigned int(32) gain_map_max_denominator;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, gainMap->gainMapGamma[c].n, 32)); // unsigned int(32) gamma_numerator;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, gainMap->gainMapGamma[c].d, 32)); // unsigned int(32) gamma_denominator;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)gainMap->baseOffset[c].n, 32)); // int(32) base_offset_numerator;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, gainMap->baseOffset[c].d, 32)); // unsigned int(32) base_offset_denominator;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)gainMap->alternateOffset[c].n, 32)); // int(32) alternate_offset_numerator;
+            AVIF_CHECKRES(avifRWStreamWriteBits(s, gainMap->alternateOffset[c].d, 32)); // unsigned int(32) alternate_offset_denominator;
         }
     }
 
-    AVIF_ASSERT_OR_RETURN(avifRWStreamOffset(s) == offset + avifGainmapMetadataSize(metadata));
+    AVIF_ASSERT_OR_RETURN(avifRWStreamOffset(s) == offset + avifGainMapMetadataSize(gainMap));
     return AVIF_RESULT_OK;
 }
 
-static avifResult avifWriteToneMappedImagePayload(avifRWData * data, const avifGainMapMetadata * metadata, avifDiagnostics * diag)
+static avifResult avifWriteToneMappedImagePayload(avifRWData * data, const avifGainMap * gainMap, avifDiagnostics * diag)
 {
     avifRWStream s;
     avifRWStreamStart(&s, data);
@@ -967,7 +967,7 @@
     const uint8_t version = 0;
     AVIF_CHECKRES(avifRWStreamWriteU8(&s, version)); // unsigned int(8) version = 0;
     if (version == 0) {
-        AVIF_CHECKRES(avifWriteGainmapMetadata(&s, metadata, diag)); // GainMapMetadata;
+        AVIF_CHECKRES(avifWriteGainmapMetadata(&s, gainMap, diag)); // GainMapMetadata;
     }
     avifRWStreamFinishWrite(&s);
     return AVIF_RESULT_OK;
@@ -1688,26 +1688,24 @@
                 avifDiagnosticsPrintf(&encoder->diag, "all cells should have the same alternate image metadata in the gain map");
                 return AVIF_RESULT_INVALID_IMAGE_GRID;
             }
-            const avifGainMapMetadata * firstMetadata = &firstGainMap->metadata;
-            const avifGainMapMetadata * cellMetadata = &cellGainMap->metadata;
-            if (cellMetadata->baseHdrHeadroomN != firstMetadata->baseHdrHeadroomN ||
-                cellMetadata->baseHdrHeadroomD != firstMetadata->baseHdrHeadroomD ||
-                cellMetadata->alternateHdrHeadroomN != firstMetadata->alternateHdrHeadroomN ||
-                cellMetadata->alternateHdrHeadroomD != firstMetadata->alternateHdrHeadroomD) {
+            if (cellGainMap->baseHdrHeadroom.n != firstGainMap->baseHdrHeadroom.n ||
+                cellGainMap->baseHdrHeadroom.d != firstGainMap->baseHdrHeadroom.d ||
+                cellGainMap->alternateHdrHeadroom.n != firstGainMap->alternateHdrHeadroom.n ||
+                cellGainMap->alternateHdrHeadroom.d != firstGainMap->alternateHdrHeadroom.d) {
                 avifDiagnosticsPrintf(&encoder->diag, "all cells should have the same gain map metadata");
                 return AVIF_RESULT_INVALID_IMAGE_GRID;
             }
             for (int c = 0; c < 3; ++c) {
-                if (cellMetadata->gainMapMinN[c] != firstMetadata->gainMapMinN[c] ||
-                    cellMetadata->gainMapMinD[c] != firstMetadata->gainMapMinD[c] ||
-                    cellMetadata->gainMapMaxN[c] != firstMetadata->gainMapMaxN[c] ||
-                    cellMetadata->gainMapMaxD[c] != firstMetadata->gainMapMaxD[c] ||
-                    cellMetadata->gainMapGammaN[c] != firstMetadata->gainMapGammaN[c] ||
-                    cellMetadata->gainMapGammaD[c] != firstMetadata->gainMapGammaD[c] ||
-                    cellMetadata->baseOffsetN[c] != firstMetadata->baseOffsetN[c] ||
-                    cellMetadata->baseOffsetD[c] != firstMetadata->baseOffsetD[c] ||
-                    cellMetadata->alternateOffsetN[c] != firstMetadata->alternateOffsetN[c] ||
-                    cellMetadata->alternateOffsetD[c] != firstMetadata->alternateOffsetD[c]) {
+                if (cellGainMap->gainMapMin[c].n != firstGainMap->gainMapMin[c].n ||
+                    cellGainMap->gainMapMin[c].d != firstGainMap->gainMapMin[c].d ||
+                    cellGainMap->gainMapMax[c].n != firstGainMap->gainMapMax[c].n ||
+                    cellGainMap->gainMapMax[c].d != firstGainMap->gainMapMax[c].d ||
+                    cellGainMap->gainMapGamma[c].n != firstGainMap->gainMapGamma[c].n ||
+                    cellGainMap->gainMapGamma[c].d != firstGainMap->gainMapGamma[c].d ||
+                    cellGainMap->baseOffset[c].n != firstGainMap->baseOffset[c].n ||
+                    cellGainMap->baseOffset[c].d != firstGainMap->baseOffset[c].d ||
+                    cellGainMap->alternateOffset[c].n != firstGainMap->alternateOffset[c].n ||
+                    cellGainMap->alternateOffset[c].d != firstGainMap->alternateOffset[c].d) {
                     avifDiagnosticsPrintf(&encoder->diag, "all cells should have the same gain map metadata");
                     return AVIF_RESULT_INVALID_IMAGE_GRID;
                 }
@@ -1869,8 +1867,7 @@
                                                                          infeNameGainMap,
                                                                          /*infeNameSize=*/strlen(infeNameGainMap) + 1,
                                                                          /*cellIndex=*/0);
-            AVIF_CHECKRES(
-                avifWriteToneMappedImagePayload(&toneMappedItem->metadataPayload, &firstCell->gainMap->metadata, &encoder->diag));
+            AVIF_CHECKRES(avifWriteToneMappedImagePayload(&toneMappedItem->metadataPayload, firstCell->gainMap, &encoder->diag));
             // Even though the 'tmap' item is related to the gain map, it represents a color image and its metadata is more similar to the color item.
             toneMappedItem->itemCategory = AVIF_ITEM_COLOR;
             uint16_t toneMappedItemID = toneMappedItem->id;
diff --git a/tests/gtest/avif_fuzztest_enc_dec_experimental.cc b/tests/gtest/avif_fuzztest_enc_dec_experimental.cc
index cb40b65..718f53e 100644
--- a/tests/gtest/avif_fuzztest_enc_dec_experimental.cc
+++ b/tests/gtest/avif_fuzztest_enc_dec_experimental.cc
@@ -19,24 +19,24 @@
 
 ::testing::Environment* const kStackLimitEnv = SetStackLimitTo512x1024Bytes();
 
-void CheckGainMapMetadataMatches(const avifGainMapMetadata& actual,
-                                 const avifGainMapMetadata& expected) {
-  EXPECT_EQ(actual.baseHdrHeadroomN, expected.baseHdrHeadroomN);
-  EXPECT_EQ(actual.baseHdrHeadroomD, expected.baseHdrHeadroomD);
-  EXPECT_EQ(actual.alternateHdrHeadroomN, expected.alternateHdrHeadroomN);
-  EXPECT_EQ(actual.alternateHdrHeadroomD, expected.alternateHdrHeadroomD);
+void CheckGainMapMetadataMatches(const avifGainMap& actual,
+                                 const avifGainMap& expected) {
+  EXPECT_EQ(actual.baseHdrHeadroom.n, expected.baseHdrHeadroom.n);
+  EXPECT_EQ(actual.baseHdrHeadroom.d, expected.baseHdrHeadroom.d);
+  EXPECT_EQ(actual.alternateHdrHeadroom.n, expected.alternateHdrHeadroom.n);
+  EXPECT_EQ(actual.alternateHdrHeadroom.d, expected.alternateHdrHeadroom.d);
   for (int c = 0; c < 3; ++c) {
     SCOPED_TRACE(c);
-    EXPECT_EQ(actual.baseOffsetN[c], expected.baseOffsetN[c]);
-    EXPECT_EQ(actual.baseOffsetD[c], expected.baseOffsetD[c]);
-    EXPECT_EQ(actual.alternateOffsetN[c], expected.alternateOffsetN[c]);
-    EXPECT_EQ(actual.alternateOffsetD[c], expected.alternateOffsetD[c]);
-    EXPECT_EQ(actual.gainMapGammaN[c], expected.gainMapGammaN[c]);
-    EXPECT_EQ(actual.gainMapGammaD[c], expected.gainMapGammaD[c]);
-    EXPECT_EQ(actual.gainMapMinN[c], expected.gainMapMinN[c]);
-    EXPECT_EQ(actual.gainMapMinD[c], expected.gainMapMinD[c]);
-    EXPECT_EQ(actual.gainMapMaxN[c], expected.gainMapMaxN[c]);
-    EXPECT_EQ(actual.gainMapMaxD[c], expected.gainMapMaxD[c]);
+    EXPECT_EQ(actual.baseOffset[c].n, expected.baseOffset[c].n);
+    EXPECT_EQ(actual.baseOffset[c].d, expected.baseOffset[c].d);
+    EXPECT_EQ(actual.alternateOffset[c].n, expected.alternateOffset[c].n);
+    EXPECT_EQ(actual.alternateOffset[c].d, expected.alternateOffset[c].d);
+    EXPECT_EQ(actual.gainMapGamma[c].n, expected.gainMapGamma[c].n);
+    EXPECT_EQ(actual.gainMapGamma[c].d, expected.gainMapGamma[c].d);
+    EXPECT_EQ(actual.gainMapMin[c].n, expected.gainMapMin[c].n);
+    EXPECT_EQ(actual.gainMapMin[c].d, expected.gainMapMin[c].d);
+    EXPECT_EQ(actual.gainMapMax[c].n, expected.gainMapMax[c].n);
+    EXPECT_EQ(actual.gainMapMax[c].d, expected.gainMapMax[c].d);
   }
 }
 
@@ -84,8 +84,7 @@
     EXPECT_EQ(decoded_image->gainMap->image->alphaPlane, nullptr);
 
     if (decoder->enableParsingGainMapMetadata) {
-      CheckGainMapMetadataMatches(decoded_image->gainMap->metadata,
-                                  image->gainMap->metadata);
+      CheckGainMapMetadataMatches(*decoded_image->gainMap, *image->gainMap);
     }
   }
 
diff --git a/tests/gtest/avif_fuzztest_helpers.cc b/tests/gtest/avif_fuzztest_helpers.cc
index b42919a..c4fb021 100644
--- a/tests/gtest/avif_fuzztest_helpers.cc
+++ b/tests/gtest/avif_fuzztest_helpers.cc
@@ -189,22 +189,35 @@
     bool use_base_color_space) {
   image->gainMap = avifGainMapCreate();
   image->gainMap->image = gain_map.release();
-  image->gainMap->metadata = avifGainMapMetadata{
-      {gain_map_min_n0, gain_map_min_n1, gain_map_min_n2},
-      {gain_map_min_d0, gain_map_min_d1, gain_map_min_d2},
-      {gain_map_max_n0, gain_map_max_n1, gain_map_max_n2},
-      {gain_map_max_d0, gain_map_max_d1, gain_map_max_d2},
-      {gain_map_gamma_n0, gain_map_gamma_n1, gain_map_gamma_n2},
-      {gain_map_gamma_d0, gain_map_gamma_d1, gain_map_gamma_d2},
-      {base_offset_n0, base_offset_n1, base_offset_n2},
-      {base_offset_d0, base_offset_d1, base_offset_d2},
-      {alternate_offset_n0, alternate_offset_n1, alternate_offset_n2},
-      {alternate_offset_d0, alternate_offset_d1, alternate_offset_d2},
-      base_hdr_headroom_n,
-      base_hdr_headroom_d,
-      alternate_hdr_headroom_n,
-      alternate_hdr_headroom_d,
-      use_base_color_space};
+
+  image->gainMap->gainMapMin[0] = {gain_map_min_n0, gain_map_min_d0};
+  image->gainMap->gainMapMin[1] = {gain_map_min_n1, gain_map_min_d1};
+  image->gainMap->gainMapMin[2] = {gain_map_min_n2, gain_map_min_d2};
+
+  image->gainMap->gainMapMax[0] = {gain_map_max_n0, gain_map_max_d0};
+  image->gainMap->gainMapMax[1] = {gain_map_max_n1, gain_map_max_d1};
+  image->gainMap->gainMapMax[2] = {gain_map_max_n2, gain_map_max_d2};
+
+  image->gainMap->gainMapGamma[0] = {gain_map_gamma_n0, gain_map_gamma_d0};
+  image->gainMap->gainMapGamma[1] = {gain_map_gamma_n1, gain_map_gamma_d1};
+  image->gainMap->gainMapGamma[2] = {gain_map_gamma_n2, gain_map_gamma_d2};
+
+  image->gainMap->baseOffset[0] = {base_offset_n0, base_offset_d0};
+  image->gainMap->baseOffset[1] = {base_offset_n1, base_offset_d1};
+  image->gainMap->baseOffset[2] = {base_offset_n2, base_offset_d2};
+
+  image->gainMap->alternateOffset[0] = {alternate_offset_n0,
+                                        alternate_offset_d0};
+  image->gainMap->alternateOffset[1] = {alternate_offset_n1,
+                                        alternate_offset_d1};
+  image->gainMap->alternateOffset[2] = {alternate_offset_n2,
+                                        alternate_offset_d2};
+
+  image->gainMap->baseHdrHeadroom = {base_hdr_headroom_n, base_hdr_headroom_d};
+  image->gainMap->alternateHdrHeadroom = {alternate_hdr_headroom_n,
+                                          alternate_hdr_headroom_d};
+  image->gainMap->useBaseColorSpace = use_base_color_space;
+
   return image;
 }
 #endif
diff --git a/tests/gtest/avif_fuzztest_helpers.h b/tests/gtest/avif_fuzztest_helpers.h
index 6b678c3..6564bc9 100644
--- a/tests/gtest/avif_fuzztest_helpers.h
+++ b/tests/gtest/avif_fuzztest_helpers.h
@@ -171,9 +171,6 @@
 }
 
 #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
-// Note that avifGainMapMetadata is passed as many individual arguments
-// because the C array fields in the struct seem to prevent fuzztest from
-// handling it natively.
 // TODO: Try StructOf<Metadata>(StructOf<uint32_t[3]>())?
 ImagePtr AddGainMapToImage(
     ImagePtr image, ImagePtr gain_map, int32_t gain_map_min_n0,
@@ -206,8 +203,9 @@
       fuzztest::InRange<uint32_t>(1, std::numeric_limits<uint32_t>::max()),
       fuzztest::InRange<uint32_t>(1, std::numeric_limits<uint32_t>::max()),
       fuzztest::InRange<uint32_t>(1, std::numeric_limits<uint32_t>::max()),
-      fuzztest::Arbitrary<uint32_t>(), fuzztest::Arbitrary<uint32_t>(),
-      fuzztest::Arbitrary<uint32_t>(),
+      fuzztest::InRange<uint32_t>(1, std::numeric_limits<uint32_t>::max()),
+      fuzztest::InRange<uint32_t>(1, std::numeric_limits<uint32_t>::max()),
+      fuzztest::InRange<uint32_t>(1, std::numeric_limits<uint32_t>::max()),
       fuzztest::InRange<uint32_t>(1, std::numeric_limits<uint32_t>::max()),
       fuzztest::InRange<uint32_t>(1, std::numeric_limits<uint32_t>::max()),
       fuzztest::InRange<uint32_t>(1, std::numeric_limits<uint32_t>::max()),
diff --git a/tests/gtest/avifgainmaptest.cc b/tests/gtest/avifgainmaptest.cc
index 98c6436..1dadf96 100644
--- a/tests/gtest/avifgainmaptest.cc
+++ b/tests/gtest/avifgainmaptest.cc
@@ -23,51 +23,41 @@
 // Used to pass the data folder path to the GoogleTest suites.
 const char* data_path = nullptr;
 
-void CheckGainMapMetadataMatches(const avifGainMapMetadata& lhs,
-                                 const avifGainMapMetadata& rhs) {
-  EXPECT_EQ(lhs.baseHdrHeadroomN, rhs.baseHdrHeadroomN);
-  EXPECT_EQ(lhs.baseHdrHeadroomD, rhs.baseHdrHeadroomD);
-  EXPECT_EQ(lhs.alternateHdrHeadroomN, rhs.alternateHdrHeadroomN);
-  EXPECT_EQ(lhs.alternateHdrHeadroomD, rhs.alternateHdrHeadroomD);
+void CheckGainMapMetadataMatches(const avifGainMap& lhs,
+                                 const avifGainMap& rhs) {
+  EXPECT_EQ(lhs.baseHdrHeadroom.n, rhs.baseHdrHeadroom.n);
+  EXPECT_EQ(lhs.baseHdrHeadroom.d, rhs.baseHdrHeadroom.d);
+  EXPECT_EQ(lhs.alternateHdrHeadroom.n, rhs.alternateHdrHeadroom.n);
+  EXPECT_EQ(lhs.alternateHdrHeadroom.d, rhs.alternateHdrHeadroom.d);
   for (int c = 0; c < 3; ++c) {
     SCOPED_TRACE(c);
-    EXPECT_EQ(lhs.baseOffsetN[c], rhs.baseOffsetN[c]);
-    EXPECT_EQ(lhs.baseOffsetD[c], rhs.baseOffsetD[c]);
-    EXPECT_EQ(lhs.alternateOffsetN[c], rhs.alternateOffsetN[c]);
-    EXPECT_EQ(lhs.alternateOffsetD[c], rhs.alternateOffsetD[c]);
-    EXPECT_EQ(lhs.gainMapGammaN[c], rhs.gainMapGammaN[c]);
-    EXPECT_EQ(lhs.gainMapGammaD[c], rhs.gainMapGammaD[c]);
-    EXPECT_EQ(lhs.gainMapMinN[c], rhs.gainMapMinN[c]);
-    EXPECT_EQ(lhs.gainMapMinD[c], rhs.gainMapMinD[c]);
-    EXPECT_EQ(lhs.gainMapMaxN[c], rhs.gainMapMaxN[c]);
-    EXPECT_EQ(lhs.gainMapMaxD[c], rhs.gainMapMaxD[c]);
+    EXPECT_EQ(lhs.baseOffset[c].n, rhs.baseOffset[c].n);
+    EXPECT_EQ(lhs.baseOffset[c].d, rhs.baseOffset[c].d);
+    EXPECT_EQ(lhs.alternateOffset[c].n, rhs.alternateOffset[c].n);
+    EXPECT_EQ(lhs.alternateOffset[c].d, rhs.alternateOffset[c].d);
+    EXPECT_EQ(lhs.gainMapGamma[c].n, rhs.gainMapGamma[c].n);
+    EXPECT_EQ(lhs.gainMapGamma[c].d, rhs.gainMapGamma[c].d);
+    EXPECT_EQ(lhs.gainMapMin[c].n, rhs.gainMapMin[c].n);
+    EXPECT_EQ(lhs.gainMapMin[c].d, rhs.gainMapMin[c].d);
+    EXPECT_EQ(lhs.gainMapMax[c].n, rhs.gainMapMax[c].n);
+    EXPECT_EQ(lhs.gainMapMax[c].d, rhs.gainMapMax[c].d);
   }
 }
 
-avifGainMapMetadata GetTestGainMapMetadata(bool base_rendition_is_hdr) {
-  avifGainMapMetadata metadata = {};
-  metadata.useBaseColorSpace = true;
-  metadata.baseHdrHeadroomN = 0;
-  metadata.baseHdrHeadroomD = 1;
-  metadata.alternateHdrHeadroomN = 6;
-  metadata.alternateHdrHeadroomD = 2;
+void FillTestGainMapMetadata(bool base_rendition_is_hdr, avifGainMap* gainMap) {
+  gainMap->useBaseColorSpace = true;
+  gainMap->baseHdrHeadroom = {0, 1};
+  gainMap->alternateHdrHeadroom = {6, 2};
   if (base_rendition_is_hdr) {
-    std::swap(metadata.baseHdrHeadroomN, metadata.alternateHdrHeadroomN);
-    std::swap(metadata.baseHdrHeadroomD, metadata.alternateHdrHeadroomD);
+    std::swap(gainMap->baseHdrHeadroom, gainMap->alternateHdrHeadroom);
   }
   for (int c = 0; c < 3; ++c) {
-    metadata.baseOffsetN[c] = 10 * c;
-    metadata.baseOffsetD[c] = 1000;
-    metadata.alternateOffsetN[c] = 20 * c;
-    metadata.alternateOffsetD[c] = 1000;
-    metadata.gainMapGammaN[c] = 1;
-    metadata.gainMapGammaD[c] = c + 1;
-    metadata.gainMapMinN[c] = -1;
-    metadata.gainMapMinD[c] = c + 1;
-    metadata.gainMapMaxN[c] = 10 + c + 1;
-    metadata.gainMapMaxD[c] = c + 1;
+    gainMap->baseOffset[c] = {10 * c, 1000};
+    gainMap->alternateOffset[c] = {20 * c, 1000};
+    gainMap->gainMapGamma[c] = {1, static_cast<uint32_t>(c + 1)};
+    gainMap->gainMapMin[c] = {-1, static_cast<uint32_t>(c + 1)};
+    gainMap->gainMapMax[c] = {10 + c + 1, static_cast<uint32_t>(c + 1)};
   }
-  return metadata;
 }
 
 ImagePtr CreateTestImageWithGainMap(bool base_rendition_is_hdr) {
@@ -94,7 +84,7 @@
     return nullptr;
   }
   image->gainMap->image = gain_map.release();  // 'image' now owns the gain map.
-  image->gainMap->metadata = GetTestGainMapMetadata(base_rendition_is_hdr);
+  FillTestGainMapMetadata(base_rendition_is_hdr, image->gainMap);
 
   if (base_rendition_is_hdr) {
     image->clli.maxCLL = 10;
@@ -164,8 +154,7 @@
   EXPECT_EQ(decoded->gainMap->image->width, image->gainMap->image->width);
   EXPECT_EQ(decoded->gainMap->image->height, image->gainMap->image->height);
   EXPECT_EQ(decoded->gainMap->image->depth, image->gainMap->image->depth);
-  CheckGainMapMetadataMatches(decoded->gainMap->metadata,
-                              image->gainMap->metadata);
+  CheckGainMapMetadataMatches(*decoded->gainMap, *image->gainMap);
 
   // Decode the image.
   result = avifDecoderNextImage(decoder.get());
@@ -226,8 +215,7 @@
   EXPECT_EQ(decoded->gainMap->image->width, image->gainMap->image->width);
   EXPECT_EQ(decoded->gainMap->image->height, image->gainMap->image->height);
   EXPECT_EQ(decoded->gainMap->image->depth, image->gainMap->image->depth);
-  CheckGainMapMetadataMatches(decoded->gainMap->metadata,
-                              image->gainMap->metadata);
+  CheckGainMapMetadataMatches(*decoded->gainMap, *image->gainMap);
 
   // Uncomment the following to save the encoded image as an AVIF file.
   //  std::ofstream("/tmp/avifgainmaptest_basehdr.avif", std::ios::binary)
@@ -280,14 +268,14 @@
   ASSERT_NE(image, nullptr);
 
   const uint32_t kDenominator = 1000;
-  image->gainMap->metadata.baseHdrHeadroomD = kDenominator;
-  image->gainMap->metadata.alternateHdrHeadroomD = kDenominator;
+  image->gainMap->baseHdrHeadroom.d = kDenominator;
+  image->gainMap->alternateHdrHeadroom.d = kDenominator;
   for (int c = 0; c < 3; ++c) {
-    image->gainMap->metadata.baseOffsetD[c] = kDenominator;
-    image->gainMap->metadata.alternateOffsetD[c] = kDenominator;
-    image->gainMap->metadata.gainMapGammaD[c] = kDenominator;
-    image->gainMap->metadata.gainMapMinD[c] = kDenominator;
-    image->gainMap->metadata.gainMapMaxD[c] = kDenominator;
+    image->gainMap->baseOffset[c].d = kDenominator;
+    image->gainMap->alternateOffset[c].d = kDenominator;
+    image->gainMap->gainMapGamma[c].d = kDenominator;
+    image->gainMap->gainMapMin[c].d = kDenominator;
+    image->gainMap->gainMapMax[c].d = kDenominator;
   }
 
   EncoderPtr encoder(avifEncoderCreate());
@@ -309,8 +297,7 @@
       << avifResultToString(result) << " " << decoder->diag.error;
 
   // Verify that the gain map metadata matches the input.
-  CheckGainMapMetadataMatches(decoded->gainMap->metadata,
-                              image->gainMap->metadata);
+  CheckGainMapMetadataMatches(*decoded->gainMap, *image->gainMap);
 }
 
 TEST(GainMapTest, EncodeDecodeMetadataAllChannelsIdentical) {
@@ -318,16 +305,11 @@
   ASSERT_NE(image, nullptr);
 
   for (int c = 0; c < 3; ++c) {
-    image->gainMap->metadata.baseOffsetN[c] = 1;
-    image->gainMap->metadata.baseOffsetD[c] = 2;
-    image->gainMap->metadata.alternateOffsetN[c] = 3;
-    image->gainMap->metadata.alternateOffsetD[c] = 4;
-    image->gainMap->metadata.gainMapGammaN[c] = 5;
-    image->gainMap->metadata.gainMapGammaD[c] = 6;
-    image->gainMap->metadata.gainMapMinN[c] = 7;
-    image->gainMap->metadata.gainMapMinD[c] = 8;
-    image->gainMap->metadata.gainMapMaxN[c] = 9;
-    image->gainMap->metadata.gainMapMaxD[c] = 10;
+    image->gainMap->baseOffset[c] = {1, 2};
+    image->gainMap->alternateOffset[c] = {3, 4};
+    image->gainMap->gainMapGamma[c] = {5, 6};
+    image->gainMap->gainMapMin[c] = {7, 8};
+    image->gainMap->gainMapMax[c] = {9, 10};
   }
 
   EncoderPtr encoder(avifEncoderCreate());
@@ -349,8 +331,7 @@
       << avifResultToString(result) << " " << decoder->diag.error;
 
   // Verify that the gain map metadata matches the input.
-  CheckGainMapMetadataMatches(decoded->gainMap->metadata,
-                              image->gainMap->metadata);
+  CheckGainMapMetadataMatches(*decoded->gainMap, *image->gainMap);
 }
 
 TEST(GainMapTest, EncodeDecodeGrid) {
@@ -362,9 +343,6 @@
   constexpr int kCellWidth = 128;
   constexpr int kCellHeight = 200;
 
-  avifGainMapMetadata gain_map_metadata =
-      GetTestGainMapMetadata(/*base_rendition_is_hdr=*/true);
-
   for (int i = 0; i < kGridCols * kGridRows; ++i) {
     ImagePtr image =
         testutil::CreateImage(kCellWidth, kCellHeight, /*depth=*/10,
@@ -382,7 +360,7 @@
     ASSERT_NE(image->gainMap, nullptr);
     image->gainMap->image = gain_map.release();
     // all cells must have the same metadata
-    image->gainMap->metadata = gain_map_metadata;
+    FillTestGainMapMetadata(/*base_rendition_is_hdr=*/true, image->gainMap);
 
     cell_ptrs.push_back(image.get());
     gain_map_ptrs.push_back(image->gainMap->image);
@@ -435,7 +413,7 @@
   ASSERT_NE(decoded->gainMap->image, nullptr);
   ASSERT_GT(testutil::GetPsnr(*merged_gain_map, *decoded->gainMap->image),
             40.0);
-  CheckGainMapMetadataMatches(decoded->gainMap->metadata, gain_map_metadata);
+  CheckGainMapMetadataMatches(*decoded->gainMap, *cell_ptrs[0]->gainMap);
 
   // Check that non-incremental and incremental decodings of a grid AVIF produce
   // the same pixels.
@@ -457,9 +435,6 @@
   constexpr int kGridCols = 2;
   constexpr int kGridRows = 2;
 
-  avifGainMapMetadata gain_map_metadata =
-      GetTestGainMapMetadata(/*base_rendition_is_hdr=*/true);
-
   for (int i = 0; i < kGridCols * kGridRows; ++i) {
     ImagePtr image =
         testutil::CreateImage(/*width=*/64, /*height=*/100, /*depth=*/10,
@@ -477,7 +452,7 @@
     ASSERT_NE(image->gainMap, nullptr);
     image->gainMap->image = gain_map.release();
     // all cells must have the same metadata
-    image->gainMap->metadata = gain_map_metadata;
+    FillTestGainMapMetadata(/*base_rendition_is_hdr=*/true, image->gainMap);
 
     cell_ptrs.push_back(image.get());
     cells.push_back(std::move(image));
@@ -508,15 +483,15 @@
       << avifResultToString(result) << " " << encoder->diag.error;
   cells[1]->gainMap->image->depth = cells[0]->gainMap->image->depth;  // Revert.
 
-  // Invalid: one cell has different gain map metadata.
-  cells[1]->gainMap->metadata.gainMapGammaN[0] = 42;
+  // Invalid: one cell has different gain map metadata
+  cells[1]->gainMap->gainMapGamma[0].n = 42;
   result =
       avifEncoderAddImageGrid(encoder.get(), kGridCols, kGridRows,
                               cell_ptrs.data(), AVIF_ADD_IMAGE_FLAG_SINGLE);
   EXPECT_EQ(result, AVIF_RESULT_INVALID_IMAGE_GRID)
       << avifResultToString(result) << " " << encoder->diag.error;
-  cells[1]->gainMap->metadata.gainMapGammaN[0] =
-      cells[0]->gainMap->metadata.gainMapGammaN[0];  // Revert.
+  cells[1]->gainMap->gainMapGamma[0].n =
+      cells[0]->gainMap->gainMapGamma[0].n;  // Revert.
 }
 
 TEST(GainMapTest, SequenceNotSupported) {
@@ -598,7 +573,7 @@
   ASSERT_NE(decoded, nullptr);
   DecoderPtr decoder(avifDecoderCreate());
   ASSERT_NE(decoder, nullptr);
-  decoder->enableParsingGainMapMetadata = AVIF_TRUE;  // Read gain map metadata.
+  decoder->enableParsingGainMapMetadata = AVIF_TRUE;  // Read gain map metadata
   result = avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
                                  encoded.size);
   ASSERT_EQ(result, AVIF_RESULT_OK)
@@ -612,8 +587,7 @@
   // ... but not decoded because enableDecodingGainMap is false by default.
   EXPECT_EQ(decoded->gainMap->image, nullptr);
   // Check that the gain map metadata WAS populated.
-  CheckGainMapMetadataMatches(decoded->gainMap->metadata,
-                              image->gainMap->metadata);
+  CheckGainMapMetadataMatches(*decoded->gainMap, *image->gainMap);
   EXPECT_EQ(decoded->gainMap->altDepth, image->gainMap->altDepth);
   EXPECT_EQ(decoded->gainMap->altPlaneCount, image->gainMap->altPlaneCount);
   EXPECT_EQ(decoded->gainMap->altColorPrimaries,
@@ -688,8 +662,7 @@
   ASSERT_NE(decoded->gainMap->image, nullptr);
   EXPECT_GT(testutil::GetPsnr(*image->gainMap->image, *decoded->gainMap->image),
             40.0);
-  CheckGainMapMetadataMatches(decoded->gainMap->metadata,
-                              image->gainMap->metadata);
+  CheckGainMapMetadataMatches(*decoded->gainMap, *image->gainMap);
 }
 
 TEST(GainMapTest, IgnoreAll) {
@@ -708,7 +681,7 @@
   // Ignore both the main image and the gain map.
   decoder->ignoreColorAndAlpha = AVIF_TRUE;
   decoder->enableDecodingGainMap = AVIF_FALSE;
-  // But do read the gain map metadata.
+  // But do read the gain map metadata
   decoder->enableParsingGainMapMetadata = AVIF_TRUE;
 
   // Parsing just the header should work.
@@ -717,8 +690,7 @@
   ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK);
 
   EXPECT_TRUE(decoder->gainMapPresent);
-  CheckGainMapMetadataMatches(decoder->image->gainMap->metadata,
-                              image->gainMap->metadata);
+  CheckGainMapMetadataMatches(*decoder->image->gainMap, *image->gainMap);
   ASSERT_EQ(decoder->image->gainMap->image, nullptr);
 
   // But trying to access the next image should give an error because both
@@ -791,8 +763,8 @@
   EXPECT_EQ(decoded->gainMap->image->width, 64u * 2u);
   EXPECT_EQ(decoded->gainMap->image->height, 80u * 2u);
   EXPECT_EQ(decoded->gainMap->image->depth, 8u);
-  EXPECT_EQ(decoded->gainMap->metadata.baseHdrHeadroomN, 6u);
-  EXPECT_EQ(decoded->gainMap->metadata.baseHdrHeadroomD, 2u);
+  EXPECT_EQ(decoded->gainMap->baseHdrHeadroom.n, 6u);
+  EXPECT_EQ(decoded->gainMap->baseHdrHeadroom.d, 2u);
 
   // Decode the image.
   result = avifDecoderNextImage(decoder.get());
@@ -820,8 +792,8 @@
   // Gain map: single image of size 64x80.
   EXPECT_EQ(decoded->gainMap->image->width, 64u);
   EXPECT_EQ(decoded->gainMap->image->height, 80u);
-  EXPECT_EQ(decoded->gainMap->metadata.baseHdrHeadroomN, 6u);
-  EXPECT_EQ(decoded->gainMap->metadata.baseHdrHeadroomD, 2u);
+  EXPECT_EQ(decoded->gainMap->baseHdrHeadroom.n, 6u);
+  EXPECT_EQ(decoded->gainMap->baseHdrHeadroom.d, 2u);
 }
 
 TEST(GainMapTest, DecodeColorNoGridGainMapGrid) {
@@ -844,8 +816,8 @@
   // Gain map: 2x2 grid of 64x80 tiles.
   EXPECT_EQ(decoded->gainMap->image->width, 64u * 2u);
   EXPECT_EQ(decoded->gainMap->image->height, 80u * 2u);
-  EXPECT_EQ(decoded->gainMap->metadata.baseHdrHeadroomN, 6u);
-  EXPECT_EQ(decoded->gainMap->metadata.baseHdrHeadroomD, 2u);
+  EXPECT_EQ(decoded->gainMap->baseHdrHeadroom.n, 6u);
+  EXPECT_EQ(decoded->gainMap->baseHdrHeadroom.d, 2u);
 }
 
 TEST(GainMapTest, DecodeUnsupportedVersion) {
@@ -958,97 +930,14 @@
   EXPECT_NEAR(std::abs((double)numerator / denominator), expected, \
               expected * 0.001);
 
-TEST(GainMapTest, ConvertMetadata) {
-  avifGainMapMetadataDouble metadata_double = {};
-  metadata_double.gainMapMin[0] = 1.0;
-  metadata_double.gainMapMin[1] = 1.1;
-  metadata_double.gainMapMin[2] = 1.2;
-  metadata_double.gainMapMax[0] = 10.0;
-  metadata_double.gainMapMax[1] = 10.1;
-  metadata_double.gainMapMax[2] = 10.2;
-  metadata_double.gainMapGamma[0] = 1.0;
-  metadata_double.gainMapGamma[1] = 1.0;
-  metadata_double.gainMapGamma[2] = 1.2;
-  metadata_double.baseOffset[0] = 1.0 / 32.0;
-  metadata_double.baseOffset[1] = 1.0 / 64.0;
-  metadata_double.baseOffset[2] = 1.0 / 128.0;
-  metadata_double.alternateOffset[0] = 0.004564;
-  metadata_double.alternateOffset[1] = 0.0;
-  metadata_double.baseHdrHeadroom = 1.0;
-  metadata_double.alternateHdrHeadroom = 10.0;
-
-  // Convert to avifGainMapMetadata.
-  avifGainMapMetadata metadata = {};
-  ASSERT_TRUE(
-      avifGainMapMetadataDoubleToFractions(&metadata, &metadata_double));
-
-  for (int i = 0; i < 3; ++i) {
-    EXPECT_FRACTION_NEAR(metadata.gainMapMinN[i], metadata.gainMapMinD[i],
-                         metadata_double.gainMapMin[i]);
-    EXPECT_FRACTION_NEAR(metadata.gainMapMaxN[i], metadata.gainMapMaxD[i],
-                         metadata_double.gainMapMax[i]);
-    EXPECT_FRACTION_NEAR(metadata.gainMapGammaN[i], metadata.gainMapGammaD[i],
-                         metadata_double.gainMapGamma[i]);
-    EXPECT_FRACTION_NEAR(metadata.baseOffsetN[i], metadata.baseOffsetD[i],
-                         metadata_double.baseOffset[i]);
-    EXPECT_FRACTION_NEAR(metadata.alternateOffsetN[i],
-                         metadata.alternateOffsetD[i],
-                         metadata_double.alternateOffset[i]);
-  }
-  EXPECT_FRACTION_NEAR(metadata.baseHdrHeadroomN, metadata.baseHdrHeadroomD,
-                       metadata_double.baseHdrHeadroom);
-  EXPECT_FRACTION_NEAR(metadata.alternateHdrHeadroomN,
-                       metadata.alternateHdrHeadroomD,
-                       metadata_double.alternateHdrHeadroom);
-
-  // Convert back to avifGainMapMetadataDouble.
-  avifGainMapMetadataDouble metadata_double2 = {};
-  ASSERT_TRUE(
-      avifGainMapMetadataFractionsToDouble(&metadata_double2, &metadata));
-
-  constexpr double kEpsilon = 0.000001;
-  for (int i = 0; i < 3; ++i) {
-    EXPECT_NEAR(metadata_double2.gainMapMin[i], metadata_double.gainMapMin[i],
-                kEpsilon);
-    EXPECT_NEAR(metadata_double2.gainMapMax[i], metadata_double.gainMapMax[i],
-                kEpsilon);
-    EXPECT_NEAR(metadata_double2.gainMapGamma[i],
-                metadata_double.gainMapGamma[i], kEpsilon);
-    EXPECT_NEAR(metadata_double2.baseOffset[i], metadata_double.baseOffset[i],
-                kEpsilon);
-    EXPECT_NEAR(metadata_double2.alternateOffset[i],
-                metadata_double.alternateOffset[i], kEpsilon);
-  }
-  EXPECT_NEAR(metadata_double2.baseHdrHeadroom, metadata_double.baseHdrHeadroom,
-              kEpsilon);
-  EXPECT_NEAR(metadata_double2.alternateHdrHeadroom,
-              metadata_double.alternateHdrHeadroom, kEpsilon);
-}
-
-TEST(GainMapTest, ConvertMetadataToFractionInvalid) {
-  avifGainMapMetadataDouble metadata_double = {};
-  metadata_double.gainMapGamma[0] = -42;  // A negative value is invalid!
-  avifGainMapMetadata metadata = {};
-  ASSERT_FALSE(
-      avifGainMapMetadataDoubleToFractions(&metadata, &metadata_double));
-}
-
-TEST(GainMapTest, ConvertMetadataToDoubleInvalid) {
-  avifGainMapMetadata metadata = {};  // Denominators are zero.
-  avifGainMapMetadataDouble metadata_double = {};
-  ASSERT_FALSE(
-      avifGainMapMetadataFractionsToDouble(&metadata_double, &metadata));
-}
-
 static void SwapBaseAndAlternate(const avifImage& new_alternate,
                                  avifGainMap& gain_map) {
-  avifGainMapMetadata& metadata = gain_map.metadata;
-  metadata.useBaseColorSpace = !metadata.useBaseColorSpace;
-  std::swap(metadata.baseHdrHeadroomN, metadata.alternateHdrHeadroomN);
-  std::swap(metadata.baseHdrHeadroomD, metadata.alternateHdrHeadroomD);
+  gain_map.useBaseColorSpace = !gain_map.useBaseColorSpace;
+  std::swap(gain_map.baseHdrHeadroom.n, gain_map.alternateHdrHeadroom.n);
+  std::swap(gain_map.baseHdrHeadroom.d, gain_map.alternateHdrHeadroom.d);
   for (int c = 0; c < 3; ++c) {
-    std::swap(metadata.baseOffsetN[c], metadata.alternateOffsetN[c]);
-    std::swap(metadata.baseOffsetD[c], metadata.alternateOffsetD[c]);
+    std::swap(gain_map.baseOffset[c].n, gain_map.alternateOffset[c].n);
+    std::swap(gain_map.baseOffset[c].d, gain_map.alternateOffset[c].d);
   }
   gain_map.altColorPrimaries = new_alternate.colorPrimaries;
   gain_map.altTransferCharacteristics = new_alternate.transferCharacteristics;
@@ -1383,13 +1272,10 @@
   ASSERT_NE(image->gainMap->image, nullptr);
 
   // Force the alternate and base HDR headroom to the same value.
-  image->gainMap->metadata.baseHdrHeadroomN =
-      image->gainMap->metadata.alternateHdrHeadroomN;
-  image->gainMap->metadata.baseHdrHeadroomD =
-      image->gainMap->metadata.alternateHdrHeadroomD;
-  const float headroom = static_cast<float>(
-      static_cast<float>(image->gainMap->metadata.baseHdrHeadroomN) /
-      image->gainMap->metadata.baseHdrHeadroomD);
+  image->gainMap->baseHdrHeadroom = image->gainMap->alternateHdrHeadroom;
+  const float headroom =
+      static_cast<float>(static_cast<float>(image->gainMap->baseHdrHeadroom.n) /
+                         image->gainMap->baseHdrHeadroom.d);
 
   // Check that when the two headrooms are the same, the gain map is not applied
   // whatever the target headroom is.
@@ -1436,7 +1322,7 @@
                                     gain_map_depth, gain_map_format);
 
   avifDiagnostics diag;
-  gain_map->metadata.useBaseColorSpace = true;
+  gain_map->useBaseColorSpace = true;
   avifResult result = avifImageComputeGainMap(image1.get(), image2.get(),
                                               gain_map.get(), &diag);
   ASSERT_EQ(result, AVIF_RESULT_OK)
@@ -1445,11 +1331,10 @@
   EXPECT_EQ(gain_map->image->width, gain_map_width);
   EXPECT_EQ(gain_map->image->height, gain_map_height);
 
-  const float image1_headroom = (float)gain_map->metadata.baseHdrHeadroomN /
-                                gain_map->metadata.baseHdrHeadroomD;
-  const float image2_headroom =
-      (float)gain_map->metadata.alternateHdrHeadroomN /
-      gain_map->metadata.alternateHdrHeadroomD;
+  const float image1_headroom =
+      (float)gain_map->baseHdrHeadroom.n / gain_map->baseHdrHeadroom.d;
+  const float image2_headroom = (float)gain_map->alternateHdrHeadroom.n /
+                                gain_map->alternateHdrHeadroom.d;
 
   // Tone map from image1 to image2 by applying the gainmap forward.
   float psnr_image1_to_image2_forward;
@@ -1471,14 +1356,14 @@
   //                                  "/tmp/gain_map_image1_to_image2.png"));
 
   // Compute the gain map in the other direction (from image2 to image1).
-  gain_map->metadata.useBaseColorSpace = false;
+  gain_map->useBaseColorSpace = false;
   result = avifImageComputeGainMap(image2.get(), image1.get(), gain_map.get(),
                                    &diag);
   ASSERT_EQ(result, AVIF_RESULT_OK)
       << avifResultToString(result) << " " << diag.error;
 
-  const float image2_headroom2 = (float)gain_map->metadata.baseHdrHeadroomN /
-                                 gain_map->metadata.baseHdrHeadroomD;
+  const float image2_headroom2 =
+      (float)gain_map->baseHdrHeadroom.n / gain_map->baseHdrHeadroom.d;
   EXPECT_NEAR(image2_headroom2, image2_headroom, 0.001);
 
   // Tone map from image2 to image1 by applying the new gainmap forward.
diff --git a/tests/gtest/avifjpeggainmaptest.cc b/tests/gtest/avifjpeggainmaptest.cc
index b11ee52..5f729f9 100644
--- a/tests/gtest/avifjpeggainmaptest.cc
+++ b/tests/gtest/avifjpeggainmaptest.cc
@@ -17,55 +17,55 @@
 //------------------------------------------------------------------------------
 
 void CheckGainMapMetadata(
-    const avifGainMapMetadata& m, std::array<double, 3> gain_map_min,
+    const avifGainMap& gm, std::array<double, 3> gain_map_min,
     std::array<double, 3> gain_map_max, std::array<double, 3> gain_map_gamma,
     std::array<double, 3> base_offset, std::array<double, 3> alternate_offset,
     double base_hdr_headroom, double alternate_hdr_headroom) {
   const double kEpsilon = 1e-8;
 
-  EXPECT_NEAR(static_cast<double>(m.gainMapMinN[0]) / m.gainMapMinD[0],
+  EXPECT_NEAR(static_cast<double>(gm.gainMapMin[0].n) / gm.gainMapMin[0].d,
               gain_map_min[0], kEpsilon);
-  EXPECT_NEAR(static_cast<double>(m.gainMapMinN[1]) / m.gainMapMinD[1],
+  EXPECT_NEAR(static_cast<double>(gm.gainMapMin[1].n) / gm.gainMapMin[1].d,
               gain_map_min[1], kEpsilon);
-  EXPECT_NEAR(static_cast<double>(m.gainMapMinN[2]) / m.gainMapMinD[2],
+  EXPECT_NEAR(static_cast<double>(gm.gainMapMin[2].n) / gm.gainMapMin[2].d,
               gain_map_min[2], kEpsilon);
 
-  EXPECT_NEAR(static_cast<double>(m.gainMapMaxN[0]) / m.gainMapMaxD[0],
+  EXPECT_NEAR(static_cast<double>(gm.gainMapMax[0].n) / gm.gainMapMax[0].d,
               gain_map_max[0], kEpsilon);
-  EXPECT_NEAR(static_cast<double>(m.gainMapMaxN[1]) / m.gainMapMaxD[1],
+  EXPECT_NEAR(static_cast<double>(gm.gainMapMax[1].n) / gm.gainMapMax[1].d,
               gain_map_max[1], kEpsilon);
-  EXPECT_NEAR(static_cast<double>(m.gainMapMaxN[2]) / m.gainMapMaxD[2],
+  EXPECT_NEAR(static_cast<double>(gm.gainMapMax[2].n) / gm.gainMapMax[2].d,
               gain_map_max[2], kEpsilon);
 
-  EXPECT_NEAR(static_cast<double>(m.gainMapGammaN[0]) / m.gainMapGammaD[0],
+  EXPECT_NEAR(static_cast<double>(gm.gainMapGamma[0].n) / gm.gainMapGamma[0].d,
               gain_map_gamma[0], kEpsilon);
-  EXPECT_NEAR(static_cast<double>(m.gainMapGammaN[1]) / m.gainMapGammaD[1],
+  EXPECT_NEAR(static_cast<double>(gm.gainMapGamma[1].n) / gm.gainMapGamma[1].d,
               gain_map_gamma[1], kEpsilon);
-  EXPECT_NEAR(static_cast<double>(m.gainMapGammaN[2]) / m.gainMapGammaD[2],
+  EXPECT_NEAR(static_cast<double>(gm.gainMapGamma[2].n) / gm.gainMapGamma[2].d,
               gain_map_gamma[2], kEpsilon);
 
-  EXPECT_NEAR(static_cast<double>(m.baseOffsetN[0]) / m.baseOffsetD[0],
+  EXPECT_NEAR(static_cast<double>(gm.baseOffset[0].n) / gm.baseOffset[0].d,
               base_offset[0], kEpsilon);
-  EXPECT_NEAR(static_cast<double>(m.baseOffsetN[1]) / m.baseOffsetD[1],
+  EXPECT_NEAR(static_cast<double>(gm.baseOffset[1].n) / gm.baseOffset[1].d,
               base_offset[1], kEpsilon);
-  EXPECT_NEAR(static_cast<double>(m.baseOffsetN[2]) / m.baseOffsetD[2],
+  EXPECT_NEAR(static_cast<double>(gm.baseOffset[2].n) / gm.baseOffset[2].d,
               base_offset[2], kEpsilon);
 
   EXPECT_NEAR(
-      static_cast<double>(m.alternateOffsetN[0]) / m.alternateOffsetD[0],
+      static_cast<double>(gm.alternateOffset[0].n) / gm.alternateOffset[0].d,
       alternate_offset[0], kEpsilon);
   EXPECT_NEAR(
-      static_cast<double>(m.alternateOffsetN[1]) / m.alternateOffsetD[1],
+      static_cast<double>(gm.alternateOffset[1].n) / gm.alternateOffset[1].d,
       alternate_offset[1], kEpsilon);
   EXPECT_NEAR(
-      static_cast<double>(m.alternateOffsetN[2]) / m.alternateOffsetD[2],
+      static_cast<double>(gm.alternateOffset[2].n) / gm.alternateOffset[2].d,
       alternate_offset[2], kEpsilon);
 
-  EXPECT_NEAR(static_cast<double>(m.baseHdrHeadroomN) / m.baseHdrHeadroomD,
+  EXPECT_NEAR(static_cast<double>(gm.baseHdrHeadroom.n) / gm.baseHdrHeadroom.d,
               base_hdr_headroom, kEpsilon);
-  EXPECT_NEAR(
-      static_cast<double>(m.alternateHdrHeadroomN) / m.alternateHdrHeadroomD,
-      alternate_hdr_headroom, kEpsilon);
+  EXPECT_NEAR(static_cast<double>(gm.alternateHdrHeadroom.n) /
+                  gm.alternateHdrHeadroom.d,
+              alternate_hdr_headroom, kEpsilon);
 }
 
 TEST(JpegTest, ReadJpegWithGainMap) {
@@ -88,7 +88,7 @@
     // be read to parse the gain map.
     EXPECT_EQ(image->xmp.size, 0u);
 
-    CheckGainMapMetadata(image->gainMap->metadata,
+    CheckGainMapMetadata(*image->gainMap,
                          /*gain_map_min=*/{0.0, 0.0, 0.0},
                          /*gain_map_max=*/{3.5, 3.6, 3.7},
                          /*gain_map_gamma=*/{1.0, 1.0, 1.0},
@@ -156,11 +156,11 @@
 </x:xmpmeta>
 <?xpacket end="w"?>
   )";
-  avifGainMapMetadata metadata;
+  GainMapPtr gainMap(avifGainMapCreate());
   ASSERT_TRUE(avifJPEGParseGainMapXMP((const uint8_t*)xmp.data(), xmp.size(),
-                                      &metadata));
+                                      gainMap.get()));
 
-  CheckGainMapMetadata(metadata,
+  CheckGainMapMetadata(*gainMap,
                        /*gain_map_min=*/{0.025869, 0.075191, 0.142298},
                        /*gain_map_max=*/{3.527605, 2.830234, 1.537243},
                        /*gain_map_gamma=*/{0.506828, 0.590032, 1.517708},
@@ -181,12 +181,12 @@
 </x:xmpmeta>
 <?xpacket end="w"?>
   )";
-  avifGainMapMetadata metadata;
+  GainMapPtr gainMap(avifGainMapCreate());
   ASSERT_TRUE(avifJPEGParseGainMapXMP((const uint8_t*)xmp.data(), xmp.size(),
-                                      &metadata));
+                                      gainMap.get()));
 
   CheckGainMapMetadata(
-      metadata,
+      *gainMap,
       /*gain_map_min=*/{0.0, 0.0, 0.0},
       /*gain_map_max=*/{1.0, 1.0, 1.0},
       /*gain_map_gamma=*/{1.0, 1.0, 1.0},
@@ -220,15 +220,15 @@
   </rdf:RDF>
 </x:xmpmeta>
   )";
-  avifGainMapMetadata metadata;
+  GainMapPtr gainMap(avifGainMapCreate());
   ASSERT_TRUE(avifJPEGParseGainMapXMP((const uint8_t*)xmp.data(), xmp.size(),
-                                      &metadata));
+                                      gainMap.get()));
 
   // Note that this test passes because the gain map metadata is in the primary
   // XMP. If it was in the extended part, we wouldn't detect it (but probably
   // should).
   CheckGainMapMetadata(
-      metadata,
+      *gainMap,
       /*gain_map_min=*/{0.0, 0.0, 0.0},
       /*gain_map_max=*/{1.0, 1.0, 1.0},
       /*gain_map_gamma=*/{1.0, 1.0, 1.0},
@@ -258,9 +258,9 @@
   </rdf:RDF>
 </x:xmpmeta>
   )";
-  avifGainMapMetadata metadata;
+  GainMapPtr gainMap(avifGainMapCreate());
   EXPECT_FALSE(avifJPEGParseGainMapXMP((const uint8_t*)xmp.data(), xmp.size(),
-                                       &metadata));
+                                       gainMap.get()));
 }
 
 TEST(JpegTest, WrongVersion) {
@@ -273,9 +273,9 @@
   </rdf:RDF>
 </x:xmpmeta>
   )";
-  avifGainMapMetadata metadata;
+  GainMapPtr gainMap(avifGainMapCreate());
   EXPECT_FALSE(avifJPEGParseGainMapXMP((const uint8_t*)xmp.data(), xmp.size(),
-                                       &metadata));
+                                       gainMap.get()));
 }
 
 TEST(JpegTest, InvalidXMP) {
@@ -287,16 +287,16 @@
   </rdf:RDF>
 </x:xmpmeta>
   )";
-  avifGainMapMetadata metadata;
+  GainMapPtr gainMap(avifGainMapCreate());
   EXPECT_FALSE(avifJPEGParseGainMapXMP((const uint8_t*)xmp.data(), xmp.size(),
-                                       &metadata));
+                                       gainMap.get()));
 }
 
 TEST(JpegTest, EmptyXMP) {
   const std::string xmp = "";
-  avifGainMapMetadata metadata;
+  GainMapPtr gainMap(avifGainMapCreate());
   EXPECT_FALSE(avifJPEGParseGainMapXMP((const uint8_t*)xmp.data(), xmp.size(),
-                                       &metadata));
+                                       gainMap.get()));
 }
 
 //------------------------------------------------------------------------------
diff --git a/tests/gtest/avifutilstest.cc b/tests/gtest/avifutilstest.cc
index b5da9cd..fb464e7 100644
--- a/tests/gtest/avifutilstest.cc
+++ b/tests/gtest/avifutilstest.cc
@@ -10,33 +10,31 @@
 namespace {
 
 // Converts a double value to a fraction, and checks that the difference
-// between numerator/denominator and v is below relative_tolerance.
+// between fraction.n/fraction.d and v is below relative_tolerance.
 void TestRoundTrip(double v, double relative_tolerance) {
   // Unsigned.
   if (v >= 0) {
-    uint32_t numerator, denominator;
-    ASSERT_TRUE(avifDoubleToUnsignedFraction(v, &numerator, &denominator)) << v;
-    const double reconstructed = (double)numerator / denominator;
+    avifUnsignedFraction fraction;
+    ASSERT_TRUE(avifDoubleToUnsignedFraction(v, &fraction)) << v;
+    const double reconstructed = (double)fraction.n / fraction.d;
     const double tolerance = v * relative_tolerance;
     EXPECT_NEAR(reconstructed, v, tolerance)
-        << "numerator " << (double)numerator << " denominator "
-        << (double)denominator;
+        << "fraction.n " << (double)fraction.n << " fraction.d "
+        << (double)fraction.d;
   }
 
   // Signed.
   if (v <= INT32_MAX) {
     for (double multiplier : {1.0, -1.0}) {
       double v2 = v * multiplier;
-      int32_t numerator;
-      uint32_t denominator;
+      avifSignedFraction fraction;
 
-      ASSERT_TRUE(avifDoubleToSignedFraction(v2, &numerator, &denominator))
-          << v2;
-      const double reconstructed = (double)numerator / denominator;
+      ASSERT_TRUE(avifDoubleToSignedFraction(v2, &fraction)) << v2;
+      const double reconstructed = (double)fraction.n / fraction.d;
       const double tolerance = v * relative_tolerance;
       EXPECT_NEAR(reconstructed, v2, tolerance)
-          << "numerator " << (double)numerator << " denominator "
-          << (double)denominator;
+          << "fraction.n " << (double)fraction.n << " fraction.d "
+          << (double)fraction.d;
     }
   }
 }
@@ -89,9 +87,9 @@
   double max_relative_error_v = 0;
   for (uint64_t i = 0; i < UINT32_MAX; i += 1000) {
     const double v = i + kLotsOfDecimals;
-    uint32_t numerator, denominator;
-    ASSERT_TRUE(avifDoubleToUnsignedFraction(v, &numerator, &denominator)) << v;
-    const double reconstructed = (double)numerator / denominator;
+    avifUnsignedFraction fraction;
+    ASSERT_TRUE(avifDoubleToUnsignedFraction(v, &fraction)) << v;
+    const double reconstructed = (double)fraction.n / fraction.d;
     const double error = abs(reconstructed - v);
     const double relative_error = error / v;
     if (error > max_error) {
@@ -116,9 +114,9 @@
   double max_relative_error_v = 0;
   for (uint64_t i = 1; i < UINT32_MAX; i += 1000) {
     const double v = 1.0 / (i + kLotsOfDecimals);
-    uint32_t numerator, denominator;
-    ASSERT_TRUE(avifDoubleToUnsignedFraction(v, &numerator, &denominator)) << v;
-    const double reconstructed = (double)numerator / denominator;
+    avifUnsignedFraction fraction;
+    ASSERT_TRUE(avifDoubleToUnsignedFraction(v, &fraction)) << v;
+    const double reconstructed = (double)fraction.n / fraction.d;
     const double error = abs(reconstructed - v);
     const double relative_error = error / v;
     if (error > max_error) {
@@ -135,12 +133,12 @@
 }
 
 TEST(ToFractionTest, BadValues) {
-  uint32_t numerator, denominator;
+  avifUnsignedFraction fraction;
   // Negative value.
-  EXPECT_FALSE(avifDoubleToUnsignedFraction(-0.1, &numerator, &denominator));
+  EXPECT_FALSE(avifDoubleToUnsignedFraction(-0.1, &fraction));
   // Too large.
-  EXPECT_FALSE(avifDoubleToUnsignedFraction(((double)UINT32_MAX) + 1.0,
-                                            &numerator, &denominator));
+  EXPECT_FALSE(
+      avifDoubleToUnsignedFraction(((double)UINT32_MAX) + 1.0, &fraction));
 }
 
 }  // namespace