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}"