Refactor codec implementations to pave the way for image sequence encoding
diff --git a/include/avif/avif.h b/include/avif/avif.h
index ec0e8c9..41a827a 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -336,8 +336,8 @@
 } avifImage;
 
 avifImage * avifImageCreate(int width, int height, int depth, avifPixelFormat yuvFormat);
-avifImage * avifImageCreateEmpty(void);                               // helper for making an image to decode into
-void avifImageCopy(avifImage * dstImage, const avifImage * srcImage); // deep copy
+avifImage * avifImageCreateEmpty(void); // helper for making an image to decode into
+void avifImageCopy(avifImage * dstImage, const avifImage * srcImage, uint32_t planes); // deep copy
 void avifImageDestroy(avifImage * image);
 
 void avifImageSetProfileICC(avifImage * image, const uint8_t * icc, size_t iccSize);
diff --git a/include/avif/internal.h b/include/avif/internal.h
index 6f8a52f..3f80251 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -136,11 +136,8 @@
 typedef avifBool (*avifCodecOpenFunc)(struct avifCodec * codec, uint32_t firstSampleIndex);
 typedef avifBool (*avifCodecGetNextImageFunc)(struct avifCodec * codec, avifImage * image);
 // avifCodecEncodeImageFunc: if either OBU* is null, skip its encode. alpha should always be lossless
-typedef avifBool (*avifCodecEncodeImageFunc)(struct avifCodec * codec,
-                                             const avifImage * image,
-                                             avifEncoder * encoder,
-                                             avifRWData * obu,
-                                             avifBool alpha);
+typedef avifBool (*avifCodecEncodeImageFunc)(struct avifCodec * codec, const avifImage * image, avifEncoder * encoder, avifBool alpha);
+typedef avifBool (*avifCodecEncodeFinishFunc)(struct avifCodec * codec, avifRWData * obu);
 typedef void (*avifCodecDestroyInternalFunc)(struct avifCodec * codec);
 
 typedef struct avifCodec
@@ -152,6 +149,7 @@
     avifCodecOpenFunc open;
     avifCodecGetNextImageFunc getNextImage;
     avifCodecEncodeImageFunc encodeImage;
+    avifCodecEncodeFinishFunc encodeFinish;
     avifCodecDestroyInternalFunc destroyInternal;
 } avifCodec;
 
diff --git a/src/avif.c b/src/avif.c
index 28a5031..6d98729 100644
--- a/src/avif.c
+++ b/src/avif.c
@@ -131,7 +131,7 @@
     return avifImageCreate(0, 0, 0, AVIF_PIXEL_FORMAT_NONE);
 }
 
-void avifImageCopy(avifImage * dstImage, const avifImage * srcImage)
+void avifImageCopy(avifImage * dstImage, const avifImage * srcImage, uint32_t planes)
 {
     avifImageFreePlanes(dstImage, AVIF_PLANES_ALL);
 
@@ -157,7 +157,7 @@
     avifImageSetMetadataExif(dstImage, srcImage->exif.data, srcImage->exif.size);
     avifImageSetMetadataXMP(dstImage, srcImage->xmp.data, srcImage->xmp.size);
 
-    if (srcImage->yuvPlanes[AVIF_CHAN_Y]) {
+    if ((planes & AVIF_PLANES_YUV) && srcImage->yuvPlanes[AVIF_CHAN_Y]) {
         avifImageAllocatePlanes(dstImage, AVIF_PLANES_YUV);
 
         avifPixelFormatInfo formatInfo;
@@ -191,7 +191,7 @@
         }
     }
 
-    if (srcImage->alphaPlane) {
+    if ((planes & AVIF_PLANES_A) && srcImage->alphaPlane) {
         avifImageAllocatePlanes(dstImage, AVIF_PLANES_A);
         for (uint32_t j = 0; j < dstImage->height; ++j) {
             uint8_t * srcAlphaRow = &srcImage->alphaPlane[j * srcImage->alphaRowBytes];
diff --git a/src/codec_aom.c b/src/codec_aom.c
index 9e447c7..b313a12 100644
--- a/src/codec_aom.c
+++ b/src/codec_aom.c
@@ -34,6 +34,12 @@
     aom_codec_iter_t iter;
     uint32_t inputSampleIndex;
     aom_image_t * image;
+
+    avifBool encoderInitialized;
+    aom_codec_ctx_t encoder;
+    avifPixelFormatInfo formatInfo;
+    aom_img_fmt_t aomFormat;
+    int yShift;
 };
 
 static void aomCodecDestroyInternal(avifCodec * codec)
@@ -41,6 +47,9 @@
     if (codec->internal->decoderInitialized) {
         aom_codec_destroy(&codec->internal->decoder);
     }
+    if (codec->internal->encoderInitialized) {
+        aom_codec_destroy(&codec->internal->encoder);
+    }
     avifFree(codec->internal);
 }
 
@@ -226,122 +235,120 @@
     return fmt;
 }
 
-static avifBool aomCodecEncodeImage(avifCodec * codec, const avifImage * image, avifEncoder * encoder, avifRWData * obu, avifBool alpha)
+static avifBool aomCodecEncodeImage(avifCodec * codec, const avifImage * image, avifEncoder * encoder, avifBool alpha)
 {
-    avifBool success = AVIF_FALSE;
-    aom_codec_iface_t * encoder_interface = aom_codec_av1_cx();
-    aom_codec_ctx_t aomEncoder;
+    if (!codec->internal->encoderInitialized) {
+        // Map encoder speed to AOM usage + CpuUsed:
+        // Speed  0: GoodQuality CpuUsed 0
+        // Speed  1: GoodQuality CpuUsed 1
+        // Speed  2: GoodQuality CpuUsed 2
+        // Speed  3: GoodQuality CpuUsed 3
+        // Speed  4: GoodQuality CpuUsed 4
+        // Speed  5: GoodQuality CpuUsed 5
+        // Speed  6: GoodQuality CpuUsed 5
+        // Speed  7: GoodQuality CpuUsed 5
+        // Speed  8: RealTime    CpuUsed 6
+        // Speed  9: RealTime    CpuUsed 7
+        // Speed 10: RealTime    CpuUsed 8
+        unsigned int aomUsage = AOM_USAGE_GOOD_QUALITY;
+        int aomCpuUsed = -1;
+        if (encoder->speed != AVIF_SPEED_DEFAULT) {
+            if (encoder->speed < 8) {
+                aomUsage = AOM_USAGE_GOOD_QUALITY;
+                aomCpuUsed = AVIF_CLAMP(encoder->speed, 0, 5);
+            } else {
+                aomUsage = AOM_USAGE_REALTIME;
+                aomCpuUsed = AVIF_CLAMP(encoder->speed - 2, 6, 8);
+            }
+        }
 
-    // Map encoder speed to AOM usage + CpuUsed:
-    // Speed  0: GoodQuality CpuUsed 0
-    // Speed  1: GoodQuality CpuUsed 1
-    // Speed  2: GoodQuality CpuUsed 2
-    // Speed  3: GoodQuality CpuUsed 3
-    // Speed  4: GoodQuality CpuUsed 4
-    // Speed  5: GoodQuality CpuUsed 5
-    // Speed  6: GoodQuality CpuUsed 5
-    // Speed  7: GoodQuality CpuUsed 5
-    // Speed  8: RealTime    CpuUsed 6
-    // Speed  9: RealTime    CpuUsed 7
-    // Speed 10: RealTime    CpuUsed 8
-    unsigned int aomUsage = AOM_USAGE_GOOD_QUALITY;
-    int aomCpuUsed = -1;
-    if (encoder->speed != AVIF_SPEED_DEFAULT) {
-        if (encoder->speed < 8) {
-            aomUsage = AOM_USAGE_GOOD_QUALITY;
-            aomCpuUsed = AVIF_CLAMP(encoder->speed, 0, 5);
-        } else {
-            aomUsage = AOM_USAGE_REALTIME;
-            aomCpuUsed = AVIF_CLAMP(encoder->speed - 2, 6, 8);
+        int aomMajorVersion = aom_codec_version_major();
+        if ((aomMajorVersion < 2) && (image->depth > 8)) {
+            // Due to a known issue with libavif v1.0.0-errata1-avif, 10bpc and
+            // 12bpc image encodes will call the wrong variant of
+            // aom_subtract_block when cpu-used is 7 or 8, and crash. Until we get
+            // a new tagged release from libaom with the fix and can verify we're
+            // running with that version of libaom, we must avoid using
+            // cpu-used=7/8 on any >8bpc image encodes.
+            //
+            // Context:
+            //   * https://github.com/AOMediaCodec/libavif/issues/49
+            //   * https://bugs.chromium.org/p/aomedia/issues/detail?id=2587
+            //
+            // Continued bug tracking here:
+            //   * https://github.com/AOMediaCodec/libavif/issues/56
+
+            if (aomCpuUsed > 6) {
+                aomCpuUsed = 6;
+            }
+        }
+
+        codec->internal->aomFormat = avifImageCalcAOMFmt(image, alpha, &codec->internal->yShift);
+        if (codec->internal->aomFormat == AOM_IMG_FMT_NONE) {
+            return AVIF_FALSE;
+        }
+
+        avifGetPixelFormatInfo(image->yuvFormat, &codec->internal->formatInfo);
+
+        aom_codec_iface_t * encoder_interface = aom_codec_av1_cx();
+        struct aom_codec_enc_cfg cfg;
+        aom_codec_enc_config_default(encoder_interface, &cfg, aomUsage);
+        codec->internal->encoderInitialized = AVIF_TRUE;
+
+        cfg.g_profile = codec->configBox.seqProfile;
+        cfg.g_bit_depth = image->depth;
+        cfg.g_input_bit_depth = image->depth;
+        cfg.g_w = image->width;
+        cfg.g_h = image->height;
+        if (encoder->maxThreads > 1) {
+            cfg.g_threads = encoder->maxThreads;
+        }
+
+        int minQuantizer = AVIF_CLAMP(encoder->minQuantizer, 0, 63);
+        int maxQuantizer = AVIF_CLAMP(encoder->maxQuantizer, 0, 63);
+        if (alpha) {
+            minQuantizer = AVIF_CLAMP(encoder->minQuantizerAlpha, 0, 63);
+            maxQuantizer = AVIF_CLAMP(encoder->maxQuantizerAlpha, 0, 63);
+        }
+        avifBool lossless = ((minQuantizer == AVIF_QUANTIZER_LOSSLESS) && (maxQuantizer == AVIF_QUANTIZER_LOSSLESS));
+        cfg.rc_min_quantizer = minQuantizer;
+        cfg.rc_max_quantizer = maxQuantizer;
+
+        if (alpha || (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400)) {
+            cfg.monochrome = 1;
+        }
+
+        aom_codec_flags_t encoderFlags = 0;
+        if (image->depth > 8) {
+            encoderFlags |= AOM_CODEC_USE_HIGHBITDEPTH;
+        }
+        aom_codec_enc_init(&codec->internal->encoder, encoder_interface, &cfg, encoderFlags);
+
+        if (lossless) {
+            aom_codec_control(&codec->internal->encoder, AV1E_SET_LOSSLESS, 1);
+        }
+        if (encoder->maxThreads > 1) {
+            aom_codec_control(&codec->internal->encoder, AV1E_SET_ROW_MT, 1);
+        }
+        if (encoder->tileRowsLog2 != 0) {
+            int tileRowsLog2 = AVIF_CLAMP(encoder->tileRowsLog2, 0, 6);
+            aom_codec_control(&codec->internal->encoder, AV1E_SET_TILE_ROWS, tileRowsLog2);
+        }
+        if (encoder->tileColsLog2 != 0) {
+            int tileColsLog2 = AVIF_CLAMP(encoder->tileColsLog2, 0, 6);
+            aom_codec_control(&codec->internal->encoder, AV1E_SET_TILE_COLUMNS, tileColsLog2);
+        }
+        if (aomCpuUsed != -1) {
+            aom_codec_control(&codec->internal->encoder, AOME_SET_CPUUSED, aomCpuUsed);
         }
     }
 
-    int aomMajorVersion = aom_codec_version_major();
-    if ((aomMajorVersion < 2) && (image->depth > 8)) {
-        // Due to a known issue with libavif v1.0.0-errata1-avif, 10bpc and
-        // 12bpc image encodes will call the wrong variant of
-        // aom_subtract_block when cpu-used is 7 or 8, and crash. Until we get
-        // a new tagged release from libaom with the fix and can verify we're
-        // running with that version of libaom, we must avoid using
-        // cpu-used=7/8 on any >8bpc image encodes.
-        //
-        // Context:
-        //   * https://github.com/AOMediaCodec/libavif/issues/49
-        //   * https://bugs.chromium.org/p/aomedia/issues/detail?id=2587
-        //
-        // Continued bug tracking here:
-        //   * https://github.com/AOMediaCodec/libavif/issues/56
-
-        if (aomCpuUsed > 6) {
-            aomCpuUsed = 6;
-        }
-    }
-
-    int yShift = 0;
-    aom_img_fmt_t aomFormat = avifImageCalcAOMFmt(image, alpha, &yShift);
-    if (aomFormat == AOM_IMG_FMT_NONE) {
-        return AVIF_FALSE;
-    }
-
-    avifPixelFormatInfo formatInfo;
-    avifGetPixelFormatInfo(image->yuvFormat, &formatInfo);
-
-    struct aom_codec_enc_cfg cfg;
-    aom_codec_enc_config_default(encoder_interface, &cfg, aomUsage);
-
-    cfg.g_profile = codec->configBox.seqProfile;
-    cfg.g_bit_depth = image->depth;
-    cfg.g_input_bit_depth = image->depth;
-    cfg.g_w = image->width;
-    cfg.g_h = image->height;
-    if (encoder->maxThreads > 1) {
-        cfg.g_threads = encoder->maxThreads;
-    }
-
-    int minQuantizer = AVIF_CLAMP(encoder->minQuantizer, 0, 63);
-    int maxQuantizer = AVIF_CLAMP(encoder->maxQuantizer, 0, 63);
-    if (alpha) {
-        minQuantizer = AVIF_CLAMP(encoder->minQuantizerAlpha, 0, 63);
-        maxQuantizer = AVIF_CLAMP(encoder->maxQuantizerAlpha, 0, 63);
-    }
-    avifBool lossless = ((minQuantizer == AVIF_QUANTIZER_LOSSLESS) && (maxQuantizer == AVIF_QUANTIZER_LOSSLESS));
-    cfg.rc_min_quantizer = minQuantizer;
-    cfg.rc_max_quantizer = maxQuantizer;
-
-    if (alpha || (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400)) {
-        cfg.monochrome = 1;
-    }
-
-    aom_codec_flags_t encoderFlags = 0;
-    if (image->depth > 8) {
-        encoderFlags |= AOM_CODEC_USE_HIGHBITDEPTH;
-    }
-    aom_codec_enc_init(&aomEncoder, encoder_interface, &cfg, encoderFlags);
-
-    if (lossless) {
-        aom_codec_control(&aomEncoder, AV1E_SET_LOSSLESS, 1);
-    }
-    if (encoder->maxThreads > 1) {
-        aom_codec_control(&aomEncoder, AV1E_SET_ROW_MT, 1);
-    }
-    if (encoder->tileRowsLog2 != 0) {
-        int tileRowsLog2 = AVIF_CLAMP(encoder->tileRowsLog2, 0, 6);
-        aom_codec_control(&aomEncoder, AV1E_SET_TILE_ROWS, tileRowsLog2);
-    }
-    if (encoder->tileColsLog2 != 0) {
-        int tileColsLog2 = AVIF_CLAMP(encoder->tileColsLog2, 0, 6);
-        aom_codec_control(&aomEncoder, AV1E_SET_TILE_COLUMNS, tileColsLog2);
-    }
-    if (aomCpuUsed != -1) {
-        aom_codec_control(&aomEncoder, AOME_SET_CPUUSED, aomCpuUsed);
-    }
-
-    uint32_t uvHeight = (image->height + yShift) >> yShift;
-    aom_image_t * aomImage = aom_img_alloc(NULL, aomFormat, image->width, image->height, 16);
+    uint32_t uvHeight = (image->height + codec->internal->yShift) >> codec->internal->yShift;
+    aom_image_t * aomImage = aom_img_alloc(NULL, codec->internal->aomFormat, image->width, image->height, 16);
 
     if (alpha) {
         aomImage->range = (image->alphaRange == AVIF_RANGE_FULL) ? AOM_CR_FULL_RANGE : AOM_CR_STUDIO_RANGE;
-        aom_codec_control(&aomEncoder, AV1E_SET_COLOR_RANGE, aomImage->range);
+        aom_codec_control(&codec->internal->encoder, AV1E_SET_COLOR_RANGE, aomImage->range);
         aomImage->monochrome = 1;
         for (uint32_t j = 0; j < image->height; ++j) {
             uint8_t * srcAlphaRow = &image->alphaPlane[j * image->alphaRowBytes];
@@ -352,7 +359,7 @@
         // Ignore UV planes when monochrome
     } else {
         aomImage->range = (image->yuvRange == AVIF_RANGE_FULL) ? AOM_CR_FULL_RANGE : AOM_CR_STUDIO_RANGE;
-        aom_codec_control(&aomEncoder, AV1E_SET_COLOR_RANGE, aomImage->range);
+        aom_codec_control(&codec->internal->encoder, AV1E_SET_COLOR_RANGE, aomImage->range);
         int yuvPlaneCount = 3;
         if (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
             yuvPlaneCount = 1; // Ignore UV planes when monochrome
@@ -362,10 +369,10 @@
             int aomPlaneIndex = yuvPlane;
             int planeHeight = image->height;
             if (yuvPlane == AVIF_CHAN_U) {
-                aomPlaneIndex = formatInfo.aomIndexU;
+                aomPlaneIndex = codec->internal->formatInfo.aomIndexU;
                 planeHeight = uvHeight;
             } else if (yuvPlane == AVIF_CHAN_V) {
-                aomPlaneIndex = formatInfo.aomIndexV;
+                aomPlaneIndex = codec->internal->formatInfo.aomIndexV;
                 planeHeight = uvHeight;
             }
 
@@ -379,22 +386,29 @@
         aomImage->cp = (aom_color_primaries_t)image->colorPrimaries;
         aomImage->tc = (aom_transfer_characteristics_t)image->transferCharacteristics;
         aomImage->mc = (aom_matrix_coefficients_t)image->matrixCoefficients;
-        aom_codec_control(&aomEncoder, AV1E_SET_COLOR_PRIMARIES, aomImage->cp);
-        aom_codec_control(&aomEncoder, AV1E_SET_TRANSFER_CHARACTERISTICS, aomImage->tc);
-        aom_codec_control(&aomEncoder, AV1E_SET_MATRIX_COEFFICIENTS, aomImage->mc);
+        aom_codec_control(&codec->internal->encoder, AV1E_SET_COLOR_PRIMARIES, aomImage->cp);
+        aom_codec_control(&codec->internal->encoder, AV1E_SET_TRANSFER_CHARACTERISTICS, aomImage->tc);
+        aom_codec_control(&codec->internal->encoder, AV1E_SET_MATRIX_COEFFICIENTS, aomImage->mc);
     }
 
-    aom_codec_encode(&aomEncoder, aomImage, 0, 1, 0);
+    aom_codec_encode(&codec->internal->encoder, aomImage, 0, 1, 0);
+    aom_img_free(aomImage);
+    return AVIF_TRUE;
+}
+
+static avifBool aomCodecEncodeFinish(avifCodec * codec, avifRWData * obu)
+{
+    avifBool success = AVIF_FALSE;
 
     avifBool flushed = AVIF_FALSE;
     aom_codec_iter_t iter = NULL;
     for (;;) {
-        const aom_codec_cx_pkt_t * pkt = aom_codec_get_cx_data(&aomEncoder, &iter);
+        const aom_codec_cx_pkt_t * pkt = aom_codec_get_cx_data(&codec->internal->encoder, &iter);
         if (pkt == NULL) {
             if (flushed)
                 break;
 
-            aom_codec_encode(&aomEncoder, NULL, 0, 1, 0); // flush
+            aom_codec_encode(&codec->internal->encoder, NULL, 0, 1, 0); // flush
             flushed = AVIF_TRUE;
             continue;
         }
@@ -404,9 +418,6 @@
             break;
         }
     }
-
-    aom_img_free(aomImage);
-    aom_codec_destroy(&aomEncoder);
     return success;
 }
 
@@ -422,6 +433,7 @@
     codec->open = aomCodecOpen;
     codec->getNextImage = aomCodecGetNextImage;
     codec->encodeImage = aomCodecEncodeImage;
+    codec->encodeFinish = aomCodecEncodeFinish;
     codec->destroyInternal = aomCodecDestroyInternal;
 
     codec->internal = (struct avifCodecInternal *)avifAlloc(sizeof(struct avifCodecInternal));
diff --git a/src/codec_rav1e.c b/src/codec_rav1e.c
index 7c7a6f4..02bccf9 100644
--- a/src/codec_rav1e.c
+++ b/src/codec_rav1e.c
@@ -9,168 +9,159 @@
 
 struct avifCodecInternal
 {
-    uint32_t unused; // rav1e codec has no state
+    RaContext * rav1eContext;
+    RaChromaSampling chromaSampling;
+    int yShift;
 };
 
 static void rav1eCodecDestroyInternal(avifCodec * codec)
 {
+    if (codec->internal->rav1eContext) {
+        rav1e_context_unref(codec->internal->rav1eContext);
+        codec->internal->rav1eContext = NULL;
+    }
     avifFree(codec->internal);
 }
 
 static avifBool rav1eCodecOpen(struct avifCodec * codec, uint32_t firstSampleIndex)
 {
     (void)firstSampleIndex; // Codec is encode-only, this isn't used
-    (void)codec;
+
+    codec->internal->rav1eContext = NULL;
     return AVIF_TRUE;
 }
 
-static avifBool rav1eCodecEncodeImage(avifCodec * codec, const avifImage * image, avifEncoder * encoder, avifRWData * obu, avifBool alpha)
+static avifBool rav1eCodecEncodeImage(avifCodec * codec, const avifImage * image, avifEncoder * encoder, avifBool alpha)
 {
     (void)codec; // unused
 
     avifBool success = AVIF_FALSE;
 
     RaConfig * rav1eConfig = NULL;
-    RaContext * rav1eContext = NULL;
     RaFrame * rav1eFrame = NULL;
-    RaPacket * pkt = NULL;
 
-    int yShift = 0;
-    RaChromaSampling chromaSampling;
-    RaPixelRange rav1eRange;
-    if (alpha) {
-        rav1eRange = (image->alphaRange == AVIF_RANGE_FULL) ? RA_PIXEL_RANGE_FULL : RA_PIXEL_RANGE_LIMITED;
-        chromaSampling = RA_CHROMA_SAMPLING_CS400;
-    } else {
-        rav1eRange = (image->yuvRange == AVIF_RANGE_FULL) ? RA_PIXEL_RANGE_FULL : RA_PIXEL_RANGE_LIMITED;
-        switch (image->yuvFormat) {
-            case AVIF_PIXEL_FORMAT_YUV444:
-                chromaSampling = RA_CHROMA_SAMPLING_CS444;
-                break;
-            case AVIF_PIXEL_FORMAT_YUV422:
-                chromaSampling = RA_CHROMA_SAMPLING_CS422;
-                break;
-            case AVIF_PIXEL_FORMAT_YUV420:
-                chromaSampling = RA_CHROMA_SAMPLING_CS420;
-                yShift = 1;
-                break;
-            case AVIF_PIXEL_FORMAT_YUV400:
-                chromaSampling = RA_CHROMA_SAMPLING_CS400;
-                yShift = 1;
-                break;
-            case AVIF_PIXEL_FORMAT_YV12:
-            case AVIF_PIXEL_FORMAT_NONE:
-            default:
-                return AVIF_FALSE;
+    if (!codec->internal->rav1eContext) {
+        RaPixelRange rav1eRange;
+        if (alpha) {
+            rav1eRange = (image->alphaRange == AVIF_RANGE_FULL) ? RA_PIXEL_RANGE_FULL : RA_PIXEL_RANGE_LIMITED;
+            codec->internal->chromaSampling = RA_CHROMA_SAMPLING_CS400;
+        } else {
+            rav1eRange = (image->yuvRange == AVIF_RANGE_FULL) ? RA_PIXEL_RANGE_FULL : RA_PIXEL_RANGE_LIMITED;
+            codec->internal->yShift = 0;
+            switch (image->yuvFormat) {
+                case AVIF_PIXEL_FORMAT_YUV444:
+                    codec->internal->chromaSampling = RA_CHROMA_SAMPLING_CS444;
+                    break;
+                case AVIF_PIXEL_FORMAT_YUV422:
+                    codec->internal->chromaSampling = RA_CHROMA_SAMPLING_CS422;
+                    break;
+                case AVIF_PIXEL_FORMAT_YUV420:
+                    codec->internal->chromaSampling = RA_CHROMA_SAMPLING_CS420;
+                    codec->internal->yShift = 1;
+                    break;
+                case AVIF_PIXEL_FORMAT_YUV400:
+                    codec->internal->chromaSampling = RA_CHROMA_SAMPLING_CS400;
+                    codec->internal->yShift = 1;
+                    break;
+                case AVIF_PIXEL_FORMAT_YV12:
+                case AVIF_PIXEL_FORMAT_NONE:
+                default:
+                    return AVIF_FALSE;
+            }
         }
-    }
 
-    rav1eConfig = rav1e_config_default();
-    if (rav1e_config_set_pixel_format(
-            rav1eConfig, (uint8_t)image->depth, chromaSampling, RA_CHROMA_SAMPLE_POSITION_UNKNOWN, rav1eRange) < 0) {
-        goto cleanup;
-    }
-
-    if (rav1e_config_parse(rav1eConfig, "still_picture", "true") == -1) {
-        goto cleanup;
-    }
-    if (rav1e_config_parse_int(rav1eConfig, "width", image->width) == -1) {
-        goto cleanup;
-    }
-    if (rav1e_config_parse_int(rav1eConfig, "height", image->height) == -1) {
-        goto cleanup;
-    }
-    if (rav1e_config_parse_int(rav1eConfig, "threads", encoder->maxThreads) == -1) {
-        goto cleanup;
-    }
-
-    int minQuantizer = AVIF_CLAMP(encoder->minQuantizer, 0, 63);
-    int maxQuantizer = AVIF_CLAMP(encoder->maxQuantizer, 0, 63);
-    if (alpha) {
-        minQuantizer = AVIF_CLAMP(encoder->minQuantizerAlpha, 0, 63);
-        maxQuantizer = AVIF_CLAMP(encoder->maxQuantizerAlpha, 0, 63);
-    }
-    minQuantizer = (minQuantizer * 255) / 63; // Rescale quantizer values as rav1e's QP range is [0,255]
-    maxQuantizer = (maxQuantizer * 255) / 63;
-    if (rav1e_config_parse_int(rav1eConfig, "min_quantizer", minQuantizer) == -1) {
-        goto cleanup;
-    }
-    if (rav1e_config_parse_int(rav1eConfig, "quantizer", maxQuantizer) == -1) {
-        goto cleanup;
-    }
-    if (encoder->tileRowsLog2 != 0) {
-        int tileRowsLog2 = AVIF_CLAMP(encoder->tileRowsLog2, 0, 6);
-        if (rav1e_config_parse_int(rav1eConfig, "tile_rows", 1 << tileRowsLog2) == -1) {
+        rav1eConfig = rav1e_config_default();
+        if (rav1e_config_set_pixel_format(
+                rav1eConfig, (uint8_t)image->depth, codec->internal->chromaSampling, RA_CHROMA_SAMPLE_POSITION_UNKNOWN, rav1eRange) < 0) {
             goto cleanup;
         }
-    }
-    if (encoder->tileColsLog2 != 0) {
-        int tileColsLog2 = AVIF_CLAMP(encoder->tileColsLog2, 0, 6);
-        if (rav1e_config_parse_int(rav1eConfig, "tile_cols", 1 << tileColsLog2) == -1) {
+
+        if (rav1e_config_parse(rav1eConfig, "still_picture", "true") == -1) {
             goto cleanup;
         }
-    }
-    if (encoder->speed != AVIF_SPEED_DEFAULT) {
-        int speed = AVIF_CLAMP(encoder->speed, 0, 10);
-        if (rav1e_config_parse_int(rav1eConfig, "speed", speed) == -1) {
+        if (rav1e_config_parse_int(rav1eConfig, "width", image->width) == -1) {
+            goto cleanup;
+        }
+        if (rav1e_config_parse_int(rav1eConfig, "height", image->height) == -1) {
+            goto cleanup;
+        }
+        if (rav1e_config_parse_int(rav1eConfig, "threads", encoder->maxThreads) == -1) {
+            goto cleanup;
+        }
+
+        int minQuantizer = AVIF_CLAMP(encoder->minQuantizer, 0, 63);
+        int maxQuantizer = AVIF_CLAMP(encoder->maxQuantizer, 0, 63);
+        if (alpha) {
+            minQuantizer = AVIF_CLAMP(encoder->minQuantizerAlpha, 0, 63);
+            maxQuantizer = AVIF_CLAMP(encoder->maxQuantizerAlpha, 0, 63);
+        }
+        minQuantizer = (minQuantizer * 255) / 63; // Rescale quantizer values as rav1e's QP range is [0,255]
+        maxQuantizer = (maxQuantizer * 255) / 63;
+        if (rav1e_config_parse_int(rav1eConfig, "min_quantizer", minQuantizer) == -1) {
+            goto cleanup;
+        }
+        if (rav1e_config_parse_int(rav1eConfig, "quantizer", maxQuantizer) == -1) {
+            goto cleanup;
+        }
+        if (encoder->tileRowsLog2 != 0) {
+            int tileRowsLog2 = AVIF_CLAMP(encoder->tileRowsLog2, 0, 6);
+            if (rav1e_config_parse_int(rav1eConfig, "tile_rows", 1 << tileRowsLog2) == -1) {
+                goto cleanup;
+            }
+        }
+        if (encoder->tileColsLog2 != 0) {
+            int tileColsLog2 = AVIF_CLAMP(encoder->tileColsLog2, 0, 6);
+            if (rav1e_config_parse_int(rav1eConfig, "tile_cols", 1 << tileColsLog2) == -1) {
+                goto cleanup;
+            }
+        }
+        if (encoder->speed != AVIF_SPEED_DEFAULT) {
+            int speed = AVIF_CLAMP(encoder->speed, 0, 10);
+            if (rav1e_config_parse_int(rav1eConfig, "speed", speed) == -1) {
+                goto cleanup;
+            }
+        }
+
+        rav1e_config_set_color_description(rav1eConfig,
+                                           (RaMatrixCoefficients)image->matrixCoefficients,
+                                           (RaColorPrimaries)image->colorPrimaries,
+                                           (RaTransferCharacteristics)image->transferCharacteristics);
+
+        codec->internal->rav1eContext = rav1e_context_new(rav1eConfig);
+        if (!codec->internal->rav1eContext) {
             goto cleanup;
         }
     }
 
-    rav1e_config_set_color_description(rav1eConfig,
-                                       (RaMatrixCoefficients)image->matrixCoefficients,
-                                       (RaColorPrimaries)image->colorPrimaries,
-                                       (RaTransferCharacteristics)image->transferCharacteristics);
-
-    rav1eContext = rav1e_context_new(rav1eConfig);
-    if (!rav1eContext) {
-        goto cleanup;
-    }
-    rav1eFrame = rav1e_frame_new(rav1eContext);
+    rav1eFrame = rav1e_frame_new(codec->internal->rav1eContext);
 
     int byteWidth = (image->depth > 8) ? 2 : 1;
     if (alpha) {
         rav1e_frame_fill_plane(rav1eFrame, 0, image->alphaPlane, image->alphaRowBytes * image->height, image->alphaRowBytes, byteWidth);
     } else {
-        uint32_t uvHeight = (image->height + yShift) >> yShift;
+        uint32_t uvHeight = (image->height + codec->internal->yShift) >> codec->internal->yShift;
         rav1e_frame_fill_plane(rav1eFrame, 0, image->yuvPlanes[0], image->yuvRowBytes[0] * image->height, image->yuvRowBytes[0], byteWidth);
-        if (chromaSampling != RA_CHROMA_SAMPLING_CS400) {
+        if (codec->internal->chromaSampling != RA_CHROMA_SAMPLING_CS400) {
             rav1e_frame_fill_plane(rav1eFrame, 1, image->yuvPlanes[1], image->yuvRowBytes[1] * uvHeight, image->yuvRowBytes[1], byteWidth);
             rav1e_frame_fill_plane(rav1eFrame, 2, image->yuvPlanes[2], image->yuvRowBytes[2] * uvHeight, image->yuvRowBytes[2], byteWidth);
         }
     }
 
-    RaEncoderStatus encoderStatus = rav1e_send_frame(rav1eContext, rav1eFrame);
+    RaEncoderStatus encoderStatus = rav1e_send_frame(codec->internal->rav1eContext, rav1eFrame);
     if (encoderStatus != 0) {
         goto cleanup;
     }
-    encoderStatus = rav1e_send_frame(rav1eContext, NULL); // flush
+    encoderStatus = rav1e_send_frame(codec->internal->rav1eContext, NULL); // flush
     if (encoderStatus != 0) {
         goto cleanup;
     }
 
-    encoderStatus = rav1e_receive_packet(rav1eContext, &pkt);
-    if (encoderStatus != 0) {
-        goto cleanup;
-    }
-
-    if (pkt && pkt->data && (pkt->len > 0)) {
-        avifRWDataSet(obu, pkt->data, pkt->len);
-        success = AVIF_TRUE;
-    }
+    success = AVIF_TRUE;
 cleanup:
-    if (pkt) {
-        rav1e_packet_unref(pkt);
-        pkt = NULL;
-    }
     if (rav1eFrame) {
         rav1e_frame_unref(rav1eFrame);
         rav1eFrame = NULL;
     }
-    if (rav1eContext) {
-        rav1e_context_unref(rav1eContext);
-        rav1eContext = NULL;
-    }
     if (rav1eConfig) {
         rav1e_config_unref(rav1eConfig);
         rav1eConfig = NULL;
@@ -178,6 +169,19 @@
     return success;
 }
 
+static avifBool rav1eCodecEncodeFinish(avifCodec * codec, avifRWData * obu)
+{
+    RaPacket * pkt = NULL;
+    RaEncoderStatus encoderStatus = rav1e_receive_packet(codec->internal->rav1eContext, &pkt);
+    if ((encoderStatus == 0) && pkt && pkt->data && (pkt->len > 0)) {
+        avifRWDataSet(obu, pkt->data, pkt->len);
+        rav1e_packet_unref(pkt);
+        pkt = NULL;
+        return AVIF_TRUE;
+    }
+    return AVIF_FALSE;
+}
+
 const char * avifCodecVersionRav1e(void)
 {
     return rav1e_version_full();
@@ -189,6 +193,7 @@
     memset(codec, 0, sizeof(struct avifCodec));
     codec->open = rav1eCodecOpen;
     codec->encodeImage = rav1eCodecEncodeImage;
+    codec->encodeFinish = rav1eCodecEncodeFinish;
     codec->destroyInternal = rav1eCodecDestroyInternal;
 
     codec->internal = (struct avifCodecInternal *)avifAlloc(sizeof(struct avifCodecInternal));
diff --git a/src/read.c b/src/read.c
index 22249f7..ee52c97 100644
--- a/src/read.c
+++ b/src/read.c
@@ -2481,6 +2481,6 @@
     if (result != AVIF_RESULT_OK) {
         return result;
     }
-    avifImageCopy(image, decoder->image);
+    avifImageCopy(image, decoder->image, AVIF_PLANES_ALL);
     return AVIF_RESULT_OK;
 }
diff --git a/src/write.c b/src/write.c
index d643aee..434e95d 100644
--- a/src/write.c
+++ b/src/write.c
@@ -37,9 +37,8 @@
 {
     uint16_t id;
     uint8_t type[4];
-    const avifImage * image; // avifImage* to use when encoding or populating ipma for this item (unowned)
-    avifCodec * codec;       // only present on type==av01
-    avifRWData content;      // OBU data on av01, metadata payload for Exif/XMP
+    avifCodec * codec;  // only present on type==av01
+    avifRWData content; // OBU data on av01, metadata payload for Exif/XMP
     avifBool alpha;
 
     const char * infeName;
@@ -61,6 +60,9 @@
 typedef struct avifEncoderData
 {
     avifEncoderItemArray items;
+    avifImage * imageMetadata;
+    avifEncoderItem * colorItem;
+    avifEncoderItem * alphaItem;
     uint16_t lastItemID;
     uint16_t primaryItemID;
 } avifEncoderData;
@@ -69,6 +71,7 @@
 {
     avifEncoderData * data = (avifEncoderData *)avifAlloc(sizeof(avifEncoderData));
     memset(data, 0, sizeof(avifEncoderData));
+    data->imageMetadata = avifImageCreateEmpty();
     avifArrayCreate(&data->items, sizeof(avifEncoderItem), 8);
     return data;
 }
@@ -93,6 +96,7 @@
         }
         avifRWDataFree(&item->content);
     }
+    avifImageDestroy(data->imageMetadata);
     avifArrayDestroy(&data->items);
     avifFree(data);
 }
@@ -121,112 +125,111 @@
     avifFree(encoder);
 }
 
-avifResult avifEncoderWrite(avifEncoder * encoder, const avifImage * image, avifRWData * output)
+static avifResult avifEncoderAddImage(avifEncoder * encoder, const avifImage * image)
 {
+    // -----------------------------------------------------------------------
+    // Validate image
+
     if ((image->depth != 8) && (image->depth != 10) && (image->depth != 12)) {
         return AVIF_RESULT_UNSUPPORTED_DEPTH;
     }
 
-    avifResult result = AVIF_RESULT_UNKNOWN_ERROR;
-
-    avifEncoderItem * colorItem = avifEncoderDataCreateItem(encoder->data, "av01", "Color", 6);
-    colorItem->image = image;
-    colorItem->codec = avifCodecCreate(encoder->codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE);
-    if (!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->primaryItemID = colorItem->id;
-
-    avifBool imageIsOpaque = avifImageIsOpaque(image);
-    if (!imageIsOpaque) {
-        avifEncoderItem * alphaItem = avifEncoderDataCreateItem(encoder->data, "av01", "Alpha", 6);
-        alphaItem->image = image;
-        alphaItem->codec = avifCodecCreate(encoder->codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE);
-        if (!alphaItem->codec) {
-            return AVIF_RESULT_NO_CODEC_AVAILABLE;
-        }
-        alphaItem->alpha = AVIF_TRUE;
-        alphaItem->irefToID = encoder->data->primaryItemID;
-        alphaItem->irefType = "auxl";
-    }
-
-    // -----------------------------------------------------------------------
-    // Create metadata items (Exif, XMP)
-
-    if (image->exif.size > 0) {
-        // Validate Exif payload (if any) and find TIFF header offset
-        uint32_t exifTiffHeaderOffset = 0;
-        if (image->exif.size > 0) {
-            if (image->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))) {
-                    break;
-                }
-                if (!memcmp(&image->exif.data[exifTiffHeaderOffset], tiffHeaderLE, sizeof(tiffHeaderLE))) {
-                    break;
-                }
-            }
-
-            if (exifTiffHeaderOffset >= image->exif.size - 4) {
-                // Couldn't find the TIFF header
-                return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
-            }
-        }
-
-        avifEncoderItem * exifItem = avifEncoderDataCreateItem(encoder->data, "Exif", "Exif", 5);
-        exifItem->irefToID = encoder->data->primaryItemID;
-        exifItem->irefType = "cdsc";
-
-        avifRWDataRealloc(&exifItem->content, sizeof(uint32_t) + image->exif.size);
-        exifTiffHeaderOffset = avifHTONL(exifTiffHeaderOffset);
-        memcpy(exifItem->content.data, &exifTiffHeaderOffset, sizeof(uint32_t));
-        memcpy(exifItem->content.data + sizeof(uint32_t), image->exif.data, image->exif.size);
-    }
-
-    if (image->xmp.size > 0) {
-        avifEncoderItem * xmpItem = avifEncoderDataCreateItem(encoder->data, "mime", "XMP", 4);
-        xmpItem->irefToID = encoder->data->primaryItemID;
-        xmpItem->irefType = "cdsc";
-
-        xmpItem->infeContentType = xmpContentType;
-        xmpItem->infeContentTypeSize = xmpContentTypeSize;
-        avifRWDataSet(&xmpItem->content, image->xmp.data, image->xmp.size);
-    }
-
-    // -----------------------------------------------------------------------
-    // Pre-fill config boxes based on image (codec can query/update later)
-
-    for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) {
-        avifEncoderItem * item = &encoder->data->items.item[itemIndex];
-        if (item->codec && item->image) {
-            fillConfigBox(item->codec, item->image, item->alpha);
-        }
-    }
-
-    // -----------------------------------------------------------------------
-    // Begin write stream
-
-    avifRWStream s;
-    avifRWStreamStart(&s, output);
-
-    // -----------------------------------------------------------------------
-    // Validate image
-
     if (!image->width || !image->height || !image->yuvPlanes[AVIF_CHAN_Y]) {
-        result = AVIF_RESULT_NO_CONTENT;
-        goto writeCleanup;
+        return AVIF_RESULT_NO_CONTENT;
     }
 
     if (image->yuvFormat == AVIF_PIXEL_FORMAT_NONE) {
-        result = AVIF_RESULT_NO_YUV_FORMAT_SELECTED;
-        goto writeCleanup;
+        return AVIF_RESULT_NO_YUV_FORMAT_SELECTED;
+    }
+
+    // -----------------------------------------------------------------------
+
+    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);
+
+        // 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->primaryItemID = encoder->data->colorItem->id;
+
+        avifBool imageIsOpaque = avifImageIsOpaque(image);
+        if (!imageIsOpaque) {
+            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->alphaItem->alpha = AVIF_TRUE;
+            encoder->data->alphaItem->irefToID = encoder->data->primaryItemID;
+            encoder->data->alphaItem->irefType = "auxl";
+        }
+
+        // -----------------------------------------------------------------------
+        // Create metadata items (Exif, XMP)
+
+        if (image->exif.size > 0) {
+            // Validate Exif payload (if any) and find TIFF header offset
+            uint32_t exifTiffHeaderOffset = 0;
+            if (image->exif.size > 0) {
+                if (image->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))) {
+                        break;
+                    }
+                    if (!memcmp(&image->exif.data[exifTiffHeaderOffset], tiffHeaderLE, sizeof(tiffHeaderLE))) {
+                        break;
+                    }
+                }
+
+                if (exifTiffHeaderOffset >= image->exif.size - 4) {
+                    // Couldn't find the TIFF header
+                    return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
+                }
+            }
+
+            avifEncoderItem * exifItem = avifEncoderDataCreateItem(encoder->data, "Exif", "Exif", 5);
+            exifItem->irefToID = encoder->data->primaryItemID;
+            exifItem->irefType = "cdsc";
+
+            avifRWDataRealloc(&exifItem->content, sizeof(uint32_t) + image->exif.size);
+            exifTiffHeaderOffset = avifHTONL(exifTiffHeaderOffset);
+            memcpy(exifItem->content.data, &exifTiffHeaderOffset, sizeof(uint32_t));
+            memcpy(exifItem->content.data + sizeof(uint32_t), image->exif.data, image->exif.size);
+        }
+
+        if (image->xmp.size > 0) {
+            avifEncoderItem * xmpItem = avifEncoderDataCreateItem(encoder->data, "mime", "XMP", 4);
+            xmpItem->irefToID = encoder->data->primaryItemID;
+            xmpItem->irefType = "cdsc";
+
+            xmpItem->infeContentType = xmpContentType;
+            xmpItem->infeContentTypeSize = xmpContentTypeSize;
+            avifRWDataSet(&xmpItem->content, image->xmp.data, image->xmp.size);
+        }
+
+        // -----------------------------------------------------------------------
+        // Pre-fill config boxes based on image (codec can query/update later)
+
+        for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) {
+            avifEncoderItem * item = &encoder->data->items.item[itemIndex];
+            if (item->codec) {
+                fillConfigBox(item->codec, image, item->alpha);
+            }
+        }
+    } else {
+        // Another frame in an image sequence
     }
 
     // -----------------------------------------------------------------------
@@ -234,13 +237,31 @@
 
     for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) {
         avifEncoderItem * item = &encoder->data->items.item[itemIndex];
-        if (item->codec && item->image) {
-            if (!item->codec->encodeImage(item->codec, item->image, encoder, &item->content, item->alpha)) {
-                result = item->alpha ? AVIF_RESULT_ENCODE_ALPHA_FAILED : AVIF_RESULT_ENCODE_COLOR_FAILED;
-                goto writeCleanup;
+        if (item->codec) {
+            if (!item->codec->encodeImage(item->codec, image, encoder, item->alpha)) {
+                return item->alpha ? AVIF_RESULT_ENCODE_ALPHA_FAILED : AVIF_RESULT_ENCODE_COLOR_FAILED;
+            }
+        }
+    }
+    return AVIF_RESULT_OK;
+}
+
+static avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output)
+{
+    if (encoder->data->items.count < 1) {
+        return AVIF_RESULT_NO_CONTENT;
+    }
+
+    // -----------------------------------------------------------------------
+    // Finish up AV1 encoding
+
+    for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) {
+        avifEncoderItem * item = &encoder->data->items.item[itemIndex];
+        if (item->codec) {
+            if (!item->codec->encodeFinish(item->codec, &item->content)) {
+                return item->alpha ? AVIF_RESULT_ENCODE_ALPHA_FAILED : AVIF_RESULT_ENCODE_COLOR_FAILED;
             }
 
-            // TODO: rethink this if/when image grid encoding support is added
             if (item->alpha) {
                 encoder->ioStats.alphaOBUSize = item->content.size;
             } else {
@@ -250,19 +271,27 @@
     }
 
     // -----------------------------------------------------------------------
+    // Begin write stream
+
+    avifImage * imageMetadata = encoder->data->imageMetadata;
+
+    avifRWStream s;
+    avifRWStreamStart(&s, output);
+
+    // -----------------------------------------------------------------------
     // Write ftyp
 
     avifBoxMarker ftyp = avifRWStreamWriteBox(&s, "ftyp", -1, 0);
-    avifRWStreamWriteChars(&s, "avif", 4);                         // unsigned int(32) major_brand;
-    avifRWStreamWriteU32(&s, 0);                                   // unsigned int(32) minor_version;
-    avifRWStreamWriteChars(&s, "avif", 4);                         // unsigned int(32) compatible_brands[];
-    avifRWStreamWriteChars(&s, "mif1", 4);                         // ... compatible_brands[]
-    avifRWStreamWriteChars(&s, "miaf", 4);                         // ... compatible_brands[]
-    if ((image->depth == 8) || (image->depth == 10)) {             //
-        if (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV420) {        //
-            avifRWStreamWriteChars(&s, "MA1B", 4);                 // ... compatible_brands[]
-        } else if (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV444) { //
-            avifRWStreamWriteChars(&s, "MA1A", 4);                 // ... compatible_brands[]
+    avifRWStreamWriteChars(&s, "avif", 4);                                 // unsigned int(32) major_brand;
+    avifRWStreamWriteU32(&s, 0);                                           // unsigned int(32) minor_version;
+    avifRWStreamWriteChars(&s, "avif", 4);                                 // unsigned int(32) compatible_brands[];
+    avifRWStreamWriteChars(&s, "mif1", 4);                                 // ... compatible_brands[]
+    avifRWStreamWriteChars(&s, "miaf", 4);                                 // ... compatible_brands[]
+    if ((imageMetadata->depth == 8) || (imageMetadata->depth == 10)) {     //
+        if (imageMetadata->yuvFormat == AVIF_PIXEL_FORMAT_YUV420) {        //
+            avifRWStreamWriteChars(&s, "MA1B", 4);                         // ... compatible_brands[]
+        } else if (imageMetadata->yuvFormat == AVIF_PIXEL_FORMAT_YUV444) { //
+            avifRWStreamWriteChars(&s, "MA1A", 4);                         // ... compatible_brands[]
         }
     }
     avifRWStreamFinishBox(&s, ftyp);
@@ -362,7 +391,7 @@
     for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) {
         avifEncoderItem * item = &encoder->data->items.item[itemIndex];
         memset(&item->ipma, 0, sizeof(item->ipma));
-        if (!item->image || !item->codec) {
+        if (!item->codec) {
             // No ipma to write for this item
             continue;
         }
@@ -370,8 +399,8 @@
         // Properties all av01 items need
 
         avifBoxMarker ispe = avifRWStreamWriteBox(&s, "ispe", 0, 0);
-        avifRWStreamWriteU32(&s, item->image->width);  // unsigned int(32) image_width;
-        avifRWStreamWriteU32(&s, item->image->height); // unsigned int(32) image_height;
+        avifRWStreamWriteU32(&s, imageMetadata->width);  // unsigned int(32) image_width;
+        avifRWStreamWriteU32(&s, imageMetadata->height); // unsigned int(32) image_height;
         avifRWStreamFinishBox(&s, ispe);
         ipmaPush(&item->ipma, ++itemPropertyIndex, AVIF_FALSE); // ipma is 1-indexed, doing this afterwards is correct
 
@@ -379,7 +408,7 @@
         avifBoxMarker pixi = avifRWStreamWriteBox(&s, "pixi", 0, 0);
         avifRWStreamWriteU8(&s, channelCount); // unsigned int (8) num_channels;
         for (uint8_t chan = 0; chan < channelCount; ++chan) {
-            avifRWStreamWriteU8(&s, (uint8_t)item->image->depth); // unsigned int (8) bits_per_channel;
+            avifRWStreamWriteU8(&s, (uint8_t)imageMetadata->depth); // unsigned int (8) bits_per_channel;
         }
         avifRWStreamFinishBox(&s, pixi);
         ipmaPush(&item->ipma, ++itemPropertyIndex, AVIF_FALSE);
@@ -397,55 +426,55 @@
         } else {
             // Color specific properties
 
-            if (item->image->icc.data && (item->image->icc.size > 0)) {
+            if (imageMetadata->icc.data && (imageMetadata->icc.size > 0)) {
                 avifBoxMarker colr = avifRWStreamWriteBox(&s, "colr", -1, 0);
                 avifRWStreamWriteChars(&s, "prof", 4); // unsigned int(32) colour_type;
-                avifRWStreamWrite(&s, item->image->icc.data, item->image->icc.size);
+                avifRWStreamWrite(&s, imageMetadata->icc.data, imageMetadata->icc.size);
                 avifRWStreamFinishBox(&s, colr);
                 ipmaPush(&item->ipma, ++itemPropertyIndex, AVIF_FALSE);
             } else {
                 avifBoxMarker colr = avifRWStreamWriteBox(&s, "colr", -1, 0);
-                avifRWStreamWriteChars(&s, "nclx", 4);                                    // unsigned int(32) colour_type;
-                avifRWStreamWriteU16(&s, (uint16_t)item->image->colorPrimaries);          // unsigned int(16) colour_primaries;
-                avifRWStreamWriteU16(&s, (uint16_t)item->image->transferCharacteristics); // unsigned int(16) transfer_characteristics;
-                avifRWStreamWriteU16(&s, (uint16_t)item->image->matrixCoefficients);      // unsigned int(16) matrix_coefficients;
-                avifRWStreamWriteU8(&s, (item->image->yuvRange == AVIF_RANGE_FULL) ? 0x80 : 0); // unsigned int(1) full_range_flag;
-                                                                                                // unsigned int(7) reserved = 0;
+                avifRWStreamWriteChars(&s, "nclx", 4);                                      // unsigned int(32) colour_type;
+                avifRWStreamWriteU16(&s, (uint16_t)imageMetadata->colorPrimaries);          // unsigned int(16) colour_primaries;
+                avifRWStreamWriteU16(&s, (uint16_t)imageMetadata->transferCharacteristics); // unsigned int(16) transfer_characteristics;
+                avifRWStreamWriteU16(&s, (uint16_t)imageMetadata->matrixCoefficients); // unsigned int(16) matrix_coefficients;
+                avifRWStreamWriteU8(&s, (imageMetadata->yuvRange == AVIF_RANGE_FULL) ? 0x80 : 0); // unsigned int(1) full_range_flag;
+                                                                                                  // unsigned int(7) reserved = 0;
                 avifRWStreamFinishBox(&s, colr);
                 ipmaPush(&item->ipma, ++itemPropertyIndex, AVIF_FALSE);
             }
 
             // Write (Optional) Transformations
-            if (item->image->transformFlags & AVIF_TRANSFORM_PASP) {
+            if (imageMetadata->transformFlags & AVIF_TRANSFORM_PASP) {
                 avifBoxMarker pasp = avifRWStreamWriteBox(&s, "pasp", -1, 0);
-                avifRWStreamWriteU32(&s, item->image->pasp.hSpacing); // unsigned int(32) hSpacing;
-                avifRWStreamWriteU32(&s, item->image->pasp.vSpacing); // unsigned int(32) vSpacing;
+                avifRWStreamWriteU32(&s, imageMetadata->pasp.hSpacing); // unsigned int(32) hSpacing;
+                avifRWStreamWriteU32(&s, imageMetadata->pasp.vSpacing); // unsigned int(32) vSpacing;
                 avifRWStreamFinishBox(&s, pasp);
                 ipmaPush(&item->ipma, ++itemPropertyIndex, AVIF_FALSE);
             }
-            if (item->image->transformFlags & AVIF_TRANSFORM_CLAP) {
+            if (imageMetadata->transformFlags & AVIF_TRANSFORM_CLAP) {
                 avifBoxMarker clap = avifRWStreamWriteBox(&s, "clap", -1, 0);
-                avifRWStreamWriteU32(&s, item->image->clap.widthN);    // unsigned int(32) cleanApertureWidthN;
-                avifRWStreamWriteU32(&s, item->image->clap.widthD);    // unsigned int(32) cleanApertureWidthD;
-                avifRWStreamWriteU32(&s, item->image->clap.heightN);   // unsigned int(32) cleanApertureHeightN;
-                avifRWStreamWriteU32(&s, item->image->clap.heightD);   // unsigned int(32) cleanApertureHeightD;
-                avifRWStreamWriteU32(&s, item->image->clap.horizOffN); // unsigned int(32) horizOffN;
-                avifRWStreamWriteU32(&s, item->image->clap.horizOffD); // unsigned int(32) horizOffD;
-                avifRWStreamWriteU32(&s, item->image->clap.vertOffN);  // unsigned int(32) vertOffN;
-                avifRWStreamWriteU32(&s, item->image->clap.vertOffD);  // unsigned int(32) vertOffD;
+                avifRWStreamWriteU32(&s, imageMetadata->clap.widthN);    // unsigned int(32) cleanApertureWidthN;
+                avifRWStreamWriteU32(&s, imageMetadata->clap.widthD);    // unsigned int(32) cleanApertureWidthD;
+                avifRWStreamWriteU32(&s, imageMetadata->clap.heightN);   // unsigned int(32) cleanApertureHeightN;
+                avifRWStreamWriteU32(&s, imageMetadata->clap.heightD);   // unsigned int(32) cleanApertureHeightD;
+                avifRWStreamWriteU32(&s, imageMetadata->clap.horizOffN); // unsigned int(32) horizOffN;
+                avifRWStreamWriteU32(&s, imageMetadata->clap.horizOffD); // unsigned int(32) horizOffD;
+                avifRWStreamWriteU32(&s, imageMetadata->clap.vertOffN);  // unsigned int(32) vertOffN;
+                avifRWStreamWriteU32(&s, imageMetadata->clap.vertOffD);  // unsigned int(32) vertOffD;
                 avifRWStreamFinishBox(&s, clap);
                 ipmaPush(&item->ipma, ++itemPropertyIndex, AVIF_TRUE);
             }
-            if (item->image->transformFlags & AVIF_TRANSFORM_IROT) {
+            if (imageMetadata->transformFlags & AVIF_TRANSFORM_IROT) {
                 avifBoxMarker irot = avifRWStreamWriteBox(&s, "irot", -1, 0);
-                uint8_t angle = item->image->irot.angle & 0x3;
+                uint8_t angle = imageMetadata->irot.angle & 0x3;
                 avifRWStreamWrite(&s, &angle, 1); // unsigned int (6) reserved = 0; unsigned int (2) angle;
                 avifRWStreamFinishBox(&s, irot);
                 ipmaPush(&item->ipma, ++itemPropertyIndex, AVIF_TRUE);
             }
-            if (item->image->transformFlags & AVIF_TRANSFORM_IMIR) {
+            if (imageMetadata->transformFlags & AVIF_TRANSFORM_IMIR) {
                 avifBoxMarker imir = avifRWStreamWriteBox(&s, "imir", -1, 0);
-                uint8_t axis = item->image->imir.axis & 0x1;
+                uint8_t axis = imageMetadata->imir.axis & 0x1;
                 avifRWStreamWrite(&s, &axis, 1); // unsigned int (7) reserved = 0; unsigned int (1) axis;
                 avifRWStreamFinishBox(&s, imir);
                 ipmaPush(&item->ipma, ++itemPropertyIndex, AVIF_TRUE);
@@ -518,13 +547,16 @@
 
     avifRWStreamFinishWrite(&s);
 
-    // -----------------------------------------------------------------------
-    // Set result and cleanup
+    return AVIF_RESULT_OK;
+}
 
-    result = AVIF_RESULT_OK;
-
-writeCleanup:
-    return result;
+avifResult avifEncoderWrite(avifEncoder * encoder, const avifImage * image, avifRWData * output)
+{
+    avifResult addImageResult = avifEncoderAddImage(encoder, image);
+    if (addImageResult != AVIF_RESULT_OK) {
+        return addImageResult;
+    }
+    return avifEncoderFinish(encoder, output);
 }
 
 static avifBool avifImageIsOpaque(const avifImage * image)