Fix out-of-order dimg grid associations (#2312)

Add sofa_grid1x5_420_reversed_dimg_order.avif and a test.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb9a284..7ade277 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,8 @@
 * Fix aom link flags so that transitive library link flags are included when
   aom is a static library
   https://github.com/AOMediaCodec/libavif/issues/2274.
+* Fix out-of-order 'dimg' grid associations
+  https://github.com/AOMediaCodec/libavif/issues/2311.
 
 ## [1.1.0] - 2024-07-11
 
diff --git a/src/read.c b/src/read.c
index 12996cf..46f6b42 100644
--- a/src/read.c
+++ b/src/read.c
@@ -1420,17 +1420,42 @@
     return AVIF_CODEC_TYPE_UNKNOWN;
 }
 
-static avifResult avifDecoderGenerateImageGridTiles(avifDecoder * decoder, avifImageGrid * grid, avifDecoderItem * gridItem, avifItemCategory itemCategory)
+// Fills the dimgIdxToItemIdx array with a mapping from each 0-based tile index in the 'dimg' reference
+// to its corresponding 0-based index in the avifMeta::items array.
+static avifResult avifFillDimgIdxToItemIdxArray(uint32_t * dimgIdxToItemIdx, uint32_t numExpectedTiles, const avifDecoderItem * gridItem)
 {
-    // Count number of dimg for this item, bail out if it doesn't match perfectly
-    unsigned int tilesAvailable = 0;
+    const uint32_t itemIndexNotSet = UINT32_MAX;
+    for (uint32_t dimgIdx = 0; dimgIdx < numExpectedTiles; ++dimgIdx) {
+        dimgIdxToItemIdx[dimgIdx] = itemIndexNotSet;
+    }
+    uint32_t numTiles = 0;
+    for (uint32_t i = 0; i < gridItem->meta->items.count; ++i) {
+        if (gridItem->meta->items.item[i]->dimgForID == gridItem->id) {
+            const uint32_t tileItemDimgIdx = gridItem->meta->items.item[i]->dimgIdx;
+            AVIF_CHECKERR(tileItemDimgIdx < numExpectedTiles, AVIF_RESULT_INVALID_IMAGE_GRID);
+            AVIF_CHECKERR(dimgIdxToItemIdx[tileItemDimgIdx] == itemIndexNotSet, AVIF_RESULT_INVALID_IMAGE_GRID);
+            dimgIdxToItemIdx[tileItemDimgIdx] = i;
+            ++numTiles;
+        }
+    }
+    // The number of tiles has been verified in avifDecoderItemReadAndParse().
+    AVIF_ASSERT_OR_RETURN(numTiles == numExpectedTiles);
+    return AVIF_RESULT_OK;
+}
+
+// Creates the tiles and associate them to the items in the order of the 'dimg' association.
+static avifResult avifDecoderGenerateImageGridTiles(avifDecoder * decoder,
+                                                    avifDecoderItem * gridItem,
+                                                    avifItemCategory itemCategory,
+                                                    const uint32_t * dimgIdxToItemIdx,
+                                                    uint32_t numTiles)
+{
     avifDecoderItem * firstTileItem = NULL;
     avifBool progressive = AVIF_TRUE;
-    for (uint32_t i = 0; i < gridItem->meta->items.count; ++i) {
-        avifDecoderItem * item = gridItem->meta->items.item[i];
-        if (item->dimgForID != gridItem->id) {
-            continue;
-        }
+    for (uint32_t dimgIdx = 0; dimgIdx < numTiles; ++dimgIdx) {
+        const uint32_t itemIdx = dimgIdxToItemIdx[dimgIdx];
+        AVIF_ASSERT_OR_RETURN(itemIdx < gridItem->meta->items.count);
+        avifDecoderItem * item = gridItem->meta->items.item[itemIdx];
 
         // According to HEIF (ISO 14496-12), Section 6.6.2.3.1, the SingleItemTypeReferenceBox of type 'dimg'
         // identifies the input images of the derived image item of type 'grid'. Since the reference_count
@@ -1505,22 +1530,11 @@
         if (!item->progressive) {
             progressive = AVIF_FALSE;
         }
-        ++tilesAvailable;
     }
     if (itemCategory == AVIF_ITEM_COLOR && progressive) {
         // If all the items that make up the grid are progressive, then propagate that status to the top-level grid item.
         gridItem->progressive = AVIF_TRUE;
     }
-
-    if (tilesAvailable != grid->rows * grid->columns) {
-        avifDiagnosticsPrintf(&decoder->diag,
-                              "Grid image of dimensions %ux%u requires %u tiles, but %u were found",
-                              grid->columns,
-                              grid->rows,
-                              grid->rows * grid->columns,
-                              tilesAvailable);
-        return AVIF_RESULT_INVALID_IMAGE_GRID;
-    }
     return AVIF_RESULT_OK;
 }
 
@@ -4650,14 +4664,19 @@
         return AVIF_RESULT_OK;
     }
     // If color item is a grid, check if there is an alpha channel which is represented as an auxl item to each color tile item.
-    uint32_t colorItemCount = colorInfo->grid.rows * colorInfo->grid.columns;
-    if (colorItemCount == 0) {
+    const uint32_t tileCount = colorInfo->grid.rows * colorInfo->grid.columns;
+    if (tileCount == 0) {
         *alphaItem = NULL;
         *isAlphaItemInInput = AVIF_FALSE;
         return AVIF_RESULT_OK;
     }
-    uint32_t * alphaItemIndices = (uint32_t *)avifAlloc(colorItemCount * sizeof(uint32_t));
-    AVIF_CHECKERR(alphaItemIndices, AVIF_RESULT_OUT_OF_MEMORY);
+    // Keep the same 'dimg' order as it defines where each tile is located in the reconstructed image.
+    uint32_t * dimgIdxToAlphaItemIdx = (uint32_t *)avifAlloc(tileCount * sizeof(uint32_t));
+    AVIF_CHECKERR(dimgIdxToAlphaItemIdx != NULL, AVIF_RESULT_OUT_OF_MEMORY);
+    const uint32_t itemIndexNotSet = UINT32_MAX;
+    for (uint32_t dimgIdx = 0; dimgIdx < tileCount; ++dimgIdx) {
+        dimgIdxToAlphaItemIdx[dimgIdx] = itemIndexNotSet;
+    }
     uint32_t alphaItemCount = 0;
     for (uint32_t i = 0; i < meta->items.count; ++i) {
         const avifDecoderItem * const item = meta->items.item[i];
@@ -4666,27 +4685,34 @@
             for (uint32_t j = 0; j < meta->items.count; ++j) {
                 avifDecoderItem * auxlItem = meta->items.item[j];
                 if (avifDecoderItemIsAlphaAux(auxlItem, item->id)) {
-                    if (seenAlphaForCurrentItem || auxlItem->dimgForID != 0) {
+                    if (seenAlphaForCurrentItem || auxlItem->dimgForID != 0 || item->dimgIdx >= tileCount ||
+                        dimgIdxToAlphaItemIdx[item->dimgIdx] != itemIndexNotSet) {
                         // One of the following invalid cases:
                         // * Multiple items are claiming to be the alpha auxiliary of the current item.
                         // * Alpha auxiliary is dimg for another item.
-                        avifFree(alphaItemIndices);
+                        // * There are too many items in the dimg array (also checked later in avifFillDimgIdxToItemIdxArray()).
+                        // * There is a repetition in the dimg array (also checked later in avifFillDimgIdxToItemIdxArray()).
+                        avifFree(dimgIdxToAlphaItemIdx);
                         return AVIF_RESULT_INVALID_IMAGE_GRID;
                     }
-                    alphaItemIndices[alphaItemCount++] = j;
+                    dimgIdxToAlphaItemIdx[item->dimgIdx] = j;
+                    ++alphaItemCount;
                     seenAlphaForCurrentItem = AVIF_TRUE;
                 }
             }
             if (!seenAlphaForCurrentItem) {
                 // No alpha auxiliary item was found for the current item. Treat this as an image without alpha.
-                avifFree(alphaItemIndices);
+                avifFree(dimgIdxToAlphaItemIdx);
                 *alphaItem = NULL;
                 *isAlphaItemInInput = AVIF_FALSE;
                 return AVIF_RESULT_OK;
             }
         }
     }
-    AVIF_ASSERT_OR_RETURN(alphaItemCount == colorItemCount);
+    if (alphaItemCount != tileCount) {
+        avifFree(dimgIdxToAlphaItemIdx);
+        return AVIF_RESULT_INVALID_IMAGE_GRID;
+    }
     // Find an unused ID.
     avifResult result;
     if (meta->items.count >= UINT32_MAX - 1) {
@@ -4708,17 +4734,22 @@
         result = avifMetaFindOrCreateItem(meta, newItemID, alphaItem); // Create new empty item.
     }
     if (result != AVIF_RESULT_OK) {
-        avifFree(alphaItemIndices);
+        avifFree(dimgIdxToAlphaItemIdx);
         return result;
     }
     memcpy((*alphaItem)->type, "grid", 4); // Make it a grid and register alpha items as its tiles.
     (*alphaItem)->width = colorItem->width;
     (*alphaItem)->height = colorItem->height;
-    for (uint32_t i = 0; i < alphaItemCount; ++i) {
-        avifDecoderItem * item = meta->items.item[alphaItemIndices[i]];
-        item->dimgForID = (*alphaItem)->id;
+    for (uint32_t dimgIdx = 0; dimgIdx < tileCount; ++dimgIdx) {
+        if (dimgIdxToAlphaItemIdx[dimgIdx] >= meta->items.count) {
+            avifFree(dimgIdxToAlphaItemIdx);
+            AVIF_ASSERT_OR_RETURN(AVIF_FALSE);
+        }
+        avifDecoderItem * alphaTileItem = meta->items.item[dimgIdxToAlphaItemIdx[dimgIdx]];
+        alphaTileItem->dimgForID = (*alphaItem)->id;
+        alphaTileItem->dimgIdx = dimgIdx;
     }
-    avifFree(alphaItemIndices);
+    avifFree(dimgIdxToAlphaItemIdx);
     *isAlphaItemInInput = AVIF_FALSE;
     alphaInfo->grid = colorInfo->grid;
     return AVIF_RESULT_OK;
@@ -5014,7 +5045,16 @@
 {
     const uint32_t previousTileCount = decoder->data->tiles.count;
     if ((info->grid.rows > 0) && (info->grid.columns > 0)) {
-        AVIF_CHECKRES(avifDecoderGenerateImageGridTiles(decoder, &info->grid, item, itemCategory));
+        // The number of tiles was verified in avifDecoderItemReadAndParse().
+        const uint32_t numTiles = info->grid.rows * info->grid.columns;
+        uint32_t * dimgIdxToItemIdx = (uint32_t *)avifAlloc(numTiles * sizeof(uint32_t));
+        AVIF_CHECKERR(dimgIdxToItemIdx != NULL, AVIF_RESULT_OUT_OF_MEMORY);
+        avifResult result = avifFillDimgIdxToItemIdxArray(dimgIdxToItemIdx, numTiles, item);
+        if (result == AVIF_RESULT_OK) {
+            result = avifDecoderGenerateImageGridTiles(decoder, item, itemCategory, dimgIdxToItemIdx, numTiles);
+        }
+        avifFree(dimgIdxToItemIdx);
+        AVIF_CHECKRES(result);
     } else {
         AVIF_CHECKERR(item->size != 0, AVIF_RESULT_MISSING_IMAGE_ITEM);
 
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 5739276..be18478 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -114,6 +114,7 @@
     add_avif_internal_gtest_with_data(avifcolrconverttest)
     add_avif_internal_gtest(avifcolrtest)
     add_avif_gtest_with_data(avifdecodetest)
+    add_avif_gtest_with_data(avifdimgtest avifincrtest_helpers)
     add_avif_gtest_with_data(avifencodetest)
 
     if(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
@@ -127,7 +128,12 @@
     add_avif_gtest(avifgridapitest)
     add_avif_gtest_with_data(avifilocextenttest)
     add_avif_gtest(avifimagetest)
-    add_avif_gtest_with_data(avifincrtest avifincrtest_helpers)
+
+    add_executable(avifincrtest gtest/avifincrtest.cc)
+    target_link_libraries(avifincrtest aviftest_helpers avifincrtest_helpers)
+    add_test(NAME avifincrtest COMMAND avifincrtest ${CMAKE_CURRENT_SOURCE_DIR}/data/)
+    register_test_for_coverage(avifincrtest ${CMAKE_CURRENT_SOURCE_DIR}/data/)
+
     add_avif_gtest_with_data(avifiostatstest)
     add_avif_gtest_with_data(avifkeyframetest)
     add_avif_gtest_with_data(aviflosslesstest)
@@ -342,6 +348,7 @@
                 avifchangesettingtest
                 avifcllitest
                 avifcolrconverttest
+                avifdimgtest
                 avifencodetest
                 avifgridapitest
                 avifincrtest
diff --git a/tests/data/README.md b/tests/data/README.md
index 8ed0c6c..2996eb9 100644
--- a/tests/data/README.md
+++ b/tests/data/README.md
@@ -363,6 +363,15 @@
 Source: Personal photo converted with `avifenc --grid 1x5 --yuv 420` at
 commit [632d131](https://github.com/AOMediaCodec/libavif/commit/632d13188f9b7faa40f20d870e792174b8b5b8e6).
 
+### File [sofa_grid1x5_420_reversed_dimg_order.avif](sofa_grid1x5_420_reversed_dimg_order.avif)
+
+![](sofa_grid1x5_420_reversed_dimg_order.avif)
+
+License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE)
+
+Source: `sofa_grid1x5_420.avif` manually edited so that the `dimg` order is
+6,5,4,3,2 instead of 2,3,4,5,6.
+
 ### File [color_grid_alpha_nogrid.avif](color_grid_alpha_nogrid.avif)
 
 ![](color_grid_alpha_nogrid.avif)
diff --git a/tests/data/sofa_grid1x5_420_reversed_dimg_order.avif b/tests/data/sofa_grid1x5_420_reversed_dimg_order.avif
new file mode 100644
index 0000000..4b83ed6
--- /dev/null
+++ b/tests/data/sofa_grid1x5_420_reversed_dimg_order.avif
Binary files differ
diff --git a/tests/gtest/avifdimgtest.cc b/tests/gtest/avifdimgtest.cc
new file mode 100644
index 0000000..c166c5b
--- /dev/null
+++ b/tests/gtest/avifdimgtest.cc
@@ -0,0 +1,69 @@
+// Copyright 2024 Google LLC
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include <algorithm>
+#include <iostream>
+#include <string>
+
+#include "avif/avif.h"
+#include "avifincrtest_helpers.h"
+#include "aviftest_helpers.h"
+#include "gtest/gtest.h"
+
+namespace avif {
+namespace {
+
+// Used to pass the data folder path to the GoogleTest suites.
+const char* data_path = nullptr;
+
+//------------------------------------------------------------------------------
+
+TEST(IncrementalTest, Dimg) {
+  testutil::AvifRwData avif =
+      testutil::ReadFile(std::string(data_path) + "sofa_grid1x5_420.avif");
+  ASSERT_NE(avif.size, 0u);
+  ImagePtr decoded(avifImageCreateEmpty());
+  ASSERT_NE(decoded, nullptr);
+  DecoderPtr decoder(avifDecoderCreate());
+  ASSERT_NE(decoder, nullptr);
+  ASSERT_EQ(
+      avifDecoderReadMemory(decoder.get(), decoded.get(), avif.data, avif.size),
+      AVIF_RESULT_OK);
+
+  testutil::AvifRwData avif_reversed_dimg_order = testutil::ReadFile(
+      std::string(data_path) + "sofa_grid1x5_420_reversed_dimg_order.avif");
+  ImagePtr decoded_reversed_dimg_order(avifImageCreateEmpty());
+  ASSERT_NE(decoded_reversed_dimg_order, nullptr);
+  ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(),
+                                  avif_reversed_dimg_order.data,
+                                  avif_reversed_dimg_order.size),
+            AVIF_RESULT_OK);
+  EXPECT_FALSE(
+      testutil::AreImagesEqual(*decoded, *decoded_reversed_dimg_order));
+
+  // Verify that it works incrementally.
+  // enable_fine_incremental_check is false because the tiles are out-of-order.
+  ASSERT_EQ(testutil::DecodeIncrementally(
+                avif_reversed_dimg_order, decoder.get(), /*is_persistent=*/true,
+                /*give_size_hint=*/true,
+                /*use_nth_image_api=*/false, *decoded_reversed_dimg_order,
+                /*cell_height=*/154, /*enable_fine_incremental_check=*/false),
+            AVIF_RESULT_OK);
+}
+
+//------------------------------------------------------------------------------
+
+}  // namespace
+}  // namespace avif
+
+int main(int argc, char** argv) {
+  ::testing::InitGoogleTest(&argc, argv);
+  if (argc < 2) {
+    std::cerr
+        << "The path to the test data folder must be provided as an argument"
+        << std::endl;
+    return 1;
+  }
+  avif::data_path = argv[1];
+  return RUN_ALL_TESTS();
+}