avifenc: Add an experimental --progressive option

If --progressive is specified, avifenc will encode a progressive,
layered image. The current implementation is hardcoded to use two
layers.

The --progressive option is marked as experimental because it doesn't
work properly even though it produces a correctly encoded layered image.

Refactor avifEncodeImagesFixedQuality(). Move the big while loop in
avifEncodeImagesFixedQuality() to a new avifEncodeRestOfImageSequence()
function. Then change avifEncodeImagesFixedQuality() to call either
avifEncodeRestOfImageSequence() or avifEncodeRestOfLayeredImage()
depending on whether we are encoding an image sequence or a progressive,
layered image.
diff --git a/apps/avifenc.c b/apps/avifenc.c
index 72d9ad1..a4dd41f 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -138,6 +138,7 @@
            AVIF_QUANTIZER_WORST_QUALITY,
            AVIF_QUANTIZER_LOSSLESS);
     printf("    --target-size S                   : Set target file size in bytes (up to 7 times slower)\n");
+    printf("    --progressive                     : EXPERIMENTAL: Encode a progressive image\n");
     printf("    --                                : Signals the end of options. Everything after this is interpreted as file names.\n");
     printf("\n");
     if (avifCodecName(AVIF_CODEC_CHOICE_AOM, 0)) {
@@ -624,6 +625,7 @@
     int tileRowsLog2;
     int tileColsLog2;
     avifBool autoTiling;
+    avifBool progressive;
     int speed;
 
     int paspCount;
@@ -653,6 +655,147 @@
     avifCodecSpecificOptions codecSpecificOptions;
 } avifSettings;
 
+static avifBool avifEncodeRestOfImageSequence(avifEncoder * encoder,
+                                              const avifSettings * settings,
+                                              avifInput * input,
+                                              int imageIndex,
+                                              const avifImage * firstImage)
+{
+    avifBool success = AVIF_FALSE;
+    avifImage * nextImage = NULL;
+
+    const avifInputFile * nextFile;
+    while ((nextFile = avifInputGetFile(input, imageIndex)) != NULL) {
+        uint64_t nextDurationInTimescales = nextFile->duration ? nextFile->duration : settings->outputTiming.duration;
+
+        printf(" * Encoding frame %d [%" PRIu64 "/%" PRIu64 " ts]: %s\n",
+               imageIndex,
+               nextDurationInTimescales,
+               settings->outputTiming.timescale,
+               nextFile->filename);
+
+        if (nextImage) {
+            avifImageDestroy(nextImage);
+        }
+        nextImage = avifImageCreateEmpty();
+        if (!nextImage) {
+            fprintf(stderr, "ERROR: Out of memory\n");
+            goto cleanup;
+        }
+        nextImage->colorPrimaries = firstImage->colorPrimaries;
+        nextImage->transferCharacteristics = firstImage->transferCharacteristics;
+        nextImage->matrixCoefficients = firstImage->matrixCoefficients;
+        nextImage->yuvRange = firstImage->yuvRange;
+        nextImage->alphaPremultiplied = firstImage->alphaPremultiplied;
+
+        // Ignore ICC, Exif and XMP because only the metadata of the first frame is taken into
+        // account by the libavif API.
+        if (!avifInputReadImage(input,
+                                imageIndex,
+                                /*ignoreICC=*/AVIF_TRUE,
+                                /*ignoreExif=*/AVIF_TRUE,
+                                /*ignoreXMP=*/AVIF_TRUE,
+                                nextImage,
+                                /*outDepth=*/NULL,
+                                /*sourceIsRGB=*/NULL,
+                                /*sourceTiming=*/NULL,
+                                settings->chromaDownsampling)) {
+            goto cleanup;
+        }
+
+        // Verify that this frame's properties matches the first frame's properties
+        if ((firstImage->width != nextImage->width) || (firstImage->height != nextImage->height)) {
+            fprintf(stderr,
+                    "ERROR: Image sequence dimensions mismatch, [%ux%u] vs [%ux%u]: %s\n",
+                    firstImage->width,
+                    firstImage->height,
+                    nextImage->width,
+                    nextImage->height,
+                    nextFile->filename);
+            goto cleanup;
+        }
+        if (firstImage->depth != nextImage->depth) {
+            fprintf(stderr,
+                    "ERROR: Image sequence depth mismatch, [%u] vs [%u]: %s\n",
+                    firstImage->depth,
+                    nextImage->depth,
+                    nextFile->filename);
+            goto cleanup;
+        }
+        if ((firstImage->colorPrimaries != nextImage->colorPrimaries) ||
+            (firstImage->transferCharacteristics != nextImage->transferCharacteristics) ||
+            (firstImage->matrixCoefficients != nextImage->matrixCoefficients)) {
+            fprintf(stderr,
+                    "ERROR: Image sequence CICP mismatch, [%u/%u/%u] vs [%u/%u/%u]: %s\n",
+                    firstImage->colorPrimaries,
+                    firstImage->matrixCoefficients,
+                    firstImage->transferCharacteristics,
+                    nextImage->colorPrimaries,
+                    nextImage->transferCharacteristics,
+                    nextImage->matrixCoefficients,
+                    nextFile->filename);
+            goto cleanup;
+        }
+        if (firstImage->yuvRange != nextImage->yuvRange) {
+            fprintf(stderr,
+                    "ERROR: Image sequence range mismatch, [%s] vs [%s]: %s\n",
+                    (firstImage->yuvRange == AVIF_RANGE_FULL) ? "Full" : "Limited",
+                    (nextImage->yuvRange == AVIF_RANGE_FULL) ? "Full" : "Limited",
+                    nextFile->filename);
+            goto cleanup;
+        }
+
+        const avifResult nextImageResult = avifEncoderAddImage(encoder, nextImage, nextDurationInTimescales, AVIF_ADD_IMAGE_FLAG_NONE);
+        if (nextImageResult != AVIF_RESULT_OK) {
+            fprintf(stderr, "ERROR: Failed to encode image: %s\n", avifResultToString(nextImageResult));
+            goto cleanup;
+        }
+        ++imageIndex;
+    }
+    success = AVIF_TRUE;
+
+cleanup:
+    if (nextImage) {
+        avifImageDestroy(nextImage);
+    }
+    return success;
+}
+
+static avifBool avifEncodeRestOfLayeredImage(avifEncoder * encoder, const avifSettings * settings, int layerIndex, const avifImage * firstImage)
+{
+    avifBool success = AVIF_FALSE;
+    int layers = encoder->extraLayerCount + 1;
+    int qualityIncrement = (settings->quality - encoder->quality) / encoder->extraLayerCount;
+    int qualityAlphaIncrement = (settings->qualityAlpha - encoder->qualityAlpha) / encoder->extraLayerCount;
+
+    while (layerIndex < layers) {
+        encoder->quality += qualityIncrement;
+        encoder->qualityAlpha += qualityAlphaIncrement;
+        if (layerIndex == layers - 1) {
+            encoder->quality = settings->quality;
+            encoder->qualityAlpha = settings->qualityAlpha;
+        }
+
+        printf(" * Encoding layer %d: color quality [%d (%s)], alpha quality [%d (%s)]\n",
+               layerIndex,
+               encoder->quality,
+               qualityString(encoder->quality),
+               encoder->qualityAlpha,
+               qualityString(encoder->qualityAlpha));
+
+        const avifResult result = avifEncoderAddImage(encoder, firstImage, settings->outputTiming.duration, AVIF_ADD_IMAGE_FLAG_NONE);
+        if (result != AVIF_RESULT_OK) {
+            fprintf(stderr, "ERROR: Failed to encode image: %s\n", avifResultToString(result));
+            goto cleanup;
+        }
+        ++layerIndex;
+    }
+    success = AVIF_TRUE;
+
+cleanup:
+    return success;
+}
+
 static avifBool avifEncodeImagesFixedQuality(const avifSettings * settings,
                                              avifInput * input,
                                              const avifInputFile * firstFile,
@@ -664,7 +807,6 @@
     avifBool success = AVIF_FALSE;
     avifRWDataFree(encoded);
     avifEncoder * encoder = avifEncoderCreate();
-    avifImage * nextImage = NULL;
     if (!encoder) {
         fprintf(stderr, "ERROR: Out of memory\n");
         goto cleanup;
@@ -698,6 +840,25 @@
     encoder->keyframeInterval = settings->keyframeInterval;
     encoder->repetitionCount = settings->repetitionCount;
 
+    if (settings->progressive) {
+        // If the color quality or alpha quality is less than 10, the main()
+        // function overrides --progressive and sets settings->progressive to
+        // false.
+        assert((settings->quality >= 10) && (settings->qualityAlpha >= 10));
+        encoder->extraLayerCount = 1;
+        // Encode the base layer with a very low quality to ensure a small encoded size.
+        encoder->quality = 2;
+        if (firstImage->alphaPlane && firstImage->alphaRowBytes) {
+            encoder->qualityAlpha = 2;
+        }
+        printf(" * Encoding layer %d: color quality [%d (%s)], alpha quality [%d (%s)]\n",
+               0,
+               encoder->quality,
+               qualityString(encoder->quality),
+               encoder->qualityAlpha,
+               qualityString(encoder->qualityAlpha));
+    }
+
     for (int i = 0; i < settings->codecSpecificOptions.count; ++i) {
         if (avifEncoderSetCodecSpecificOption(encoder, settings->codecSpecificOptions.keys[i], settings->codecSpecificOptions.values[i]) !=
             AVIF_RESULT_OK) {
@@ -720,7 +881,7 @@
         int imageIndex = 1; // firstImage with imageIndex 0 is already available.
 
         avifAddImageFlags addImageFlags = AVIF_ADD_IMAGE_FLAG_NONE;
-        if (!avifInputHasRemainingData(input, imageIndex)) {
+        if (!avifInputHasRemainingData(input, imageIndex) && !settings->progressive) {
             addImageFlags |= AVIF_ADD_IMAGE_FLAG_SINGLE;
         }
 
@@ -738,96 +899,16 @@
             goto cleanup;
         }
 
-        // Not generating a single-image grid: Use all remaining input files as subsequent frames.
-
-        const avifInputFile * nextFile;
-        while ((nextFile = avifInputGetFile(input, imageIndex)) != NULL) {
-            uint64_t nextDurationInTimescales = nextFile->duration ? nextFile->duration : settings->outputTiming.duration;
-
-            printf(" * Encoding frame %d [%" PRIu64 "/%" PRIu64 " ts]: %s\n",
-                   imageIndex,
-                   nextDurationInTimescales,
-                   settings->outputTiming.timescale,
-                   nextFile->filename);
-
-            if (nextImage) {
-                avifImageDestroy(nextImage);
-            }
-            nextImage = avifImageCreateEmpty();
-            if (!nextImage) {
-                fprintf(stderr, "ERROR: Out of memory\n");
+        if (settings->progressive) {
+            if (!avifEncodeRestOfLayeredImage(encoder, settings, imageIndex, firstImage)) {
                 goto cleanup;
             }
-            nextImage->colorPrimaries = firstImage->colorPrimaries;
-            nextImage->transferCharacteristics = firstImage->transferCharacteristics;
-            nextImage->matrixCoefficients = firstImage->matrixCoefficients;
-            nextImage->yuvRange = firstImage->yuvRange;
-            nextImage->alphaPremultiplied = firstImage->alphaPremultiplied;
-
-            // Ignore ICC, Exif and XMP because only the metadata of the first frame is taken into
-            // account by the libavif API.
-            if (!avifInputReadImage(input,
-                                    imageIndex,
-                                    /*ignoreICC=*/AVIF_TRUE,
-                                    /*ignoreExif=*/AVIF_TRUE,
-                                    /*ignoreXMP=*/AVIF_TRUE,
-                                    nextImage,
-                                    /*outDepth=*/NULL,
-                                    /*sourceIsRGB=*/NULL,
-                                    /*sourceTiming=*/NULL,
-                                    settings->chromaDownsampling)) {
+        } else {
+            // Not generating a single-image grid: Use all remaining input files as subsequent
+            // frames.
+            if (!avifEncodeRestOfImageSequence(encoder, settings, input, imageIndex, firstImage)) {
                 goto cleanup;
             }
-
-            // Verify that this frame's properties matches the first frame's properties
-            if ((firstImage->width != nextImage->width) || (firstImage->height != nextImage->height)) {
-                fprintf(stderr,
-                        "ERROR: Image sequence dimensions mismatch, [%ux%u] vs [%ux%u]: %s\n",
-                        firstImage->width,
-                        firstImage->height,
-                        nextImage->width,
-                        nextImage->height,
-                        nextFile->filename);
-                goto cleanup;
-            }
-            if (firstImage->depth != nextImage->depth) {
-                fprintf(stderr,
-                        "ERROR: Image sequence depth mismatch, [%u] vs [%u]: %s\n",
-                        firstImage->depth,
-                        nextImage->depth,
-                        nextFile->filename);
-                goto cleanup;
-            }
-            if ((firstImage->colorPrimaries != nextImage->colorPrimaries) ||
-                (firstImage->transferCharacteristics != nextImage->transferCharacteristics) ||
-                (firstImage->matrixCoefficients != nextImage->matrixCoefficients)) {
-                fprintf(stderr,
-                        "ERROR: Image sequence CICP mismatch, [%u/%u/%u] vs [%u/%u/%u]: %s\n",
-                        firstImage->colorPrimaries,
-                        firstImage->matrixCoefficients,
-                        firstImage->transferCharacteristics,
-                        nextImage->colorPrimaries,
-                        nextImage->transferCharacteristics,
-                        nextImage->matrixCoefficients,
-                        nextFile->filename);
-                goto cleanup;
-            }
-            if (firstImage->yuvRange != nextImage->yuvRange) {
-                fprintf(stderr,
-                        "ERROR: Image sequence range mismatch, [%s] vs [%s]: %s\n",
-                        (firstImage->yuvRange == AVIF_RANGE_FULL) ? "Full" : "Limited",
-                        (nextImage->yuvRange == AVIF_RANGE_FULL) ? "Full" : "Limited",
-                        nextFile->filename);
-                goto cleanup;
-            }
-
-            const avifResult nextImageResult =
-                avifEncoderAddImage(encoder, nextImage, nextDurationInTimescales, AVIF_ADD_IMAGE_FLAG_NONE);
-            if (nextImageResult != AVIF_RESULT_OK) {
-                fprintf(stderr, "ERROR: Failed to encode image: %s\n", avifResultToString(nextImageResult));
-                goto cleanup;
-            }
-            ++imageIndex;
         }
     }
 
@@ -846,9 +927,6 @@
         }
         avifEncoderDestroy(encoder);
     }
-    if (nextImage) {
-        avifImageDestroy(nextImage);
-    }
     return success;
 }
 
@@ -974,6 +1052,7 @@
     settings.tileRowsLog2 = -1;
     settings.tileColsLog2 = -1;
     settings.autoTiling = AVIF_FALSE;
+    settings.progressive = AVIF_FALSE;
     settings.speed = 6;
     settings.repetitionCount = AVIF_REPETITION_COUNT_INFINITE;
     settings.keyframeInterval = 0;
@@ -1151,6 +1230,8 @@
             }
         } else if (!strcmp(arg, "--autotiling")) {
             settings.autoTiling = AVIF_TRUE;
+        } else if (!strcmp(arg, "--progressive")) {
+            settings.progressive = AVIF_TRUE;
         } else if (!strcmp(arg, "-g") || !strcmp(arg, "--grid")) {
             NEXTARG();
             settings.gridDimsCount = parseU32List(settings.gridDims, arg);
@@ -1433,6 +1514,15 @@
             }
         }
     }
+    assert(settings.quality != INVALID_QUALITY);
+    assert(settings.qualityAlpha != INVALID_QUALITY);
+    // In progressive encoding we use a very low quality (2) for the base layer to ensure a small
+    // encoded size. If the target quality is close to the quality of the base layer, don't bother
+    // with progressive encoding.
+    if (settings.progressive && ((settings.quality < 10) || (settings.qualityAlpha < 10))) {
+        settings.progressive = AVIF_FALSE;
+        printf("The --progressive option was ignored because the quality is below 10.\n");
+    }
 
     stdinFile.filename = "(stdin)";
     stdinFile.duration = settings.outputTiming.duration;
@@ -1749,7 +1839,10 @@
     }
     printf("AVIF to be written:%s\n", lossyHint);
     const avifImage * avif = gridCells ? gridCells[0] : image;
-    avifImageDump(avif, settings.gridDims[0], settings.gridDims[1], AVIF_PROGRESSIVE_STATE_UNAVAILABLE);
+    avifImageDump(avif,
+                  settings.gridDims[0],
+                  settings.gridDims[1],
+                  settings.progressive ? AVIF_PROGRESSIVE_STATE_AVAILABLE : AVIF_PROGRESSIVE_STATE_UNAVAILABLE);
 
     if (settings.autoTiling) {
         if ((settings.tileRowsLog2 >= 0) || (settings.tileColsLog2 >= 0)) {
diff --git a/tests/test_cmd.sh b/tests/test_cmd.sh
index 1e5d70b..d3ec04a 100755
--- a/tests/test_cmd.sh
+++ b/tests/test_cmd.sh
@@ -68,6 +68,12 @@
   "${AVIFDEC}" "${ENCODED_FILE}" "${DECODED_FILE}"
   "${ARE_IMAGES_EQUAL}" "${INPUT_Y4M}" "${DECODED_FILE}" 0 && exit 1
 
+  # Progressive test.
+  echo "Testing basic progressive"
+  "${AVIFENC}" --progressive -s 8 "${INPUT_Y4M}" -o "${ENCODED_FILE}"
+  "${AVIFDEC}" "${ENCODED_FILE}" "${DECODED_FILE}"
+  "${AVIFDEC}" --progressive "${ENCODED_FILE}" "${DECODED_FILE}"
+
   # Argument parsing test with filenames starting with a dash.
   echo "Testing arguments"
   "${AVIFENC}" -s 10 "${INPUT_Y4M}" -- "${ENCODED_FILE_WITH_DASH}"