Add --grid option to avifgainmaputil (#2963)

Fixes #2865
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 635fd53..ef463df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@
   the other chunks are ignored as per the PNG Specification Third Edition
   Section 4.3.
 * Support Sample Transform derived image items with grid input image items.
+* Add --grid option to avifgainmaputil.
 
 ### Changed since 1.3.0
 
@@ -53,6 +54,8 @@
 * Set tuning before applying the user-provided specific aom codec options.
 * Use AOM_TUNE_PSNR by default when encoding alpha with libaom because
   AOM_TUNE_SSIM causes ringing for alpha.
+* Converting an image containing a gain map using avifenc with the --grid flag
+  now also splits the gain map into a grid.
 
 ### Removed since 1.3.0
 
diff --git a/apps/avifenc.c b/apps/avifenc.c
index 1dfee92..5d7aedb 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -778,125 +778,6 @@
     return AVIF_TRUE;
 }
 
-// Returns the best cell size for a given horizontal or vertical dimension.
-static avifBool avifGetBestCellSize(const char * dimensionStr, uint32_t numPixels, uint32_t numCells, avifBool isSubsampled, uint32_t * cellSize)
-{
-    assert(numPixels);
-    assert(numCells);
-
-    // ISO/IEC 23008-12:2017, Section 6.6.2.3.1:
-    //   The reconstructed image is formed by tiling the input images into a grid with a column width
-    //   (potentially excluding the right-most column) equal to tile_width and a row height (potentially
-    //   excluding the bottom-most row) equal to tile_height, without gap or overlap, and then
-    //   trimming on the right and the bottom to the indicated output_width and output_height.
-    // The priority could be to use a cell size that is a multiple of 64, but there is not always a valid one,
-    // even though it is recommended by MIAF. Just use ceil(numPixels/numCells) for simplicity and to avoid
-    // as much padding in the right-most and bottom-most cells as possible.
-    // Use uint64_t computation to avoid a potential uint32_t overflow.
-    *cellSize = (uint32_t)(((uint64_t)numPixels + numCells - 1) / numCells);
-
-    // ISO/IEC 23000-22:2019, Section 7.3.11.4.2:
-    //   - the tile_width shall be greater than or equal to 64, and should be a multiple of 64
-    //   - the tile_height shall be greater than or equal to 64, and should be a multiple of 64
-    if (*cellSize < 64) {
-        *cellSize = 64;
-        if ((uint64_t)(numCells - 1) * *cellSize >= (uint64_t)numPixels) {
-            // Some cells would be entirely off-canvas.
-            fprintf(stderr, "ERROR: There are too many cells %s (%u) to have at least 64 pixels per cell.\n", dimensionStr, numCells);
-            return AVIF_FALSE;
-        }
-    }
-
-    // The maximum AV1 frame size is 65536 pixels inclusive.
-    if (*cellSize > 65536) {
-        fprintf(stderr, "ERROR: Cell size %u is bigger %s than the maximum frame size 65536.\n", *cellSize, dimensionStr);
-        return AVIF_FALSE;
-    }
-
-    // ISO/IEC 23000-22:2019, Section 7.3.11.4.2:
-    //   - when the images are in the 4:2:2 chroma sampling format the horizontal tile offsets and widths,
-    //     and the output width, shall be even numbers;
-    //   - when the images are in the 4:2:0 chroma sampling format both the horizontal and vertical tile
-    //     offsets and widths, and the output width and height, shall be even numbers.
-    if (isSubsampled && (*cellSize & 1)) {
-        ++*cellSize;
-        if ((uint64_t)(numCells - 1) * *cellSize >= (uint64_t)numPixels) {
-            // Some cells would be entirely off-canvas.
-            fprintf(stderr, "ERROR: Odd cell size %u is forbidden on a %s subsampled image.\n", *cellSize - 1, dimensionStr);
-            return AVIF_FALSE;
-        }
-    }
-
-    // Each pixel is covered by exactly one cell, and each cell contains at least one pixel.
-    assert(((uint64_t)(numCells - 1) * *cellSize < (uint64_t)numPixels) && ((uint64_t)numCells * *cellSize >= (uint64_t)numPixels));
-    return AVIF_TRUE;
-}
-
-static avifBool avifImageSplitGrid(const avifImage * gridSplitImage, uint32_t gridCols, uint32_t gridRows, avifImage ** gridCells)
-{
-    uint32_t cellWidth, cellHeight;
-    avifPixelFormatInfo formatInfo;
-    avifGetPixelFormatInfo(gridSplitImage->yuvFormat, &formatInfo);
-    const avifBool isSubsampledX = !formatInfo.monochrome && formatInfo.chromaShiftX;
-    const avifBool isSubsampledY = !formatInfo.monochrome && formatInfo.chromaShiftY;
-    if (!avifGetBestCellSize("horizontally", gridSplitImage->width, gridCols, isSubsampledX, &cellWidth) ||
-        !avifGetBestCellSize("vertically", gridSplitImage->height, gridRows, isSubsampledY, &cellHeight)) {
-        return AVIF_FALSE;
-    }
-
-    for (uint32_t gridY = 0; gridY < gridRows; ++gridY) {
-        for (uint32_t gridX = 0; gridX < gridCols; ++gridX) {
-            uint32_t gridIndex = gridX + (gridY * gridCols);
-            avifImage * cellImage = avifImageCreateEmpty();
-            if (!cellImage) {
-                fprintf(stderr, "ERROR: Cell creation failed: out of memory\n");
-                return AVIF_FALSE;
-            }
-            gridCells[gridIndex] = cellImage;
-
-            avifCropRect cellRect = { gridX * cellWidth, gridY * cellHeight, cellWidth, cellHeight };
-            if (cellRect.x + cellRect.width > gridSplitImage->width) {
-                cellRect.width = gridSplitImage->width - cellRect.x;
-            }
-            if (cellRect.y + cellRect.height > gridSplitImage->height) {
-                cellRect.height = gridSplitImage->height - cellRect.y;
-            }
-            const avifResult copyResult = avifImageSetViewRect(cellImage, gridSplitImage, &cellRect);
-            if (copyResult != AVIF_RESULT_OK) {
-                fprintf(stderr, "ERROR: Cell creation failed: %s\n", avifResultToString(copyResult));
-                return AVIF_FALSE;
-            }
-        }
-    }
-
-    // Copy over metadata blobs to the first cell since avifImageSetViewRect() does not copy any
-    // properties that require an allocation.
-    avifImage * firstCell = gridCells[0];
-    if (gridSplitImage->icc.size > 0) {
-        const avifResult result = avifImageSetProfileICC(firstCell, gridSplitImage->icc.data, gridSplitImage->icc.size);
-        if (result != AVIF_RESULT_OK) {
-            fprintf(stderr, "ERROR: Failed to set ICC profile on grid cell: %s\n", avifResultToString(result));
-            return AVIF_FALSE;
-        }
-    }
-    if (gridSplitImage->exif.size > 0) {
-        const avifResult result = avifImageSetMetadataExif(firstCell, gridSplitImage->exif.data, gridSplitImage->exif.size);
-        if (result != AVIF_RESULT_OK) {
-            fprintf(stderr, "ERROR: Failed to set Exif metadata on grid cell: %s\n", avifResultToString(result));
-            return AVIF_FALSE;
-        }
-    }
-    if (gridSplitImage->xmp.size > 0) {
-        const avifResult result = avifImageSetMetadataXMP(firstCell, gridSplitImage->xmp.data, gridSplitImage->xmp.size);
-        if (result != AVIF_RESULT_OK) {
-            fprintf(stderr, "ERROR: Failed to set XMP metadata on grid cell: %s\n", avifResultToString(result));
-            return AVIF_FALSE;
-        }
-    }
-
-    return AVIF_TRUE;
-}
-
 #define INVALID_QUALITY (-1)
 #define DEFAULT_QUALITY 60 // Maps to a quantizer (QP) of 25.
 #define DEFAULT_QUALITY_GAIN_MAP DEFAULT_QUALITY
diff --git a/apps/avifgainmaputil/combine_command.cc b/apps/avifgainmaputil/combine_command.cc
index 7f01c1b..d32d036 100644
--- a/apps/avifgainmaputil/combine_command.cc
+++ b/apps/avifgainmaputil/combine_command.cc
@@ -184,7 +184,10 @@
   encoder->qualityAlpha = arg_image_encode_.quality_alpha;
   encoder->qualityGainMap = arg_gain_map_quality_;
   encoder->speed = arg_image_encode_.speed;
-  result = WriteAvif(base_image.get(), encoder.get(), arg_output_filename_);
+  result =
+      WriteAvifGrid(base_image.get(), arg_image_encode_.grid.value().grid_cols,
+                    arg_image_encode_.grid.value().grid_rows, encoder.get(),
+                    arg_output_filename_);
   if (result != AVIF_RESULT_OK) {
     std::cout << "Failed to encode image: " << avifResultToString(result)
               << " (" << encoder->diag.error << ")\n";
diff --git a/apps/avifgainmaputil/convert_command.cc b/apps/avifgainmaputil/convert_command.cc
index b67f512..58a7690 100644
--- a/apps/avifgainmaputil/convert_command.cc
+++ b/apps/avifgainmaputil/convert_command.cc
@@ -127,7 +127,9 @@
   encoder->qualityGainMap = arg_gain_map_quality_;
   encoder->speed = arg_image_encode_.speed;
   const avifResult result =
-      WriteAvif(image.get(), encoder.get(), arg_output_filename_);
+      WriteAvifGrid(image.get(), arg_image_encode_.grid.value().grid_cols,
+                    arg_image_encode_.grid.value().grid_rows, encoder.get(),
+                    arg_output_filename_);
   if (result != AVIF_RESULT_OK) {
     std::cout << "Failed to encode image: " << avifResultToString(result)
               << " (" << encoder->diag.error << ")\n";
diff --git a/apps/avifgainmaputil/extractgainmap_command.cc b/apps/avifgainmaputil/extractgainmap_command.cc
index 51f7caa..42e3f8b 100644
--- a/apps/avifgainmaputil/extractgainmap_command.cc
+++ b/apps/avifgainmaputil/extractgainmap_command.cc
@@ -36,8 +36,10 @@
     return AVIF_RESULT_INVALID_ARGUMENT;
   }
 
-  return WriteImage(decoder->image->gainMap->image, arg_output_filename_,
-                    arg_image_encode_.quality, arg_image_encode_.speed);
+  return WriteImage(
+      decoder->image->gainMap->image, arg_image_encode_.grid.value().grid_cols,
+      arg_image_encode_.grid.value().grid_rows, arg_output_filename_,
+      arg_image_encode_.quality, arg_image_encode_.speed);
 }
 
 }  // namespace avif
diff --git a/apps/avifgainmaputil/imageio.cc b/apps/avifgainmaputil/imageio.cc
index 4da4742..9a7e4e8 100644
--- a/apps/avifgainmaputil/imageio.cc
+++ b/apps/avifgainmaputil/imageio.cc
@@ -7,6 +7,7 @@
 #include <fstream>
 #include <iostream>
 #include <memory>
+#include <vector>
 
 #include "avif/avif_cxx.h"
 #include "avifjpeg.h"
@@ -21,7 +22,7 @@
   return (x < low) ? low : (high < x) ? high : x;
 }
 
-avifResult WriteImage(const avifImage* image,
+avifResult WriteImage(const avifImage* image, int grid_cols, int grid_rows,
                       const std::string& output_filename, int quality,
                       int speed) {
   quality = Clamp(quality, 0, 100);
@@ -54,7 +55,8 @@
     }
     encoder->quality = quality;
     encoder->speed = speed;
-    return WriteAvif(image, encoder.get(), output_filename);
+    return WriteAvifGrid(image, grid_cols, grid_rows, encoder.get(),
+                         output_filename);
   } else {
     std::cerr << "Unsupported output file extension: " << output_filename
               << "\n";
@@ -80,6 +82,7 @@
   }
   std::ofstream f(output_filename, std::ios::binary);
   f.write(reinterpret_cast<char*>(encoded.data), encoded.size);
+  avifRWDataFree(&encoded);
   if (f.fail()) {
     std::cerr << "Failed to write image " << output_filename << ": "
               << std::strerror(errno) << "\n";
@@ -89,6 +92,67 @@
   return AVIF_RESULT_OK;
 }
 
+avifResult WriteAvifGrid(const avifImage* image, int grid_cols, int grid_rows,
+                         avifEncoder* encoder, const std::string& filename) {
+  if (grid_cols == 1 && grid_rows == 1) {
+    return WriteAvif(image, encoder, filename);
+  }
+
+  const uint32_t grid_cell_count = grid_cols * grid_rows;
+  std::cout << "Preparing to encode a " << grid_cols << "x" << grid_rows
+            << " grid (" << grid_cell_count << " cells)...\n";
+
+  std::vector<avifImage*> grid_cells_ptrs(grid_cell_count);
+  if (!avifImageSplitGrid(image, grid_cols, grid_rows,
+                          grid_cells_ptrs.data())) {
+    for (avifImage* img : grid_cells_ptrs) {
+      if (img) {
+        avifImageDestroy(img);
+      }
+    }
+    return AVIF_RESULT_UNKNOWN_ERROR;
+  }
+  // Take ownership of the pointers returned by avifImageSplitGrid.
+  std::vector<ImagePtr> grid_cells(grid_cell_count);
+  for (uint32_t i = 0; i < grid_cell_count; i++) {
+    grid_cells[i].reset(grid_cells_ptrs[i]);
+  }
+
+  avifRWData encoded = AVIF_DATA_EMPTY;
+  std::cout << "AVIF to be written:\n";
+  avifImageDump(image, grid_cols, grid_rows,
+                AVIF_PROGRESSIVE_STATE_UNAVAILABLE);
+  std::cout << "Encoding AVIF at quality " << encoder->quality << " speed "
+            << encoder->speed << ", please wait...\n";
+  avifResult result = avifEncoderAddImageGrid(
+      encoder, grid_cols, grid_rows,
+      const_cast<const avifImage* const*>(grid_cells_ptrs.data()),
+      AVIF_ADD_IMAGE_FLAG_SINGLE);
+  if (result != AVIF_RESULT_OK) {
+    std::cerr << "Failed to encode image grid: " << avifResultToString(result)
+              << " (" << encoder->diag.error << ")\n";
+    return result;
+  }
+  result = avifEncoderFinish(encoder, &encoded);
+  if (result != AVIF_RESULT_OK) {
+    std::cerr << "Failed to finish encoding image grid: "
+              << avifResultToString(result) << " (" << encoder->diag.error
+              << ")\n";
+    return result;
+  }
+
+  std::ofstream f(filename, std::ios::binary);
+  f.write(reinterpret_cast<char*>(encoded.data), encoded.size);
+  avifRWDataFree(&encoded);
+  if (f.fail()) {
+    std::cerr << "Failed to write image " << filename << ": "
+              << std::strerror(errno) << "\n";
+    return AVIF_RESULT_IO_ERROR;
+  }
+  std::cout << "Wrote AVIF: " << filename << "\n";
+  return AVIF_RESULT_OK;
+}
+
 avifResult ReadImage(avifImage* image, const std::string& input_filename,
                      avifPixelFormat requested_format, uint32_t requested_depth,
                      bool ignore_profile) {
diff --git a/apps/avifgainmaputil/imageio.h b/apps/avifgainmaputil/imageio.h
index 31dd5e0..441e5d7 100644
--- a/apps/avifgainmaputil/imageio.h
+++ b/apps/avifgainmaputil/imageio.h
@@ -11,7 +11,9 @@
 namespace avif {
 
 // Writes an image in any of the supported formats based on the file extension.
-avifResult WriteImage(const avifImage* image,
+// Grid images are supported by Avif only, grid_cols/grid_rows is ignored for
+// other formats.
+avifResult WriteImage(const avifImage* image, int grid_cols, int grid_rows,
                       const std::string& output_filename, int quality,
                       int speed);
 // Reads an image in any of the supported formats. Ignores any gain map.
@@ -23,6 +25,11 @@
 avifResult WriteAvif(const avifImage* image, avifEncoder* encoder,
                      const std::string& output_filename);
 
+// If grid_cols*grid_rows > 1, splits 'image' into a grid and writes it to an
+// AVIF file. Otherwise, just writes a single AVIF image.
+avifResult WriteAvifGrid(const avifImage* image, int grid_cols, int grid_rows,
+                         avifEncoder* encoder, const std::string& filename);
+
 // Reads an image in avif format given a pre-configured decoder.
 // The image can be accessed at decoder->image.
 avifResult ReadAvif(avifDecoder* decoder, const std::string& input_filename,
diff --git a/apps/avifgainmaputil/program_command.cc b/apps/avifgainmaputil/program_command.cc
index e1a57bd..5da3040 100644
--- a/apps/avifgainmaputil/program_command.cc
+++ b/apps/avifgainmaputil/program_command.cc
@@ -1,5 +1,7 @@
 #include "program_command.h"
 
+#include <vector>
+
 namespace avif {
 
 ProgramCommand::ProgramCommand(const std::string& name,
@@ -88,4 +90,23 @@
 
 std::vector<std::string> ClliConverter::default_choices() { return {}; }
 
+argparse::ConvertedValue<GridOptions> GridOptionsConverter::from_str(
+    const std::string& str) {
+  argparse::ConvertedValue<GridOptions> converted_value;
+
+  std::vector<int> grid_dims;
+  if (!ParseList(str, 'x', 2, &grid_dims) || grid_dims[0] <= 0 ||
+      grid_dims[1] <= 0) {
+    converted_value.set_error("Invalid grid dimensions: " + str);
+    return converted_value;
+  }
+  GridOptions grid_options = {grid_dims[0], grid_dims[1]};
+
+  converted_value.set_value(grid_options);
+
+  return converted_value;
+}
+
+std::vector<std::string> GridOptionsConverter::default_choices() { return {}; }
+
 }  // namespace avif
diff --git a/apps/avifgainmaputil/program_command.h b/apps/avifgainmaputil/program_command.h
index 5b0c306..785479a 100644
--- a/apps/avifgainmaputil/program_command.h
+++ b/apps/avifgainmaputil/program_command.h
@@ -64,13 +64,15 @@
   avifMatrixCoefficients matrix_coefficients;
 };
 
-// CicpValues converter for use with argparse.
+// CicpValues converter for use with argparse. Parses the format 'P/T/M'.
 struct CicpConverter {
   // Methods expected by argparse.
   argparse::ConvertedValue<CicpValues> from_str(const std::string& str);
   std::vector<std::string> default_choices();
 };
 
+// avifContentLightLevelInformationBox converter for use with argparse.
+// Parses the format 'maxCLL,maxPALL'.
 struct ClliConverter {
   // Methods expected by argparse.
   argparse::ConvertedValue<avifContentLightLevelInformationBox> from_str(
@@ -78,11 +80,24 @@
   std::vector<std::string> default_choices();
 };
 
+struct GridOptions {
+  int grid_cols = 0;
+  int grid_rows = 0;
+};
+
+// GridOptions converter for use with argparse. Parses the format 'MxN'.
+struct GridOptionsConverter {
+  // Methods expected by argparse.
+  argparse::ConvertedValue<GridOptions> from_str(const std::string& str);
+  std::vector<std::string> default_choices();
+};
+
 // Basic flags for image writing.
 struct BasicImageEncodeArgs {
   argparse::ArgValue<int> speed;
   argparse::ArgValue<int> quality;
   argparse::ArgValue<int> quality_alpha;
+  argparse::ArgValue<GridOptions> grid;
 
   // can_have_alpha should be true if the image can have alpha and the
   // output format can be avif.
@@ -100,6 +115,9 @@
           .help("Quality for alpha (0-100, where 100 is lossless)")
           .default_value("100");
     }
+    argparse.add_argument<GridOptions, GridOptionsConverter>(grid, "--grid")
+        .help("Encode a grid AVIF with M cols and N rows, expressed as MxN")
+        .default_value("1x1");
   }
 };
 
diff --git a/apps/avifgainmaputil/swapbase_command.cc b/apps/avifgainmaputil/swapbase_command.cc
index e6fed88..e1d1e3b 100644
--- a/apps/avifgainmaputil/swapbase_command.cc
+++ b/apps/avifgainmaputil/swapbase_command.cc
@@ -225,7 +225,10 @@
   encoder->qualityAlpha = arg_image_encode_.quality_alpha;
   encoder->qualityGainMap = arg_gain_map_quality_;
   encoder->speed = arg_image_encode_.speed;
-  result = WriteAvif(new_base.get(), encoder.get(), arg_output_filename_);
+  result =
+      WriteAvifGrid(new_base.get(), arg_image_encode_.grid.value().grid_cols,
+                    arg_image_encode_.grid.value().grid_rows, encoder.get(),
+                    arg_output_filename_);
   if (result != AVIF_RESULT_OK) {
     std::cout << "Failed to encode image: " << avifResultToString(result)
               << " (" << encoder->diag.error << ")\n";
diff --git a/apps/avifgainmaputil/tonemap_command.cc b/apps/avifgainmaputil/tonemap_command.cc
index 74fb91b..c5e5520 100644
--- a/apps/avifgainmaputil/tonemap_command.cc
+++ b/apps/avifgainmaputil/tonemap_command.cc
@@ -205,8 +205,10 @@
   tone_mapped->colorPrimaries = cicp.color_primaries;
   tone_mapped->matrixCoefficients = cicp.matrix_coefficients;
 
-  return WriteImage(tone_mapped.get(), arg_output_filename_,
-                    arg_image_encode_.quality, arg_image_encode_.speed);
+  return WriteImage(tone_mapped.get(), arg_image_encode_.grid.value().grid_cols,
+                    arg_image_encode_.grid.value().grid_rows,
+                    arg_output_filename_, arg_image_encode_.quality,
+                    arg_image_encode_.speed);
 }
 
 }  // namespace avif
diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c
index 111435f..0dd9e17 100644
--- a/apps/shared/avifutil.c
+++ b/apps/shared/avifutil.c
@@ -3,8 +3,10 @@
 
 #include "avifutil.h"
 
+#include <assert.h>
 #include <ctype.h>
 #include <stdio.h>
+#include <stdlib.h>
 #include <string.h>
 
 #include "avifjpeg.h"
@@ -479,3 +481,171 @@
 }
 
 #endif
+
+// Returns the best cell size for a given horizontal or vertical dimension.
+avifBool avifGetBestCellSize(const char * dimensionStr, uint32_t numPixels, uint32_t numCells, avifBool isSubsampled, uint32_t * cellSize)
+{
+    assert(numPixels);
+    assert(numCells);
+
+    // ISO/IEC 23008-12:2017, Section 6.6.2.3.1:
+    //   The reconstructed image is formed by tiling the input images into a grid with a column width
+    //   (potentially excluding the right-most column) equal to tile_width and a row height (potentially
+    //   excluding the bottom-most row) equal to tile_height, without gap or overlap, and then
+    //   trimming on the right and the bottom to the indicated output_width and output_height.
+    // The priority could be to use a cell size that is a multiple of 64, but there is not always a valid one,
+    // even though it is recommended by MIAF. Just use ceil(numPixels/numCells) for simplicity and to avoid
+    // as much padding in the right-most and bottom-most cells as possible.
+    // Use uint64_t computation to avoid a potential uint32_t overflow.
+    *cellSize = (uint32_t)(((uint64_t)numPixels + numCells - 1) / numCells);
+
+    // ISO/IEC 23000-22:2019, Section 7.3.11.4.2:
+    //   - the tile_width shall be greater than or equal to 64, and should be a multiple of 64
+    //   - the tile_height shall be greater than or equal to 64, and should be a multiple of 64
+    if (*cellSize < 64) {
+        *cellSize = 64;
+        if ((uint64_t)(numCells - 1) * *cellSize >= (uint64_t)numPixels) {
+            // Some cells would be entirely off-canvas.
+            fprintf(stderr, "ERROR: There are too many cells %s (%u) to have at least 64 pixels per cell.\n", dimensionStr, numCells);
+            return AVIF_FALSE;
+        }
+    }
+
+    // The maximum AV1 frame size is 65536 pixels inclusive.
+    if (*cellSize > 65536) {
+        fprintf(stderr, "ERROR: Cell size %u is bigger %s than the maximum frame size 65536.\n", *cellSize, dimensionStr);
+        return AVIF_FALSE;
+    }
+
+    // ISO/IEC 23000-22:2019, Section 7.3.11.4.2:
+    //   - when the images are in the 4:2:2 chroma sampling format the horizontal tile offsets and widths,
+    //     and the output width, shall be even numbers;
+    //   - when the images are in the 4:2:0 chroma sampling format both the horizontal and vertical tile
+    //     offsets and widths, and the output width and height, shall be even numbers.
+    if (isSubsampled && (*cellSize & 1)) {
+        ++*cellSize;
+        if ((uint64_t)(numCells - 1) * *cellSize >= (uint64_t)numPixels) {
+            // Some cells would be entirely off-canvas.
+            fprintf(stderr, "ERROR: Odd cell size %u is forbidden on a %s subsampled image.\n", *cellSize - 1, dimensionStr);
+            return AVIF_FALSE;
+        }
+    }
+
+    // Each pixel is covered by exactly one cell, and each cell contains at least one pixel.
+    assert(((uint64_t)(numCells - 1) * *cellSize < (uint64_t)numPixels) && ((uint64_t)numCells * *cellSize >= (uint64_t)numPixels));
+    return AVIF_TRUE;
+}
+
+avifBool avifImageSplitGrid(const avifImage * gridSplitImage, uint32_t gridCols, uint32_t gridRows, avifImage ** gridCells)
+{
+    uint32_t cellWidth, cellHeight;
+    avifPixelFormatInfo formatInfo;
+    avifGetPixelFormatInfo(gridSplitImage->yuvFormat, &formatInfo);
+    const avifBool isSubsampledX = !formatInfo.monochrome && formatInfo.chromaShiftX;
+    const avifBool isSubsampledY = !formatInfo.monochrome && formatInfo.chromaShiftY;
+    if (!avifGetBestCellSize("horizontally", gridSplitImage->width, gridCols, isSubsampledX, &cellWidth) ||
+        !avifGetBestCellSize("vertically", gridSplitImage->height, gridRows, isSubsampledY, &cellHeight)) {
+        return AVIF_FALSE;
+    }
+    const avifBool hasGainMap = gridSplitImage->gainMap && gridSplitImage->gainMap->image;
+
+    for (uint32_t gridY = 0; gridY < gridRows; ++gridY) {
+        for (uint32_t gridX = 0; gridX < gridCols; ++gridX) {
+            uint32_t gridIndex = gridX + (gridY * gridCols);
+            avifImage * cellImage = avifImageCreateEmpty();
+            if (!cellImage) {
+                fprintf(stderr, "ERROR: Cell creation failed: out of memory\n");
+                return AVIF_FALSE;
+            }
+            gridCells[gridIndex] = cellImage;
+
+            avifCropRect cellRect = { gridX * cellWidth, gridY * cellHeight, cellWidth, cellHeight };
+            if (cellRect.x + cellRect.width > gridSplitImage->width) {
+                cellRect.width = gridSplitImage->width - cellRect.x;
+            }
+            if (cellRect.y + cellRect.height > gridSplitImage->height) {
+                cellRect.height = gridSplitImage->height - cellRect.y;
+            }
+            const avifResult copyResult = avifImageSetViewRect(cellImage, gridSplitImage, &cellRect);
+            if (copyResult != AVIF_RESULT_OK) {
+                fprintf(stderr, "ERROR: Cell creation failed: %s\n", avifResultToString(copyResult));
+                return AVIF_FALSE;
+            }
+
+            if (hasGainMap) {
+                cellImage->gainMap = avifGainMapCreate();
+                if (!cellImage->gainMap) {
+                    fprintf(stderr, "ERROR: Gain map creation failed: out of memory\n");
+                    return AVIF_FALSE;
+                }
+                // Copy gain map metadata.
+                memcpy(cellImage->gainMap, gridSplitImage->gainMap, sizeof(avifGainMap));
+                cellImage->gainMap->altICC.data = NULL; // Copied later in this function.
+                cellImage->gainMap->altICC.size = 0;
+                cellImage->gainMap->image = NULL; // Set later in this function.
+            }
+        }
+    }
+
+    if (hasGainMap) {
+        avifImage ** gainMapGridCells = NULL;
+        gainMapGridCells = (avifImage **)calloc(gridCols * gridRows, sizeof(avifImage *));
+        if (!gainMapGridCells) {
+            fprintf(stderr, "ERROR: Memory allocation failed for gain map grid cells\n");
+            return AVIF_FALSE;
+        }
+        if (!avifImageSplitGrid(gridSplitImage->gainMap->image, gridCols, gridRows, gainMapGridCells)) {
+            for (uint32_t i = 0; i < gridCols * gridRows; ++i) {
+                if (gainMapGridCells[i]) {
+                    avifImageDestroy(gainMapGridCells[i]);
+                }
+            }
+            free(gainMapGridCells);
+            return AVIF_FALSE;
+        }
+
+        for (uint32_t gridIndex = 0; gridIndex < gridCols * gridRows; ++gridIndex) {
+            // Ownership of the gain map cell is transferred.
+            gridCells[gridIndex]->gainMap->image = gainMapGridCells[gridIndex];
+        }
+        free(gainMapGridCells);
+    }
+
+    // Copy over metadata blobs to the first cell since avifImageSetViewRect() does not copy any
+    // properties that require an allocation.
+    avifImage * firstCell = gridCells[0];
+    if (gridSplitImage->icc.size > 0) {
+        const avifResult result = avifImageSetProfileICC(firstCell, gridSplitImage->icc.data, gridSplitImage->icc.size);
+        if (result != AVIF_RESULT_OK) {
+            fprintf(stderr, "ERROR: Failed to set ICC profile on grid cell: %s\n", avifResultToString(result));
+            return AVIF_FALSE;
+        }
+    }
+    if (gridSplitImage->exif.size > 0) {
+        const avifResult result = avifImageSetMetadataExif(firstCell, gridSplitImage->exif.data, gridSplitImage->exif.size);
+        if (result != AVIF_RESULT_OK) {
+            fprintf(stderr, "ERROR: Failed to set Exif metadata on grid cell: %s\n", avifResultToString(result));
+            return AVIF_FALSE;
+        }
+    }
+    if (gridSplitImage->xmp.size > 0) {
+        const avifResult result = avifImageSetMetadataXMP(firstCell, gridSplitImage->xmp.data, gridSplitImage->xmp.size);
+        if (result != AVIF_RESULT_OK) {
+            fprintf(stderr, "ERROR: Failed to set XMP metadata on grid cell: %s\n", avifResultToString(result));
+            return AVIF_FALSE;
+        }
+    }
+    if (gridSplitImage->gainMap && gridSplitImage->gainMap->image && gridSplitImage->gainMap->altICC.size > 0) {
+        for (uint32_t i = 0; i < gridCols * gridRows; ++i) {
+            avifImage * cellImage = gridCells[i];
+            const avifResult result =
+                avifRWDataSet(&cellImage->gainMap->altICC, gridSplitImage->gainMap->altICC.data, gridSplitImage->gainMap->altICC.size);
+            if (result != AVIF_RESULT_OK) {
+                fprintf(stderr, "ERROR: Failed to set ICC profile on gain map grid cell: %s\n", avifResultToString(result));
+                return AVIF_FALSE;
+            }
+        }
+    }
+
+    return AVIF_TRUE;
+}
diff --git a/apps/shared/avifutil.h b/apps/shared/avifutil.h
index 1c664e6..5474dfe 100644
--- a/apps/shared/avifutil.h
+++ b/apps/shared/avifutil.h
@@ -52,6 +52,13 @@
 // Guesses the format of a buffer by looking at the first bytes.
 avifAppFileFormat avifGuessBufferFileFormat(const uint8_t * data, size_t size);
 
+// Returns the best cell size for a given horizontal or vertical dimension.
+avifBool avifGetBestCellSize(const char * dimensionStr, uint32_t numPixels, uint32_t numCells, avifBool isSubsampled, uint32_t * cellSize);
+
+// Splits an image into a grid of cells, including its gain map, if any.
+// The returned cells must be destroyed with avifImageDestroy().
+avifBool avifImageSplitGrid(const avifImage * gridSplitImage, uint32_t gridCols, uint32_t gridRows, avifImage ** gridCells);
+
 // This structure holds any timing data coming from source (typically non-AVIF) inputs being fed
 // into avifenc. If either or both values are 0, the timing is "invalid" / sentinel and the values
 // should be ignored. This structure is used to override the timing defaults in avifenc when the
diff --git a/tests/data/goldens/paris_exif_xmp_gainmap_bigendian_grid.avif.xml b/tests/data/goldens/paris_exif_xmp_gainmap_bigendian_grid.avif.xml
new file mode 100644
index 0000000..f4a0b17
--- /dev/null
+++ b/tests/data/goldens/paris_exif_xmp_gainmap_bigendian_grid.avif.xml
@@ -0,0 +1,221 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ISOBaseMediaFileTrace>
+<!--MP4Box dump trace-->
+<IsoMediaFile xmlns="urn:mpeg:isobmff:schema:file:2016" Name="paris_exif_xmp_gainmap_bigendian_grid.avif">
+<FileTypeBox Size="36" Type="ftyp" Specification="p12" Container="file otyp" MajorBrand="avif" MinorVersion="0">
+<BrandEntry AlternateBrand="avif"/>
+<BrandEntry AlternateBrand="mif1"/>
+<BrandEntry AlternateBrand="miaf"/>
+<BrandEntry AlternateBrand="MA1A"/>
+<BrandEntry AlternateBrand="tmap"/>
+</FileTypeBox>
+<MetaBox Size="1049" Type="meta" Version="0" Flags="0" Specification="p12" Container="file moov trak moof traf udta" >
+<HandlerBox Size="33" Type="hdlr" Version="0" Flags="0" Specification="p12" Container="mdia meta minf" hdlrType="pict" Name="" reserved1="0" reserved2="data:application/octet-string,000000000000000000000000">
+</HandlerBox>
+<PrimaryItemBox Size="14" Type="pitm" Version="0" Flags="0" Specification="p12" Container="meta" item_ID="1">
+</PrimaryItemBox>
+<ItemLocationBox Size="198" Type="iloc" Version="0" Flags="0" Specification="p12" Container="meta" offset_size="4" length_size="4" base_offset_size="0" index_size="0">
+<ItemLocationEntry item_ID="1" data_reference_index="0" base_offset="0" construction_method="0">
+<ItemExtentEntry extent_offset="REDACTED" extent_length="REDACTED" extent_index="0" />
+</ItemLocationEntry>
+<ItemLocationEntry item_ID="2" data_reference_index="0" base_offset="0" construction_method="0">
+<ItemExtentEntry extent_offset="REDACTED" extent_length="REDACTED" extent_index="0" />
+</ItemLocationEntry>
+<ItemLocationEntry item_ID="3" data_reference_index="0" base_offset="0" construction_method="0">
+<ItemExtentEntry extent_offset="REDACTED" extent_length="REDACTED" extent_index="0" />
+</ItemLocationEntry>
+<ItemLocationEntry item_ID="4" data_reference_index="0" base_offset="0" construction_method="0">
+<ItemExtentEntry extent_offset="REDACTED" extent_length="REDACTED" extent_index="0" />
+</ItemLocationEntry>
+<ItemLocationEntry item_ID="5" data_reference_index="0" base_offset="0" construction_method="0">
+<ItemExtentEntry extent_offset="REDACTED" extent_length="REDACTED" extent_index="0" />
+</ItemLocationEntry>
+<ItemLocationEntry item_ID="6" data_reference_index="0" base_offset="0" construction_method="0">
+<ItemExtentEntry extent_offset="REDACTED" extent_length="REDACTED" extent_index="0" />
+</ItemLocationEntry>
+<ItemLocationEntry item_ID="7" data_reference_index="0" base_offset="0" construction_method="0">
+<ItemExtentEntry extent_offset="REDACTED" extent_length="REDACTED" extent_index="0" />
+</ItemLocationEntry>
+<ItemLocationEntry item_ID="8" data_reference_index="0" base_offset="0" construction_method="0">
+<ItemExtentEntry extent_offset="REDACTED" extent_length="REDACTED" extent_index="0" />
+</ItemLocationEntry>
+<ItemLocationEntry item_ID="9" data_reference_index="0" base_offset="0" construction_method="0">
+<ItemExtentEntry extent_offset="REDACTED" extent_length="REDACTED" extent_index="0" />
+</ItemLocationEntry>
+<ItemLocationEntry item_ID="10" data_reference_index="0" base_offset="0" construction_method="0">
+<ItemExtentEntry extent_offset="REDACTED" extent_length="REDACTED" extent_index="0" />
+</ItemLocationEntry>
+<ItemLocationEntry item_ID="11" data_reference_index="0" base_offset="0" construction_method="0">
+<ItemExtentEntry extent_offset="REDACTED" extent_length="REDACTED" extent_index="0" />
+</ItemLocationEntry>
+<ItemLocationEntry item_ID="12" data_reference_index="0" base_offset="0" construction_method="0">
+<ItemExtentEntry extent_offset="REDACTED" extent_length="REDACTED" extent_index="0" />
+</ItemLocationEntry>
+<ItemLocationEntry item_ID="13" data_reference_index="0" base_offset="0" construction_method="0">
+<ItemExtentEntry extent_offset="REDACTED" extent_length="REDACTED" extent_index="0" />
+</ItemLocationEntry>
+</ItemLocationBox>
+<ItemInfoBox Size="363" Type="iinf" Version="0" Flags="0" Specification="p12" Container="meta" >
+<ItemInfoEntryBox Size="26" Type="infe" Version="2" Flags="0" Specification="p12" Container="iinf" item_ID="1" item_protection_index="0" item_name="Color" content_type="(null)" content_encoding="(null)" item_type="grid">
+</ItemInfoEntryBox>
+<ItemInfoEntryBox Size="26" Type="infe" Version="2" Flags="1" Specification="p12" Container="iinf" item_ID="2" item_protection_index="0" item_name="Color" content_type="(null)" content_encoding="(null)" item_type="av01">
+</ItemInfoEntryBox>
+<ItemInfoEntryBox Size="26" Type="infe" Version="2" Flags="1" Specification="p12" Container="iinf" item_ID="3" item_protection_index="0" item_name="Color" content_type="(null)" content_encoding="(null)" item_type="av01">
+</ItemInfoEntryBox>
+<ItemInfoEntryBox Size="26" Type="infe" Version="2" Flags="1" Specification="p12" Container="iinf" item_ID="4" item_protection_index="0" item_name="Color" content_type="(null)" content_encoding="(null)" item_type="av01">
+</ItemInfoEntryBox>
+<ItemInfoEntryBox Size="26" Type="infe" Version="2" Flags="1" Specification="p12" Container="iinf" item_ID="5" item_protection_index="0" item_name="Color" content_type="(null)" content_encoding="(null)" item_type="av01">
+</ItemInfoEntryBox>
+<ItemInfoEntryBox Size="25" Type="infe" Version="2" Flags="0" Specification="p12" Container="iinf" item_ID="6" item_protection_index="0" item_name="GMap" content_type="(null)" content_encoding="(null)" item_type="tmap">
+</ItemInfoEntryBox>
+<ItemInfoEntryBox Size="25" Type="infe" Version="2" Flags="1" Specification="p12" Container="iinf" item_ID="7" item_protection_index="0" item_name="GMap" content_type="(null)" content_encoding="(null)" item_type="grid">
+</ItemInfoEntryBox>
+<ItemInfoEntryBox Size="25" Type="infe" Version="2" Flags="1" Specification="p12" Container="iinf" item_ID="8" item_protection_index="0" item_name="GMap" content_type="(null)" content_encoding="(null)" item_type="av01">
+</ItemInfoEntryBox>
+<ItemInfoEntryBox Size="25" Type="infe" Version="2" Flags="1" Specification="p12" Container="iinf" item_ID="9" item_protection_index="0" item_name="GMap" content_type="(null)" content_encoding="(null)" item_type="av01">
+</ItemInfoEntryBox>
+<ItemInfoEntryBox Size="25" Type="infe" Version="2" Flags="1" Specification="p12" Container="iinf" item_ID="10" item_protection_index="0" item_name="GMap" content_type="(null)" content_encoding="(null)" item_type="av01">
+</ItemInfoEntryBox>
+<ItemInfoEntryBox Size="25" Type="infe" Version="2" Flags="1" Specification="p12" Container="iinf" item_ID="11" item_protection_index="0" item_name="GMap" content_type="(null)" content_encoding="(null)" item_type="av01">
+</ItemInfoEntryBox>
+<ItemInfoEntryBox Size="25" Type="infe" Version="2" Flags="0" Specification="p12" Container="iinf" item_ID="12" item_protection_index="0" item_name="Exif" content_type="(null)" content_encoding="(null)" item_type="Exif">
+</ItemInfoEntryBox>
+<ItemInfoEntryBox Size="44" Type="infe" Version="2" Flags="0" Specification="p12" Container="iinf" item_ID="13" item_protection_index="0" item_name="XMP" content_type="application/rdf+xml" content_encoding="(null)" item_type="mime">
+</ItemInfoEntryBox>
+</ItemInfoBox>
+<ItemReferenceBox Size="96" Type="iref" Version="0" Flags="0" Specification="p12" Container="meta" >
+<ItemReferenceBox Size="20" Type="dimg" Specification="p12" Container="iref" from_item_id="1">
+<ItemReferenceBoxEntry ItemID="2"/>
+<ItemReferenceBoxEntry ItemID="3"/>
+<ItemReferenceBoxEntry ItemID="4"/>
+<ItemReferenceBoxEntry ItemID="5"/>
+</ItemReferenceBox>
+<ItemReferenceBox Size="16" Type="dimg" Specification="p12" Container="iref" from_item_id="6">
+<ItemReferenceBoxEntry ItemID="1"/>
+<ItemReferenceBoxEntry ItemID="7"/>
+</ItemReferenceBox>
+<ItemReferenceBox Size="20" Type="dimg" Specification="p12" Container="iref" from_item_id="7">
+<ItemReferenceBoxEntry ItemID="8"/>
+<ItemReferenceBoxEntry ItemID="9"/>
+<ItemReferenceBoxEntry ItemID="10"/>
+<ItemReferenceBoxEntry ItemID="11"/>
+</ItemReferenceBox>
+<ItemReferenceBox Size="14" Type="cdsc" Specification="p12" Container="iref" from_item_id="12">
+<ItemReferenceBoxEntry ItemID="1"/>
+</ItemReferenceBox>
+<ItemReferenceBox Size="14" Type="cdsc" Specification="p12" Container="iref" from_item_id="13">
+<ItemReferenceBoxEntry ItemID="1"/>
+</ItemReferenceBox>
+</ItemReferenceBox>
+<ItemPropertiesBox Size="297" Type="iprp" Specification="iff" Container="meta" >
+<ItemPropertyContainerBox Size="199" Type="ipco" Specification="iff" Container="iprp" >
+<ImageSpatialExtentsPropertyBox Size="20" Type="ispe" Version="0" Flags="0" Specification="iff" Container="ipco" image_width="403" image_height="302">
+</ImageSpatialExtentsPropertyBox>
+<PixelInformationPropertyBox Size="16" Type="pixi" Version="0" Flags="0" Specification="iff" Container="ipco" >
+<BitPerChannel bits_per_channel="8"/>
+<BitPerChannel bits_per_channel="8"/>
+<BitPerChannel bits_per_channel="8"/>
+</PixelInformationPropertyBox>
+<ColourInformationBox Size="19" Type="colr" Specification="iff" Container="video_sample_entry ipco encv resv" colour_type="nclx" colour_primaries="1" transfer_characteristics="13" matrix_coefficients="6" full_range_flag="1">
+</ColourInformationBox>
+<ImageSpatialExtentsPropertyBox Size="20" Type="ispe" Version="0" Flags="0" Specification="iff" Container="ipco" image_width="202" image_height="151">
+</ImageSpatialExtentsPropertyBox>
+<AV1ConfigurationBox>
+<AV1Config version="1" profile="1" level_idx0="0" tier="0" high_bitdepth="0" twelve_bit="0" monochrome="0" chroma_subsampling_x="0" chroma_subsampling_y="0" chroma_sample_position="0" initial_presentation_delay="1" OBUs_count="0">
+</AV1Config>
+</AV1ConfigurationBox>
+<ColourInformationBox Size="19" Type="colr" Specification="iff" Container="video_sample_entry ipco encv resv" colour_type="nclx" colour_primaries="1" transfer_characteristics="16" matrix_coefficients="6" full_range_flag="1">
+</ColourInformationBox>
+<ImageSpatialExtentsPropertyBox Size="20" Type="ispe" Version="0" Flags="0" Specification="iff" Container="ipco" image_width="512" image_height="384">
+</ImageSpatialExtentsPropertyBox>
+<PixelInformationPropertyBox Size="14" Type="pixi" Version="0" Flags="0" Specification="iff" Container="ipco" >
+<BitPerChannel bits_per_channel="8"/>
+</PixelInformationPropertyBox>
+<ColourInformationBox Size="19" Type="colr" Specification="iff" Container="video_sample_entry ipco encv resv" colour_type="nclx" colour_primaries="2" transfer_characteristics="2" matrix_coefficients="6" full_range_flag="1">
+</ColourInformationBox>
+<ImageSpatialExtentsPropertyBox Size="20" Type="ispe" Version="0" Flags="0" Specification="iff" Container="ipco" image_width="256" image_height="192">
+</ImageSpatialExtentsPropertyBox>
+<AV1ConfigurationBox>
+<AV1Config version="1" profile="0" level_idx0="0" tier="0" high_bitdepth="0" twelve_bit="0" monochrome="1" chroma_subsampling_x="1" chroma_subsampling_y="1" chroma_sample_position="0" initial_presentation_delay="1" OBUs_count="0">
+</AV1Config>
+</AV1ConfigurationBox>
+</ItemPropertyContainerBox>
+<ItemPropertyAssociationBox Size="90" Type="ipma" Version="0" Flags="0" Specification="iff" Container="iprp" entry_count="11">
+<AssociationEntry item_ID="1" association_count="3">
+<Property index="1" essential="0"/>
+<Property index="2" essential="0"/>
+<Property index="3" essential="0"/>
+</AssociationEntry>
+<AssociationEntry item_ID="2" association_count="4">
+<Property index="4" essential="0"/>
+<Property index="2" essential="0"/>
+<Property index="5" essential="1"/>
+<Property index="3" essential="0"/>
+</AssociationEntry>
+<AssociationEntry item_ID="3" association_count="4">
+<Property index="4" essential="0"/>
+<Property index="2" essential="0"/>
+<Property index="5" essential="1"/>
+<Property index="3" essential="0"/>
+</AssociationEntry>
+<AssociationEntry item_ID="4" association_count="4">
+<Property index="4" essential="0"/>
+<Property index="2" essential="0"/>
+<Property index="5" essential="1"/>
+<Property index="3" essential="0"/>
+</AssociationEntry>
+<AssociationEntry item_ID="5" association_count="4">
+<Property index="4" essential="0"/>
+<Property index="2" essential="0"/>
+<Property index="5" essential="1"/>
+<Property index="3" essential="0"/>
+</AssociationEntry>
+<AssociationEntry item_ID="6" association_count="3">
+<Property index="1" essential="0"/>
+<Property index="2" essential="0"/>
+<Property index="6" essential="0"/>
+</AssociationEntry>
+<AssociationEntry item_ID="7" association_count="3">
+<Property index="7" essential="0"/>
+<Property index="8" essential="0"/>
+<Property index="9" essential="0"/>
+</AssociationEntry>
+<AssociationEntry item_ID="8" association_count="4">
+<Property index="10" essential="0"/>
+<Property index="8" essential="0"/>
+<Property index="11" essential="1"/>
+<Property index="9" essential="0"/>
+</AssociationEntry>
+<AssociationEntry item_ID="9" association_count="4">
+<Property index="10" essential="0"/>
+<Property index="8" essential="0"/>
+<Property index="11" essential="1"/>
+<Property index="9" essential="0"/>
+</AssociationEntry>
+<AssociationEntry item_ID="10" association_count="4">
+<Property index="10" essential="0"/>
+<Property index="8" essential="0"/>
+<Property index="11" essential="1"/>
+<Property index="9" essential="0"/>
+</AssociationEntry>
+<AssociationEntry item_ID="11" association_count="4">
+<Property index="10" essential="0"/>
+<Property index="8" essential="0"/>
+<Property index="11" essential="1"/>
+<Property index="9" essential="0"/>
+</AssociationEntry>
+</ItemPropertyAssociationBox>
+</ItemPropertiesBox>
+<GroupListBox Size="36" Type="grpl" Specification="iff" Container="meta" >
+<EntityToGroupTypeBox Size="28" Type="altr" Version="0" Flags="0" Specification="iff" Container="grpl" group_id="14">
+<EntityToGroupTypeBoxEntry EntityID="6"/>
+<EntityToGroupTypeBoxEntry EntityID="1"/>
+</EntityToGroupTypeBox>
+</GroupListBox>
+</MetaBox>
+<MediaDataBox Size="REDACTED" Type="mdat" Specification="p12" Container="file" dataSize="REDACTED">
+</MediaDataBox>
+</IsoMediaFile>
+<Tracks>
+</Tracks>
+</ISOBaseMediaFileTrace>
diff --git a/tests/test_cmd_avifgainmaputil.sh b/tests/test_cmd_avifgainmaputil.sh
index 22fb2db..754bb2c 100755
--- a/tests/test_cmd_avifgainmaputil.sh
+++ b/tests/test_cmd_avifgainmaputil.sh
@@ -50,7 +50,7 @@
   "${AVIFGAINMAPUTIL}" combine "${INPUT_JPEG_AVIF_GAINMAP_SDR}" "${INPUT_AVIF_GAINMAP_HDR}" "${AVIF_OUTPUT}" \
       -q 50 --qgain-map 90 --ignore-profile
   "${AVIFGAINMAPUTIL}" combine "${INPUT_AVIF_GAINMAP_SDR}" "${INPUT_AVIF_GAINMAP_HDR2020}" "${AVIF_OUTPUT}" \
-      -q 50 --downscaling 2 --yuv-gain-map 400
+      -q 50 --downscaling 2 --yuv-gain-map 400 --grid 2x2
 
   "${AVIFGAINMAPUTIL}" combine "${INPUT_AVIF_GAINMAP_HDR}" "${INPUT_AVIF_GAINMAP_SDR}" "${AVIF_OUTPUT}" \
       -q 90 --qgain-map 90
diff --git a/tests/test_cmd_enc_gainmap_boxes_golden.sh b/tests/test_cmd_enc_gainmap_boxes_golden.sh
index 11b77ee..b138b0e 100644
--- a/tests/test_cmd_enc_gainmap_boxes_golden.sh
+++ b/tests/test_cmd_enc_gainmap_boxes_golden.sh
@@ -30,6 +30,8 @@
         "${AVIFENC}" -s 9 "${TESTDATA_DIR}/$f" -o "$f.avif"
     done
 
+    "${AVIFENC}" -s 9 --yuv 444 --grid 2x2 "${TESTDATA_DIR}/paris_exif_xmp_gainmap_bigendian.jpg" -o "paris_exif_xmp_gainmap_bigendian_grid.avif"
+
     # Ignore gain map.
     "${AVIFENC}" -s 9 --ignore-gain-map "${TESTDATA_DIR}/paris_exif_xmp_gainmap_bigendian.jpg" \
       -o "paris_exif_xmp_gainmap_bigendian_ignore.jpg.avif"