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"