// Copyright 2019 Joe Drago. All rights reserved.
// SPDX-License-Identifier: BSD-2-Clause

#include "avif/avif.h"

#include "avifjpeg.h"
#include "avifpng.h"
#include "avifutil.h"
#include "y4m.h"

#include <assert.h>
#include <inttypes.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#if defined(_WIN32)
// for setmode()
#include <fcntl.h>
#include <io.h>
#endif

#define NEXTARG()                                                     \
    if (((argIndex + 1) == argc) || (argv[argIndex + 1][0] == '-')) { \
        fprintf(stderr, "%s requires an argument.", arg);             \
        goto cleanup;                                                 \
    }                                                                 \
    arg = argv[++argIndex]

typedef struct
{
    avifCodecChoice codecChoice;
    int jobs;
    int targetSize;
    avifBool qualityIsConstrained;      // true if quality explicitly set by the user
    avifBool qualityAlphaIsConstrained; // true if qualityAlpha explicitly set by the user
    int overrideQuality;
    int overrideQualityAlpha;
    avifBool progressive; // automatic layered encoding (progressive) with single input
    avifBool layered;     // manual layered encoding by specifying each layer
    int layers;
    int speed;
    avifHeaderFormatFlags headerFormat;
    uint64_t creationTime;
    uint64_t modificationTime;

    avifBool paspPresent;
    uint32_t paspValues[2];
    avifBool clapValid; // clapValues may contain 4 crop values and need conversion. In this case clapValid is also false.
    uint32_t clapValues[8];
    avifBool gridDimsPresent;
    uint32_t gridDims[2];
    avifBool clliPresent;
    uint32_t clliValues[2];

    int repetitionCount;
    int keyframeInterval;
    avifBool ignoreExif;
    avifBool ignoreXMP;
    avifBool ignoreColorProfile;

    // These settings are only relevant when compiled with AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION.
    avifBool qualityGainMapIsConstrained; // true if qualityGainMap explicitly set by the user
    int qualityGainMap;
    avifBool ignoreGainMap; // ignore any gain map present in the input file.

    // This holds the output timing for image sequences. The timescale member in this struct will
    // become the timescale set on avifEncoder, and the duration member will be the default duration
    // for any frame that doesn't have a specific duration set on the commandline. See the
    // declaration of avifAppSourceTiming for more documentation.
    avifAppSourceTiming outputTiming;

    avifBool cicpExplicitlySet;
    avifColorPrimaries colorPrimaries;
    avifTransferCharacteristics transferCharacteristics;
    avifMatrixCoefficients matrixCoefficients;
    avifChromaDownsampling chromaDownsampling;
    avifAppFileFormat inputFormat;
} avifSettings;

typedef struct
{
    char ** keys;
    char ** values;
    int count;
} avifCodecSpecificOptions;

typedef struct avifSettingsEntryInt
{
    int value;
    avifBool set;
} avifSettingsEntryInt;

static avifSettingsEntryInt intSettingsEntryOf(int value)
{
    avifSettingsEntryInt entry = { value, AVIF_TRUE };
    return entry;
}

typedef avifSettingsEntryInt avifSettingsEntryBool;

static avifSettingsEntryBool boolSettingsEntryOf(avifBool value)
{
    return intSettingsEntryOf(value);
}

typedef struct avifSettingsEntryScalingMode
{
    avifScalingMode value;
    avifBool set;
} avifSettingsEntryScalingMode;

static avifSettingsEntryScalingMode scalingModeSettingsEntryOf(uint32_t n, uint32_t d)
{
    avifFraction mode = { (int32_t)n, (int32_t)d };
    avifSettingsEntryScalingMode entry = { { mode, mode }, AVIF_TRUE };
    return entry;
}

// Each entry records an "update" action to the corresponding field in avifEncoder.
// Fields in avifEncoder are not reset after encoding an image,
// so updates naturally apply to all following inputs,
// and is only recorded once on the first applicable input.
typedef struct avifInputFileSettings
{
    avifSettingsEntryInt quality;
    avifSettingsEntryInt qualityAlpha;
    avifSettingsEntryInt minQuantizer;
    avifSettingsEntryInt maxQuantizer;
    avifSettingsEntryInt minQuantizerAlpha;
    avifSettingsEntryInt maxQuantizerAlpha;
    avifSettingsEntryInt tileRowsLog2;
    avifSettingsEntryInt tileColsLog2;
    avifSettingsEntryBool autoTiling;
    avifSettingsEntryScalingMode scalingMode;

    avifCodecSpecificOptions codecSpecificOptions;
} avifInputFileSettings;

#define AVIF_FILENAME_STDIN NULL
static const char * avifPrettyFilename(const char * filename)
{
    if (filename == AVIF_FILENAME_STDIN) {
        return "(stdin)";
    }
    return filename;
}

typedef struct avifInputFile
{
    const char * filename; // If AVIF_FILENAME_STDIN, use stdin
    uint64_t duration;     // If 0, use the default duration
    avifInputFileSettings settings;
} avifInputFile;

typedef struct
{
    int fileIndex;
    avifImage * image;
    const avifInputFileSettings * settings;
    uint32_t fileBitDepth;
    avifBool fileIsRGB;
    avifAppSourceTiming sourceTiming;
} avifInputCacheEntry;

typedef struct avifInput
{
    avifInputFile * files;
    int filesCount;
    int fileIndex;
    struct y4mFrameIterator * frameIter;
    avifPixelFormat requestedFormat;
    int requestedDepth;
    int requestedDepthExtension;

    avifBool cacheEnabled;
    avifInputCacheEntry * cache;
    int cacheCount;
} avifInput;

typedef struct avifEncodedByteSizes
{
    size_t colorSizeBytes;
    size_t alphaSizeBytes;
    size_t gainMapSizeBytes;
} avifEncodedByteSizes;

static void syntaxShort(void)
{
    printf("Syntax: avifenc [options] -q quality input.[jpg|jpeg|png|y4m] output.avif\n");
    printf("where quality is between %d (worst quality) and %d (lossless).\n", AVIF_QUALITY_WORST, AVIF_QUALITY_LOSSLESS);
    printf("Typical value is 60-80.\n\n");
    printf("Try -h for an exhaustive list of options.\n");
}

static void syntaxLong(void)
{
    printf("Syntax: avifenc [options] input.[jpg|jpeg|png|y4m] output.avif\n");
    printf("Standard options:\n");
    printf("    -h,--help                         : Show syntax help (this page)\n");
    printf("    -V,--version                      : Show the version number\n");
    printf("\n");
    printf("Basic options:\n");
    printf("    -q,--qcolor Q                     : Quality for color in %d..%d where %d is lossless\n",
           AVIF_QUALITY_WORST,
           AVIF_QUALITY_BEST,
           AVIF_QUALITY_LOSSLESS);
    printf("    --qalpha Q                        : Quality for alpha in %d..%d where %d is lossless\n",
           AVIF_QUALITY_WORST,
           AVIF_QUALITY_BEST,
           AVIF_QUALITY_LOSSLESS);
    printf("    -s,--speed S                      : Encoder speed in %d..%d where 0 is the slowest, 10 is the fastest. Or 'default' or 'd' for codec internal defaults. (Default: 6)\n",
           AVIF_SPEED_SLOWEST,
           AVIF_SPEED_FASTEST);
    printf("\n");
    printf("Advanced options:\n");
    printf("    -j,--jobs J                       : Number of jobs (worker threads), or 'all' to potentially use as many cores as possible. (Default: all)\n");
    printf("    --no-overwrite                    : Never overwrite existing output file\n");
    printf("    -o,--output FILENAME              : Instead of using the last filename given as output, use this filename\n");
#if defined(AVIF_ENABLE_EXPERIMENTAL_MINI)
    printf("    --mini                            : EXPERIMENTAL: Use reduced header if possible (backward-incompatible)\n");
#endif
    printf("    -l,--lossless                     : Set all defaults to encode losslessly, and emit warnings when settings/input don't allow for it\n");
    printf("    -d,--depth D[,Dextension]         : D is the output bit depth per channel. D must be 8, 10 or 12. (JPEG/PNG only; y4m or stdin: bit depth is retained)\n");
    printf("                                        If specified, Dextension adds a hidden encoded image of Dextension bit depth in the same file as the primary image to reach 16-bit depth at decoding.\n");
    printf("                                        See avifSampleTransformRecipe for the supported combinations (8,8 and 12,4 and 12,8).\n");
    printf("    -y,--yuv FORMAT                   : Output format, one of 'auto' (default), 444, 422, 420 or 400. Ignored for y4m or stdin (y4m format is retained)\n");
    printf("                                        For JPEG, auto honors the JPEG's internal format, if possible. For grayscale PNG, auto defaults to 400. For all other cases, auto defaults to 444\n");
    printf("    -p,--premultiply                  : Premultiply color by the alpha channel and signal this in the AVIF\n");
    printf("    --sharpyuv                        : Use sharp RGB to YUV420 conversion (if supported). Ignored for y4m or if output is not 420.\n");
    printf("    --stdin                           : Read input from stdin instead of file paths. No other input is allowed. The input format is assumed to be y4m unless --input-format is specified. The output file path must still be provided.\n");
    printf("    --input-format FORMAT             : File format of the input data. One of: jpeg/png/y4m/auto. (Default: auto, except for stdin where auto is not supported and the default is y4m)\n");
    printf("    --cicp,--nclx P/T/M               : Set CICP values (nclx colr box) (3 raw numbers, use -r to set range flag)\n");
    printf("                                        P = color primaries\n");
    printf("                                        T = transfer characteristics\n");
    printf("                                        M = matrix coefficients\n");
    printf("                                        Use 2 for any you wish to leave unspecified\n");
    printf("    -r,--range RANGE                  : YUV range, one of 'limited' or 'l', 'full' or 'f'. (JPEG/PNG only, default: full; For y4m or stdin, range is retained)\n");
    printf("    --target-size S                   : Set target file size in bytes (up to 7 times slower)\n");
    printf("    --progressive                     : Automatically set parameters to encode a simple layered image supporting progressive rendering from a single input frame.\n");
    printf("    --layered                         : Encode a layered AVIF. Each input is encoded as one layer and at most %d layers can be encoded.\n",
           AVIF_MAX_AV1_LAYER_COUNT);
    printf("    -g,--grid MxN                     : Encode a single-image grid AVIF with M cols & N rows. Either supply MxN identical W/H/D images, or a single\n");
    printf("                                        image that can be evenly split into the MxN grid and follow AVIF grid image restrictions. The grid will adopt\n");
    printf("                                        the color profile of the first image supplied.\n");
    printf("    -c,--codec C                      : Codec to use (choose from versions list below)\n");
    printf("    --exif FILENAME                   : Provide an Exif metadata payload to be associated with the primary item (implies --ignore-exif)\n");
    printf("    --xmp FILENAME                    : Provide an XMP metadata payload to be associated with the primary item (implies --ignore-xmp)\n");
    printf("    --icc FILENAME                    : Provide an ICC profile payload to be associated with the primary item (implies --ignore-icc)\n");
    printf("    --timescale,--fps V               : Timescale for image sequences. If all frames are 1 timescale in length, this is equivalent to frames per second. (Default: 30)\n");
    printf("                                        If neither duration nor timescale are set, avifenc will attempt to use the framerate stored in a y4m header, if present.\n");
    printf("    --creation-time                   : Creation time for image sequences, in seconds since 1970-01-01 00:00:00 UTC (the Unix epoch). (Default: 0, use the modification time)\n");
    printf("    --modification-time               : Modification time for image sequences, in seconds since 1970-01-01 00:00:00 UTC (the Unix epoch). (Default: 0, use the current time)\n");
    printf("    -k,--keyframe INTERVAL            : Maximum keyframe interval for image sequences (any set of INTERVAL consecutive frames will have at least one keyframe). Set to 0 to disable (default).\n");
    printf("    --ignore-exif                     : If the input file contains embedded Exif metadata, ignore it (no-op if absent)\n");
    printf("    --ignore-xmp                      : If the input file contains embedded XMP metadata, ignore it (no-op if absent)\n");
    printf("    --ignore-profile,--ignore-icc     : If the input file contains an embedded color profile, ignore it (no-op if absent)\n");
#if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
    printf("    --ignore-gain-map                 : If the input file contains an embedded gain map, ignore it (no-op if absent)\n");
    printf("    --qgain-map Q                     : Quality for the gain map in %d..%d where %d is lossless\n",
           AVIF_QUALITY_WORST,
           AVIF_QUALITY_BEST,
           AVIF_QUALITY_LOSSLESS);
#endif
    printf("    --pasp H,V                        : Add pasp property (aspect ratio). H=horizontal spacing, V=vertical spacing\n");
    printf("    --crop CROPX,CROPY,CROPW,CROPH    : Add clap property (clean aperture), but calculated from a crop rectangle\n");
    printf("    --clap WN,WD,HN,HD,HON,HOD,VON,VOD: Add clap property (clean aperture). Width, Height, HOffset, VOffset (in numerator/denominator pairs)\n");
    printf("    --irot ANGLE                      : Add irot property (rotation), in 0..3. Makes (90 * ANGLE) degree rotation anti-clockwise\n");
    printf("    --imir AXIS                       : Add imir property (mirroring). 0=top-to-bottom, 1=left-to-right\n");
    printf("    --clli MaxCLL,MaxPALL             : Add clli property (content light level information).\n");
    printf("    --repetition-count N              : Number of times an animated image sequence will be repeated, or 'infinite' for infinite repetitions. (Default: infinite)\n");
    printf("    --                                : Signal the end of options. Everything after this is interpreted as file names.\n");
    printf("\n");
    printf("Updatable options:\n");
    printf("  The following options can optionally have a :u (or :update) suffix like `-q:u Q`, to apply only to input files appearing after the option:\n");
    printf("    -q,--qcolor Q                     : Quality for color in %d..%d where %d is lossless\n",
           AVIF_QUALITY_WORST,
           AVIF_QUALITY_BEST,
           AVIF_QUALITY_LOSSLESS);
    printf("    --qalpha Q                        : Quality for alpha in %d..%d where %d is lossless\n",
           AVIF_QUALITY_WORST,
           AVIF_QUALITY_BEST,
           AVIF_QUALITY_LOSSLESS);
#if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
    printf("    --qgain-map Q                     : Quality for the gain map in %d..%d where %d is lossless\n",
           AVIF_QUALITY_WORST,
           AVIF_QUALITY_BEST,
           AVIF_QUALITY_LOSSLESS);
#endif
    printf("    --tilerowslog2 R                  : log2 of number of tile rows in 0..6. (Default: 0)\n");
    printf("                                        If specified, switch to manual tiling.\n");
    printf("    --tilecolslog2 C                  : log2 of number of tile columns 0..6. (Default: 0)\n");
    printf("                                        If specified, switch to manual tiling.\n");
    printf("    --autotiling                      : Set --tilerowslog2 and --tilecolslog2 automatically\n");
    printf("                                        If specified, switch to automatic tiling.\n");
    printf("                                        avifenc starts in automatic tiling mode.\n");
    printf("    --min QP                          : Deprecated, use -q 0..100 instead\n");
    printf("    --max QP                          : Deprecated, use -q 0..100 instead\n");
    printf("    --minalpha QP                     : Deprecated, use --qalpha 0..100 instead\n");
    printf("    --maxalpha QP                     : Deprecated, use --qalpha 0..100 instead\n");
    printf("    --scaling-mode N[/D]              : Set frame (layer) scaling mode as given fraction. If omitted, the denominator defaults to 1. (Default: 1/1)\n");
    printf("    --duration D                      : Frame durations (in timescales) (default: 1). This option always applies to following inputs with or without the `:u` suffix.\n");
    printf("    -a,--advanced KEY[=VALUE]         : Pass an advanced, codec-specific key/value string pair directly to the codec. avifenc will warn on any not used by the codec.\n");
    printf("\n");
    if (avifCodecName(AVIF_CODEC_CHOICE_AOM, 0)) {
        printf("aom-specific advanced options:\n");
        printf("    1. <key>=<value> applies to both the color (YUV) planes and the alpha plane (if present).\n");
        printf("    2. color:<key>=<value> or c:<key>=<value> applies only to the color (YUV) planes.\n");
        printf("    3. alpha:<key>=<value> or a:<key>=<value> applies only to the alpha plane (if present).\n");
        printf("       Since the alpha plane is encoded as a monochrome image, the options that refer to the chroma planes,\n");
        printf("       such as enable-chroma-deltaq=B, should not be used with the alpha plane. In addition, the film grain\n");
        printf("       options are unlikely to make sense for the alpha plane.\n");
        printf("\n");
        printf("    When used with libaom 3.0.0 or later, any key-value pairs supported by the aom_codec_set_option() function\n");
        printf("    can be used. When used with libaom 2.0.x or older, the following key-value pairs can be used:\n");
        printf("\n");
        printf("    aq-mode=M                         : Adaptive quantization mode. 0=off (default), 1=variance, 2=complexity, 3=cyclic refresh\n");
        printf("    cq-level=Q                        : Constant/Constrained Quality level in 0..63, end-usage must be set to cq or q\n");
        printf("    enable-chroma-deltaq=B            : Enable delta quantization in chroma planes. 0=disable (default), 1=enable\n");
        printf("    end-usage=MODE                    : Rate control mode, one of 'vbr', 'cbr', 'cq', or 'q'\n");
        printf("    sharpness=S                       : Bias towards block sharpness in rate-distortion optimization of transform coefficients in 0..7. (Default: 0)\n");
        printf("    tune=METRIC                       : Tune the encoder for distortion metric, one of 'psnr', 'ssim' or 'iq'.\n");
        printf("                                        (Default for color: still non-RGB images (libaom v3.13.0+): iq, otherwise: ssim; default for alpha: psnr)\n");
        printf("    film-grain-test=TEST              : Film grain test vectors in 0..16. 0=none (default), 1=test1, 2=test2, ... 16=test16\n");
        printf("    film-grain-table=FILENAME         : Path to file containing film grain parameters\n");
        printf("\n");
    }
    avifPrintVersions();
}

// This is *very* arbitrary, I just want to set people's expectations a bit
static const char * qualityString(int quality)
{
    if (quality == AVIF_QUALITY_LOSSLESS) {
        return "Lossless";
    }
    if (quality >= 80) {
        return "High";
    }
    if (quality >= 50) {
        return "Medium";
    }
    if (quality == AVIF_QUALITY_WORST) {
        return "Worst";
    }
    return "Low";
}

// Parse exactly n uint32_t from arg with separator character delim.
// Output must be able to hold at least n elements.
// Length of arg shall not exceed 127 characters or result can be truncated.
static avifBool parseU32List(uint32_t output[], int n, const char * arg, const char delim)
{
    char buffer[128];
    strncpy(buffer, arg, 127);
    buffer[127] = 0;

    // strtok wants a string for delim, so build a single character string here.
    const char delims[2] = { delim, '\0' };

    int index = 0;
    char * token = buffer;
    if (n > 1) {
        token = strtok(buffer, delims);
    }
    while (token != NULL) {
        output[index] = (uint32_t)atoi(token);
        ++index;
        if (index >= n) {
            break;
        }

        token = strtok(NULL, delims);
    }

    // Exactly n, and no more separator character
    if (index == n && strchr(token, delim) == NULL) {
        return AVIF_TRUE;
    }
    return AVIF_FALSE;
}

typedef enum avifOptionSuffixType
{
    AVIF_OPTION_SUFFIX_NONE,
    AVIF_OPTION_SUFFIX_UPDATE,
    AVIF_OPTION_SUFFIX_INVALID,
} avifOptionSuffixType;

static avifOptionSuffixType parseOptionSuffix(const char * arg, avifBool warnNoSuffix)
{
    const char * suffix = strchr(arg, ':');

    if (suffix == NULL) {
        if (warnNoSuffix) {
            fprintf(stderr,
                    "WARNING: %s is applying to all inputs. Use %s:u to apply only to inputs after it, "
                    "or move it before first input to avoid ambiguity.\n",
                    arg,
                    arg);
        }
        return AVIF_OPTION_SUFFIX_NONE;
    }

    if (!strcmp(suffix, ":u") || !strcmp(suffix, ":update")) {
        return AVIF_OPTION_SUFFIX_UPDATE;
    }

    fprintf(stderr, "ERROR: Unknown option suffix in flag %s.\n", arg);
    return AVIF_OPTION_SUFFIX_INVALID;
}

static avifBool strpre(const char * str, const char * prefix)
{
    return strncmp(str, prefix, strlen(prefix)) == 0;
}

static avifBool convertCropToClap(uint32_t srcW, uint32_t srcH, uint32_t clapValues[8])
{
    avifCleanApertureBox clap;
    avifCropRect cropRect;
    cropRect.x = clapValues[0];
    cropRect.y = clapValues[1];
    cropRect.width = clapValues[2];
    cropRect.height = clapValues[3];

    avifDiagnostics diag;
    avifDiagnosticsClearError(&diag);
    avifBool convertResult = avifCleanApertureBoxFromCropRect(&clap, &cropRect, srcW, srcH, &diag);
    if (!convertResult) {
        fprintf(stderr,
                "ERROR: Impossible crop rect: imageSize:[%ux%u], cropRect:[%u,%u, %ux%u] - %s\n",
                srcW,
                srcH,
                cropRect.x,
                cropRect.y,
                cropRect.width,
                cropRect.height,
                diag.error);
        return convertResult;
    }

    clapValues[0] = clap.widthN;
    clapValues[1] = clap.widthD;
    clapValues[2] = clap.heightN;
    clapValues[3] = clap.heightD;
    clapValues[4] = clap.horizOffN;
    clapValues[5] = clap.horizOffD;
    clapValues[6] = clap.vertOffN;
    clapValues[7] = clap.vertOffD;
    return AVIF_TRUE;
}

static avifBool avifInputAddCachedImage(avifInput * input)
{
    avifImage * newImage = avifImageCreateEmpty();
    if (!newImage) {
        return AVIF_FALSE;
    }
    avifInputCacheEntry * newCachedImages = malloc((input->cacheCount + 1) * sizeof(*input->cache));
    if (!newCachedImages) {
        avifImageDestroy(newImage);
        return AVIF_FALSE;
    }
    avifInputCacheEntry * oldCachedImages = input->cache;
    input->cache = newCachedImages;
    if (input->cacheCount) {
        memcpy(input->cache, oldCachedImages, input->cacheCount * sizeof(*input->cache));
    }
    memset(&input->cache[input->cacheCount], 0, sizeof(input->cache[input->cacheCount]));
    input->cache[input->cacheCount].fileIndex = input->fileIndex;
    input->cache[input->cacheCount].image = newImage;
    ++input->cacheCount;
    free(oldCachedImages);
    return AVIF_TRUE;
}

static avifBool fileExists(const char * filename)
{
    FILE * outfile = fopen(filename, "rb");
    if (outfile) {
        fclose(outfile);
        return AVIF_TRUE;
    }
    return AVIF_FALSE;
}

static const avifInputFile * avifInputGetFile(const avifInput * input, int imageIndex)
{
    if (imageIndex < input->cacheCount) {
        return &input->files[input->cache[imageIndex].fileIndex];
    }
    if (input->fileIndex >= input->filesCount) {
        return NULL;
    }

    if (input->files[input->fileIndex].filename == AVIF_FILENAME_STDIN) {
        assert(input->fileIndex == 0);
        ungetc(fgetc(stdin), stdin); // Kick stdin to force EOF

        if (feof(stdin)) {
            return NULL;
        }
    }

    return &input->files[input->fileIndex];
}

static avifBool avifInputHasRemainingData(const avifInput * input, int imageIndex)
{
    if (imageIndex < input->cacheCount) {
        return AVIF_TRUE;
    }
    if (input->fileIndex >= input->filesCount) {
        return AVIF_FALSE;
    }
    if (input->files[input->fileIndex].filename == AVIF_FILENAME_STDIN) {
        return !feof(stdin);
    }
    return AVIF_TRUE;
}

static avifBool avifInputReadImage(avifInput * input,
                                   int imageIndex,
                                   avifBool ignoreColorProfile,
                                   avifBool ignoreExif,
                                   avifBool ignoreXMP,
                                   avifBool allowChangingCicp,
                                   avifBool ignoreGainMap,
                                   avifImage * image,
                                   const avifInputFileSettings ** settings,
                                   uint32_t * outDepth,
                                   avifBool * sourceIsRGB,
                                   avifAppSourceTiming * sourceTiming,
                                   avifChromaDownsampling chromaDownsampling,
                                   avifAppFileFormat inputFormat)
{
    if (imageIndex < input->cacheCount) {
        const avifInputCacheEntry * cached = &input->cache[imageIndex];
        const avifCropRect rect = { 0, 0, cached->image->width, cached->image->height };
        if (avifImageSetViewRect(image, cached->image, &rect) != AVIF_RESULT_OK) {
            assert(AVIF_FALSE);
        }
#if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
        if (cached->image->gainMap && cached->image->gainMap->image) {
            image->gainMap->image = avifImageCreateEmpty();
            const avifCropRect gainMapRect = { 0, 0, cached->image->gainMap->image->width, cached->image->gainMap->image->height };
            if (avifImageSetViewRect(image->gainMap->image, cached->image->gainMap->image, &gainMapRect) != AVIF_RESULT_OK) {
                assert(AVIF_FALSE);
            }
        }
#endif
        if (settings) {
            *settings = cached->settings;
        }
        if (outDepth) {
            *outDepth = cached->fileBitDepth;
        }
        if (sourceIsRGB) {
            *sourceIsRGB = cached->fileIsRGB;
        }
        if (sourceTiming) {
            *sourceTiming = cached->sourceTiming;
        }
        return AVIF_TRUE;
    }

    avifImage * dstImage = image;
    const avifInputFileSettings ** dstSettings = settings;
    uint32_t * dstDepth = outDepth;
    avifBool * dstSourceIsRGB = sourceIsRGB;
    avifAppSourceTiming * dstSourceTiming = sourceTiming;
    if (input->cacheEnabled) {
        if (!avifInputAddCachedImage(input)) {
            fprintf(stderr, "ERROR: Out of memory");
            return AVIF_FALSE;
        }
        assert(imageIndex + 1 == input->cacheCount);
        dstImage = input->cache[imageIndex].image;
        // Copy CICP, clap etc.
        if (avifImageCopy(dstImage, image, /*planes=*/0) != AVIF_RESULT_OK) {
            assert(AVIF_FALSE);
        }
        dstSettings = &input->cache[imageIndex].settings;
        dstDepth = &input->cache[imageIndex].fileBitDepth;
        dstSourceIsRGB = &input->cache[imageIndex].fileIsRGB;
        dstSourceTiming = &input->cache[imageIndex].sourceTiming;
    }

    if (dstSourceTiming) {
        // A source timing of all 0s is a sentinel value hinting that the value is unset / should be
        // ignored. This is memset here as many of the paths in avifInputReadImage() do not set these
        // values. See the declaration for avifAppSourceTiming for more information.
        memset(dstSourceTiming, 0, sizeof(avifAppSourceTiming));
    }

    if (input->fileIndex >= input->filesCount) {
        return AVIF_FALSE;
    }
    const avifInputFile * currentFile = &input->files[input->fileIndex];
    if (currentFile->filename == AVIF_FILENAME_STDIN) {
        if (feof(stdin)) {
            return AVIF_FALSE;
        }
        if (inputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) {
            inputFormat = AVIF_APP_FILE_FORMAT_Y4M;
        }
    }

    const avifColorPrimaries colorPrimariesBefore = dstImage->colorPrimaries;
    const avifTransferCharacteristics transferCharacteristicsBefore = dstImage->transferCharacteristics;
    if (avifReadImage(currentFile->filename,
                      inputFormat,
                      input->requestedFormat,
                      input->requestedDepthExtension == 0 ? input->requestedDepth : 16,
                      chromaDownsampling,
                      ignoreColorProfile,
                      ignoreExif,
                      ignoreXMP,
                      ignoreGainMap,
                      UINT32_MAX,
                      dstImage,
                      dstDepth,
                      dstSourceTiming,
                      &input->frameIter) == AVIF_APP_FILE_FORMAT_UNKNOWN) {
        fprintf(stderr,
                "ERROR: Couldn't read %s file %s\n",
                avifFileFormatToString(inputFormat),
                currentFile->filename == AVIF_FILENAME_STDIN ? "from standard input" : currentFile->filename);
        if (currentFile->filename == AVIF_FILENAME_STDIN && inputFormat == AVIF_APP_FILE_FORMAT_Y4M) {
            fprintf(stderr, "Specify --input-format if the input is not y4m\n");
        }
        return AVIF_FALSE;
    }
    if (!allowChangingCicp) {
        // Restore the previous primaries/transfer in case avifReadImage changed them.
        dstImage->colorPrimaries = colorPrimariesBefore;
        dstImage->transferCharacteristics = transferCharacteristicsBefore;
    }

    if (!input->frameIter) {
        ++input->fileIndex;
    }
    if (dstSourceIsRGB) {
        *dstSourceIsRGB = (inputFormat != AVIF_APP_FILE_FORMAT_Y4M);
    }
    if (dstSettings) {
        *dstSettings = &currentFile->settings;
    }
    assert(dstImage->yuvFormat != AVIF_PIXEL_FORMAT_NONE);

    if (input->cacheEnabled) {
        // Reuse the just created cache entry.
        assert(imageIndex < input->cacheCount);
        return avifInputReadImage(input,
                                  imageIndex,
                                  ignoreColorProfile,
                                  ignoreExif,
                                  ignoreXMP,
                                  allowChangingCicp,
                                  ignoreGainMap,
                                  image,
                                  settings,
                                  outDepth,
                                  sourceIsRGB,
                                  sourceTiming,
                                  chromaDownsampling,
                                  inputFormat);
    }
    return AVIF_TRUE;
}

// Returns NULL if a memory allocation failed. The return value should be freed with free().
static char * avifStrdup(const char * str)
{
    size_t len = strlen(str);
    char * dup = malloc(len + 1);
    if (!dup) {
        return NULL;
    }
    memcpy(dup, str, len + 1);
    return dup;
}

// key is not null-terminated. value is null-terminated.
static avifBool avifCodecSpecificOptionsAddKeyValue(avifCodecSpecificOptions * options, const char * key, size_t keyLength, const char * value)
{
    avifBool success = AVIF_FALSE;
    char ** oldKeys = options->keys;
    char ** oldValues = options->values;
    options->keys = malloc((options->count + 1) * sizeof(*options->keys));
    options->values = malloc((options->count + 1) * sizeof(*options->values));
    if (!options->keys || !options->values) {
        free(options->keys);
        free(options->values);
        options->keys = oldKeys;
        options->values = oldValues;
        return AVIF_FALSE;
    }
    if (options->count) {
        memcpy(options->keys, oldKeys, options->count * sizeof(*options->keys));
        memcpy(options->values, oldValues, options->count * sizeof(*options->values));
    }

    options->values[options->count] = avifStrdup(value);
    options->keys[options->count] = malloc(keyLength + 1);
    if (!options->values[options->count] || !options->keys[options->count]) {
        goto cleanup;
    }
    memcpy(options->keys[options->count], key, keyLength);
    options->keys[options->count][keyLength] = '\0';
    success = AVIF_TRUE;
cleanup:
    ++options->count;
    free(oldKeys);
    free(oldValues);
    return success;
}

static avifBool avifCodecSpecificOptionsAdd(avifCodecSpecificOptions * options, const char * keyValue)
{
    const char * value = strchr(keyValue, '=');
    if (value) {
        // Keep the parts on the left and on the right of the equal sign,
        // but not the equal sign itself.
        const size_t keyLength = value - keyValue;
        return avifCodecSpecificOptionsAddKeyValue(options, keyValue, keyLength, value + 1);
    }
    // Pass in a non-NULL, empty string. Codecs can use the mere existence of a key as a boolean value.
    return avifCodecSpecificOptionsAddKeyValue(options, keyValue, strlen(keyValue), "");
}

static void avifCodecSpecificOptionsFree(avifCodecSpecificOptions * options)
{
    while (options->count) {
        --options->count;
        free(options->keys[options->count]);
        free(options->values[options->count]);
    }
    free(options->keys);
    free(options->values);
    options->keys = NULL;
    options->values = NULL;
}

static void avifSettingsEntryIntOverwrite(avifSettingsEntryInt * dst, const avifSettingsEntryInt * src)
{
    if (src->set) {
        dst->set = AVIF_TRUE;
        dst->value = src->value;
    }
}

static avifBool avifInputFileSettingsOverwrite(avifInputFileSettings * dst, const avifInputFileSettings * src)
{
    avifSettingsEntryIntOverwrite(&dst->quality, &src->quality);
    avifSettingsEntryIntOverwrite(&dst->qualityAlpha, &src->qualityAlpha);
    avifSettingsEntryIntOverwrite(&dst->minQuantizer, &src->minQuantizer);
    avifSettingsEntryIntOverwrite(&dst->maxQuantizer, &src->maxQuantizer);
    avifSettingsEntryIntOverwrite(&dst->minQuantizerAlpha, &src->minQuantizerAlpha);
    avifSettingsEntryIntOverwrite(&dst->maxQuantizerAlpha, &src->maxQuantizerAlpha);
    avifSettingsEntryIntOverwrite(&dst->tileRowsLog2, &src->tileRowsLog2);
    avifSettingsEntryIntOverwrite(&dst->tileColsLog2, &src->tileColsLog2);
    avifSettingsEntryIntOverwrite(&dst->autoTiling, &src->autoTiling);
    if (src->scalingMode.set) {
        dst->scalingMode.set = AVIF_TRUE;
        dst->scalingMode.value = src->scalingMode.value;
    }
    for (int i = 0; i < src->codecSpecificOptions.count; ++i) {
        const char * key = src->codecSpecificOptions.keys[i];
        const char * value = src->codecSpecificOptions.values[i];
        if (!avifCodecSpecificOptionsAddKeyValue(&dst->codecSpecificOptions, key, strlen(key), value)) {
            fprintf(stderr, "ERROR: Failed to copy codec specific option: %s = %s\n", key, value);
            return AVIF_FALSE;
        }
    }
    return AVIF_TRUE;
}

#define INVALID_QUALITY (-1)
#define DEFAULT_QUALITY 60 // Maps to a quantizer (QP) of 25.
#define DEFAULT_QUALITY_GAIN_MAP DEFAULT_QUALITY
#define PROGRESSIVE_WORST_QUALITY 10 // Not doing automatic layered encoding below this quality
#define PROGRESSIVE_START_QUALITY 2  // First layer use this quality

static avifBool avifEncodeUpdateEncoderSettings(avifEncoder * encoder, const avifInputFileSettings * settings)
{
    if (!settings) {
        return AVIF_TRUE;
    }

    if (settings->quality.set) {
        encoder->quality = settings->quality.value;
    }
    if (settings->qualityAlpha.set) {
        encoder->qualityAlpha = settings->qualityAlpha.value;
    }
    if (settings->minQuantizer.set) {
        encoder->minQuantizer = settings->minQuantizer.value;
    }
    if (settings->maxQuantizer.set) {
        encoder->maxQuantizer = settings->maxQuantizer.value;
    }
    if (settings->minQuantizerAlpha.set) {
        encoder->minQuantizerAlpha = settings->minQuantizerAlpha.value;
    }
    if (settings->maxQuantizerAlpha.set) {
        encoder->maxQuantizerAlpha = settings->maxQuantizerAlpha.value;
    }
    if (settings->tileRowsLog2.set) {
        encoder->tileRowsLog2 = settings->tileRowsLog2.value;
    }
    if (settings->tileColsLog2.set) {
        encoder->tileColsLog2 = settings->tileColsLog2.value;
    }
    if (settings->autoTiling.set) {
        encoder->autoTiling = settings->autoTiling.value;
    }
    if (settings->scalingMode.set) {
        encoder->scalingMode = settings->scalingMode.value;
    }
    for (int i = 0; i < settings->codecSpecificOptions.count; ++i) {
        if (avifEncoderSetCodecSpecificOption(encoder, settings->codecSpecificOptions.keys[i], settings->codecSpecificOptions.values[i]) !=
            AVIF_RESULT_OK) {
            fprintf(stderr,
                    "ERROR: Failed to set codec specific option: %s = %s\n",
                    settings->codecSpecificOptions.keys[i],
                    settings->codecSpecificOptions.values[i]);
            return AVIF_FALSE;
        }
    }

    return AVIF_TRUE;
}

static avifBool avifEncoderVerifyImageCompatibility(const avifImage * refImage,
                                                    const avifImage * testImage,
                                                    const char * seriesType,
                                                    const char * filename)
{
    // Verify that this frame's properties matches the first frame's properties
    if ((refImage->width != testImage->width) || (refImage->height != testImage->height)) {
        fprintf(stderr,
                "ERROR: Image %s dimensions mismatch, [%ux%u] vs [%ux%u]: %s\n",
                seriesType,
                refImage->width,
                refImage->height,
                testImage->width,
                testImage->height,
                filename);
        return AVIF_FALSE;
    }
    if (refImage->depth != testImage->depth) {
        fprintf(stderr, "ERROR: Image %s depth mismatch, [%u] vs [%u]: %s\n", seriesType, refImage->depth, testImage->depth, filename);
        return AVIF_FALSE;
    }
    if ((refImage->colorPrimaries != testImage->colorPrimaries) ||
        (refImage->transferCharacteristics != testImage->transferCharacteristics) ||
        (refImage->matrixCoefficients != testImage->matrixCoefficients)) {
        fprintf(stderr,
                "ERROR: Image %s CICP mismatch, [%u/%u/%u] vs [%u/%u/%u]: %s\n",
                seriesType,
                refImage->colorPrimaries,
                refImage->matrixCoefficients,
                refImage->transferCharacteristics,
                testImage->colorPrimaries,
                testImage->transferCharacteristics,
                testImage->matrixCoefficients,
                filename);
        return AVIF_FALSE;
    }
    if (refImage->yuvRange != testImage->yuvRange) {
        fprintf(stderr,
                "ERROR: Image %s range mismatch, [%s] vs [%s]: %s\n",
                seriesType,
                (refImage->yuvRange == AVIF_RANGE_FULL) ? "Full" : "Limited",
                (testImage->yuvRange == AVIF_RANGE_FULL) ? "Full" : "Limited",
                filename);
        return AVIF_FALSE;
    }

    return AVIF_TRUE;
}

static avifBool avifEncodeRestOfImageSequence(avifEncoder * encoder,
                                              const avifSettings * settings,
                                              avifInput * input,
                                              int imageIndex,
                                              const avifImage * firstImage)
{
    avifBool success = AVIF_FALSE;
    avifImage * nextImage = NULL;
    const avifInputFileSettings * nextSettings = NULL;

    const avifInputFile * nextFile;
    while ((nextFile = avifInputGetFile(input, imageIndex)) != NULL) {
        uint64_t nextDurationInTimescales = nextFile->duration ? nextFile->duration : settings->outputTiming.duration;

        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.
        // Ignore gain map as it's not supported for sequences.
        if (!avifInputReadImage(input,
                                imageIndex,
                                /*ignoreColorProfile=*/AVIF_TRUE,
                                /*ignoreExif=*/AVIF_TRUE,
                                /*ignoreXMP=*/AVIF_TRUE,
                                /*allowChangingCicp=*/AVIF_FALSE,
                                /*ignoreGainMap=*/AVIF_TRUE,
                                nextImage,
                                &nextSettings,
                                /*outDepth=*/NULL,
                                /*sourceIsRGB=*/NULL,
                                /*sourceTiming=*/NULL,
                                settings->chromaDownsampling,
                                settings->inputFormat)) {
            goto cleanup;
        }
        if (!avifEncoderVerifyImageCompatibility(firstImage, nextImage, "sequence", avifPrettyFilename(nextFile->filename))) {
            goto cleanup;
        }
        if (!avifEncodeUpdateEncoderSettings(encoder, nextSettings)) {
            goto cleanup;
        }

        char manualTilingStr[128];
        snprintf(manualTilingStr, sizeof(manualTilingStr), "tileRowsLog2 [%d], tileColsLog2 [%d]", encoder->tileRowsLog2, encoder->tileColsLog2);

        printf(" * Encoding frame %d [%" PRIu64 "/%" PRIu64 " ts] color quality [%d (%s)], alpha quality [%d (%s)], %s: %s\n",
               imageIndex,
               nextDurationInTimescales,
               settings->outputTiming.timescale,
               encoder->quality,
               qualityString(encoder->quality),
               encoder->qualityAlpha,
               qualityString(encoder->qualityAlpha),
               encoder->autoTiling ? "automatic tiling" : manualTilingStr,
               avifPrettyFilename(nextFile->filename));

        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,
                                             avifInput * input,
                                             int layerIndex,
                                             const avifImage * firstImage)
{
    avifBool success = AVIF_FALSE;
    int layers = encoder->extraLayerCount + 1;
    // --progressive only allows one input, so directly read from it.
    int targetQuality = (settings->overrideQuality != INVALID_QUALITY) ? settings->overrideQuality
                                                                       : input->files[0].settings.quality.value;

    avifImage * nextImage = NULL;
    const avifImage * encodingImage = firstImage;
    const avifInputFileSettings * nextSettings = NULL;

    if (settings->progressive && avifInputHasRemainingData(input, layerIndex)) {
        fprintf(stderr, "ERROR: Automatic layered encoding can only have one input image.\n");
        goto cleanup;
    }

    while (layerIndex < layers) {
        if (settings->progressive) {
            // reversed lerp, so that last layer reaches exact targetQuality
            encoder->quality = targetQuality - (targetQuality - PROGRESSIVE_START_QUALITY) *
                                                   (encoder->extraLayerCount - layerIndex) / encoder->extraLayerCount;
        } else {
            const avifInputFile * nextFile = avifInputGetFile(input, layerIndex);
            // main() function should set number of layers to number of input,
            // so nextFile should not be NULL.
            assert(nextFile);

            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.
            // Ignore gain map because the two features are currently incompatible.
            if (!avifInputReadImage(input,
                                    layerIndex,
                                    /*ignoreColorProfile=*/AVIF_TRUE,
                                    /*ignoreExif=*/AVIF_TRUE,
                                    /*ignoreXMP=*/AVIF_TRUE,
                                    !settings->cicpExplicitlySet,
                                    /*ignoreGainMap=*/AVIF_TRUE,
                                    nextImage,
                                    &nextSettings,
                                    /*outDepth=*/NULL,
                                    /*sourceIsRGB=*/NULL,
                                    /*sourceTiming=*/NULL,
                                    settings->chromaDownsampling,
                                    settings->inputFormat)) {
                goto cleanup;
            }
            // frameIter is NULL if y4m reached end, so single frame y4m is still supported.
            if (input->frameIter) {
                fprintf(stderr,
                        "ERROR: Layered encoding does not support input with multiple frames: %s.\n",
                        avifPrettyFilename(nextFile->filename));
                goto cleanup;
            }
            if (!avifEncoderVerifyImageCompatibility(firstImage, nextImage, "layer", avifPrettyFilename(nextFile->filename))) {
                goto cleanup;
            }
            if (!avifEncodeUpdateEncoderSettings(encoder, nextSettings)) {
                goto cleanup;
            }
            encodingImage = nextImage;
        }

        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, encodingImage, 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;
    }

    // main() function should set number of layers to number of input,
    // so there should be no input left.
    assert(!avifInputHasRemainingData(input, layerIndex));
    success = AVIF_TRUE;
cleanup:
    if (nextImage) {
        avifImageDestroy(nextImage);
    }
    return success;
}

static avifBool avifEncodeImagesFixedQuality(const avifSettings * settings,
                                             avifInput * input,
                                             const avifInputFile * firstFile,
                                             const avifImage * firstImage,
                                             const avifImage * const * gridCells,
                                             avifRWData * encoded,
                                             avifEncodedByteSizes * byteSizes)
{
    avifBool success = AVIF_FALSE;
    avifRWDataFree(encoded);
    avifEncoder * encoder = avifEncoderCreate();
    if (!encoder) {
        fprintf(stderr, "ERROR: Out of memory\n");
        goto cleanup;
    }

    encoder->maxThreads = settings->jobs;
    encoder->codecChoice = settings->codecChoice;
    encoder->speed = settings->speed;
    encoder->timescale = settings->outputTiming.timescale;
    encoder->keyframeInterval = settings->keyframeInterval;
    encoder->repetitionCount = settings->repetitionCount;
    encoder->headerFormat = settings->headerFormat;
    encoder->creationTime = settings->creationTime;
    encoder->modificationTime = settings->modificationTime;
    encoder->extraLayerCount = settings->layers - 1;
    if (!avifEncodeUpdateEncoderSettings(encoder, &firstFile->settings)) {
        goto cleanup;
    }

    if (settings->overrideQuality != INVALID_QUALITY) {
        encoder->quality = settings->overrideQuality;
    }
    if (settings->overrideQualityAlpha != INVALID_QUALITY) {
        encoder->qualityAlpha = settings->overrideQualityAlpha;
    }
#if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
    if (settings->qualityGainMap != INVALID_QUALITY) {
        encoder->qualityGainMap = settings->qualityGainMap;
    }
#endif

    if (input->requestedDepth == 8 && input->requestedDepthExtension == 8) {
        encoder->sampleTransformRecipe = AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B;
    } else if (input->requestedDepth == 12 && input->requestedDepthExtension == 4) {
        encoder->sampleTransformRecipe = AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B;
    } else if (input->requestedDepth == 12 && input->requestedDepthExtension == 8) {
        encoder->sampleTransformRecipe = AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_8B_OVERLAP_4B;
    } else if (input->requestedDepthExtension != 0) {
        fprintf(stderr, "ERROR: Unsupported bit depth extension scheme: %d + %d\n", input->requestedDepth, input->requestedDepthExtension);
        goto cleanup;
    }

    const char * const codecName = avifCodecName(settings->codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE);
    char speedStr[16];
    if (settings->speed == AVIF_SPEED_DEFAULT) {
        strcpy(speedStr, "default");
    } else {
        snprintf(speedStr, sizeof(speedStr), "%d", settings->speed);
    }
    char gainMapStr[100] = { 0 };
#if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
    if (firstImage->gainMap && firstImage->gainMap->image) {
        snprintf(gainMapStr, sizeof(gainMapStr), ", gain map quality [%d (%s)]", encoder->qualityGainMap, qualityString(encoder->qualityGainMap));
    }
#endif

    char manualTilingStr[128];
    snprintf(manualTilingStr, sizeof(manualTilingStr), "tileRowsLog2 [%d], tileColsLog2 [%d]", encoder->tileRowsLog2, encoder->tileColsLog2);

    printf("Encoding with initial settings: codec '%s' speed [%s], color quality [%d (%s)], alpha quality [%d (%s)]%s, %s, %d worker thread(s), please wait...\n",
           codecName ? codecName : "none",
           speedStr,
           encoder->quality,
           qualityString(encoder->quality),
           encoder->qualityAlpha,
           qualityString(encoder->qualityAlpha),
           gainMapStr,
           encoder->autoTiling ? "automatic tiling" : manualTilingStr,
           settings->jobs);
    if (settings->progressive) {
        // If --progressive is specified and the color quality is less than
        // PROGRESSIVE_WORST_QUALITY, the main() function returns an error and
        // we should not reach here.
        assert(encoder->quality >= PROGRESSIVE_WORST_QUALITY);
        // Encode the base layer with a very low quality to ensure a small encoded size.
        encoder->quality = 2;
        // Low alpha quality resulted in weird artifact, so we don't do it.
    }

    if (settings->layers > 1) {
        printf(" * Encoding layer %d: color quality [%d (%s)], alpha quality [%d (%s)]\n",
               0,
               encoder->quality,
               qualityString(encoder->quality),
               encoder->qualityAlpha,
               qualityString(encoder->qualityAlpha));
    }

    if (settings->gridDimsPresent) {
        const avifResult addImageResult =
            avifEncoderAddImageGrid(encoder, settings->gridDims[0], settings->gridDims[1], gridCells, AVIF_ADD_IMAGE_FLAG_SINGLE);
        if (addImageResult != AVIF_RESULT_OK) {
            fprintf(stderr, "ERROR: Failed to encode image grid: %s\n", avifResultToString(addImageResult));
            goto cleanup;
        }
    } else {
        int imageIndex = 1; // firstImage with imageIndex 0 is already available.

        avifAddImageFlags addImageFlags = AVIF_ADD_IMAGE_FLAG_NONE;
        if (!avifInputHasRemainingData(input, imageIndex) && (settings->layers == 1)) {
            addImageFlags |= AVIF_ADD_IMAGE_FLAG_SINGLE;
        }

        uint64_t firstDurationInTimescales = firstFile->duration ? firstFile->duration : settings->outputTiming.duration;
        if ((firstFile->filename == AVIF_FILENAME_STDIN && settings->inputFormat == AVIF_APP_FILE_FORMAT_Y4M) ||
            (settings->layers == 1 && input->filesCount > 1)) {
            snprintf(manualTilingStr,
                     sizeof(manualTilingStr),
                     "tileRowsLog2 [%d], tileColsLog2 [%d]",
                     encoder->tileRowsLog2,
                     encoder->tileColsLog2);

            printf(" * Encoding frame %d [%" PRIu64 "/%" PRIu64 " ts] color quality [%d (%s)], alpha quality [%d (%s)], %s: %s\n",
                   0,
                   firstDurationInTimescales,
                   settings->outputTiming.timescale,
                   encoder->quality,
                   qualityString(encoder->quality),
                   encoder->qualityAlpha,
                   qualityString(encoder->qualityAlpha),
                   encoder->autoTiling ? "automatic tiling" : manualTilingStr,
                   avifPrettyFilename(firstFile->filename));
        }
        const avifResult addImageResult = avifEncoderAddImage(encoder, firstImage, firstDurationInTimescales, addImageFlags);
        if (addImageResult != AVIF_RESULT_OK) {
            fprintf(stderr, "ERROR: Failed to encode image: %s\n", avifResultToString(addImageResult));
            goto cleanup;
        }

        if (settings->layers > 1) {
            if (!avifEncodeRestOfLayeredImage(encoder, settings, input, imageIndex, firstImage)) {
                goto cleanup;
            }
        } else {
            // Not generating a single-image grid: Use all remaining input files as subsequent
            // frames.
            if (!avifEncodeRestOfImageSequence(encoder, settings, input, imageIndex, firstImage)) {
                goto cleanup;
            }
        }
    }

    const avifResult finishResult = avifEncoderFinish(encoder, encoded);
    if (finishResult != AVIF_RESULT_OK) {
        fprintf(stderr, "ERROR: Failed to finish encoding: %s\n", avifResultToString(finishResult));
        goto cleanup;
    }
    success = AVIF_TRUE;
    byteSizes->colorSizeBytes = encoder->ioStats.colorOBUSize;
    byteSizes->alphaSizeBytes = encoder->ioStats.alphaOBUSize;
#if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
    byteSizes->gainMapSizeBytes = avifEncoderGetGainMapSizeBytes(encoder);
#endif

cleanup:
    if (encoder) {
        if (!success) {
            avifDumpDiagnostics(&encoder->diag);
        }
        avifEncoderDestroy(encoder);
    }
    return success;
}

static avifBool avifEncodeImages(avifSettings * settings,
                                 avifInput * input,
                                 const avifInputFile * firstFile,
                                 const avifImage * firstImage,
                                 const avifImage * const * gridCells,
                                 avifRWData * encoded,
                                 avifEncodedByteSizes * byteSizes)
{
    if (settings->targetSize == -1) {
        return avifEncodeImagesFixedQuality(settings, input, firstFile, firstImage, gridCells, encoded, byteSizes);
    }

    avifBool hasGainMap = AVIF_FALSE;
    avifBool allQualitiesConstrained = settings->qualityIsConstrained && settings->qualityAlphaIsConstrained;
#if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
    hasGainMap = (firstImage->gainMap && firstImage->gainMap->image);
    if (hasGainMap) {
        allQualitiesConstrained = allQualitiesConstrained && settings->qualityGainMapIsConstrained;
    }
#endif

    if (allQualitiesConstrained) {
        fprintf(stderr, "ERROR: --target-size is used with constrained --qcolor and --qalpha %s\n", (hasGainMap ? "and --qgain-map" : ""));
        return AVIF_FALSE;
    }

    printf("Starting a binary search to find the %s%s generating the encoded image size closest to %d bytes, please wait...\n",
           settings->qualityAlphaIsConstrained ? "color quality"
                                               : (settings->qualityIsConstrained ? "alpha quality" : "color and alpha qualities"),
           (hasGainMap && !settings->qualityGainMapIsConstrained) ? " and gain map quality" : "",
           settings->targetSize);
    const size_t targetSize = (size_t)settings->targetSize;

    // TODO(yguyon): Use quantizer instead of quality because quantizer range is smaller (faster binary search).
    int closestQuality = INVALID_QUALITY;
    avifRWData closestEncoded = { NULL, 0 };
    size_t closestSizeDiff = 0;
    avifEncodedByteSizes closestByteSizes = { 0, 0, 0 };

    int minQuality = settings->progressive ? PROGRESSIVE_WORST_QUALITY : AVIF_QUALITY_WORST; // inclusive
    int maxQuality = AVIF_QUALITY_BEST;                                                      // inclusive
    while (minQuality <= maxQuality) {
        const int quality = (minQuality + maxQuality) / 2;
        if (!settings->qualityIsConstrained) {
            settings->overrideQuality = quality;
        }
        if (!settings->qualityAlphaIsConstrained) {
            settings->overrideQualityAlpha = quality;
        }
        if (!settings->qualityGainMapIsConstrained) {
            settings->qualityGainMap = quality;
        }

        if (!avifEncodeImagesFixedQuality(settings, input, firstFile, firstImage, gridCells, encoded, byteSizes)) {
            avifRWDataFree(&closestEncoded);
            return AVIF_FALSE;
        }
        printf("Encoded image of size %" AVIF_FMT_ZU " bytes.\n", encoded->size);

        if (encoded->size == targetSize) {
            return AVIF_TRUE;
        }

        size_t sizeDiff;
        if (encoded->size > targetSize) {
            sizeDiff = encoded->size - targetSize;
            maxQuality = quality - 1;
        } else {
            sizeDiff = targetSize - encoded->size;
            minQuality = quality + 1;
        }

        if ((closestQuality == INVALID_QUALITY) || (sizeDiff < closestSizeDiff)) {
            closestQuality = quality;
            avifRWDataFree(&closestEncoded);
            closestEncoded = *encoded;
            encoded->size = 0;
            encoded->data = NULL;
            closestSizeDiff = sizeDiff;
            closestByteSizes = *byteSizes;
        }
    }

    if (!settings->qualityIsConstrained) {
        settings->overrideQuality = closestQuality;
    }
    if (!settings->qualityAlphaIsConstrained) {
        settings->overrideQualityAlpha = closestQuality;
    }
    avifRWDataFree(encoded);
    *encoded = closestEncoded;
    *byteSizes = closestByteSizes;
    printf("Kept the encoded image of size %" AVIF_FMT_ZU " bytes generated with ", encoded->size);
    if (!settings->qualityIsConstrained) {
        printf("color quality %d", settings->overrideQuality);
    }
    if (!settings->qualityAlphaIsConstrained) {
        if (!settings->qualityIsConstrained) {
            printf(" and ");
        }
        printf("alpha quality %d", settings->overrideQualityAlpha);
    }
    printf(".\n");
    return AVIF_TRUE;
}

static void avifInputAdd(avifInput * input, const char * filePath, uint64_t duration, avifInputFileSettings * pendingSettings)
{
    input->files[input->filesCount].filename = filePath;
    input->files[input->filesCount].duration = duration;
    input->files[input->filesCount].settings = *pendingSettings;
    memset(pendingSettings, 0, sizeof(*pendingSettings));
    ++input->filesCount;
}

static int quantizerToQuality(int minQuantizer, int maxQuantizer)
{
    // When calculating the average quantizer, discard the fractional part for a
    // bias toward better quality.
    const int quantizer = (minQuantizer + maxQuantizer) / 2;
    const int quality = ((63 - quantizer) * 100 + 31) / 63;
    return quality;
}

int main(int argc, char * argv[])
{
    if (argc < 2) {
        syntaxShort();
        return 1;
    }

    const char * outputFilename = NULL;

    avifInput input;
    memset(&input, 0, sizeof(input));
    input.files = malloc(sizeof(avifInputFile) * argc);
    if (input.files == NULL) {
        fprintf(stderr, "ERROR: memory allocation failure\n");
        return 1;
    }
    input.requestedFormat = AVIF_PIXEL_FORMAT_NONE; // AVIF_PIXEL_FORMAT_NONE is used as a sentinel for "auto"

    // See here for the discussion on the semi-arbitrary defaults for speed:
    //     https://github.com/AOMediaCodec/libavif/issues/440

    int returnCode = 1;
    avifBool noOverwrite = AVIF_FALSE;
    avifSettings settings;
    memset(&settings, 0, sizeof(settings));
    settings.codecChoice = AVIF_CODEC_CHOICE_AUTO;
    settings.jobs = -1;
    settings.targetSize = -1;
    settings.qualityIsConstrained = AVIF_FALSE;
    settings.qualityAlphaIsConstrained = AVIF_FALSE;
    settings.overrideQuality = INVALID_QUALITY;
    settings.overrideQualityAlpha = INVALID_QUALITY;
    settings.qualityGainMap = DEFAULT_QUALITY_GAIN_MAP;
    settings.progressive = AVIF_FALSE;
    settings.layered = AVIF_FALSE;
    settings.layers = 0;
    settings.speed = 6;
    settings.headerFormat = AVIF_HEADER_DEFAULT;
    settings.creationTime = 0;
    settings.modificationTime = 0;
    settings.repetitionCount = AVIF_REPETITION_COUNT_INFINITE;
    settings.keyframeInterval = 0;
    settings.ignoreExif = AVIF_FALSE;
    settings.ignoreXMP = AVIF_FALSE;
    settings.ignoreColorProfile = AVIF_FALSE;
    settings.ignoreGainMap = AVIF_FALSE;
    settings.cicpExplicitlySet = AVIF_FALSE;
    settings.inputFormat = AVIF_APP_FILE_FORMAT_UNKNOWN;

    avifInputFileSettings pendingSettings;
    memset(&pendingSettings, 0, sizeof(pendingSettings));

    avifBool cropConversionRequired = AVIF_FALSE;
    uint8_t irotAngle = 0xff; // sentinel value indicating "unused"
    uint8_t imirAxis = 0xff;  // sentinel value indicating "unused"
    avifRange requestedRange = AVIF_RANGE_FULL;
    avifBool lossless = AVIF_FALSE;
    avifImage * image = NULL;
    avifRWData raw = AVIF_DATA_EMPTY;
    avifRWData exifOverride = AVIF_DATA_EMPTY;
    avifRWData xmpOverride = AVIF_DATA_EMPTY;
    avifRWData iccOverride = AVIF_DATA_EMPTY;
    avifBool premultiplyAlpha = AVIF_FALSE;
    uint32_t gridCellCount = 0;
    avifImage ** gridCells = NULL;
    avifImage * gridSplitImage = NULL; // used for cleanup tracking

    // By default, the color profile itself is unspecified, so CP/TC are set (to 2) accordingly.
    // However, if the end-user doesn't specify any CICP, we will convert to YUV using BT601
    // coefficients anyway (as MC:2 falls back to MC:5/6), so we might as well signal it explicitly.
    // See: ISO/IEC 23000-22:2019 Amendment 2, or the comment in avifCalcYUVCoefficients()
    settings.colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED;
    settings.transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED;
    settings.matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601;
    settings.chromaDownsampling = AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC;

    int argIndex = 1;
    while (argIndex < argc) {
        const char * arg = argv[argIndex];

        if (!strcmp(arg, "--")) {
            // Stop parsing flags, everything after this is positional arguments
            ++argIndex;
            // Parse additional positional arguments if any
            while (argIndex < argc) {
                arg = argv[argIndex];
                avifInputAdd(&input, /*filePath=*/arg, settings.outputTiming.duration, &pendingSettings);
                ++argIndex;
            }
            break;
        } else if (!strcmp(arg, "-h") || !strcmp(arg, "--help")) {
            syntaxLong();
            returnCode = 0;
            goto cleanup;
        } else if (!strcmp(arg, "-V") || !strcmp(arg, "--version")) {
            avifPrintVersions();
            returnCode = 0;
            goto cleanup;
        } else if (!strcmp(arg, "--no-overwrite")) {
            noOverwrite = AVIF_TRUE;
        } else if (!strcmp(arg, "-j") || !strcmp(arg, "--jobs")) {
            NEXTARG();
            if (!strcmp(arg, "all")) {
                settings.jobs = avifQueryCPUCount();
            } else {
                settings.jobs = atoi(arg);
                if (settings.jobs < 1) {
                    settings.jobs = 1;
                }
            }
        } else if (!strcmp(arg, "--stdin")) {
            avifInputAdd(&input, AVIF_FILENAME_STDIN, settings.outputTiming.duration, &pendingSettings);

            if (input.filesCount == 2) {
                // The only valid pattern here is
                //   avifenc [settings...] output.avif [more_settings...] --stdin
                // because multiple input files are forbidden with --stdin and
                // there must be an output file specified with or without --output.
                // Invalid patterns will be rejected later on.

                // Merge all the settings seen so far and associate them with stdin only.
                if (!avifInputFileSettingsOverwrite(/*dst=*/&input.files[0].settings, /*src=*/&input.files[1].settings)) {
                    fprintf(stderr, "ERROR: memory allocation failure\n");
                    goto cleanup;
                }
                avifCodecSpecificOptionsFree(&input.files[1].settings.codecSpecificOptions);
                input.files[1].settings = input.files[0].settings;
                memset(&input.files[0].settings, 0, sizeof(input.files[0].settings));

                // Swap the file and stdin so that the file is last and picked as output later on.
                const avifInputFile output = input.files[0];
                input.files[0] = input.files[1];
                input.files[1] = output;

                // This should now be equivalent to:
                //   avifenc [settings...] [more_settings...] --stdin output.avif

            } else if (input.filesCount > 2) {
                fprintf(stderr, "ERROR: there cannot be any other input if --stdin is specified\n");
                goto cleanup;
            }
        } else if (!strcmp(arg, "--input-format")) {
            NEXTARG();
            if (!strcmp(arg, "jpg") || !strcmp(arg, "jpeg")) {
                settings.inputFormat = AVIF_APP_FILE_FORMAT_JPEG;
            } else if (!strcmp(arg, "png")) {
                settings.inputFormat = AVIF_APP_FILE_FORMAT_PNG;
            } else if (!strcmp(arg, "y4m")) {
                settings.inputFormat = AVIF_APP_FILE_FORMAT_Y4M;
            } else if (!strcmp(arg, "auto")) {
                settings.inputFormat = AVIF_APP_FILE_FORMAT_UNKNOWN;
            } else {
                fprintf(stderr, "ERROR: invalid stdin file format (expected one of jpeg/png/y4m/auto): %s\n", arg);
                goto cleanup;
            }
        } else if (!strcmp(arg, "-o") || !strcmp(arg, "--output")) {
            NEXTARG();
            outputFilename = arg;
#if defined(AVIF_ENABLE_EXPERIMENTAL_MINI)
        } else if (!strcmp(arg, "--mini")) {
            settings.headerFormat |= AVIF_HEADER_MINI;
#endif // AVIF_ENABLE_EXPERIMENTAL_MINI
        } else if (!strcmp(arg, "-d") || !strcmp(arg, "--depth")) {
            NEXTARG();
            uint32_t depthValues[2] = { 0, 0 };
            if (parseU32List(depthValues, 1, arg, ',') || parseU32List(depthValues, 2, arg, ',')) {
                if ((depthValues[0] != 8 && depthValues[0] != 10 && depthValues[0] != 12) ||
                    (depthValues[1] != 0 && depthValues[1] != 4 && depthValues[1] != 8)) {
                    fprintf(stderr, "ERROR: unsupported depth or depth extension: %s\n", arg);
                    goto cleanup;
                }
                input.requestedDepth = (int)depthValues[0];
                input.requestedDepthExtension = (int)depthValues[1];
            } else {
                fprintf(stderr, "ERROR: invalid depth and depth extension combination: %s\n", arg);
                goto cleanup;
            }
        } else if (!strcmp(arg, "-y") || !strcmp(arg, "--yuv")) {
            NEXTARG();
            if (!strcmp(arg, "444")) {
                input.requestedFormat = AVIF_PIXEL_FORMAT_YUV444;
            } else if (!strcmp(arg, "422")) {
                input.requestedFormat = AVIF_PIXEL_FORMAT_YUV422;
            } else if (!strcmp(arg, "420")) {
                input.requestedFormat = AVIF_PIXEL_FORMAT_YUV420;
            } else if (!strcmp(arg, "400")) {
                input.requestedFormat = AVIF_PIXEL_FORMAT_YUV400;
            } else {
                fprintf(stderr, "ERROR: invalid format: %s\n", arg);
                goto cleanup;
            }
        } else if (!strcmp(arg, "-k") || !strcmp(arg, "--keyframe")) {
            NEXTARG();
            settings.keyframeInterval = atoi(arg);
        } else if (!strcmp(arg, "-q") || !strcmp(arg, "--qcolor") || strpre(arg, "-q:") || strpre(arg, "--qcolor:")) {
            // For compatibility reason unsuffixed flags always apply to all input (as if they appear before first input).
            // Print a warning if unsuffixed flag appears after input file.
            avifOptionSuffixType type = parseOptionSuffix(arg, /*warnNoSuffix=*/input.filesCount != 0);
            if (type == AVIF_OPTION_SUFFIX_INVALID) {
                goto cleanup;
            }
            NEXTARG();
            int quality = atoi(arg);
            if (quality < AVIF_QUALITY_WORST) {
                quality = AVIF_QUALITY_WORST;
            }
            if (quality > AVIF_QUALITY_BEST) {
                quality = AVIF_QUALITY_BEST;
            }
            if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) {
                pendingSettings.quality = intSettingsEntryOf(quality);
            } else {
                input.files[0].settings.quality = intSettingsEntryOf(quality);
            }
        } else if (!strcmp(arg, "--qalpha") || strpre(arg, "--qalpha:")) {
            avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0);
            if (type == AVIF_OPTION_SUFFIX_INVALID) {
                goto cleanup;
            }
            NEXTARG();
            int qualityAlpha = atoi(arg);
            if (qualityAlpha < AVIF_QUALITY_WORST) {
                qualityAlpha = AVIF_QUALITY_WORST;
            }
            if (qualityAlpha > AVIF_QUALITY_BEST) {
                qualityAlpha = AVIF_QUALITY_BEST;
            }
            if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) {
                pendingSettings.qualityAlpha = intSettingsEntryOf(qualityAlpha);
            } else {
                input.files[0].settings.qualityAlpha = intSettingsEntryOf(qualityAlpha);
            }
        } else if (!strcmp(arg, "--min") || strpre(arg, "--min:")) {
            avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0);
            if (type == AVIF_OPTION_SUFFIX_INVALID) {
                goto cleanup;
            }
            NEXTARG();
            int minQuantizer = atoi(arg);
            if (minQuantizer < AVIF_QUANTIZER_BEST_QUALITY) {
                minQuantizer = AVIF_QUANTIZER_BEST_QUALITY;
            }
            if (minQuantizer > AVIF_QUANTIZER_WORST_QUALITY) {
                minQuantizer = AVIF_QUANTIZER_WORST_QUALITY;
            }
            if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) {
                pendingSettings.minQuantizer = intSettingsEntryOf(minQuantizer);
            } else {
                input.files[0].settings.minQuantizer = intSettingsEntryOf(minQuantizer);
            }
        } else if (!strcmp(arg, "--max") || strpre(arg, "--max:")) {
            avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0);
            if (type == AVIF_OPTION_SUFFIX_INVALID) {
                goto cleanup;
            }
            NEXTARG();
            int maxQuantizer = atoi(arg);
            if (maxQuantizer < AVIF_QUANTIZER_BEST_QUALITY) {
                maxQuantizer = AVIF_QUANTIZER_BEST_QUALITY;
            }
            if (maxQuantizer > AVIF_QUANTIZER_WORST_QUALITY) {
                maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY;
            }
            if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) {
                pendingSettings.maxQuantizer = intSettingsEntryOf(maxQuantizer);
            } else {
                input.files[0].settings.maxQuantizer = intSettingsEntryOf(maxQuantizer);
            }
        } else if (!strcmp(arg, "--minalpha") || strpre(arg, "--minalpha:")) {
            avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0);
            if (type == AVIF_OPTION_SUFFIX_INVALID) {
                goto cleanup;
            }
            NEXTARG();
            int minQuantizerAlpha = atoi(arg);
            if (minQuantizerAlpha < AVIF_QUANTIZER_BEST_QUALITY) {
                minQuantizerAlpha = AVIF_QUANTIZER_BEST_QUALITY;
            }
            if (minQuantizerAlpha > AVIF_QUANTIZER_WORST_QUALITY) {
                minQuantizerAlpha = AVIF_QUANTIZER_WORST_QUALITY;
            }
            if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) {
                pendingSettings.minQuantizerAlpha = intSettingsEntryOf(minQuantizerAlpha);
            } else {
                input.files[0].settings.minQuantizerAlpha = intSettingsEntryOf(minQuantizerAlpha);
            }
        } else if (!strcmp(arg, "--maxalpha") || strpre(arg, "--maxalpha:")) {
            avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0);
            if (type == AVIF_OPTION_SUFFIX_INVALID) {
                goto cleanup;
            }
            NEXTARG();
            int maxQuantizerAlpha = atoi(arg);
            if (maxQuantizerAlpha < AVIF_QUANTIZER_BEST_QUALITY) {
                maxQuantizerAlpha = AVIF_QUANTIZER_BEST_QUALITY;
            }
            if (maxQuantizerAlpha > AVIF_QUANTIZER_WORST_QUALITY) {
                maxQuantizerAlpha = AVIF_QUANTIZER_WORST_QUALITY;
            }
            if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) {
                pendingSettings.maxQuantizerAlpha = intSettingsEntryOf(maxQuantizerAlpha);
            } else {
                input.files[0].settings.maxQuantizerAlpha = intSettingsEntryOf(maxQuantizerAlpha);
            }
        } else if (!strcmp(arg, "--target-size")) {
            NEXTARG();
            settings.targetSize = atoi(arg);
            if (settings.targetSize < 0) {
                settings.targetSize = -1;
            }
        } else if (!strcmp(arg, "--tilerowslog2") || strpre(arg, "--tilerowslog2:")) {
            avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0);
            if (type == AVIF_OPTION_SUFFIX_INVALID) {
                goto cleanup;
            }
            NEXTARG();
            int tileRowsLog2 = atoi(arg);
            if (tileRowsLog2 < 0) {
                tileRowsLog2 = 0;
            }
            if (tileRowsLog2 > 6) {
                tileRowsLog2 = 6;
            }
            if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) {
                pendingSettings.tileRowsLog2 = intSettingsEntryOf(tileRowsLog2);
            } else {
                input.files[0].settings.tileRowsLog2 = intSettingsEntryOf(tileRowsLog2);
            }
        } else if (!strcmp(arg, "--tilecolslog2") || strpre(arg, "--tilecolslog2:")) {
            avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0);
            if (type == AVIF_OPTION_SUFFIX_INVALID) {
                goto cleanup;
            }
            NEXTARG();
            int tileColsLog2 = atoi(arg);
            if (tileColsLog2 < 0) {
                tileColsLog2 = 0;
            }
            if (tileColsLog2 > 6) {
                tileColsLog2 = 6;
            }
            if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) {
                pendingSettings.tileColsLog2 = intSettingsEntryOf(tileColsLog2);
            } else {
                input.files[0].settings.tileColsLog2 = intSettingsEntryOf(tileColsLog2);
            }
        } else if (!strcmp(arg, "--autotiling") || strpre(arg, "--autotiling:")) {
            avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0);
            if (type == AVIF_OPTION_SUFFIX_INVALID) {
                goto cleanup;
            }
            if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) {
                pendingSettings.autoTiling = boolSettingsEntryOf(AVIF_TRUE);
            } else {
                input.files[0].settings.autoTiling = boolSettingsEntryOf(AVIF_TRUE);
            }
        } else if (!strcmp(arg, "--progressive")) {
            if (settings.layered) {
                fprintf(stderr, "ERROR: Can not use both --progressive and --layered\n");
                goto cleanup;
            }
            settings.progressive = AVIF_TRUE;
        } else if (!strcmp(arg, "--layered")) {
            if (settings.progressive) {
                fprintf(stderr, "ERROR: Can not use both --progressive and --layered\n");
                goto cleanup;
            }
            settings.layered = AVIF_TRUE;
        } else if (!strcmp(arg, "--scaling-mode") || strpre(arg, "--scaling-mode:")) {
            avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0);
            if (type == AVIF_OPTION_SUFFIX_INVALID) {
                goto cleanup;
            }
            NEXTARG();
            uint32_t frac[2] = { 0, 1 };
            if (!(parseU32List(frac, 1, arg, '/') || parseU32List(frac, 2, arg, '/')) || frac[0] > INT32_MAX || frac[1] > INT32_MAX) {
                fprintf(stderr, "ERROR: Invalid scaling mode: %s\n", arg);
                goto cleanup;
            }
            if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) {
                pendingSettings.scalingMode = scalingModeSettingsEntryOf(frac[0], frac[1]);
            } else {
                input.files[0].settings.scalingMode = scalingModeSettingsEntryOf(frac[0], frac[1]);
            }
        } else if (!strcmp(arg, "-g") || !strcmp(arg, "--grid")) {
            NEXTARG();
            if (!parseU32List(settings.gridDims, 2, arg, 'x')) {
                fprintf(stderr, "ERROR: Invalid grid dims: %s\n", arg);
                goto cleanup;
            }
            settings.gridDimsPresent = AVIF_TRUE;
            if ((settings.gridDims[0] == 0) || (settings.gridDims[0] > 256) || (settings.gridDims[1] == 0) ||
                (settings.gridDims[1] > 256)) {
                fprintf(stderr, "ERROR: Invalid grid dims (valid dim range [1-256]): %s\n", arg);
                goto cleanup;
            }
        } else if (!strcmp(arg, "--cicp") || !strcmp(arg, "--nclx")) {
            NEXTARG();
            uint32_t cicp[3];
            if (!parseU32List(cicp, 3, arg, '/')) {
                fprintf(stderr, "ERROR: Invalid CICP value: %s\n", arg);
                goto cleanup;
            }
            settings.colorPrimaries = (avifColorPrimaries)cicp[0];
            settings.transferCharacteristics = (avifTransferCharacteristics)cicp[1];
            settings.matrixCoefficients = (avifMatrixCoefficients)cicp[2];
            settings.cicpExplicitlySet = AVIF_TRUE;
        } else if (!strcmp(arg, "-r") || !strcmp(arg, "--range")) {
            NEXTARG();
            if (!strcmp(arg, "limited") || !strcmp(arg, "l")) {
                requestedRange = AVIF_RANGE_LIMITED;
            } else if (!strcmp(arg, "full") || !strcmp(arg, "f")) {
                requestedRange = AVIF_RANGE_FULL;
            } else {
                fprintf(stderr, "ERROR: Unknown range: %s\n", arg);
                goto cleanup;
            }
        } else if (!strcmp(arg, "-s") || !strcmp(arg, "--speed")) {
            NEXTARG();
            if (!strcmp(arg, "default") || !strcmp(arg, "d")) {
                settings.speed = AVIF_SPEED_DEFAULT;
            } else {
                settings.speed = atoi(arg);
                if (settings.speed > AVIF_SPEED_FASTEST) {
                    settings.speed = AVIF_SPEED_FASTEST;
                }
                if (settings.speed < AVIF_SPEED_SLOWEST) {
                    settings.speed = AVIF_SPEED_SLOWEST;
                }
            }
        } else if (!strcmp(arg, "--exif")) {
            NEXTARG();
            if (!avifReadEntireFile(arg, &exifOverride)) {
                fprintf(stderr, "ERROR: Unable to read Exif metadata: %s\n", arg);
                goto cleanup;
            }
            settings.ignoreExif = AVIF_TRUE;
        } else if (!strcmp(arg, "--xmp")) {
            NEXTARG();
            if (!avifReadEntireFile(arg, &xmpOverride)) {
                fprintf(stderr, "ERROR: Unable to read XMP metadata: %s\n", arg);
                goto cleanup;
            }
            settings.ignoreXMP = AVIF_TRUE;
        } else if (!strcmp(arg, "--icc")) {
            NEXTARG();
            if (!avifReadEntireFile(arg, &iccOverride)) {
                fprintf(stderr, "ERROR: Unable to read ICC profile: %s\n", arg);
                goto cleanup;
            }
            settings.ignoreColorProfile = AVIF_TRUE;
        } else if (!strcmp(arg, "--duration") || strpre(arg, "--duration:")) {
            // --duration is special, we always treat it as suffixed with :u, so don't print warning for it.
            avifOptionSuffixType type = parseOptionSuffix(arg, /*warnNoSuffix=*/AVIF_FALSE);
            if (type == AVIF_OPTION_SUFFIX_INVALID) {
                goto cleanup;
            }
            NEXTARG();
            int durationInt = atoi(arg);
            if (durationInt < 1) {
                fprintf(stderr, "ERROR: Invalid duration: %d\n", durationInt);
                goto cleanup;
            }
            settings.outputTiming.duration = (uint64_t)durationInt;
        } else if (!strcmp(arg, "--timescale") || !strcmp(arg, "--fps")) {
            NEXTARG();
            int timescaleInt = atoi(arg);
            if (timescaleInt < 1) {
                fprintf(stderr, "ERROR: Invalid timescale: %d\n", timescaleInt);
                goto cleanup;
            }
            settings.outputTiming.timescale = (uint64_t)timescaleInt;
        } else if (!strcmp(arg, "--creation-time")) {
            NEXTARG();
            long long creationTime = strtoll(arg, NULL, 0);
            if (creationTime < 0) {
                fprintf(stderr, "ERROR: Invalid creation time: %lld\n", creationTime);
                goto cleanup;
            }
#if LLONG_MAX > UINT64_MAX
            if ((unsigned long long)creationTime > UINT64_MAX) {
                fprintf(stderr, "ERROR: Invalid creation time: %lld\n", creationTime);
                goto cleanup;
            }
#endif
            settings.creationTime = (uint64_t)creationTime;
        } else if (!strcmp(arg, "--modification-time")) {
            NEXTARG();
            long long modificationTime = strtoll(arg, NULL, 0);
            if (modificationTime < 0) {
                fprintf(stderr, "ERROR: Invalid modification time: %lld\n", modificationTime);
                goto cleanup;
            }
#if LLONG_MAX > UINT64_MAX
            if ((unsigned long long)modificationTime > UINT64_MAX) {
                fprintf(stderr, "ERROR: Invalid modification time: %lld\n", modificationTime);
                goto cleanup;
            }
#endif
            settings.modificationTime = (uint64_t)modificationTime;
        } else if (!strcmp(arg, "-c") || !strcmp(arg, "--codec")) {
            NEXTARG();
            settings.codecChoice = avifCodecChoiceFromName(arg);
            if (settings.codecChoice == AVIF_CODEC_CHOICE_AUTO) {
                fprintf(stderr, "ERROR: Unrecognized codec: %s\n", arg);
                goto cleanup;
            } else {
                const char * codecName = avifCodecName(settings.codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE);
                if (codecName == NULL) {
                    fprintf(stderr, "ERROR: Codec cannot encode: %s\n", arg);
                    goto cleanup;
                }
            }
        } else if (!strcmp(arg, "-a") || !strcmp(arg, "--advanced") || strpre(arg, "-a:") || strpre(arg, "--advanced:")) {
            avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0);
            if (type == AVIF_OPTION_SUFFIX_INVALID) {
                goto cleanup;
            }
            NEXTARG();
            avifInputFileSettings * targetSettings =
                (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) ? &pendingSettings : &input.files[0].settings;
            if (!avifCodecSpecificOptionsAdd(&targetSettings->codecSpecificOptions, arg)) {
                fprintf(stderr, "ERROR: Out of memory when setting codec specific option: %s\n", arg);
                goto cleanup;
            }
        } else if (!strcmp(arg, "--ignore-exif")) {
            settings.ignoreExif = AVIF_TRUE;
        } else if (!strcmp(arg, "--ignore-xmp")) {
            settings.ignoreXMP = AVIF_TRUE;
        } else if (!strcmp(arg, "--ignore-profile") || !strcmp(arg, "--ignore-icc")) {
            settings.ignoreColorProfile = AVIF_TRUE;
#if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
        } else if (!strcmp(arg, "--ignore-gain-map")) {
            settings.ignoreGainMap = AVIF_TRUE;
        } else if (!strcmp(arg, "--qgain-map")) {
            NEXTARG();
            int qualityGainMap = atoi(arg);
            if (qualityGainMap < AVIF_QUALITY_WORST) {
                qualityGainMap = AVIF_QUALITY_WORST;
            }
            if (qualityGainMap > AVIF_QUALITY_BEST) {
                qualityGainMap = AVIF_QUALITY_BEST;
            }
            settings.qualityGainMap = qualityGainMap;
            settings.qualityGainMapIsConstrained = AVIF_TRUE;
#endif
        } else if (!strcmp(arg, "--pasp")) {
            NEXTARG();
            if (!parseU32List(settings.paspValues, 2, arg, ',')) {
                fprintf(stderr, "ERROR: Invalid pasp values: %s\n", arg);
                goto cleanup;
            }
            settings.paspPresent = AVIF_TRUE;
        } else if (!strcmp(arg, "--crop")) {
            NEXTARG();
            if (!parseU32List(settings.clapValues, 4, arg, ',')) {
                fprintf(stderr, "ERROR: Invalid crop values: %s\n", arg);
                goto cleanup;
            }
            cropConversionRequired = AVIF_TRUE;
        } else if (!strcmp(arg, "--clap")) {
            NEXTARG();
            if (!parseU32List(settings.clapValues, 8, arg, ',')) {
                fprintf(stderr, "ERROR: Invalid clap values: %s\n", arg);
                goto cleanup;
            }
            settings.clapValid = AVIF_TRUE;
        } else if (!strcmp(arg, "--irot")) {
            NEXTARG();
            irotAngle = (uint8_t)atoi(arg);
            if (irotAngle > 3) {
                fprintf(stderr, "ERROR: Invalid irot angle: %s\n", arg);
                goto cleanup;
            }
        } else if (!strcmp(arg, "--imir")) {
            NEXTARG();
            imirAxis = (uint8_t)atoi(arg);
            if (imirAxis > 1) {
                fprintf(stderr, "ERROR: Invalid imir axis: %s\n", arg);
                goto cleanup;
            }
        } else if (!strcmp(arg, "--clli")) {
            NEXTARG();
            if (!parseU32List(settings.clliValues, 2, arg, ',') || settings.clliValues[0] >= (1u << 16) ||
                settings.clliValues[1] >= (1u << 16)) {
                fprintf(stderr, "ERROR: Invalid clli values: %s\n", arg);
                goto cleanup;
            }
            settings.clliPresent = AVIF_TRUE;
        } else if (!strcmp(arg, "--repetition-count")) {
            NEXTARG();
            if (!strcmp(arg, "infinite")) {
                settings.repetitionCount = AVIF_REPETITION_COUNT_INFINITE;
            } else {
                settings.repetitionCount = atoi(arg);
                if (settings.repetitionCount < 0) {
                    fprintf(stderr, "ERROR: Invalid repetition count: %s\n", arg);
                    goto cleanup;
                }
            }
        } else if (!strcmp(arg, "-l") || !strcmp(arg, "--lossless")) {
            lossless = AVIF_TRUE;
        } else if (!strcmp(arg, "-p") || !strcmp(arg, "--premultiply")) {
            premultiplyAlpha = AVIF_TRUE;
        } else if (!strcmp(arg, "--sharpyuv")) {
            settings.chromaDownsampling = AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV;
        } else if (arg[0] == '-') {
            fprintf(stderr, "ERROR: unrecognized option %s\n\n", arg);
            syntaxLong();
            goto cleanup;
        } else {
            // Positional argument
            avifInputAdd(&input, /*filePath=*/arg, settings.outputTiming.duration, &pendingSettings);
        }

        ++argIndex;
    }

    // Alpha quality defaults to the same value as (color) quality until the first time it's explicitly set.
    for (int i = 0; i < input.filesCount; ++i) {
        avifInputFileSettings * fileSettings = &input.files[i].settings;
        if (fileSettings->qualityAlpha.set) {
            break;
        }
        fileSettings->qualityAlpha = fileSettings->quality;
    }

    if (settings.jobs == -1) {
        settings.jobs = avifQueryCPUCount();
    }

    // Check global lossless parameters and set to default if needed.
    if (lossless) {
        // Pixel format.
        if (input.requestedFormat != AVIF_PIXEL_FORMAT_NONE && input.requestedFormat != AVIF_PIXEL_FORMAT_YUV444 &&
            input.requestedFormat != AVIF_PIXEL_FORMAT_YUV400) {
            fprintf(stderr,
                    "When set, the pixel format can only be 444 in lossless "
                    "mode. 400 also works if the input is grayscale.\n");
            goto cleanup;
        }
        // Codec.
        const char * codecName = avifCodecName(settings.codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE);
        if (codecName && !strcmp(codecName, "rav1e")) {
            fprintf(stderr, "rav1e doesn't support lossless encoding yet: https://github.com/xiph/rav1e/issues/151\n");
            goto cleanup;
        } else if (codecName && !strcmp(codecName, "svt")) {
            char versions[256];
            avifCodecVersions(versions);
            const char svtVersionPrefix[] = "svt [enc]:v";
            const char * svtVersionPrefixPos = strstr(versions, svtVersionPrefix);
            if (svtVersionPrefixPos == NULL) {
                // Make sure the syntax returned by avifCodecVersions() did not change.
                assert(strstr(versions, "svt") == NULL && strstr(versions, "SVT") == NULL);
                // Let the encode fail later because SVT was not included in the build.
            } else {
                const char * svtVersion = svtVersionPrefixPos + strlen(svtVersionPrefix);
                const int svtMajorVersion = atoi(svtVersion);
                if (svtMajorVersion < 3) {
                    fprintf(stderr, "SVT-AV1 lossless support was added in version 3.0.0, current major version is only %d\n", svtMajorVersion);
                    goto cleanup;
                }
            }
        }
        // Range.
        if (requestedRange != AVIF_RANGE_FULL) {
            fprintf(stderr, "Range has to be full in lossless mode.\n");
            goto cleanup;
        }
        // Matrix coefficients.
        if (settings.cicpExplicitlySet) {
            if (settings.matrixCoefficients != AVIF_MATRIX_COEFFICIENTS_IDENTITY &&
                settings.matrixCoefficients != AVIF_MATRIX_COEFFICIENTS_YCGCO_RE &&
                settings.matrixCoefficients != AVIF_MATRIX_COEFFICIENTS_YCGCO_RO) {
                fprintf(stderr, "Matrix coefficients have to be identity, YCgCo-Re, or YCgCo-Ro in lossless mode.\n");
                goto cleanup;
            }
        } else {
            settings.matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY;
        }
        if (settings.progressive) {
            fprintf(stderr, "Automatic layered encoding is unsupported in lossless mode.\n");
        }
    }

    avifInputFileSettings emptySettingsReference;
    memset(&emptySettingsReference, 0, sizeof(emptySettingsReference));

    if (!outputFilename && input.filesCount > 1 && input.files[input.filesCount - 1].filename != AVIF_FILENAME_STDIN) {
        --input.filesCount;
        outputFilename = input.files[input.filesCount].filename;
        if (memcmp(&input.files[input.filesCount].settings, &emptySettingsReference, sizeof(avifInputFileSettings)) != 0) {
            fprintf(stderr, "WARNING: Trailing options with update suffix has no effect. Place them before the input you intend to apply to.\n");
        }
    }

    if (input.filesCount < 1) {
        fprintf(stderr, "ERROR: no input specified\n");
        goto cleanup;
    }

    if (input.files[0].filename == AVIF_FILENAME_STDIN && input.filesCount > 1) {
        fprintf(stderr, "ERROR: there cannot be any other input if --stdin is specified\n");
        goto cleanup;
    }

    if (!outputFilename) {
        fprintf(stderr, "ERROR: no output specified\n");
        goto cleanup;
    }

    if (noOverwrite && fileExists(outputFilename)) {
        fprintf(stderr, "ERROR: output file %s already exists and --no-overwrite was specified\n", outputFilename);
        goto cleanup;
    }

#if defined(_WIN32)
    if (input.files[0].filename == AVIF_FILENAME_STDIN) {
        setmode(fileno(stdin), O_BINARY);
    }
#endif

    if (memcmp(&pendingSettings, &emptySettingsReference, sizeof(avifInputFileSettings)) != 0) {
        fprintf(stderr, "WARNING: Trailing options with update suffix has no effect. Place them before the input you intend to apply to.\n");
    }

    // Check layer config
    if (settings.progressive) {
        assert(!settings.layered);
        if (input.filesCount > 1) {
            fprintf(stderr, "ERROR: --progressive only supports one input.\n");
            goto cleanup;
        }
        // for automatic layered encoding, make a 2 layer AVIF.
        settings.layers = 2;
    } else if (settings.layered) {
        // For manual layered encoding, infer number of layers from input count.
        // For multi-frame input (Y4Ms) layered encoding only use the first frame,
        // so that we can assume number of inputs is number of layers.
        // We don't support changing config halfway encoding on input,
        // therefore encoding layered AVIF with single multi-frame input is not very meaningful.
        if (input.filesCount < 2 || input.filesCount > AVIF_MAX_AV1_LAYER_COUNT) {
            fprintf(stderr, "Encoding layered AVIF required 2 to %d inputs, but got %d.\n", AVIF_MAX_AV1_LAYER_COUNT, input.filesCount);
            goto cleanup;
        }
        settings.layers = input.filesCount;
    } else {
        settings.layers = 1;
    }
    if (settings.layers > 1 && settings.gridDimsPresent) {
        fprintf(stderr, "Layered grid image unimplemented in avifenc.\n");
        goto cleanup;
    }

    for (int i = 0; i < input.filesCount; ++i) {
        avifInputFile * file = &input.files[i];
        avifInputFileSettings * fileSettings = &file->settings;

        // Check tiling parameters.
        // Automatic tiling (autoTiling) and manual tiling (tileRowsLog2, tileColsLog2) are mutually exclusive, which means:
        // - At each input, only one of the two may be set.
        // - At some input, specifying one disables the other.
        if (fileSettings->autoTiling.set) {
            if (fileSettings->tileRowsLog2.set || fileSettings->tileColsLog2.set) {
                fprintf(stderr, "ERROR: --autotiling is specified but --tilerowslog2 or --tilecolslog2 is also specified for current input.\n");
                goto cleanup;
            }
            // At this point, autoTiling of this input file can only be set by command line.
            // (automatic generation of setting entries happens below)
            // Since it's a boolean flag, its value must be AVIF_TRUE.
            assert(fileSettings->autoTiling.value);
            // Therefore disable manual tiling at this input (in case it was enabled at previous input).
            fileSettings->tileRowsLog2 = intSettingsEntryOf(0);
            fileSettings->tileColsLog2 = intSettingsEntryOf(0);
        } else if (fileSettings->tileColsLog2.set || fileSettings->tileRowsLog2.set) {
            // If this file has manual tile config set, disable automatic tiling, for the same reason as above.
            fileSettings->autoTiling = boolSettingsEntryOf(AVIF_FALSE);
        }

        // Check per-input lossy/lossless parameters.
        if (lossless) {
            // Quality.
            if ((fileSettings->quality.set && fileSettings->quality.value != AVIF_QUALITY_LOSSLESS) ||
                (fileSettings->qualityAlpha.set && fileSettings->qualityAlpha.value != AVIF_QUALITY_LOSSLESS)) {
                fprintf(stderr, "ERROR: Quality cannot be set in lossless mode, except to %d.\n", AVIF_QUALITY_LOSSLESS);
                goto cleanup;
            }
            // Quantizers.
            if ((fileSettings->minQuantizer.set && fileSettings->minQuantizer.value != AVIF_QUANTIZER_LOSSLESS) ||
                (fileSettings->maxQuantizer.set && fileSettings->maxQuantizer.value != AVIF_QUANTIZER_LOSSLESS) ||
                (fileSettings->minQuantizerAlpha.set && fileSettings->minQuantizerAlpha.value != AVIF_QUANTIZER_LOSSLESS) ||
                (fileSettings->maxQuantizerAlpha.set && fileSettings->maxQuantizerAlpha.value != AVIF_QUANTIZER_LOSSLESS)) {
                fprintf(stderr, "ERROR: Quantizers cannot be set in lossless mode, except to %d.\n", AVIF_QUANTIZER_LOSSLESS);
                goto cleanup;
            }
        } else {
            if (settings.progressive) {
                assert(DEFAULT_QUALITY >= PROGRESSIVE_WORST_QUALITY);
                if (fileSettings->quality.set && fileSettings->quality.value < PROGRESSIVE_WORST_QUALITY) {
                    fprintf(stderr, "ERROR: --qcolor must be at least %d when using --progressive.\n", PROGRESSIVE_WORST_QUALITY);
                    goto cleanup;
                }
                // --progressive only adjust color quality
            }
        }

        // Set defaults for first input file.
        if (i == 0) {
            // This check only applies to the first input.
            // Following inputs can change only one and leave the other unchanged.
            if (fileSettings->minQuantizer.set != fileSettings->maxQuantizer.set) {
                fprintf(stderr,
                        "ERROR: --min and --max must be either both specified or both unspecified for input %s.\n",
                        avifPrettyFilename(file->filename));
                goto cleanup;
            }
            if (fileSettings->minQuantizerAlpha.set != fileSettings->maxQuantizerAlpha.set) {
                fprintf(stderr,
                        "ERROR: --minalpha and --maxalpha must be either both specified or both unspecified for input %s.\n",
                        avifPrettyFilename(file->filename));
                goto cleanup;
            }
            if (fileSettings->minQuantizer.set && fileSettings->maxQuantizer.set) {
                fprintf(stderr,
                        "WARNING: --min and --max are deprecated, please use -q 0..100 instead. "
                        "--min %d --max %d is equivalent to -q %d\n",
                        fileSettings->minQuantizer.value,
                        fileSettings->maxQuantizer.value,
                        quantizerToQuality(fileSettings->minQuantizer.value, fileSettings->maxQuantizer.value));
            }
            if (fileSettings->minQuantizerAlpha.set && fileSettings->maxQuantizerAlpha.set) {
                fprintf(stderr,
                        "WARNING: --minalpha and --maxalpha are deprecated, please use --qalpha 0..100 instead. "
                        "--minalpha %d --maxalpha %d is equivalent to --qalpha %d\n",
                        fileSettings->minQuantizerAlpha.value,
                        fileSettings->maxQuantizerAlpha.value,
                        quantizerToQuality(fileSettings->minQuantizerAlpha.value, fileSettings->maxQuantizerAlpha.value));
            }

            if (!fileSettings->autoTiling.set) {
                fileSettings->autoTiling = boolSettingsEntryOf(AVIF_TRUE);
            }
            if (!fileSettings->tileRowsLog2.set) {
                fileSettings->tileRowsLog2 = intSettingsEntryOf(0);
            }
            if (!fileSettings->tileColsLog2.set) {
                fileSettings->tileColsLog2 = intSettingsEntryOf(0);
            }

            // Set lossy/lossless parameters to default if needed.
            if (lossless) {
                // Add lossless settings.
                // Settings on first input will be inherited by all inputs, so this is sufficient.
                fileSettings->quality = intSettingsEntryOf(AVIF_QUALITY_LOSSLESS);
                fileSettings->qualityAlpha = intSettingsEntryOf(AVIF_QUALITY_LOSSLESS);
                fileSettings->minQuantizer = intSettingsEntryOf(AVIF_QUANTIZER_LOSSLESS);
                fileSettings->maxQuantizer = intSettingsEntryOf(AVIF_QUANTIZER_LOSSLESS);
                fileSettings->minQuantizerAlpha = intSettingsEntryOf(AVIF_QUANTIZER_LOSSLESS);
                fileSettings->maxQuantizerAlpha = intSettingsEntryOf(AVIF_QUANTIZER_LOSSLESS);
            } else {
                settings.qualityIsConstrained = fileSettings->quality.set;
                settings.qualityAlphaIsConstrained = fileSettings->qualityAlpha.set;

                if (fileSettings->minQuantizer.set) {
                    assert(fileSettings->maxQuantizer.set);
                    if (!fileSettings->quality.set) {
                        const int quality = quantizerToQuality(fileSettings->minQuantizer.value, fileSettings->maxQuantizer.value);
                        fileSettings->quality = intSettingsEntryOf(quality);
                    }
                } else {
                    assert(!fileSettings->maxQuantizer.set);
                    if (!fileSettings->quality.set) {
                        fileSettings->quality = intSettingsEntryOf(DEFAULT_QUALITY);
                    }
                    fileSettings->minQuantizer = intSettingsEntryOf(AVIF_QUANTIZER_BEST_QUALITY);
                    fileSettings->maxQuantizer = intSettingsEntryOf(AVIF_QUANTIZER_WORST_QUALITY);
                }

                if (fileSettings->minQuantizerAlpha.set) {
                    assert(fileSettings->maxQuantizerAlpha.set);
                    if (!fileSettings->qualityAlpha.set) {
                        const int qualityAlpha =
                            quantizerToQuality(fileSettings->minQuantizerAlpha.value, fileSettings->maxQuantizerAlpha.value);
                        fileSettings->qualityAlpha = intSettingsEntryOf(qualityAlpha);
                    }
                } else {
                    assert(!fileSettings->maxQuantizerAlpha.set);
                    if (!fileSettings->qualityAlpha.set) {
                        fileSettings->qualityAlpha = fileSettings->quality;
                    }
                    fileSettings->minQuantizerAlpha = intSettingsEntryOf(AVIF_QUANTIZER_BEST_QUALITY);
                    fileSettings->maxQuantizerAlpha = intSettingsEntryOf(AVIF_QUANTIZER_WORST_QUALITY);
                }
            }

            if (!fileSettings->scalingMode.set) {
                fileSettings->scalingMode = scalingModeSettingsEntryOf(1, 1);
            }
        }
    }

    image = avifImageCreateEmpty();
    if (!image) {
        fprintf(stderr, "ERROR: Out of memory\n");
        goto cleanup;
    }

    // Set these in advance so any upcoming RGB -> YUV use the proper coefficients
    image->colorPrimaries = settings.colorPrimaries;
    image->transferCharacteristics = settings.transferCharacteristics;
    image->matrixCoefficients = settings.matrixCoefficients;
    image->yuvRange = requestedRange;
    image->alphaPremultiplied = premultiplyAlpha;

    if ((image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY) && (input.requestedFormat != AVIF_PIXEL_FORMAT_NONE) &&
        (input.requestedFormat != AVIF_PIXEL_FORMAT_YUV444)) {
        // User explicitly asked for non YUV444 yuvFormat, while matrixCoefficients was likely
        // set to AVIF_MATRIX_COEFFICIENTS_IDENTITY as a side effect of --lossless,
        // and Identity is only valid with YUV444. Set matrixCoefficients back to the default.
        image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601;

        if (settings.cicpExplicitlySet) {
            // Only warn if someone explicitly asked for identity.
            printf("WARNING: matrixCoefficients may not be set to identity (0) when %s. Resetting MC to defaults (%d).\n",
                   (input.requestedFormat == AVIF_PIXEL_FORMAT_YUV400) ? "encoding 4:0:0" : "subsampling",
                   image->matrixCoefficients);
        }
    }

    // --target-size requires multiple encodings of the same files. Cache the input images.
    input.cacheEnabled = (settings.targetSize != -1);

    const avifInputFile * firstFile = avifInputGetFile(&input, /*imageIndex=*/0);
    uint32_t sourceDepth = 0;
    avifBool sourceWasRGB = AVIF_FALSE;
    avifAppSourceTiming firstSourceTiming;
    const avifBool isImageSequence = (!settings.gridDimsPresent) && (settings.layers == 1) && (input.filesCount > 1);
    // Gain maps are not supported for animations or layered images.
    const avifBool ignoreGainMap = settings.ignoreGainMap || isImageSequence || settings.progressive;
    if (!avifInputReadImage(&input,
                            /*imageIndex=*/0,
                            settings.ignoreColorProfile,
                            settings.ignoreExif,
                            settings.ignoreXMP,
                            /*allowChangingCicp=*/!settings.cicpExplicitlySet,
                            ignoreGainMap,
                            image,
                            /*settings=*/NULL, // Must use the setting for first input
                            &sourceDepth,
                            &sourceWasRGB,
                            &firstSourceTiming,
                            settings.chromaDownsampling,
                            settings.inputFormat)) {
        goto cleanup;
    }

    // Check again for -y auto or for y4m input (y4m input ignores input.requestedFormat and
    // retains the format in file).
    if ((image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY) && (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400)) {
        image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601;

        if (settings.cicpExplicitlySet) {
            // Only warn if someone explicitly asked for identity.
            printf("WARNING: matrixCoefficients may not be set to identity (0) when encoding 4:0:0. Resetting MC to defaults (%d).\n",
                   image->matrixCoefficients);
        }
    }
    if ((image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY) && (image->yuvFormat != AVIF_PIXEL_FORMAT_YUV444)) {
        fprintf(stderr, "matrixCoefficients may not be set to identity (0) when subsampling.\n");
        goto cleanup;
    }

    printf("Successfully loaded: %s\n", avifPrettyFilename(firstFile->filename));

    // Prepare image timings
    if ((settings.outputTiming.duration == 0) && (settings.outputTiming.timescale == 0) && (firstSourceTiming.duration > 0) &&
        (firstSourceTiming.timescale > 0)) {
        // Set the default duration and timescale to the first image's timing.
        settings.outputTiming = firstSourceTiming;
    } else {
        // Set output timing defaults to 30 fps
        if (settings.outputTiming.duration == 0) {
            settings.outputTiming.duration = 1;
        }
        if (settings.outputTiming.timescale == 0) {
            settings.outputTiming.timescale = 30;
        }
    }

    if ((iccOverride.size && (avifImageSetProfileICC(image, iccOverride.data, iccOverride.size) != AVIF_RESULT_OK)) ||
        (exifOverride.size && (avifImageSetMetadataExif(image, exifOverride.data, exifOverride.size) != AVIF_RESULT_OK)) ||
        (xmpOverride.size && (avifImageSetMetadataXMP(image, xmpOverride.data, xmpOverride.size) != AVIF_RESULT_OK))) {
        fprintf(stderr, "Error when setting overridden metadata: out of memory.\n");
        goto cleanup;
    }

    if (!image->icc.size && !settings.cicpExplicitlySet && (image->colorPrimaries == AVIF_COLOR_PRIMARIES_UNSPECIFIED) &&
        (image->transferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED)) {
        // The final image has no ICC profile, the user didn't specify any CICP, and the source
        // image didn't provide any CICP. Explicitly signal SRGB CP/TC here, as 2/2/x will be
        // interpreted as SRGB anyway.
        image->colorPrimaries = AVIF_COLOR_PRIMARIES_SRGB;
        image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB;
    }

#if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
    if (image->gainMap && !image->gainMap->altICC.size) {
        if (image->gainMap->altColorPrimaries == AVIF_COLOR_PRIMARIES_UNSPECIFIED) {
            // Assume the alternate image has the same primaries as the base image.
            image->gainMap->altColorPrimaries = image->colorPrimaries;
        }
        if (image->gainMap->altTransferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED) {
            // Assume the alternate image is PQ HDR.
            image->gainMap->altTransferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_PQ;
        }
    }
#endif // AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION

    if (settings.paspPresent) {
        image->transformFlags |= AVIF_TRANSFORM_PASP;
        image->pasp.hSpacing = settings.paspValues[0];
        image->pasp.vSpacing = settings.paspValues[1];
    }
    if (cropConversionRequired) {
        if (!convertCropToClap(image->width, image->height, settings.clapValues)) {
            goto cleanup;
        }
        settings.clapValid = AVIF_TRUE;
    }
    if (settings.clapValid) {
        image->transformFlags |= AVIF_TRANSFORM_CLAP;
        image->clap.widthN = settings.clapValues[0];
        image->clap.widthD = settings.clapValues[1];
        image->clap.heightN = settings.clapValues[2];
        image->clap.heightD = settings.clapValues[3];
        image->clap.horizOffN = settings.clapValues[4];
        image->clap.horizOffD = settings.clapValues[5];
        image->clap.vertOffN = settings.clapValues[6];
        image->clap.vertOffD = settings.clapValues[7];

        // Validate clap
        avifCropRect cropRect;
        avifDiagnostics diag;
        avifDiagnosticsClearError(&diag);
        if (!avifCropRectFromCleanApertureBox(&cropRect, &image->clap, image->width, image->height, &diag)) {
            fprintf(stderr,
                    "ERROR: Invalid clap: width:[%d / %d], height:[%d / %d], horizOff:[%d / %d], vertOff:[%d / %d] - %s\n",
                    (int32_t)image->clap.widthN,
                    (int32_t)image->clap.widthD,
                    (int32_t)image->clap.heightN,
                    (int32_t)image->clap.heightD,
                    (int32_t)image->clap.horizOffN,
                    (int32_t)image->clap.horizOffD,
                    (int32_t)image->clap.vertOffN,
                    (int32_t)image->clap.vertOffD,
                    diag.error);
            goto cleanup;
        }
    }
    if (irotAngle != 0xff) {
        image->transformFlags |= AVIF_TRANSFORM_IROT;
        image->irot.angle = irotAngle;
    }
    if (imirAxis != 0xff) {
        image->transformFlags |= AVIF_TRANSFORM_IMIR;
        image->imir.axis = imirAxis;
    }
    if (settings.clliPresent) {
        image->clli.maxCLL = (uint16_t)settings.clliValues[0];
        image->clli.maxPALL = (uint16_t)settings.clliValues[1];
    }

    avifBool hasAlpha = (image->alphaPlane && image->alphaRowBytes);
    avifBool usingLosslessColor = (firstFile->settings.quality.value == AVIF_QUALITY_LOSSLESS);
    avifBool usingLosslessAlpha = (firstFile->settings.qualityAlpha.value == AVIF_QUALITY_LOSSLESS);
    avifBool using400 = (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400);
    avifBool using444 = (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV444);
    avifBool usingFullRange = (image->yuvRange == AVIF_RANGE_FULL);
    avifBool usingIdentityMatrix = (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY);

    // Guess if the enduser is asking for lossless and enable it so that warnings can be emitted
    if (!lossless && usingLosslessColor && (!hasAlpha || usingLosslessAlpha)) {
        // The enduser is probably expecting lossless. Turn it on and emit warnings
        printf("Quality set to %d, assuming --lossless to enable warnings on potential lossless issues.\n", AVIF_QUALITY_LOSSLESS);
        lossless = AVIF_TRUE;
    }

    // Check for any reasons lossless will fail, and complain loudly
    if (lossless) {
        if (!usingLosslessColor) {
            fprintf(stderr,
                    "WARNING: [--lossless] Color quality (-q or --qcolor) not set to %d. Color output might not be lossless.\n",
                    AVIF_QUALITY_LOSSLESS);
            lossless = AVIF_FALSE;
        }

        if (hasAlpha && !usingLosslessAlpha) {
            fprintf(stderr,
                    "WARNING: [--lossless] Alpha present and alpha quality (--qalpha) not set to %d. Alpha output might not be lossless.\n",
                    AVIF_QUALITY_LOSSLESS);
            lossless = AVIF_FALSE;
        }

        if (usingIdentityMatrix && (sourceDepth != image->depth)) {
            fprintf(stderr,
                    "WARNING: [--lossless] Identity matrix is used but input depth (%d) does not match output depth (%d). Output might not be lossless.\n",
                    sourceDepth,
                    image->depth);
            lossless = AVIF_FALSE;
        }

        if (sourceWasRGB) {
            if (!using444 && !using400) {
                fprintf(stderr,
                        "WARNING: [--lossless] Input data was RGB and YUV "
                        "subsampling (-y) isn't YUV444 or YUV400. Output might "
                        "not be lossless.\n");
                lossless = AVIF_FALSE;
            }

            if (!usingFullRange) {
                fprintf(stderr, "WARNING: [--lossless] Input data was RGB and output range (-r) isn't full. Output might not be lossless.\n");
                lossless = AVIF_FALSE;
            }

            avifBool matrixCoefficientsAreLosslessCompatible = usingIdentityMatrix ||
                                                               image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE ||
                                                               image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO;
            if (!matrixCoefficientsAreLosslessCompatible && !using400) {
                fprintf(stderr, "WARNING: [--lossless] Input data was RGB and matrixCoefficients isn't set to identity (--cicp x/x/0) or YCgCo-Re/Ro (--cicp x/x/16 or x/x/17); Output might not be lossless.\n");
                lossless = AVIF_FALSE;
            }
        }
    }

    if (settings.gridDimsPresent) {
        // Grid image!

        gridCellCount = settings.gridDims[0] * settings.gridDims[1];
        printf("Preparing to encode a %ux%u grid (%u cells)...\n", settings.gridDims[0], settings.gridDims[1], gridCellCount);

        gridCells = calloc(gridCellCount, sizeof(avifImage *));
        if (gridCells == NULL) {
            fprintf(stderr, "ERROR: memory allocation failure\n");
            goto cleanup;
        }
        gridCells[0] = image; // take ownership of image

        int imageIndex = 1; // The first grid cell was loaded into image (imageIndex 0).
        const avifInputFile * nextFile;
        while ((nextFile = avifInputGetFile(&input, imageIndex)) != NULL) {
            if (imageIndex == 1) {
                printf("Loading additional cells for grid image (%u cells)...\n", gridCellCount);
            }
            if (imageIndex >= (int)gridCellCount) {
                // We have enough, warn and continue
                fprintf(stderr,
                        "WARNING: [--grid] More than %u images were supplied for this %ux%u grid. The rest will be ignored.\n",
                        gridCellCount,
                        settings.gridDims[0],
                        settings.gridDims[1]);
                break;
            }

            // Ensure no settings is set for other cells
            if (memcmp(&nextFile->settings, &emptySettingsReference, sizeof(avifInputFileSettings)) != 0) {
                fprintf(stderr, "ERROR: Grid image cannot use different settings for each cell.\n");
                goto cleanup;
            }

            avifImage * cellImage = avifImageCreateEmpty();
            if (!cellImage) {
                fprintf(stderr, "ERROR: Out of memory\n");
                goto cleanup;
            }
            cellImage->colorPrimaries = image->colorPrimaries;
            cellImage->transferCharacteristics = image->transferCharacteristics;
            cellImage->matrixCoefficients = image->matrixCoefficients;
            cellImage->yuvRange = image->yuvRange;
            cellImage->alphaPremultiplied = image->alphaPremultiplied;
            gridCells[imageIndex] = cellImage;

            // 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,
                                    /*ignoreColorProfile=*/AVIF_TRUE,
                                    /*ignoreExif=*/AVIF_TRUE,
                                    /*ignoreXMP=*/AVIF_TRUE,
                                    /*allowChangingCicp=*/AVIF_FALSE,
                                    settings.ignoreGainMap,
                                    cellImage,
                                    /*settings=*/NULL,
                                    /*outDepth=*/NULL,
                                    /*sourceIsRGB=*/NULL,
                                    /*sourceTiming=*/NULL,
                                    settings.chromaDownsampling,
                                    settings.inputFormat)) {
                goto cleanup;
            }
            // Let avifEncoderAddImageGrid() verify the grid integrity (valid cell sizes, depths etc.).

            ++imageIndex;
        }

        if (imageIndex == 1) {
            printf("Single image input for a grid image. Attempting to split into %u cells...\n", gridCellCount);
            gridSplitImage = image;
            gridCells[0] = NULL;

            if (!avifImageSplitGrid(gridSplitImage, settings.gridDims[0], settings.gridDims[1], gridCells)) {
                goto cleanup;
            }
        } else if (imageIndex != (int)gridCellCount) {
            fprintf(stderr, "ERROR: Not enough input files for grid image! (expecting %u, or a single image to be split)\n", gridCellCount);
            goto cleanup;
        }
    }

    const char * lossyHint = " (Lossy)";
    if (lossless) {
        lossyHint = " (Lossless)";
    }
    printf("AVIF to be written:%s\n", lossyHint);
    const avifImage * avif = gridCells ? gridCells[0] : image;
    avifImageDump(avif,
                  settings.gridDims[0],
                  settings.gridDims[1],
                  settings.layers > 1 ? AVIF_PROGRESSIVE_STATE_AVAILABLE : AVIF_PROGRESSIVE_STATE_UNAVAILABLE);

    avifEncodedByteSizes byteSizes = { 0, 0, 0 };
    if (!avifEncodeImages(&settings, &input, firstFile, image, (const avifImage * const *)gridCells, &raw, &byteSizes)) {
        goto cleanup;
    }

    printf("Encoded successfully.\n");
    printf(" * Color total size: %" AVIF_FMT_ZU " bytes\n", byteSizes.colorSizeBytes);
    printf(" * Alpha total size: %" AVIF_FMT_ZU " bytes\n", byteSizes.alphaSizeBytes);
    if (byteSizes.gainMapSizeBytes > 0) {
        printf(" * Gain Map AV1 total size: %" AVIF_FMT_ZU " bytes\n", byteSizes.gainMapSizeBytes);
    }
    if (isImageSequence) {
        if (settings.repetitionCount == AVIF_REPETITION_COUNT_INFINITE) {
            printf(" * Repetition Count: Infinite\n");
        } else {
            printf(" * Repetition Count: %d\n", settings.repetitionCount);
        }
    }
    if (noOverwrite && fileExists(outputFilename)) {
        // check again before write
        fprintf(stderr, "ERROR: output file %s already exists and --no-overwrite was specified\n", outputFilename);
        goto cleanup;
    }
    FILE * f = fopen(outputFilename, "wb");
    if (!f) {
        fprintf(stderr, "ERROR: Failed to open file for write: %s\n", outputFilename);
        goto cleanup;
    }
    if (fwrite(raw.data, 1, raw.size, f) != raw.size) {
        fprintf(stderr, "Failed to write %" AVIF_FMT_ZU " bytes: %s\n", raw.size, outputFilename);
        goto cleanup;
    } else {
        printf("Wrote AVIF: %s\n", outputFilename);
    }
    fclose(f);
    returnCode = 0;

cleanup:
    if (gridCells) {
        for (uint32_t i = 0; i < gridCellCount; ++i) {
            if (gridCells[i]) {
                avifImageDestroy(gridCells[i]);
            }
        }
        free(gridCells);
    } else if (image) { // image is owned/cleaned up by gridCells if it exists
        avifImageDestroy(image);
    }
    if (gridSplitImage) {
        avifImageDestroy(gridSplitImage);
    }
    avifRWDataFree(&raw);
    avifRWDataFree(&exifOverride);
    avifRWDataFree(&xmpOverride);
    avifRWDataFree(&iccOverride);
    avifCodecSpecificOptionsFree(&pendingSettings.codecSpecificOptions);
    while (input.cacheCount) {
        --input.cacheCount;
        if (input.cache[input.cacheCount].image) {
            avifImageDestroy(input.cache[input.cacheCount].image);
        }
    }
    free(input.cache);
    while (input.filesCount) {
        --input.filesCount;
        avifInputFile * file = &input.files[input.filesCount];
        avifCodecSpecificOptionsFree(&file->settings.codecSpecificOptions);
    }
    free(input.files);

    return returnCode;
}
