Image Sequence metadata support (colr/EXIF/XMP/transformations)

* trak boxes now optionally have a meta box containing EXIF/XMP
* Sample tables stsd boxes now carry property boxes (colr, transformations)
* Added proper read support for the above features
diff --git a/src/read.c b/src/read.c
index 7784396..183d43b 100644
--- a/src/read.c
+++ b/src/read.c
@@ -88,43 +88,7 @@
 
 struct avifMeta;
 
-// one "item" worth for decoding (all iref, iloc, iprp, etc refer to one of these)
-typedef struct avifDecoderItem
-{
-    uint32_t id;
-    struct avifMeta * meta;
-    uint8_t type[4];
-    uint32_t offset;
-    uint32_t size;
-    uint32_t idatID; // If non-zero, offset is relative to this idat box (iloc construction_method==1)
-    avifBool ispePresent;
-    avifImageSpatialExtents ispe;
-    avifBool auxCPresent;
-    avifAuxiliaryType auxC;
-    avifContentType contentType;
-    avifBool colrPresent;
-    avifColourInformationBox colr;
-    avifBool av1CPresent;
-    avifCodecConfigurationBox av1C;
-    avifBool paspPresent;
-    avifPixelAspectRatioBox pasp;
-    avifBool clapPresent;
-    avifCleanApertureBox clap;
-    avifBool irotPresent;
-    avifImageRotation irot;
-    avifBool imirPresent;
-    avifImageMirror imir;
-    avifBool pixiPresent;
-    avifPixelInformationProperty pixi;
-    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 a derived image for Item #{dimgForID}
-    avifBool hasUnsupportedEssentialProperty; // If true, this item cites a property flagged as 'essential' that libavif doesn't support (yet). Ignore the item, if so.
-} avifDecoderItem;
-AVIF_ARRAY_DECLARE(avifDecoderItemArray, avifDecoderItem, item);
-
-// Temporary storage for ipco contents until they can be associated and memcpy'd to an avifDecoderItem
+// Temporary storage for ipco/stsd contents until they can be associated and memcpy'd to an avifDecoderItem
 typedef struct avifProperty
 {
     uint8_t type[4];
@@ -143,6 +107,36 @@
 } avifProperty;
 AVIF_ARRAY_DECLARE(avifPropertyArray, avifProperty, prop);
 
+static const avifProperty * avifPropertyArrayFind(const avifPropertyArray * properties, const char * type)
+{
+    for (uint32_t propertyIndex = 0; propertyIndex < properties->count; ++propertyIndex) {
+        avifProperty * prop = &properties->prop[propertyIndex];
+        if (!memcmp(prop->type, type, 4)) {
+            return prop;
+        }
+    }
+    return NULL;
+}
+
+// one "item" worth for decoding (all iref, iloc, iprp, etc refer to one of these)
+typedef struct avifDecoderItem
+{
+    uint32_t id;
+    struct avifMeta * meta;
+    uint8_t type[4];
+    uint32_t offset;
+    uint32_t size;
+    uint32_t idatID; // If non-zero, offset is relative to this idat box (iloc construction_method==1)
+    avifContentType contentType;
+    avifPropertyArray properties;
+    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 a derived image for Item #{dimgForID}
+    avifBool hasUnsupportedEssentialProperty; // If true, this item cites a property flagged as 'essential' that libavif doesn't support (yet). Ignore the item, if so.
+} avifDecoderItem;
+AVIF_ARRAY_DECLARE(avifDecoderItemArray, avifDecoderItem, item);
+
 // idat storage
 typedef struct avifDecoderItemData
 {
@@ -199,8 +193,7 @@
 typedef struct avifSampleDescription
 {
     uint8_t format[4];
-    avifBool av1CPresent;
-    avifCodecConfigurationBox av1C;
+    avifPropertyArray properties;
 } avifSampleDescription;
 AVIF_ARRAY_DECLARE(avifSampleDescriptionArray, avifSampleDescription, description);
 
@@ -231,6 +224,10 @@
 static void avifSampleTableDestroy(avifSampleTable * sampleTable)
 {
     avifArrayDestroy(&sampleTable->chunks);
+    for (uint32_t i = 0; i < sampleTable->sampleDescriptions.count; ++i) {
+        avifSampleDescription * description = &sampleTable->sampleDescriptions.description[i];
+        avifArrayDestroy(&description->properties);
+    }
     avifArrayDestroy(&sampleTable->sampleDescriptions);
     avifArrayDestroy(&sampleTable->sampleToChunks);
     avifArrayDestroy(&sampleTable->sampleSizes);
@@ -274,12 +271,12 @@
     return 8;
 }
 
-static const avifCodecConfigurationBox * avifSampleTableGetConfigurationBox(const avifSampleTable * sampleTable)
+static const avifPropertyArray * avifSampleTableGetProperties(const avifSampleTable * sampleTable)
 {
     for (uint32_t i = 0; i < sampleTable->sampleDescriptions.count; ++i) {
         const avifSampleDescription * description = &sampleTable->sampleDescriptions.description[i];
-        if (!memcmp(description->format, "av01", 4) && description->av1CPresent) {
-            return &description->av1C;
+        if (!memcmp(description->format, "av01", 4)) {
+            return &description->properties;
         }
     }
     return NULL;
@@ -295,6 +292,7 @@
     uint32_t width;
     uint32_t height;
     avifSampleTable * sampleTable;
+    struct avifMeta * meta;
 } avifTrack;
 AVIF_ARRAY_DECLARE(avifTrackArray, avifTrack, track);
 
@@ -377,6 +375,16 @@
 }
 
 // ---------------------------------------------------------------------------
+// Helper macros
+
+#define BEGIN_STREAM(VARNAME, PTR, SIZE) \
+    avifROStream VARNAME;                \
+    avifROData VARNAME##_roData;         \
+    VARNAME##_roData.data = PTR;         \
+    VARNAME##_roData.size = SIZE;        \
+    avifROStreamStart(&VARNAME, &VARNAME##_roData)
+
+// ---------------------------------------------------------------------------
 // avifDecoderData
 
 typedef struct avifTile
@@ -408,6 +416,10 @@
 
 static void avifMetaDestroy(avifMeta * meta)
 {
+    for (uint32_t i = 0; i < meta->items.count; ++i) {
+        avifDecoderItem * item = &meta->items.item[i];
+        avifArrayDestroy(&item->properties);
+    }
     avifArrayDestroy(&meta->items);
     avifArrayDestroy(&meta->properties);
     avifArrayDestroy(&meta->idats);
@@ -427,6 +439,7 @@
     }
 
     avifDecoderItem * item = (avifDecoderItem *)avifArrayPushPtr(&meta->items);
+    avifArrayCreate(&item->properties, sizeof(avifProperty), 16);
     item->id = itemID;
     item->meta = meta;
     return item;
@@ -485,6 +498,13 @@
     return tile;
 }
 
+static avifTrack * avifDecoderDataCreateTrack(avifDecoderData * data)
+{
+    avifTrack * track = (avifTrack *)avifArrayPushPtr(&data->tracks);
+    track->meta = avifMetaCreate();
+    return track;
+}
+
 static void avifDecoderDataClearTiles(avifDecoderData * data)
 {
     for (unsigned int i = 0; i < data->tiles.count; ++i) {
@@ -511,8 +531,12 @@
 {
     avifMetaDestroy(data->meta);
     for (uint32_t i = 0; i < data->tracks.count; ++i) {
-        if (data->tracks.track[i].sampleTable) {
-            avifSampleTableDestroy(data->tracks.track[i].sampleTable);
+        avifTrack * track = &data->tracks.track[i];
+        if (track->sampleTable) {
+            avifSampleTableDestroy(track->sampleTable);
+        }
+        if (track->meta) {
+            avifMetaDestroy(track->meta);
         }
     }
     avifArrayDestroy(&data->tracks);
@@ -727,10 +751,57 @@
     return AVIF_TRUE;
 }
 
+// If colorId == 0, accept any found EXIF/XMP metadata
+avifBool avifDecoderDataFindMetadata(avifDecoderData * data, avifMeta * meta, avifImage * image, uint32_t colorId)
+{
+    avifROData exifData = AVIF_DATA_EMPTY;
+    avifROData xmpData = AVIF_DATA_EMPTY;
+
+    for (uint32_t itemIndex = 0; itemIndex < meta->items.count; ++itemIndex) {
+        avifDecoderItem * item = &meta->items.item[itemIndex];
+        if (!item->id || !item->size) {
+            break;
+        }
+        if (item->hasUnsupportedEssentialProperty) {
+            // An essential property isn't supported by libavif; ignore the item.
+            continue;
+        }
+
+        if ((colorId > 0) && (item->descForID != colorId)) {
+            // Not a content description (metadata) for the colorOBU, skip it
+            continue;
+        }
+
+        if (!memcmp(item->type, "Exif", 4)) {
+            // Advance past Annex A.2.1's header
+            const uint8_t * boxPtr = avifDecoderDataCalcItemPtr(data, item);
+            BEGIN_STREAM(exifBoxStream, boxPtr, item->size);
+            uint32_t exifTiffHeaderOffset;
+            CHECK(avifROStreamReadU32(&exifBoxStream, &exifTiffHeaderOffset)); // unsigned int(32) exif_tiff_header_offset;
+
+            exifData.data = avifROStreamCurrent(&exifBoxStream);
+            exifData.size = avifROStreamRemainingBytes(&exifBoxStream);
+        }
+
+        if (!memcmp(item->type, "mime", 4) && !memcmp(item->contentType.contentType, xmpContentType, xmpContentTypeSize)) {
+            xmpData.data = avifDecoderDataCalcItemPtr(data, item);
+            xmpData.size = item->size;
+        }
+    }
+
+    if (exifData.data && exifData.size) {
+        avifImageSetMetadataExif(image, exifData.data, exifData.size);
+    }
+    if (xmpData.data && xmpData.size) {
+        avifImageSetMetadataXMP(image, xmpData.data, xmpData.size);
+    }
+    return AVIF_TRUE;
+}
+
 // ---------------------------------------------------------------------------
 // URN
 
-static avifBool isAlphaURN(char * urn)
+static avifBool isAlphaURN(const char * urn)
 {
     return !strcmp(urn, URN_ALPHA0) || !strcmp(urn, URN_ALPHA1);
 }
@@ -738,13 +809,6 @@
 // ---------------------------------------------------------------------------
 // BMFF Parsing
 
-#define BEGIN_STREAM(VARNAME, PTR, SIZE) \
-    avifROStream VARNAME;                \
-    avifROData VARNAME##_roData;         \
-    VARNAME##_roData.data = PTR;         \
-    VARNAME##_roData.size = SIZE;        \
-    avifROStreamStart(&VARNAME, &VARNAME##_roData)
-
 static avifBool avifParseItemLocationBox(avifMeta * meta, const uint8_t * raw, size_t rawLen)
 {
     BEGIN_STREAM(s, raw, rawLen);
@@ -1027,7 +1091,7 @@
     return AVIF_TRUE;
 }
 
-static avifBool avifParseItemPropertyContainerBox(avifMeta * meta, const uint8_t * raw, size_t rawLen)
+static avifBool avifParseItemPropertyContainerBox(avifPropertyArray * properties, const uint8_t * raw, size_t rawLen)
 {
     BEGIN_STREAM(s, raw, rawLen);
 
@@ -1035,34 +1099,26 @@
         avifBoxHeader header;
         CHECK(avifROStreamReadBoxHeader(&s, &header));
 
-        int propertyIndex = avifArrayPushIndex(&meta->properties);
-        avifProperty * prop = &meta->properties.prop[propertyIndex];
+        int propertyIndex = avifArrayPushIndex(properties);
+        avifProperty * prop = &properties->prop[propertyIndex];
         memcpy(prop->type, header.type, 4);
         if (!memcmp(header.type, "ispe", 4)) {
             CHECK(avifParseImageSpatialExtentsProperty(prop, avifROStreamCurrent(&s), header.size));
-        }
-        if (!memcmp(header.type, "auxC", 4)) {
+        } else if (!memcmp(header.type, "auxC", 4)) {
             CHECK(avifParseAuxiliaryTypeProperty(prop, avifROStreamCurrent(&s), header.size));
-        }
-        if (!memcmp(header.type, "colr", 4)) {
+        } else if (!memcmp(header.type, "colr", 4)) {
             CHECK(avifParseColourInformationBox(prop, avifROStreamCurrent(&s), header.size));
-        }
-        if (!memcmp(header.type, "av1C", 4)) {
+        } else if (!memcmp(header.type, "av1C", 4)) {
             CHECK(avifParseAV1CodecConfigurationBoxProperty(prop, avifROStreamCurrent(&s), header.size));
-        }
-        if (!memcmp(header.type, "pasp", 4)) {
+        } else if (!memcmp(header.type, "pasp", 4)) {
             CHECK(avifParsePixelAspectRatioBoxProperty(prop, avifROStreamCurrent(&s), header.size));
-        }
-        if (!memcmp(header.type, "clap", 4)) {
+        } else if (!memcmp(header.type, "clap", 4)) {
             CHECK(avifParseCleanApertureBoxProperty(prop, avifROStreamCurrent(&s), header.size));
-        }
-        if (!memcmp(header.type, "irot", 4)) {
+        } else if (!memcmp(header.type, "irot", 4)) {
             CHECK(avifParseImageRotationProperty(prop, avifROStreamCurrent(&s), header.size));
-        }
-        if (!memcmp(header.type, "imir", 4)) {
+        } else if (!memcmp(header.type, "imir", 4)) {
             CHECK(avifParseImageMirrorProperty(prop, avifROStreamCurrent(&s), header.size));
-        }
-        if (!memcmp(header.type, "pixi", 4)) {
+        } else if (!memcmp(header.type, "pixi", 4)) {
             CHECK(avifParsePixelInformationProperty(prop, avifROStreamCurrent(&s), header.size));
         }
 
@@ -1122,35 +1178,21 @@
                 return AVIF_FALSE;
             }
 
-            // Associate property with item
-            avifProperty * prop = &meta->properties.prop[propertyIndex];
-            if (!memcmp(prop->type, "ispe", 4)) {
-                item->ispePresent = AVIF_TRUE;
-                memcpy(&item->ispe, &prop->u.ispe, sizeof(avifImageSpatialExtents));
-            } else if (!memcmp(prop->type, "auxC", 4)) {
-                item->auxCPresent = AVIF_TRUE;
-                memcpy(&item->auxC, &prop->u.auxC, sizeof(avifAuxiliaryType));
-            } else if (!memcmp(prop->type, "colr", 4)) {
-                item->colrPresent = AVIF_TRUE;
-                memcpy(&item->colr, &prop->u.colr, sizeof(avifColourInformationBox));
-            } else if (!memcmp(prop->type, "av1C", 4)) {
-                item->av1CPresent = AVIF_TRUE;
-                memcpy(&item->av1C, &prop->u.av1C, sizeof(avifCodecConfigurationBox));
-            } else if (!memcmp(prop->type, "pasp", 4)) {
-                item->paspPresent = AVIF_TRUE;
-                memcpy(&item->pasp, &prop->u.pasp, sizeof(avifPixelAspectRatioBox));
-            } else if (!memcmp(prop->type, "clap", 4)) {
-                item->clapPresent = AVIF_TRUE;
-                memcpy(&item->clap, &prop->u.clap, sizeof(avifCleanApertureBox));
-            } else if (!memcmp(prop->type, "irot", 4)) {
-                item->irotPresent = AVIF_TRUE;
-                memcpy(&item->irot, &prop->u.irot, sizeof(avifImageRotation));
-            } else if (!memcmp(prop->type, "imir", 4)) {
-                item->imirPresent = AVIF_TRUE;
-                memcpy(&item->imir, &prop->u.imir, sizeof(avifImageMirror));
-            } else if (!memcmp(prop->type, "pixi", 4)) {
-                item->pixiPresent = AVIF_TRUE;
-                memcpy(&item->pixi, &prop->u.pixi, sizeof(avifPixelInformationProperty));
+            // Copy property to item
+            avifProperty * srcProp = &meta->properties.prop[propertyIndex];
+
+            static const char * supportedTypes[] = { "ispe", "auxC", "colr", "av1C", "pasp", "clap", "irot", "imir", "pixi" };
+            size_t supportedTypesCount = sizeof(supportedTypes) / sizeof(supportedTypes[0]);
+            avifBool supportedType = AVIF_FALSE;
+            for (size_t i = 0; i < supportedTypesCount; ++i) {
+                if (!memcmp(srcProp->type, supportedTypes[i], 4)) {
+                    supportedType = AVIF_TRUE;
+                    break;
+                }
+            }
+            if (supportedType) {
+                avifProperty * dstProp = (avifProperty *)avifArrayPushPtr(&item->properties);
+                memcpy(dstProp, srcProp, sizeof(avifProperty));
             } else {
                 if (essential) {
                     // Discovered an essential item property that libavif doesn't support!
@@ -1216,7 +1258,7 @@
     }
 
     // Read all item properties inside of ItemPropertyContainerBox
-    CHECK(avifParseItemPropertyContainerBox(meta, avifROStreamCurrent(&s), ipcoHeader.size));
+    CHECK(avifParseItemPropertyContainerBox(&meta->properties, avifROStreamCurrent(&s), ipcoHeader.size));
     CHECK(avifROStreamSkip(&s, ipcoHeader.size));
 
     // Now read all ItemPropertyAssociation until the end of the box, and make associations
@@ -1592,21 +1634,12 @@
         CHECK(avifROStreamReadBoxHeader(&s, &sampleEntryHeader));
 
         avifSampleDescription * description = (avifSampleDescription *)avifArrayPushPtr(&sampleTable->sampleDescriptions);
+        avifArrayCreate(&description->properties, sizeof(avifProperty), 16);
         memcpy(description->format, sampleEntryHeader.type, sizeof(description->format));
         size_t remainingBytes = avifROStreamRemainingBytes(&s);
         if (!memcmp(description->format, "av01", 4) && (remainingBytes > VISUALSAMPLEENTRY_SIZE)) {
-            BEGIN_STREAM(av01Stream, avifROStreamCurrent(&s) + VISUALSAMPLEENTRY_SIZE, remainingBytes - VISUALSAMPLEENTRY_SIZE);
-            while (avifROStreamHasBytesLeft(&av01Stream, 1)) {
-                avifBoxHeader av01ChildHeader;
-                CHECK(avifROStreamReadBoxHeader(&av01Stream, &av01ChildHeader));
-
-                if (!memcmp(av01ChildHeader.type, "av1C", 4)) {
-                    CHECK(avifParseAV1CodecConfigurationBox(avifROStreamCurrent(&av01Stream), av01ChildHeader.size, &description->av1C));
-                    description->av1CPresent = AVIF_TRUE;
-                }
-
-                CHECK(avifROStreamSkip(&av01Stream, av01ChildHeader.size));
-            }
+            CHECK(avifParseItemPropertyContainerBox(
+                &description->properties, avifROStreamCurrent(&s) + VISUALSAMPLEENTRY_SIZE, remainingBytes - VISUALSAMPLEENTRY_SIZE));
         }
 
         CHECK(avifROStreamSkip(&s, sampleEntryHeader.size));
@@ -1709,7 +1742,7 @@
 {
     BEGIN_STREAM(s, raw, rawLen);
 
-    avifTrack * track = (avifTrack *)avifArrayPushPtr(&data->tracks);
+    avifTrack * track = avifDecoderDataCreateTrack(data);
 
     while (avifROStreamHasBytesLeft(&s, 1)) {
         avifBoxHeader header;
@@ -1717,6 +1750,8 @@
 
         if (!memcmp(header.type, "tkhd", 4)) {
             CHECK(avifParseTrackHeaderBox(track, avifROStreamCurrent(&s), header.size));
+        } else if (!memcmp(header.type, "meta", 4)) {
+            CHECK(avifParseMetaBox(track->meta, avifROStreamCurrent(&s), header.size));
         } else if (!memcmp(header.type, "mdia", 4)) {
             CHECK(avifParseMediaBox(track, avifROStreamCurrent(&s), header.size));
         } else if (!memcmp(header.type, "tref", 4)) {
@@ -1985,7 +2020,7 @@
         data->source = decoder->requestedSource;
     }
 
-    const avifCodecConfigurationBox * configBox = NULL;
+    const avifPropertyArray * colorProperties = NULL;
     if (data->source == AVIF_DECODER_SOURCE_TRACKS) {
         avifTrack * colorTrack = NULL;
         avifTrack * alphaTrack = NULL;
@@ -2018,6 +2053,18 @@
         }
         colorTrack = &decoder->data->tracks.track[colorTrackIndex];
 
+        colorProperties = avifSampleTableGetProperties(colorTrack->sampleTable);
+        if (!colorProperties) {
+            return AVIF_RESULT_BMFF_PARSE_FAILED;
+        }
+
+        // Find Exif and/or XMP metadata, if any
+        if (colorTrack->meta) {
+            if (!avifDecoderDataFindMetadata(data, colorTrack->meta, decoder->image, 0)) {
+                return AVIF_RESULT_BMFF_PARSE_FAILED;
+            }
+        }
+
         uint32_t alphaTrackIndex = 0;
         for (; alphaTrackIndex < decoder->data->tracks.count; ++alphaTrackIndex) {
             avifTrack * track = &decoder->data->tracks.track[alphaTrackIndex];
@@ -2075,15 +2122,12 @@
 
         decoder->image->width = colorTrack->width;
         decoder->image->height = colorTrack->height;
-        configBox = avifSampleTableGetConfigurationBox(colorTrack->sampleTable);
         decoder->alphaPresent = (alphaTrack != NULL);
     } else {
         // Create from items
 
         avifROData colorOBU = AVIF_DATA_EMPTY;
         avifROData alphaOBU = AVIF_DATA_EMPTY;
-        avifROData exifData = AVIF_DATA_EMPTY;
-        avifROData xmpData = AVIF_DATA_EMPTY;
         avifDecoderItem * colorOBUItem = NULL;
         avifDecoderItem * alphaOBUItem = NULL;
 
@@ -2131,6 +2175,7 @@
         if (!colorOBUItem) {
             return AVIF_RESULT_NO_AV1_ITEMS_FOUND;
         }
+        colorProperties = &colorOBUItem->properties;
 
         // Find the alphaOBU item, if any
         for (uint32_t itemIndex = 0; itemIndex < data->meta->items.count; ++itemIndex) {
@@ -2152,7 +2197,8 @@
                 continue;
             }
 
-            if (isAlphaURN(item->auxC.auxType) && (item->auxForID == colorOBUItem->id)) {
+            const avifProperty * auxCProp = avifPropertyArrayFind(&item->properties, "auxC");
+            if (auxCProp && isAlphaURN(auxCProp->u.auxC.auxType) && (item->auxForID == colorOBUItem->id)) {
                 if (isGrid) {
                     const uint8_t * itemPtr = avifDecoderDataCalcItemPtr(data, item);
                     if (itemPtr == NULL) {
@@ -2172,36 +2218,8 @@
         }
 
         // Find Exif and/or XMP metadata, if any
-        for (uint32_t itemIndex = 0; itemIndex < data->meta->items.count; ++itemIndex) {
-            avifDecoderItem * item = &data->meta->items.item[itemIndex];
-            if (!item->id || !item->size) {
-                break;
-            }
-            if (item->hasUnsupportedEssentialProperty) {
-                // An essential property isn't supported by libavif; ignore the item.
-                continue;
-            }
-
-            if (item->descForID != colorOBUItem->id) {
-                // Not a content description (metadata) for the colorOBU, skip it
-                continue;
-            }
-
-            if (!memcmp(item->type, "Exif", 4)) {
-                // Advance past Annex A.2.1's header
-                const uint8_t * boxPtr = avifDecoderDataCalcItemPtr(data, item);
-                BEGIN_STREAM(exifBoxStream, boxPtr, item->size);
-                uint32_t exifTiffHeaderOffset;
-                CHECK(avifROStreamReadU32(&exifBoxStream, &exifTiffHeaderOffset)); // unsigned int(32) exif_tiff_header_offset;
-
-                exifData.data = avifROStreamCurrent(&exifBoxStream);
-                exifData.size = avifROStreamRemainingBytes(&exifBoxStream);
-            }
-
-            if (!memcmp(item->type, "mime", 4) && !memcmp(item->contentType.contentType, xmpContentType, xmpContentTypeSize)) {
-                xmpData.data = avifDecoderDataCalcItemPtr(data, item);
-                xmpData.size = item->size;
-            }
+        if (!avifDecoderDataFindMetadata(data, data->meta, decoder->image, colorOBUItem->id)) {
+            return AVIF_RESULT_BMFF_PARSE_FAILED;
         }
 
         if ((data->colorGrid.rows > 0) && (data->colorGrid.columns > 0)) {
@@ -2239,43 +2257,6 @@
             }
         }
 
-        if (colorOBUItem->colrPresent) {
-            if (colorOBUItem->colr.hasICC) {
-                avifImageSetProfileICC(decoder->image, colorOBUItem->colr.icc, colorOBUItem->colr.iccSize);
-            } else if (colorOBUItem->colr.hasNCLX) {
-                data->cicpSet = AVIF_TRUE;
-                decoder->image->colorPrimaries = colorOBUItem->colr.colorPrimaries;
-                decoder->image->transferCharacteristics = colorOBUItem->colr.transferCharacteristics;
-                decoder->image->matrixCoefficients = colorOBUItem->colr.matrixCoefficients;
-                decoder->image->yuvRange = colorOBUItem->colr.range;
-            }
-        }
-
-        // Transformations
-        if (colorOBUItem->paspPresent) {
-            decoder->image->transformFlags |= AVIF_TRANSFORM_PASP;
-            memcpy(&decoder->image->pasp, &colorOBUItem->pasp, sizeof(avifPixelAspectRatioBox));
-        }
-        if (colorOBUItem->clapPresent) {
-            decoder->image->transformFlags |= AVIF_TRANSFORM_CLAP;
-            memcpy(&decoder->image->clap, &colorOBUItem->clap, sizeof(avifCleanApertureBox));
-        }
-        if (colorOBUItem->irotPresent) {
-            decoder->image->transformFlags |= AVIF_TRANSFORM_IROT;
-            memcpy(&decoder->image->irot, &colorOBUItem->irot, sizeof(avifImageRotation));
-        }
-        if (colorOBUItem->imirPresent) {
-            decoder->image->transformFlags |= AVIF_TRANSFORM_IMIR;
-            memcpy(&decoder->image->imir, &colorOBUItem->imir, sizeof(avifImageMirror));
-        }
-
-        if (exifData.data && exifData.size) {
-            avifImageSetMetadataExif(decoder->image, exifData.data, exifData.size);
-        }
-        if (xmpData.data && xmpData.size) {
-            avifImageSetMetadataXMP(decoder->image, xmpData.data, xmpData.size);
-        }
-
         // Set all counts and timing to safe-but-uninteresting values
         decoder->imageIndex = -1;
         decoder->imageCount = 1;
@@ -2291,19 +2272,52 @@
         decoder->ioStats.colorOBUSize = colorOBU.size;
         decoder->ioStats.alphaOBUSize = alphaOBU.size;
 
-        if (colorOBUItem->ispePresent) {
-            decoder->image->width = colorOBUItem->ispe.width;
-            decoder->image->height = colorOBUItem->ispe.height;
+        const avifProperty * ispeProp = avifPropertyArrayFind(colorProperties, "ispe");
+        if (ispeProp) {
+            decoder->image->width = ispeProp->u.ispe.width;
+            decoder->image->height = ispeProp->u.ispe.height;
         } else {
             decoder->image->width = 0;
             decoder->image->height = 0;
         }
-        if (colorOBUItem->av1CPresent) {
-            configBox = &colorOBUItem->av1C;
-        }
         decoder->alphaPresent = (alphaOBUItem != NULL);
     }
 
+    const avifProperty * colrProp = avifPropertyArrayFind(colorProperties, "colr");
+    if (colrProp) {
+        if (colrProp->u.colr.hasICC) {
+            avifImageSetProfileICC(decoder->image, colrProp->u.colr.icc, colrProp->u.colr.iccSize);
+        } else if (colrProp->u.colr.hasNCLX) {
+            data->cicpSet = AVIF_TRUE;
+            decoder->image->colorPrimaries = colrProp->u.colr.colorPrimaries;
+            decoder->image->transferCharacteristics = colrProp->u.colr.transferCharacteristics;
+            decoder->image->matrixCoefficients = colrProp->u.colr.matrixCoefficients;
+            decoder->image->yuvRange = colrProp->u.colr.range;
+        }
+    }
+
+    // Transformations
+    const avifProperty * paspProp = avifPropertyArrayFind(colorProperties, "pasp");
+    if (paspProp) {
+        decoder->image->transformFlags |= AVIF_TRANSFORM_PASP;
+        memcpy(&decoder->image->pasp, &paspProp->u.pasp, sizeof(avifPixelAspectRatioBox));
+    }
+    const avifProperty * clapProp = avifPropertyArrayFind(colorProperties, "clap");
+    if (clapProp) {
+        decoder->image->transformFlags |= AVIF_TRANSFORM_CLAP;
+        memcpy(&decoder->image->clap, &clapProp->u.clap, sizeof(avifCleanApertureBox));
+    }
+    const avifProperty * irotProp = avifPropertyArrayFind(colorProperties, "irot");
+    if (irotProp) {
+        decoder->image->transformFlags |= AVIF_TRANSFORM_IROT;
+        memcpy(&decoder->image->irot, &irotProp->u.irot, sizeof(avifImageRotation));
+    }
+    const avifProperty * imirProp = avifPropertyArrayFind(colorProperties, "imir");
+    if (imirProp) {
+        decoder->image->transformFlags |= AVIF_TRANSFORM_IMIR;
+        memcpy(&decoder->image->imir, &imirProp->u.imir, sizeof(avifImageMirror));
+    }
+
     if (!decoder->data->cicpSet && (data->tiles.count > 0)) {
         avifTile * firstTile = &data->tiles.tile[0];
         if (firstTile->input->samples.count > 0) {
@@ -2319,23 +2333,24 @@
         }
     }
 
-    if (configBox) {
-        memcpy(&decoder->av1C, configBox, sizeof(decoder->av1C));
+    const avifProperty * av1CProp = avifPropertyArrayFind(colorProperties, "av1C");
+    if (av1CProp) {
+        memcpy(&decoder->av1C, &av1CProp->u.av1C, sizeof(decoder->av1C));
 
-        decoder->image->depth = avifCodecConfigurationBoxGetDepth(configBox);
-        if (configBox->monochrome) {
+        decoder->image->depth = avifCodecConfigurationBoxGetDepth(&decoder->av1C);
+        if (decoder->av1C.monochrome) {
             decoder->image->yuvFormat = AVIF_PIXEL_FORMAT_YUV400;
         } else {
-            if (configBox->chromaSubsamplingX && configBox->chromaSubsamplingY) {
+            if (decoder->av1C.chromaSubsamplingX && decoder->av1C.chromaSubsamplingY) {
                 decoder->image->yuvFormat = AVIF_PIXEL_FORMAT_YUV420;
-            } else if (configBox->chromaSubsamplingX) {
+            } else if (decoder->av1C.chromaSubsamplingX) {
                 decoder->image->yuvFormat = AVIF_PIXEL_FORMAT_YUV422;
 
             } else {
                 decoder->image->yuvFormat = AVIF_PIXEL_FORMAT_YUV444;
             }
         }
-        decoder->image->yuvSamplePosition = (avifChromaSamplePosition)configBox->chromaSamplePosition;
+        decoder->image->yuvSamplePosition = (avifChromaSamplePosition)decoder->av1C.chromaSamplePosition;
     } else {
         // I believe this path is very, very unlikely. An av1C box should be mandatory
         // in all valid AVIF configurations.