Add avifEncoderAddImageGrid() for grid image encoding, basic mdat and iprp dedup
diff --git a/src/write.c b/src/write.c
index 17b59e4..82c6b82 100644
--- a/src/write.c
+++ b/src/write.c
@@ -77,6 +77,7 @@
     avifCodecEncodeOutput * encodeOutput; // AV1 sample data
     avifRWData metadataPayload;           // Exif/XMP data
     avifCodecConfigurationBox av1C;       // Harvested in avifEncoderFinish(), if encodeOutput has samples
+    uint32_t cellIndex;                   // Which row-major cell index corresponds to this item. ignored on non-av01 types
     avifBool alpha;
 
     const char * infeName;
@@ -88,6 +89,11 @@
     uint16_t irefToID; // if non-zero, make an iref from this id -> irefToID
     const char * irefType;
 
+    uint8_t gridCols; // if non-zero, this is a grid item
+    uint8_t gridRows; // if non-zero, this is a grid item
+
+    uint16_t dimgFromID; // if non-zero, make an iref from dimgFromID -> this id
+
     struct ipmaArray ipma;
 } avifEncoderItem;
 AVIF_ARRAY_DECLARE(avifEncoderItemArray, avifEncoderItem, item);
@@ -109,11 +115,10 @@
     avifEncoderItemArray items;
     avifEncoderFrameArray frames;
     avifImage * imageMetadata;
-    avifEncoderItem * colorItem;
-    avifEncoderItem * alphaItem;
     uint16_t lastItemID;
     uint16_t primaryItemID;
     avifBool singleImage; // if true, the AVIF_ADD_IMAGE_FLAG_SINGLE flag was set on the first call to avifEncoderAddImage()
+    avifBool alphaPresent;
 } avifEncoderData;
 
 static avifEncoderData * avifEncoderDataCreate()
@@ -126,7 +131,7 @@
     return data;
 }
 
-static avifEncoderItem * avifEncoderDataCreateItem(avifEncoderData * data, const char * type, const char * infeName, size_t infeNameSize)
+static avifEncoderItem * avifEncoderDataCreateItem(avifEncoderData * data, const char * type, const char * infeName, size_t infeNameSize, int cellIndex)
 {
     avifEncoderItem * item = (avifEncoderItem *)avifArrayPushPtr(&data->items);
     ++data->lastItemID;
@@ -135,6 +140,7 @@
     item->infeName = infeName;
     item->infeNameSize = infeNameSize;
     item->encodeOutput = avifCodecEncodeOutputCreate();
+    item->cellIndex = cellIndex;
     avifArrayCreate(&item->mdatFixups, sizeof(avifOffsetFixup), 4);
     return item;
 }
@@ -338,7 +344,47 @@
     avifRWStreamFinishBox(s, meta);
 }
 
-avifResult avifEncoderAddImage(avifEncoder * encoder, const avifImage * image, uint64_t durationInTimescales, uint32_t addImageFlags)
+static void avifWriteGridPayload(avifRWData * data, uint8_t gridCols, uint8_t gridRows, const avifImage * firstCell)
+{
+    // ISO/IEC 23008-12 6.6.2.3.2
+    // aligned(8) class ImageGrid {
+    //     unsigned int(8) version = 0;
+    //     unsigned int(8) flags;
+    //     FieldLength = ((flags & 1) + 1) * 16;
+    //     unsigned int(8) rows_minus_one;
+    //     unsigned int(8) columns_minus_one;
+    //     unsigned int(FieldLength) output_width;
+    //     unsigned int(FieldLength) output_height;
+    // }
+
+    uint32_t gridWidth = firstCell->width * gridCols;
+    uint32_t gridHeight = firstCell->height * gridRows;
+    uint8_t gridFlags = ((gridWidth > 65535) || (gridHeight > 65535)) ? 1 : 0;
+
+    avifRWStream s;
+    avifRWStreamStart(&s, data);
+    avifRWStreamWriteU8(&s, 0);                     // unsigned int(8) version = 0;
+    avifRWStreamWriteU8(&s, gridFlags);             // unsigned int(8) flags;
+    avifRWStreamWriteU8(&s, (uint8_t)gridRows - 1); // unsigned int(8) rows_minus_one;
+    avifRWStreamWriteU8(&s, (uint8_t)gridCols - 1); // unsigned int(8) columns_minus_one;
+    if (gridFlags & 1) {
+        avifRWStreamWriteU32(&s, gridWidth);  // unsigned int(FieldLength) output_width;
+        avifRWStreamWriteU32(&s, gridHeight); // unsigned int(FieldLength) output_height;
+    } else {
+        uint16_t tmpWidth = (uint16_t)gridWidth;
+        uint16_t tmpHeight = (uint16_t)gridHeight;
+        avifRWStreamWriteU16(&s, tmpWidth);  // unsigned int(FieldLength) output_width;
+        avifRWStreamWriteU16(&s, tmpHeight); // unsigned int(FieldLength) output_height;
+    }
+    avifRWStreamFinishWrite(&s);
+}
+
+static avifResult avifEncoderAddImageInternal(avifEncoder * encoder,
+                                              uint8_t gridCols,
+                                              uint8_t gridRows,
+                                              const avifImage ** cellImages,
+                                              uint64_t durationInTimescales,
+                                              uint32_t addImageFlags)
 {
     // -----------------------------------------------------------------------
     // Verify encoding is possible
@@ -348,18 +394,36 @@
     }
 
     // -----------------------------------------------------------------------
-    // Validate image
+    // Validate images
 
-    if ((image->depth != 8) && (image->depth != 10) && (image->depth != 12)) {
-        return AVIF_RESULT_UNSUPPORTED_DEPTH;
+    const uint32_t cellCount = gridCols * gridRows;
+    if (cellCount == 0) {
+        return AVIF_RESULT_INVALID_ARGUMENT;
     }
 
-    if (!image->width || !image->height || !image->yuvPlanes[AVIF_CHAN_Y]) {
-        return AVIF_RESULT_NO_CONTENT;
+    const avifImage * firstCell = cellImages[0];
+    if ((firstCell->width < 64) || (firstCell->height < 64)) {
+        return AVIF_RESULT_INVALID_IMAGE_GRID;
     }
 
-    if (image->yuvFormat == AVIF_PIXEL_FORMAT_NONE) {
-        return AVIF_RESULT_NO_YUV_FORMAT_SELECTED;
+    for (uint32_t cellIndex = 0; cellIndex < cellCount; ++cellIndex) {
+        const avifImage * cellImage = cellImages[cellIndex];
+        if ((cellImage->depth != 8) && (cellImage->depth != 10) && (cellImage->depth != 12)) {
+            return AVIF_RESULT_UNSUPPORTED_DEPTH;
+        }
+
+        if ((cellImage->depth != firstCell->depth) || (cellImage->width != firstCell->width) ||
+            (cellImage->height != firstCell->height) || (!!cellImage->alphaPlane != !!firstCell->alphaPlane)) {
+            return AVIF_RESULT_INVALID_IMAGE_GRID;
+        }
+
+        if (!cellImage->width || !cellImage->height || !cellImage->yuvPlanes[AVIF_CHAN_Y]) {
+            return AVIF_RESULT_NO_CONTENT;
+        }
+
+        if (cellImage->yuvFormat == AVIF_PIXEL_FORMAT_NONE) {
+            return AVIF_RESULT_NO_YUV_FORMAT_SELECTED;
+        }
     }
 
     // -----------------------------------------------------------------------
@@ -388,20 +452,38 @@
 
     if (encoder->data->items.count == 0) {
         // Make a copy of the first image's metadata (sans pixels) for future writing/validation
-        avifImageCopy(encoder->data->imageMetadata, image, 0);
+        avifImageCopy(encoder->data->imageMetadata, firstCell, 0);
 
         // Prepare all AV1 items
 
-        encoder->data->colorItem = avifEncoderDataCreateItem(encoder->data, "av01", "Color", 6);
-        encoder->data->colorItem->codec = avifCodecCreate(encoder->codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE);
-        if (!encoder->data->colorItem->codec) {
-            // Just bail out early, we're not surviving this function without an encoder compiled in
-            return AVIF_RESULT_NO_CODEC_AVAILABLE;
-        }
-        encoder->data->colorItem->codec->csOptions = encoder->csOptions;
-        encoder->data->primaryItemID = encoder->data->colorItem->id;
+        uint16_t gridColorID = 0;
+        if (cellCount > 1) {
+            avifEncoderItem * gridColorItem = avifEncoderDataCreateItem(encoder->data, "grid", "Color", 6, 0);
+            avifWriteGridPayload(&gridColorItem->metadataPayload, gridCols, gridRows, firstCell);
+            gridColorItem->gridCols = gridCols;
+            gridColorItem->gridRows = gridRows;
 
-        avifBool needsAlpha = (image->alphaPlane != NULL);
+            gridColorID = gridColorItem->id;
+            encoder->data->primaryItemID = gridColorID;
+        }
+
+        for (uint32_t cellIndex = 0; cellIndex < cellCount; ++cellIndex) {
+            avifEncoderItem * item = avifEncoderDataCreateItem(encoder->data, "av01", "Color", 6, cellIndex);
+            item->codec = avifCodecCreate(encoder->codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE);
+            if (!item->codec) {
+                // Just bail out early, we're not surviving this function without an encoder compiled in
+                return AVIF_RESULT_NO_CODEC_AVAILABLE;
+            }
+            item->codec->csOptions = encoder->csOptions;
+
+            if (gridColorID) {
+                item->dimgFromID = gridColorID;
+            } else if (!encoder->data->primaryItemID) {
+                encoder->data->primaryItemID = item->id;
+            }
+        }
+
+        encoder->data->alphaPresent = (firstCell->alphaPlane != NULL);
         if (addImageFlags & AVIF_ADD_IMAGE_FLAG_SINGLE) {
             // If encoding a single image in which the alpha plane exists but is entirely opaque,
             // simply skip writing an alpha AV1 payload entirely, as it'll be interpreted as opaque
@@ -412,73 +494,103 @@
             // be a fade out later in the sequence. This is why avifImageIsOpaque() is only called
             // when encoding a single image.
 
-            needsAlpha = !avifImageIsOpaque(image);
-        }
-        if (needsAlpha) {
-            encoder->data->alphaItem = avifEncoderDataCreateItem(encoder->data, "av01", "Alpha", 6);
-            encoder->data->alphaItem->codec = avifCodecCreate(encoder->codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE);
-            if (!encoder->data->alphaItem->codec) {
-                return AVIF_RESULT_NO_CODEC_AVAILABLE;
+            encoder->data->alphaPresent = AVIF_FALSE;
+            for (uint32_t cellIndex = 0; cellIndex < cellCount; ++cellIndex) {
+                const avifImage * cellImage = cellImages[cellIndex];
+                if (!avifImageIsOpaque(cellImage)) {
+                    encoder->data->alphaPresent = AVIF_TRUE;
+                    break;
+                }
             }
-            encoder->data->alphaItem->codec->csOptions = encoder->csOptions;
-            encoder->data->alphaItem->alpha = AVIF_TRUE;
-            encoder->data->alphaItem->irefToID = encoder->data->primaryItemID;
-            encoder->data->alphaItem->irefType = "auxl";
+        }
+
+        if (encoder->data->alphaPresent) {
+            uint16_t gridAlphaID = 0;
+            if (cellCount > 1) {
+                avifEncoderItem * gridAlphaItem = avifEncoderDataCreateItem(encoder->data, "grid", "Alpha", 6, 0);
+                avifWriteGridPayload(&gridAlphaItem->metadataPayload, gridCols, gridRows, firstCell);
+                gridAlphaItem->alpha = AVIF_TRUE;
+                gridAlphaItem->irefToID = encoder->data->primaryItemID;
+                gridAlphaItem->irefType = "auxl";
+                gridAlphaID = gridAlphaItem->id;
+            }
+
+            for (uint32_t cellIndex = 0; cellIndex < cellCount; ++cellIndex) {
+                avifEncoderItem * item = avifEncoderDataCreateItem(encoder->data, "av01", "Alpha", 6, 0);
+                item->codec = avifCodecCreate(encoder->codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE);
+                if (!item->codec) {
+                    return AVIF_RESULT_NO_CODEC_AVAILABLE;
+                }
+                item->codec->csOptions = encoder->csOptions;
+                item->alpha = AVIF_TRUE;
+
+                if (gridAlphaID) {
+                    item->dimgFromID = gridAlphaID;
+                } else {
+                    item->irefToID = encoder->data->primaryItemID;
+                    item->irefType = "auxl";
+                }
+            }
         }
 
         // -----------------------------------------------------------------------
         // Create metadata items (Exif, XMP)
 
-        if (image->exif.size > 0) {
+        if (firstCell->exif.size > 0) {
             // Validate Exif payload (if any) and find TIFF header offset
             uint32_t exifTiffHeaderOffset = 0;
-            if (image->exif.size < 4) {
+            if (firstCell->exif.size < 4) {
                 // Can't even fit the TIFF header, something is wrong
                 return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
             }
 
             const uint8_t tiffHeaderBE[4] = { 'M', 'M', 0, 42 };
             const uint8_t tiffHeaderLE[4] = { 'I', 'I', 42, 0 };
-            for (; exifTiffHeaderOffset < (image->exif.size - 4); ++exifTiffHeaderOffset) {
-                if (!memcmp(&image->exif.data[exifTiffHeaderOffset], tiffHeaderBE, sizeof(tiffHeaderBE))) {
+            for (; exifTiffHeaderOffset < (firstCell->exif.size - 4); ++exifTiffHeaderOffset) {
+                if (!memcmp(&firstCell->exif.data[exifTiffHeaderOffset], tiffHeaderBE, sizeof(tiffHeaderBE))) {
                     break;
                 }
-                if (!memcmp(&image->exif.data[exifTiffHeaderOffset], tiffHeaderLE, sizeof(tiffHeaderLE))) {
+                if (!memcmp(&firstCell->exif.data[exifTiffHeaderOffset], tiffHeaderLE, sizeof(tiffHeaderLE))) {
                     break;
                 }
             }
 
-            if (exifTiffHeaderOffset >= image->exif.size - 4) {
+            if (exifTiffHeaderOffset >= firstCell->exif.size - 4) {
                 // Couldn't find the TIFF header
                 return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
             }
 
-            avifEncoderItem * exifItem = avifEncoderDataCreateItem(encoder->data, "Exif", "Exif", 5);
+            avifEncoderItem * exifItem = avifEncoderDataCreateItem(encoder->data, "Exif", "Exif", 5, 0);
             exifItem->irefToID = encoder->data->primaryItemID;
             exifItem->irefType = "cdsc";
 
-            avifRWDataRealloc(&exifItem->metadataPayload, sizeof(uint32_t) + image->exif.size);
+            avifRWDataRealloc(&exifItem->metadataPayload, sizeof(uint32_t) + firstCell->exif.size);
             exifTiffHeaderOffset = avifHTONL(exifTiffHeaderOffset);
             memcpy(exifItem->metadataPayload.data, &exifTiffHeaderOffset, sizeof(uint32_t));
-            memcpy(exifItem->metadataPayload.data + sizeof(uint32_t), image->exif.data, image->exif.size);
+            memcpy(exifItem->metadataPayload.data + sizeof(uint32_t), firstCell->exif.data, firstCell->exif.size);
         }
 
-        if (image->xmp.size > 0) {
-            avifEncoderItem * xmpItem = avifEncoderDataCreateItem(encoder->data, "mime", "XMP", 4);
+        if (firstCell->xmp.size > 0) {
+            avifEncoderItem * xmpItem = avifEncoderDataCreateItem(encoder->data, "mime", "XMP", 4, 0);
             xmpItem->irefToID = encoder->data->primaryItemID;
             xmpItem->irefType = "cdsc";
 
             xmpItem->infeContentType = xmpContentType;
             xmpItem->infeContentTypeSize = xmpContentTypeSize;
-            avifRWDataSet(&xmpItem->metadataPayload, image->xmp.data, image->xmp.size);
+            avifRWDataSet(&xmpItem->metadataPayload, firstCell->xmp.data, firstCell->xmp.size);
         }
     } else {
         // Another frame in an image sequence
 
-        if (encoder->data->alphaItem && !image->alphaPlane) {
-            // If the first image in the sequence had an alpha plane (even if fully opaque), all
-            // subsequence images must have alpha as well.
-            return AVIF_RESULT_ENCODE_ALPHA_FAILED;
+        if (encoder->data->alphaPresent) {
+            for (uint32_t cellIndex = 0; cellIndex < cellCount; ++cellIndex) {
+                const avifImage * cellImage = cellImages[cellIndex];
+                if (!cellImage->alphaPlane) {
+                    // If the first image in the sequence had an alpha plane (even if fully opaque), all
+                    // subsequence images must have alpha as well.
+                    return AVIF_RESULT_ENCODE_ALPHA_FAILED;
+                }
+            }
         }
     }
 
@@ -492,8 +604,9 @@
     for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) {
         avifEncoderItem * item = &encoder->data->items.item[itemIndex];
         if (item->codec) {
+            const avifImage * cellImage = cellImages[item->cellIndex];
             avifResult encodeResult =
-                item->codec->encodeImage(item->codec, encoder, image, item->alpha, addImageFlags, item->encodeOutput);
+                item->codec->encodeImage(item->codec, encoder, cellImage, item->alpha, addImageFlags, item->encodeOutput);
             if (encodeResult == AVIF_RESULT_UNKNOWN_ERROR) {
                 encodeResult = item->alpha ? AVIF_RESULT_ENCODE_ALPHA_FAILED : AVIF_RESULT_ENCODE_COLOR_FAILED;
             }
@@ -508,6 +621,33 @@
     return AVIF_RESULT_OK;
 }
 
+avifResult avifEncoderAddImage(avifEncoder * encoder, const avifImage * image, uint64_t durationInTimescales, uint32_t addImageFlags)
+{
+    return avifEncoderAddImageInternal(encoder, 1, 1, &image, durationInTimescales, addImageFlags);
+}
+
+avifResult avifEncoderAddImageGrid(avifEncoder * encoder, uint8_t gridCols, uint8_t gridRows, const avifImage ** cellImages, uint32_t addImageFlags)
+{
+    return avifEncoderAddImageInternal(
+        encoder, gridCols, gridRows, cellImages, 1, addImageFlags | AVIF_ADD_IMAGE_FLAG_SINGLE); // only single image grids are supported
+}
+
+size_t avifEncoderFindExistingChunk(avifRWStream * s, size_t mdatStartOffset, const uint8_t * data, size_t size)
+{
+    const size_t mdatCurrentOffset = avifRWStreamOffset(s);
+    const size_t mdatSearchSize = mdatCurrentOffset - mdatStartOffset;
+    if (mdatSearchSize < size) {
+        return 0;
+    }
+    const size_t mdatEndSearchOffset = mdatStartOffset + (mdatSearchSize - size);
+    for (size_t searchOffset = mdatStartOffset; searchOffset <= mdatEndSearchOffset; ++searchOffset) {
+        if (!memcmp(data, &s->raw->data[searchOffset], size)) {
+            return searchOffset;
+        }
+    }
+    return 0;
+}
+
 avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output)
 {
     if (encoder->data->items.count == 0) {
@@ -527,16 +667,6 @@
             if (item->encodeOutput->samples.count != encoder->data->frames.count) {
                 return item->alpha ? AVIF_RESULT_ENCODE_ALPHA_FAILED : AVIF_RESULT_ENCODE_COLOR_FAILED;
             }
-
-            size_t obuSize = 0;
-            for (uint32_t sampleIndex = 0; sampleIndex < item->encodeOutput->samples.count; ++sampleIndex) {
-                obuSize += item->encodeOutput->samples.sample[sampleIndex].data.size;
-            }
-            if (item->alpha) {
-                encoder->ioStats.alphaOBUSize = obuSize;
-            } else {
-                encoder->ioStats.colorOBUSize = obuSize;
-            }
         }
     }
 
@@ -685,6 +815,32 @@
     avifBoxMarker iref = 0;
     for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) {
         avifEncoderItem * item = &encoder->data->items.item[itemIndex];
+
+        // Count how many other items refer to this item with dimgFromID
+        uint16_t dimgCount = 0;
+        for (uint32_t dimgIndex = 0; dimgIndex < encoder->data->items.count; ++dimgIndex) {
+            avifEncoderItem * dimgItem = &encoder->data->items.item[dimgIndex];
+            if (dimgItem->dimgFromID == item->id) {
+                ++dimgCount;
+            }
+        }
+
+        if (dimgCount > 0) {
+            if (!iref) {
+                iref = avifRWStreamWriteFullBox(&s, "iref", AVIF_BOX_SIZE_TBD, 0, 0);
+            }
+            avifBoxMarker refType = avifRWStreamWriteBox(&s, "dimg", AVIF_BOX_SIZE_TBD);
+            avifRWStreamWriteU16(&s, item->id);  // unsigned int(16) from_item_ID;
+            avifRWStreamWriteU16(&s, dimgCount); // unsigned int(16) reference_count;
+            for (uint32_t dimgIndex = 0; dimgIndex < encoder->data->items.count; ++dimgIndex) {
+                avifEncoderItem * dimgItem = &encoder->data->items.item[dimgIndex];
+                if (dimgItem->dimgFromID == item->id) {
+                    avifRWStreamWriteU16(&s, dimgItem->id); // unsigned int(16) to_item_ID;
+                }
+            }
+            avifRWStreamFinishBox(&s, refType);
+        }
+
         if (item->irefToID != 0) {
             if (!iref) {
                 iref = avifRWStreamWriteFullBox(&s, "iref", AVIF_BOX_SIZE_TBD, 0, 0);
@@ -709,17 +865,45 @@
     avifBoxMarker ipco = avifRWStreamWriteBox(&s, "ipco", AVIF_BOX_SIZE_TBD);
     for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) {
         avifEncoderItem * item = &encoder->data->items.item[itemIndex];
+        const avifBool isGrid = (item->gridCols > 0);
         memset(&item->ipma, 0, sizeof(item->ipma));
-        if (!item->codec) {
+        uint32_t imageWidth = imageMetadata->width;
+        uint32_t imageHeight = imageMetadata->height;
+        if (!item->codec && !isGrid) {
             // No ipma to write for this item
             continue;
         }
 
+        if (item->dimgFromID) {
+            // All image cells from a grid should share the exact same properties, so see if we've
+            // already written properties out for another cell in this grid, and if so, just steal
+            // their ipma and move on. This is a sneaky way to provide iprp deduplication.
+
+            avifBool foundPreviousCell = AVIF_FALSE;
+            for (uint32_t dedupIndex = 0; dedupIndex < itemIndex; ++dedupIndex) {
+                avifEncoderItem * dedupItem = &encoder->data->items.item[dedupIndex];
+                if (item->dimgFromID == dedupItem->dimgFromID) {
+                    // We've already written dedup's items out. Steal their ipma indices and move on!
+                    memcpy(&item->ipma, &dedupItem->ipma, sizeof(struct ipmaArray));
+                    foundPreviousCell = AVIF_TRUE;
+                    break;
+                }
+            }
+            if (foundPreviousCell) {
+                continue;
+            }
+        }
+
+        if (isGrid) {
+            imageWidth = imageMetadata->width * item->gridCols;
+            imageHeight = imageMetadata->height * item->gridRows;
+        }
+
         // Properties all av01 items need
 
         avifBoxMarker ispe = avifRWStreamWriteFullBox(&s, "ispe", AVIF_BOX_SIZE_TBD, 0, 0);
-        avifRWStreamWriteU32(&s, imageMetadata->width);  // unsigned int(32) image_width;
-        avifRWStreamWriteU32(&s, imageMetadata->height); // unsigned int(32) image_height;
+        avifRWStreamWriteU32(&s, imageWidth);  // unsigned int(32) image_width;
+        avifRWStreamWriteU32(&s, imageHeight); // unsigned int(32) image_height;
         avifRWStreamFinishBox(&s, ispe);
         ipmaPush(&item->ipma, ++itemPropertyIndex, AVIF_FALSE); // ipma is 1-indexed, doing this afterwards is correct
 
@@ -732,8 +916,10 @@
         avifRWStreamFinishBox(&s, pixi);
         ipmaPush(&item->ipma, ++itemPropertyIndex, AVIF_FALSE);
 
-        writeConfigBox(&s, &item->av1C);
-        ipmaPush(&item->ipma, ++itemPropertyIndex, AVIF_TRUE);
+        if (item->codec) {
+            writeConfigBox(&s, &item->av1C);
+            ipmaPush(&item->ipma, ++itemPropertyIndex, AVIF_TRUE);
+        }
 
         if (item->alpha) {
             // Alpha specific properties
@@ -996,7 +1182,11 @@
     // -----------------------------------------------------------------------
     // Write mdat
 
+    encoder->ioStats.colorOBUSize = 0;
+    encoder->ioStats.alphaOBUSize = 0;
+
     avifBoxMarker mdat = avifRWStreamWriteBox(&s, "mdat", AVIF_BOX_SIZE_TBD);
+    const size_t mdatStartOffset = avifRWStreamOffset(&s);
     for (uint32_t itemPasses = 0; itemPasses < 3; ++itemPasses) {
         // Use multiple passes to pack in the following order:
         //   * Pass 0: metadata (Exif/XMP)
@@ -1028,21 +1218,40 @@
                 continue;
             }
 
-            uint32_t chunkOffset = (uint32_t)avifRWStreamOffset(&s);
-            if (item->encodeOutput->samples.count > 0) {
-                for (uint32_t sampleIndex = 0; sampleIndex < item->encodeOutput->samples.count; ++sampleIndex) {
-                    avifEncodeSample * sample = &item->encodeOutput->samples.sample[sampleIndex];
-                    avifRWStreamWrite(&s, sample->data.data, sample->data.size);
-                }
+            size_t chunkOffset = 0;
+
+            // Deduplication - See if an identical chunk to this has already been written
+            if (item->encodeOutput->samples.count == 1) {
+                avifEncodeSample * sample = &item->encodeOutput->samples.sample[0];
+                chunkOffset = avifEncoderFindExistingChunk(&s, mdatStartOffset, sample->data.data, sample->data.size);
             } else {
-                avifRWStreamWrite(&s, item->metadataPayload.data, item->metadataPayload.size);
+                chunkOffset = avifEncoderFindExistingChunk(&s, mdatStartOffset, item->metadataPayload.data, item->metadataPayload.size);
+            }
+
+            if (!chunkOffset) {
+                // We've never seen this chunk before; write it out
+                chunkOffset = (uint32_t)avifRWStreamOffset(&s);
+                if (item->encodeOutput->samples.count > 0) {
+                    for (uint32_t sampleIndex = 0; sampleIndex < item->encodeOutput->samples.count; ++sampleIndex) {
+                        avifEncodeSample * sample = &item->encodeOutput->samples.sample[sampleIndex];
+                        avifRWStreamWrite(&s, sample->data.data, sample->data.size);
+
+                        if (item->alpha) {
+                            encoder->ioStats.alphaOBUSize += sample->data.size;
+                        } else {
+                            encoder->ioStats.colorOBUSize += sample->data.size;
+                        }
+                    }
+                } else {
+                    avifRWStreamWrite(&s, item->metadataPayload.data, item->metadataPayload.size);
+                }
             }
 
             for (uint32_t fixupIndex = 0; fixupIndex < item->mdatFixups.count; ++fixupIndex) {
                 avifOffsetFixup * fixup = &item->mdatFixups.fixup[fixupIndex];
                 size_t prevOffset = avifRWStreamOffset(&s);
                 avifRWStreamSetOffset(&s, fixup->offset);
-                avifRWStreamWriteU32(&s, chunkOffset);
+                avifRWStreamWriteU32(&s, (uint32_t)chunkOffset);
                 avifRWStreamSetOffset(&s, prevOffset);
             }
         }