Layered encoding support

Layered image can be encoded by calling avifEncoderAddImage() multiple
times, one layer per call.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d7e84c..a0d8fb0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@
   minQuantizer, maxQuantizer, minQuantizerAlpha, and maxQuantizerAlpha
   initialized to the default values.
 * Add the public API function avifImageIsOpaque() in avif.h.
+* Add experimental API for progressive AVIF encoding.
 
 ### Changed
 * Exif and XMP metadata is exported to PNG and JPEG files by default,
diff --git a/include/avif/avif.h b/include/avif/avif.h
index e4350d0..f8c9a73 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -99,6 +99,9 @@
 // to handle this case however they want.
 #define AVIF_REPETITION_COUNT_UNKNOWN -2
 
+// The number of spatial layers in AV1, with spatial_id = 0..3.
+#define AVIF_MAX_AV1_LAYER_COUNT 4
+
 typedef enum avifPlanesFlag
 {
     AVIF_PLANES_YUV = (1 << 0),
@@ -345,6 +348,15 @@
 AVIF_API void avifDiagnosticsClearError(avifDiagnostics * diag);
 
 // ---------------------------------------------------------------------------
+// Fraction utility
+
+typedef struct avifFraction
+{
+    int32_t n;
+    int32_t d;
+} avifFraction;
+
+// ---------------------------------------------------------------------------
 // Optional transformation structs
 
 typedef enum avifTransformFlag
@@ -1067,6 +1079,12 @@
 struct avifEncoderData;
 struct avifCodecSpecificOptions;
 
+typedef struct avifScalingMode
+{
+    avifFraction horizontal;
+    avifFraction vertical;
+} avifScalingMode;
+
 // Notes:
 // * If avifEncoderWrite() returns AVIF_RESULT_OK, output must be freed with avifRWDataFree()
 // * If (maxThreads < 2), multithreading is disabled
@@ -1087,6 +1105,8 @@
 //   image in less bytes. AVIF_SPEED_DEFAULT means "Leave the AV1 codec to its default speed settings"./
 //   If avifEncoder uses rav1e, the speed value is directly passed through (0-10). If libaom is used,
 //   a combination of settings are tweaked to simulate this speed range.
+// * Extra layer count: [0 - (AVIF_MAX_AV1_LAYER_COUNT-1)]. Non-zero value indicates a layered
+//   (progressive) image.
 // * Some encoder settings can be changed after encoding starts. Changes will take effect in the next
 //   call to avifEncoderAddImage().
 typedef struct avifEncoder
@@ -1097,12 +1117,13 @@
     // settings (see Notes above)
     int maxThreads;
     int speed;
-    int keyframeInterval; // How many frames between automatic forced keyframes; 0 to disable (default).
-    uint64_t timescale;   // timescale of the media (Hz)
-    int repetitionCount;  // Number of times the image sequence should be repeated. This can also be set to
-                          // AVIF_REPETITION_COUNT_INFINITE for infinite repetitions.  Only applicable for image sequences.
-                          // Essentially, if repetitionCount is a non-negative integer `n`, then the image sequence should be
-                          // played back `n + 1` times. Defaults to AVIF_REPETITION_COUNT_INFINITE.
+    int keyframeInterval;     // How many frames between automatic forced keyframes; 0 to disable (default).
+    uint64_t timescale;       // timescale of the media (Hz)
+    int repetitionCount;      // Number of times the image sequence should be repeated. This can also be set to
+                              // AVIF_REPETITION_COUNT_INFINITE for infinite repetitions.  Only applicable for image sequences.
+                              // Essentially, if repetitionCount is a non-negative integer `n`, then the image sequence should be
+                              // played back `n + 1` times. Defaults to AVIF_REPETITION_COUNT_INFINITE.
+    uint32_t extraLayerCount; // EXPERIMENTAL: Non-zero value encodes layered image.
 
     // changeable encoder settings
     int quality;
@@ -1114,6 +1135,7 @@
     int tileRowsLog2;
     int tileColsLog2;
     avifBool autoTiling;
+    avifScalingMode scalingMode;
 
     // stats from the most recent write
     avifIOStats ioStats;
@@ -1137,26 +1159,36 @@
     // Force this frame to be a keyframe (sync frame).
     AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME = (1 << 0),
 
-    // Use this flag when encoding a single image. Signals "still_picture" to AV1 encoders, which
-    // tweaks various compression rules. This is enabled automatically when using the
-    // avifEncoderWrite() single-image encode path.
+    // Use this flag when encoding a single frame, single layer image.
+    // Signals "still_picture" to AV1 encoders, which tweaks various compression rules.
+    // This is enabled automatically when using the avifEncoderWrite() single-image encode path.
     AVIF_ADD_IMAGE_FLAG_SINGLE = (1 << 1)
 } avifAddImageFlag;
 typedef uint32_t avifAddImageFlags;
 
-// Multi-function alternative to avifEncoderWrite() for image sequences.
+// Multi-function alternative to avifEncoderWrite() for advanced features.
 //
 // Usage / function call order is:
 // * avifEncoderCreate()
-// * Set encoder->timescale (Hz) correctly
-// * avifEncoderAddImage() ... [repeatedly; at least once]
-//   OR
-// * avifEncoderAddImageGrid() [exactly once, AVIF_ADD_IMAGE_FLAG_SINGLE is assumed]
+// - Still image:
+//   * avifEncoderAddImage() [exactly once]
+// - Still image grid:
+//   * avifEncoderAddImageGrid() [exactly once, AVIF_ADD_IMAGE_FLAG_SINGLE is assumed]
+// - Image sequence:
+//   * Set encoder->timescale (Hz) correctly
+//   * avifEncoderAddImage() ... [repeatedly; at least once]
+// - Still layered image:
+//   * Set encoder->extraLayerCount correctly
+//   * avifEncoderAddImage() ... [exactly encoder->extraLayerCount+1 times]
+// - Still layered grid:
+//   * Set encoder->extraLayerCount correctly
+//   * avifEncoderAddImageGrid() ... [exactly encoder->extraLayerCount+1 times]
 // * avifEncoderFinish()
 // * avifEncoderDestroy()
 //
 
-// durationInTimescales is ignored if AVIF_ADD_IMAGE_FLAG_SINGLE is set in addImageFlags.
+// durationInTimescales is ignored if AVIF_ADD_IMAGE_FLAG_SINGLE is set in addImageFlags,
+// or if we are encoding a layered image.
 AVIF_API avifResult avifEncoderAddImage(avifEncoder * encoder, const avifImage * image, uint64_t durationInTimescales, avifAddImageFlags addImageFlags);
 AVIF_API avifResult avifEncoderAddImageGrid(avifEncoder * encoder,
                                             uint32_t gridCols,
diff --git a/include/avif/internal.h b/include/avif/internal.h
index ae59f79..d8833e6 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -72,6 +72,11 @@
 void avifArrayPop(void * arrayStruct);
 void avifArrayDestroy(void * arrayStruct);
 
+void avifFractionSimplify(avifFraction * f);
+avifBool avifFractionCD(avifFraction * a, avifFraction * b);
+avifBool avifFractionAdd(avifFraction a, avifFraction b, avifFraction * result);
+avifBool avifFractionSub(avifFraction a, avifFraction b, avifFraction * result);
+
 void avifImageSetDefaults(avifImage * image);
 // Copies all fields that do not need to be freed/allocated from srcImage to dstImage.
 void avifImageCopyNoAlloc(avifImage * dstImage, const avifImage * srcImage);
@@ -288,6 +293,7 @@
     AVIF_ENCODER_CHANGE_TILE_COLS_LOG2 = (1u << 5),
     AVIF_ENCODER_CHANGE_QUANTIZER = (1u << 6),
     AVIF_ENCODER_CHANGE_QUANTIZER_ALPHA = (1u << 7),
+    AVIF_ENCODER_CHANGE_SCALING_MODE = (1u << 8),
 
     AVIF_ENCODER_CHANGE_CODEC_SPECIFIC = (1u << 31)
 } avifEncoderChange;
diff --git a/src/avif.c b/src/avif.c
index 62eba20..2aff76f 100644
--- a/src/avif.c
+++ b/src/avif.c
@@ -563,15 +563,9 @@
 // ---------------------------------------------------------------------------
 // avifCropRect
 
-typedef struct clapFraction
+static avifFraction calcCenter(int32_t dim)
 {
-    int32_t n;
-    int32_t d;
-} clapFraction;
-
-static clapFraction calcCenter(int32_t dim)
-{
-    clapFraction f;
+    avifFraction f;
     f.n = dim >> 1;
     f.d = 1;
     if ((dim % 2) != 0) {
@@ -581,95 +575,11 @@
     return f;
 }
 
-// |a| and |b| hold int32_t values. The int64_t type is used so that we can negate INT32_MIN without
-// overflowing int32_t.
-static int64_t calcGCD(int64_t a, int64_t b)
-{
-    if (a < 0) {
-        a *= -1;
-    }
-    if (b < 0) {
-        b *= -1;
-    }
-    while (b != 0) {
-        int64_t r = a % b;
-        a = b;
-        b = r;
-    }
-    return a;
-}
-
-static void clapFractionSimplify(clapFraction * f)
-{
-    int64_t gcd = calcGCD(f->n, f->d);
-    if (gcd > 1) {
-        f->n = (int32_t)(f->n / gcd);
-        f->d = (int32_t)(f->d / gcd);
-    }
-}
-
 static avifBool overflowsInt32(int64_t x)
 {
     return (x < INT32_MIN) || (x > INT32_MAX);
 }
 
-// Make the fractions have a common denominator
-static avifBool clapFractionCD(clapFraction * a, clapFraction * b)
-{
-    clapFractionSimplify(a);
-    clapFractionSimplify(b);
-    if (a->d != b->d) {
-        const int64_t ad = a->d;
-        const int64_t bd = b->d;
-        const int64_t anNew = a->n * bd;
-        const int64_t adNew = a->d * bd;
-        const int64_t bnNew = b->n * ad;
-        const int64_t bdNew = b->d * ad;
-        if (overflowsInt32(anNew) || overflowsInt32(adNew) || overflowsInt32(bnNew) || overflowsInt32(bdNew)) {
-            return AVIF_FALSE;
-        }
-        a->n = (int32_t)anNew;
-        a->d = (int32_t)adNew;
-        b->n = (int32_t)bnNew;
-        b->d = (int32_t)bdNew;
-    }
-    return AVIF_TRUE;
-}
-
-static avifBool clapFractionAdd(clapFraction a, clapFraction b, clapFraction * result)
-{
-    if (!clapFractionCD(&a, &b)) {
-        return AVIF_FALSE;
-    }
-
-    const int64_t resultN = (int64_t)a.n + b.n;
-    if (overflowsInt32(resultN)) {
-        return AVIF_FALSE;
-    }
-    result->n = (int32_t)resultN;
-    result->d = a.d;
-
-    clapFractionSimplify(result);
-    return AVIF_TRUE;
-}
-
-static avifBool clapFractionSub(clapFraction a, clapFraction b, clapFraction * result)
-{
-    if (!clapFractionCD(&a, &b)) {
-        return AVIF_FALSE;
-    }
-
-    const int64_t resultN = (int64_t)a.n - b.n;
-    if (overflowsInt32(resultN)) {
-        return AVIF_FALSE;
-    }
-    result->n = (int32_t)resultN;
-    result->d = a.d;
-
-    clapFractionSimplify(result);
-    return AVIF_TRUE;
-}
-
 static avifBool avifCropRectIsValid(const avifCropRect * cropRect, uint32_t imageW, uint32_t imageH, avifPixelFormat yuvFormat, avifDiagnostics * diag)
 
 {
@@ -755,32 +665,32 @@
         avifDiagnosticsPrintf(diag, "[Strict] image width %u or height %u is greater than INT32_MAX", imageW, imageH);
         return AVIF_FALSE;
     }
-    clapFraction uncroppedCenterX = calcCenter((int32_t)imageW);
-    clapFraction uncroppedCenterY = calcCenter((int32_t)imageH);
+    avifFraction uncroppedCenterX = calcCenter((int32_t)imageW);
+    avifFraction uncroppedCenterY = calcCenter((int32_t)imageH);
 
-    clapFraction horizOff;
+    avifFraction horizOff;
     horizOff.n = horizOffN;
     horizOff.d = horizOffD;
-    clapFraction croppedCenterX;
-    if (!clapFractionAdd(uncroppedCenterX, horizOff, &croppedCenterX)) {
+    avifFraction croppedCenterX;
+    if (!avifFractionAdd(uncroppedCenterX, horizOff, &croppedCenterX)) {
         avifDiagnosticsPrintf(diag, "[Strict] croppedCenterX overflowed");
         return AVIF_FALSE;
     }
 
-    clapFraction vertOff;
+    avifFraction vertOff;
     vertOff.n = vertOffN;
     vertOff.d = vertOffD;
-    clapFraction croppedCenterY;
-    if (!clapFractionAdd(uncroppedCenterY, vertOff, &croppedCenterY)) {
+    avifFraction croppedCenterY;
+    if (!avifFractionAdd(uncroppedCenterY, vertOff, &croppedCenterY)) {
         avifDiagnosticsPrintf(diag, "[Strict] croppedCenterY overflowed");
         return AVIF_FALSE;
     }
 
-    clapFraction halfW;
+    avifFraction halfW;
     halfW.n = clapW;
     halfW.d = 2;
-    clapFraction cropX;
-    if (!clapFractionSub(croppedCenterX, halfW, &cropX)) {
+    avifFraction cropX;
+    if (!avifFractionSub(croppedCenterX, halfW, &cropX)) {
         avifDiagnosticsPrintf(diag, "[Strict] cropX overflowed");
         return AVIF_FALSE;
     }
@@ -789,11 +699,11 @@
         return AVIF_FALSE;
     }
 
-    clapFraction halfH;
+    avifFraction halfH;
     halfH.n = clapH;
     halfH.d = 2;
-    clapFraction cropY;
-    if (!clapFractionSub(croppedCenterY, halfH, &cropY)) {
+    avifFraction cropY;
+    if (!avifFractionSub(croppedCenterY, halfH, &cropY)) {
         avifDiagnosticsPrintf(diag, "[Strict] cropY overflowed");
         return AVIF_FALSE;
     }
@@ -831,8 +741,8 @@
         avifDiagnosticsPrintf(diag, "[Strict] image width %u or height %u is greater than INT32_MAX", imageW, imageH);
         return AVIF_FALSE;
     }
-    clapFraction uncroppedCenterX = calcCenter((int32_t)imageW);
-    clapFraction uncroppedCenterY = calcCenter((int32_t)imageH);
+    avifFraction uncroppedCenterX = calcCenter((int32_t)imageW);
+    avifFraction uncroppedCenterY = calcCenter((int32_t)imageH);
 
     if ((cropRect->width > INT32_MAX) || (cropRect->height > INT32_MAX)) {
         avifDiagnosticsPrintf(diag,
@@ -841,14 +751,14 @@
                               cropRect->height);
         return AVIF_FALSE;
     }
-    clapFraction croppedCenterX = calcCenter((int32_t)cropRect->width);
+    avifFraction croppedCenterX = calcCenter((int32_t)cropRect->width);
     const int64_t croppedCenterXN = croppedCenterX.n + (int64_t)cropRect->x * croppedCenterX.d;
     if (overflowsInt32(croppedCenterXN)) {
         avifDiagnosticsPrintf(diag, "[Strict] croppedCenterX overflowed");
         return AVIF_FALSE;
     }
     croppedCenterX.n = (int32_t)croppedCenterXN;
-    clapFraction croppedCenterY = calcCenter((int32_t)cropRect->height);
+    avifFraction croppedCenterY = calcCenter((int32_t)cropRect->height);
     const int64_t croppedCenterYN = croppedCenterY.n + (int64_t)cropRect->y * croppedCenterY.d;
     if (overflowsInt32(croppedCenterYN)) {
         avifDiagnosticsPrintf(diag, "[Strict] croppedCenterY overflowed");
@@ -856,13 +766,13 @@
     }
     croppedCenterY.n = (int32_t)croppedCenterYN;
 
-    clapFraction horizOff;
-    if (!clapFractionSub(croppedCenterX, uncroppedCenterX, &horizOff)) {
+    avifFraction horizOff;
+    if (!avifFractionSub(croppedCenterX, uncroppedCenterX, &horizOff)) {
         avifDiagnosticsPrintf(diag, "[Strict] horizOff overflowed");
         return AVIF_FALSE;
     }
-    clapFraction vertOff;
-    if (!clapFractionSub(croppedCenterY, uncroppedCenterY, &vertOff)) {
+    avifFraction vertOff;
+    if (!avifFractionSub(croppedCenterY, uncroppedCenterY, &vertOff)) {
         avifDiagnosticsPrintf(diag, "[Strict] vertOff overflowed");
         return AVIF_FALSE;
     }
diff --git a/src/codec_aom.c b/src/codec_aom.c
index 586f1e5..dde3ad4 100644
--- a/src/codec_aom.c
+++ b/src/codec_aom.c
@@ -67,6 +67,7 @@
     // Whether 'tuning' (of the specified distortion metric) was set with an
     // avifEncoderSetCodecSpecificOption(encoder, "tune", value) call.
     avifBool tuningSet;
+    uint32_t currentLayer;
 #endif
 };
 
@@ -507,6 +508,33 @@
     return AVIF_TRUE;
 }
 
+struct aomScalingModeMapList
+{
+    avifFraction avifMode;
+    AOM_SCALING_MODE aomMode;
+};
+
+static const struct aomScalingModeMapList scalingModeMap[] = {
+    { { 1, 1 }, AOME_NORMAL },    { { 1, 2 }, AOME_ONETWO },    { { 1, 4 }, AOME_ONEFOUR },  { { 1, 8 }, AOME_ONEEIGHT },
+    { { 3, 4 }, AOME_THREEFOUR }, { { 3, 5 }, AOME_THREEFIVE }, { { 4, 5 }, AOME_FOURFIVE },
+};
+
+static const int scalingModeMapSize = sizeof(scalingModeMap) / sizeof(scalingModeMap[0]);
+
+static avifBool avifFindAOMScalingMode(const avifFraction * avifMode, AOM_SCALING_MODE * aomMode)
+{
+    avifFraction simplifiedFraction = *avifMode;
+    avifFractionSimplify(&simplifiedFraction);
+    for (int i = 0; i < scalingModeMapSize; ++i) {
+        if (scalingModeMap[i].avifMode.n == simplifiedFraction.n && scalingModeMap[i].avifMode.d == simplifiedFraction.d) {
+            *aomMode = scalingModeMap[i].aomMode;
+            return AVIF_TRUE;
+        }
+    }
+
+    return AVIF_FALSE;
+}
+
 static avifBool aomCodecEncodeFinish(avifCodec * codec, avifCodecEncodeOutput * output);
 
 static avifResult aomCodecEncodeImage(avifCodec * codec,
@@ -523,6 +551,11 @@
     struct aom_codec_enc_cfg * cfg = &codec->internal->cfg;
     avifBool quantizerUpdated = AVIF_FALSE;
 
+    // For encoder->scalingMode.horizontal and encoder->scalingMode.vertical to take effect in AOM
+    // encoder, config should be applied for each frame, so we don't care about changes on these
+    // two fields.
+    encoderChanges &= ~AVIF_ENCODER_CHANGE_SCALING_MODE;
+
     if (!codec->internal->encoderInitialized) {
         // Map encoder speed to AOM usage + CpuUsed:
         // Speed  0: GoodQuality CpuUsed 0
@@ -673,6 +706,11 @@
             // Tell libaom that all frames will be key frames.
             cfg->kf_max_dist = 0;
         }
+        if (encoder->extraLayerCount > 0) {
+            // For layered image, disable lagged encoding to always get output
+            // frame for each input frame.
+            cfg->g_lag_in_frames = 0;
+        }
         if (encoder->maxThreads > 1) {
             cfg->g_threads = encoder->maxThreads;
         }
@@ -752,6 +790,12 @@
         if (tileColsLog2 != 0) {
             aom_codec_control(&codec->internal->encoder, AV1E_SET_TILE_COLUMNS, tileColsLog2);
         }
+        if (encoder->extraLayerCount > 0) {
+            int layerCount = encoder->extraLayerCount + 1;
+            if (aom_codec_control(&codec->internal->encoder, AOME_SET_NUMBER_SPATIAL_LAYERS, layerCount) != AOM_CODEC_OK) {
+                return AVIF_RESULT_UNKNOWN_ERROR;
+            };
+        }
         if (aomCpuUsed != -1) {
             if (aom_codec_control(&codec->internal->encoder, AOME_SET_CPUUSED, aomCpuUsed) != AOM_CODEC_OK) {
                 return AVIF_RESULT_UNKNOWN_ERROR;
@@ -838,6 +882,30 @@
         }
     }
 
+    if (codec->internal->currentLayer > encoder->extraLayerCount) {
+        avifDiagnosticsPrintf(codec->diag,
+                              "Too many layers sent. Expected %u layers, but got %u layers.",
+                              encoder->extraLayerCount + 1,
+                              codec->internal->currentLayer + 1);
+        return AVIF_RESULT_INVALID_ARGUMENT;
+    }
+    if (encoder->extraLayerCount > 0) {
+        aom_codec_control(&codec->internal->encoder, AOME_SET_SPATIAL_LAYER_ID, codec->internal->currentLayer);
+        ++codec->internal->currentLayer;
+    }
+
+    aom_scaling_mode_t aomScalingMode;
+    if (!avifFindAOMScalingMode(&encoder->scalingMode.horizontal, &aomScalingMode.h_scaling_mode)) {
+        return AVIF_RESULT_NOT_IMPLEMENTED;
+    }
+    if (!avifFindAOMScalingMode(&encoder->scalingMode.vertical, &aomScalingMode.v_scaling_mode)) {
+        return AVIF_RESULT_NOT_IMPLEMENTED;
+    }
+    if ((aomScalingMode.h_scaling_mode != AOME_NORMAL) || (aomScalingMode.v_scaling_mode != AOME_NORMAL)) {
+        // AOME_SET_SCALEMODE only applies to next frame (layer), so we have to set it every time.
+        aom_codec_control(&codec->internal->encoder, AOME_SET_SCALEMODE, &aomScalingMode);
+    }
+
     aom_image_t aomImage;
     // We prefer to simply set the aomImage.planes[] pointers to the plane buffers in 'image'. When
     // doing this, we set aomImage.w equal to aomImage.d_w and aomImage.h equal to aomImage.d_h and
@@ -988,6 +1056,10 @@
     if (addImageFlags & AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME) {
         encodeFlags |= AOM_EFLAG_FORCE_KF;
     }
+    if (codec->internal->currentLayer > 0) {
+        encodeFlags |= AOM_EFLAG_NO_REF_GF | AOM_EFLAG_NO_REF_ARF | AOM_EFLAG_NO_REF_BWD | AOM_EFLAG_NO_REF_ARF2 |
+                       AOM_EFLAG_NO_UPD_GF | AOM_EFLAG_NO_UPD_ARF;
+    }
     aom_codec_err_t encodeErr = aom_codec_encode(&codec->internal->encoder, &aomImage, 0, 1, encodeFlags);
     avifFree(monoUVPlane);
     if (aomImageAllocated) {
@@ -1012,8 +1084,10 @@
         }
     }
 
-    if (addImageFlags & AVIF_ADD_IMAGE_FLAG_SINGLE) {
-        // Flush and clean up encoder resources early to save on overhead when encoding alpha or grid images
+    if ((addImageFlags & AVIF_ADD_IMAGE_FLAG_SINGLE) ||
+        ((encoder->extraLayerCount > 0) && (encoder->extraLayerCount + 1 == codec->internal->currentLayer))) {
+        // Flush and clean up encoder resources early to save on overhead when encoding alpha or grid images,
+        // as encoding is finished now. For layered image, encoding finishes when the last layer is encoded.
 
         if (!aomCodecEncodeFinish(codec, output)) {
             return AVIF_RESULT_UNKNOWN_ERROR;
diff --git a/src/codec_rav1e.c b/src/codec_rav1e.c
index 4b93b79..534dd5d 100644
--- a/src/codec_rav1e.c
+++ b/src/codec_rav1e.c
@@ -73,6 +73,11 @@
         return AVIF_RESULT_NOT_IMPLEMENTED;
     }
 
+    // rav1e does not support encoding layered image.
+    if (encoder->extraLayerCount > 0) {
+        return AVIF_RESULT_NOT_IMPLEMENTED;
+    }
+
     avifResult result = AVIF_RESULT_UNKNOWN_ERROR;
 
     RaConfig * rav1eConfig = NULL;
diff --git a/src/codec_svt.c b/src/codec_svt.c
index 158b2a4..4e13450 100644
--- a/src/codec_svt.c
+++ b/src/codec_svt.c
@@ -65,6 +65,11 @@
         }
     }
 
+    // SVT-AV1 does not support encoding layered image.
+    if (encoder->extraLayerCount > 0) {
+        return AVIF_RESULT_NOT_IMPLEMENTED;
+    }
+
     avifResult result = AVIF_RESULT_UNKNOWN_ERROR;
     EbColorFormat color_format = EB_YUV420;
     EbBufferHeaderType * input_buffer = NULL;
diff --git a/src/read.c b/src/read.c
index e5b569c..0fafd8f 100644
--- a/src/read.c
+++ b/src/read.c
@@ -37,8 +37,6 @@
 // can't be more than 4 unique tuples right now.
 #define MAX_IPMA_VERSION_AND_FLAGS_SEEN 4
 
-#define MAX_AV1_LAYER_COUNT 4
-
 // ---------------------------------------------------------------------------
 // Box data structures
 
@@ -577,7 +575,7 @@
         sample->itemID = item->id;
         sample->offset = 0;
         sample->size = sampleSize;
-        assert(lselProp->u.lsel.layerID < MAX_AV1_LAYER_COUNT);
+        assert(lselProp->u.lsel.layerID < AVIF_MAX_AV1_LAYER_COUNT);
         sample->spatialID = (uint8_t)lselProp->u.lsel.layerID;
         sample->sync = AVIF_TRUE;
     } else if (allowProgressive && item->progressive) {
@@ -1899,7 +1897,7 @@
 
     avifLayerSelectorProperty * lsel = &prop->u.lsel;
     AVIF_CHECK(avifROStreamReadU16(&s, &lsel->layerID));
-    if ((lsel->layerID != 0xFFFF) && (lsel->layerID >= MAX_AV1_LAYER_COUNT)) {
+    if ((lsel->layerID != 0xFFFF) && (lsel->layerID >= AVIF_MAX_AV1_LAYER_COUNT)) {
         avifDiagnosticsPrintf(diag, "Box[lsel] contains an unsupported layer [%u]", lsel->layerID);
         return AVIF_FALSE;
     }
diff --git a/src/utils.c b/src/utils.c
index 697cb0e..5fc1cb6 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -146,3 +146,92 @@
     }
     memset(arr, 0, sizeof(avifArrayInternal));
 }
+
+// |a| and |b| hold int32_t values. The int64_t type is used so that we can negate INT32_MIN without
+// overflowing int32_t.
+static int64_t calcGCD(int64_t a, int64_t b)
+{
+    if (a < 0) {
+        a *= -1;
+    }
+    if (b < 0) {
+        b *= -1;
+    }
+    while (b != 0) {
+        int64_t r = a % b;
+        a = b;
+        b = r;
+    }
+    return a;
+}
+
+void avifFractionSimplify(avifFraction * f)
+{
+    int64_t gcd = calcGCD(f->n, f->d);
+    if (gcd > 1) {
+        f->n = (int32_t)(f->n / gcd);
+        f->d = (int32_t)(f->d / gcd);
+    }
+}
+
+static avifBool overflowsInt32(int64_t x)
+{
+    return (x < INT32_MIN) || (x > INT32_MAX);
+}
+
+// Make the fractions have a common denominator
+avifBool avifFractionCD(avifFraction * a, avifFraction * b)
+{
+    avifFractionSimplify(a);
+    avifFractionSimplify(b);
+    if (a->d != b->d) {
+        const int64_t ad = a->d;
+        const int64_t bd = b->d;
+        const int64_t anNew = a->n * bd;
+        const int64_t adNew = a->d * bd;
+        const int64_t bnNew = b->n * ad;
+        const int64_t bdNew = b->d * ad;
+        if (overflowsInt32(anNew) || overflowsInt32(adNew) || overflowsInt32(bnNew) || overflowsInt32(bdNew)) {
+            return AVIF_FALSE;
+        }
+        a->n = (int32_t)anNew;
+        a->d = (int32_t)adNew;
+        b->n = (int32_t)bnNew;
+        b->d = (int32_t)bdNew;
+    }
+    return AVIF_TRUE;
+}
+
+avifBool avifFractionAdd(avifFraction a, avifFraction b, avifFraction * result)
+{
+    if (!avifFractionCD(&a, &b)) {
+        return AVIF_FALSE;
+    }
+
+    const int64_t resultN = (int64_t)a.n + b.n;
+    if (overflowsInt32(resultN)) {
+        return AVIF_FALSE;
+    }
+    result->n = (int32_t)resultN;
+    result->d = a.d;
+
+    avifFractionSimplify(result);
+    return AVIF_TRUE;
+}
+
+avifBool avifFractionSub(avifFraction a, avifFraction b, avifFraction * result)
+{
+    if (!avifFractionCD(&a, &b)) {
+        return AVIF_FALSE;
+    }
+
+    const int64_t resultN = (int64_t)a.n - b.n;
+    if (overflowsInt32(resultN)) {
+        return AVIF_FALSE;
+    }
+    result->n = (int32_t)resultN;
+    result->d = a.d;
+
+    avifFractionSimplify(result);
+    return AVIF_TRUE;
+}
diff --git a/src/write.c b/src/write.c
index ff9117c..649183e 100644
--- a/src/write.c
+++ b/src/write.c
@@ -181,6 +181,8 @@
     uint32_t gridWidth;
     uint32_t gridHeight;
 
+    uint32_t extraLayerCount; // if non-zero (legal range [1-(AVIF_MAX_AV1_LAYER_COUNT-1)]), this is a layered AV1 image
+
     uint16_t dimgFromID; // if non-zero, make an iref from dimgFromID -> this id
 
     struct ipmaArray ipma;
@@ -188,6 +190,13 @@
 AVIF_ARRAY_DECLARE(avifEncoderItemArray, avifEncoderItem, item);
 
 // ---------------------------------------------------------------------------
+// avifEncoderItemReference
+
+// pointer to one "item" interested in
+typedef avifEncoderItem * avifEncoderItemReference;
+AVIF_ARRAY_DECLARE(avifEncoderItemReferenceArray, avifEncoderItemReference, ref);
+
+// ---------------------------------------------------------------------------
 // avifEncoderFrame
 
 typedef struct avifEncoderFrame
@@ -383,6 +392,8 @@
 
 // ---------------------------------------------------------------------------
 
+static const avifScalingMode noScaling = { { 1, 1 }, { 1, 1 } };
+
 avifEncoder * avifEncoderCreate(void)
 {
     avifEncoder * encoder = (avifEncoder *)avifAlloc(sizeof(avifEncoder));
@@ -401,6 +412,7 @@
     encoder->tileRowsLog2 = 0;
     encoder->tileColsLog2 = 0;
     encoder->autoTiling = AVIF_FALSE;
+    encoder->scalingMode = noScaling;
     encoder->data = avifEncoderDataCreate();
     encoder->csOptions = avifCodecSpecificOptionsCreate();
     return encoder;
@@ -431,6 +443,7 @@
     lastEncoder->keyframeInterval = encoder->keyframeInterval;
     lastEncoder->timescale = encoder->timescale;
     lastEncoder->repetitionCount = encoder->repetitionCount;
+    lastEncoder->extraLayerCount = encoder->extraLayerCount;
     lastEncoder->minQuantizer = encoder->minQuantizer;
     lastEncoder->maxQuantizer = encoder->maxQuantizer;
     lastEncoder->minQuantizerAlpha = encoder->minQuantizerAlpha;
@@ -439,6 +452,7 @@
     encoder->data->lastQuantizerAlpha = encoder->data->quantizerAlpha;
     encoder->data->lastTileRowsLog2 = encoder->data->tileRowsLog2;
     encoder->data->lastTileColsLog2 = encoder->data->tileColsLog2;
+    lastEncoder->scalingMode = encoder->scalingMode;
 }
 
 // This function detects changes made on avifEncoder. It returns true on success (i.e., if every
@@ -456,7 +470,8 @@
 
     if ((lastEncoder->codecChoice != encoder->codecChoice) || (lastEncoder->maxThreads != encoder->maxThreads) ||
         (lastEncoder->speed != encoder->speed) || (lastEncoder->keyframeInterval != encoder->keyframeInterval) ||
-        (lastEncoder->timescale != encoder->timescale) || (lastEncoder->repetitionCount != encoder->repetitionCount)) {
+        (lastEncoder->timescale != encoder->timescale) || (lastEncoder->repetitionCount != encoder->repetitionCount) ||
+        (lastEncoder->extraLayerCount != encoder->extraLayerCount)) {
         return AVIF_FALSE;
     }
 
@@ -484,6 +499,9 @@
     if (encoder->data->lastTileColsLog2 != encoder->data->tileColsLog2) {
         *encoderChanges |= AVIF_ENCODER_CHANGE_TILE_COLS_LOG2;
     }
+    if (memcmp(&lastEncoder->scalingMode, &encoder->scalingMode, sizeof(avifScalingMode)) != 0) {
+        *encoderChanges |= AVIF_ENCODER_CHANGE_SCALING_MODE;
+    }
     if (encoder->csOptions->count > 0) {
         *encoderChanges |= AVIF_ENCODER_CHANGE_CODEC_SPECIFIC;
     }
@@ -858,6 +876,10 @@
         return AVIF_RESULT_NO_CODEC_AVAILABLE;
     }
 
+    if (encoder->extraLayerCount >= AVIF_MAX_AV1_LAYER_COUNT) {
+        return AVIF_RESULT_INVALID_ARGUMENT;
+    }
+
     // -----------------------------------------------------------------------
     // Validate images
 
@@ -940,6 +962,11 @@
     if (addImageFlags & AVIF_ADD_IMAGE_FLAG_SINGLE) {
         encoder->data->singleImage = AVIF_TRUE;
 
+        if (encoder->extraLayerCount > 0) {
+            // AVIF_ADD_IMAGE_FLAG_SINGLE may not be set for layered image.
+            return AVIF_RESULT_INVALID_ARGUMENT;
+        }
+
         if (encoder->data->items.count > 0) {
             // AVIF_ADD_IMAGE_FLAG_SINGLE may only be set on the first and only image.
             return AVIF_RESULT_INVALID_ARGUMENT;
@@ -1009,6 +1036,7 @@
             }
             item->codec->csOptions = encoder->csOptions;
             item->codec->diag = &encoder->diag;
+            item->extraLayerCount = encoder->extraLayerCount;
 
             if (cellCount > 1) {
                 item->dimgFromID = gridColorID;
@@ -1070,6 +1098,7 @@
                 item->codec->csOptions = encoder->csOptions;
                 item->codec->diag = &encoder->diag;
                 item->alpha = AVIF_TRUE;
+                item->extraLayerCount = encoder->extraLayerCount;
 
                 if (cellCount > 1) {
                     item->dimgFromID = gridAlphaID;
@@ -1105,7 +1134,7 @@
             }
         }
     } else {
-        // Another frame in an image sequence
+        // Another frame in an image sequence, or layer in a layered image
 
         const avifImage * imageMetadata = encoder->data->imageMetadata;
         // If the first image in the sequence had an alpha plane (even if fully opaque), all
@@ -1203,7 +1232,10 @@
     if ((gridCols == 0) || (gridCols > 256) || (gridRows == 0) || (gridRows > 256)) {
         return AVIF_RESULT_INVALID_IMAGE_GRID;
     }
-    return avifEncoderAddImageInternal(encoder, gridCols, gridRows, cellImages, 1, addImageFlags | AVIF_ADD_IMAGE_FLAG_SINGLE); // image grids cannot be image sequences
+    if (encoder->extraLayerCount == 0) {
+        addImageFlags |= AVIF_ADD_IMAGE_FLAG_SINGLE; // image grids cannot be image sequences
+    }
+    return avifEncoderAddImageInternal(encoder, gridCols, gridRows, cellImages, 1, addImageFlags);
 }
 
 static size_t avifEncoderFindExistingChunk(avifRWStream * s, size_t mdatStartOffset, const uint8_t * data, size_t size)
@@ -1242,6 +1274,15 @@
             if (item->encodeOutput->samples.count != encoder->data->frames.count) {
                 return item->alpha ? AVIF_RESULT_ENCODE_ALPHA_FAILED : AVIF_RESULT_ENCODE_COLOR_FAILED;
             }
+
+            if ((item->extraLayerCount > 0) && (item->encodeOutput->samples.count != item->extraLayerCount + 1)) {
+                // Check whether user has sent enough frames to encoder.
+                avifDiagnosticsPrintf(&encoder->diag,
+                                      "Expected %u frames given to avifEncoderAddImage() to encode this layered image according to extraLayerCount, but got %u frames.",
+                                      item->extraLayerCount + 1,
+                                      item->encodeOutput->samples.count);
+                return AVIF_RESULT_INVALID_ARGUMENT;
+            }
         }
     }
 
@@ -1277,8 +1318,11 @@
     // -----------------------------------------------------------------------
     // Write ftyp
 
+    // Layered sequence is not supported for now.
+    const avifBool isSequence = (encoder->extraLayerCount == 0) && (encoder->data->frames.count > 1);
+
     const char * majorBrand = "avif";
-    if (encoder->data->frames.count > 1) {
+    if (isSequence) {
         majorBrand = "avis";
     }
 
@@ -1286,7 +1330,7 @@
     avifRWStreamWriteChars(&s, majorBrand, 4);                             // unsigned int(32) major_brand;
     avifRWStreamWriteU32(&s, 0);                                           // unsigned int(32) minor_version;
     avifRWStreamWriteChars(&s, "avif", 4);                                 // unsigned int(32) compatible_brands[];
-    if (encoder->data->frames.count > 1) {                                 //
+    if (isSequence) {                                                      //
         avifRWStreamWriteChars(&s, "avis", 4);                             // ... compatible_brands[]
         avifRWStreamWriteChars(&s, "msf1", 4);                             // ... compatible_brands[]
         avifRWStreamWriteChars(&s, "iso8", 4);                             // ... compatible_brands[]
@@ -1339,6 +1383,20 @@
 
     for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) {
         avifEncoderItem * item = &encoder->data->items.item[itemIndex];
+        avifRWStreamWriteU16(&s, item->id); // unsigned int(16) item_ID;
+        avifRWStreamWriteU16(&s, 0);        // unsigned int(16) data_reference_index;
+
+        // Layered Image, write location for all samples
+        if (item->extraLayerCount > 0) {
+            uint32_t layerCount = item->extraLayerCount + 1;
+            avifRWStreamWriteU16(&s, (uint16_t)layerCount); // unsigned int(16) extent_count;
+            for (uint32_t i = 0; i < layerCount; ++i) {
+                avifEncoderItemAddMdatFixup(item, &s);
+                avifRWStreamWriteU32(&s, 0 /* set later */); // unsigned int(offset_size*8) extent_offset;
+                avifRWStreamWriteU32(&s, (uint32_t)item->encodeOutput->samples.sample[i].data.size); // unsigned int(length_size*8) extent_length;
+            }
+            continue;
+        }
 
         uint32_t contentSize = (uint32_t)item->metadataPayload.size;
         if (item->encodeOutput->samples.count > 0) {
@@ -1353,8 +1411,6 @@
             contentSize = (uint32_t)item->encodeOutput->samples.sample[0].data.size;
         }
 
-        avifRWStreamWriteU16(&s, item->id);              // unsigned int(16) item_ID;
-        avifRWStreamWriteU16(&s, 0);                     // unsigned int(16) data_reference_index;
         avifRWStreamWriteU16(&s, 1);                     // unsigned int(16) extent_count;
         avifEncoderItemAddMdatFixup(item, &s);           //
         avifRWStreamWriteU32(&s, 0 /* set later */);     // unsigned int(offset_size*8) extent_offset;
@@ -1449,15 +1505,16 @@
             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.
+        if (item->dimgFromID && (item->extraLayerCount == 0)) {
+            // All image cells from a grid should share the exact same properties unless they are
+            // layered image which have different al1x, 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) {
+                if ((item->dimgFromID == dedupItem->dimgFromID) && (dedupItem->extraLayerCount == 0)) {
                     // We've already written dedup's items out. Steal their ipma indices and move on!
                     item->ipma = dedupItem->ipma;
                     foundPreviousCell = AVIF_TRUE;
@@ -1514,6 +1571,38 @@
 
             avifEncoderWriteColorProperties(&s, imageMetadata, &item->ipma, dedup);
         }
+
+        if (item->extraLayerCount > 0) {
+            // Layered Image Indexing Property
+
+            avifItemPropertyDedupStart(dedup);
+            avifBoxMarker a1lx = avifRWStreamWriteBox(&dedup->s, "a1lx", AVIF_BOX_SIZE_TBD);
+            uint32_t layerSize[AVIF_MAX_AV1_LAYER_COUNT - 1] = { 0 };
+            avifBool largeSize = AVIF_FALSE;
+
+            for (uint32_t validLayer = 0; validLayer < item->extraLayerCount; ++validLayer) {
+                uint32_t size = (uint32_t)item->encodeOutput->samples.sample[validLayer].data.size;
+                layerSize[validLayer] = size;
+                if (size > 0xffff) {
+                    largeSize = AVIF_TRUE;
+                }
+            }
+
+            avifRWStreamWriteU8(&dedup->s, (uint8_t)largeSize); // unsigned int(7) reserved = 0;
+                                                                // unsigned int(1) large_size;
+
+            // FieldLength = (large_size + 1) * 16;
+            // unsigned int(FieldLength) layer_size[3];
+            for (uint32_t layer = 0; layer < AVIF_MAX_AV1_LAYER_COUNT - 1; ++layer) {
+                if (largeSize) {
+                    avifRWStreamWriteU32(&dedup->s, layerSize[layer]);
+                } else {
+                    avifRWStreamWriteU16(&dedup->s, (uint16_t)layerSize[layer]);
+                }
+            }
+            avifRWStreamFinishBox(&dedup->s, a1lx);
+            ipmaPush(&item->ipma, avifItemPropertyDedupFinish(dedup, &s), AVIF_FALSE);
+        }
     }
     avifRWStreamFinishBox(&s, ipco);
     avifItemPropertyDedupDestroy(dedup);
@@ -1559,7 +1648,7 @@
     // -----------------------------------------------------------------------
     // Write tracks (if an image sequence)
 
-    if (encoder->data->frames.count > 1) {
+    if (isSequence) {
         static const uint8_t unityMatrix[9][4] = {
             /* clang-format off */
             { 0x00, 0x01, 0x00, 0x00 },
@@ -1827,6 +1916,13 @@
     encoder->ioStats.colorOBUSize = 0;
     encoder->ioStats.alphaOBUSize = 0;
 
+    avifEncoderItemReferenceArray layeredColorItems;
+    avifEncoderItemReferenceArray layeredAlphaItems;
+    if (!avifArrayCreate(&layeredColorItems, sizeof(avifEncoderItemReference), 1) ||
+        !avifArrayCreate(&layeredAlphaItems, sizeof(avifEncoderItemReference), 1)) {
+        return AVIF_RESULT_OUT_OF_MEMORY;
+    }
+
     avifBoxMarker mdat = avifRWStreamWriteBox(&s, "mdat", AVIF_BOX_SIZE_TBD);
     const size_t mdatStartOffset = avifRWStreamOffset(&s);
     for (uint32_t itemPasses = 0; itemPasses < 3; ++itemPasses) {
@@ -1861,6 +1957,17 @@
                 continue;
             }
 
+            if ((encoder->extraLayerCount > 0) && (item->encodeOutput->samples.count > 0)) {
+                // Interleave - Pick out AV1 items and interleave them later.
+                // We always interleave all AV1 items for layered images.
+                assert(item->encodeOutput->samples.count == item->mdatFixups.count);
+
+                avifEncoderItemReference * ref = item->alpha ? avifArrayPushPtr(&layeredAlphaItems)
+                                                             : avifArrayPushPtr(&layeredColorItems);
+                *ref = item;
+                continue;
+            }
+
             size_t chunkOffset = 0;
 
             // Deduplication - See if an identical chunk to this has already been written
@@ -1899,6 +2006,61 @@
             }
         }
     }
+
+    uint32_t layeredItemCount = AVIF_MAX(layeredColorItems.count, layeredAlphaItems.count);
+    if (layeredItemCount > 0) {
+        // Interleave samples of all AV1 items.
+        // We first write the first layer of all items,
+        // in which we write first layer of each cell,
+        // in which we write alpha first and then color.
+        avifBool hasMoreSample;
+        uint32_t layerIndex = 0;
+        do {
+            hasMoreSample = AVIF_FALSE;
+            for (uint32_t itemIndex = 0; itemIndex < layeredItemCount; ++itemIndex) {
+                for (int samplePass = 0; samplePass < 2; ++samplePass) {
+                    // Alpha coming before color
+                    avifEncoderItemReferenceArray * currentItems = (samplePass == 0) ? &layeredAlphaItems : &layeredColorItems;
+                    if (itemIndex >= currentItems->count) {
+                        continue;
+                    }
+
+                    // TODO: Offer the ability for a user to specify which grid cell should be written first.
+                    avifEncoderItem * item = currentItems->ref[itemIndex];
+                    if (item->encodeOutput->samples.count <= layerIndex) {
+                        // We've already written all samples of this item
+                        continue;
+                    } else if (item->encodeOutput->samples.count > layerIndex + 1) {
+                        hasMoreSample = AVIF_TRUE;
+                    }
+                    avifRWData * data = &item->encodeOutput->samples.sample[layerIndex].data;
+                    size_t chunkOffset = avifEncoderFindExistingChunk(&s, mdatStartOffset, data->data, data->size);
+                    if (!chunkOffset) {
+                        // We've never seen this chunk before; write it out
+                        chunkOffset = avifRWStreamOffset(&s);
+                        avifRWStreamWrite(&s, data->data, data->size);
+                        if (samplePass == 0) {
+                            encoder->ioStats.alphaOBUSize += data->size;
+                        } else {
+                            encoder->ioStats.colorOBUSize += data->size;
+                        }
+                    }
+
+                    size_t prevOffset = avifRWStreamOffset(&s);
+                    avifRWStreamSetOffset(&s, item->mdatFixups.fixup[layerIndex].offset);
+                    avifRWStreamWriteU32(&s, (uint32_t)chunkOffset);
+                    avifRWStreamSetOffset(&s, prevOffset);
+                }
+            }
+            ++layerIndex;
+        } while (hasMoreSample);
+
+        assert(layerIndex <= AVIF_MAX_AV1_LAYER_COUNT);
+    }
+
+    avifArrayDestroy(&layeredColorItems);
+    avifArrayDestroy(&layeredAlphaItems);
+
     avifRWStreamFinishBox(&s, mdat);
 
     // -----------------------------------------------------------------------
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 247ed7a..f6101bc 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -118,6 +118,11 @@
     target_include_directories(avifpng16bittest PRIVATE ${GTEST_INCLUDE_DIRS})
     add_test(NAME avifpng16bittest COMMAND avifpng16bittest ${CMAKE_CURRENT_SOURCE_DIR}/data/)
 
+    add_executable(avifprogressivetest gtest/avifprogressivetest.cc)
+    target_link_libraries(avifprogressivetest aviftest_helpers ${GTEST_BOTH_LIBRARIES})
+    target_include_directories(avifprogressivetest PRIVATE ${GTEST_INCLUDE_DIRS})
+    add_test(NAME avifprogressivetest COMMAND avifprogressivetest)
+
     add_executable(avifreadimagetest gtest/avifreadimagetest.cc)
     target_link_libraries(avifreadimagetest aviftest_helpers ${GTEST_LIBRARIES})
     target_include_directories(avifreadimagetest PRIVATE ${GTEST_INCLUDE_DIRS})
diff --git a/tests/gtest/avifprogressivetest.cc b/tests/gtest/avifprogressivetest.cc
new file mode 100644
index 0000000..16d90e3
--- /dev/null
+++ b/tests/gtest/avifprogressivetest.cc
@@ -0,0 +1,167 @@
+// Copyright 2022 Yuan Tong. All rights reserved.
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include "avif/avif.h"
+#include "aviftest_helpers.h"
+#include "gtest/gtest.h"
+
+namespace libavif {
+namespace {
+
+class ProgressiveTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    if (avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE) ==
+        nullptr) {
+      GTEST_SKIP() << "ProgressiveTest requires the AOM encoder.";
+    }
+
+    ASSERT_NE(encoder_, nullptr);
+    encoder_->codecChoice = AVIF_CODEC_CHOICE_AOM;
+    // The fastest speed that uses AOM_USAGE_GOOD_QUALITY.
+    encoder_->speed = 6;
+
+    ASSERT_NE(decoder_, nullptr);
+    decoder_->allowProgressive = true;
+
+    ASSERT_NE(image_, nullptr);
+    testutil::FillImageGradient(image_.get());
+  }
+
+  void TestDecode(uint32_t expect_width, uint32_t expect_height) {
+    ASSERT_EQ(avifDecoderSetIOMemory(decoder_.get(), encoded_avif_.data,
+                                     encoded_avif_.size),
+              AVIF_RESULT_OK);
+    ASSERT_EQ(avifDecoderParse(decoder_.get()), AVIF_RESULT_OK);
+    ASSERT_EQ(decoder_->progressiveState, AVIF_PROGRESSIVE_STATE_ACTIVE);
+    ASSERT_EQ(static_cast<uint32_t>(decoder_->imageCount),
+              encoder_->extraLayerCount + 1);
+
+    for (uint32_t layer = 0; layer < encoder_->extraLayerCount + 1; ++layer) {
+      ASSERT_EQ(avifDecoderNextImage(decoder_.get()), AVIF_RESULT_OK);
+      // libavif scales frame automatically.
+      ASSERT_EQ(decoder_->image->width, expect_width);
+      ASSERT_EQ(decoder_->image->height, expect_height);
+      // TODO(wtc): Check avifDecoderNthImageMaxExtent().
+    }
+
+    // TODO(wtc): Check decoder_->image and image_ are similar, and better
+    // quality layer is more similar.
+  }
+
+  static constexpr uint32_t kImageSize = 256;
+
+  testutil::AvifEncoderPtr encoder_{avifEncoderCreate(), avifEncoderDestroy};
+  testutil::AvifDecoderPtr decoder_{avifDecoderCreate(), avifDecoderDestroy};
+
+  testutil::AvifImagePtr image_ =
+      testutil::CreateImage(kImageSize, kImageSize, 8, AVIF_PIXEL_FORMAT_YUV444,
+                            AVIF_PLANES_YUV, AVIF_RANGE_FULL);
+
+  testutil::AvifRwData encoded_avif_;
+};
+
+TEST_F(ProgressiveTest, QualityChange) {
+  encoder_->extraLayerCount = 1;
+  encoder_->minQuantizer = 50;
+  encoder_->maxQuantizer = 50;
+
+  ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1,
+                                AVIF_ADD_IMAGE_FLAG_NONE),
+            AVIF_RESULT_OK);
+
+  encoder_->minQuantizer = 0;
+  encoder_->maxQuantizer = 0;
+  ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1,
+                                AVIF_ADD_IMAGE_FLAG_NONE),
+            AVIF_RESULT_OK);
+
+  ASSERT_EQ(avifEncoderFinish(encoder_.get(), &encoded_avif_), AVIF_RESULT_OK);
+
+  TestDecode(kImageSize, kImageSize);
+}
+
+TEST_F(ProgressiveTest, DimensionChange) {
+  if (avifLibYUVVersion() == 0) {
+    GTEST_SKIP() << "libyuv not available, skip test.";
+  }
+
+  encoder_->extraLayerCount = 1;
+  encoder_->minQuantizer = 0;
+  encoder_->maxQuantizer = 0;
+  encoder_->scalingMode = {{1, 2}, {1, 2}};
+
+  ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1,
+                                AVIF_ADD_IMAGE_FLAG_NONE),
+            AVIF_RESULT_OK);
+
+  encoder_->scalingMode = {{1, 1}, {1, 1}};
+  ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1,
+                                AVIF_ADD_IMAGE_FLAG_NONE),
+            AVIF_RESULT_OK);
+
+  ASSERT_EQ(avifEncoderFinish(encoder_.get(), &encoded_avif_), AVIF_RESULT_OK);
+
+  TestDecode(kImageSize, kImageSize);
+}
+
+TEST_F(ProgressiveTest, LayeredGrid) {
+  encoder_->extraLayerCount = 1;
+  encoder_->minQuantizer = 50;
+  encoder_->maxQuantizer = 50;
+
+  avifImage* image_grid[2] = {image_.get(), image_.get()};
+  ASSERT_EQ(avifEncoderAddImageGrid(encoder_.get(), 2, 1, image_grid,
+                                    AVIF_ADD_IMAGE_FLAG_NONE),
+            AVIF_RESULT_OK);
+
+  encoder_->minQuantizer = 0;
+  encoder_->maxQuantizer = 0;
+  ASSERT_EQ(avifEncoderAddImageGrid(encoder_.get(), 2, 1, image_grid,
+                                    AVIF_ADD_IMAGE_FLAG_NONE),
+            AVIF_RESULT_OK);
+
+  ASSERT_EQ(avifEncoderFinish(encoder_.get(), &encoded_avif_), AVIF_RESULT_OK);
+
+  TestDecode(2 * kImageSize, kImageSize);
+}
+
+TEST_F(ProgressiveTest, SameLayers) {
+  encoder_->extraLayerCount = 3;
+  for (uint32_t layer = 0; layer < encoder_->extraLayerCount + 1; ++layer) {
+    ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1,
+                                  AVIF_ADD_IMAGE_FLAG_NONE),
+              AVIF_RESULT_OK);
+  }
+  ASSERT_EQ(avifEncoderFinish(encoder_.get(), &encoded_avif_), AVIF_RESULT_OK);
+
+  TestDecode(kImageSize, kImageSize);
+}
+
+TEST_F(ProgressiveTest, TooManyLayers) {
+  encoder_->extraLayerCount = 1;
+
+  ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1,
+                                AVIF_ADD_IMAGE_FLAG_NONE),
+            AVIF_RESULT_OK);
+  ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1,
+                                AVIF_ADD_IMAGE_FLAG_NONE),
+            AVIF_RESULT_OK);
+  ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1,
+                                AVIF_ADD_IMAGE_FLAG_NONE),
+            AVIF_RESULT_INVALID_ARGUMENT);
+}
+
+TEST_F(ProgressiveTest, TooFewLayers) {
+  encoder_->extraLayerCount = 1;
+
+  ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1,
+                                AVIF_ADD_IMAGE_FLAG_NONE),
+            AVIF_RESULT_OK);
+
+  ASSERT_EQ(avifEncoderFinish(encoder_.get(), &encoded_avif_),
+            AVIF_RESULT_INVALID_ARGUMENT);
+}
+
+}  // namespace
+}  // namespace libavif