Keep every target of a 'thmb', 'auxl', 'cdsc' or 'prem' reference (#3331)
A SingleItemTypeReferenceBox links to an array of to_item_IDs, and several
boxes of one type may share a from_item_ID. libavif stored these four relations
in a single valued field per item, and the parsing loop overwrote it per entry,
so the target parsed last silently won and the others were dropped, with no
error and an exit code of zero.
Store the references in a per item array instead, and turn the read sites into
membership checks. 'dimg' is unchanged: it refers in the opposite direction and
already refuses what it cannot represent.
The MinimizedImageBox path stored the one bit alpha_is_premultiplied value in
premByID, which the decoder compares against the alpha item ID. That ID is 2 in
this path, so the comparison never matched and a premultiplied image decoded as
straight alpha, with no error and an exit code of zero.
The parameterized test in avifminitest.cc already compares a MinimizedImageBox
against a regular MetaBox for the same image, and AreImagesEqual() compares
alphaPremultiplied. No parameter set marks the alpha as premultiplied though, so
the flag was never exercised. Add a test that does.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 31d7841..745566b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -42,6 +42,12 @@
specific data after the entity_id array are no longer rejected.
* Reset Sample Transform decoder state in avifDecoderReset() so repeated resets
and avifDecoderSetSource() calls do not fail.
+* Support a 'thmb', 'auxl', 'cdsc' or 'prem' item reference that links one item
+ to several items. Only the target parsed last used to be kept, silently
+ dropping the others.
+* Keep the premultiplied alpha flag of a MinimizedImageBox. The one bit
+ alpha_is_premultiplied value was stored in a field compared against the alpha
+ item ID, so such an image used to decode as straight alpha.
## [1.4.2] - 2026-05-26
diff --git a/src/read.c b/src/read.c
index 405b24f..e921369 100644
--- a/src/read.c
+++ b/src/read.c
@@ -209,6 +209,16 @@
AVIF_ARRAY_DECLARE(avifExtentArray, avifExtent, extent);
+// One entry of a SingleItemTypeReferenceBox: "this item is a {type} for item #{toID}".
+// ISO/IEC 14496-12 section 8.11.12.1 allows an array of to_item_IDs per box, and several
+// boxes of the same type may share a from_item_ID, so one item can hold several of these.
+typedef struct avifItemReference
+{
+ uint8_t type[4];
+ uint32_t toID;
+} avifItemReference;
+AVIF_ARRAY_DECLARE(avifItemReferenceArray, avifItemReference, ref);
+
// one "item" worth for decoding (all iref, iloc, iprp, etc refer to one of these)
typedef struct avifDecoderItem
{
@@ -227,13 +237,11 @@
// 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 thumbnailForID; // if non-zero, this item is a thumbnail for Item #{thumbnailForID}
- uint32_t auxForID; // if non-zero, this item is an auxC plane for Item #{auxForID}
- uint32_t descForID; // if non-zero, this item is a content description for Item #{descForID}
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'
- uint32_t premByID; // if non-zero, this item is premultiplied by Item #{premByID}
+ // 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.
avifBool ipmaSeen; // if true, this item already received a property association
avifBool progressive; // if true, this item has progressive layers (a1lx), but does not select a specific layer (the layer_id value in lsel is set to 0xFFFF)
@@ -244,6 +252,39 @@
} avifDecoderItem;
AVIF_ARRAY_DECLARE(avifDecoderItemArray, avifDecoderItem *, item);
+// Records "this item is a {type} for item #{toID}".
+static avifResult avifDecoderItemAddReference(avifDecoderItem * item, const char * type, uint32_t toID)
+{
+ avifItemReference * reference = (avifItemReference *)avifArrayPush(&item->references);
+ AVIF_CHECKERR(reference != NULL, AVIF_RESULT_OUT_OF_MEMORY);
+ memcpy(reference->type, type, 4);
+ reference->toID = toID;
+ return AVIF_RESULT_OK;
+}
+
+// Returns AVIF_TRUE if this item is a {type} for item #{toID}.
+static avifBool avifDecoderItemReferences(const avifDecoderItem * item, const char * type, uint32_t toID)
+{
+ for (uint32_t i = 0; i < item->references.count; ++i) {
+ const avifItemReference * reference = &item->references.ref[i];
+ if (!memcmp(reference->type, type, 4) && reference->toID == toID) {
+ return AVIF_TRUE;
+ }
+ }
+ return AVIF_FALSE;
+}
+
+// Returns AVIF_TRUE if this item is a {type} for at least one item.
+static avifBool avifDecoderItemHasReference(const avifDecoderItem * item, const char * type)
+{
+ for (uint32_t i = 0; i < item->references.count; ++i) {
+ if (!memcmp(item->references.ref[i].type, type, 4)) {
+ return AVIF_TRUE;
+ }
+ }
+ return AVIF_FALSE;
+}
+
// grid storage
typedef struct avifImageGrid
{
@@ -870,6 +911,7 @@
avifDecoderItem * item = meta->items.item[i];
avifPropertyArrayDestroy(&item->properties);
avifArrayDestroy(&item->extents);
+ avifArrayDestroy(&item->references);
if (item->ownsMergedExtents) {
avifRWDataFree(&item->mergedExtents);
}
@@ -938,6 +980,14 @@
avifArrayPop(&meta->items);
return AVIF_RESULT_OUT_OF_MEMORY;
}
+ if (!avifArrayCreate(&(*item)->references, sizeof(avifItemReference), 1)) {
+ 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;
@@ -1897,7 +1947,7 @@
continue;
}
- if ((colorId > 0) && (item->descForID != colorId)) {
+ if ((colorId > 0) && !avifDecoderItemReferences(item, "cdsc", colorId)) {
// Not a content description (metadata) for the colorOBU, skip it
continue;
}
@@ -3390,12 +3440,13 @@
AVIF_CHECKRES(avifCheckItemID("iref", toID, diag));
// Read this reference as "{fromID} is a {irefType} for {toID}"
- if (!memcmp(irefHeader.type, "thmb", 4)) {
- item->thumbnailForID = toID;
- } else if (!memcmp(irefHeader.type, "auxl", 4)) {
- item->auxForID = toID;
- } else if (!memcmp(irefHeader.type, "cdsc", 4)) {
- item->descForID = toID;
+ 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.
+ // 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(avifDecoderItemAddReference(item, (const char *)irefHeader.type, toID));
} else if (!memcmp(irefHeader.type, "dimg", 4)) {
// derived images refer in the opposite direction
avifDecoderItem * dimg;
@@ -3409,8 +3460,6 @@
AVIF_CHECKERR(dimg->dimgForID == 0, AVIF_RESULT_NOT_IMPLEMENTED);
dimg->dimgForID = fromID;
dimg->dimgIdx = refIndex;
- } else if (!memcmp(irefHeader.type, "prem", 4)) {
- item->premByID = toID;
}
}
@@ -4532,8 +4581,10 @@
if (hasAlpha) {
// Property with fixed index 7.
- alphaItem->auxForID = colorItem->id;
- colorItem->premByID = alphaIsPremultiplied;
+ AVIF_CHECKRES(avifDecoderItemAddReference(alphaItem, "auxl", colorItem->id));
+ if (alphaIsPremultiplied) {
+ AVIF_CHECKRES(avifDecoderItemAddReference(colorItem, "prem", alphaItem->id));
+ }
avifProperty * alphaAuxProp = avifMetaCreateProperty(meta, "auxC");
AVIF_CHECKERR(alphaAuxProp, AVIF_RESULT_OUT_OF_MEMORY);
static_assert(sizeof(alphaAuxProp->u.auxC.auxType) >= sizeof(AVIF_URN_ALPHA0), "");
@@ -4757,7 +4808,7 @@
avifDecoderItem * exifItem;
AVIF_CHECKRES(avifMetaFindOrCreateItem(meta, /*itemID=*/6, &exifItem));
memcpy(exifItem->type, "Exif", 4);
- exifItem->descForID = colorItem->id; // 'cdsc'
+ AVIF_CHECKRES(avifDecoderItemAddReference(exifItem, "cdsc", colorItem->id));
avifExtent * exifExtent = (avifExtent *)avifArrayPush(&exifItem->extents);
AVIF_CHECKERR(exifExtent, AVIF_RESULT_OUT_OF_MEMORY);
@@ -4772,7 +4823,7 @@
AVIF_CHECKRES(avifMetaFindOrCreateItem(meta, /*itemID=*/7, &xmpItem));
memcpy(xmpItem->type, "mime", 4);
memcpy(xmpItem->contentType.contentType, AVIF_CONTENT_TYPE_XMP, sizeof(AVIF_CONTENT_TYPE_XMP));
- xmpItem->descForID = colorItem->id; // 'cdsc'
+ AVIF_CHECKRES(avifDecoderItemAddReference(xmpItem, "cdsc", colorItem->id));
avifExtent * xmpExtent = (avifExtent *)avifArrayPush(&xmpItem->extents);
AVIF_CHECKERR(xmpExtent, AVIF_RESULT_OUT_OF_MEMORY);
@@ -5291,7 +5342,8 @@
static avifBool avifDecoderItemShouldBeSkipped(const avifDecoderItem * item)
{
return !item->size || item->hasUnsupportedEssentialProperty ||
- (avifGetCodecType(item->type) == AVIF_CODEC_TYPE_UNKNOWN && memcmp(item->type, "grid", 4)) || item->thumbnailForID != 0;
+ (avifGetCodecType(item->type) == AVIF_CODEC_TYPE_UNKNOWN && memcmp(item->type, "grid", 4)) ||
+ avifDecoderItemHasReference(item, "thmb");
}
avifResult avifDecoderParse(avifDecoder * decoder)
@@ -5489,7 +5541,7 @@
// item.
static avifBool avifDecoderItemIsAlphaAux(const avifDecoderItem * item, uint32_t colorItemId)
{
- if (item->auxForID != colorItemId)
+ if (!avifDecoderItemReferences(item, "auxl", colorItemId))
return AVIF_FALSE;
const avifProperty * auxCProp = avifPropertyArrayFind(&item->properties, "auxC");
return auxCProp && isAlphaURN(auxCProp->u.auxC.auxType);
@@ -5703,7 +5755,7 @@
{
for (uint32_t itemIndex = 0; itemIndex < data->meta->items.count; ++itemIndex) {
avifDecoderItem * item = data->meta->items.item[itemIndex];
- if (!item->size || item->hasUnsupportedEssentialProperty || item->thumbnailForID != 0) {
+ if (!item->size || item->hasUnsupportedEssentialProperty || avifDecoderItemHasReference(item, "thmb")) {
continue;
}
if (!memcmp(item->type, "tmap", 4)) {
@@ -6012,7 +6064,7 @@
for (uint32_t itemIndex = 0; itemIndex < data->meta->items.count; ++itemIndex) {
avifDecoderItem * item = data->meta->items.item[itemIndex];
if (!memcmp(item->type, "sato", 4) && item->id != data->meta->primaryItemID && item->size != 0 &&
- !item->hasUnsupportedEssentialProperty && item->thumbnailForID == 0 &&
+ !item->hasUnsupportedEssentialProperty && !avifDecoderItemHasReference(item, "thmb") &&
avifIsPreferredAlternativeTo(data, item->id, data->meta->primaryItemID)) {
return item;
}
@@ -6450,8 +6502,8 @@
AVIF_CHECKERR(!mainItems[alphaCategory] == !mainItems[AVIF_ITEM_ALPHA], AVIF_RESULT_NOT_IMPLEMENTED);
if (mainItems[alphaCategory] != NULL) {
AVIF_CHECKERR(isAlphaInputImageItemInInput == isAlphaItemInInput, AVIF_RESULT_NOT_IMPLEMENTED);
- AVIF_CHECKERR((mainItems[*category]->premByID == mainItems[alphaCategory]->id) ==
- (mainItems[AVIF_ITEM_COLOR]->premByID == mainItems[AVIF_ITEM_ALPHA]->id),
+ AVIF_CHECKERR(avifDecoderItemReferences(mainItems[*category], "prem", mainItems[alphaCategory]->id) ==
+ avifDecoderItemReferences(mainItems[AVIF_ITEM_COLOR], "prem", mainItems[AVIF_ITEM_ALPHA]->id),
AVIF_RESULT_NOT_IMPLEMENTED);
AVIF_CHECKRES(avifDecoderItemReadAndParse(decoder,
mainItems[alphaCategory],
@@ -6551,8 +6603,8 @@
decoder->image->width = mainItems[AVIF_ITEM_COLOR]->width;
decoder->image->height = mainItems[AVIF_ITEM_COLOR]->height;
decoder->alphaPresent = (mainItems[AVIF_ITEM_ALPHA] != NULL);
- decoder->image->alphaPremultiplied = decoder->alphaPresent &&
- (mainItems[AVIF_ITEM_COLOR]->premByID == mainItems[AVIF_ITEM_ALPHA]->id);
+ decoder->image->alphaPremultiplied =
+ decoder->alphaPresent && avifDecoderItemReferences(mainItems[AVIF_ITEM_COLOR], "prem", mainItems[AVIF_ITEM_ALPHA]->id);
if (mainItems[AVIF_ITEM_ALPHA]) {
alphaProperties = &mainItems[AVIF_ITEM_ALPHA]->properties;
diff --git a/tests/data/README.md b/tests/data/README.md
index 2b59153..c0875ff 100644
--- a/tests/data/README.md
+++ b/tests/data/README.md
@@ -54,6 +54,27 @@
as invalid and ignores it. The behavior changed starting with libpng 1.6.47.
See https://github.com/pnggroup/libpng/blob/libpng16/CHANGES#L6243-L6246.
+### File [circle_auxl_two_targets.avif](circle_auxl_two_targets.avif)
+
+
+
+License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE)
+
+Source: `avifenc -q 60 -y 420 --exif exif.bin circle-trns-after-plte.png`, where
+exif.bin is a minimal 14-byte TIFF header, with the `auxl` reference from the
+alpha item 2 manually edited to list the two targets 1,3 instead of the single
+target 1. Item 1 is the primary color item and item 3 is the Exif item.
+
+### File [circle_cdsc_two_targets.avif](circle_cdsc_two_targets.avif)
+
+
+
+License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE)
+
+Source: Same as `circle_auxl_two_targets.avif`, with the `cdsc` reference from
+the Exif item 3 manually edited to list the two targets 1,2 instead of the
+single target 1. Item 2 is the alpha item.
+
### File [circle_custom_properties.avif](circle_custom_properties.avif)

diff --git a/tests/data/circle_auxl_two_targets.avif b/tests/data/circle_auxl_two_targets.avif
new file mode 100644
index 0000000..063c7ee
--- /dev/null
+++ b/tests/data/circle_auxl_two_targets.avif
Binary files differ
diff --git a/tests/data/circle_cdsc_two_targets.avif b/tests/data/circle_cdsc_two_targets.avif
new file mode 100644
index 0000000..6d3b6d6
--- /dev/null
+++ b/tests/data/circle_cdsc_two_targets.avif
Binary files differ
diff --git a/tests/gtest/avifmetadatatest.cc b/tests/gtest/avifmetadatatest.cc
index 28dfb26..7fc531b 100644
--- a/tests/gtest/avifmetadatatest.cc
+++ b/tests/gtest/avifmetadatatest.cc
@@ -468,6 +468,48 @@
}
//------------------------------------------------------------------------------
+// Multiple targets in a single item type reference box
+
+// Section 8.11.12.1 of ISO/IEC 14496-12 represents the targets of a
+// SingleItemTypeReferenceBox as an array of to_item_IDs. libavif used to keep
+// one target per item, so the target parsed last silently won.
+// In circle_cdsc_two_targets.avif the Exif item describes items [1, 2]. The
+// Exif metadata used to disappear without any error, exit code or diagnostic;
+// swapping the two IDs made it reappear. It must survive either way.
+TEST(MetadataTest, CdscWithMultipleTargets) {
+ ImagePtr decoded(avifImageCreateEmpty());
+ ASSERT_NE(decoded, nullptr);
+ DecoderPtr decoder(avifDecoderCreate());
+ ASSERT_NE(decoder, nullptr);
+ ASSERT_EQ(
+ avifDecoderReadFile(
+ decoder.get(), decoded.get(),
+ (std::string(data_path) + "circle_cdsc_two_targets.avif").c_str()),
+ AVIF_RESULT_OK);
+ EXPECT_NE(decoded->exif.size, 0u);
+ EXPECT_NE(decoded->exif.data, nullptr);
+}
+
+// Same for the auxiliary item relation. The consequence is heavier than for
+// cdsc: in circle_auxl_two_targets.avif the alpha item is declared auxiliary
+// for items [1, 3], so the alpha plane used to be dropped and a transparent
+// image decoded as opaque, with no error, no diagnostic and an exit code of
+// zero.
+TEST(MetadataTest, AuxlWithMultipleTargets) {
+ ImagePtr decoded(avifImageCreateEmpty());
+ ASSERT_NE(decoded, nullptr);
+ DecoderPtr decoder(avifDecoderCreate());
+ ASSERT_NE(decoder, nullptr);
+ ASSERT_EQ(
+ avifDecoderReadFile(
+ decoder.get(), decoded.get(),
+ (std::string(data_path) + "circle_auxl_two_targets.avif").c_str()),
+ AVIF_RESULT_OK);
+ ASSERT_NE(decoded->alphaPlane, nullptr);
+ EXPECT_NE(decoded->alphaRowBytes, 0u);
+}
+
+//------------------------------------------------------------------------------
} // namespace
} // namespace avif
diff --git a/tests/gtest/avifminitest.cc b/tests/gtest/avifminitest.cc
index 32e4620..6a7c159 100644
--- a/tests/gtest/avifminitest.cc
+++ b/tests/gtest/avifminitest.cc
@@ -17,7 +17,8 @@
: public testing::TestWithParam<std::tuple<
/*width=*/int, /*height=*/int, /*depth=*/int, avifPixelFormat,
avifPlanesFlags, avifRange, /*create_icc=*/bool, /*create_exif=*/bool,
- /*create_xmp=*/bool, avifTransformFlags, /*create_hdr=*/bool>> {};
+ /*create_xmp=*/bool, avifTransformFlags, /*create_hdr=*/bool,
+ /*create_premul=*/bool>> {};
TEST_P(AvifMinimizedImageBoxTest, All) {
const int width = std::get<0>(GetParam());
@@ -31,11 +32,13 @@
const bool create_xmp = std::get<8>(GetParam());
const avifTransformFlags create_transform_flags = std::get<9>(GetParam());
const bool create_hdr = std::get<10>(GetParam());
+ const bool create_premul = std::get<11>(GetParam());
ImagePtr image =
testutil::CreateImage(width, height, depth, format, planes, range);
ASSERT_NE(image, nullptr);
testutil::FillImageGradient(image.get()); // The pixels do not matter.
+ image->alphaPremultiplied = create_premul ? AVIF_TRUE : AVIF_FALSE;
if (create_icc) {
ASSERT_EQ(avifImageSetProfileICC(image.get(), testutil::kSampleIcc.data(),
testutil::kSampleIcc.size()),
@@ -119,6 +122,12 @@
EXPECT_TRUE(
testutil::AreImagesEqual(*decoded_meta.get(), *decoded_mini.get()));
EXPECT_EQ(decoded_meta->gainMap != nullptr, decoded_mini->gainMap != nullptr);
+ // AreImagesEqual() only compares alphaPremultiplied when an alpha plane is
+ // present and the image is not opaque, so assert the flag on its own too.
+ EXPECT_EQ(decoded_mini->alphaPremultiplied, decoded_meta->alphaPremultiplied);
+ if (create_premul && (planes & AVIF_PLANES_A)) {
+ EXPECT_TRUE(decoded_mini->alphaPremultiplied);
+ }
if (create_hdr) {
ASSERT_NE(decoded_meta->gainMap, nullptr);
ASSERT_NE(decoded_mini->gainMap, nullptr);
@@ -129,6 +138,8 @@
}
}
+//------------------------------------------------------------------------------
+
INSTANTIATE_TEST_SUITE_P(OnePixel, AvifMinimizedImageBoxTest,
Combine(/*width=*/Values(1), /*height=*/Values(1),
/*depth=*/Values(8),
@@ -139,7 +150,8 @@
/*create_exif=*/Values(false, true),
/*create_xmp=*/Values(false, true),
Values(AVIF_TRANSFORM_NONE),
- /*create_hdr=*/Values(false)));
+ /*create_hdr=*/Values(false),
+ /*create_premul=*/Values(false)));
INSTANTIATE_TEST_SUITE_P(
DepthsSubsamplings, AvifMinimizedImageBoxTest,
@@ -150,7 +162,7 @@
Values(AVIF_PLANES_ALL), Values(AVIF_RANGE_FULL),
/*create_icc=*/Values(false), /*create_exif=*/Values(false),
/*create_xmp=*/Values(false), Values(AVIF_TRANSFORM_NONE),
- /*create_hdr=*/Values(false)));
+ /*create_hdr=*/Values(false), /*create_premul=*/Values(false)));
INSTANTIATE_TEST_SUITE_P(
Dimensions, AvifMinimizedImageBoxTest,
@@ -158,7 +170,8 @@
Values(AVIF_PIXEL_FORMAT_YUV444), Values(AVIF_PLANES_ALL),
Values(AVIF_RANGE_FULL), /*create_icc=*/Values(true),
/*create_exif=*/Values(true), /*create_xmp=*/Values(true),
- Values(AVIF_TRANSFORM_NONE), /*create_hdr=*/Values(false)));
+ Values(AVIF_TRANSFORM_NONE), /*create_hdr=*/Values(false),
+ /*create_premul=*/Values(false, true)));
INSTANTIATE_TEST_SUITE_P(
Orientation, AvifMinimizedImageBoxTest,
@@ -169,7 +182,7 @@
Values(AVIF_TRANSFORM_NONE, AVIF_TRANSFORM_IROT,
AVIF_TRANSFORM_IMIR,
AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR),
- /*create_hdr=*/Values(false)));
+ /*create_hdr=*/Values(false), /*create_premul=*/Values(false)));
INSTANTIATE_TEST_SUITE_P(
Hdr, AvifMinimizedImageBoxTest,
@@ -178,7 +191,8 @@
Values(AVIF_PLANES_YUV, AVIF_PLANES_ALL), Values(AVIF_RANGE_FULL),
/*create_icc=*/Values(false),
/*create_exif=*/Values(false), /*create_xmp=*/Values(false),
- Values(AVIF_TRANSFORM_NONE), /*create_hdr=*/Values(true)));
+ Values(AVIF_TRANSFORM_NONE), /*create_hdr=*/Values(true),
+ /*create_premul=*/Values(false)));
//------------------------------------------------------------------------------