| // Copyright 2019 Joe Drago. All rights reserved. | |
| // SPDX-License-Identifier: BSD-2-Clause | |
| #include "avif/avif.h" | |
| #include "avifjpeg.h" | |
| #include "avifpng.h" | |
| #include "avifutil.h" | |
| #include "y4m.h" | |
| #include <assert.h> | |
| #include <inttypes.h> | |
| #include <limits.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #if defined(_WIN32) | |
| // for setmode() | |
| #include <fcntl.h> | |
| #include <io.h> | |
| #endif | |
| #define NEXTARG() \ | |
| if (((argIndex + 1) == argc) || (argv[argIndex + 1][0] == '-')) { \ | |
| fprintf(stderr, "%s requires an argument.", arg); \ | |
| goto cleanup; \ | |
| } \ | |
| arg = argv[++argIndex] | |
| typedef struct | |
| { | |
| avifCodecChoice codecChoice; | |
| int jobs; | |
| int targetSize; | |
| avifBool qualityIsConstrained; // true if quality explicitly set by the user | |
| avifBool qualityAlphaIsConstrained; // true if qualityAlpha explicitly set by the user | |
| int overrideQuality; | |
| int overrideQualityAlpha; | |
| avifBool progressive; // automatic layered encoding (progressive) with single input | |
| avifBool layered; // manual layered encoding by specifying each layer | |
| int layers; | |
| int speed; | |
| avifHeaderFormatFlags headerFormat; | |
| uint64_t creationTime; | |
| uint64_t modificationTime; | |
| avifBool paspPresent; | |
| uint32_t paspValues[2]; | |
| avifBool clapValid; // clapValues may contain 4 crop values and need conversion. In this case clapValid is also false. | |
| uint32_t clapValues[8]; | |
| avifBool gridDimsPresent; | |
| uint32_t gridDims[2]; | |
| avifBool clliPresent; | |
| uint32_t clliValues[2]; | |
| int repetitionCount; | |
| int keyframeInterval; | |
| avifBool ignoreExif; | |
| avifBool ignoreXMP; | |
| avifBool ignoreColorProfile; | |
| avifBool ignoreAlpha; | |
| // These settings are only relevant when compiled with AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION. | |
| avifBool qualityGainMapIsConstrained; // true if qualityGainMap explicitly set by the user | |
| int qualityGainMap; | |
| avifBool ignoreGainMap; // ignore any gain map present in the input file. | |
| // This holds the output timing for image sequences. The timescale member in this struct will | |
| // become the timescale set on avifEncoder, and the duration member will be the default duration | |
| // for any frame that doesn't have a specific duration set on the commandline. See the | |
| // declaration of avifAppSourceTiming for more documentation. | |
| avifAppSourceTiming outputTiming; | |
| avifBool cicpExplicitlySet; | |
| avifColorPrimaries colorPrimaries; | |
| avifTransferCharacteristics transferCharacteristics; | |
| avifMatrixCoefficients matrixCoefficients; | |
| avifChromaDownsampling chromaDownsampling; | |
| avifAppFileFormat inputFormat; | |
| } avifSettings; | |
| typedef struct | |
| { | |
| char ** keys; | |
| char ** values; | |
| int count; | |
| } avifCodecSpecificOptions; | |
| typedef struct avifSettingsEntryInt | |
| { | |
| int value; | |
| avifBool set; | |
| } avifSettingsEntryInt; | |
| static avifSettingsEntryInt intSettingsEntryOf(int value) | |
| { | |
| avifSettingsEntryInt entry = { value, AVIF_TRUE }; | |
| return entry; | |
| } | |
| typedef avifSettingsEntryInt avifSettingsEntryBool; | |
| static avifSettingsEntryBool boolSettingsEntryOf(avifBool value) | |
| { | |
| return intSettingsEntryOf(value); | |
| } | |
| typedef struct avifSettingsEntryScalingMode | |
| { | |
| avifScalingMode value; | |
| avifBool set; | |
| } avifSettingsEntryScalingMode; | |
| static avifSettingsEntryScalingMode scalingModeSettingsEntryOf(uint32_t n, uint32_t d) | |
| { | |
| avifFraction mode = { (int32_t)n, (int32_t)d }; | |
| avifSettingsEntryScalingMode entry = { { mode, mode }, AVIF_TRUE }; | |
| return entry; | |
| } | |
| // Each entry records an "update" action to the corresponding field in avifEncoder. | |
| // Fields in avifEncoder are not reset after encoding an image, | |
| // so updates naturally apply to all following inputs, | |
| // and is only recorded once on the first applicable input. | |
| typedef struct avifInputFileSettings | |
| { | |
| avifSettingsEntryInt quality; | |
| avifSettingsEntryInt qualityAlpha; | |
| avifSettingsEntryInt minQuantizer; | |
| avifSettingsEntryInt maxQuantizer; | |
| avifSettingsEntryInt minQuantizerAlpha; | |
| avifSettingsEntryInt maxQuantizerAlpha; | |
| avifSettingsEntryInt tileRowsLog2; | |
| avifSettingsEntryInt tileColsLog2; | |
| avifSettingsEntryBool autoTiling; | |
| avifSettingsEntryScalingMode scalingMode; | |
| avifCodecSpecificOptions codecSpecificOptions; | |
| } avifInputFileSettings; | |
| #define AVIF_FILENAME_STDIN NULL | |
| static const char * avifPrettyFilename(const char * filename) | |
| { | |
| if (filename == AVIF_FILENAME_STDIN) { | |
| return "(stdin)"; | |
| } | |
| return filename; | |
| } | |
| typedef struct avifInputFile | |
| { | |
| const char * filename; // If AVIF_FILENAME_STDIN, use stdin | |
| uint64_t duration; // If 0, use the default duration | |
| avifInputFileSettings settings; | |
| } avifInputFile; | |
| typedef struct | |
| { | |
| int fileIndex; | |
| avifImage * image; | |
| const avifInputFileSettings * settings; | |
| uint32_t fileBitDepth; | |
| avifBool fileIsRGB; | |
| avifAppSourceTiming sourceTiming; | |
| } avifInputCacheEntry; | |
| typedef struct avifInput | |
| { | |
| avifInputFile * files; | |
| int filesCount; | |
| int fileIndex; | |
| struct y4mFrameIterator * frameIter; | |
| avifPixelFormat requestedFormat; | |
| int requestedDepth; | |
| int requestedDepthExtension; | |
| avifBool cacheEnabled; | |
| avifInputCacheEntry * cache; | |
| int cacheCount; | |
| } avifInput; | |
| typedef struct avifEncodedByteSizes | |
| { | |
| size_t colorSizeBytes; | |
| size_t alphaSizeBytes; | |
| size_t gainMapSizeBytes; | |
| } avifEncodedByteSizes; | |
| static void syntaxShort(void) | |
| { | |
| printf("Syntax: avifenc [options] -q quality input.[jpg|jpeg|png|y4m] output.avif\n"); | |
| printf("where quality is between %d (worst quality) and %d (lossless).\n", AVIF_QUALITY_WORST, AVIF_QUALITY_LOSSLESS); | |
| printf("Typical value is 60-80.\n\n"); | |
| printf("Try -h for an exhaustive list of options.\n"); | |
| } | |
| static void syntaxLong(void) | |
| { | |
| printf("Syntax: avifenc [options] input.[jpg|jpeg|png|y4m] output.avif\n"); | |
| printf("Standard options:\n"); | |
| printf(" -h,--help : Show syntax help (this page)\n"); | |
| printf(" -V,--version : Show the version number\n"); | |
| printf("\n"); | |
| printf("Basic options:\n"); | |
| printf(" -q,--qcolor Q : Quality for color in %d..%d where %d is lossless\n", | |
| AVIF_QUALITY_WORST, | |
| AVIF_QUALITY_BEST, | |
| AVIF_QUALITY_LOSSLESS); | |
| printf(" --qalpha Q : Quality for alpha in %d..%d where %d is lossless\n", | |
| AVIF_QUALITY_WORST, | |
| AVIF_QUALITY_BEST, | |
| AVIF_QUALITY_LOSSLESS); | |
| printf(" -s,--speed S : Encoder speed in %d..%d where 0 is the slowest, 10 is the fastest. Or 'default' or 'd' for codec internal defaults. (Default: 6)\n", | |
| AVIF_SPEED_SLOWEST, | |
| AVIF_SPEED_FASTEST); | |
| printf("\n"); | |
| printf("Advanced options:\n"); | |
| printf(" -j,--jobs J : Number of jobs (worker threads), or 'all' to potentially use as many cores as possible. (Default: all)\n"); | |
| printf(" --no-overwrite : Never overwrite existing output file\n"); | |
| printf(" -o,--output FILENAME : Instead of using the last filename given as output, use this filename\n"); | |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_MINI) | |
| printf(" --mini : EXPERIMENTAL: Use reduced header if possible (backward-incompatible)\n"); | |
| #endif | |
| printf(" -l,--lossless : Set all defaults to encode losslessly, and emit warnings when settings/input don't allow for it\n"); | |
| printf(" -d,--depth D[,Dextension] : D is the output bit depth per channel. D must be 8, 10 or 12. (JPEG/PNG only; y4m: D must match the input bit depth, and Dextension is unsupported)\n"); | |
| printf(" If specified, Dextension adds a hidden encoded image of Dextension bit depth in the same file as the primary image to reach 16-bit depth at decoding.\n"); | |
| printf(" See avifSampleTransformRecipe for the supported combinations (8,8 and 12,4 and 12,8).\n"); | |
| printf(" -y,--yuv FORMAT : Output format, one of 'auto' (default), 444, 422, 420 or 400. Ignored for y4m (y4m format is retained)\n"); | |
| printf(" For JPEG, auto honors the JPEG's internal format, if possible. For grayscale PNG, auto defaults to 400. For all other cases, auto defaults to 444\n"); | |
| printf(" -p,--premultiply : Premultiply color by the alpha channel and signal this in the AVIF\n"); | |
| printf(" --sharpyuv : Use sharp RGB to YUV420 conversion (if supported). Ignored for y4m or if output is not 420.\n"); | |
| printf(" --stdin : Read input from stdin instead of file paths. No other input is allowed. The input format is assumed to be y4m unless --input-format is specified. The output file path must still be provided.\n"); | |
| printf(" --input-format FORMAT : File format of the input data. One of: jpeg/png/y4m/auto. (Default: auto, except for stdin where auto is not supported and the default is y4m)\n"); | |
| printf(" --cicp,--nclx P/T/M : Set CICP values (nclx colr box) (3 raw numbers, use -r to set range flag)\n"); | |
| printf(" P = color primaries\n"); | |
| printf(" T = transfer characteristics\n"); | |
| printf(" M = matrix coefficients\n"); | |
| printf(" Use 2 for any you wish to leave unspecified\n"); | |
| printf(" -r,--range RANGE : YUV range, one of 'limited' or 'l', 'full' or 'f'. (JPEG/PNG only, default: full; For y4m, range is retained)\n"); | |
| printf(" --target-size S : Set target file size in bytes (up to 7 times slower)\n"); | |
| printf(" --progressive : Automatically set parameters to encode a simple layered image supporting progressive rendering from a single input frame.\n"); | |
| printf(" --layered : Encode a layered AVIF. Each input is encoded as one layer and at most %d layers can be encoded.\n", | |
| AVIF_MAX_AV1_LAYER_COUNT); | |
| printf(" -g,--grid MxN : Encode a single-image grid AVIF with M cols & N rows. Either supply MxN identical W/H/D images, or a single\n"); | |
| printf(" image that can be evenly split into the MxN grid and follow AVIF grid image restrictions. The grid will adopt\n"); | |
| printf(" the color profile of the first image supplied.\n"); | |
| printf(" -c,--codec C : Codec to use (choose from versions list below)\n"); | |
| printf(" --exif FILENAME : Provide an Exif metadata payload to be associated with the primary item (implies --ignore-exif)\n"); | |
| printf(" --xmp FILENAME : Provide an XMP metadata payload to be associated with the primary item (implies --ignore-xmp)\n"); | |
| printf(" --icc FILENAME : Provide an ICC profile payload to be associated with the primary item (implies --ignore-icc)\n"); | |
| printf(" --timescale,--fps V : Timescale for image sequences. If all frames are 1 timescale in length, this is equivalent to frames per second. (Default: 30)\n"); | |
| printf(" If neither duration nor timescale are set, avifenc will attempt to use the framerate stored in a y4m header, if present.\n"); | |
| printf(" --creation-time : Creation time for image sequences, in seconds since 1970-01-01 00:00:00 UTC (the Unix epoch). (Default: 0, use the modification time)\n"); | |
| printf(" --modification-time : Modification time for image sequences, in seconds since 1970-01-01 00:00:00 UTC (the Unix epoch). (Default: 0, use the current time)\n"); | |
| printf(" -k,--keyframe INTERVAL : Maximum keyframe interval for image sequences (any set of INTERVAL consecutive frames will have at least one keyframe). Set to 0 to disable (default).\n"); | |
| printf(" --ignore-exif : If the input file contains embedded Exif metadata, ignore it (no-op if absent)\n"); | |
| printf(" --ignore-xmp : If the input file contains embedded XMP metadata, ignore it (no-op if absent)\n"); | |
| printf(" --ignore-profile,--ignore-icc : If the input file contains an embedded color profile, ignore it (no-op if absent)\n"); | |
| printf(" --ignore-alpha : If the input file contains an alpha channel, ignore it (no-op if absent)\n"); | |
| #if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) | |
| printf(" --ignore-gain-map : If the input file contains an embedded gain map, ignore it (no-op if absent)\n"); | |
| printf(" --qgain-map Q : Quality for the gain map in %d..%d where %d is lossless\n", | |
| AVIF_QUALITY_WORST, | |
| AVIF_QUALITY_BEST, | |
| AVIF_QUALITY_LOSSLESS); | |
| #endif | |
| printf(" --pasp H,V : Add pasp property (aspect ratio). H=horizontal spacing, V=vertical spacing\n"); | |
| printf(" --crop CROPX,CROPY,CROPW,CROPH : Add clap property (clean aperture), but calculated from a crop rectangle\n"); | |
| printf(" --clap WN,WD,HN,HD,HON,HOD,VON,VOD: Add clap property (clean aperture). Width, Height, HOffset, VOffset (in numerator/denominator pairs)\n"); | |
| printf(" --irot ANGLE : Add irot property (rotation), in 0..3. Makes (90 * ANGLE) degree rotation anti-clockwise\n"); | |
| printf(" --imir AXIS : Add imir property (mirroring). 0=top-to-bottom, 1=left-to-right\n"); | |
| printf(" --clli MaxCLL,MaxPALL : Add clli property (content light level information).\n"); | |
| printf(" --repetition-count N : Number of times an animated image sequence will be repeated, or 'infinite' for infinite repetitions. (Default: infinite)\n"); | |
| printf(" -- : Signal the end of options. Everything after this is interpreted as file names.\n"); | |
| printf("\n"); | |
| printf("Updatable options:\n"); | |
| printf(" The following options can optionally have a :u (or :update) suffix like `-q:u Q`, to apply only to input files appearing after the option:\n"); | |
| printf(" -q,--qcolor Q : Quality for color in %d..%d where %d is lossless\n", | |
| AVIF_QUALITY_WORST, | |
| AVIF_QUALITY_BEST, | |
| AVIF_QUALITY_LOSSLESS); | |
| printf(" --qalpha Q : Quality for alpha in %d..%d where %d is lossless\n", | |
| AVIF_QUALITY_WORST, | |
| AVIF_QUALITY_BEST, | |
| AVIF_QUALITY_LOSSLESS); | |
| #if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) | |
| printf(" --qgain-map Q : Quality for the gain map in %d..%d where %d is lossless\n", | |
| AVIF_QUALITY_WORST, | |
| AVIF_QUALITY_BEST, | |
| AVIF_QUALITY_LOSSLESS); | |
| #endif | |
| printf(" --tilerowslog2 R : log2 of number of tile rows in 0..6. (Default: 0)\n"); | |
| printf(" If specified, switch to manual tiling.\n"); | |
| printf(" --tilecolslog2 C : log2 of number of tile columns 0..6. (Default: 0)\n"); | |
| printf(" If specified, switch to manual tiling.\n"); | |
| printf(" --autotiling : Set --tilerowslog2 and --tilecolslog2 automatically\n"); | |
| printf(" If specified, switch to automatic tiling.\n"); | |
| printf(" avifenc starts in automatic tiling mode.\n"); | |
| printf(" --min QP : Deprecated, use -q 0..100 instead\n"); | |
| printf(" --max QP : Deprecated, use -q 0..100 instead\n"); | |
| printf(" --minalpha QP : Deprecated, use --qalpha 0..100 instead\n"); | |
| printf(" --maxalpha QP : Deprecated, use --qalpha 0..100 instead\n"); | |
| printf(" --scaling-mode N[/D] : Set frame (layer) scaling mode as given fraction. If omitted, the denominator defaults to 1. (Default: 1/1)\n"); | |
| printf(" --duration D : Frame durations (in timescales) (default: 1). This option always applies to following inputs with or without the `:u` suffix.\n"); | |
| printf(" -a,--advanced KEY[=VALUE] : Pass an advanced, codec-specific key/value string pair directly to the codec. avifenc will warn on any not used by the codec.\n"); | |
| printf("\n"); | |
| if (avifCodecName(AVIF_CODEC_CHOICE_AOM, 0)) { | |
| printf("aom-specific advanced options:\n"); | |
| printf(" 1. <key>=<value> applies to both the color (YUV) planes and the alpha plane (if present).\n"); | |
| printf(" 2. color:<key>=<value> or c:<key>=<value> applies only to the color (YUV) planes.\n"); | |
| printf(" 3. alpha:<key>=<value> or a:<key>=<value> applies only to the alpha plane (if present).\n"); | |
| printf(" Since the alpha plane is encoded as a monochrome image, the options that refer to the chroma planes,\n"); | |
| printf(" such as enable-chroma-deltaq=B, should not be used with the alpha plane. In addition, the film grain\n"); | |
| printf(" options are unlikely to make sense for the alpha plane.\n"); | |
| printf("\n"); | |
| printf(" When used with libaom 3.0.0 or later, any key-value pairs supported by the aom_codec_set_option() function\n"); | |
| printf(" can be used. When used with libaom 2.0.x or older, the following key-value pairs can be used:\n"); | |
| printf("\n"); | |
| printf(" aq-mode=M : Adaptive quantization mode. 0=off (default), 1=variance, 2=complexity, 3=cyclic refresh\n"); | |
| printf(" cq-level=Q : Constant/Constrained Quality level in 0..63, end-usage must be set to cq or q\n"); | |
| printf(" enable-chroma-deltaq=B : Enable delta quantization in chroma planes. 0=disable (default), 1=enable\n"); | |
| printf(" end-usage=MODE : Rate control mode, one of 'vbr', 'cbr', 'cq', or 'q'\n"); | |
| printf(" sharpness=S : Bias towards block sharpness in rate-distortion optimization of transform coefficients in 0..7. (Default: 0)\n"); | |
| printf(" tune=METRIC : Tune the encoder for distortion metric, one of 'psnr', 'ssim' or 'iq'.\n"); | |
| printf(" (Default for color: still non-RGB images (libaom v3.13.0+): iq, otherwise: ssim; default for alpha: psnr)\n"); | |
| printf(" film-grain-test=TEST : Film grain test vectors in 0..16. 0=none (default), 1=test1, 2=test2, ... 16=test16\n"); | |
| printf(" film-grain-table=FILENAME : Path to file containing film grain parameters\n"); | |
| printf("\n"); | |
| } | |
| avifPrintVersions(); | |
| } | |
| // This is *very* arbitrary, I just want to set people's expectations a bit | |
| static const char * qualityString(int quality) | |
| { | |
| if (quality == AVIF_QUALITY_LOSSLESS) { | |
| return "Lossless"; | |
| } | |
| if (quality >= 80) { | |
| return "High"; | |
| } | |
| if (quality >= 50) { | |
| return "Medium"; | |
| } | |
| if (quality == AVIF_QUALITY_WORST) { | |
| return "Worst"; | |
| } | |
| return "Low"; | |
| } | |
| // Parse exactly n uint32_t from arg with separator character delim. | |
| // Output must be able to hold at least n elements. | |
| // Length of arg shall not exceed 127 characters or result can be truncated. | |
| static avifBool parseU32List(uint32_t output[], int n, const char * arg, const char delim) | |
| { | |
| char buffer[128]; | |
| strncpy(buffer, arg, 127); | |
| buffer[127] = 0; | |
| // strtok wants a string for delim, so build a single character string here. | |
| const char delims[2] = { delim, '\0' }; | |
| int index = 0; | |
| char * token = buffer; | |
| if (n > 1) { | |
| token = strtok(buffer, delims); | |
| } | |
| while (token != NULL) { | |
| output[index] = (uint32_t)atoi(token); | |
| ++index; | |
| if (index >= n) { | |
| break; | |
| } | |
| token = strtok(NULL, delims); | |
| } | |
| // Exactly n, and no more separator character | |
| if (index == n && strchr(token, delim) == NULL) { | |
| return AVIF_TRUE; | |
| } | |
| return AVIF_FALSE; | |
| } | |
| typedef enum avifOptionSuffixType | |
| { | |
| AVIF_OPTION_SUFFIX_NONE, | |
| AVIF_OPTION_SUFFIX_UPDATE, | |
| AVIF_OPTION_SUFFIX_INVALID, | |
| } avifOptionSuffixType; | |
| static avifOptionSuffixType parseOptionSuffix(const char * arg, avifBool warnNoSuffix) | |
| { | |
| const char * suffix = strchr(arg, ':'); | |
| if (suffix == NULL) { | |
| if (warnNoSuffix) { | |
| fprintf(stderr, | |
| "WARNING: %s is applying to all inputs. Use %s:u to apply only to inputs after it, " | |
| "or move it before first input to avoid ambiguity.\n", | |
| arg, | |
| arg); | |
| } | |
| return AVIF_OPTION_SUFFIX_NONE; | |
| } | |
| if (!strcmp(suffix, ":u") || !strcmp(suffix, ":update")) { | |
| return AVIF_OPTION_SUFFIX_UPDATE; | |
| } | |
| fprintf(stderr, "ERROR: Unknown option suffix in flag %s.\n", arg); | |
| return AVIF_OPTION_SUFFIX_INVALID; | |
| } | |
| static avifBool strpre(const char * str, const char * prefix) | |
| { | |
| return strncmp(str, prefix, strlen(prefix)) == 0; | |
| } | |
| static avifBool convertCropToClap(uint32_t srcW, uint32_t srcH, uint32_t clapValues[8]) | |
| { | |
| avifCleanApertureBox clap; | |
| avifCropRect cropRect; | |
| cropRect.x = clapValues[0]; | |
| cropRect.y = clapValues[1]; | |
| cropRect.width = clapValues[2]; | |
| cropRect.height = clapValues[3]; | |
| avifDiagnostics diag; | |
| avifDiagnosticsClearError(&diag); | |
| avifBool convertResult = avifCleanApertureBoxFromCropRect(&clap, &cropRect, srcW, srcH, &diag); | |
| if (!convertResult) { | |
| fprintf(stderr, | |
| "ERROR: Impossible crop rect: imageSize:[%ux%u], cropRect:[%u,%u, %ux%u] - %s\n", | |
| srcW, | |
| srcH, | |
| cropRect.x, | |
| cropRect.y, | |
| cropRect.width, | |
| cropRect.height, | |
| diag.error); | |
| return convertResult; | |
| } | |
| clapValues[0] = clap.widthN; | |
| clapValues[1] = clap.widthD; | |
| clapValues[2] = clap.heightN; | |
| clapValues[3] = clap.heightD; | |
| clapValues[4] = clap.horizOffN; | |
| clapValues[5] = clap.horizOffD; | |
| clapValues[6] = clap.vertOffN; | |
| clapValues[7] = clap.vertOffD; | |
| return AVIF_TRUE; | |
| } | |
| static avifBool avifInputAddCachedImage(avifInput * input) | |
| { | |
| avifImage * newImage = avifImageCreateEmpty(); | |
| if (!newImage) { | |
| return AVIF_FALSE; | |
| } | |
| avifInputCacheEntry * newCachedImages = malloc((input->cacheCount + 1) * sizeof(*input->cache)); | |
| if (!newCachedImages) { | |
| avifImageDestroy(newImage); | |
| return AVIF_FALSE; | |
| } | |
| avifInputCacheEntry * oldCachedImages = input->cache; | |
| input->cache = newCachedImages; | |
| if (input->cacheCount) { | |
| memcpy(input->cache, oldCachedImages, input->cacheCount * sizeof(*input->cache)); | |
| } | |
| memset(&input->cache[input->cacheCount], 0, sizeof(input->cache[input->cacheCount])); | |
| input->cache[input->cacheCount].fileIndex = input->fileIndex; | |
| input->cache[input->cacheCount].image = newImage; | |
| ++input->cacheCount; | |
| free(oldCachedImages); | |
| return AVIF_TRUE; | |
| } | |
| static avifBool fileExists(const char * filename) | |
| { | |
| FILE * outfile = fopen(filename, "rb"); | |
| if (outfile) { | |
| fclose(outfile); | |
| return AVIF_TRUE; | |
| } | |
| return AVIF_FALSE; | |
| } | |
| static const avifInputFile * avifInputGetFile(const avifInput * input, int imageIndex) | |
| { | |
| if (imageIndex < input->cacheCount) { | |
| return &input->files[input->cache[imageIndex].fileIndex]; | |
| } | |
| if (input->fileIndex >= input->filesCount) { | |
| return NULL; | |
| } | |
| if (input->files[input->fileIndex].filename == AVIF_FILENAME_STDIN) { | |
| assert(input->fileIndex == 0); | |
| ungetc(fgetc(stdin), stdin); // Kick stdin to force EOF | |
| if (feof(stdin)) { | |
| return NULL; | |
| } | |
| } | |
| return &input->files[input->fileIndex]; | |
| } | |
| static avifBool avifInputHasRemainingData(const avifInput * input, int imageIndex) | |
| { | |
| if (imageIndex < input->cacheCount) { | |
| return AVIF_TRUE; | |
| } | |
| if (input->fileIndex >= input->filesCount) { | |
| return AVIF_FALSE; | |
| } | |
| if (input->files[input->fileIndex].filename == AVIF_FILENAME_STDIN) { | |
| return !feof(stdin); | |
| } | |
| return AVIF_TRUE; | |
| } | |
| static avifBool avifInputReadImage(avifInput * input, | |
| int imageIndex, | |
| avifBool ignoreColorProfile, | |
| avifBool ignoreExif, | |
| avifBool ignoreXMP, | |
| avifBool allowChangingCicp, | |
| avifBool ignoreAlpha, | |
| avifBool ignoreGainMap, | |
| avifImage * image, | |
| const avifInputFileSettings ** settings, | |
| uint32_t * outDepth, | |
| avifBool * sourceIsRGB, | |
| avifAppSourceTiming * sourceTiming, | |
| avifChromaDownsampling chromaDownsampling, | |
| avifAppFileFormat inputFormat) | |
| { | |
| if (imageIndex < input->cacheCount) { | |
| const avifInputCacheEntry * cached = &input->cache[imageIndex]; | |
| const avifCropRect rect = { 0, 0, cached->image->width, cached->image->height }; | |
| if (avifImageSetViewRect(image, cached->image, &rect) != AVIF_RESULT_OK) { | |
| assert(AVIF_FALSE); | |
| } | |
| #if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) | |
| if (cached->image->gainMap && cached->image->gainMap->image) { | |
| image->gainMap->image = avifImageCreateEmpty(); | |
| if (!image->gainMap->image) { | |
| fprintf(stderr, "ERROR: Out of memory\n"); | |
| return AVIF_FALSE; | |
| } | |
| 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\n"); | |
| return AVIF_FALSE; | |
| } | |
| assert(imageIndex + 1 == input->cacheCount); | |
| dstImage = input->cache[imageIndex].image; | |
| // Copy CICP, clap etc. | |
| if (avifImageCopy(dstImage, image, /*planes=*/0) != AVIF_RESULT_OK) { | |
| assert(AVIF_FALSE); | |
| } | |
| dstSettings = &input->cache[imageIndex].settings; | |
| dstDepth = &input->cache[imageIndex].fileBitDepth; | |
| dstSourceIsRGB = &input->cache[imageIndex].fileIsRGB; | |
| dstSourceTiming = &input->cache[imageIndex].sourceTiming; | |
| } | |
| if (dstSourceTiming) { | |
| // A source timing of all 0s is a sentinel value hinting that the value is unset / should be | |
| // ignored. This is memset here as many of the paths in avifInputReadImage() do not set these | |
| // values. See the declaration for avifAppSourceTiming for more information. | |
| memset(dstSourceTiming, 0, sizeof(avifAppSourceTiming)); | |
| } | |
| if (input->fileIndex >= input->filesCount) { | |
| return AVIF_FALSE; | |
| } | |
| const avifInputFile * currentFile = &input->files[input->fileIndex]; | |
| if (currentFile->filename == AVIF_FILENAME_STDIN) { | |
| if (feof(stdin)) { | |
| return AVIF_FALSE; | |
| } | |
| if (inputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) { | |
| inputFormat = AVIF_APP_FILE_FORMAT_Y4M; | |
| } | |
| } | |
| const avifColorPrimaries colorPrimariesBefore = dstImage->colorPrimaries; | |
| const avifTransferCharacteristics transferCharacteristicsBefore = dstImage->transferCharacteristics; | |
| const avifAppFileFormat actualInputFormat = avifReadImage(currentFile->filename, | |
| inputFormat, | |
| input->requestedFormat, | |
| input->requestedDepthExtension == 0 ? input->requestedDepth : 16, | |
| chromaDownsampling, | |
| ignoreColorProfile, | |
| ignoreExif, | |
| ignoreXMP, | |
| ignoreAlpha, | |
| ignoreGainMap, | |
| UINT32_MAX, | |
| dstImage, | |
| dstDepth, | |
| dstSourceTiming, | |
| &input->frameIter); | |
| if (actualInputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) { | |
| fprintf(stderr, | |
| "ERROR: Couldn't read %s file %s\n", | |
| avifFileFormatToString(inputFormat), | |
| currentFile->filename == AVIF_FILENAME_STDIN ? "from standard input" : currentFile->filename); | |
| if (currentFile->filename == AVIF_FILENAME_STDIN && inputFormat == AVIF_APP_FILE_FORMAT_Y4M) { | |
| fprintf(stderr, "Specify --input-format if the standard input is not Y4M\n"); | |
| } | |
| return AVIF_FALSE; | |
| } | |
| if (actualInputFormat == AVIF_APP_FILE_FORMAT_Y4M) { | |
| if (input->requestedDepthExtension) { | |
| fprintf(stderr, "ERROR: --depth %d,%d is not supported for Y4M input\n", input->requestedDepth, input->requestedDepthExtension); | |
| return AVIF_FALSE; | |
| } | |
| if (input->requestedDepth && (input->requestedDepth != (int)dstImage->depth)) { | |
| fprintf(stderr, | |
| "ERROR: --depth %d does not match Y4M bit depth %u; Y4M bit-depth conversion is not supported\n", | |
| input->requestedDepth, | |
| dstImage->depth); | |
| return AVIF_FALSE; | |
| } | |
| } | |
| if (!allowChangingCicp) { | |
| // Restore the previous primaries/transfer in case avifReadImage changed them. | |
| dstImage->colorPrimaries = colorPrimariesBefore; | |
| dstImage->transferCharacteristics = transferCharacteristicsBefore; | |
| } | |
| if (!input->frameIter) { | |
| ++input->fileIndex; | |
| } | |
| if (dstSourceIsRGB) { | |
| *dstSourceIsRGB = (actualInputFormat != AVIF_APP_FILE_FORMAT_Y4M); | |
| } | |
| 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, | |
| ignoreAlpha, | |
| ignoreGainMap, | |
| image, | |
| settings, | |
| outDepth, | |
| sourceIsRGB, | |
| sourceTiming, | |
| chromaDownsampling, | |
| inputFormat); | |
| } | |
| return AVIF_TRUE; | |
| } | |
| // Returns NULL if a memory allocation failed. The return value should be freed with free(). | |
| static char * avifStrdup(const char * str) | |
| { | |
| size_t len = strlen(str); | |
| char * dup = malloc(len + 1); | |
| if (!dup) { | |
| return NULL; | |
| } | |
| memcpy(dup, str, len + 1); | |
| return dup; | |
| } | |
| // key is not null-terminated. value is null-terminated. | |
| static avifBool avifCodecSpecificOptionsAddKeyValue(avifCodecSpecificOptions * options, const char * key, size_t keyLength, const char * value) | |
| { | |
| avifBool success = AVIF_FALSE; | |
| char ** oldKeys = options->keys; | |
| char ** oldValues = options->values; | |
| options->keys = malloc((options->count + 1) * sizeof(*options->keys)); | |
| options->values = malloc((options->count + 1) * sizeof(*options->values)); | |
| if (!options->keys || !options->values) { | |
| free(options->keys); | |
| free(options->values); | |
| options->keys = oldKeys; | |
| options->values = oldValues; | |
| return AVIF_FALSE; | |
| } | |
| if (options->count) { | |
| memcpy(options->keys, oldKeys, options->count * sizeof(*options->keys)); | |
| memcpy(options->values, oldValues, options->count * sizeof(*options->values)); | |
| } | |
| options->values[options->count] = avifStrdup(value); | |
| options->keys[options->count] = malloc(keyLength + 1); | |
| if (!options->values[options->count] || !options->keys[options->count]) { | |
| goto cleanup; | |
| } | |
| memcpy(options->keys[options->count], key, keyLength); | |
| options->keys[options->count][keyLength] = '\0'; | |
| success = AVIF_TRUE; | |
| cleanup: | |
| ++options->count; | |
| free(oldKeys); | |
| free(oldValues); | |
| return success; | |
| } | |
| static avifBool avifCodecSpecificOptionsAdd(avifCodecSpecificOptions * options, const char * keyValue) | |
| { | |
| const char * value = strchr(keyValue, '='); | |
| if (value) { | |
| // Keep the parts on the left and on the right of the equal sign, | |
| // but not the equal sign itself. | |
| const size_t keyLength = value - keyValue; | |
| return avifCodecSpecificOptionsAddKeyValue(options, keyValue, keyLength, value + 1); | |
| } | |
| // Pass in a non-NULL, empty string. Codecs can use the mere existence of a key as a boolean value. | |
| return avifCodecSpecificOptionsAddKeyValue(options, keyValue, strlen(keyValue), ""); | |
| } | |
| static void avifCodecSpecificOptionsFree(avifCodecSpecificOptions * options) | |
| { | |
| while (options->count) { | |
| --options->count; | |
| free(options->keys[options->count]); | |
| free(options->values[options->count]); | |
| } | |
| free(options->keys); | |
| free(options->values); | |
| options->keys = NULL; | |
| options->values = NULL; | |
| } | |
| static void avifSettingsEntryIntOverwrite(avifSettingsEntryInt * dst, const avifSettingsEntryInt * src) | |
| { | |
| if (src->set) { | |
| dst->set = AVIF_TRUE; | |
| dst->value = src->value; | |
| } | |
| } | |
| static avifBool avifInputFileSettingsOverwrite(avifInputFileSettings * dst, const avifInputFileSettings * src) | |
| { | |
| avifSettingsEntryIntOverwrite(&dst->quality, &src->quality); | |
| avifSettingsEntryIntOverwrite(&dst->qualityAlpha, &src->qualityAlpha); | |
| avifSettingsEntryIntOverwrite(&dst->minQuantizer, &src->minQuantizer); | |
| avifSettingsEntryIntOverwrite(&dst->maxQuantizer, &src->maxQuantizer); | |
| avifSettingsEntryIntOverwrite(&dst->minQuantizerAlpha, &src->minQuantizerAlpha); | |
| avifSettingsEntryIntOverwrite(&dst->maxQuantizerAlpha, &src->maxQuantizerAlpha); | |
| avifSettingsEntryIntOverwrite(&dst->tileRowsLog2, &src->tileRowsLog2); | |
| avifSettingsEntryIntOverwrite(&dst->tileColsLog2, &src->tileColsLog2); | |
| avifSettingsEntryIntOverwrite(&dst->autoTiling, &src->autoTiling); | |
| if (src->scalingMode.set) { | |
| dst->scalingMode.set = AVIF_TRUE; | |
| dst->scalingMode.value = src->scalingMode.value; | |
| } | |
| for (int i = 0; i < src->codecSpecificOptions.count; ++i) { | |
| const char * key = src->codecSpecificOptions.keys[i]; | |
| const char * value = src->codecSpecificOptions.values[i]; | |
| if (!avifCodecSpecificOptionsAddKeyValue(&dst->codecSpecificOptions, key, strlen(key), value)) { | |
| fprintf(stderr, "ERROR: Failed to copy codec specific option: %s = %s\n", key, value); | |
| return AVIF_FALSE; | |
| } | |
| } | |
| return AVIF_TRUE; | |
| } | |
| #define INVALID_QUALITY (-1) | |
| #define DEFAULT_QUALITY 60 // Maps to a quantizer (QP) of 25. | |
| #define DEFAULT_QUALITY_GAIN_MAP DEFAULT_QUALITY | |
| #define PROGRESSIVE_WORST_QUALITY 10 // Not doing automatic layered encoding below this quality | |
| #define PROGRESSIVE_START_QUALITY 2 // First layer use this quality | |
| static avifBool avifEncodeUpdateEncoderSettings(avifEncoder * encoder, const avifInputFileSettings * settings) | |
| { | |
| if (!settings) { | |
| return AVIF_TRUE; | |
| } | |
| if (settings->quality.set) { | |
| encoder->quality = settings->quality.value; | |
| } | |
| if (settings->qualityAlpha.set) { | |
| encoder->qualityAlpha = settings->qualityAlpha.value; | |
| } | |
| if (settings->minQuantizer.set) { | |
| encoder->minQuantizer = settings->minQuantizer.value; | |
| } | |
| if (settings->maxQuantizer.set) { | |
| encoder->maxQuantizer = settings->maxQuantizer.value; | |
| } | |
| if (settings->minQuantizerAlpha.set) { | |
| encoder->minQuantizerAlpha = settings->minQuantizerAlpha.value; | |
| } | |
| if (settings->maxQuantizerAlpha.set) { | |
| encoder->maxQuantizerAlpha = settings->maxQuantizerAlpha.value; | |
| } | |
| if (settings->tileRowsLog2.set) { | |
| encoder->tileRowsLog2 = settings->tileRowsLog2.value; | |
| } | |
| if (settings->tileColsLog2.set) { | |
| encoder->tileColsLog2 = settings->tileColsLog2.value; | |
| } | |
| if (settings->autoTiling.set) { | |
| encoder->autoTiling = settings->autoTiling.value; | |
| } | |
| if (settings->scalingMode.set) { | |
| encoder->scalingMode = settings->scalingMode.value; | |
| } | |
| for (int i = 0; i < settings->codecSpecificOptions.count; ++i) { | |
| if (avifEncoderSetCodecSpecificOption(encoder, settings->codecSpecificOptions.keys[i], settings->codecSpecificOptions.values[i]) != | |
| AVIF_RESULT_OK) { | |
| fprintf(stderr, | |
| "ERROR: Failed to set codec specific option: %s = %s\n", | |
| settings->codecSpecificOptions.keys[i], | |
| settings->codecSpecificOptions.values[i]); | |
| return AVIF_FALSE; | |
| } | |
| } | |
| return AVIF_TRUE; | |
| } | |
| static avifBool avifEncoderVerifyImageCompatibility(const avifImage * refImage, | |
| const avifImage * testImage, | |
| const char * seriesType, | |
| const char * filename) | |
| { | |
| // Verify that this frame's properties matches the first frame's properties | |
| if ((refImage->width != testImage->width) || (refImage->height != testImage->height)) { | |
| fprintf(stderr, | |
| "ERROR: Image %s dimensions mismatch, [%ux%u] vs [%ux%u]: %s\n", | |
| seriesType, | |
| refImage->width, | |
| refImage->height, | |
| testImage->width, | |
| testImage->height, | |
| filename); | |
| return AVIF_FALSE; | |
| } | |
| if (refImage->depth != testImage->depth) { | |
| fprintf(stderr, "ERROR: Image %s depth mismatch, [%u] vs [%u]: %s\n", seriesType, refImage->depth, testImage->depth, filename); | |
| return AVIF_FALSE; | |
| } | |
| if ((refImage->colorPrimaries != testImage->colorPrimaries) || | |
| (refImage->transferCharacteristics != testImage->transferCharacteristics) || | |
| (refImage->matrixCoefficients != testImage->matrixCoefficients)) { | |
| fprintf(stderr, | |
| "ERROR: Image %s CICP mismatch, [%u/%u/%u] vs [%u/%u/%u]: %s\n", | |
| seriesType, | |
| refImage->colorPrimaries, | |
| refImage->matrixCoefficients, | |
| refImage->transferCharacteristics, | |
| testImage->colorPrimaries, | |
| testImage->transferCharacteristics, | |
| testImage->matrixCoefficients, | |
| filename); | |
| return AVIF_FALSE; | |
| } | |
| if (refImage->yuvRange != testImage->yuvRange) { | |
| fprintf(stderr, | |
| "ERROR: Image %s range mismatch, [%s] vs [%s]: %s\n", | |
| seriesType, | |
| (refImage->yuvRange == AVIF_RANGE_FULL) ? "Full" : "Limited", | |
| (testImage->yuvRange == AVIF_RANGE_FULL) ? "Full" : "Limited", | |
| filename); | |
| return AVIF_FALSE; | |
| } | |
| return AVIF_TRUE; | |
| } | |
| static avifBool avifEncodeRestOfImageSequence(avifEncoder * encoder, | |
| const avifSettings * settings, | |
| avifInput * input, | |
| int imageIndex, | |
| const avifImage * firstImage) | |
| { | |
| avifBool success = AVIF_FALSE; | |
| avifImage * nextImage = NULL; | |
| const avifInputFileSettings * nextSettings = NULL; | |
| const avifInputFile * nextFile; | |
| while ((nextFile = avifInputGetFile(input, imageIndex)) != NULL) { | |
| uint64_t nextDurationInTimescales = nextFile->duration ? nextFile->duration : settings->outputTiming.duration; | |
| if (nextImage) { | |
| avifImageDestroy(nextImage); | |
| } | |
| nextImage = avifImageCreateEmpty(); | |
| if (!nextImage) { | |
| fprintf(stderr, "ERROR: Out of memory\n"); | |
| goto cleanup; | |
| } | |
| nextImage->colorPrimaries = firstImage->colorPrimaries; | |
| nextImage->transferCharacteristics = firstImage->transferCharacteristics; | |
| nextImage->matrixCoefficients = firstImage->matrixCoefficients; | |
| nextImage->yuvRange = firstImage->yuvRange; | |
| nextImage->alphaPremultiplied = firstImage->alphaPremultiplied; | |
| // Ignore ICC, Exif and XMP because only the metadata of the first frame is taken into | |
| // account by the libavif API. | |
| // Ignore gain map as it's not supported for sequences. | |
| if (!avifInputReadImage(input, | |
| imageIndex, | |
| /*ignoreColorProfile=*/AVIF_TRUE, | |
| /*ignoreExif=*/AVIF_TRUE, | |
| /*ignoreXMP=*/AVIF_TRUE, | |
| /*allowChangingCicp=*/AVIF_FALSE, | |
| settings->ignoreAlpha, | |
| /*ignoreGainMap=*/AVIF_TRUE, | |
| nextImage, | |
| &nextSettings, | |
| /*outDepth=*/NULL, | |
| /*sourceIsRGB=*/NULL, | |
| /*sourceTiming=*/NULL, | |
| settings->chromaDownsampling, | |
| settings->inputFormat)) { | |
| goto cleanup; | |
| } | |
| if (!avifEncoderVerifyImageCompatibility(firstImage, nextImage, "sequence", avifPrettyFilename(nextFile->filename))) { | |
| goto cleanup; | |
| } | |
| if (!avifEncodeUpdateEncoderSettings(encoder, nextSettings)) { | |
| goto cleanup; | |
| } | |
| char manualTilingStr[128]; | |
| snprintf(manualTilingStr, sizeof(manualTilingStr), "tileRowsLog2 [%d], tileColsLog2 [%d]", encoder->tileRowsLog2, encoder->tileColsLog2); | |
| printf(" * Encoding frame %d [%" PRIu64 "/%" PRIu64 " ts] color quality [%d (%s)], alpha quality [%d (%s)], %s: %s\n", | |
| imageIndex, | |
| nextDurationInTimescales, | |
| settings->outputTiming.timescale, | |
| encoder->quality, | |
| qualityString(encoder->quality), | |
| encoder->qualityAlpha, | |
| qualityString(encoder->qualityAlpha), | |
| encoder->autoTiling ? "automatic tiling" : manualTilingStr, | |
| avifPrettyFilename(nextFile->filename)); | |
| const avifResult nextImageResult = avifEncoderAddImage(encoder, nextImage, nextDurationInTimescales, AVIF_ADD_IMAGE_FLAG_NONE); | |
| if (nextImageResult != AVIF_RESULT_OK) { | |
| fprintf(stderr, "ERROR: Failed to encode image: %s\n", avifResultToString(nextImageResult)); | |
| goto cleanup; | |
| } | |
| ++imageIndex; | |
| } | |
| success = AVIF_TRUE; | |
| cleanup: | |
| if (nextImage) { | |
| avifImageDestroy(nextImage); | |
| } | |
| return success; | |
| } | |
| static avifBool avifEncodeRestOfLayeredImage(avifEncoder * encoder, | |
| const avifSettings * settings, | |
| avifInput * input, | |
| int layerIndex, | |
| const avifImage * firstImage) | |
| { | |
| avifBool success = AVIF_FALSE; | |
| int layers = encoder->extraLayerCount + 1; | |
| // --progressive only allows one input, so directly read from it. | |
| int targetQuality = (settings->overrideQuality != INVALID_QUALITY) ? settings->overrideQuality | |
| : input->files[0].settings.quality.value; | |
| avifImage * nextImage = NULL; | |
| const avifImage * encodingImage = firstImage; | |
| const avifInputFileSettings * nextSettings = NULL; | |
| if (settings->progressive && avifInputHasRemainingData(input, layerIndex)) { | |
| fprintf(stderr, "ERROR: Automatic layered encoding can only have one input image.\n"); | |
| goto cleanup; | |
| } | |
| while (layerIndex < layers) { | |
| if (settings->progressive) { | |
| // reversed lerp, so that last layer reaches exact targetQuality | |
| encoder->quality = targetQuality - (targetQuality - PROGRESSIVE_START_QUALITY) * | |
| (encoder->extraLayerCount - layerIndex) / encoder->extraLayerCount; | |
| // We scaled the first layer by a half (numerator: 1, denominator: 2). | |
| // Don't perform any scaling for the second layer (numerator: 1, denominator: 1). | |
| const avifScalingMode scalingMode = { { 1, 1 }, { 1, 1 } }; | |
| encoder->scalingMode = scalingMode; | |
| } 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, | |
| settings->ignoreAlpha, | |
| /*ignoreGainMap=*/AVIF_TRUE, | |
| nextImage, | |
| &nextSettings, | |
| /*outDepth=*/NULL, | |
| /*sourceIsRGB=*/NULL, | |
| /*sourceTiming=*/NULL, | |
| settings->chromaDownsampling, | |
| settings->inputFormat)) { | |
| goto cleanup; | |
| } | |
| // frameIter is NULL if y4m reached end, so single frame y4m is still supported. | |
| if (input->frameIter) { | |
| fprintf(stderr, | |
| "ERROR: Layered encoding does not support input with multiple frames: %s.\n", | |
| avifPrettyFilename(nextFile->filename)); | |
| goto cleanup; | |
| } | |
| if (!avifEncoderVerifyImageCompatibility(firstImage, nextImage, "layer", avifPrettyFilename(nextFile->filename))) { | |
| goto cleanup; | |
| } | |
| if (!avifEncodeUpdateEncoderSettings(encoder, nextSettings)) { | |
| goto cleanup; | |
| } | |
| encodingImage = nextImage; | |
| } | |
| printf(" * Encoding layer %d: color quality [%d (%s)], alpha quality [%d (%s)]\n", | |
| layerIndex, | |
| encoder->quality, | |
| qualityString(encoder->quality), | |
| encoder->qualityAlpha, | |
| qualityString(encoder->qualityAlpha)); | |
| const avifResult result = avifEncoderAddImage(encoder, encodingImage, settings->outputTiming.duration, AVIF_ADD_IMAGE_FLAG_NONE); | |
| if (result != AVIF_RESULT_OK) { | |
| fprintf(stderr, "ERROR: Failed to encode image: %s\n", avifResultToString(result)); | |
| goto cleanup; | |
| } | |
| ++layerIndex; | |
| } | |
| // main() function should set number of layers to number of input, | |
| // so there should be no input left. | |
| assert(!avifInputHasRemainingData(input, layerIndex)); | |
| success = AVIF_TRUE; | |
| cleanup: | |
| if (nextImage) { | |
| avifImageDestroy(nextImage); | |
| } | |
| return success; | |
| } | |
| static avifBool avifEncodeImagesFixedQuality(const avifSettings * settings, | |
| avifInput * input, | |
| const avifInputFile * firstFile, | |
| const avifImage * firstImage, | |
| const avifImage * const * gridCells, | |
| avifRWData * encoded, | |
| avifEncodedByteSizes * byteSizes) | |
| { | |
| avifBool success = AVIF_FALSE; | |
| avifRWDataFree(encoded); | |
| avifEncoder * encoder = avifEncoderCreate(); | |
| if (!encoder) { | |
| fprintf(stderr, "ERROR: Out of memory\n"); | |
| goto cleanup; | |
| } | |
| encoder->maxThreads = settings->jobs; | |
| encoder->codecChoice = settings->codecChoice; | |
| encoder->speed = settings->speed; | |
| encoder->timescale = settings->outputTiming.timescale; | |
| encoder->keyframeInterval = settings->keyframeInterval; | |
| encoder->repetitionCount = settings->repetitionCount; | |
| encoder->headerFormat = settings->headerFormat; | |
| encoder->creationTime = settings->creationTime; | |
| encoder->modificationTime = settings->modificationTime; | |
| encoder->extraLayerCount = settings->layers - 1; | |
| if (!avifEncodeUpdateEncoderSettings(encoder, &firstFile->settings)) { | |
| goto cleanup; | |
| } | |
| if (settings->overrideQuality != INVALID_QUALITY) { | |
| encoder->quality = settings->overrideQuality; | |
| } | |
| if (settings->overrideQualityAlpha != INVALID_QUALITY) { | |
| encoder->qualityAlpha = settings->overrideQualityAlpha; | |
| } | |
| #if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) | |
| if (settings->qualityGainMap != INVALID_QUALITY) { | |
| encoder->qualityGainMap = settings->qualityGainMap; | |
| } | |
| #endif | |
| if (input->requestedDepth == 8 && input->requestedDepthExtension == 8) { | |
| encoder->sampleTransformRecipe = AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B; | |
| } else if (input->requestedDepth == 12 && input->requestedDepthExtension == 4) { | |
| encoder->sampleTransformRecipe = AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B; | |
| } else if (input->requestedDepth == 12 && input->requestedDepthExtension == 8) { | |
| encoder->sampleTransformRecipe = AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_8B_OVERLAP_4B; | |
| } else if (input->requestedDepthExtension != 0) { | |
| fprintf(stderr, "ERROR: Unsupported bit depth extension scheme: %d + %d\n", input->requestedDepth, input->requestedDepthExtension); | |
| goto cleanup; | |
| } | |
| const char * const codecName = avifCodecName(settings->codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE); | |
| char speedStr[16]; | |
| if (settings->speed == AVIF_SPEED_DEFAULT) { | |
| strcpy(speedStr, "default"); | |
| } else { | |
| snprintf(speedStr, sizeof(speedStr), "%d", settings->speed); | |
| } | |
| char gainMapStr[100] = { 0 }; | |
| #if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) | |
| if (firstImage->gainMap && firstImage->gainMap->image) { | |
| snprintf(gainMapStr, sizeof(gainMapStr), ", gain map quality [%d (%s)]", encoder->qualityGainMap, qualityString(encoder->qualityGainMap)); | |
| } | |
| #endif | |
| char manualTilingStr[128]; | |
| snprintf(manualTilingStr, sizeof(manualTilingStr), "tileRowsLog2 [%d], tileColsLog2 [%d]", encoder->tileRowsLog2, encoder->tileColsLog2); | |
| // Note: this is mirrored in apps/avifgainmaputil/imageio.cc, changes here may be mirrored there if relevant. | |
| printf("Encoding with initial settings: codec '%s' speed [%s], color quality [%d (%s)], alpha quality [%d (%s)]%s, %s, %d worker thread(s), please wait...\n", | |
| codecName ? codecName : "none", | |
| speedStr, | |
| encoder->quality, | |
| qualityString(encoder->quality), | |
| encoder->qualityAlpha, | |
| qualityString(encoder->qualityAlpha), | |
| gainMapStr, | |
| encoder->autoTiling ? "automatic tiling" : manualTilingStr, | |
| settings->jobs); | |
| if (settings->progressive) { | |
| // If --progressive is specified and the color quality is less than | |
| // PROGRESSIVE_WORST_QUALITY, the main() function returns an error and | |
| // we should not reach here. | |
| assert(encoder->quality >= PROGRESSIVE_WORST_QUALITY); | |
| // Encode the base layer with a very low quality to ensure a small encoded size. | |
| encoder->quality = 10; | |
| // Low alpha quality resulted in weird artifact, so we don't do it. | |
| // For further size savings, scale the first layer by a half (numerator: 1, denominator: 2). | |
| const avifScalingMode scalingMode = { { 1, 2 }, { 1, 2 } }; | |
| encoder->scalingMode = scalingMode; | |
| } | |
| if (settings->layers > 1) { | |
| printf(" * Encoding layer %d: color quality [%d (%s)], alpha quality [%d (%s)]\n", | |
| 0, | |
| encoder->quality, | |
| qualityString(encoder->quality), | |
| encoder->qualityAlpha, | |
| qualityString(encoder->qualityAlpha)); | |
| } | |
| if (settings->gridDimsPresent) { | |
| const avifResult addImageResult = | |
| avifEncoderAddImageGrid(encoder, settings->gridDims[0], settings->gridDims[1], gridCells, AVIF_ADD_IMAGE_FLAG_SINGLE); | |
| if (addImageResult != AVIF_RESULT_OK) { | |
| fprintf(stderr, "ERROR: Failed to encode image grid: %s\n", avifResultToString(addImageResult)); | |
| goto cleanup; | |
| } | |
| } else { | |
| int imageIndex = 1; // firstImage with imageIndex 0 is already available. | |
| avifAddImageFlags addImageFlags = AVIF_ADD_IMAGE_FLAG_NONE; | |
| if (!avifInputHasRemainingData(input, imageIndex) && (settings->layers == 1)) { | |
| addImageFlags |= AVIF_ADD_IMAGE_FLAG_SINGLE; | |
| } | |
| uint64_t firstDurationInTimescales = firstFile->duration ? firstFile->duration : settings->outputTiming.duration; | |
| if ((firstFile->filename == AVIF_FILENAME_STDIN && settings->inputFormat == AVIF_APP_FILE_FORMAT_Y4M) || | |
| (settings->layers == 1 && input->filesCount > 1)) { | |
| snprintf(manualTilingStr, | |
| sizeof(manualTilingStr), | |
| "tileRowsLog2 [%d], tileColsLog2 [%d]", | |
| encoder->tileRowsLog2, | |
| encoder->tileColsLog2); | |
| printf(" * Encoding frame %d [%" PRIu64 "/%" PRIu64 " ts] color quality [%d (%s)], alpha quality [%d (%s)], %s: %s\n", | |
| 0, | |
| firstDurationInTimescales, | |
| settings->outputTiming.timescale, | |
| encoder->quality, | |
| qualityString(encoder->quality), | |
| encoder->qualityAlpha, | |
| qualityString(encoder->qualityAlpha), | |
| encoder->autoTiling ? "automatic tiling" : manualTilingStr, | |
| avifPrettyFilename(firstFile->filename)); | |
| } | |
| const avifResult addImageResult = avifEncoderAddImage(encoder, firstImage, firstDurationInTimescales, addImageFlags); | |
| if (addImageResult != AVIF_RESULT_OK) { | |
| fprintf(stderr, "ERROR: Failed to encode image: %s\n", avifResultToString(addImageResult)); | |
| goto cleanup; | |
| } | |
| if (settings->layers > 1) { | |
| if (!avifEncodeRestOfLayeredImage(encoder, settings, input, imageIndex, firstImage)) { | |
| goto cleanup; | |
| } | |
| } else { | |
| // Not generating a single-image grid: Use all remaining input files as subsequent | |
| // frames. | |
| if (!avifEncodeRestOfImageSequence(encoder, settings, input, imageIndex, firstImage)) { | |
| goto cleanup; | |
| } | |
| } | |
| } | |
| const avifResult finishResult = avifEncoderFinish(encoder, encoded); | |
| if (finishResult != AVIF_RESULT_OK) { | |
| fprintf(stderr, "ERROR: Failed to finish encoding: %s\n", avifResultToString(finishResult)); | |
| goto cleanup; | |
| } | |
| success = AVIF_TRUE; | |
| byteSizes->colorSizeBytes = encoder->ioStats.colorOBUSize; | |
| byteSizes->alphaSizeBytes = encoder->ioStats.alphaOBUSize; | |
| #if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) | |
| byteSizes->gainMapSizeBytes = avifEncoderGetGainMapSizeBytes(encoder); | |
| #endif | |
| cleanup: | |
| if (encoder) { | |
| if (!success) { | |
| avifDumpDiagnostics(&encoder->diag); | |
| } | |
| avifEncoderDestroy(encoder); | |
| } | |
| return success; | |
| } | |
| static avifBool avifEncodeImages(avifSettings * settings, | |
| avifInput * input, | |
| const avifInputFile * firstFile, | |
| const avifImage * firstImage, | |
| const avifImage * const * gridCells, | |
| avifRWData * encoded, | |
| avifEncodedByteSizes * byteSizes) | |
| { | |
| if (settings->targetSize == -1) { | |
| return avifEncodeImagesFixedQuality(settings, input, firstFile, firstImage, gridCells, encoded, byteSizes); | |
| } | |
| avifBool hasGainMap = AVIF_FALSE; | |
| avifBool allQualitiesConstrained = settings->qualityIsConstrained && settings->qualityAlphaIsConstrained; | |
| #if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) | |
| hasGainMap = (firstImage->gainMap && firstImage->gainMap->image); | |
| if (hasGainMap) { | |
| allQualitiesConstrained = allQualitiesConstrained && settings->qualityGainMapIsConstrained; | |
| } | |
| #endif | |
| if (allQualitiesConstrained) { | |
| fprintf(stderr, "ERROR: --target-size is used with constrained --qcolor and --qalpha %s\n", (hasGainMap ? "and --qgain-map" : "")); | |
| return AVIF_FALSE; | |
| } | |
| printf("Starting a binary search to find the %s%s generating the encoded image size closest to %d bytes, please wait...\n", | |
| settings->qualityAlphaIsConstrained ? "color quality" | |
| : (settings->qualityIsConstrained ? "alpha quality" : "color and alpha qualities"), | |
| (hasGainMap && !settings->qualityGainMapIsConstrained) ? " and gain map quality" : "", | |
| settings->targetSize); | |
| const size_t targetSize = (size_t)settings->targetSize; | |
| // TODO(yguyon): Use quantizer instead of quality because quantizer range is smaller (faster binary search). | |
| int closestQuality = INVALID_QUALITY; | |
| avifRWData closestEncoded = { NULL, 0 }; | |
| size_t closestSizeDiff = 0; | |
| avifEncodedByteSizes closestByteSizes = { 0, 0, 0 }; | |
| int minQuality = settings->progressive ? PROGRESSIVE_WORST_QUALITY : AVIF_QUALITY_WORST; // inclusive | |
| int maxQuality = AVIF_QUALITY_BEST; // inclusive | |
| while (minQuality <= maxQuality) { | |
| const int quality = (minQuality + maxQuality) / 2; | |
| if (!settings->qualityIsConstrained) { | |
| settings->overrideQuality = quality; | |
| } | |
| if (!settings->qualityAlphaIsConstrained) { | |
| settings->overrideQualityAlpha = quality; | |
| } | |
| if (!settings->qualityGainMapIsConstrained) { | |
| settings->qualityGainMap = quality; | |
| } | |
| if (!avifEncodeImagesFixedQuality(settings, input, firstFile, firstImage, gridCells, encoded, byteSizes)) { | |
| avifRWDataFree(&closestEncoded); | |
| return AVIF_FALSE; | |
| } | |
| printf("Encoded image of size %" AVIF_FMT_ZU " bytes.\n", encoded->size); | |
| if (encoded->size == targetSize) { | |
| return AVIF_TRUE; | |
| } | |
| size_t sizeDiff; | |
| if (encoded->size > targetSize) { | |
| sizeDiff = encoded->size - targetSize; | |
| maxQuality = quality - 1; | |
| } else { | |
| sizeDiff = targetSize - encoded->size; | |
| minQuality = quality + 1; | |
| } | |
| if ((closestQuality == INVALID_QUALITY) || (sizeDiff < closestSizeDiff)) { | |
| closestQuality = quality; | |
| avifRWDataFree(&closestEncoded); | |
| closestEncoded = *encoded; | |
| encoded->size = 0; | |
| encoded->data = NULL; | |
| closestSizeDiff = sizeDiff; | |
| closestByteSizes = *byteSizes; | |
| } | |
| } | |
| if (!settings->qualityIsConstrained) { | |
| settings->overrideQuality = closestQuality; | |
| } | |
| if (!settings->qualityAlphaIsConstrained) { | |
| settings->overrideQualityAlpha = closestQuality; | |
| } | |
| avifRWDataFree(encoded); | |
| *encoded = closestEncoded; | |
| *byteSizes = closestByteSizes; | |
| printf("Kept the encoded image of size %" AVIF_FMT_ZU " bytes generated with ", encoded->size); | |
| if (!settings->qualityIsConstrained) { | |
| printf("color quality %d", settings->overrideQuality); | |
| } | |
| if (!settings->qualityAlphaIsConstrained) { | |
| if (!settings->qualityIsConstrained) { | |
| printf(" and "); | |
| } | |
| printf("alpha quality %d", settings->overrideQualityAlpha); | |
| } | |
| printf(".\n"); | |
| return AVIF_TRUE; | |
| } | |
| static void avifInputAdd(avifInput * input, const char * filePath, uint64_t duration, avifInputFileSettings * pendingSettings) | |
| { | |
| input->files[input->filesCount].filename = filePath; | |
| input->files[input->filesCount].duration = duration; | |
| input->files[input->filesCount].settings = *pendingSettings; | |
| memset(pendingSettings, 0, sizeof(*pendingSettings)); | |
| ++input->filesCount; | |
| } | |
| static int quantizerToQuality(int minQuantizer, int maxQuantizer) | |
| { | |
| // When calculating the average quantizer, discard the fractional part for a | |
| // bias toward better quality. | |
| const int quantizer = (minQuantizer + maxQuantizer) / 2; | |
| const int quality = ((63 - quantizer) * 100 + 31) / 63; | |
| return quality; | |
| } | |
| int main(int argc, char * argv[]) | |
| { | |
| if (argc < 2) { | |
| syntaxShort(); | |
| return 1; | |
| } | |
| const char * outputFilename = NULL; | |
| avifInput input; | |
| memset(&input, 0, sizeof(input)); | |
| input.files = malloc(sizeof(avifInputFile) * argc); | |
| if (input.files == NULL) { | |
| fprintf(stderr, "ERROR: memory allocation failure\n"); | |
| return 1; | |
| } | |
| input.requestedFormat = AVIF_PIXEL_FORMAT_NONE; // AVIF_PIXEL_FORMAT_NONE is used as a sentinel for "auto" | |
| // See here for the discussion on the semi-arbitrary defaults for speed: | |
| // https://github.com/AOMediaCodec/libavif/issues/440 | |
| int returnCode = 1; | |
| avifBool noOverwrite = AVIF_FALSE; | |
| avifSettings settings; | |
| memset(&settings, 0, sizeof(settings)); | |
| settings.codecChoice = AVIF_CODEC_CHOICE_AUTO; | |
| settings.jobs = -1; | |
| settings.targetSize = -1; | |
| settings.qualityIsConstrained = AVIF_FALSE; | |
| settings.qualityAlphaIsConstrained = AVIF_FALSE; | |
| settings.overrideQuality = INVALID_QUALITY; | |
| settings.overrideQualityAlpha = INVALID_QUALITY; | |
| settings.qualityGainMap = DEFAULT_QUALITY_GAIN_MAP; | |
| settings.progressive = AVIF_FALSE; | |
| settings.layered = AVIF_FALSE; | |
| settings.layers = 0; | |
| settings.speed = 6; | |
| settings.headerFormat = AVIF_HEADER_DEFAULT; | |
| settings.creationTime = 0; | |
| settings.modificationTime = 0; | |
| settings.repetitionCount = AVIF_REPETITION_COUNT_INFINITE; | |
| settings.keyframeInterval = 0; | |
| settings.ignoreExif = AVIF_FALSE; | |
| settings.ignoreXMP = AVIF_FALSE; | |
| settings.ignoreColorProfile = AVIF_FALSE; | |
| settings.ignoreAlpha = AVIF_FALSE; | |
| settings.ignoreGainMap = AVIF_FALSE; | |
| settings.cicpExplicitlySet = AVIF_FALSE; | |
| settings.inputFormat = AVIF_APP_FILE_FORMAT_UNKNOWN; | |
| avifInputFileSettings pendingSettings; | |
| memset(&pendingSettings, 0, sizeof(pendingSettings)); | |
| avifBool cropConversionRequired = AVIF_FALSE; | |
| uint8_t irotAngle = 0xff; // sentinel value indicating "unused" | |
| uint8_t imirAxis = 0xff; // sentinel value indicating "unused" | |
| avifRange requestedRange = AVIF_RANGE_FULL; | |
| avifBool lossless = AVIF_FALSE; | |
| avifImage * image = NULL; | |
| avifRWData raw = AVIF_DATA_EMPTY; | |
| avifRWData exifOverride = AVIF_DATA_EMPTY; | |
| avifRWData xmpOverride = AVIF_DATA_EMPTY; | |
| avifRWData iccOverride = AVIF_DATA_EMPTY; | |
| avifBool premultiplyAlpha = AVIF_FALSE; | |
| uint32_t gridCellCount = 0; | |
| avifImage ** gridCells = NULL; | |
| avifImage * gridSplitImage = NULL; // used for cleanup tracking | |
| // By default, the color profile itself is unspecified, so CP/TC are set (to 2) accordingly. | |
| // However, if the end-user doesn't specify any CICP, we will convert to YUV using BT601 | |
| // coefficients anyway (as MC:2 falls back to MC:5/6), so we might as well signal it explicitly. | |
| // See: ISO/IEC 23000-22:2019 Amendment 2, or the comment in avifCalcYUVCoefficients() | |
| settings.colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED; | |
| settings.transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; | |
| settings.matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; | |
| settings.chromaDownsampling = AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC; | |
| int argIndex = 1; | |
| while (argIndex < argc) { | |
| const char * arg = argv[argIndex]; | |
| if (!strcmp(arg, "--")) { | |
| // Stop parsing flags, everything after this is positional arguments | |
| ++argIndex; | |
| // Parse additional positional arguments if any | |
| while (argIndex < argc) { | |
| arg = argv[argIndex]; | |
| avifInputAdd(&input, /*filePath=*/arg, settings.outputTiming.duration, &pendingSettings); | |
| ++argIndex; | |
| } | |
| break; | |
| } else if (!strcmp(arg, "-h") || !strcmp(arg, "--help")) { | |
| syntaxLong(); | |
| returnCode = 0; | |
| goto cleanup; | |
| } else if (!strcmp(arg, "-V") || !strcmp(arg, "--version")) { | |
| avifPrintVersions(); | |
| returnCode = 0; | |
| goto cleanup; | |
| } else if (!strcmp(arg, "--no-overwrite")) { | |
| noOverwrite = AVIF_TRUE; | |
| } else if (!strcmp(arg, "-j") || !strcmp(arg, "--jobs")) { | |
| NEXTARG(); | |
| if (!strcmp(arg, "all")) { | |
| settings.jobs = avifQueryCPUCount(); | |
| } else { | |
| settings.jobs = atoi(arg); | |
| if (settings.jobs < 1) { | |
| settings.jobs = 1; | |
| } | |
| } | |
| } else if (!strcmp(arg, "--stdin")) { | |
| avifInputAdd(&input, AVIF_FILENAME_STDIN, settings.outputTiming.duration, &pendingSettings); | |
| if (input.filesCount == 2) { | |
| // The only valid pattern here is | |
| // avifenc [settings...] output.avif [more_settings...] --stdin | |
| // because multiple input files are forbidden with --stdin and | |
| // there must be an output file specified with or without --output. | |
| // Invalid patterns will be rejected later on. | |
| // Merge all the settings seen so far and associate them with stdin only. | |
| if (!avifInputFileSettingsOverwrite(/*dst=*/&input.files[0].settings, /*src=*/&input.files[1].settings)) { | |
| fprintf(stderr, "ERROR: memory allocation failure\n"); | |
| goto cleanup; | |
| } | |
| avifCodecSpecificOptionsFree(&input.files[1].settings.codecSpecificOptions); | |
| input.files[1].settings = input.files[0].settings; | |
| memset(&input.files[0].settings, 0, sizeof(input.files[0].settings)); | |
| // Swap the file and stdin so that the file is last and picked as output later on. | |
| const avifInputFile output = input.files[0]; | |
| input.files[0] = input.files[1]; | |
| input.files[1] = output; | |
| // This should now be equivalent to: | |
| // avifenc [settings...] [more_settings...] --stdin output.avif | |
| } else if (input.filesCount > 2) { | |
| fprintf(stderr, "ERROR: there cannot be any other input if --stdin is specified\n"); | |
| goto cleanup; | |
| } | |
| } else if (!strcmp(arg, "--input-format")) { | |
| NEXTARG(); | |
| if (!strcmp(arg, "jpg") || !strcmp(arg, "jpeg")) { | |
| settings.inputFormat = AVIF_APP_FILE_FORMAT_JPEG; | |
| } else if (!strcmp(arg, "png")) { | |
| settings.inputFormat = AVIF_APP_FILE_FORMAT_PNG; | |
| } else if (!strcmp(arg, "y4m")) { | |
| settings.inputFormat = AVIF_APP_FILE_FORMAT_Y4M; | |
| } else if (!strcmp(arg, "auto")) { | |
| settings.inputFormat = AVIF_APP_FILE_FORMAT_UNKNOWN; | |
| } else { | |
| fprintf(stderr, "ERROR: invalid stdin file format (expected one of jpeg/png/y4m/auto): %s\n", arg); | |
| goto cleanup; | |
| } | |
| } else if (!strcmp(arg, "-o") || !strcmp(arg, "--output")) { | |
| NEXTARG(); | |
| outputFilename = arg; | |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_MINI) | |
| } else if (!strcmp(arg, "--mini")) { | |
| settings.headerFormat |= AVIF_HEADER_MINI; | |
| #endif // AVIF_ENABLE_EXPERIMENTAL_MINI | |
| } else if (!strcmp(arg, "-d") || !strcmp(arg, "--depth")) { | |
| NEXTARG(); | |
| uint32_t depthValues[2] = { 0, 0 }; | |
| if (parseU32List(depthValues, 1, arg, ',') || parseU32List(depthValues, 2, arg, ',')) { | |
| if ((depthValues[0] != 8 && depthValues[0] != 10 && depthValues[0] != 12) || | |
| (depthValues[1] != 0 && depthValues[1] != 4 && depthValues[1] != 8)) { | |
| fprintf(stderr, "ERROR: unsupported depth or depth extension: %s\n", arg); | |
| goto cleanup; | |
| } | |
| input.requestedDepth = (int)depthValues[0]; | |
| input.requestedDepthExtension = (int)depthValues[1]; | |
| } else { | |
| fprintf(stderr, "ERROR: invalid depth and depth extension combination: %s\n", arg); | |
| goto cleanup; | |
| } | |
| } else if (!strcmp(arg, "-y") || !strcmp(arg, "--yuv")) { | |
| NEXTARG(); | |
| if (!strcmp(arg, "444")) { | |
| input.requestedFormat = AVIF_PIXEL_FORMAT_YUV444; | |
| } else if (!strcmp(arg, "422")) { | |
| input.requestedFormat = AVIF_PIXEL_FORMAT_YUV422; | |
| } else if (!strcmp(arg, "420")) { | |
| input.requestedFormat = AVIF_PIXEL_FORMAT_YUV420; | |
| } else if (!strcmp(arg, "400")) { | |
| input.requestedFormat = AVIF_PIXEL_FORMAT_YUV400; | |
| } else { | |
| fprintf(stderr, "ERROR: invalid format: %s\n", arg); | |
| goto cleanup; | |
| } | |
| } else if (!strcmp(arg, "-k") || !strcmp(arg, "--keyframe")) { | |
| NEXTARG(); | |
| settings.keyframeInterval = atoi(arg); | |
| } else if (!strcmp(arg, "-q") || !strcmp(arg, "--qcolor") || strpre(arg, "-q:") || strpre(arg, "--qcolor:")) { | |
| // For compatibility reason unsuffixed flags always apply to all input (as if they appear before first input). | |
| // Print a warning if unsuffixed flag appears after input file. | |
| avifOptionSuffixType type = parseOptionSuffix(arg, /*warnNoSuffix=*/input.filesCount != 0); | |
| if (type == AVIF_OPTION_SUFFIX_INVALID) { | |
| goto cleanup; | |
| } | |
| NEXTARG(); | |
| int quality = atoi(arg); | |
| if (quality < AVIF_QUALITY_WORST) { | |
| quality = AVIF_QUALITY_WORST; | |
| } | |
| if (quality > AVIF_QUALITY_BEST) { | |
| quality = AVIF_QUALITY_BEST; | |
| } | |
| if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) { | |
| pendingSettings.quality = intSettingsEntryOf(quality); | |
| } else { | |
| input.files[0].settings.quality = intSettingsEntryOf(quality); | |
| } | |
| } else if (!strcmp(arg, "--qalpha") || strpre(arg, "--qalpha:")) { | |
| avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0); | |
| if (type == AVIF_OPTION_SUFFIX_INVALID) { | |
| goto cleanup; | |
| } | |
| NEXTARG(); | |
| int qualityAlpha = atoi(arg); | |
| if (qualityAlpha < AVIF_QUALITY_WORST) { | |
| qualityAlpha = AVIF_QUALITY_WORST; | |
| } | |
| if (qualityAlpha > AVIF_QUALITY_BEST) { | |
| qualityAlpha = AVIF_QUALITY_BEST; | |
| } | |
| if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) { | |
| pendingSettings.qualityAlpha = intSettingsEntryOf(qualityAlpha); | |
| } else { | |
| input.files[0].settings.qualityAlpha = intSettingsEntryOf(qualityAlpha); | |
| } | |
| } else if (!strcmp(arg, "--min") || strpre(arg, "--min:")) { | |
| avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0); | |
| if (type == AVIF_OPTION_SUFFIX_INVALID) { | |
| goto cleanup; | |
| } | |
| NEXTARG(); | |
| int minQuantizer = atoi(arg); | |
| if (minQuantizer < AVIF_QUANTIZER_BEST_QUALITY) { | |
| minQuantizer = AVIF_QUANTIZER_BEST_QUALITY; | |
| } | |
| if (minQuantizer > AVIF_QUANTIZER_WORST_QUALITY) { | |
| minQuantizer = AVIF_QUANTIZER_WORST_QUALITY; | |
| } | |
| if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) { | |
| pendingSettings.minQuantizer = intSettingsEntryOf(minQuantizer); | |
| } else { | |
| input.files[0].settings.minQuantizer = intSettingsEntryOf(minQuantizer); | |
| } | |
| } else if (!strcmp(arg, "--max") || strpre(arg, "--max:")) { | |
| avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0); | |
| if (type == AVIF_OPTION_SUFFIX_INVALID) { | |
| goto cleanup; | |
| } | |
| NEXTARG(); | |
| int maxQuantizer = atoi(arg); | |
| if (maxQuantizer < AVIF_QUANTIZER_BEST_QUALITY) { | |
| maxQuantizer = AVIF_QUANTIZER_BEST_QUALITY; | |
| } | |
| if (maxQuantizer > AVIF_QUANTIZER_WORST_QUALITY) { | |
| maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY; | |
| } | |
| if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) { | |
| pendingSettings.maxQuantizer = intSettingsEntryOf(maxQuantizer); | |
| } else { | |
| input.files[0].settings.maxQuantizer = intSettingsEntryOf(maxQuantizer); | |
| } | |
| } else if (!strcmp(arg, "--minalpha") || strpre(arg, "--minalpha:")) { | |
| avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0); | |
| if (type == AVIF_OPTION_SUFFIX_INVALID) { | |
| goto cleanup; | |
| } | |
| NEXTARG(); | |
| int minQuantizerAlpha = atoi(arg); | |
| if (minQuantizerAlpha < AVIF_QUANTIZER_BEST_QUALITY) { | |
| minQuantizerAlpha = AVIF_QUANTIZER_BEST_QUALITY; | |
| } | |
| if (minQuantizerAlpha > AVIF_QUANTIZER_WORST_QUALITY) { | |
| minQuantizerAlpha = AVIF_QUANTIZER_WORST_QUALITY; | |
| } | |
| if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) { | |
| pendingSettings.minQuantizerAlpha = intSettingsEntryOf(minQuantizerAlpha); | |
| } else { | |
| input.files[0].settings.minQuantizerAlpha = intSettingsEntryOf(minQuantizerAlpha); | |
| } | |
| } else if (!strcmp(arg, "--maxalpha") || strpre(arg, "--maxalpha:")) { | |
| avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0); | |
| if (type == AVIF_OPTION_SUFFIX_INVALID) { | |
| goto cleanup; | |
| } | |
| NEXTARG(); | |
| int maxQuantizerAlpha = atoi(arg); | |
| if (maxQuantizerAlpha < AVIF_QUANTIZER_BEST_QUALITY) { | |
| maxQuantizerAlpha = AVIF_QUANTIZER_BEST_QUALITY; | |
| } | |
| if (maxQuantizerAlpha > AVIF_QUANTIZER_WORST_QUALITY) { | |
| maxQuantizerAlpha = AVIF_QUANTIZER_WORST_QUALITY; | |
| } | |
| if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) { | |
| pendingSettings.maxQuantizerAlpha = intSettingsEntryOf(maxQuantizerAlpha); | |
| } else { | |
| input.files[0].settings.maxQuantizerAlpha = intSettingsEntryOf(maxQuantizerAlpha); | |
| } | |
| } else if (!strcmp(arg, "--target-size")) { | |
| NEXTARG(); | |
| settings.targetSize = atoi(arg); | |
| if (settings.targetSize < 0) { | |
| settings.targetSize = -1; | |
| } | |
| } else if (!strcmp(arg, "--tilerowslog2") || strpre(arg, "--tilerowslog2:")) { | |
| avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0); | |
| if (type == AVIF_OPTION_SUFFIX_INVALID) { | |
| goto cleanup; | |
| } | |
| NEXTARG(); | |
| int tileRowsLog2 = atoi(arg); | |
| if (tileRowsLog2 < 0) { | |
| tileRowsLog2 = 0; | |
| } | |
| if (tileRowsLog2 > 6) { | |
| tileRowsLog2 = 6; | |
| } | |
| if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) { | |
| pendingSettings.tileRowsLog2 = intSettingsEntryOf(tileRowsLog2); | |
| } else { | |
| input.files[0].settings.tileRowsLog2 = intSettingsEntryOf(tileRowsLog2); | |
| } | |
| } else if (!strcmp(arg, "--tilecolslog2") || strpre(arg, "--tilecolslog2:")) { | |
| avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0); | |
| if (type == AVIF_OPTION_SUFFIX_INVALID) { | |
| goto cleanup; | |
| } | |
| NEXTARG(); | |
| int tileColsLog2 = atoi(arg); | |
| if (tileColsLog2 < 0) { | |
| tileColsLog2 = 0; | |
| } | |
| if (tileColsLog2 > 6) { | |
| tileColsLog2 = 6; | |
| } | |
| if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) { | |
| pendingSettings.tileColsLog2 = intSettingsEntryOf(tileColsLog2); | |
| } else { | |
| input.files[0].settings.tileColsLog2 = intSettingsEntryOf(tileColsLog2); | |
| } | |
| } else if (!strcmp(arg, "--autotiling") || strpre(arg, "--autotiling:")) { | |
| avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0); | |
| if (type == AVIF_OPTION_SUFFIX_INVALID) { | |
| goto cleanup; | |
| } | |
| if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) { | |
| pendingSettings.autoTiling = boolSettingsEntryOf(AVIF_TRUE); | |
| } else { | |
| input.files[0].settings.autoTiling = boolSettingsEntryOf(AVIF_TRUE); | |
| } | |
| } else if (!strcmp(arg, "--progressive")) { | |
| if (settings.layered) { | |
| fprintf(stderr, "ERROR: Can not use both --progressive and --layered\n"); | |
| goto cleanup; | |
| } | |
| settings.progressive = AVIF_TRUE; | |
| } else if (!strcmp(arg, "--layered")) { | |
| if (settings.progressive) { | |
| fprintf(stderr, "ERROR: Can not use both --progressive and --layered\n"); | |
| goto cleanup; | |
| } | |
| settings.layered = AVIF_TRUE; | |
| } else if (!strcmp(arg, "--scaling-mode") || strpre(arg, "--scaling-mode:")) { | |
| avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0); | |
| if (type == AVIF_OPTION_SUFFIX_INVALID) { | |
| goto cleanup; | |
| } | |
| NEXTARG(); | |
| uint32_t frac[2] = { 0, 1 }; | |
| if (!(parseU32List(frac, 1, arg, '/') || parseU32List(frac, 2, arg, '/')) || frac[0] > INT32_MAX || frac[1] > INT32_MAX) { | |
| fprintf(stderr, "ERROR: Invalid scaling mode: %s\n", arg); | |
| goto cleanup; | |
| } | |
| if (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) { | |
| pendingSettings.scalingMode = scalingModeSettingsEntryOf(frac[0], frac[1]); | |
| } else { | |
| input.files[0].settings.scalingMode = scalingModeSettingsEntryOf(frac[0], frac[1]); | |
| } | |
| } else if (!strcmp(arg, "-g") || !strcmp(arg, "--grid")) { | |
| NEXTARG(); | |
| if (!parseU32List(settings.gridDims, 2, arg, 'x')) { | |
| fprintf(stderr, "ERROR: Invalid grid dims: %s\n", arg); | |
| goto cleanup; | |
| } | |
| settings.gridDimsPresent = AVIF_TRUE; | |
| if ((settings.gridDims[0] == 0) || (settings.gridDims[0] > 256) || (settings.gridDims[1] == 0) || | |
| (settings.gridDims[1] > 256)) { | |
| fprintf(stderr, "ERROR: Invalid grid dims (valid dim range [1-256]): %s\n", arg); | |
| goto cleanup; | |
| } | |
| } else if (!strcmp(arg, "--cicp") || !strcmp(arg, "--nclx")) { | |
| NEXTARG(); | |
| uint32_t cicp[3]; | |
| if (!parseU32List(cicp, 3, arg, '/')) { | |
| fprintf(stderr, "ERROR: Invalid CICP value: %s\n", arg); | |
| goto cleanup; | |
| } | |
| settings.colorPrimaries = (avifColorPrimaries)cicp[0]; | |
| settings.transferCharacteristics = (avifTransferCharacteristics)cicp[1]; | |
| settings.matrixCoefficients = (avifMatrixCoefficients)cicp[2]; | |
| settings.cicpExplicitlySet = AVIF_TRUE; | |
| } else if (!strcmp(arg, "-r") || !strcmp(arg, "--range")) { | |
| NEXTARG(); | |
| if (!strcmp(arg, "limited") || !strcmp(arg, "l")) { | |
| requestedRange = AVIF_RANGE_LIMITED; | |
| } else if (!strcmp(arg, "full") || !strcmp(arg, "f")) { | |
| requestedRange = AVIF_RANGE_FULL; | |
| } else { | |
| fprintf(stderr, "ERROR: Unknown range: %s\n", arg); | |
| goto cleanup; | |
| } | |
| } else if (!strcmp(arg, "-s") || !strcmp(arg, "--speed")) { | |
| NEXTARG(); | |
| if (!strcmp(arg, "default") || !strcmp(arg, "d")) { | |
| settings.speed = AVIF_SPEED_DEFAULT; | |
| } else { | |
| settings.speed = atoi(arg); | |
| if (settings.speed > AVIF_SPEED_FASTEST) { | |
| settings.speed = AVIF_SPEED_FASTEST; | |
| } | |
| if (settings.speed < AVIF_SPEED_SLOWEST) { | |
| settings.speed = AVIF_SPEED_SLOWEST; | |
| } | |
| } | |
| } else if (!strcmp(arg, "--exif")) { | |
| NEXTARG(); | |
| if (!avifReadEntireFile(arg, &exifOverride)) { | |
| fprintf(stderr, "ERROR: Unable to read Exif metadata: %s\n", arg); | |
| goto cleanup; | |
| } | |
| settings.ignoreExif = AVIF_TRUE; | |
| } else if (!strcmp(arg, "--xmp")) { | |
| NEXTARG(); | |
| if (!avifReadEntireFile(arg, &xmpOverride)) { | |
| fprintf(stderr, "ERROR: Unable to read XMP metadata: %s\n", arg); | |
| goto cleanup; | |
| } | |
| settings.ignoreXMP = AVIF_TRUE; | |
| } else if (!strcmp(arg, "--icc")) { | |
| NEXTARG(); | |
| if (!avifReadEntireFile(arg, &iccOverride)) { | |
| fprintf(stderr, "ERROR: Unable to read ICC profile: %s\n", arg); | |
| goto cleanup; | |
| } | |
| settings.ignoreColorProfile = AVIF_TRUE; | |
| } else if (!strcmp(arg, "--duration") || strpre(arg, "--duration:")) { | |
| // --duration is special, we always treat it as suffixed with :u, so don't print warning for it. | |
| avifOptionSuffixType type = parseOptionSuffix(arg, /*warnNoSuffix=*/AVIF_FALSE); | |
| if (type == AVIF_OPTION_SUFFIX_INVALID) { | |
| goto cleanup; | |
| } | |
| NEXTARG(); | |
| int durationInt = atoi(arg); | |
| if (durationInt < 1) { | |
| fprintf(stderr, "ERROR: Invalid duration: %d\n", durationInt); | |
| goto cleanup; | |
| } | |
| settings.outputTiming.duration = (uint64_t)durationInt; | |
| } else if (!strcmp(arg, "--timescale") || !strcmp(arg, "--fps")) { | |
| NEXTARG(); | |
| int timescaleInt = atoi(arg); | |
| if (timescaleInt < 1) { | |
| fprintf(stderr, "ERROR: Invalid timescale: %d\n", timescaleInt); | |
| goto cleanup; | |
| } | |
| settings.outputTiming.timescale = (uint64_t)timescaleInt; | |
| } else if (!strcmp(arg, "--creation-time")) { | |
| NEXTARG(); | |
| long long creationTime = strtoll(arg, NULL, 0); | |
| if (creationTime < 0) { | |
| fprintf(stderr, "ERROR: Invalid creation time: %lld\n", creationTime); | |
| goto cleanup; | |
| } | |
| #if LLONG_MAX > UINT64_MAX | |
| if ((unsigned long long)creationTime > UINT64_MAX) { | |
| fprintf(stderr, "ERROR: Invalid creation time: %lld\n", creationTime); | |
| goto cleanup; | |
| } | |
| #endif | |
| settings.creationTime = (uint64_t)creationTime; | |
| } else if (!strcmp(arg, "--modification-time")) { | |
| NEXTARG(); | |
| long long modificationTime = strtoll(arg, NULL, 0); | |
| if (modificationTime < 0) { | |
| fprintf(stderr, "ERROR: Invalid modification time: %lld\n", modificationTime); | |
| goto cleanup; | |
| } | |
| #if LLONG_MAX > UINT64_MAX | |
| if ((unsigned long long)modificationTime > UINT64_MAX) { | |
| fprintf(stderr, "ERROR: Invalid modification time: %lld\n", modificationTime); | |
| goto cleanup; | |
| } | |
| #endif | |
| settings.modificationTime = (uint64_t)modificationTime; | |
| } else if (!strcmp(arg, "-c") || !strcmp(arg, "--codec")) { | |
| NEXTARG(); | |
| settings.codecChoice = avifCodecChoiceFromName(arg); | |
| if (settings.codecChoice == AVIF_CODEC_CHOICE_AUTO) { | |
| fprintf(stderr, "ERROR: Unrecognized codec: %s\n", arg); | |
| goto cleanup; | |
| } else { | |
| const char * codecName = avifCodecName(settings.codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE); | |
| if (codecName == NULL) { | |
| fprintf(stderr, "ERROR: Codec cannot encode: %s\n", arg); | |
| goto cleanup; | |
| } | |
| } | |
| } else if (!strcmp(arg, "-a") || !strcmp(arg, "--advanced") || strpre(arg, "-a:") || strpre(arg, "--advanced:")) { | |
| avifOptionSuffixType type = parseOptionSuffix(arg, input.filesCount != 0); | |
| if (type == AVIF_OPTION_SUFFIX_INVALID) { | |
| goto cleanup; | |
| } | |
| NEXTARG(); | |
| avifInputFileSettings * targetSettings = | |
| (type == AVIF_OPTION_SUFFIX_UPDATE || input.filesCount == 0) ? &pendingSettings : &input.files[0].settings; | |
| if (!avifCodecSpecificOptionsAdd(&targetSettings->codecSpecificOptions, arg)) { | |
| fprintf(stderr, "ERROR: Out of memory when setting codec specific option: %s\n", arg); | |
| goto cleanup; | |
| } | |
| } else if (!strcmp(arg, "--ignore-exif")) { | |
| settings.ignoreExif = AVIF_TRUE; | |
| } else if (!strcmp(arg, "--ignore-xmp")) { | |
| settings.ignoreXMP = AVIF_TRUE; | |
| } else if (!strcmp(arg, "--ignore-profile") || !strcmp(arg, "--ignore-icc")) { | |
| settings.ignoreColorProfile = AVIF_TRUE; | |
| } else if (!strcmp(arg, "--ignore-alpha")) { | |
| settings.ignoreAlpha = AVIF_TRUE; | |
| #if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) | |
| } else if (!strcmp(arg, "--ignore-gain-map")) { | |
| settings.ignoreGainMap = AVIF_TRUE; | |
| } else if (!strcmp(arg, "--qgain-map")) { | |
| NEXTARG(); | |
| int qualityGainMap = atoi(arg); | |
| if (qualityGainMap < AVIF_QUALITY_WORST) { | |
| qualityGainMap = AVIF_QUALITY_WORST; | |
| } | |
| if (qualityGainMap > AVIF_QUALITY_BEST) { | |
| qualityGainMap = AVIF_QUALITY_BEST; | |
| } | |
| settings.qualityGainMap = qualityGainMap; | |
| settings.qualityGainMapIsConstrained = AVIF_TRUE; | |
| #endif | |
| } else if (!strcmp(arg, "--pasp")) { | |
| NEXTARG(); | |
| if (!parseU32List(settings.paspValues, 2, arg, ',')) { | |
| fprintf(stderr, "ERROR: Invalid pasp values: %s\n", arg); | |
| goto cleanup; | |
| } | |
| settings.paspPresent = AVIF_TRUE; | |
| } else if (!strcmp(arg, "--crop")) { | |
| NEXTARG(); | |
| if (!parseU32List(settings.clapValues, 4, arg, ',')) { | |
| fprintf(stderr, "ERROR: Invalid crop values: %s\n", arg); | |
| goto cleanup; | |
| } | |
| cropConversionRequired = AVIF_TRUE; | |
| } else if (!strcmp(arg, "--clap")) { | |
| NEXTARG(); | |
| if (!parseU32List(settings.clapValues, 8, arg, ',')) { | |
| fprintf(stderr, "ERROR: Invalid clap values: %s\n", arg); | |
| goto cleanup; | |
| } | |
| settings.clapValid = AVIF_TRUE; | |
| } else if (!strcmp(arg, "--irot")) { | |
| NEXTARG(); | |
| irotAngle = (uint8_t)atoi(arg); | |
| if (irotAngle > 3) { | |
| fprintf(stderr, "ERROR: Invalid irot angle: %s\n", arg); | |
| goto cleanup; | |
| } | |
| } else if (!strcmp(arg, "--imir")) { | |
| NEXTARG(); | |
| imirAxis = (uint8_t)atoi(arg); | |
| if (imirAxis > 1) { | |
| fprintf(stderr, "ERROR: Invalid imir axis: %s\n", arg); | |
| goto cleanup; | |
| } | |
| } else if (!strcmp(arg, "--clli")) { | |
| NEXTARG(); | |
| if (!parseU32List(settings.clliValues, 2, arg, ',') || settings.clliValues[0] >= (1u << 16) || | |
| settings.clliValues[1] >= (1u << 16)) { | |
| fprintf(stderr, "ERROR: Invalid clli values: %s\n", arg); | |
| goto cleanup; | |
| } | |
| settings.clliPresent = AVIF_TRUE; | |
| } else if (!strcmp(arg, "--repetition-count")) { | |
| NEXTARG(); | |
| if (!strcmp(arg, "infinite")) { | |
| settings.repetitionCount = AVIF_REPETITION_COUNT_INFINITE; | |
| } else { | |
| settings.repetitionCount = atoi(arg); | |
| if (settings.repetitionCount < 0) { | |
| fprintf(stderr, "ERROR: Invalid repetition count: %s\n", arg); | |
| goto cleanup; | |
| } | |
| } | |
| } else if (!strcmp(arg, "-l") || !strcmp(arg, "--lossless")) { | |
| lossless = AVIF_TRUE; | |
| } else if (!strcmp(arg, "-p") || !strcmp(arg, "--premultiply")) { | |
| premultiplyAlpha = AVIF_TRUE; | |
| } else if (!strcmp(arg, "--sharpyuv")) { | |
| settings.chromaDownsampling = AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV; | |
| } else if (arg[0] == '-') { | |
| fprintf(stderr, "ERROR: unrecognized option %s\n\n", arg); | |
| syntaxLong(); | |
| goto cleanup; | |
| } else { | |
| // Positional argument | |
| avifInputAdd(&input, /*filePath=*/arg, settings.outputTiming.duration, &pendingSettings); | |
| } | |
| ++argIndex; | |
| } | |
| // Alpha quality defaults to the same value as (color) quality until the first time it's explicitly set. | |
| for (int i = 0; i < input.filesCount; ++i) { | |
| avifInputFileSettings * fileSettings = &input.files[i].settings; | |
| if (fileSettings->qualityAlpha.set) { | |
| break; | |
| } | |
| fileSettings->qualityAlpha = fileSettings->quality; | |
| } | |
| if (settings.jobs == -1) { | |
| settings.jobs = avifQueryCPUCount(); | |
| } | |
| // Check global lossless parameters and set to default if needed. | |
| if (lossless) { | |
| // Pixel format. | |
| if (input.requestedFormat != AVIF_PIXEL_FORMAT_NONE && input.requestedFormat != AVIF_PIXEL_FORMAT_YUV444 && | |
| input.requestedFormat != AVIF_PIXEL_FORMAT_YUV400) { | |
| fprintf(stderr, | |
| "When set, the pixel format can only be 444 in lossless " | |
| "mode. 400 also works if the input is grayscale.\n"); | |
| goto cleanup; | |
| } | |
| // Codec. | |
| const char * codecName = avifCodecName(settings.codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE); | |
| if (codecName && !strcmp(codecName, "rav1e")) { | |
| fprintf(stderr, "rav1e doesn't support lossless encoding yet: https://github.com/xiph/rav1e/issues/151\n"); | |
| goto cleanup; | |
| } else if (codecName && !strcmp(codecName, "svt")) { | |
| char versions[256]; | |
| avifCodecVersions(versions); | |
| const char svtVersionPrefix[] = "svt [enc]:v"; | |
| const char * svtVersionPrefixPos = strstr(versions, svtVersionPrefix); | |
| if (svtVersionPrefixPos == NULL) { | |
| // Make sure the syntax returned by avifCodecVersions() did not change. | |
| assert(strstr(versions, "svt") == NULL && strstr(versions, "SVT") == NULL); | |
| // Let the encode fail later because SVT was not included in the build. | |
| } else { | |
| const char * svtVersion = svtVersionPrefixPos + strlen(svtVersionPrefix); | |
| const int svtMajorVersion = atoi(svtVersion); | |
| if (svtMajorVersion < 3) { | |
| fprintf(stderr, "SVT-AV1 lossless support was added in version 3.0.0, current major version is only %d\n", svtMajorVersion); | |
| goto cleanup; | |
| } | |
| } | |
| } | |
| // Range. | |
| if (requestedRange != AVIF_RANGE_FULL) { | |
| fprintf(stderr, "Range has to be full in lossless mode.\n"); | |
| goto cleanup; | |
| } | |
| // Matrix coefficients. | |
| if (settings.cicpExplicitlySet) { | |
| if (settings.matrixCoefficients != AVIF_MATRIX_COEFFICIENTS_IDENTITY && | |
| settings.matrixCoefficients != AVIF_MATRIX_COEFFICIENTS_YCGCO_RE && | |
| settings.matrixCoefficients != AVIF_MATRIX_COEFFICIENTS_YCGCO_RO) { | |
| fprintf(stderr, "Matrix coefficients have to be identity, YCgCo-Re, or YCgCo-Ro in lossless mode.\n"); | |
| goto cleanup; | |
| } | |
| } else { | |
| settings.matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY; | |
| } | |
| if (settings.progressive) { | |
| fprintf(stderr, "Automatic layered encoding is unsupported in lossless mode.\n"); | |
| } | |
| } | |
| avifInputFileSettings emptySettingsReference; | |
| memset(&emptySettingsReference, 0, sizeof(emptySettingsReference)); | |
| if (!outputFilename && input.filesCount > 1 && input.files[input.filesCount - 1].filename != AVIF_FILENAME_STDIN) { | |
| --input.filesCount; | |
| outputFilename = input.files[input.filesCount].filename; | |
| if (memcmp(&input.files[input.filesCount].settings, &emptySettingsReference, sizeof(avifInputFileSettings)) != 0) { | |
| fprintf(stderr, "WARNING: Trailing options with update suffix has no effect. Place them before the input you intend to apply to.\n"); | |
| } | |
| } | |
| if (input.filesCount < 1) { | |
| fprintf(stderr, "ERROR: no input specified\n"); | |
| goto cleanup; | |
| } | |
| if (input.files[0].filename == AVIF_FILENAME_STDIN && input.filesCount > 1) { | |
| fprintf(stderr, "ERROR: there cannot be any other input if --stdin is specified\n"); | |
| goto cleanup; | |
| } | |
| if (!outputFilename) { | |
| fprintf(stderr, "ERROR: no output specified\n"); | |
| goto cleanup; | |
| } | |
| if (noOverwrite && fileExists(outputFilename)) { | |
| fprintf(stderr, "ERROR: output file %s already exists and --no-overwrite was specified\n", outputFilename); | |
| goto cleanup; | |
| } | |
| #if defined(_WIN32) | |
| if (input.files[0].filename == AVIF_FILENAME_STDIN) { | |
| setmode(fileno(stdin), O_BINARY); | |
| } | |
| #endif | |
| if (memcmp(&pendingSettings, &emptySettingsReference, sizeof(avifInputFileSettings)) != 0) { | |
| fprintf(stderr, "WARNING: Trailing options with update suffix has no effect. Place them before the input you intend to apply to.\n"); | |
| } | |
| // Check layer config | |
| if (settings.progressive) { | |
| assert(!settings.layered); | |
| if (input.filesCount > 1) { | |
| fprintf(stderr, "ERROR: --progressive only supports one input.\n"); | |
| goto cleanup; | |
| } | |
| // for automatic layered encoding, make a 2 layer AVIF. | |
| settings.layers = 2; | |
| } else if (settings.layered) { | |
| // For manual layered encoding, infer number of layers from input count. | |
| // For multi-frame input (Y4Ms) layered encoding only use the first frame, | |
| // so that we can assume number of inputs is number of layers. | |
| // We don't support changing config halfway encoding on input, | |
| // therefore encoding layered AVIF with single multi-frame input is not very meaningful. | |
| if (input.filesCount < 2 || input.filesCount > AVIF_MAX_AV1_LAYER_COUNT) { | |
| fprintf(stderr, "Encoding layered AVIF required 2 to %d inputs, but got %d.\n", AVIF_MAX_AV1_LAYER_COUNT, input.filesCount); | |
| goto cleanup; | |
| } | |
| settings.layers = input.filesCount; | |
| } else { | |
| settings.layers = 1; | |
| } | |
| if (settings.layers > 1 && settings.gridDimsPresent) { | |
| fprintf(stderr, "Layered grid image unimplemented in avifenc.\n"); | |
| goto cleanup; | |
| } | |
| for (int i = 0; i < input.filesCount; ++i) { | |
| avifInputFile * file = &input.files[i]; | |
| avifInputFileSettings * fileSettings = &file->settings; | |
| // Check tiling parameters. | |
| // Automatic tiling (autoTiling) and manual tiling (tileRowsLog2, tileColsLog2) are mutually exclusive, which means: | |
| // - At each input, only one of the two may be set. | |
| // - At some input, specifying one disables the other. | |
| if (fileSettings->autoTiling.set) { | |
| if (fileSettings->tileRowsLog2.set || fileSettings->tileColsLog2.set) { | |
| fprintf(stderr, "ERROR: --autotiling is specified but --tilerowslog2 or --tilecolslog2 is also specified for current input.\n"); | |
| goto cleanup; | |
| } | |
| // At this point, autoTiling of this input file can only be set by command line. | |
| // (automatic generation of setting entries happens below) | |
| // Since it's a boolean flag, its value must be AVIF_TRUE. | |
| assert(fileSettings->autoTiling.value); | |
| // Therefore disable manual tiling at this input (in case it was enabled at previous input). | |
| fileSettings->tileRowsLog2 = intSettingsEntryOf(0); | |
| fileSettings->tileColsLog2 = intSettingsEntryOf(0); | |
| } else if (fileSettings->tileColsLog2.set || fileSettings->tileRowsLog2.set) { | |
| // If this file has manual tile config set, disable automatic tiling, for the same reason as above. | |
| fileSettings->autoTiling = boolSettingsEntryOf(AVIF_FALSE); | |
| } | |
| // Check per-input lossy/lossless parameters. | |
| if (lossless) { | |
| // Quality. | |
| if ((fileSettings->quality.set && fileSettings->quality.value != AVIF_QUALITY_LOSSLESS) || | |
| (fileSettings->qualityAlpha.set && fileSettings->qualityAlpha.value != AVIF_QUALITY_LOSSLESS)) { | |
| fprintf(stderr, "ERROR: Quality cannot be set in lossless mode, except to %d.\n", AVIF_QUALITY_LOSSLESS); | |
| goto cleanup; | |
| } | |
| // Quantizers. | |
| if ((fileSettings->minQuantizer.set && fileSettings->minQuantizer.value != AVIF_QUANTIZER_LOSSLESS) || | |
| (fileSettings->maxQuantizer.set && fileSettings->maxQuantizer.value != AVIF_QUANTIZER_LOSSLESS) || | |
| (fileSettings->minQuantizerAlpha.set && fileSettings->minQuantizerAlpha.value != AVIF_QUANTIZER_LOSSLESS) || | |
| (fileSettings->maxQuantizerAlpha.set && fileSettings->maxQuantizerAlpha.value != AVIF_QUANTIZER_LOSSLESS)) { | |
| fprintf(stderr, "ERROR: Quantizers cannot be set in lossless mode, except to %d.\n", AVIF_QUANTIZER_LOSSLESS); | |
| goto cleanup; | |
| } | |
| } else { | |
| if (settings.progressive) { | |
| assert(DEFAULT_QUALITY >= PROGRESSIVE_WORST_QUALITY); | |
| if (fileSettings->quality.set && fileSettings->quality.value < PROGRESSIVE_WORST_QUALITY) { | |
| fprintf(stderr, "ERROR: --qcolor must be at least %d when using --progressive.\n", PROGRESSIVE_WORST_QUALITY); | |
| goto cleanup; | |
| } | |
| // --progressive only adjust color quality | |
| } | |
| } | |
| // Set defaults for first input file. | |
| if (i == 0) { | |
| // This check only applies to the first input. | |
| // Following inputs can change only one and leave the other unchanged. | |
| if (fileSettings->minQuantizer.set != fileSettings->maxQuantizer.set) { | |
| fprintf(stderr, | |
| "ERROR: --min and --max must be either both specified or both unspecified for input %s.\n", | |
| avifPrettyFilename(file->filename)); | |
| goto cleanup; | |
| } | |
| if (fileSettings->minQuantizerAlpha.set != fileSettings->maxQuantizerAlpha.set) { | |
| fprintf(stderr, | |
| "ERROR: --minalpha and --maxalpha must be either both specified or both unspecified for input %s.\n", | |
| avifPrettyFilename(file->filename)); | |
| goto cleanup; | |
| } | |
| if (fileSettings->minQuantizer.set && fileSettings->maxQuantizer.set) { | |
| fprintf(stderr, | |
| "WARNING: --min and --max are deprecated, please use -q 0..100 instead. " | |
| "--min %d --max %d is equivalent to -q %d\n", | |
| fileSettings->minQuantizer.value, | |
| fileSettings->maxQuantizer.value, | |
| quantizerToQuality(fileSettings->minQuantizer.value, fileSettings->maxQuantizer.value)); | |
| } | |
| if (fileSettings->minQuantizerAlpha.set && fileSettings->maxQuantizerAlpha.set) { | |
| fprintf(stderr, | |
| "WARNING: --minalpha and --maxalpha are deprecated, please use --qalpha 0..100 instead. " | |
| "--minalpha %d --maxalpha %d is equivalent to --qalpha %d\n", | |
| fileSettings->minQuantizerAlpha.value, | |
| fileSettings->maxQuantizerAlpha.value, | |
| quantizerToQuality(fileSettings->minQuantizerAlpha.value, fileSettings->maxQuantizerAlpha.value)); | |
| } | |
| if (!fileSettings->autoTiling.set) { | |
| fileSettings->autoTiling = boolSettingsEntryOf(AVIF_TRUE); | |
| } | |
| if (!fileSettings->tileRowsLog2.set) { | |
| fileSettings->tileRowsLog2 = intSettingsEntryOf(0); | |
| } | |
| if (!fileSettings->tileColsLog2.set) { | |
| fileSettings->tileColsLog2 = intSettingsEntryOf(0); | |
| } | |
| // Set lossy/lossless parameters to default if needed. | |
| if (lossless) { | |
| // Add lossless settings. | |
| // Settings on first input will be inherited by all inputs, so this is sufficient. | |
| fileSettings->quality = intSettingsEntryOf(AVIF_QUALITY_LOSSLESS); | |
| fileSettings->qualityAlpha = intSettingsEntryOf(AVIF_QUALITY_LOSSLESS); | |
| fileSettings->minQuantizer = intSettingsEntryOf(AVIF_QUANTIZER_LOSSLESS); | |
| fileSettings->maxQuantizer = intSettingsEntryOf(AVIF_QUANTIZER_LOSSLESS); | |
| fileSettings->minQuantizerAlpha = intSettingsEntryOf(AVIF_QUANTIZER_LOSSLESS); | |
| fileSettings->maxQuantizerAlpha = intSettingsEntryOf(AVIF_QUANTIZER_LOSSLESS); | |
| } else { | |
| settings.qualityIsConstrained = fileSettings->quality.set; | |
| settings.qualityAlphaIsConstrained = fileSettings->qualityAlpha.set; | |
| if (fileSettings->minQuantizer.set) { | |
| assert(fileSettings->maxQuantizer.set); | |
| if (!fileSettings->quality.set) { | |
| const int quality = quantizerToQuality(fileSettings->minQuantizer.value, fileSettings->maxQuantizer.value); | |
| fileSettings->quality = intSettingsEntryOf(quality); | |
| } | |
| } else { | |
| assert(!fileSettings->maxQuantizer.set); | |
| if (!fileSettings->quality.set) { | |
| fileSettings->quality = intSettingsEntryOf(DEFAULT_QUALITY); | |
| } | |
| fileSettings->minQuantizer = intSettingsEntryOf(AVIF_QUANTIZER_BEST_QUALITY); | |
| fileSettings->maxQuantizer = intSettingsEntryOf(AVIF_QUANTIZER_WORST_QUALITY); | |
| } | |
| if (fileSettings->minQuantizerAlpha.set) { | |
| assert(fileSettings->maxQuantizerAlpha.set); | |
| if (!fileSettings->qualityAlpha.set) { | |
| const int qualityAlpha = | |
| quantizerToQuality(fileSettings->minQuantizerAlpha.value, fileSettings->maxQuantizerAlpha.value); | |
| fileSettings->qualityAlpha = intSettingsEntryOf(qualityAlpha); | |
| } | |
| } else { | |
| assert(!fileSettings->maxQuantizerAlpha.set); | |
| if (!fileSettings->qualityAlpha.set) { | |
| fileSettings->qualityAlpha = fileSettings->quality; | |
| } | |
| fileSettings->minQuantizerAlpha = intSettingsEntryOf(AVIF_QUANTIZER_BEST_QUALITY); | |
| fileSettings->maxQuantizerAlpha = intSettingsEntryOf(AVIF_QUANTIZER_WORST_QUALITY); | |
| } | |
| } | |
| if (!fileSettings->scalingMode.set) { | |
| fileSettings->scalingMode = scalingModeSettingsEntryOf(1, 1); | |
| } | |
| } | |
| } | |
| image = avifImageCreateEmpty(); | |
| if (!image) { | |
| fprintf(stderr, "ERROR: Out of memory\n"); | |
| goto cleanup; | |
| } | |
| // Set these in advance so any upcoming RGB -> YUV use the proper coefficients | |
| image->colorPrimaries = settings.colorPrimaries; | |
| image->transferCharacteristics = settings.transferCharacteristics; | |
| image->matrixCoefficients = settings.matrixCoefficients; | |
| image->yuvRange = requestedRange; | |
| image->alphaPremultiplied = premultiplyAlpha; | |
| if ((image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY) && (input.requestedFormat != AVIF_PIXEL_FORMAT_NONE) && | |
| (input.requestedFormat != AVIF_PIXEL_FORMAT_YUV444)) { | |
| // User explicitly asked for non YUV444 yuvFormat, while matrixCoefficients was likely | |
| // set to AVIF_MATRIX_COEFFICIENTS_IDENTITY as a side effect of --lossless, | |
| // and Identity is only valid with YUV444. Set matrixCoefficients back to the default. | |
| image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; | |
| if (settings.cicpExplicitlySet) { | |
| // Only warn if someone explicitly asked for identity. | |
| printf("WARNING: matrixCoefficients may not be set to identity (0) when %s. Resetting MC to defaults (%d).\n", | |
| (input.requestedFormat == AVIF_PIXEL_FORMAT_YUV400) ? "encoding 4:0:0" : "subsampling", | |
| image->matrixCoefficients); | |
| } | |
| } | |
| // --target-size requires multiple encodings of the same files. Cache the input images. | |
| input.cacheEnabled = (settings.targetSize != -1); | |
| const avifInputFile * firstFile = avifInputGetFile(&input, /*imageIndex=*/0); | |
| uint32_t sourceDepth = 0; | |
| avifBool sourceWasRGB = AVIF_FALSE; | |
| avifAppSourceTiming firstSourceTiming; | |
| const avifBool isImageSequence = (!settings.gridDimsPresent) && (settings.layers == 1) && (input.filesCount > 1); | |
| // Gain maps are not supported for animations or layered images. | |
| const avifBool ignoreGainMap = settings.ignoreGainMap || isImageSequence || settings.progressive; | |
| if (!avifInputReadImage(&input, | |
| /*imageIndex=*/0, | |
| settings.ignoreColorProfile, | |
| settings.ignoreExif, | |
| settings.ignoreXMP, | |
| /*allowChangingCicp=*/!settings.cicpExplicitlySet, | |
| settings.ignoreAlpha, | |
| ignoreGainMap, | |
| image, | |
| /*settings=*/NULL, // Must use the setting for first input | |
| &sourceDepth, | |
| &sourceWasRGB, | |
| &firstSourceTiming, | |
| settings.chromaDownsampling, | |
| settings.inputFormat)) { | |
| goto cleanup; | |
| } | |
| // Check again for -y auto or for y4m input (y4m input ignores input.requestedFormat and | |
| // retains the format in file). | |
| if ((image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY) && (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400)) { | |
| image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; | |
| if (settings.cicpExplicitlySet) { | |
| // Only warn if someone explicitly asked for identity. | |
| printf("WARNING: matrixCoefficients may not be set to identity (0) when encoding 4:0:0. Resetting MC to defaults (%d).\n", | |
| image->matrixCoefficients); | |
| } | |
| } | |
| if ((image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY) && (image->yuvFormat != AVIF_PIXEL_FORMAT_YUV444)) { | |
| fprintf(stderr, "matrixCoefficients may not be set to identity (0) when subsampling.\n"); | |
| goto cleanup; | |
| } | |
| printf("Successfully loaded: %s\n", avifPrettyFilename(firstFile->filename)); | |
| // Prepare image timings | |
| if ((settings.outputTiming.duration == 0) && (settings.outputTiming.timescale == 0) && (firstSourceTiming.duration > 0) && | |
| (firstSourceTiming.timescale > 0)) { | |
| // Set the default duration and timescale to the first image's timing. | |
| settings.outputTiming = firstSourceTiming; | |
| } else { | |
| // Set output timing defaults to 30 fps | |
| if (settings.outputTiming.duration == 0) { | |
| settings.outputTiming.duration = 1; | |
| } | |
| if (settings.outputTiming.timescale == 0) { | |
| settings.outputTiming.timescale = 30; | |
| } | |
| } | |
| if ((iccOverride.size && (avifImageSetProfileICC(image, iccOverride.data, iccOverride.size) != AVIF_RESULT_OK)) || | |
| (exifOverride.size && (avifImageSetMetadataExif(image, exifOverride.data, exifOverride.size) != AVIF_RESULT_OK)) || | |
| (xmpOverride.size && (avifImageSetMetadataXMP(image, xmpOverride.data, xmpOverride.size) != AVIF_RESULT_OK))) { | |
| fprintf(stderr, "Error when setting overridden metadata: out of memory.\n"); | |
| goto cleanup; | |
| } | |
| if (!image->icc.size && !settings.cicpExplicitlySet && (image->colorPrimaries == AVIF_COLOR_PRIMARIES_UNSPECIFIED) && | |
| (image->transferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED)) { | |
| // The final image has no ICC profile, the user didn't specify any CICP, and the source | |
| // image didn't provide any CICP. Explicitly signal SRGB CP/TC here, as 2/2/x will be | |
| // interpreted as SRGB anyway. | |
| image->colorPrimaries = AVIF_COLOR_PRIMARIES_SRGB; | |
| image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; | |
| } | |
| #if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) | |
| if (image->gainMap && !image->gainMap->altICC.size) { | |
| if (image->gainMap->altColorPrimaries == AVIF_COLOR_PRIMARIES_UNSPECIFIED) { | |
| // Assume the alternate image has the same primaries as the base image. | |
| image->gainMap->altColorPrimaries = image->colorPrimaries; | |
| } | |
| if (image->gainMap->altTransferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED) { | |
| // Assume the alternate image is PQ HDR. | |
| image->gainMap->altTransferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_PQ; | |
| } | |
| } | |
| #endif // AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION | |
| if (settings.paspPresent) { | |
| image->transformFlags |= AVIF_TRANSFORM_PASP; | |
| image->pasp.hSpacing = settings.paspValues[0]; | |
| image->pasp.vSpacing = settings.paspValues[1]; | |
| } | |
| if (cropConversionRequired) { | |
| if (!convertCropToClap(image->width, image->height, settings.clapValues)) { | |
| goto cleanup; | |
| } | |
| settings.clapValid = AVIF_TRUE; | |
| } | |
| if (settings.clapValid) { | |
| image->transformFlags |= AVIF_TRANSFORM_CLAP; | |
| image->clap.widthN = settings.clapValues[0]; | |
| image->clap.widthD = settings.clapValues[1]; | |
| image->clap.heightN = settings.clapValues[2]; | |
| image->clap.heightD = settings.clapValues[3]; | |
| image->clap.horizOffN = settings.clapValues[4]; | |
| image->clap.horizOffD = settings.clapValues[5]; | |
| image->clap.vertOffN = settings.clapValues[6]; | |
| image->clap.vertOffD = settings.clapValues[7]; | |
| // Validate clap | |
| avifCropRect cropRect; | |
| avifDiagnostics diag; | |
| avifDiagnosticsClearError(&diag); | |
| if (!avifCropRectFromCleanApertureBox(&cropRect, &image->clap, image->width, image->height, &diag)) { | |
| fprintf(stderr, | |
| "ERROR: Invalid clap: width:[%d / %d], height:[%d / %d], horizOff:[%d / %d], vertOff:[%d / %d] - %s\n", | |
| (int32_t)image->clap.widthN, | |
| (int32_t)image->clap.widthD, | |
| (int32_t)image->clap.heightN, | |
| (int32_t)image->clap.heightD, | |
| (int32_t)image->clap.horizOffN, | |
| (int32_t)image->clap.horizOffD, | |
| (int32_t)image->clap.vertOffN, | |
| (int32_t)image->clap.vertOffD, | |
| diag.error); | |
| goto cleanup; | |
| } | |
| } | |
| if (irotAngle != 0xff) { | |
| image->transformFlags |= AVIF_TRANSFORM_IROT; | |
| image->irot.angle = irotAngle; | |
| } | |
| if (imirAxis != 0xff) { | |
| image->transformFlags |= AVIF_TRANSFORM_IMIR; | |
| image->imir.axis = imirAxis; | |
| } | |
| if (settings.clliPresent) { | |
| image->clli.maxCLL = (uint16_t)settings.clliValues[0]; | |
| image->clli.maxPALL = (uint16_t)settings.clliValues[1]; | |
| } | |
| avifBool hasAlpha = (image->alphaPlane && image->alphaRowBytes); | |
| avifBool usingLosslessColor = (firstFile->settings.quality.value == AVIF_QUALITY_LOSSLESS); | |
| avifBool usingLosslessAlpha = (firstFile->settings.qualityAlpha.value == AVIF_QUALITY_LOSSLESS); | |
| avifBool using400 = (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400); | |
| avifBool using444 = (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV444); | |
| avifBool usingFullRange = (image->yuvRange == AVIF_RANGE_FULL); | |
| avifBool usingIdentityMatrix = (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY); | |
| // Guess if the enduser is asking for lossless and enable it so that warnings can be emitted | |
| if (!lossless && usingLosslessColor && (!hasAlpha || usingLosslessAlpha)) { | |
| // The enduser is probably expecting lossless. Turn it on and emit warnings | |
| printf("Quality set to %d, assuming --lossless to enable warnings on potential lossless issues.\n", AVIF_QUALITY_LOSSLESS); | |
| lossless = AVIF_TRUE; | |
| } | |
| // Check for any reasons lossless will fail, and complain loudly | |
| if (lossless) { | |
| if (!usingLosslessColor) { | |
| fprintf(stderr, | |
| "WARNING: [--lossless] Color quality (-q or --qcolor) not set to %d. Color output might not be lossless.\n", | |
| AVIF_QUALITY_LOSSLESS); | |
| lossless = AVIF_FALSE; | |
| } | |
| if (hasAlpha && !usingLosslessAlpha) { | |
| fprintf(stderr, | |
| "WARNING: [--lossless] Alpha present and alpha quality (--qalpha) not set to %d. Alpha output might not be lossless.\n", | |
| AVIF_QUALITY_LOSSLESS); | |
| lossless = AVIF_FALSE; | |
| } | |
| if (usingIdentityMatrix && (sourceDepth != image->depth)) { | |
| fprintf(stderr, | |
| "WARNING: [--lossless] Identity matrix is used but input depth (%d) does not match output depth (%d). Output might not be lossless.\n", | |
| sourceDepth, | |
| image->depth); | |
| lossless = AVIF_FALSE; | |
| } | |
| if (sourceWasRGB) { | |
| if (!using444 && !using400) { | |
| fprintf(stderr, | |
| "WARNING: [--lossless] Input data was RGB and YUV " | |
| "subsampling (-y) isn't YUV444 or YUV400. Output might " | |
| "not be lossless.\n"); | |
| lossless = AVIF_FALSE; | |
| } | |
| if (!usingFullRange) { | |
| fprintf(stderr, "WARNING: [--lossless] Input data was RGB and output range (-r) isn't full. Output might not be lossless.\n"); | |
| lossless = AVIF_FALSE; | |
| } | |
| avifBool matrixCoefficientsAreLosslessCompatible = usingIdentityMatrix || | |
| image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE || | |
| image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO; | |
| if (!matrixCoefficientsAreLosslessCompatible && !using400) { | |
| fprintf(stderr, "WARNING: [--lossless] Input data was RGB and matrixCoefficients isn't set to identity (--cicp x/x/0) or YCgCo-Re/Ro (--cicp x/x/16 or x/x/17); Output might not be lossless.\n"); | |
| lossless = AVIF_FALSE; | |
| } | |
| } | |
| } | |
| if (settings.gridDimsPresent) { | |
| // Grid image! | |
| gridCellCount = settings.gridDims[0] * settings.gridDims[1]; | |
| printf("Preparing to encode a %ux%u grid (%u cells)...\n", settings.gridDims[0], settings.gridDims[1], gridCellCount); | |
| gridCells = calloc(gridCellCount, sizeof(avifImage *)); | |
| if (gridCells == NULL) { | |
| fprintf(stderr, "ERROR: memory allocation failure\n"); | |
| goto cleanup; | |
| } | |
| gridCells[0] = image; // take ownership of image | |
| int imageIndex = 1; // The first grid cell was loaded into image (imageIndex 0). | |
| const avifInputFile * nextFile; | |
| while ((nextFile = avifInputGetFile(&input, imageIndex)) != NULL) { | |
| if (imageIndex == 1) { | |
| printf("Loading additional cells for grid image (%u cells)...\n", gridCellCount); | |
| } | |
| if (imageIndex >= (int)gridCellCount) { | |
| // We have enough, warn and continue | |
| fprintf(stderr, | |
| "WARNING: [--grid] More than %u images were supplied for this %ux%u grid. The rest will be ignored.\n", | |
| gridCellCount, | |
| settings.gridDims[0], | |
| settings.gridDims[1]); | |
| break; | |
| } | |
| // Ensure no settings is set for other cells | |
| if (memcmp(&nextFile->settings, &emptySettingsReference, sizeof(avifInputFileSettings)) != 0) { | |
| fprintf(stderr, "ERROR: Grid image cannot use different settings for each cell.\n"); | |
| goto cleanup; | |
| } | |
| avifImage * cellImage = avifImageCreateEmpty(); | |
| if (!cellImage) { | |
| fprintf(stderr, "ERROR: Out of memory\n"); | |
| goto cleanup; | |
| } | |
| cellImage->colorPrimaries = image->colorPrimaries; | |
| cellImage->transferCharacteristics = image->transferCharacteristics; | |
| cellImage->matrixCoefficients = image->matrixCoefficients; | |
| cellImage->yuvRange = image->yuvRange; | |
| cellImage->alphaPremultiplied = image->alphaPremultiplied; | |
| gridCells[imageIndex] = cellImage; | |
| // Ignore ICC, Exif and XMP because only the metadata of the first frame is taken into | |
| // account by the libavif API. | |
| if (!avifInputReadImage(&input, | |
| imageIndex, | |
| /*ignoreColorProfile=*/AVIF_TRUE, | |
| /*ignoreExif=*/AVIF_TRUE, | |
| /*ignoreXMP=*/AVIF_TRUE, | |
| /*allowChangingCicp=*/AVIF_FALSE, | |
| settings.ignoreAlpha, | |
| settings.ignoreGainMap, | |
| cellImage, | |
| /*settings=*/NULL, | |
| /*outDepth=*/NULL, | |
| /*sourceIsRGB=*/NULL, | |
| /*sourceTiming=*/NULL, | |
| settings.chromaDownsampling, | |
| settings.inputFormat)) { | |
| goto cleanup; | |
| } | |
| // Let avifEncoderAddImageGrid() verify the grid integrity (valid cell sizes, depths etc.). | |
| ++imageIndex; | |
| } | |
| if (imageIndex == 1) { | |
| printf("Single image input for a grid image. Attempting to split into %u cells...\n", gridCellCount); | |
| gridSplitImage = image; | |
| gridCells[0] = NULL; | |
| if (!avifImageSplitGrid(gridSplitImage, settings.gridDims[0], settings.gridDims[1], gridCells)) { | |
| goto cleanup; | |
| } | |
| } else if (imageIndex != (int)gridCellCount) { | |
| fprintf(stderr, "ERROR: Not enough input files for grid image! (expecting %u, or a single image to be split)\n", gridCellCount); | |
| goto cleanup; | |
| } | |
| } | |
| const char * lossyHint = " (Lossy)"; | |
| if (lossless) { | |
| lossyHint = " (Lossless)"; | |
| } | |
| printf("AVIF to be written:%s\n", lossyHint); | |
| const avifImage * avif = gridCells ? gridCells[0] : image; | |
| avifImageDump(avif, | |
| settings.gridDims[0], | |
| settings.gridDims[1], | |
| settings.layers > 1 ? AVIF_PROGRESSIVE_STATE_AVAILABLE : AVIF_PROGRESSIVE_STATE_UNAVAILABLE); | |
| avifEncodedByteSizes byteSizes = { 0, 0, 0 }; | |
| if (!avifEncodeImages(&settings, &input, firstFile, image, (const avifImage **)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; | |
| } |