read: keep 'dimg' inputs as an ordered array on the derived item (#3342) The 'dimg' box of a derived image item is an ordered array of input item IDs. libavif stored the reverse: each input item carried the ID of its derived item and its position in that list, one value each. That inversion cannot express a list twice, and it has to be reconstructed by scanning every item whenever the list is needed. Store the list where the file puts it, on the derived item, and turn the read sites into membership or position lookups. avifFillDimgIdxToItemIdxArray() then reads the list directly instead of rebuilding it, and its assertion on the tile count becomes an ordinary check. Section 8.11.12.1 of ISO/IEC 14496-12 says a given value shall occur at most once within a given array, which is checked against the list, and that a value may occur in several arrays, which the single valued field could not represent. Also drops avifReferencesOld() and avifDecoderItemIsAlphaAuxOld() from eb67309: with the list, the synthetic alpha grid can hold one auxiliary item in several cells, which is what that workaround was avoiding.
diff --git a/src/read.c b/src/read.c index 3bd4787..64640e1 100644 --- a/src/read.c +++ b/src/read.c
@@ -219,6 +219,10 @@ } avifItemReference; AVIF_ARRAY_DECLARE(avifItemReferenceArray, avifItemReference, ref); +// The ordered list of input item IDs (to_item_ID) of a derived image item, in the +// order given by its 'dimg' box. +AVIF_ARRAY_DECLARE(avifItemIDArray, uint32_t, id); + // one "item" worth for decoding (all iref, iloc, iprp, etc refer to one of these) typedef struct avifDecoderItem { @@ -237,9 +241,9 @@ // If false, mergedExtents is used as an avifROData and points to a // buffer it doesn't own. avifBool partialMergedExtents; // If true, mergedExtents doesn't have all of the item data yet - uint32_t dimgForID; // if non-zero, this item is an input of derived Item #{dimgForID} - uint32_t dimgIdx; // If dimgForId is non-zero, this is the zero-based index of this item in the list of Item #{dimgForID}'s dimg. - avifBool hasDimgFrom; // whether there is a 'dimg' box with this item's id as 'fromID' + avifBool hasDimgFrom; // whether there is a 'dimg' box with this item's id as 'from_item_ID', + // even if reference_count is zero + avifItemIDArray dimgInputs; // if this item is derived, its input items in 'dimg' order // The 'thmb', 'auxl', 'cdsc' and 'prem' references from this item, in file order. avifItemReferenceArray references; avifBool hasUnsupportedEssentialProperty; // If true, this item cites a property flagged as 'essential' that libavif doesn't support (yet). Ignore the item, if so. @@ -252,6 +256,31 @@ } avifDecoderItem; AVIF_ARRAY_DECLARE(avifDecoderItemArray, avifDecoderItem *, item); +// Returns AVIF_TRUE if inputItemID is one of derivedItem's 'dimg' inputs. When it is and +// dimgIdx is not NULL, stores the 0-based position of inputItemID in that list in *dimgIdx. +// A given value occurs at most once in a 'dimg' array, so the first match is the only one. +static avifBool avifIsDimgInput(const avifDecoderItem * derivedItem, uint32_t inputItemID, uint32_t * dimgIdx) +{ + for (uint32_t i = 0; i < derivedItem->dimgInputs.count; ++i) { + if (derivedItem->dimgInputs.id[i] == inputItemID) { + if (dimgIdx != NULL) { + *dimgIdx = i; + } + return AVIF_TRUE; + } + } + return AVIF_FALSE; +} + +// Records that inputItem is an input of derivedItem, at the end of its 'dimg' order. +static avifResult avifAddDimgInput(avifDecoderItem * derivedItem, avifDecoderItem * inputItem) +{ + uint32_t * dimgInput = (uint32_t *)avifArrayPush(&derivedItem->dimgInputs); + AVIF_CHECKERR(dimgInput != NULL, AVIF_RESULT_OUT_OF_MEMORY); + *dimgInput = inputItem->id; + return AVIF_RESULT_OK; +} + // Records "this item or track is a {type} for #{toID}". static avifResult avifAddReference(avifItemReferenceArray * references, const char * type, uint32_t toID) { @@ -274,20 +303,6 @@ return AVIF_FALSE; } -// Bug oss-fuzz:553221196: The same as avifReferences() except that it emulates -// the old behavior before commit 2f8eb91 and considers only the last reference. -static avifBool avifReferencesOld(const avifItemReferenceArray * references, const char * type, uint32_t toID) -{ - for (uint32_t i = references->count; i > 0;) { - --i; - const avifItemReference * reference = &references->ref[i]; - if (!memcmp(reference->type, type, 4)) { - return reference->toID == toID; - } - } - return AVIF_FALSE; -} - // Returns AVIF_TRUE if this item or track is a {type} for at least one item or track. static avifBool avifHasReference(const avifItemReferenceArray * references, const char * type) { @@ -928,6 +943,7 @@ avifPropertyArrayDestroy(&item->properties); avifArrayDestroy(&item->extents); avifArrayDestroy(&item->references); + avifArrayDestroy(&item->dimgInputs); if (item->ownsMergedExtents) { avifRWDataFree(&item->mergedExtents); } @@ -1004,6 +1020,15 @@ avifArrayPop(&meta->items); return AVIF_RESULT_OUT_OF_MEMORY; } + if (!avifArrayCreate(&(*item)->dimgInputs, sizeof(uint32_t), 1)) { + avifArrayDestroy(&(*item)->references); + avifArrayDestroy(&(*item)->extents); + avifPropertyArrayDestroy(&(*item)->properties); + avifFree(*item); + *item = NULL; + avifArrayPop(&meta->items); + return AVIF_RESULT_OUT_OF_MEMORY; + } (*item)->id = itemID; (*item)->meta = meta; return AVIF_RESULT_OK; @@ -1324,7 +1349,7 @@ if (!memcmp(item->type, "grid", 4)) { for (uint32_t i = 0; i < item->meta->items.count; ++i) { avifDecoderItem * tile = item->meta->items.item[i]; - if (tile->dimgForID != item->id) { + if (!avifIsDimgInput(item, tile->id, NULL)) { continue; } // Tile item types were checked in avifDecoderGenerateImageTiles(), no need to do it here. @@ -1640,7 +1665,7 @@ for (uint32_t i = 0; i < gridItem->meta->items.count; ++i) { avifDecoderItem * item = gridItem->meta->items.item[i]; const avifCodecType tileCodecType = avifGetCodecType(item->type); - if ((item->dimgForID == gridItem->id) && (tileCodecType != AVIF_CODEC_TYPE_UNKNOWN)) { + if (avifIsDimgInput(gridItem, item->id, NULL) && (tileCodecType != AVIF_CODEC_TYPE_UNKNOWN)) { return tileCodecType; } } @@ -1651,22 +1676,21 @@ // to its corresponding 0-based index in the avifMeta::items array. static avifResult avifFillDimgIdxToItemIdxArray(uint32_t * dimgIdxToItemIdx, uint32_t numExpectedTiles, const avifDecoderItem * gridItem) { - const uint32_t itemIndexNotSet = UINT32_MAX; + // The input items of a derived item are kept in 'dimg' order, so a cell index is a + // position in that list. + AVIF_CHECKERR(gridItem->dimgInputs.count == numExpectedTiles, AVIF_RESULT_INVALID_IMAGE_GRID); 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; + const uint32_t inputItemID = gridItem->dimgInputs.id[dimgIdx]; + uint32_t itemIdx = UINT32_MAX; + for (uint32_t i = 0; i < gridItem->meta->items.count; ++i) { + if (gridItem->meta->items.item[i]->id == inputItemID) { + itemIdx = i; + break; + } } + AVIF_CHECKERR(itemIdx != UINT32_MAX, AVIF_RESULT_INVALID_IMAGE_GRID); + dimgIdxToItemIdx[dimgIdx] = itemIdx; } - // The number of tiles has been verified in avifDecoderItemReadAndParse(). - AVIF_ASSERT_OR_RETURN(numTiles == numExpectedTiles); return AVIF_RESULT_OK; } @@ -2380,7 +2404,7 @@ // avifDecoderAdoptGridTileCodecType() copies that property from the first cell to the grid item anyway. for (uint32_t i = 0; i < item->meta->items.count; ++i) { avifDecoderItem * inputImageItem = item->meta->items.item[i]; - if (inputImageItem->dimgForID == item->id) { + if (avifIsDimgInput(item, inputImageItem->id, NULL)) { return avifPropertyArrayFind(&inputImageItem->properties, avifGetConfigurationPropertyName(avifGetCodecType(inputImageItem->type))); } @@ -2422,7 +2446,7 @@ // Check that all input image items of the 'sato' derived image item share the same properties. for (uint32_t i = 0; i < satoItem->meta->items.count; ++i) { avifDecoderItem * inputImageItem = satoItem->meta->items.item[i]; - if (inputImageItem->dimgForID != satoItem->id) { + if (!avifIsDimgInput(satoItem, inputImageItem->id, NULL)) { continue; } @@ -2445,7 +2469,7 @@ for (uint32_t j = i + 1; j < satoItem->meta->items.count; ++j) { avifDecoderItem * otherInputImageItem = satoItem->meta->items.item[j]; - if (otherInputImageItem->dimgForID != satoItem->id) { + if (!avifIsDimgInput(satoItem, otherInputImageItem->id, NULL)) { continue; } @@ -2524,7 +2548,7 @@ // Validate that there are exactly the same number of dimg items to form the grid. uint32_t dimgItemCount = 0; for (uint32_t i = 0; i < item->meta->items.count; ++i) { - if (item->meta->items.item[i]->dimgForID == item->id) { + if (avifIsDimgInput(item, item->meta->items.item[i]->id, NULL)) { ++dimgItemCount; } } @@ -3465,23 +3489,22 @@ if (!memcmp(irefHeader.type, "thmb", 4) || !memcmp(irefHeader.type, "auxl", 4) || !memcmp(irefHeader.type, "cdsc", 4) || !memcmp(irefHeader.type, "prem", 4)) { // Section 8.11.12.1 of ISO/IEC 14496-12: - // The items linked to are then represented by an array of to_item_IDs. + // The items linked to are then represented by an array of to_item_IDs; + // within a given array, a given value shall occur at most once. + // That last requirement is not checked for these reference types. // Several boxes of the same type may also share a from_item_ID, so an item can // be the source of more than one reference of a given type. Every target is kept. AVIF_CHECKRES(avifAddReference(&item->references, (const char *)irefHeader.type, toID)); } else if (!memcmp(irefHeader.type, "dimg", 4)) { // derived images refer in the opposite direction - avifDecoderItem * dimg; - AVIF_CHECKRES(avifMetaFindOrCreateItem(meta, toID, &dimg)); + avifDecoderItem * dimgInput; + AVIF_CHECKRES(avifMetaFindOrCreateItem(meta, toID, &dimgInput)); // Section 8.11.12.1 of ISO/IEC 14496-12: // The items linked to are then represented by an array of to_item_IDs; // within a given array, a given value shall occur at most once. - AVIF_CHECKERR(dimg->dimgForID != fromID, AVIF_RESULT_INVALID_IMAGE_GRID); - // A given value may occur within multiple arrays but this is not supported by libavif. - AVIF_CHECKERR(dimg->dimgForID == 0, AVIF_RESULT_NOT_IMPLEMENTED); - dimg->dimgForID = fromID; - dimg->dimgIdx = refIndex; + AVIF_CHECKERR(!avifIsDimgInput(item, toID, NULL), AVIF_RESULT_INVALID_IMAGE_GRID); + AVIF_CHECKRES(avifAddDimgInput(item, dimgInput)); } } @@ -4529,8 +4552,7 @@ if (hasGainmap) { AVIF_CHECKRES(avifMetaFindOrCreateItem(meta, /*itemID=*/3, &tmapItem)); memcpy(tmapItem->type, "tmap", 4); - colorItem->dimgForID = tmapItem->id; - colorItem->dimgIdx = 0; + AVIF_CHECKRES(avifAddDimgInput(tmapItem, colorItem)); // avifDecoderReset() requires the 'tmap' item to be an alternative to the primary item. avifEntityToGroup * group = avifArrayPush(&data->meta->entityToGroups); @@ -4550,8 +4572,7 @@ memcpy(gainmapItem->type, infeType, 4); gainmapItem->width = gainmapWidth; gainmapItem->height = gainmapHeight; - gainmapItem->dimgForID = tmapItem->id; - gainmapItem->dimgIdx = 1; + AVIF_CHECKRES(avifAddDimgInput(tmapItem, gainmapItem)); } // Property with fixed index 1. @@ -5577,17 +5598,6 @@ return auxCProp && isAlphaURN(auxCProp->u.auxC.auxType); } -// Bug oss-fuzz:553221196: The same as avifDecoderItemIsAlphaAux() except that -// it calls avifReferencesOld() instead of avifReferences() to emulate the old -// behavior before commit 2f8eb91. -static avifBool avifDecoderItemIsAlphaAuxOld(const avifDecoderItem * item, uint32_t colorItemId) -{ - if (!avifReferencesOld(&item->references, "auxl", colorItemId)) - return AVIF_FALSE; - const avifProperty * auxCProp = avifPropertyArrayFind(&item->properties, "auxC"); - return auxCProp && isAlphaURN(auxCProp->u.auxC.auxType); -} - // Finds the alpha item whose parent item is colorItem and sets it in the alphaItem output parameter. Returns AVIF_RESULT_OK on // success. Note that *alphaItem can be NULL even if the return value is AVIF_RESULT_OK. If the colorItem is a grid and the alpha // item is represented as a set of auxl items to each color tile, then a fake item will be created and *isAlphaItemInInput will be @@ -5634,22 +5644,21 @@ uint32_t alphaItemCount = 0; for (uint32_t i = 0; i < meta->items.count; ++i) { const avifDecoderItem * const item = meta->items.item[i]; - if (item->dimgForID == colorItem->id) { + uint32_t colorTileIdx; + if (avifIsDimgInput(colorItem, item->id, &colorTileIdx)) { avifBool seenAlphaForCurrentItem = AVIF_FALSE; for (uint32_t j = 0; j < meta->items.count; ++j) { avifDecoderItem * auxlItem = meta->items.item[j]; - if (avifDecoderItemIsAlphaAuxOld(auxlItem, item->id)) { - if (seenAlphaForCurrentItem || auxlItem->dimgForID != 0 || item->dimgIdx >= tileCount || - dimgIdxToAlphaItemIdx[item->dimgIdx] != itemIndexNotSet) { + if (avifDecoderItemIsAlphaAux(auxlItem, item->id)) { + if (seenAlphaForCurrentItem || colorTileIdx >= tileCount || dimgIdxToAlphaItemIdx[colorTileIdx] != 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. // * 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; } - dimgIdxToAlphaItemIdx[item->dimgIdx] = j; + dimgIdxToAlphaItemIdx[colorTileIdx] = j; ++alphaItemCount; seenAlphaForCurrentItem = AVIF_TRUE; } @@ -5700,8 +5709,14 @@ AVIF_ASSERT_NOT_REACHED_OR_RETURN; } avifDecoderItem * alphaTileItem = meta->items.item[dimgIdxToAlphaItemIdx[dimgIdx]]; - alphaTileItem->dimgForID = (*alphaItem)->id; - alphaTileItem->dimgIdx = dimgIdx; + // The alpha grid is synthesized here rather than read from a file, so one item + // may stand in for several cells: an auxiliary item can be the alpha of more + // than one color tile. + const avifResult addResult = avifAddDimgInput(*alphaItem, alphaTileItem); + if (addResult != AVIF_RESULT_OK) { + avifFree(dimgIdxToAlphaItemIdx); + return addResult; + } } avifFree(dimgIdxToAlphaItemIdx); *isAlphaItemInInput = AVIF_FALSE; @@ -5806,12 +5821,13 @@ uint32_t numDimgItemIDs = 0; for (uint32_t otherItemIndex = 0; otherItemIndex < data->meta->items.count; ++otherItemIndex) { avifDecoderItem * otherItem = data->meta->items.item[otherItemIndex]; - if (otherItem->dimgForID != item->id) { + uint32_t otherDimgIdx; + if (!avifIsDimgInput(item, otherItem->id, &otherDimgIdx)) { continue; } - if (otherItem->dimgIdx < 2) { - AVIF_ASSERT_OR_RETURN(dimgItemIDs[otherItem->dimgIdx] == 0); - dimgItemIDs[otherItem->dimgIdx] = otherItem->id; + if (otherDimgIdx < 2) { + AVIF_ASSERT_OR_RETURN(dimgItemIDs[otherDimgIdx] == 0); + dimgItemIDs[otherDimgIdx] = otherItem->id; } numDimgItemIDs++; } @@ -6470,7 +6486,7 @@ for (uint32_t i = 0; i < data->meta->items.count; ++i) { avifDecoderItem * inputImageItem = data->meta->items.item[i]; - if (inputImageItem->dimgForID == sampleTransformItem->id) { + if (avifIsDimgInput(sampleTransformItem, inputImageItem->id, NULL)) { ++data->sampleTransformNumInputImageItems; } } @@ -6493,7 +6509,8 @@ uint32_t numExtraInputImageItems = 0; for (uint32_t i = 0; i < data->meta->items.count; ++i) { avifDecoderItem * inputImageItem = data->meta->items.item[i]; - if (inputImageItem->dimgForID != sampleTransformItem->id) { + uint32_t inputDimgIdx; + if (!avifIsDimgInput(sampleTransformItem, inputImageItem->id, &inputDimgIdx)) { continue; } if (avifDecoderItemShouldBeSkipped(inputImageItem)) { @@ -6501,8 +6518,8 @@ return AVIF_RESULT_DECODE_SAMPLE_TRANSFORM_FAILED; } - AVIF_ASSERT_OR_RETURN(inputImageItem->dimgIdx < AVIF_SAMPLE_TRANSFORM_MAX_NUM_INPUT_IMAGE_ITEMS); - avifItemCategory * category = &data->sampleTransformInputImageItems[inputImageItem->dimgIdx]; + AVIF_ASSERT_OR_RETURN(inputDimgIdx < AVIF_SAMPLE_TRANSFORM_MAX_NUM_INPUT_IMAGE_ITEMS); + avifItemCategory * category = &data->sampleTransformInputImageItems[inputDimgIdx]; avifBool foundItem = AVIF_FALSE; for (int c = AVIF_ITEM_COLOR; c < AVIF_ITEM_CATEGORY_COUNT; ++c) { if (mainItems[c] && inputImageItem->id == mainItems[c]->id) {
diff --git a/tests/data/README.md b/tests/data/README.md index c0875ff..86ff1e0 100644 --- a/tests/data/README.md +++ b/tests/data/README.md
@@ -556,6 +556,28 @@ associations are manually edited from 2,3,4,5 to 2,3,4,A. The item with ID 0xA is an item used in two grids. +### File [color_grid_alpha_item_shared_in_auxl.avif](color_grid_alpha_item_shared_in_auxl.avif) + +License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE) + +Source: Same as `color_grid_alpha_nogrid.avif` + +The color plane is a grid of two tiles and each tile has its own alpha auxiliary +item. The `from_item_ID` of the second `auxl` box is manually edited from 6 to 5, so +item 5 is the alpha auxiliary of both tiles and item 6 of neither. Only that one +field differs from the source file. + +### File [color_grid_alpha_grid_shared_tiles.avif](color_grid_alpha_grid_shared_tiles.avif) + +License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE) + +Source: Same as `color_grid_alpha_grid_tile_shared_in_dimg.avif` + +The `dimg` associations are manually edited so that both grids list the same +cells: the color grid from 2,3,4,A to 2,3,4,5 and the alpha grid from 7,8,9,A to +2,3,4,5. Each of the items 2 to 5 is then an input of two derived items. Five +bytes differ from the source file. + ### File [alpha_noispe.avif](alpha_noispe.avif) License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE)
diff --git a/tests/data/color_grid_alpha_grid_shared_tiles.avif b/tests/data/color_grid_alpha_grid_shared_tiles.avif new file mode 100644 index 0000000..71b512f --- /dev/null +++ b/tests/data/color_grid_alpha_grid_shared_tiles.avif Binary files differ
diff --git a/tests/data/color_grid_alpha_item_shared_in_auxl.avif b/tests/data/color_grid_alpha_item_shared_in_auxl.avif new file mode 100644 index 0000000..e055470 --- /dev/null +++ b/tests/data/color_grid_alpha_item_shared_in_auxl.avif Binary files differ
diff --git a/tests/gtest/avifdimgtest.cc b/tests/gtest/avifdimgtest.cc index e3ce635..9d555d6 100644 --- a/tests/gtest/avifdimgtest.cc +++ b/tests/gtest/avifdimgtest.cc
@@ -31,6 +31,10 @@ AVIF_RESULT_INVALID_IMAGE_GRID); } +// Item 10 is a cell of both the color grid and the alpha grid. Sharing a cell +// between two derived items is allowed, so this file now reaches the tile +// consistency check and is refused there: the av1C of a color tile cannot match +// the av1C of the alpha tiles it stands among. TEST(DimgTest, ItemShared) { testutil::AvifRwData avif = testutil::ReadFile(std::string(data_path) + @@ -42,7 +46,79 @@ ASSERT_NE(decoder, nullptr); ASSERT_EQ(avifDecoderReadMemory(decoder.get(), reference.get(), avif.data, avif.size), - AVIF_RESULT_NOT_IMPLEMENTED); + AVIF_RESULT_BMFF_PARSE_FAILED); +} + +//------------------------------------------------------------------------------ + +// The color grid and the alpha grid are built from the same four cells, so each +// of the items 2 to 5 is an input of two derived items. The ordered 'dimg' list +// can express that, and the tiles stay consistent because both grids use the +// same ones. +TEST(DimgTest, TilesSharedBetweenGrids) { + testutil::AvifRwData avif = testutil::ReadFile( + std::string(data_path) + "color_grid_alpha_grid_shared_tiles.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); + ASSERT_NE(decoded->alphaPlane, nullptr); +} + +//------------------------------------------------------------------------------ + +// One item is the alpha auxiliary of both tiles of a color grid. The synthetic +// alpha grid then holds that item in both cells, which the ordered dimg input +// list can express. Every cell carries the same alpha, so the two halves of the +// alpha plane match each other while they differ in the file this one is +// derived from. +TEST(DimgTest, AlphaItemSharedBetweenTiles) { + testutil::AvifRwData avif = testutil::ReadFile( + std::string(data_path) + "color_grid_alpha_item_shared_in_auxl.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); + ASSERT_NE(decoded->alphaPlane, nullptr); + + // The grid is two cells tall, so a row in the second cell repeats a row of + // the first one. The cell height is the tile height, not half the output + // height. + testutil::AvifRwData reference = testutil::ReadFile( + std::string(data_path) + "color_grid_alpha_nogrid.avif"); + ASSERT_NE(reference.size, 0u); + ImagePtr referenceDecoded(avifImageCreateEmpty()); + ASSERT_NE(referenceDecoded, nullptr); + DecoderPtr referenceDecoder(avifDecoderCreate()); + ASSERT_NE(referenceDecoder, nullptr); + ASSERT_EQ( + avifDecoderReadMemory(referenceDecoder.get(), referenceDecoded.get(), + reference.data, reference.size), + AVIF_RESULT_OK); + ASSERT_NE(referenceDecoded->alphaPlane, nullptr); + ASSERT_EQ(decoded->width, referenceDecoded->width); + ASSERT_EQ(decoded->height, referenceDecoded->height); + + // The first cell is unchanged, and the second one now repeats it instead of + // carrying the alpha of the item that no longer claims that tile. + const uint32_t width = decoded->width; + EXPECT_EQ(memcmp(decoded->alphaPlane, referenceDecoded->alphaPlane, width), + 0); + const uint8_t* lastRow = + decoded->alphaPlane + + (size_t)decoded->alphaRowBytes * (decoded->height - 1); + const uint8_t* referenceLastRow = + referenceDecoded->alphaPlane + + (size_t)referenceDecoded->alphaRowBytes * (referenceDecoded->height - 1); + EXPECT_NE(memcmp(lastRow, referenceLastRow, width), 0); } //------------------------------------------------------------------------------