Allow padding in avifenc avifImageSplitGrid()

So that images with dimensions that cannot be evenly split into the
number of cells specified with the --grid flag can still be encoded
with the right-most and bottom-most cells padded.
Add test_cmd_grid.sh.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 51b9bcf..2a6a112 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,10 @@
 * Add STATIC library target avif_internal to allow tests to access functions
   from internal.h when BUILD_SHARED_LIBS is ON.
 
+### Changed
+* The --grid flag in avifenc can be used for images that are not evenly divided
+  into cells.
+
 ## [0.11.1] - 2022-10-19
 
 ### Changed
diff --git a/apps/avifenc.c b/apps/avifenc.c
index ba37b72..69e28a9 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -340,25 +340,67 @@
     return AVIF_TRUE;
 }
 
-static avifBool avifImageSplitGrid(const avifImage * gridSplitImage, uint32_t gridCols, uint32_t gridRows, avifImage ** gridCells)
+// 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)
 {
-    if ((gridSplitImage->width % gridCols) != 0) {
-        fprintf(stderr, "ERROR: Can't split image width (%u) evenly into %u columns.\n", gridSplitImage->width, gridCols);
-        return AVIF_FALSE;
+    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;
+        }
     }
-    if ((gridSplitImage->height % gridRows) != 0) {
-        fprintf(stderr, "ERROR: Can't split image height (%u) evenly into %u rows.\n", gridSplitImage->height, gridRows);
+
+    // The maximum AV1 frame size is 65536 pixels inclusive.
+    if (*cellSize > 65536) {
+        fprintf(stderr, "ERROR: Cell size %u is bigger %s than the maximum AV1 frame size 65536.\n", *cellSize, dimensionStr);
         return AVIF_FALSE;
     }
 
-    uint32_t cellWidth = gridSplitImage->width / gridCols;
-    uint32_t cellHeight = gridSplitImage->height / gridRows;
-    if ((cellWidth < 64) || (cellHeight < 64)) {
-        fprintf(stderr, "ERROR: Split cell dimensions are too small (must be at least 64x64, and were %ux%u)\n", cellWidth, cellHeight);
-        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;
+        }
     }
-    if (((cellWidth % 2) != 0) || ((cellHeight % 2) != 0)) {
-        fprintf(stderr, "ERROR: Odd split cell dimensions are unsupported (%ux%u)\n", cellWidth, cellHeight);
+
+    // 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);
+    if (!avifGetBestCellSize("horizontally", gridSplitImage->width, gridCols, formatInfo.chromaShiftX, &cellWidth) ||
+        !avifGetBestCellSize("vertically", gridSplitImage->height, gridRows, formatInfo.chromaShiftY, &cellHeight)) {
         return AVIF_FALSE;
     }
 
@@ -368,47 +410,18 @@
             avifImage * cellImage = avifImageCreateEmpty();
             gridCells[gridIndex] = cellImage;
 
-            const avifResult copyResult = avifImageCopy(cellImage, gridSplitImage, 0);
+            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: Image copy failed: %s\n", avifResultToString(copyResult));
+                fprintf(stderr, "ERROR: Cell creation failed: %s\n", avifResultToString(copyResult));
                 return AVIF_FALSE;
             }
-            cellImage->width = cellWidth;
-            cellImage->height = cellHeight;
-
-            const uint32_t bytesPerPixel = avifImageUsesU16(cellImage) ? 2 : 1;
-
-            const uint32_t bytesPerRowY = bytesPerPixel * cellWidth;
-            const uint32_t srcRowBytesY = gridSplitImage->yuvRowBytes[AVIF_CHAN_Y];
-            cellImage->yuvPlanes[AVIF_CHAN_Y] =
-                &gridSplitImage->yuvPlanes[AVIF_CHAN_Y][(gridX * bytesPerRowY) + (gridY * cellHeight) * srcRowBytesY];
-            cellImage->yuvRowBytes[AVIF_CHAN_Y] = srcRowBytesY;
-
-            if (gridSplitImage->yuvFormat != AVIF_PIXEL_FORMAT_YUV400) {
-                avifPixelFormatInfo info;
-                avifGetPixelFormatInfo(gridSplitImage->yuvFormat, &info);
-
-                const uint32_t uvWidth = (cellWidth + info.chromaShiftX) >> info.chromaShiftX;
-                const uint32_t uvHeight = (cellHeight + info.chromaShiftY) >> info.chromaShiftY;
-                const uint32_t bytesPerRowUV = bytesPerPixel * uvWidth;
-
-                const uint32_t srcRowBytesU = gridSplitImage->yuvRowBytes[AVIF_CHAN_U];
-                cellImage->yuvPlanes[AVIF_CHAN_U] =
-                    &gridSplitImage->yuvPlanes[AVIF_CHAN_U][(gridX * bytesPerRowUV) + (gridY * uvHeight) * srcRowBytesU];
-                cellImage->yuvRowBytes[AVIF_CHAN_U] = srcRowBytesU;
-
-                const uint32_t srcRowBytesV = gridSplitImage->yuvRowBytes[AVIF_CHAN_V];
-                cellImage->yuvPlanes[AVIF_CHAN_V] =
-                    &gridSplitImage->yuvPlanes[AVIF_CHAN_V][(gridX * bytesPerRowUV) + (gridY * uvHeight) * srcRowBytesV];
-                cellImage->yuvRowBytes[AVIF_CHAN_V] = srcRowBytesV;
-            }
-
-            if (gridSplitImage->alphaPlane) {
-                const uint32_t bytesPerRowA = bytesPerPixel * cellWidth;
-                const uint32_t srcRowBytesA = gridSplitImage->alphaRowBytes;
-                cellImage->alphaPlane = &gridSplitImage->alphaPlane[(gridX * bytesPerRowA) + (gridY * cellHeight) * srcRowBytesA];
-                cellImage->alphaRowBytes = srcRowBytesA;
-            }
         }
     }
     return AVIF_TRUE;
@@ -1065,33 +1078,7 @@
                 returnCode = 1;
                 goto cleanup;
             }
-
-            // Verify that this cell's properties matches the first cell's properties
-            if ((image->width != cellImage->width) || (image->height != cellImage->height)) {
-                fprintf(stderr,
-                        "ERROR: Image grid dimensions mismatch, [%ux%u] vs [%ux%u]: %s\n",
-                        image->width,
-                        image->height,
-                        cellImage->width,
-                        cellImage->height,
-                        nextFile->filename);
-                returnCode = 1;
-                goto cleanup;
-            }
-            if (image->depth != cellImage->depth) {
-                fprintf(stderr, "ERROR: Image grid depth mismatch, [%u] vs [%u]: %s\n", image->depth, cellImage->depth, nextFile->filename);
-                returnCode = 1;
-                goto cleanup;
-            }
-            if (image->yuvRange != cellImage->yuvRange) {
-                fprintf(stderr,
-                        "ERROR: Image grid range mismatch, [%s] vs [%s]: %s\n",
-                        (image->yuvRange == AVIF_RANGE_FULL) ? "Full" : "Limited",
-                        (nextImage->yuvRange == AVIF_RANGE_FULL) ? "Full" : "Limited",
-                        nextFile->filename);
-                returnCode = 1;
-                goto cleanup;
-            }
+            // Let avifEncoderAddImageGrid() verify the grid integrity (valid cell sizes, depths etc.).
         }
 
         if (gridCellIndex == 0) {
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index e7c3635..5964fea 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -140,6 +140,9 @@
     add_test(NAME test_cmd COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd.sh ${CMAKE_BINARY_DIR}
                                    ${CMAKE_CURRENT_SOURCE_DIR}/data
     )
+    add_test(NAME test_cmd_grid COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_grid.sh ${CMAKE_BINARY_DIR}
+                                        ${CMAKE_CURRENT_SOURCE_DIR}/data
+    )
     add_test(NAME test_cmd_lossless COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_lossless.sh ${CMAKE_BINARY_DIR}
                                             ${CMAKE_CURRENT_SOURCE_DIR}/data
     )
diff --git a/tests/test_cmd_grid.sh b/tests/test_cmd_grid.sh
new file mode 100755
index 0000000..a38b5ab
--- /dev/null
+++ b/tests/test_cmd_grid.sh
@@ -0,0 +1,97 @@
+#!/bin/bash
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ------------------------------------------------------------------------------
+#
+# tests for command lines (grid)
+
+# Very verbose but useful for debugging.
+set -ex
+
+if [[ "$#" -ge 1 ]]; then
+  # eval so that the passed in directory can contain variables.
+  BINARY_DIR="$(eval echo "$1")"
+else
+  # Assume "tests" is the current directory.
+  BINARY_DIR="$(pwd)/.."
+fi
+if [[ "$#" -ge 2 ]]; then
+  TESTDATA_DIR="$(eval echo "$2")"
+else
+  TESTDATA_DIR="$(pwd)/data"
+fi
+if [[ "$#" -ge 3 ]]; then
+  TMP_DIR="$(eval echo "$3")"
+else
+  TMP_DIR="$(mktemp -d)"
+fi
+
+AVIFENC="${BINARY_DIR}/avifenc"
+AVIFDEC="${BINARY_DIR}/avifdec"
+ARE_IMAGES_EQUAL="${BINARY_DIR}/tests/are_images_equal"
+
+# Input file paths.
+INPUT_PNG="${TESTDATA_DIR}/paris_icc_exif_xmp.png" # 403 x 302 px
+# Output file names.
+ENCODED_FILE="avif_test_cmd_grid_encoded.avif"
+DECODED_FILE="avif_test_cmd_grid_decoded.png"
+ENCODED_FILE_2x2="avif_test_cmd_grid_2x2_encoded.avif"
+DECODED_FILE_2x2="avif_test_cmd_grid_2x2_decoded.png"
+ENCODED_FILE_7x5="avif_test_cmd_grid_7x5_encoded.avif"
+DECODED_FILE_7x5="avif_test_cmd_grid_7x5_decoded.png"
+
+# Cleanup
+cleanup() {
+  pushd ${TMP_DIR}
+    rm -- "${ENCODED_FILE}" "${DECODED_FILE}" \
+          "${ENCODED_FILE_2x2}" "${DECODED_FILE_2x2}" \
+          "${ENCODED_FILE_7x5}" "${DECODED_FILE_7x5}"
+  popd
+}
+trap cleanup EXIT
+
+pushd ${TMP_DIR}
+  echo "Testing basic grid"
+  "${AVIFENC}" -s 8 "${INPUT_PNG}" --grid 2x2 -o "${ENCODED_FILE_2x2}"
+  "${AVIFDEC}" "${ENCODED_FILE_2x2}" "${DECODED_FILE_2x2}"
+
+  echo "Testing max grid"
+  "${AVIFENC}" -s 8 "${INPUT_PNG}" --grid 7x5 -o "${ENCODED_FILE_7x5}"
+  "${AVIFDEC}" "${ENCODED_FILE_7x5}" "${DECODED_FILE_7x5}"
+
+  # Lossy compression should give different results for different tiles.
+  "${ARE_IMAGES_EQUAL}" "${DECODED_FILE_2x2}" "${DECODED_FILE_7x5}" 1 && exit 1
+
+  echo "Testing grid patterns"
+  for GRID in 1x1 1x2 1x3 1x4 1x5 2x1 3x1 4x1 5x1 6x1 7x1; do
+    "${AVIFENC}" -s 10 "${INPUT_PNG}" --grid ${GRID} -o "${ENCODED_FILE}"
+    "${AVIFDEC}" "${ENCODED_FILE}" "${DECODED_FILE}"
+  done
+
+  echo "Testing wrong grid arguments"
+  "${AVIFENC}" "${INPUT_PNG}" --grid 0x0 -o "${ENCODED_FILE}" && exit 1
+  "${AVIFENC}" "${INPUT_PNG}" --grid 8x5 -o "${ENCODED_FILE}" && exit 1
+  "${AVIFENC}" "${INPUT_PNG}" --grid 7x6 -o "${ENCODED_FILE}" && exit 1
+  "${AVIFENC}" "${INPUT_PNG}" --grid 4294967295x4294967295 -o "${ENCODED_FILE}" && exit 1
+  "${AVIFENC}" "${INPUT_PNG}" --grid 2147483647x2147483647 -o "${ENCODED_FILE}" && exit 1
+
+  echo "Testing valid grid arguments but invalid grid image dimensions for subsampled image"
+  "${AVIFENC}" "${INPUT_PNG}" --grid 2x2 --yuv 420 -o "${ENCODED_FILE}" && exit 1
+  # Even if there is a single tile in the odd dimension, it is forbidden.
+  "${AVIFENC}" "${INPUT_PNG}" --grid 1x2 --yuv 422 -o "${ENCODED_FILE}" && exit 1
+  # 1x1 is not a real grid.
+  "${AVIFENC}" "${INPUT_PNG}" --grid 1x1 --yuv 420 -o "${ENCODED_FILE}"
+popd
+
+exit 0