| // Copyright 2019 Joe Drago. All rights reserved. |
| // SPDX-License-Identifier: BSD-2-Clause |
| |
| #include "avif/avif.h" |
| |
| #include "avifjpeg.h" |
| #include "avifpng.h" |
| #include "avifutil.h" |
| #include "y4m.h" |
| |
| #include <assert.h> |
| #include <inttypes.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| |
| #define DEFAULT_JPEG_QUALITY 90 |
| |
| #define NEXTARG() \ |
| if (((argIndex + 1) == argc) || (argv[argIndex + 1][0] == '-')) { \ |
| fprintf(stderr, "%s requires an argument.", arg); \ |
| return 1; \ |
| } \ |
| arg = argv[++argIndex] |
| |
| static void syntax(void) |
| { |
| printf("Syntax: avifdec [options] input.avif output.[jpg|jpeg|png|y4m]\n"); |
| printf(" avifdec --info input.avif\n"); |
| printf("Options:\n"); |
| printf(" -h,--help : Show syntax help\n"); |
| printf(" -V,--version : Show the version number\n"); |
| printf(" -j,--jobs J : Number of jobs (worker threads). Use \"all\" to potentially use as many cores as possible (default: all)\n"); |
| printf(" -c,--codec C : Codec to use (choose from versions list below)\n"); |
| printf(" -d,--depth D : Output depth [8,16]. (PNG only; For y4m, depth is retained, and JPEG is always 8bpc)\n"); |
| printf(" -q,--quality Q : Output quality [0-100]. (JPEG only, default: %d)\n", DEFAULT_JPEG_QUALITY); |
| printf(" --png-compress L : Set PNG compression level (PNG only; 0-9, 0=none, 9=max). Defaults to libpng's builtin default.\n"); |
| printf(" -u,--upsampling U : Chroma upsampling (for 420/422). automatic (default), fastest, best, nearest, or bilinear\n"); |
| printf(" -r,--raw-color : Output raw RGB values instead of multiplying by alpha when saving to opaque formats\n"); |
| printf(" (JPEG only; not applicable to y4m)\n"); |
| printf(" --index I : When decoding an image sequence or progressive image, specify which frame index to decode (Default: 0)\n"); |
| printf(" --progressive : Enable progressive AVIF processing. If a progressive image is encountered and --progressive is passed,\n"); |
| printf(" avifdec will use --index to choose which layer to decode (in progressive order).\n"); |
| printf(" --no-strict : Disable strict decoding, which disables strict validation checks and errors\n"); |
| printf(" -i,--info : Decode all frames and display all image information instead of saving to disk\n"); |
| printf(" --icc FILENAME : Provide an ICC profile payload (implies --ignore-icc)\n"); |
| printf(" --ignore-icc : If the input file contains an embedded ICC profile, ignore it (no-op if absent)\n"); |
| printf(" --size-limit C : Specifies the image size limit (in total pixels) that should be tolerated.\n"); |
| printf(" Default: %u, set to a smaller value to further restrict.\n", AVIF_DEFAULT_IMAGE_SIZE_LIMIT); |
| printf(" --dimension-limit C : Specifies the image dimension limit (width or height) that should be tolerated.\n"); |
| printf(" Default: %u, set to 0 to ignore.\n", AVIF_DEFAULT_IMAGE_DIMENSION_LIMIT); |
| printf(" -- : Signals the end of options. Everything after this is interpreted as file names.\n"); |
| printf("\n"); |
| avifPrintVersions(); |
| } |
| |
| int main(int argc, char * argv[]) |
| { |
| const char * inputFilename = NULL; |
| const char * outputFilename = NULL; |
| int requestedDepth = 0; |
| int jobs = -1; |
| int jpegQuality = DEFAULT_JPEG_QUALITY; |
| int pngCompressionLevel = -1; // -1 is a sentinel to avifPNGWrite() to skip calling png_set_compression_level() |
| avifCodecChoice codecChoice = AVIF_CODEC_CHOICE_AUTO; |
| avifBool infoOnly = AVIF_FALSE; |
| avifChromaUpsampling chromaUpsampling = AVIF_CHROMA_UPSAMPLING_AUTOMATIC; |
| const char * iccOverrideFilename = NULL; |
| avifBool ignoreICC = AVIF_FALSE; |
| avifBool rawColor = AVIF_FALSE; |
| avifBool allowProgressive = AVIF_FALSE; |
| avifStrictFlags strictFlags = AVIF_STRICT_ENABLED; |
| uint32_t frameIndex = 0; |
| uint32_t imageSizeLimit = AVIF_DEFAULT_IMAGE_SIZE_LIMIT; |
| uint32_t imageDimensionLimit = AVIF_DEFAULT_IMAGE_DIMENSION_LIMIT; |
| |
| if (argc < 2) { |
| syntax(); |
| return 1; |
| } |
| |
| 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]; |
| if (!inputFilename) { |
| inputFilename = arg; |
| } else if (!outputFilename) { |
| outputFilename = arg; |
| } else { |
| fprintf(stderr, "Too many positional arguments: %s\n\n", arg); |
| syntax(); |
| return 1; |
| } |
| ++argIndex; |
| } |
| break; |
| } else if (!strcmp(arg, "-h") || !strcmp(arg, "--help")) { |
| syntax(); |
| return 0; |
| } else if (!strcmp(arg, "-V") || !strcmp(arg, "--version")) { |
| avifPrintVersions(); |
| return 0; |
| } else if (!strcmp(arg, "-j") || !strcmp(arg, "--jobs")) { |
| NEXTARG(); |
| if (!strcmp(arg, "all")) { |
| jobs = avifQueryCPUCount(); |
| } else { |
| jobs = atoi(arg); |
| if (jobs < 1) { |
| jobs = 1; |
| } |
| } |
| } else if (!strcmp(arg, "-c") || !strcmp(arg, "--codec")) { |
| NEXTARG(); |
| codecChoice = avifCodecChoiceFromName(arg); |
| if (codecChoice == AVIF_CODEC_CHOICE_AUTO) { |
| fprintf(stderr, "ERROR: Unrecognized codec: %s\n", arg); |
| return 1; |
| } else { |
| const char * codecName = avifCodecName(codecChoice, AVIF_CODEC_FLAG_CAN_DECODE); |
| if (codecName == NULL) { |
| fprintf(stderr, "ERROR: Codec cannot decode: %s\n", arg); |
| return 1; |
| } |
| } |
| } else if (!strcmp(arg, "-d") || !strcmp(arg, "--depth")) { |
| NEXTARG(); |
| requestedDepth = atoi(arg); |
| if ((requestedDepth != 8) && (requestedDepth != 16)) { |
| fprintf(stderr, "ERROR: invalid depth: %s\n", arg); |
| return 1; |
| } |
| } else if (!strcmp(arg, "-q") || !strcmp(arg, "--quality")) { |
| NEXTARG(); |
| jpegQuality = atoi(arg); |
| if (jpegQuality < 0) { |
| jpegQuality = 0; |
| } else if (jpegQuality > 100) { |
| jpegQuality = 100; |
| } |
| } else if (!strcmp(arg, "--png-compress")) { |
| NEXTARG(); |
| pngCompressionLevel = atoi(arg); |
| if (pngCompressionLevel < 0) { |
| pngCompressionLevel = 0; |
| } else if (pngCompressionLevel > 9) { |
| pngCompressionLevel = 9; |
| } |
| } else if (!strcmp(arg, "-u") || !strcmp(arg, "--upsampling")) { |
| NEXTARG(); |
| if (!strcmp(arg, "automatic")) { |
| chromaUpsampling = AVIF_CHROMA_UPSAMPLING_AUTOMATIC; |
| } else if (!strcmp(arg, "fastest")) { |
| chromaUpsampling = AVIF_CHROMA_UPSAMPLING_FASTEST; |
| } else if (!strcmp(arg, "best")) { |
| chromaUpsampling = AVIF_CHROMA_UPSAMPLING_BEST_QUALITY; |
| } else if (!strcmp(arg, "nearest")) { |
| chromaUpsampling = AVIF_CHROMA_UPSAMPLING_NEAREST; |
| } else if (!strcmp(arg, "bilinear")) { |
| chromaUpsampling = AVIF_CHROMA_UPSAMPLING_BILINEAR; |
| } else { |
| fprintf(stderr, "ERROR: invalid upsampling: %s\n", arg); |
| return 1; |
| } |
| } else if (!strcmp(arg, "-r") || !strcmp(arg, "--raw-color")) { |
| rawColor = AVIF_TRUE; |
| } else if (!strcmp(arg, "--progressive")) { |
| allowProgressive = AVIF_TRUE; |
| } else if (!strcmp(arg, "--index")) { |
| NEXTARG(); |
| frameIndex = (uint32_t)atoi(arg); |
| } else if (!strcmp(arg, "--no-strict")) { |
| strictFlags = AVIF_STRICT_DISABLED; |
| } else if (!strcmp(arg, "-i") || !strcmp(arg, "--info")) { |
| infoOnly = AVIF_TRUE; |
| } else if (!strcmp(arg, "--icc")) { |
| NEXTARG(); |
| iccOverrideFilename = arg; |
| ignoreICC = AVIF_TRUE; |
| } else if (!strcmp(arg, "--ignore-icc")) { |
| ignoreICC = AVIF_TRUE; |
| } else if (!strcmp(arg, "--size-limit")) { |
| NEXTARG(); |
| unsigned long value = strtoul(arg, NULL, 10); |
| if ((value > AVIF_DEFAULT_IMAGE_SIZE_LIMIT) || (value == 0)) { |
| fprintf(stderr, "ERROR: invalid image size limit: %s\n", arg); |
| return 1; |
| } |
| imageSizeLimit = (uint32_t)value; |
| } else if (!strcmp(arg, "--dimension-limit")) { |
| NEXTARG(); |
| unsigned long value = strtoul(arg, NULL, 10); |
| if (value > UINT32_MAX) { |
| fprintf(stderr, "ERROR: invalid image dimension limit: %s\n", arg); |
| return 1; |
| } |
| imageDimensionLimit = (uint32_t)value; |
| } else if (arg[0] == '-') { |
| fprintf(stderr, "ERROR: unrecognized option %s\n\n", arg); |
| syntax(); |
| return 1; |
| } else { |
| // Positional argument |
| if (!inputFilename) { |
| inputFilename = arg; |
| } else if (!outputFilename) { |
| outputFilename = arg; |
| } else { |
| fprintf(stderr, "Too many positional arguments: %s\n\n", arg); |
| syntax(); |
| return 1; |
| } |
| } |
| |
| ++argIndex; |
| } |
| |
| if (jobs == -1) { |
| jobs = avifQueryCPUCount(); |
| } |
| |
| if (!inputFilename) { |
| syntax(); |
| return 1; |
| } |
| |
| if (infoOnly) { |
| if (!inputFilename || outputFilename) { |
| syntax(); |
| return 1; |
| } |
| |
| avifDecoder * decoder = avifDecoderCreate(); |
| if (!decoder) { |
| fprintf(stderr, "Memory allocation failure\n"); |
| return 1; |
| } |
| decoder->maxThreads = jobs; |
| decoder->codecChoice = codecChoice; |
| decoder->imageSizeLimit = imageSizeLimit; |
| decoder->imageDimensionLimit = imageDimensionLimit; |
| decoder->strictFlags = strictFlags; |
| decoder->allowProgressive = allowProgressive; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| // Decode the gain map (if present) to allow showing its info. |
| decoder->enableParsingGainMapMetadata = AVIF_TRUE; |
| decoder->enableDecodingGainMap = AVIF_TRUE; |
| #endif |
| avifResult result = avifDecoderSetIOFile(decoder, inputFilename); |
| if (result != AVIF_RESULT_OK) { |
| fprintf(stderr, "Cannot open file for read: %s\n", inputFilename); |
| avifDecoderDestroy(decoder); |
| return 1; |
| } |
| result = avifDecoderParse(decoder); |
| if (result == AVIF_RESULT_OK) { |
| printf("Image decoded: %s\n", inputFilename); |
| avifContainerDump(decoder); |
| |
| printf(" * %" PRIu64 " timescales per second, %2.2f seconds (%" PRIu64 " timescales), %d frame%s\n", |
| decoder->timescale, |
| decoder->duration, |
| decoder->durationInTimescales, |
| decoder->imageCount, |
| (decoder->imageCount == 1) ? "" : "s"); |
| if (decoder->imageCount > 1) { |
| printf(" * %s Frames: (%u expected frames)\n", |
| (decoder->progressiveState != AVIF_PROGRESSIVE_STATE_UNAVAILABLE) ? "Progressive Image" : "Image Sequence", |
| decoder->imageCount); |
| } else { |
| printf(" * Frame:\n"); |
| } |
| |
| int currIndex = 0; |
| while ((result = avifDecoderNextImage(decoder)) == AVIF_RESULT_OK) { |
| printf(" * Decoded frame [%d] [pts %2.2f (%" PRIu64 " timescales)] [duration %2.2f (%" PRIu64 " timescales)] [%ux%u]\n", |
| currIndex, |
| decoder->imageTiming.pts, |
| decoder->imageTiming.ptsInTimescales, |
| decoder->imageTiming.duration, |
| decoder->imageTiming.durationInTimescales, |
| decoder->image->width, |
| decoder->image->height); |
| ++currIndex; |
| } |
| if (result == AVIF_RESULT_NO_IMAGES_REMAINING) { |
| result = AVIF_RESULT_OK; |
| } else { |
| fprintf(stderr, "ERROR: Failed to decode frame: %s\n", avifResultToString(result)); |
| avifDumpDiagnostics(&decoder->diag); |
| } |
| } else { |
| fprintf(stderr, "ERROR: Failed to parse image: %s\n", avifResultToString(result)); |
| avifDumpDiagnostics(&decoder->diag); |
| } |
| |
| avifDecoderDestroy(decoder); |
| return result != AVIF_RESULT_OK; |
| } else { |
| if (!inputFilename || !outputFilename) { |
| syntax(); |
| return 1; |
| } |
| } |
| |
| printf("Decoding with codec '%s' (%d worker thread%s), please wait...\n", |
| avifCodecName(codecChoice, AVIF_CODEC_FLAG_CAN_DECODE), |
| jobs, |
| (jobs == 1) ? "" : "s"); |
| |
| int returnCode = 1; |
| avifDecoder * decoder = avifDecoderCreate(); |
| if (!decoder) { |
| fprintf(stderr, "Memory allocation failure\n"); |
| goto cleanup; |
| } |
| decoder->maxThreads = jobs; |
| decoder->codecChoice = codecChoice; |
| decoder->imageSizeLimit = imageSizeLimit; |
| decoder->imageDimensionLimit = imageDimensionLimit; |
| decoder->strictFlags = strictFlags; |
| decoder->allowProgressive = allowProgressive; |
| |
| avifResult result = avifDecoderSetIOFile(decoder, inputFilename); |
| if (result != AVIF_RESULT_OK) { |
| fprintf(stderr, "Cannot open file for read: %s\n", inputFilename); |
| goto cleanup; |
| } |
| |
| result = avifDecoderParse(decoder); |
| if (result != AVIF_RESULT_OK) { |
| fprintf(stderr, "ERROR: Failed to parse image: %s\n", avifResultToString(result)); |
| goto cleanup; |
| } |
| |
| result = avifDecoderNthImage(decoder, frameIndex); |
| if (result != AVIF_RESULT_OK) { |
| fprintf(stderr, "ERROR: Failed to decode image: %s\n", avifResultToString(result)); |
| goto cleanup; |
| } |
| |
| printf("Image decoded: %s\n", inputFilename); |
| printf("Image details:\n"); |
| avifBool gainMapPresent = AVIF_FALSE; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| gainMapPresent = decoder->gainMapPresent; |
| #endif |
| avifImageDump(decoder->image, 0, 0, gainMapPresent, decoder->progressiveState); |
| |
| if (ignoreICC && (decoder->image->icc.size > 0)) { |
| printf("[--ignore-icc] Discarding ICC profile.\n"); |
| // This cannot fail. |
| result = avifImageSetProfileICC(decoder->image, NULL, 0); |
| assert(result == AVIF_RESULT_OK); |
| } |
| |
| if (iccOverrideFilename) { |
| avifRWData iccOverride = AVIF_DATA_EMPTY; |
| if (!avifReadEntireFile(iccOverrideFilename, &iccOverride)) { |
| fprintf(stderr, "ERROR: Unable to read ICC: %s\n", iccOverrideFilename); |
| avifRWDataFree(&iccOverride); |
| goto cleanup; |
| } |
| printf("[--icc] Setting ICC profile: %s\n", iccOverrideFilename); |
| result = avifImageSetProfileICC(decoder->image, iccOverride.data, iccOverride.size); |
| avifRWDataFree(&iccOverride); |
| if (result != AVIF_RESULT_OK) { |
| fprintf(stderr, "ERROR: Failed to set ICC: %s\n", avifResultToString(result)); |
| goto cleanup; |
| } |
| } |
| |
| avifAppFileFormat outputFormat = avifGuessFileFormat(outputFilename); |
| if (outputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) { |
| fprintf(stderr, "Cannot determine output file extension: %s\n", outputFilename); |
| goto cleanup; |
| } else if (outputFormat == AVIF_APP_FILE_FORMAT_Y4M) { |
| if (decoder->image->icc.size || decoder->image->exif.size || decoder->image->xmp.size) { |
| printf("Warning: metadata dropped when saving to y4m.\n"); |
| } |
| if (!y4mWrite(outputFilename, decoder->image)) { |
| goto cleanup; |
| } |
| } else if (outputFormat == AVIF_APP_FILE_FORMAT_JPEG) { |
| // Bypass alpha multiply step during conversion |
| if (rawColor) { |
| decoder->image->alphaPremultiplied = AVIF_TRUE; |
| } |
| if (!avifJPEGWrite(outputFilename, decoder->image, jpegQuality, chromaUpsampling)) { |
| goto cleanup; |
| } |
| } else if (outputFormat == AVIF_APP_FILE_FORMAT_PNG) { |
| if (!avifPNGWrite(outputFilename, decoder->image, requestedDepth, chromaUpsampling, pngCompressionLevel)) { |
| goto cleanup; |
| } |
| } else { |
| fprintf(stderr, "Unsupported output file extension: %s\n", outputFilename); |
| goto cleanup; |
| } |
| returnCode = 0; |
| |
| cleanup: |
| if (decoder != NULL) { |
| if (returnCode != 0) { |
| avifDumpDiagnostics(&decoder->diag); |
| } |
| avifDecoderDestroy(decoder); |
| } |
| return returnCode; |
| } |