avidec: output all frames of animations when passed `--index all` (#2670) Also refactor avifdec.c to remove duplication between --info mode and normal mode.
diff --git a/apps/avifdec.c b/apps/avifdec.c index 8af4feb..bc4f6cf 100644 --- a/apps/avifdec.c +++ b/apps/avifdec.c
@@ -15,6 +15,7 @@ #include <string.h> #define DEFAULT_JPEG_QUALITY 90 +#define DECODE_ALL_FRAMES -1 #define NEXTARG() \ if (((argIndex + 1) == argc) || (argv[argIndex + 1][0] == '-')) { \ @@ -38,7 +39,7 @@ 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 (Default: 0)\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"); @@ -54,6 +55,34 @@ 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; @@ -70,9 +99,11 @@ avifBool rawColor = AVIF_FALSE; avifBool allowProgressive = AVIF_FALSE; avifStrictFlags strictFlags = AVIF_STRICT_ENABLED; - uint32_t frameIndex = 0; + 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(); @@ -175,7 +206,12 @@ allowProgressive = AVIF_TRUE; } else if (!strcmp(arg, "--index")) { NEXTARG(); - frameIndex = (uint32_t)atoi(arg); + 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")) { @@ -231,80 +267,30 @@ return 1; } + if (!inputFilename) { + fprintf(stderr, "Missing input filename\n"); + syntax(); + return 1; + } + + avifAppFileFormat outputFormat = AVIF_APP_FILE_FORMAT_UNKNOWN; if (infoOnly) { - if (!inputFilename || outputFilename) { + if (outputFilename) { + fprintf(stderr, "ERROR: info requested (-i or --info) but output filename also provided (%s)\n", 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; - decoder->imageContentToDecode = AVIF_IMAGE_CONTENT_ALL; - - 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) { + 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", @@ -312,6 +298,7 @@ jobs, (jobs == 1) ? "" : "s"); + // ------ After this point, use 'goto cleanup;' in case of failure ------ int returnCode = 1; avifDecoder * decoder = avifDecoderCreate(); if (!decoder) { @@ -324,6 +311,9 @@ decoder->imageDimensionLimit = imageDimensionLimit; decoder->strictFlags = strictFlags; decoder->allowProgressive = allowProgressive; + if (infoOnly) { + decoder->imageContentToDecode = AVIF_IMAGE_CONTENT_ALL; + } avifResult result = avifDecoderSetIOFile(decoder, inputFilename); if (result != AVIF_RESULT_OK) { @@ -337,78 +327,142 @@ 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"); - avifImageDump(decoder->image, 0, 0, decoder->progressiveState); + avifContainerDump(decoder); - 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"); - } - } - - 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); + const int 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) { - 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) { - fprintf(stderr, "Warning: metadata dropped when saving to y4m.\n"); + 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; + while (AVIF_TRUE) { + result = decodeAllFrames ? avifDecoderNextImage(decoder) : avifDecoderNthImage(decoder, frameIndex); + if (result != AVIF_RESULT_OK) { + break; } - if (!y4mWrite(outputFilename, decoder->image)) { + + 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"); + } + } + + 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) { + printf("[--icc] Setting ICC profile: %s\n", iccOverrideFilename); + result = avifImageSetProfileICC(decoder->image, iccOverride.data, iccOverride.size); + if (result != AVIF_RESULT_OK) { + fprintf(stderr, "ERROR: Failed to set ICC: %s\n", avifResultToString(result)); + goto cleanup; + } + } + + 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, decoder->image, rawColor, jpegQuality, pngCompressionLevel, requestedDepth, chromaUpsampling)) { + goto cleanup; + } + } else { + if (!avifWriteToFile(outputFormat, outputFilename, decoder->image, 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; } - } 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); + } + if (result != AVIF_RESULT_OK) { + fprintf(stderr, "ERROR: Failed to decode %s: %s\n", isSequence ? "frame" : "image", avifResultToString(result)); goto cleanup; } + returnCode = 0; cleanup: @@ -418,5 +472,6 @@ } avifDecoderDestroy(decoder); } + avifRWDataFree(&iccOverride); return returnCode; }
diff --git a/doc/avifdec.1.md b/doc/avifdec.1.md index e13343e..d0c2eb6 100644 --- a/doc/avifdec.1.md +++ b/doc/avifdec.1.md
@@ -63,7 +63,7 @@ (JPEG only; not applicable to y4m). **\--index** _I_ -: When decoding an image sequence or progressive image, specify which frame index to decode (Default: 0). +: 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) **\--progressive** : Enable progressive AVIF processing. If a progressive image is encountered and \--progressive is passed,