| // 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 <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; |
| avifHeaderFormat headerFormat; |
| |
| 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_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION |
| // (which also implies AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP). |
| 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; |
| } 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; |
| |
| typedef struct avifInputFile |
| { |
| const char * filename; |
| uint64_t duration; // If 0, use the default duration |
| avifInputFileSettings settings; |
| } avifInputFile; |
| static avifInputFile stdinFile; |
| |
| 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; |
| avifBool useStdin; |
| |
| 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 : Set quality for color (%d-%d, where %d is lossless)\n", |
| AVIF_QUALITY_WORST, |
| AVIF_QUALITY_BEST, |
| AVIF_QUALITY_LOSSLESS); |
| printf(" --qalpha Q : Set quality for alpha (%d-%d, where %d is lossless)\n", |
| AVIF_QUALITY_WORST, |
| AVIF_QUALITY_BEST, |
| AVIF_QUALITY_LOSSLESS); |
| printf(" -s,--speed S : Encoder speed (%d-%d, slowest-fastest, 'default' or 'd' for codec internal defaults. default speed: 6)\n", |
| AVIF_SPEED_SLOWEST, |
| AVIF_SPEED_FASTEST); |
| printf("\n"); |
| printf("Advanced options:\n"); |
| printf(" -j,--jobs J : Number of jobs (worker threads). Use \"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_METAV1) |
| printf(" --metav1 : Use reduced header if possible\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 : Output depth [8,10,12]. (JPEG/PNG only; For y4m or stdin, depth is retained)\n"); |
| printf(" -y,--yuv FORMAT : Output format [default=auto, 444, 422, 420, 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 y4m frames from stdin instead of files; no input filenames allowed, must set before offering output filename\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 [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 : EXPERIMENTAL: Auto set parameters to encode a simple layered image supporting progressive rendering from a single input frame.\n"); |
| printf(" --layered : EXPERIMENTAL: 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 : Set the timescale to V. 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(" -k,--keyframe INTERVAL : Set the maximum keyframe interval (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_EXPERIMENTAL_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 : Set quality for the gain map (%d-%d, where %d is lossless)\n", |
| AVIF_QUALITY_WORST, |
| AVIF_QUALITY_BEST, |
| AVIF_QUALITY_LOSSLESS); |
| // TODO(maryla): add quality setting for the gain map. |
| #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 num/denom pairs)\n"); |
| printf(" --irot ANGLE : Add irot property (rotation). [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 or infinite : Number of times an animated image sequence will be repeated. Use 'infinite' for infinite repetitions (Default: infinite)\n"); |
| printf(" -- : Signals the end of options. Everything after this is interpreted as file names.\n"); |
| printf("\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 : Set quality for color (%d-%d, where %d is lossless)\n", |
| AVIF_QUALITY_WORST, |
| AVIF_QUALITY_BEST, |
| AVIF_QUALITY_LOSSLESS); |
| printf(" --qalpha Q : Set quality for alpha (%d-%d, where %d is lossless)\n", |
| AVIF_QUALITY_WORST, |
| AVIF_QUALITY_BEST, |
| AVIF_QUALITY_LOSSLESS); |
| printf(" --tilerowslog2 R : Set log2 of number of tile rows (0-6, default: 0)\n"); |
| printf(" --tilecolslog2 C : Set log2 of number of tile columns (0-6, default: 0)\n"); |
| printf(" --autotiling : Set --tilerowslog2 and --tilecolslog2 automatically\n"); |
| printf(" --min QP : Set min quantizer for color (%d-%d, where %d is lossless)\n", |
| AVIF_QUANTIZER_BEST_QUALITY, |
| AVIF_QUANTIZER_WORST_QUALITY, |
| AVIF_QUANTIZER_LOSSLESS); |
| printf(" --max QP : Set max quantizer for color (%d-%d, where %d is lossless)\n", |
| AVIF_QUANTIZER_BEST_QUALITY, |
| AVIF_QUANTIZER_WORST_QUALITY, |
| AVIF_QUANTIZER_LOSSLESS); |
| printf(" --minalpha QP : Set min quantizer for alpha (%d-%d, where %d is lossless)\n", |
| AVIF_QUANTIZER_BEST_QUALITY, |
| AVIF_QUANTIZER_WORST_QUALITY, |
| AVIF_QUANTIZER_LOSSLESS); |
| printf(" --maxalpha QP : Set max quantizer for alpha (%d-%d, where %d is lossless)\n", |
| AVIF_QUANTIZER_BEST_QUALITY, |
| AVIF_QUANTIZER_WORST_QUALITY, |
| AVIF_QUANTIZER_LOSSLESS); |
| printf(" --scaling-mode N[/D] : EXPERIMENTAL: Set frame (layer) scaling mode as given fraction. If omitted, D default to 1. (Default: 1/1)\n"); |
| printf(" --duration D : Set frame durations (in timescales) to D; default 1. This option always apply to following inputs with or without 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 (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 (vbr, cbr, cq, or q)\n"); |
| printf(" sharpness=S : Bias towards block sharpness in rate-distortion optimization of transform coefficients (0-7, default: 0)\n"); |
| printf(" tune=METRIC : Tune the encoder for distortion metric (psnr or ssim, default: psnr)\n"); |
| printf(" film-grain-test=TEST : Film grain test vectors (0: none (default), 1: test-1 2: test-2, ... 16: test-16)\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, avifPixelFormat yuvFormat, 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 = avifCleanApertureBoxConvertCropRect(&clap, &cropRect, srcW, srcH, yuvFormat, &diag); |
| if (!convertResult) { |
| fprintf(stderr, |
| "ERROR: Impossible crop rect: imageSize:[%ux%u], pixelFormat:%s, cropRect:[%u,%u, %ux%u] - %s\n", |
| srcW, |
| srcH, |
| avifPixelFormatToString(yuvFormat), |
| 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->useStdin) { |
| ungetc(fgetc(stdin), stdin); // Kick stdin to force EOF |
| |
| if (feof(stdin)) { |
| return NULL; |
| } |
| return &stdinFile; |
| } |
| |
| if (input->fileIndex >= input->filesCount) { |
| return NULL; |
| } |
| return &input->files[input->fileIndex]; |
| } |
| |
| static avifBool avifInputHasRemainingData(const avifInput * input, int imageIndex) |
| { |
| if (imageIndex < input->cacheCount) { |
| return AVIF_TRUE; |
| } |
| |
| if (input->useStdin) { |
| return !feof(stdin); |
| } |
| return (input->fileIndex < input->filesCount); |
| } |
| |
| 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) |
| { |
| 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_EXPERIMENTAL_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->useStdin) { |
| if (feof(stdin)) { |
| return AVIF_FALSE; |
| } |
| if (!y4mRead(NULL, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, dstImage, dstSourceTiming, &input->frameIter)) { |
| fprintf(stderr, "ERROR: Cannot read y4m through standard input"); |
| return AVIF_FALSE; |
| } |
| if (dstSettings) { |
| *dstSettings = &input->files[0].settings; |
| } |
| if (dstDepth) { |
| *dstDepth = dstImage->depth; |
| } |
| assert(dstImage->yuvFormat != AVIF_PIXEL_FORMAT_NONE); |
| if (dstSourceIsRGB) { |
| *dstSourceIsRGB = AVIF_FALSE; |
| } |
| } else { |
| if (input->fileIndex >= input->filesCount) { |
| return AVIF_FALSE; |
| } |
| |
| const avifInputFile * currentFile = &input->files[input->fileIndex]; |
| const avifAppFileFormat inputFormat = avifReadImage(currentFile->filename, |
| input->requestedFormat, |
| input->requestedDepth, |
| chromaDownsampling, |
| ignoreColorProfile, |
| ignoreExif, |
| ignoreXMP, |
| allowChangingCicp, |
| ignoreGainMap, |
| AVIF_DEFAULT_IMAGE_SIZE_LIMIT, |
| dstImage, |
| dstDepth, |
| dstSourceTiming, |
| &input->frameIter); |
| if (inputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) { |
| fprintf(stderr, "Cannot read input file: %s\n", currentFile->filename); |
| return AVIF_FALSE; |
| } |
| if (dstSourceIsRGB) { |
| *dstSourceIsRGB = (inputFormat != AVIF_APP_FILE_FORMAT_Y4M); |
| } |
| if (!input->frameIter) { |
| ++input->fileIndex; |
| } |
| if (dstSettings) { |
| *dstSettings = ¤tFile->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); |
| } |
| 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; |
| } |
| |
| static avifBool avifCodecSpecificOptionsAdd(avifCodecSpecificOptions * options, const char * keyValue) |
| { |
| 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)); |
| } |
| |
| 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. |
| options->values[options->count] = avifStrdup(value + 1); |
| const size_t keyLength = strlen(keyValue) - strlen(value); |
| options->keys[options->count] = malloc(keyLength + 1); |
| if (!options->values[options->count] || !options->keys[options->count]) { |
| goto cleanup; |
| } |
| memcpy(options->keys[options->count], keyValue, keyLength); |
| options->keys[options->count][keyLength] = '\0'; |
| } else { |
| // Pass in a non-NULL, empty string. Codecs can use the mere existence of a key as a boolean value. |
| options->values[options->count] = avifStrdup(""); |
| options->keys[options->count] = avifStrdup(keyValue); |
| if (!options->values[options->count] || !options->keys[options->count]) { |
| goto cleanup; |
| } |
| } |
| success = AVIF_TRUE; |
| cleanup: |
| ++options->count; |
| free(oldKeys); |
| free(oldValues); |
| return success; |
| } |
| |
| 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; |
| } |
| |
| // Returns the best cell size for a given horizontal or vertical dimension. |
| static avifBool avifGetBestCellSize(const char * dimensionStr, uint32_t numPixels, uint32_t numCells, avifBool isSubsampled, uint32_t * cellSize) |
| { |
| assert(numPixels); |
| assert(numCells); |
| |
| // ISO/IEC 23008-12:2017, Section 6.6.2.3.1: |
| // The reconstructed image is formed by tiling the input images into a grid with a column width |
| // (potentially excluding the right-most column) equal to tile_width and a row height (potentially |
| // excluding the bottom-most row) equal to tile_height, without gap or overlap, and then |
| // trimming on the right and the bottom to the indicated output_width and output_height. |
| // The priority could be to use a cell size that is a multiple of 64, but there is not always a valid one, |
| // even though it is recommended by MIAF. Just use ceil(numPixels/numCells) for simplicity and to avoid |
| // as much padding in the right-most and bottom-most cells as possible. |
| // Use uint64_t computation to avoid a potential uint32_t overflow. |
| *cellSize = (uint32_t)(((uint64_t)numPixels + numCells - 1) / numCells); |
| |
| // ISO/IEC 23000-22:2019, Section 7.3.11.4.2: |
| // - the tile_width shall be greater than or equal to 64, and should be a multiple of 64 |
| // - the tile_height shall be greater than or equal to 64, and should be a multiple of 64 |
| if (*cellSize < 64) { |
| *cellSize = 64; |
| if ((uint64_t)(numCells - 1) * *cellSize >= (uint64_t)numPixels) { |
| // Some cells would be entirely off-canvas. |
| fprintf(stderr, "ERROR: There are too many cells %s (%u) to have at least 64 pixels per cell.\n", dimensionStr, numCells); |
| return AVIF_FALSE; |
| } |
| } |
| |
| // The maximum AV1 frame size is 65536 pixels inclusive. |
| if (*cellSize > 65536) { |
| fprintf(stderr, "ERROR: Cell size %u is bigger %s than the maximum frame size 65536.\n", *cellSize, dimensionStr); |
| return AVIF_FALSE; |
| } |
| |
| // ISO/IEC 23000-22:2019, Section 7.3.11.4.2: |
| // - when the images are in the 4:2:2 chroma sampling format the horizontal tile offsets and widths, |
| // and the output width, shall be even numbers; |
| // - when the images are in the 4:2:0 chroma sampling format both the horizontal and vertical tile |
| // offsets and widths, and the output width and height, shall be even numbers. |
| if (isSubsampled && (*cellSize & 1)) { |
| ++*cellSize; |
| if ((uint64_t)(numCells - 1) * *cellSize >= (uint64_t)numPixels) { |
| // Some cells would be entirely off-canvas. |
| fprintf(stderr, "ERROR: Odd cell size %u is forbidden on a %s subsampled image.\n", *cellSize - 1, dimensionStr); |
| return AVIF_FALSE; |
| } |
| } |
| |
| // Each pixel is covered by exactly one cell, and each cell contains at least one pixel. |
| assert(((uint64_t)(numCells - 1) * *cellSize < (uint64_t)numPixels) && ((uint64_t)numCells * *cellSize >= (uint64_t)numPixels)); |
| return AVIF_TRUE; |
| } |
| |
| static avifBool avifImageSplitGrid(const avifImage * gridSplitImage, uint32_t gridCols, uint32_t gridRows, avifImage ** gridCells) |
| { |
| uint32_t cellWidth, cellHeight; |
| avifPixelFormatInfo formatInfo; |
| avifGetPixelFormatInfo(gridSplitImage->yuvFormat, &formatInfo); |
| const avifBool isSubsampledX = !formatInfo.monochrome && formatInfo.chromaShiftX; |
| const avifBool isSubsampledY = !formatInfo.monochrome && formatInfo.chromaShiftY; |
| if (!avifGetBestCellSize("horizontally", gridSplitImage->width, gridCols, isSubsampledX, &cellWidth) || |
| !avifGetBestCellSize("vertically", gridSplitImage->height, gridRows, isSubsampledY, &cellHeight)) { |
| return AVIF_FALSE; |
| } |
| |
| for (uint32_t gridY = 0; gridY < gridRows; ++gridY) { |
| for (uint32_t gridX = 0; gridX < gridCols; ++gridX) { |
| uint32_t gridIndex = gridX + (gridY * gridCols); |
| avifImage * cellImage = avifImageCreateEmpty(); |
| if (!cellImage) { |
| fprintf(stderr, "ERROR: Cell creation failed: out of memory\n"); |
| return AVIF_FALSE; |
| } |
| gridCells[gridIndex] = cellImage; |
| |
| avifCropRect cellRect = { gridX * cellWidth, gridY * cellHeight, cellWidth, cellHeight }; |
| if (cellRect.x + cellRect.width > gridSplitImage->width) { |
| cellRect.width = gridSplitImage->width - cellRect.x; |
| } |
| if (cellRect.y + cellRect.height > gridSplitImage->height) { |
| cellRect.height = gridSplitImage->height - cellRect.y; |
| } |
| const avifResult copyResult = avifImageSetViewRect(cellImage, gridSplitImage, &cellRect); |
| if (copyResult != AVIF_RESULT_OK) { |
| fprintf(stderr, "ERROR: Cell creation failed: %s\n", avifResultToString(copyResult)); |
| return AVIF_FALSE; |
| } |
| } |
| } |
| return AVIF_TRUE; |
| } |
| |
| #define INVALID_QUALITY (-1) |
| #define DEFAULT_QUALITY 60 // Maps to a quantizer (QP) of 25. |
| #define DEFAULT_QUALITY_ALPHA AVIF_QUALITY_LOSSLESS |
| #define DEFAULT_QUALITY_GAIN_MAP DEFAULT_QUALITY |
| #define PROGRESSIVE_WORST_QUALITY 10 // Not doing auto 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; |
| |
| printf(" * Encoding frame %d [%" PRIu64 "/%" PRIu64 " ts]: %s\n", |
| imageIndex, |
| nextDurationInTimescales, |
| settings->outputTiming.timescale, |
| nextFile->filename); |
| |
| if (nextImage) { |
| avifImageDestroy(nextImage); |
| } |
| nextImage = avifImageCreateEmpty(); |
| if (!nextImage) { |
| fprintf(stderr, "ERROR: Out of memory\n"); |
| goto cleanup; |
| } |
| nextImage->colorPrimaries = firstImage->colorPrimaries; |
| nextImage->transferCharacteristics = firstImage->transferCharacteristics; |
| nextImage->matrixCoefficients = firstImage->matrixCoefficients; |
| nextImage->yuvRange = firstImage->yuvRange; |
| nextImage->alphaPremultiplied = firstImage->alphaPremultiplied; |
| |
| // Ignore ICC, Exif and XMP because only the metadata of the first frame is taken into |
| // account by the libavif API. |
| // 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)) { |
| goto cleanup; |
| } |
| if (!avifEncoderVerifyImageCompatibility(firstImage, nextImage, "sequence", nextFile->filename)) { |
| goto cleanup; |
| } |
| if (!avifEncodeUpdateEncoderSettings(encoder, nextSettings)) { |
| goto cleanup; |
| } |
| const avifResult nextImageResult = avifEncoderAddImage(encoder, nextImage, nextDurationInTimescales, AVIF_ADD_IMAGE_FLAG_NONE); |
| if (nextImageResult != AVIF_RESULT_OK) { |
| fprintf(stderr, "ERROR: Failed to encode image: %s\n", avifResultToString(nextImageResult)); |
| goto cleanup; |
| } |
| ++imageIndex; |
| } |
| success = AVIF_TRUE; |
| |
| cleanup: |
| if (nextImage) { |
| avifImageDestroy(nextImage); |
| } |
| return success; |
| } |
| |
| static avifBool avifEncodeRestOfLayeredImage(avifEncoder * encoder, |
| const avifSettings * settings, |
| 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)) { |
| 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", nextFile->filename); |
| goto cleanup; |
| } |
| if (!avifEncoderVerifyImageCompatibility(firstImage, nextImage, "layer", 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; |
| } |
| |
| char manualTilingStr[128]; |
| snprintf(manualTilingStr, |
| sizeof(manualTilingStr), |
| "tileRowsLog2 [%d], tileColsLog2 [%d]", |
| firstFile->settings.tileRowsLog2.value, |
| firstFile->settings.tileColsLog2.value); |
| |
| 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->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_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION) |
| if (settings->qualityGainMap != INVALID_QUALITY) { |
| encoder->qualityGainMap = settings->qualityGainMap; |
| } |
| #endif |
| |
| 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_EXPERIMENTAL_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 |
| |
| printf("Encoding with 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 the color quality is less than 10, the main() function overrides |
| // --progressive and sets settings->autoProgressive to false. |
| 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 (input->useStdin || (settings->layers == 1 && input->filesCount > 1)) { |
| printf(" * Encoding frame %d [%" PRIu64 "/%" PRIu64 " ts]: %s\n", |
| 0, |
| firstDurationInTimescales, |
| settings->outputTiming.timescale, |
| 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_EXPERIMENTAL_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_EXPERIMENTAL_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; |
| } |
| |
| 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_FULL; |
| 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; |
| |
| 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]; |
| input.files[input.filesCount].filename = arg; |
| input.files[input.filesCount].duration = settings.outputTiming.duration; |
| input.files[input.filesCount].settings = pendingSettings; |
| memset(&pendingSettings, 0, sizeof(pendingSettings)); |
| ++input.filesCount; |
| ++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")) { |
| input.useStdin = AVIF_TRUE; |
| } else if (!strcmp(arg, "-o") || !strcmp(arg, "--output")) { |
| NEXTARG(); |
| outputFilename = arg; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_METAV1) |
| } else if (!strcmp(arg, "--metav1")) { |
| settings.headerFormat = AVIF_HEADER_REDUCED; |
| #endif // AVIF_ENABLE_EXPERIMENTAL_METAV1 |
| } else if (!strcmp(arg, "-d") || !strcmp(arg, "--depth")) { |
| NEXTARG(); |
| input.requestedDepth = atoi(arg); |
| if ((input.requestedDepth != 8) && (input.requestedDepth != 10) && (input.requestedDepth != 12)) { |
| fprintf(stderr, "ERROR: invalid depth: %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, "-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_EXPERIMENTAL_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 |
| input.files[input.filesCount].filename = arg; |
| input.files[input.filesCount].duration = settings.outputTiming.duration; |
| input.files[input.filesCount].settings = pendingSettings; |
| memset(&pendingSettings, 0, sizeof(pendingSettings)); |
| ++input.filesCount; |
| } |
| |
| ++argIndex; |
| } |
| |
| 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")) { |
| fprintf(stderr, "SVT-AV1 doesn't support lossless encoding yet: https://gitlab.com/AOMediaCodec/SVT-AV1/-/issues/1636\n"); |
| 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) { |
| avifBool incompatibleMC = (settings.matrixCoefficients != AVIF_MATRIX_COEFFICIENTS_IDENTITY); |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R) |
| incompatibleMC &= (settings.matrixCoefficients != AVIF_MATRIX_COEFFICIENTS_YCGCO_RE && |
| settings.matrixCoefficients != AVIF_MATRIX_COEFFICIENTS_YCGCO_RO); |
| #endif |
| if (incompatibleMC) { |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R) |
| fprintf(stderr, "Matrix coefficients have to be identity, YCgCo-Re, or YCgCo-Ro in lossless mode.\n"); |
| #else |
| fprintf(stderr, "Matrix coefficients have to be identity in lossless mode.\n"); |
| #endif |
| goto cleanup; |
| } |
| } else { |
| settings.matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY; |
| } |
| if (settings.progressive) { |
| fprintf(stderr, "Automatic layered encoding is unsupported in lossless mode.\n"); |
| } |
| } |
| |
| stdinFile.filename = "(stdin)"; |
| stdinFile.duration = settings.outputTiming.duration; |
| |
| avifInputFileSettings emptySettingsReference; |
| memset(&emptySettingsReference, 0, sizeof(emptySettingsReference)); |
| |
| if (!outputFilename) { |
| if (((input.useStdin && (input.filesCount == 1)) || (!input.useStdin && (input.filesCount > 1)))) { |
| --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 (!outputFilename || (input.useStdin && (input.filesCount > 0)) || (!input.useStdin && (input.filesCount < 1))) { |
| syntaxShort(); |
| 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.useStdin) { |
| 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. |
| // Auto tiling (autoTiling) and manual tiling (tileRowsLog2, tileColsLog2) are mutually exclusive, which means: |
| // - At each input, only one of the two shall be set. |
| // - At some input, specify 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. |
| // (auto generation of setting entries happens below) |
| // Since it's a boolean flag, its value must be AVIF_TRUE. |
| assert(fileSettings->autoTiling.value); |
| // Therefore disables 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 autotiling, 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", 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", |
| file->filename); |
| goto cleanup; |
| } |
| |
| if (!fileSettings->autoTiling.set) { |
| fileSettings->autoTiling = boolSettingsEntryOf(AVIF_FALSE); |
| } |
| 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 quantizer = (fileSettings->minQuantizer.value + fileSettings->maxQuantizer.value) / 2; |
| const int quality = ((63 - quantizer) * 100 + 31) / 63; |
| 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 quantizerAlpha = (fileSettings->minQuantizerAlpha.value + fileSettings->maxQuantizerAlpha.value) / 2; |
| const int qualityAlpha = ((63 - quantizerAlpha) * 100 + 31) / 63; |
| fileSettings->qualityAlpha = intSettingsEntryOf(qualityAlpha); |
| } |
| } else { |
| assert(!fileSettings->maxQuantizerAlpha.set); |
| if (!fileSettings->qualityAlpha.set) { |
| fileSettings->qualityAlpha = intSettingsEntryOf(DEFAULT_QUALITY_ALPHA); |
| } |
| 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)) { |
| 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", 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_EXPERIMENTAL_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_EXPERIMENTAL_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, image->yuvFormat, 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 (!avifCropRectConvertCleanApertureBox(&cropRect, &image->clap, image->width, image->height, image->yuvFormat, &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; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R) |
| matrixCoefficientsAreLosslessCompatible |= (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE || |
| image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO); |
| #endif |
| if (!matrixCoefficientsAreLosslessCompatible && !using400) { |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R) |
| 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/15 or x/x/16); Output might not be lossless.\n"); |
| #else |
| fprintf(stderr, "WARNING: [--lossless] Input data was RGB and matrixCoefficients isn't set to identity (--cicp x/x/0); Output might not be lossless.\n"); |
| #endif |
| 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)) { |
| 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; |
| } |
| // TODO(yguyon): Check if it is possible to use frames from a single input file as grid cells. Maybe forbid it. |
| } |
| |
| const char * lossyHint = " (Lossy)"; |
| if (lossless) { |
| lossyHint = " (Lossless)"; |
| } |
| printf("AVIF to be written:%s\n", lossyHint); |
| const avifImage * avif = gridCells ? gridCells[0] : image; |
| avifBool gainMapPresent = AVIF_FALSE; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION) |
| gainMapPresent = (avif->gainMap && avif->gainMap->image); |
| #endif |
| avifImageDump(avif, |
| settings.gridDims[0], |
| settings.gridDims[1], |
| gainMapPresent, |
| 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; |
| } |