| // 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 DECODE_ALL_FRAMES -1 | |
| #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), or '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, either 8 or 16. (PNG only; For y4m, depth is retained, and JPEG is always 8bpc)\n"); | |
| printf(" --sato : Enable Sample Transforms decoding (e.g. 16-bit AVIF)\n"); | |
| printf(" -q,--quality Q : Output quality in 0..100. (JPEG only, default: %d)\n", DEFAULT_JPEG_QUALITY); | |
| printf(" --png-compress L : PNG compression level in 0..9 (PNG only; 0=none, 9=max). Defaults to libpng's builtin default\n"); | |
| printf(" -u,--upsampling U : Chroma upsampling (for 420/422). One of '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, where the first frame has index 0, or 'all' to decode all frames. (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(" --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(" --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 : Maximum image size (in total pixels) that should be tolerated.\n"); | |
| printf(" 0 means unlimited. (Default: %u)\n", AVIF_DEFAULT_IMAGE_SIZE_LIMIT); | |
| printf(" --dimension-limit C : Maximum image dimension (width or height) that should be tolerated.\n"); | |
| printf(" 0 means unlimited. (Default: %u)\n", AVIF_DEFAULT_IMAGE_DIMENSION_LIMIT); | |
| printf(" -- : Signal the end of options. Everything after this is interpreted as file names.\n"); | |
| printf("\n"); | |
| avifPrintVersions(); | |
| } | |
| avifBool avifWriteToFile(avifAppFileFormat outputFormat, | |
| const char * outputFilename, | |
| avifImage * image, | |
| avifBool rawColor, | |
| int jpegQuality, | |
| int pngCompressionLevel, | |
| int requestedDepth, | |
| int chromaUpsampling) | |
| { | |
| if (outputFormat == AVIF_APP_FILE_FORMAT_Y4M) { | |
| if (image->icc.size || image->exif.size || image->xmp.size) { | |
| fprintf(stderr, "Warning: metadata dropped when saving to y4m.\n"); | |
| } | |
| return y4mWrite(outputFilename, image); | |
| } else if (outputFormat == AVIF_APP_FILE_FORMAT_JPEG) { | |
| // Bypass alpha multiply step during conversion | |
| if (rawColor) { | |
| image->alphaPremultiplied = AVIF_TRUE; | |
| } | |
| return avifJPEGWrite(outputFilename, image, jpegQuality, chromaUpsampling); | |
| } else if (outputFormat == AVIF_APP_FILE_FORMAT_PNG) { | |
| return avifPNGWrite(outputFilename, image, requestedDepth, chromaUpsampling, pngCompressionLevel); | |
| } else { | |
| fprintf(stderr, "Unsupported output file extension: %s\n", outputFilename); | |
| return AVIF_FALSE; | |
| } | |
| } | |
| int main(int argc, char * argv[]) | |
| { | |
| const char * inputFilename = NULL; | |
| const char * outputFilename = NULL; | |
| int requestedDepth = 0; | |
| avifBool enableSampleTransforms = AVIF_FALSE; | |
| 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 ignoreExif = AVIF_FALSE; | |
| avifBool ignoreXMP = AVIF_FALSE; | |
| avifBool ignoreICC = AVIF_FALSE; | |
| avifBool rawColor = AVIF_FALSE; | |
| avifBool allowProgressive = AVIF_FALSE; | |
| avifStrictFlags strictFlags = AVIF_STRICT_ENABLED; | |
| int frameIndex = 0; // Decode the first frame by default. | |
| avifBool frameIndexSpecified = AVIF_FALSE; // Whether the --index flag was passed. | |
| uint32_t imageSizeLimit = AVIF_DEFAULT_IMAGE_SIZE_LIMIT; | |
| uint32_t imageDimensionLimit = AVIF_DEFAULT_IMAGE_DIMENSION_LIMIT; | |
| avifRWData iccOverride = AVIF_DATA_EMPTY; | |
| 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, "--sato")) { | |
| enableSampleTransforms = AVIF_TRUE; | |
| } 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(); | |
| if (!strcmp(arg, "all")) { | |
| frameIndex = DECODE_ALL_FRAMES; | |
| } else { | |
| frameIndex = (uint32_t)atoi(arg); | |
| } | |
| frameIndexSpecified = AVIF_TRUE; | |
| } 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, "--ignore-exif")) { | |
| ignoreExif = AVIF_TRUE; | |
| } else if (!strcmp(arg, "--ignore-xmp")) { | |
| ignoreXMP = 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 > UINT32_MAX) { | |
| 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 (!inputFilename) { | |
| fprintf(stderr, "Missing input filename\n"); | |
| syntax(); | |
| return 1; | |
| } | |
| avifAppFileFormat outputFormat = AVIF_APP_FILE_FORMAT_UNKNOWN; | |
| if (infoOnly) { | |
| if (outputFilename) { | |
| fprintf(stderr, "ERROR: info requested (-i or --info) but output filename also provided (%s)\n", outputFilename); | |
| syntax(); | |
| return 1; | |
| } | |
| } else { | |
| if (!outputFilename) { | |
| fprintf(stderr, "Missing output filename\n"); | |
| syntax(); | |
| return 1; | |
| } | |
| outputFormat = avifGuessFileFormat(outputFilename); | |
| if (outputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) { | |
| fprintf(stderr, "Cannot determine output file extension: %s\n", outputFilename); | |
| 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"); | |
| // ------ After this point, use 'goto cleanup;' in case of failure ------ | |
| int returnCode = 1; | |
| avifImage * imageView = NULL; | |
| 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; | |
| if (infoOnly) { | |
| decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_GAIN_MAP | AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS; | |
| } else if (enableSampleTransforms) { | |
| decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS; | |
| } | |
| decoder->ignoreExif = ignoreExif; | |
| decoder->ignoreXMP = ignoreXMP; | |
| decoder->ignoreICC = ignoreICC; | |
| 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; | |
| } | |
| printf("Image decoded: %s\n", inputFilename); | |
| avifContainerDump(decoder); | |
| const avifBool isSequence = decoder->imageCount > 1; | |
| 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 (isSequence) { | |
| printf(" * %s Frames: (%u expected frames)\n", | |
| (decoder->progressiveState != AVIF_PROGRESSIVE_STATE_UNAVAILABLE) ? "Progressive Image" : "Image Sequence", | |
| decoder->imageCount); | |
| } else { | |
| printf(" * Frame:\n"); | |
| } | |
| if (iccOverrideFilename) { | |
| if (!avifReadEntireFile(iccOverrideFilename, &iccOverride)) { | |
| fprintf(stderr, "ERROR: Unable to read ICC: %s\n", iccOverrideFilename); | |
| avifRWDataFree(&iccOverride); | |
| goto cleanup; | |
| } | |
| } | |
| if (infoOnly && !frameIndexSpecified) { | |
| frameIndex = DECODE_ALL_FRAMES; // Decode all frames by default in 'info only' mode. | |
| } | |
| const avifBool decodeAllFrames = frameIndex == DECODE_ALL_FRAMES; | |
| int currIndex = decodeAllFrames ? 0 : frameIndex; | |
| for (;;) { | |
| if (imageView != NULL && imageView != decoder->image) { | |
| avifImageDestroy(imageView); | |
| } | |
| imageView = NULL; | |
| result = decodeAllFrames ? avifDecoderNextImage(decoder) : avifDecoderNthImage(decoder, frameIndex); | |
| if (result != AVIF_RESULT_OK) { | |
| break; | |
| } | |
| 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); | |
| if (infoOnly) { | |
| ++currIndex; | |
| if (decodeAllFrames) { | |
| continue; | |
| } else { | |
| break; | |
| } | |
| } | |
| if (decoder->image->transformFlags & AVIF_TRANSFORM_CLAP) { | |
| avifCropRect cropRect; | |
| if (!avifCropRectFromCleanApertureBox(&cropRect, | |
| &decoder->image->clap, | |
| decoder->image->width, | |
| decoder->image->height, | |
| &decoder->diag)) { | |
| // Should happen only if AVIF_STRICT_CLAP_VALID is disabled. | |
| fprintf(stderr, "Warning: Invalid Clean Aperture values\n"); | |
| } | |
| } | |
| assert(!ignoreExif || decoder->image->exif.size == 0); | |
| assert(!ignoreXMP || decoder->image->xmp.size == 0); | |
| assert(!ignoreICC || decoder->image->icc.size == 0); | |
| if (iccOverrideFilename) { | |
| imageView = avifImageCreateEmpty(); | |
| if (!imageView) { | |
| fprintf(stderr, "ERROR: Out of memory\n"); | |
| goto cleanup; | |
| } | |
| result = avifImageCreateView(imageView, decoder->image); | |
| if (result != AVIF_RESULT_OK) { | |
| fprintf(stderr, "ERROR: Failed to create image view\n"); | |
| goto cleanup; | |
| } | |
| printf("[--icc] Setting ICC profile: %s\n", iccOverrideFilename); | |
| result = avifImageSetProfileICC(imageView, iccOverride.data, iccOverride.size); | |
| if (result != AVIF_RESULT_OK) { | |
| fprintf(stderr, "ERROR: Failed to set ICC: %s\n", avifResultToString(result)); | |
| goto cleanup; | |
| } | |
| } else { | |
| imageView = decoder->image; | |
| } | |
| if (decodeAllFrames) { | |
| // Create filename for individual frames, in the form path/to/output-0000000000.ext | |
| char * lastDot = strrchr(outputFilename, '.'); | |
| const size_t dotPos = (lastDot != NULL) ? (size_t)(lastDot - outputFilename) : strlen(outputFilename); | |
| const char * extension = (lastDot != NULL) ? lastDot + 1 : ""; | |
| const int maxFilenameWithoutExtensionLength = 1000; | |
| const int maxExtensionLength = 10; | |
| char frameFilename[1024]; | |
| int res = snprintf(frameFilename, | |
| sizeof(frameFilename), | |
| "%.*s-%010d.%.*s", | |
| ((int)dotPos > maxFilenameWithoutExtensionLength ? maxFilenameWithoutExtensionLength : (int)dotPos), | |
| outputFilename, | |
| currIndex, | |
| maxExtensionLength, | |
| extension); | |
| if (res < 0) { | |
| fprintf(stderr, "ERROR: Unable to generate output filename\n"); | |
| goto cleanup; | |
| } | |
| if (!avifWriteToFile(outputFormat, frameFilename, imageView, rawColor, jpegQuality, pngCompressionLevel, requestedDepth, chromaUpsampling)) { | |
| goto cleanup; | |
| } | |
| } else { | |
| if (!avifWriteToFile(outputFormat, outputFilename, imageView, rawColor, jpegQuality, pngCompressionLevel, requestedDepth, chromaUpsampling)) { | |
| goto cleanup; | |
| } | |
| if (isSequence && !frameIndexSpecified) { | |
| fprintf(stderr, | |
| "INFO: Decoded the first frame of an image sequence with %d frames. To output all frames, use --index all. To silence this message, use --index 0.\n", | |
| decoder->imageCount); | |
| } | |
| break; | |
| } | |
| ++currIndex; | |
| } | |
| if (result == AVIF_RESULT_NO_IMAGES_REMAINING) { | |
| if (decodeAllFrames) { | |
| result = AVIF_RESULT_OK; | |
| } else { | |
| fprintf(stderr, | |
| "ERROR: Frame at index %d requested but the file does not contain enough frames (signalled frame count: %d)\n", | |
| frameIndex, | |
| decoder->imageCount); | |
| goto cleanup; | |
| } | |
| } | |
| if (result != AVIF_RESULT_OK) { | |
| fprintf(stderr, "ERROR: Failed to decode %s: %s\n", isSequence ? "frame" : "image", avifResultToString(result)); | |
| goto cleanup; | |
| } | |
| returnCode = 0; | |
| cleanup: | |
| if (decoder != NULL) { | |
| if (returnCode != 0) { | |
| avifDumpDiagnostics(&decoder->diag); | |
| } | |
| if (imageView != NULL && imageView != decoder->image) { | |
| avifImageDestroy(imageView); | |
| } | |
| avifDecoderDestroy(decoder); | |
| } | |
| avifRWDataFree(&iccOverride); | |
| return returnCode; | |
| } |