Add automatic tile scaling to the item's ispe or track's dims

(rough draft, scale.c needs more work)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7d78025..9cbf4bc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -206,6 +206,7 @@
     src/read.c
     src/reformat.c
     src/reformat_libyuv.c
+    src/scale.c
     src/stream.c
     src/utils.c
     src/write.c
diff --git a/include/avif/internal.h b/include/avif/internal.h
index 2f3d576..5fd8a89 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -156,6 +156,12 @@
 avifResult avifRGBImageUnpremultiplyAlphaLibYUV(avifRGBImage * rgb);
 
 // ---------------------------------------------------------------------------
+// Scaling
+
+// This scales the YUV/A planes in-place.
+avifBool avifImageScale(avifImage * image, uint32_t dstWidth, uint32_t dstHeight, avifDiagnostics * diag);
+
+// ---------------------------------------------------------------------------
 // avifCodecDecodeInput
 
 typedef struct avifDecodeSample
diff --git a/src/read.c b/src/read.c
index fe5e619..4c0ac5e 100644
--- a/src/read.c
+++ b/src/read.c
@@ -155,6 +155,8 @@
     uint8_t type[4];
     size_t size;
     uint32_t idatID; // If non-zero, offset is relative to this idat box (iloc construction_method==1)
+    uint32_t width;  // Set from this item's ispe property, if present
+    uint32_t height; // Set from this item's ispe property, if present
     avifContentType contentType;
     avifPropertyArray properties;
     avifExtentArray extents;       // All extent offsets/sizes
@@ -622,6 +624,8 @@
     avifCodecDecodeInput * input;
     struct avifCodec * codec;
     avifImage * image;
+    uint32_t width;  // Either avifTrack.width or avifDecoderItem.width
+    uint32_t height; // Either avifTrack.height or avifDecoderItem.height
     uint8_t operatingPointIndex;
 } avifTile;
 AVIF_ARRAY_DECLARE(avifTileArray, avifTile, tile);
@@ -763,11 +767,13 @@
     }
 }
 
-static avifTile * avifDecoderDataCreateTile(avifDecoderData * data, uint8_t operatingPointIndex)
+static avifTile * avifDecoderDataCreateTile(avifDecoderData * data, uint32_t width, uint32_t height, uint8_t operatingPointIndex)
 {
     avifTile * tile = (avifTile *)avifArrayPushPtr(&data->tiles);
     tile->image = avifImageCreateEmpty();
     tile->input = avifCodecDecodeInputCreate();
+    tile->width = width;
+    tile->height = height;
     tile->operatingPointIndex = operatingPointIndex;
     return tile;
 }
@@ -1125,7 +1131,7 @@
                 continue;
             }
 
-            avifTile * tile = avifDecoderDataCreateTile(decoder->data, avifDecoderItemOperatingPoint(item));
+            avifTile * tile = avifDecoderDataCreateTile(decoder->data, item->width, item->height, avifDecoderItemOperatingPoint(item));
             if (!avifCodecDecodeInputFillFromDecoderItem(tile->input,
                                                          item,
                                                          decoder->allowProgressive,
@@ -2305,6 +2311,11 @@
     track->width = width >> 16;
     track->height = height >> 16;
 
+    if ((track->width == 0) || (track->height == 0) || (track->width > (AVIF_MAX_IMAGE_SIZE / track->height))) {
+        avifDiagnosticsPrintf(diag, "Track ID [%u] has an invalid size [%ux%u]", track->id, track->width, track->height);
+        return AVIF_FALSE;
+    }
+
     // TODO: support scaling based on width/height track header info?
 
     track->id = trackID;
@@ -2986,6 +2997,37 @@
         return parseResult;
     }
 
+    // Walk the decoded items (if any) and harvest ispe
+    avifDecoderData * data = decoder->data;
+    for (uint32_t itemIndex = 0; itemIndex < data->meta->items.count; ++itemIndex) {
+        avifDecoderItem * item = &data->meta->items.item[itemIndex];
+        if (!item->size) {
+            continue;
+        }
+        if (item->hasUnsupportedEssentialProperty) {
+            // An essential property isn't supported by libavif; ignore the item.
+            continue;
+        }
+        avifBool isGrid = (memcmp(item->type, "grid", 4) == 0);
+        if (memcmp(item->type, "av01", 4) && !isGrid) {
+            // probably exif or some other data
+            continue;
+        }
+
+        const avifProperty * ispeProp = avifPropertyArrayFind(&item->properties, "ispe");
+        if (ispeProp) {
+            item->width = ispeProp->u.ispe.width;
+            item->height = ispeProp->u.ispe.height;
+
+            if ((item->width == 0) || (item->height == 0) || (item->width > (AVIF_MAX_IMAGE_SIZE / item->height))) {
+                avifDiagnosticsPrintf(data->diag, "Item ID [%u] has an invalid size [%ux%u]", item->id, item->width, item->height);
+                return AVIF_RESULT_BMFF_PARSE_FAILED;
+            }
+        } else {
+            avifDiagnosticsPrintf(data->diag, "Item ID [%u] is missing a mandatory ispe property", item->id);
+            return AVIF_RESULT_BMFF_PARSE_FAILED;
+        }
+    }
     return avifDecoderReset(decoder);
 }
 
@@ -3122,7 +3164,7 @@
             alphaTrack = &data->tracks.track[alphaTrackIndex];
         }
 
-        avifTile * colorTile = avifDecoderDataCreateTile(data, 0); // No way to set operating point via tracks
+        avifTile * colorTile = avifDecoderDataCreateTile(data, colorTrack->width, colorTrack->height, 0); // No way to set operating point via tracks
         if (!avifCodecDecodeInputFillFromSampleTable(colorTile->input,
                                                      colorTrack->sampleTable,
                                                      decoder->imageCountLimit,
@@ -3133,7 +3175,7 @@
         data->colorTileCount = 1;
 
         if (alphaTrack) {
-            avifTile * alphaTile = avifDecoderDataCreateTile(data, 0); // No way to set operating point via tracks
+            avifTile * alphaTile = avifDecoderDataCreateTile(data, alphaTrack->width, alphaTrack->height, 0); // No way to set operating point via tracks
             if (!avifCodecDecodeInputFillFromSampleTable(alphaTile->input,
                                                          alphaTrack->sampleTable,
                                                          decoder->imageCountLimit,
@@ -3284,7 +3326,8 @@
                 return AVIF_RESULT_NO_AV1_ITEMS_FOUND;
             }
 
-            avifTile * colorTile = avifDecoderDataCreateTile(data, avifDecoderItemOperatingPoint(colorItem));
+            avifTile * colorTile =
+                avifDecoderDataCreateTile(data, colorItem->width, colorItem->height, avifDecoderItemOperatingPoint(colorItem));
             if (!avifCodecDecodeInputFillFromDecoderItem(colorTile->input,
                                                          colorItem,
                                                          decoder->allowProgressive,
@@ -3315,7 +3358,8 @@
                     return AVIF_RESULT_NO_AV1_ITEMS_FOUND;
                 }
 
-                avifTile * alphaTile = avifDecoderDataCreateTile(data, avifDecoderItemOperatingPoint(alphaItem));
+                avifTile * alphaTile =
+                    avifDecoderDataCreateTile(data, alphaItem->width, alphaItem->height, avifDecoderItemOperatingPoint(alphaItem));
                 if (!avifCodecDecodeInputFillFromDecoderItem(alphaTile->input,
                                                              alphaItem,
                                                              decoder->allowProgressive,
@@ -3332,14 +3376,8 @@
         decoder->ioStats.colorOBUSize = colorItem->size;
         decoder->ioStats.alphaOBUSize = alphaItem ? alphaItem->size : 0;
 
-        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;
-        }
+        decoder->image->width = colorItem->width;
+        decoder->image->height = colorItem->height;
         decoder->alphaPresent = (alphaItem != NULL);
         decoder->image->alphaPremultiplied = decoder->alphaPresent && (colorItem->premByID == alphaItem->id);
 
@@ -3515,6 +3553,13 @@
         if (!tile->codec->getNextImage(tile->codec, decoder, sample, tile->input->alpha, tile->image)) {
             return tile->input->alpha ? AVIF_RESULT_DECODE_ALPHA_FAILED : AVIF_RESULT_DECODE_COLOR_FAILED;
         }
+
+        // Scale the decoded image so that it corresponds to this tile's output dimensions
+        if ((tile->width != tile->image->width) || (tile->height != tile->image->height)) {
+            if (!avifImageScale(tile->image, tile->width, tile->height, &decoder->diag)) {
+                return tile->input->alpha ? AVIF_RESULT_DECODE_ALPHA_FAILED : AVIF_RESULT_DECODE_COLOR_FAILED;
+            }
+        }
     }
 
     if (decoder->data->tiles.count != (decoder->data->colorTileCount + decoder->data->alphaTileCount)) {
diff --git a/src/scale.c b/src/scale.c
new file mode 100644
index 0000000..528370f
--- /dev/null
+++ b/src/scale.c
@@ -0,0 +1,122 @@
+// Copyright 2021 Joe Drago. All rights reserved.
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include "avif/internal.h"
+
+#if !defined(AVIF_LIBYUV_ENABLED)
+
+avifBool avifImageScale(avifImage * image, uint32_t dstWidth, uint32_t dstHeight, avifDiagnostics * diag)
+{
+    (void)image;
+    (void)dstWidth;
+    (void)dstHeight;
+    avifDiagnosticsPrintf(diag, "avifImageScale() called, but is unimplemented without libyuv!");
+    return AVIF_FALSE;
+}
+
+#else
+
+#if defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wstrict-prototypes" // "this function declaration is not a prototype"
+#endif
+#include <libyuv.h>
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#endif
+
+// This should be configurable and/or smarter
+#define AVIF_LIBYUV_FILTER_MODE kFilterBox
+
+avifBool avifImageScale(avifImage * image, uint32_t dstWidth, uint32_t dstHeight, avifDiagnostics * diag)
+{
+    if ((image->width == dstWidth) && (image->height == dstHeight)) {
+        // Nothing to do
+        return AVIF_TRUE;
+    }
+
+    if ((dstWidth == 0) || (dstHeight == 0) || (dstWidth > (AVIF_MAX_IMAGE_SIZE / dstHeight))) {
+        avifDiagnosticsPrintf(diag, "avifImageScale requested invalid dst dimensions [%ux%u]", dstWidth, dstHeight);
+        return AVIF_FALSE;
+    }
+
+    uint8_t * srcYUVPlanes[AVIF_PLANE_COUNT_YUV];
+    uint32_t srcYUVRowBytes[AVIF_PLANE_COUNT_YUV];
+    for (int i = 0; i < AVIF_PLANE_COUNT_YUV; ++i) {
+        srcYUVPlanes[i] = image->yuvPlanes[i];
+        image->yuvPlanes[i] = NULL;
+        srcYUVRowBytes[i] = image->yuvRowBytes[i];
+        image->yuvRowBytes[i] = 0;
+    }
+    const avifBool srcImageOwnsYUVPlanes = image->imageOwnsYUVPlanes;
+    image->imageOwnsYUVPlanes = AVIF_FALSE;
+
+    uint8_t * srcAlphaPlane = image->alphaPlane;
+    image->alphaPlane = NULL;
+    uint32_t srcAlphaRowBytes = image->alphaRowBytes;
+    image->alphaRowBytes = 0;
+    const avifBool srcImageOwnsAlphaPlane = image->imageOwnsAlphaPlane;
+    image->imageOwnsAlphaPlane = AVIF_FALSE;
+
+    const uint32_t srcWidth = image->width;
+    image->width = dstWidth;
+    const uint32_t srcHeight = image->height;
+    image->height = dstHeight;
+
+    if (srcYUVPlanes[0]) {
+        avifImageAllocatePlanes(image, AVIF_PLANES_YUV);
+
+        avifPixelFormatInfo formatInfo;
+        avifGetPixelFormatInfo(image->yuvFormat, &formatInfo);
+        const uint32_t srcUVWidth = (srcWidth + formatInfo.chromaShiftX) >> formatInfo.chromaShiftX;
+        const uint32_t srcUVHeight = (srcHeight + formatInfo.chromaShiftY) >> formatInfo.chromaShiftY;
+        const uint32_t dstUVWidth = (dstWidth + formatInfo.chromaShiftX) >> formatInfo.chromaShiftX;
+        const uint32_t dstUVHeight = (dstHeight + formatInfo.chromaShiftY) >> formatInfo.chromaShiftY;
+
+        for (int i = 0; i < AVIF_PLANE_COUNT_YUV; ++i) {
+            if (!srcYUVPlanes[i]) {
+                continue;
+            }
+
+            const uint32_t srcW = (i == AVIF_CHAN_Y) ? srcWidth : srcUVWidth;
+            const uint32_t srcH = (i == AVIF_CHAN_Y) ? srcHeight : srcUVHeight;
+            const uint32_t dstW = (i == AVIF_CHAN_Y) ? dstWidth : dstUVWidth;
+            const uint32_t dstH = (i == AVIF_CHAN_Y) ? dstHeight : dstUVHeight;
+            if (image->depth > 8) {
+                uint16_t * srcPlane = (uint16_t *)srcYUVPlanes[i];
+                uint16_t * dstPlane = (uint16_t *)image->yuvPlanes[i];
+                ScalePlane_12(srcPlane, srcYUVRowBytes[i], srcW, srcH, dstPlane, image->yuvRowBytes[i], dstW, dstH, AVIF_LIBYUV_FILTER_MODE);
+            } else {
+                uint8_t * srcPlane = srcYUVPlanes[i];
+                uint8_t * dstPlane = image->yuvPlanes[i];
+                ScalePlane(srcPlane, srcYUVRowBytes[i], srcW, srcH, dstPlane, image->yuvRowBytes[i], dstW, dstH, AVIF_LIBYUV_FILTER_MODE);
+            }
+
+            if (srcImageOwnsYUVPlanes) {
+                avifFree(srcYUVPlanes[i]);
+            }
+        }
+    }
+
+    if (srcAlphaPlane) {
+        avifImageAllocatePlanes(image, AVIF_PLANES_A);
+
+        if (image->depth > 8) {
+            uint16_t * srcPlane = (uint16_t *)srcAlphaPlane;
+            uint16_t * dstPlane = (uint16_t *)image->alphaPlane;
+            ScalePlane_12(srcPlane, srcAlphaRowBytes, srcWidth, srcHeight, dstPlane, image->alphaRowBytes, dstWidth, dstHeight, AVIF_LIBYUV_FILTER_MODE);
+        } else {
+            uint8_t * srcPlane = srcAlphaPlane;
+            uint8_t * dstPlane = image->alphaPlane;
+            ScalePlane(srcPlane, srcAlphaRowBytes, srcWidth, srcHeight, dstPlane, image->alphaRowBytes, dstWidth, dstHeight, AVIF_LIBYUV_FILTER_MODE);
+        }
+
+        if (srcImageOwnsAlphaPlane) {
+            avifFree(srcAlphaPlane);
+        }
+    }
+
+    return AVIF_TRUE;
+}
+
+#endif