diff --git a/CMakeLists.txt b/CMakeLists.txt index ae2e6ec0dc..281b902c16 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -666,6 +666,9 @@ if(AVIF_BUILD_APPS OR (AVIF_BUILD_TESTS AND (AVIF_FUZZTEST OR AVIF_GTEST))) if(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) target_link_libraries(avif_apps${suffix} PRIVATE LibXml2::LibXml2) endif() + if(UNIX AND NOT APPLE) + target_link_libraries(avif_apps${suffix} PRIVATE ${CMAKE_DL_LIBS}) # for dlsym + endif() target_include_directories(avif_apps${suffix} INTERFACE apps/shared) # In GitHub CI's macos-latest os image, /usr/local/include has not only the headers of libpng # and libjpeg but also the headers of an older version of libavif. Put the avif include @@ -843,7 +846,9 @@ if(NOT SKIP_INSTALL_LIBRARIES AND NOT SKIP_INSTALL_ALL) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/libavif.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) endif() if(NOT SKIP_INSTALL_HEADERS AND NOT SKIP_INSTALL_ALL) - install(FILES include/avif/avif.h include/avif/avif_cxx.h DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/avif") + install(FILES include/avif/avif.h include/avif/apps.h include/avif/avif_cxx.h include/avif/codec.h + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/avif" + ) endif() # --------------------------------------------------------------------------------------- diff --git a/apps/avifdec.c b/apps/avifdec.c index 5f269cd3af..93932743e8 100644 --- a/apps/avifdec.c +++ b/apps/avifdec.c @@ -29,31 +29,32 @@ 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(" -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, or name of the custom codec loaded)\n"); + printf(" --custom-codec LIBRARY : Load a custom codec shared library\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(); } @@ -110,6 +111,9 @@ int main(int argc, char * argv[]) uint32_t imageSizeLimit = AVIF_DEFAULT_IMAGE_SIZE_LIMIT; uint32_t imageDimensionLimit = AVIF_DEFAULT_IMAGE_DIMENSION_LIMIT; avifRWData iccOverride = AVIF_DATA_EMPTY; + const char * requestedCodecName = NULL; + const char * customCodecLibraryName = NULL; + avifCustomCodecLibrary customCodecLibrary = { 0 }; if (argc < 2) { syntax(); @@ -154,19 +158,16 @@ int main(int argc, char * argv[]) jobs = 1; } } - } else if (!strcmp(arg, "-c") || !strcmp(arg, "--codec")) { + } else if (!strcmp(arg, "--custom-codec")) { NEXTARG(); - codecChoice = avifCodecChoiceFromName(arg); - if (codecChoice == AVIF_CODEC_CHOICE_AUTO) { - fprintf(stderr, "ERROR: Unrecognized codec: %s\n", arg); + if (customCodecLibraryName) { + fprintf(stderr, "ERROR: --custom-codec may only be specified once\n"); 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; - } } + customCodecLibraryName = arg; + } else if (!strcmp(arg, "-c") || !strcmp(arg, "--codec")) { + NEXTARG(); + requestedCodecName = arg; } else if (!strcmp(arg, "-d") || !strcmp(arg, "--depth")) { NEXTARG(); requestedDepth = atoi(arg); @@ -318,6 +319,32 @@ int main(int argc, char * argv[]) fprintf(stderr, "Memory allocation failure\n"); goto cleanup; } + + if (customCodecLibraryName) { + avifDiagnostics diag; + avifDiagnosticsClearError(&diag); + if (!avifCustomCodecLibrarySetup(&customCodecLibrary, customCodecLibraryName, &diag)) { + fprintf(stderr, + "ERROR: Failed to load custom codec shared library %s: %s\n", + customCodecLibraryName, + diag.error[0] ? diag.error : "unknown error"); + goto cleanup; + } + } + + if (requestedCodecName) { + codecChoice = avifCodecChoiceFromName(requestedCodecName); + if (codecChoice == AVIF_CODEC_CHOICE_AUTO) { + fprintf(stderr, "ERROR: Unrecognized codec: %s\n", requestedCodecName); + goto cleanup; + } + const char * codecName = avifCodecName(codecChoice, AVIF_CODEC_FLAG_CAN_DECODE); + if (codecName == NULL) { + fprintf(stderr, "ERROR: Codec cannot decode: %s\n", requestedCodecName); + goto cleanup; + } + } + decoder->maxThreads = jobs; decoder->codecChoice = codecChoice; decoder->imageSizeLimit = imageSizeLimit; @@ -506,5 +533,24 @@ int main(int argc, char * argv[]) avifDecoderDestroy(decoder); } avifRWDataFree(&iccOverride); + if (customCodecLibrary.handle) { + avifBool unloadCustomCodecLibrary = AVIF_TRUE; + if (customCodecLibrary.initialized) { + avifDiagnostics diag; + avifDiagnosticsClearError(&diag); + const avifResult shutdownResult = avifCustomCodecLibraryShutdown(&customCodecLibrary, &diag); + if (shutdownResult != AVIF_RESULT_OK) { + fprintf(stderr, + "ERROR: Failed to shut down custom codec shared library %s: %s\n", + customCodecLibrary.name ? customCodecLibrary.name : "(unknown)", + diag.error[0] ? diag.error : avifResultToString(shutdownResult)); + returnCode = 1; + unloadCustomCodecLibrary = AVIF_FALSE; + } + } + if (unloadCustomCodecLibrary) { + avifCustomCodecLibraryUnload(&customCodecLibrary); + } + } return returnCode; } diff --git a/apps/avifenc.c b/apps/avifenc.c index c3b37d0359..5d3ce442e8 100644 --- a/apps/avifenc.c +++ b/apps/avifenc.c @@ -249,7 +249,8 @@ static void syntaxLong(void) 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(" -c,--codec C : Codec to use (choose from versions list below, or name of the custom codec loaded)\n"); + printf(" --custom-codec LIBRARY : Load a custom codec shared library\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"); @@ -1483,6 +1484,9 @@ int main(int argc, char * argv[]) } const char * outputFilename = NULL; + const char * requestedCodecName = NULL; + const char * customCodecLibraryName = NULL; + avifCustomCodecLibrary customCodecLibrary = { 0 }; avifInput input; memset(&input, 0, sizeof(input)); @@ -1976,19 +1980,16 @@ int main(int argc, char * argv[]) } #endif settings.modificationTime = (uint64_t)modificationTime; - } else if (!strcmp(arg, "-c") || !strcmp(arg, "--codec")) { + } else if (!strcmp(arg, "--custom-codec")) { NEXTARG(); - settings.codecChoice = avifCodecChoiceFromName(arg); - if (settings.codecChoice == AVIF_CODEC_CHOICE_AUTO) { - fprintf(stderr, "ERROR: Unrecognized codec: %s\n", arg); + if (customCodecLibraryName) { + fprintf(stderr, "ERROR: --custom-codec may only be specified once\n"); 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; - } } + customCodecLibraryName = arg; + } else if (!strcmp(arg, "-c") || !strcmp(arg, "--codec")) { + NEXTARG(); + requestedCodecName = arg; } 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) { @@ -2109,6 +2110,31 @@ int main(int argc, char * argv[]) settings.jobs = avifQueryCPUCount(); } + if (customCodecLibraryName) { + avifDiagnostics diag; + avifDiagnosticsClearError(&diag); + if (!avifCustomCodecLibrarySetup(&customCodecLibrary, customCodecLibraryName, &diag)) { + fprintf(stderr, + "ERROR: Failed to load custom codec shared library %s: %s\n", + customCodecLibraryName, + diag.error[0] ? diag.error : "unknown error"); + goto cleanup; + } + } + + if (requestedCodecName) { + settings.codecChoice = avifCodecChoiceFromName(requestedCodecName); + if (settings.codecChoice == AVIF_CODEC_CHOICE_AUTO) { + fprintf(stderr, "ERROR: Unrecognized codec: %s\n", requestedCodecName); + goto cleanup; + } + const char * codecName = avifCodecName(settings.codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE); + if (codecName == NULL) { + fprintf(stderr, "ERROR: Codec cannot encode: %s\n", requestedCodecName); + goto cleanup; + } + } + // Check global lossless parameters and set to default if needed. if (lossless) { // Pixel format. @@ -2817,6 +2843,25 @@ int main(int argc, char * argv[]) avifCodecSpecificOptionsFree(&file->settings.codecSpecificOptions); } free(input.files); + if (customCodecLibrary.handle) { + avifBool unloadCustomCodecLibrary = AVIF_TRUE; + if (customCodecLibrary.initialized) { + avifDiagnostics diag; + avifDiagnosticsClearError(&diag); + const avifResult shutdownResult = avifCustomCodecLibraryShutdown(&customCodecLibrary, &diag); + if (shutdownResult != AVIF_RESULT_OK) { + fprintf(stderr, + "ERROR: Failed to shut down custom codec shared library %s: %s\n", + customCodecLibrary.name ? customCodecLibrary.name : "(unknown)", + diag.error[0] ? diag.error : avifResultToString(shutdownResult)); + returnCode = 1; + unloadCustomCodecLibrary = AVIF_FALSE; + } + } + if (unloadCustomCodecLibrary) { + avifCustomCodecLibraryUnload(&customCodecLibrary); + } + } return returnCode; } diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c index 6a370fc39d..7b2539d0a8 100644 --- a/apps/shared/avifutil.c +++ b/apps/shared/avifutil.c @@ -5,14 +5,195 @@ #include #include +#include #include #include #include +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else +#include +#endif + #include "avifjpeg.h" #include "avifpng.h" #include "y4m.h" +static void avifSetDiagnostic(avifDiagnostics * diag, const char * fmt, ...) +{ + if (!diag) { + return; + } + + avifDiagnosticsClearError(diag); + va_list args; + va_start(args, fmt); + vsnprintf(diag->error, sizeof(diag->error), fmt, args); + va_end(args); +} + +static const char * avifCustomCodecLibraryError(char buffer[], size_t bufferSize) +{ +#if defined(_WIN32) + const DWORD error = GetLastError(); + if (error == 0) { + snprintf(buffer, bufferSize, "unknown error"); + return buffer; + } + + DWORD messageLength = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + error, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + buffer, + (DWORD)bufferSize, + NULL); + if (messageLength == 0) { + snprintf(buffer, bufferSize, "error 0x%08lx", (unsigned long)error); + return buffer; + } + while ((messageLength > 0) && ((buffer[messageLength - 1] == '\r') || (buffer[messageLength - 1] == '\n'))) { + buffer[--messageLength] = '\0'; + } + return buffer; +#else + const char * error = dlerror(); + if (!error) { + error = "unknown error"; + } + snprintf(buffer, bufferSize, "%s", error); + return buffer; +#endif +} + +#if defined(_WIN32) +static avifBool avifCustomCodecLibraryLoadFunction(HMODULE module, + const char * moduleName, + const char * symbolName, + void * function, + avifDiagnostics * diag) +{ + char errorBuffer[256]; + FARPROC proc = GetProcAddress(module, symbolName); + if (!proc) { + avifSetDiagnostic(diag, + "failed loading function %s from module %s: %s", + symbolName, + moduleName, + avifCustomCodecLibraryError(errorBuffer, sizeof(errorBuffer))); + return AVIF_FALSE; + } + + // MinGW/GCC warns on direct FARPROC-to-function-pointer casts. + memcpy(function, &proc, sizeof(proc)); + return AVIF_TRUE; +} +#endif + +void avifCustomCodecLibraryUnload(avifCustomCodecLibrary * library) +{ + if (!library) { + return; + } + if (library->handle) { +#if defined(_WIN32) + FreeLibrary((HMODULE)library->handle); +#else + dlclose(library->handle); +#endif + } + memset(library, 0, sizeof(*library)); +} + +static avifBool avifCustomCodecLibraryLoad(avifCustomCodecLibrary * library, const char * name, avifDiagnostics * diag) +{ + char errorBuffer[256]; + + memset(library, 0, sizeof(*library)); + library->name = name; +#if defined(_WIN32) + library->handle = LoadLibraryA(name); +#else + dlerror(); + library->handle = dlopen(name, RTLD_NOW | RTLD_LOCAL); +#endif + if (!library->handle) { + avifSetDiagnostic(diag, "failed loading module %s: %s", name, avifCustomCodecLibraryError(errorBuffer, sizeof(errorBuffer))); + return AVIF_FALSE; + } + +#if defined(_WIN32) + if (!avifCustomCodecLibraryLoadFunction((HMODULE)library->handle, name, AVIF_APPS_CUSTOM_CODEC_SETUP_SYMBOL, &library->setup, diag)) { + avifCustomCodecLibraryUnload(library); + return AVIF_FALSE; + } + if (!avifCustomCodecLibraryLoadFunction((HMODULE)library->handle, name, AVIF_APPS_CUSTOM_CODEC_SHUTDOWN_SYMBOL, &library->shutdown, diag)) { + avifCustomCodecLibraryUnload(library); + return AVIF_FALSE; + } +#else + dlerror(); + library->setup = (avifAppsCustomCodecSetupFunc)dlsym(library->handle, AVIF_APPS_CUSTOM_CODEC_SETUP_SYMBOL); + { + const char * loadError = dlerror(); + if (loadError) { + avifSetDiagnostic(diag, "failed loading function %s from module %s: %s", AVIF_APPS_CUSTOM_CODEC_SETUP_SYMBOL, name, loadError); + avifCustomCodecLibraryUnload(library); + return AVIF_FALSE; + } + } + dlerror(); + library->shutdown = (avifAppsCustomCodecShutdownFunc)dlsym(library->handle, AVIF_APPS_CUSTOM_CODEC_SHUTDOWN_SYMBOL); + { + const char * loadError = dlerror(); + if (loadError) { + avifSetDiagnostic(diag, "failed loading function %s from module %s: %s", AVIF_APPS_CUSTOM_CODEC_SHUTDOWN_SYMBOL, name, loadError); + avifCustomCodecLibraryUnload(library); + return AVIF_FALSE; + } + } +#endif + + return AVIF_TRUE; +} + +avifBool avifCustomCodecLibrarySetup(avifCustomCodecLibrary * library, const char * name, avifDiagnostics * diag) +{ + if (!avifCustomCodecLibraryLoad(library, name, diag)) { + return AVIF_FALSE; + } + + avifDiagnosticsClearError(diag); + const avifResult result = library->setup(diag); + if (result != AVIF_RESULT_OK) { + if (!diag->error[0]) { + avifSetDiagnostic(diag, "custom codec setup failed: %s", avifResultToString(result)); + } + avifCustomCodecLibraryUnload(library); + return AVIF_FALSE; + } + + library->initialized = AVIF_TRUE; + return AVIF_TRUE; +} + +avifResult avifCustomCodecLibraryShutdown(avifCustomCodecLibrary * library, avifDiagnostics * diag) +{ + if (!library || !library->initialized) { + return AVIF_RESULT_OK; + } + avifDiagnosticsClearError(diag); + const avifResult result = library->shutdown(diag); + if (result == AVIF_RESULT_OK) { + library->initialized = AVIF_FALSE; + } + return result; +} + char * avifFileFormatToString(avifAppFileFormat format) { switch (format) { @@ -461,8 +642,6 @@ void avifDumpDiagnostics(const avifDiagnostics * diag) // Windows -#include - int avifQueryCPUCount(void) { int numCPU; diff --git a/apps/shared/avifutil.h b/apps/shared/avifutil.h index bec09ab83e..427e1472c1 100644 --- a/apps/shared/avifutil.h +++ b/apps/shared/avifutil.h @@ -4,7 +4,7 @@ #ifndef LIBAVIF_APPS_SHARED_AVIFUTIL_H #define LIBAVIF_APPS_SHARED_AVIFUTIL_H -#include "avif/avif.h" +#include "avif/apps.h" #ifdef __cplusplus extern "C" { @@ -36,6 +36,19 @@ void avifPrintVersions(void); void avifDumpDiagnostics(const avifDiagnostics * diag); int avifQueryCPUCount(void); // Returns 1 if it cannot query or fails to query +typedef struct avifCustomCodecLibrary +{ + void * handle; + const char * name; + avifAppsCustomCodecSetupFunc setup; + avifAppsCustomCodecShutdownFunc shutdown; + avifBool initialized; +} avifCustomCodecLibrary; + +AVIF_NODISCARD avifBool avifCustomCodecLibrarySetup(avifCustomCodecLibrary * library, const char * name, avifDiagnostics * diag); +AVIF_NODISCARD avifResult avifCustomCodecLibraryShutdown(avifCustomCodecLibrary * library, avifDiagnostics * diag); +void avifCustomCodecLibraryUnload(avifCustomCodecLibrary * library); + typedef enum avifAppFileFormat { AVIF_APP_FILE_FORMAT_UNKNOWN = 0, diff --git a/include/avif/apps.h b/include/avif/apps.h new file mode 100644 index 0000000000..c799c7c3b6 --- /dev/null +++ b/include/avif/apps.h @@ -0,0 +1,37 @@ +// Copyright 2026 Yuan Tong. All rights reserved. +// SPDX-License-Identifier: BSD-2-Clause + +#ifndef AVIF_APPS_H +#define AVIF_APPS_H + +#include "avif/avif.h" // IWYU pragma: export + +#ifdef __cplusplus +extern "C" { +#endif + +// A shared library intended to be loaded by avifenc/avifdec with --custom-codec must export both +// functions below. Only works with build using shared libavif. +// +// Setup is called once after the library is loaded and before the codec name is resolved. The +// custom codec should initialize its private states and call avifRegisterCustomCodec(). +// Shutdown is called once before the library is unloaded, if Setup succeeded. The custom codec can +// use that as an opportunity to clean up their own state. Codecs stay registered after Shutdown, +// but the apps do not use them anymore at that point. +#define AVIF_APPS_CUSTOM_CODEC_SETUP_SYMBOL "avifAppsCustomCodecSetup" +#define AVIF_APPS_CUSTOM_CODEC_SHUTDOWN_SYMBOL "avifAppsCustomCodecShutdown" + +// These functions always need to be exported. +#define AVIF_APPS_CUSTOM_CODEC_API AVIF_HELPER_EXPORT + +typedef avifResult (*avifAppsCustomCodecSetupFunc)(avifDiagnostics * diag); +typedef avifResult (*avifAppsCustomCodecShutdownFunc)(avifDiagnostics * diag); + +AVIF_APPS_CUSTOM_CODEC_API avifResult avifAppsCustomCodecSetup(avifDiagnostics * diag); +AVIF_APPS_CUSTOM_CODEC_API avifResult avifAppsCustomCodecShutdown(avifDiagnostics * diag); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // AVIF_APPS_H diff --git a/include/avif/avif.h b/include/avif/avif.h index 92ea16132e..5a728d6839 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -1062,7 +1062,9 @@ typedef enum avifCodecChoice AVIF_CODEC_CHOICE_LIBGAV1, // Decode only AVIF_CODEC_CHOICE_RAV1E, // Encode only AVIF_CODEC_CHOICE_SVT, // Encode only - AVIF_CODEC_CHOICE_AVM // Experimental (AV2) + AVIF_CODEC_CHOICE_AVM, // Experimental (AV2) + + AVIF_CODEC_CHOICE_CUSTOM_BASE = 0x10000 } avifCodecChoice; typedef enum avifCodecFlag diff --git a/include/avif/codec.h b/include/avif/codec.h new file mode 100644 index 0000000000..595f6657b8 --- /dev/null +++ b/include/avif/codec.h @@ -0,0 +1,211 @@ +// Copyright 2019 Joe Drago. All rights reserved. +// SPDX-License-Identifier: BSD-2-Clause + +#ifndef AVIF_CODEC_H +#define AVIF_CODEC_H + +#include "avif/avif.h" // IWYU pragma: export + +#ifdef __cplusplus +extern "C" { +#endif + +// --------------------------------------------------------------------------- +// Utils + +#define AVIF_ARRAY_DECLARE(TYPENAME, ITEMSTYPE, ITEMSNAME) \ + typedef struct TYPENAME \ + { \ + ITEMSTYPE * ITEMSNAME; \ + uint32_t elementSize; \ + uint32_t count; \ + uint32_t capacity; \ + } TYPENAME +AVIF_NODISCARD AVIF_API avifBool avifArrayCreate(void * arrayStruct, uint32_t elementSize, uint32_t initialCapacity); +AVIF_NODISCARD AVIF_API void * avifArrayPush(void * arrayStruct); +AVIF_API void avifArrayPop(void * arrayStruct); +AVIF_API void avifArrayDestroy(void * arrayStruct); + +// --------------------------------------------------------------------------- +// avifDecodeSample + +// Legal spatial_id values are [0,1,2,3], so this serves as a sentinel value for "do not filter by spatial_id" +#define AVIF_SPATIAL_ID_UNSET 0xff + +typedef struct avifDecodeSample +{ + avifROData data; + avifBool ownsData; + avifBool partialData; // if true, data exists but doesn't have all of the sample in it + + uint32_t itemID; // if non-zero, data comes from a mergedExtents buffer in an avifDecoderItem, not a file offset + uint64_t offset; // additional offset into data. Can be used to offset into an itemID's payload as well. + size_t size; // + uint8_t spatialID; // If set to a value other than AVIF_SPATIAL_ID_UNSET, output frames from this sample should be + // skipped until the output frame's spatial_id matches this ID. + avifBool sync; // is sync sample (keyframe) +} avifDecodeSample; + +// --------------------------------------------------------------------------- +// avifCodecEncodeOutput + +typedef struct avifEncodeSample +{ + avifRWData data; + avifBool sync; // is sync sample (keyframe) +} avifEncodeSample; +AVIF_ARRAY_DECLARE(avifEncodeSampleArray, avifEncodeSample, sample); + +typedef struct avifCodecEncodeOutput +{ + avifEncodeSampleArray samples; +} avifCodecEncodeOutput; + +AVIF_NODISCARD AVIF_API avifCodecEncodeOutput * avifCodecEncodeOutputCreate(void); +AVIF_API avifResult avifCodecEncodeOutputAddSample(avifCodecEncodeOutput * encodeOutput, const uint8_t * data, size_t len, avifBool sync); +AVIF_API void avifCodecEncodeOutputDestroy(avifCodecEncodeOutput * encodeOutput); + +// --------------------------------------------------------------------------- +// avifCodecSpecificOptions (key/value string pairs for advanced tuning) + +typedef struct avifCodecSpecificOption +{ + char * key; // Must be a simple lowercase alphanumeric string + char * value; // Free-form string to be interpreted by the codec +} avifCodecSpecificOption; +AVIF_ARRAY_DECLARE(avifCodecSpecificOptions, avifCodecSpecificOption, entries); + +// --------------------------------------------------------------------------- +// avifCodecType (underlying video format) + +// Alliance for Open Media video formats that can be used in the AVIF image format. +typedef enum avifCodecType +{ + AVIF_CODEC_TYPE_UNKNOWN, + AVIF_CODEC_TYPE_AV1, +#if defined(AVIF_CODEC_AVM) + AVIF_CODEC_TYPE_AV2, // Experimental. +#endif +} avifCodecType; + +// --------------------------------------------------------------------------- +// avifCodec (abstraction layer to use different codec implementations) + +struct avifCodec; +struct avifCodecInternal; + +typedef enum avifEncoderChange +{ + AVIF_ENCODER_CHANGE_MIN_QUANTIZER = (1 << 0), + AVIF_ENCODER_CHANGE_MAX_QUANTIZER = (1 << 1), + AVIF_ENCODER_CHANGE_MIN_QUANTIZER_ALPHA = (1 << 2), + AVIF_ENCODER_CHANGE_MAX_QUANTIZER_ALPHA = (1 << 3), + AVIF_ENCODER_CHANGE_TILE_ROWS_LOG2 = (1 << 4), + AVIF_ENCODER_CHANGE_TILE_COLS_LOG2 = (1 << 5), + AVIF_ENCODER_CHANGE_QUALITY = (1 << 6), + AVIF_ENCODER_CHANGE_QUALITY_ALPHA = (1 << 7), + AVIF_ENCODER_CHANGE_SCALING_MODE = (1 << 8), + + AVIF_ENCODER_CHANGE_CODEC_SPECIFIC = (1 << 30) +} avifEncoderChange; +typedef int avifEncoderChanges; + +typedef avifBool (*avifCodecGetNextImageFunc)(struct avifCodec * codec, + const avifDecodeSample * sample, + avifBool alpha, + avifBool * isLimitedRangeAlpha, + avifImage * image); +// EncodeImage and EncodeFinish are not required to always emit a sample, but when all images are +// encoded and EncodeFinish is called, the number of samples emitted must match the number of submitted frames. +// avifCodecEncodeImageFunc may return AVIF_RESULT_UNKNOWN_ERROR to automatically emit the appropriate +// AVIF_RESULT_ENCODE_COLOR_FAILED or AVIF_RESULT_ENCODE_ALPHA_FAILED depending on the alpha argument. +// avifCodecEncodeImageFunc should use tileRowsLog2 and tileColsLog2 instead of +// encoder->tileRowsLog2, encoder->tileColsLog2, and encoder->autoTiling. The caller of +// avifCodecEncodeImageFunc is responsible for automatic tiling if encoder->autoTiling is set to +// AVIF_TRUE. The actual tiling values are passed to avifCodecEncodeImageFunc as parameters. +// Similarly, avifCodecEncodeImageFunc should use the quality parameter instead of +// encoder->quality, encoder->qualityAlpha, and encoder->qualityGainMap. If disableLaggedOutput is AVIF_TRUE, then +// the encoder will emit the output frame without any lag (if supported). Note that disableLaggedOutput is only +// used by the first call to this function (which initializes the encoder) and is ignored by the subsequent calls. +// +// Note: The caller of avifCodecEncodeImageFunc always passes encoder->data->tileRowsLog2 and +// encoder->data->tileColsLog2 as the tileRowsLog2 and tileColsLog2 arguments. Because +// encoder->data is of a struct type defined in src/write.c, avifCodecEncodeImageFunc cannot +// dereference encoder->data and has to receive encoder->data->tileRowsLog2 and +// encoder->data->tileColsLog2 via function parameters. +typedef avifResult (*avifCodecEncodeImageFunc)(struct avifCodec * codec, + avifEncoder * encoder, + const avifImage * image, + avifBool alpha, + int tileRowsLog2, + int tileColsLog2, + int quality, + avifEncoderChanges encoderChanges, + avifBool disableLaggedOutput, + avifAddImageFlags addImageFlags, + avifCodecEncodeOutput * output); +typedef avifBool (*avifCodecEncodeFinishFunc)(struct avifCodec * codec, avifCodecEncodeOutput * output); +typedef void (*avifCodecDestroyInternalFunc)(struct avifCodec * codec); + +typedef struct avifCodec +{ + const avifCodecSpecificOptions * csOptions; // Contains codec-specific key/value pairs for advanced tuning. + // This array is NOT owned by avifCodec. + struct avifCodecInternal * internal; // up to each codec to use how it wants + avifDiagnostics * diag; // Shallow copy; owned by avifEncoder or avifDecoder + + // Decoder options (for getNextImage): + int maxThreads; // See avifDecoder::maxThreads. + uint32_t imageSizeLimit; // See avifDecoder::imageSizeLimit. + uint32_t imageDimensionLimit; // See avifDecoder::imageDimensionLimit. + uint8_t operatingPoint; // Operating point, defaults to 0. + avifBool allLayers; // if true, the underlying codec must decode all layers, not just the best layer + + avifCodecGetNextImageFunc getNextImage; + avifCodecEncodeImageFunc encodeImage; + avifCodecEncodeFinishFunc encodeFinish; + avifCodecDestroyInternalFunc destroyInternal; +} avifCodec; + +// --------------------------------------------------------------------------- +// Codec registry + +typedef const char * (*versionFunc)(void); + +// The returned avifCodec struct must be allocated with avifAlloc(). +typedef avifCodec * (*avifCodecCreateFunc)(void); + +typedef struct avifCodecInformation +{ + // Output + avifCodecChoice choice; // Codec ID assigned by libavif. + + // Input + avifCodecType type; + const char * name; // Codec name, must be unique. + versionFunc version; + avifCodecCreateFunc create; + avifCodecFlags flags; +} avifCodecInformation; + +// Registers a custom codec. On success, codec->choice is set to the avifCodecChoice assigned to the +// codec, which can then be used as avifEncoder::codecChoice or avifDecoder::codecChoice. +// A custom codec is never picked by AVIF_CODEC_CHOICE_AUTO, and its avifCodecChoice value may +// differ between runs, so look it up with avifCodecChoiceFromName() instead of storing it. +// +// Custom codecs are meant to be registered once, early in the process, and used for the rest of +// its lifetime: +// - codec->name, codec->version and codec->create are stored as is and must stay valid until the +// process exits. If they live in a dynamically loaded library, that library must not be unloaded +// while libavif may still use them. +// - Registration is not thread-safe. Register all custom codecs before any avifEncoder or +// avifDecoder may be used concurrently, and do not register from multiple threads at once. +// - There's a limit on how many custom codecs can be registered. Prefer register once and dispatch +// inside your codec implementation. +AVIF_API avifResult avifRegisterCustomCodec(avifCodecInformation * codec); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif //AVIF_CODEC_H diff --git a/include/avif/internal.h b/include/avif/internal.h index 81d5239727..8dcf47a13e 100644 --- a/include/avif/internal.h +++ b/include/avif/internal.h @@ -4,7 +4,7 @@ #ifndef AVIF_INTERNAL_H #define AVIF_INTERNAL_H -#include "avif/avif.h" // IWYU pragma: export +#include "avif/codec.h" // IWYU pragma: export #ifdef __cplusplus extern "C" { @@ -133,19 +133,6 @@ AVIF_NODISCARD avifBool avifColorPrimariesComputeRGBToRGBMatrix(avifColorPrimari // The output values are not clamped and may be < 0 or > 1. void avifLinearRGBConvertColorSpace(float rgb[4], double coeffs[3][3]); -#define AVIF_ARRAY_DECLARE(TYPENAME, ITEMSTYPE, ITEMSNAME) \ - typedef struct TYPENAME \ - { \ - ITEMSTYPE * ITEMSNAME; \ - uint32_t elementSize; \ - uint32_t count; \ - uint32_t capacity; \ - } TYPENAME -AVIF_NODISCARD avifBool avifArrayCreate(void * arrayStruct, uint32_t elementSize, uint32_t initialCapacity); -AVIF_NODISCARD void * avifArrayPush(void * arrayStruct); -void avifArrayPop(void * arrayStruct); -void avifArrayDestroy(void * arrayStruct); - void avifFractionSimplify(avifFraction * f); // Makes the fractions have a common denominator. AVIF_NODISCARD avifBool avifFractionCD(avifFraction * a, avifFraction * b); @@ -464,22 +451,6 @@ uint8_t avifImageIrotImirToExifOrientation(const avifImage * image); // --------------------------------------------------------------------------- // avifCodecDecodeInput -// Legal spatial_id values are [0,1,2,3], so this serves as a sentinel value for "do not filter by spatial_id" -#define AVIF_SPATIAL_ID_UNSET 0xff - -typedef struct avifDecodeSample -{ - avifROData data; - avifBool ownsData; - avifBool partialData; // if true, data exists but doesn't have all of the sample in it - - uint32_t itemID; // if non-zero, data comes from a mergedExtents buffer in an avifDecoderItem, not a file offset - uint64_t offset; // additional offset into data. Can be used to offset into an itemID's payload as well. - size_t size; // - uint8_t spatialID; // If set to a value other than AVIF_SPATIAL_ID_UNSET, output frames from this sample should be - // skipped until the output frame's spatial_id matches this ID. - avifBool sync; // is sync sample (keyframe) -} avifDecodeSample; AVIF_ARRAY_DECLARE(avifDecodeSampleArray, avifDecodeSample, sample); typedef struct avifCodecDecodeInput @@ -492,35 +463,9 @@ typedef struct avifCodecDecodeInput AVIF_NODISCARD avifCodecDecodeInput * avifCodecDecodeInputCreate(void); void avifCodecDecodeInputDestroy(avifCodecDecodeInput * decodeInput); -// --------------------------------------------------------------------------- -// avifCodecEncodeOutput - -typedef struct avifEncodeSample -{ - avifRWData data; - avifBool sync; // is sync sample (keyframe) -} avifEncodeSample; -AVIF_ARRAY_DECLARE(avifEncodeSampleArray, avifEncodeSample, sample); - -typedef struct avifCodecEncodeOutput -{ - avifEncodeSampleArray samples; -} avifCodecEncodeOutput; - -AVIF_NODISCARD avifCodecEncodeOutput * avifCodecEncodeOutputCreate(void); -avifResult avifCodecEncodeOutputAddSample(avifCodecEncodeOutput * encodeOutput, const uint8_t * data, size_t len, avifBool sync); -void avifCodecEncodeOutputDestroy(avifCodecEncodeOutput * encodeOutput); - // --------------------------------------------------------------------------- // avifCodecSpecificOptions (key/value string pairs for advanced tuning) -typedef struct avifCodecSpecificOption -{ - char * key; // Must be a simple lowercase alphanumeric string - char * value; // Free-form string to be interpreted by the codec -} avifCodecSpecificOption; -AVIF_ARRAY_DECLARE(avifCodecSpecificOptions, avifCodecSpecificOption, entries); - // Returns NULL if a memory allocation failed. AVIF_NODISCARD avifCodecSpecificOptions * avifCodecSpecificOptionsCreate(void); void avifCodecSpecificOptionsClear(avifCodecSpecificOptions * csOptions); @@ -531,98 +476,12 @@ avifResult avifCodecSpecificOptionsSet(avifCodecSpecificOptions * csOptions, con // --------------------------------------------------------------------------- // avifCodecType (underlying video format) -// Alliance for Open Media video formats that can be used in the AVIF image format. -typedef enum avifCodecType -{ - AVIF_CODEC_TYPE_UNKNOWN, - AVIF_CODEC_TYPE_AV1, -#if defined(AVIF_CODEC_AVM) - AVIF_CODEC_TYPE_AV2, // Experimental. -#endif -} avifCodecType; - // Returns AVIF_CODEC_TYPE_UNKNOWN unless the chosen codec is available with the requiredFlags. avifCodecType avifCodecTypeFromChoice(avifCodecChoice choice, avifCodecFlags requiredFlags); // --------------------------------------------------------------------------- // avifCodec (abstraction layer to use different codec implementations) -struct avifCodec; -struct avifCodecInternal; - -typedef enum avifEncoderChange -{ - AVIF_ENCODER_CHANGE_MIN_QUANTIZER = (1 << 0), - AVIF_ENCODER_CHANGE_MAX_QUANTIZER = (1 << 1), - AVIF_ENCODER_CHANGE_MIN_QUANTIZER_ALPHA = (1 << 2), - AVIF_ENCODER_CHANGE_MAX_QUANTIZER_ALPHA = (1 << 3), - AVIF_ENCODER_CHANGE_TILE_ROWS_LOG2 = (1 << 4), - AVIF_ENCODER_CHANGE_TILE_COLS_LOG2 = (1 << 5), - AVIF_ENCODER_CHANGE_QUALITY = (1 << 6), - AVIF_ENCODER_CHANGE_QUALITY_ALPHA = (1 << 7), - AVIF_ENCODER_CHANGE_SCALING_MODE = (1 << 8), - - AVIF_ENCODER_CHANGE_CODEC_SPECIFIC = (1 << 30) -} avifEncoderChange; -typedef int avifEncoderChanges; - -typedef avifBool (*avifCodecGetNextImageFunc)(struct avifCodec * codec, - const avifDecodeSample * sample, - avifBool alpha, - avifBool * isLimitedRangeAlpha, - avifImage * image); -// EncodeImage and EncodeFinish are not required to always emit a sample, but when all images are -// encoded and EncodeFinish is called, the number of samples emitted must match the number of submitted frames. -// avifCodecEncodeImageFunc may return AVIF_RESULT_UNKNOWN_ERROR to automatically emit the appropriate -// AVIF_RESULT_ENCODE_COLOR_FAILED or AVIF_RESULT_ENCODE_ALPHA_FAILED depending on the alpha argument. -// avifCodecEncodeImageFunc should use tileRowsLog2 and tileColsLog2 instead of -// encoder->tileRowsLog2, encoder->tileColsLog2, and encoder->autoTiling. The caller of -// avifCodecEncodeImageFunc is responsible for automatic tiling if encoder->autoTiling is set to -// AVIF_TRUE. The actual tiling values are passed to avifCodecEncodeImageFunc as parameters. -// Similarly, avifCodecEncodeImageFunc should use the quality parameter instead of -// encoder->quality, encoder->qualityAlpha, and encoder->qualityGainMap. If disableLaggedOutput is AVIF_TRUE, then -// the encoder will emit the output frame without any lag (if supported). Note that disableLaggedOutput is only -// used by the first call to this function (which initializes the encoder) and is ignored by the subsequent calls. -// -// Note: The caller of avifCodecEncodeImageFunc always passes encoder->data->tileRowsLog2 and -// encoder->data->tileColsLog2 as the tileRowsLog2 and tileColsLog2 arguments. Because -// encoder->data is of a struct type defined in src/write.c, avifCodecEncodeImageFunc cannot -// dereference encoder->data and has to receive encoder->data->tileRowsLog2 and -// encoder->data->tileColsLog2 via function parameters. -typedef avifResult (*avifCodecEncodeImageFunc)(struct avifCodec * codec, - avifEncoder * encoder, - const avifImage * image, - avifBool alpha, - int tileRowsLog2, - int tileColsLog2, - int quality, - avifEncoderChanges encoderChanges, - avifBool disableLaggedOutput, - avifAddImageFlags addImageFlags, - avifCodecEncodeOutput * output); -typedef avifBool (*avifCodecEncodeFinishFunc)(struct avifCodec * codec, avifCodecEncodeOutput * output); -typedef void (*avifCodecDestroyInternalFunc)(struct avifCodec * codec); - -typedef struct avifCodec -{ - const avifCodecSpecificOptions * csOptions; // Contains codec-specific key/value pairs for advanced tuning. - // This array is NOT owned by avifCodec. - struct avifCodecInternal * internal; // up to each codec to use how it wants - avifDiagnostics * diag; // Shallow copy; owned by avifEncoder or avifDecoder - - // Decoder options (for getNextImage): - int maxThreads; // See avifDecoder::maxThreads. - uint32_t imageSizeLimit; // See avifDecoder::imageSizeLimit. - uint32_t imageDimensionLimit; // See avifDecoder::imageDimensionLimit. - uint8_t operatingPoint; // Operating point, defaults to 0. - avifBool allLayers; // if true, the underlying codec must decode all layers, not just the best layer - - avifCodecGetNextImageFunc getNextImage; - avifCodecEncodeImageFunc encodeImage; - avifCodecEncodeFinishFunc encodeFinish; - avifCodecDestroyInternalFunc destroyInternal; -} avifCodec; - avifResult avifCodecCreate(avifCodecChoice choice, avifCodecFlags requiredFlags, avifCodec ** codec); void avifCodecDestroy(avifCodec * codec); diff --git a/src/avif.c b/src/avif.c index 9b04f4e787..2de3efe649 100644 --- a/src/avif.c +++ b/src/avif.c @@ -1169,22 +1169,9 @@ avifResult avifCodecSpecificOptionsSet(avifCodecSpecificOptions * csOptions, con // --------------------------------------------------------------------------- // Codec availability and versions -typedef const char * (*versionFunc)(void); -typedef avifCodec * (*avifCodecCreateFunc)(void); - -struct AvailableCodec -{ - avifCodecChoice choice; - avifCodecType type; - const char * name; - versionFunc version; - avifCodecCreateFunc create; - uint32_t flags; -}; - // This is the main codec table; it determines all usage/availability in libavif. -static struct AvailableCodec availableCodecs[] = { +static struct avifCodecInformation availableCodecs[] = { // Ordered by preference (for AUTO) #if defined(AVIF_CODEC_DAV1D) @@ -1224,8 +1211,49 @@ static struct AvailableCodec availableCodecs[] = { static const int availableCodecsCount = (sizeof(availableCodecs) / sizeof(availableCodecs[0])) - 1; -static struct AvailableCodec * findAvailableCodec(avifCodecChoice choice, avifCodecFlags requiredFlags) +// --------------------------------------------------------------------------- +// Custom codec registry + +#define MAX_CUSTOM_CODECS 128 + +static avifCodecInformation customCodecs[MAX_CUSTOM_CODECS]; +static int customCodecCount = 0; +static avifCodecChoice nextCustomChoice = AVIF_CODEC_CHOICE_CUSTOM_BASE; + +avifResult avifRegisterCustomCodec(avifCodecInformation * codec) +{ + if (!codec || !codec->name || !codec->version || !codec->create) { + return AVIF_RESULT_INVALID_ARGUMENT; + } + + if (customCodecCount >= MAX_CUSTOM_CODECS) { + return AVIF_RESULT_OUT_OF_MEMORY; + } + + // Check if a codec with the same name already exists + for (int i = 0; i < availableCodecsCount; ++i) { + if (availableCodecs[i].name && !strcmp(availableCodecs[i].name, codec->name)) { + return AVIF_RESULT_INVALID_ARGUMENT; + } + } + for (int i = 0; i < customCodecCount; ++i) { + if (!strcmp(customCodecs[i].name, codec->name)) { + return AVIF_RESULT_INVALID_ARGUMENT; + } + } + + // Assign a custom choice value and register + customCodecs[customCodecCount] = *codec; + customCodecs[customCodecCount].choice = nextCustomChoice++; + codec->choice = customCodecs[customCodecCount].choice; + customCodecCount++; + + return AVIF_RESULT_OK; +} + +static struct avifCodecInformation * findavifCodecInformation(avifCodecChoice choice, avifCodecFlags requiredFlags) { + // First search built-in codecs for (int i = 0; i < availableCodecsCount; ++i) { if ((choice != AVIF_CODEC_CHOICE_AUTO) && (availableCodecs[i].choice != choice)) { continue; @@ -1239,12 +1267,29 @@ static struct AvailableCodec * findAvailableCodec(avifCodecChoice choice, avifCo } return &availableCodecs[i]; } + + if (choice == AVIF_CODEC_CHOICE_AUTO) { + // Custom codec cannot be the default, it must be explicitly selected. + return NULL; + } + + // Then search custom codecs + for (int i = 0; i < customCodecCount; ++i) { + if (customCodecs[i].choice != choice) { + continue; + } + if (requiredFlags && ((customCodecs[i].flags & requiredFlags) != requiredFlags)) { + continue; + } + return &customCodecs[i]; + } + return NULL; } const char * avifCodecName(avifCodecChoice choice, avifCodecFlags requiredFlags) { - struct AvailableCodec * availableCodec = findAvailableCodec(choice, requiredFlags); + struct avifCodecInformation * availableCodec = findavifCodecInformation(choice, requiredFlags); if (availableCodec) { return availableCodec->name; } @@ -1253,7 +1298,7 @@ const char * avifCodecName(avifCodecChoice choice, avifCodecFlags requiredFlags) avifCodecType avifCodecTypeFromChoice(avifCodecChoice choice, avifCodecFlags requiredFlags) { - struct AvailableCodec * availableCodec = findAvailableCodec(choice, requiredFlags); + struct avifCodecInformation * availableCodec = findavifCodecInformation(choice, requiredFlags); if (availableCodec) { return availableCodec->type; } @@ -1267,13 +1312,18 @@ avifCodecChoice avifCodecChoiceFromName(const char * name) return availableCodecs[i].choice; } } + for (int i = 0; i < customCodecCount; ++i) { + if (!strcmp(customCodecs[i].name, name)) { + return customCodecs[i].choice; + } + } return AVIF_CODEC_CHOICE_AUTO; } avifResult avifCodecCreate(avifCodecChoice choice, avifCodecFlags requiredFlags, avifCodec ** codec) { *codec = NULL; - struct AvailableCodec * availableCodec = findAvailableCodec(choice, requiredFlags); + struct avifCodecInformation * availableCodec = findavifCodecInformation(choice, requiredFlags); AVIF_CHECKERR(availableCodec != NULL, AVIF_RESULT_NO_CODEC_AVAILABLE); *codec = availableCodec->create(); AVIF_CHECKERR(*codec != NULL, AVIF_RESULT_OUT_OF_MEMORY); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8a3dd797a9..06d943ba92 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -88,6 +88,9 @@ if(AVIF_GTEST) add_avif_gtest(avifclaptest) add_avif_gtest(avifcllitest) add_avif_gtest(avifcodectest) + add_avif_gtest(avifcustomcodectest) + target_sources(avifcustomcodectest PRIVATE customcodec/stub_codec.c) + target_include_directories(avifcustomcodectest PRIVATE customcodec) add_avif_gtest_with_data(avifcolrconverttest) add_avif_gtest(avifcolrtest) add_avif_gtest_with_data(avifdecodetest) @@ -271,6 +274,18 @@ if(AVIF_BUILD_APPS) add_cmd_test(test_cmd_transform ${CMAKE_CURRENT_SOURCE_DIR}/data) add_cmd_test(test_cmd_avifgainmaputil ${CMAKE_CURRENT_SOURCE_DIR}/data) + # The custom codec library must share the libavif registry with the apps. Only the shared libavif build + # guarantees that: the static build hides libavif symbols (C_VISIBILITY_PRESET hidden), so the apps cannot + # export them to the library. + if(BUILD_SHARED_LIBS) + add_library(avif_custom_codec_stub MODULE customcodec/stub_codec.c customcodec/stub_plugin.c) + target_link_libraries(avif_custom_codec_stub PRIVATE avif avif_enable_warnings) + add_cmd_test(test_cmd_custom_codec ${CMAKE_CURRENT_SOURCE_DIR}/data) + set_tests_properties( + test_cmd_custom_codec PROPERTIES ENVIRONMENT "AVIF_CUSTOM_CODEC_STUB=$" + ) + endif() + if(NOT AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) set_tests_properties(test_cmd_stdin PROPERTIES DISABLED True) endif() diff --git a/tests/customcodec/stub_codec.c b/tests/customcodec/stub_codec.c new file mode 100644 index 0000000000..a0084807a9 --- /dev/null +++ b/tests/customcodec/stub_codec.c @@ -0,0 +1,173 @@ +// Copyright 2026 Yuan Tong. All rights reserved. +// SPDX-License-Identifier: BSD-2-Clause + +#include "stub_codec.h" + +#include + +// clang-format off +static const uint8_t stubBitstream[] = { + // OBU_SEQUENCE_HEADER, has_size_field, size 5: + // seq_profile 0, reduced_still_picture_header, seq_level_idx 0, max_frame_width/height 8, + // 8-bit, not monochrome, color_range 0, chroma_sample_position 0, followed by trailing bits. + 0x0A, 0x05, 0x18, 0x08, 0xBF, 0x00, 0x02, + // OBU_PADDING, has_size_field, size 16: magic payload. + 0x7A, 0x10, 'a', 'v', 'i', 'f', ' ', 'c', 'u', 's', 't', 'o', 'm', ' ', 's', 't', 'u', 'b', +}; +// clang-format on + +static avifStubCodecStats stats; + +uint8_t avifStubCodecPixel(int plane, uint32_t x, uint32_t y) +{ + switch (plane) { + case AVIF_CHAN_Y: + return (uint8_t)(16 + 8 * y + x); + case AVIF_CHAN_U: + return (uint8_t)(96 + 4 * y + x); + default: + return (uint8_t)(160 + 4 * y + x); + } +} + +static avifBool stubImageMatches(const avifImage * image) +{ + if (image->width != AVIF_STUB_CODEC_WIDTH || image->height != AVIF_STUB_CODEC_HEIGHT || + image->depth != AVIF_STUB_CODEC_DEPTH || image->yuvFormat != AVIF_STUB_CODEC_FORMAT) { + return AVIF_FALSE; + } + for (int plane = AVIF_CHAN_Y; plane <= AVIF_CHAN_V; ++plane) { + const uint32_t width = avifImagePlaneWidth(image, plane); + const uint32_t height = avifImagePlaneHeight(image, plane); + const uint32_t rowBytes = avifImagePlaneRowBytes(image, plane); + const uint8_t * pixels = avifImagePlane(image, plane); + for (uint32_t y = 0; y < height; ++y) { + for (uint32_t x = 0; x < width; ++x) { + if (pixels[y * rowBytes + x] != avifStubCodecPixel(plane, x, y)) { + return AVIF_FALSE; + } + } + } + } + return AVIF_TRUE; +} + +static avifResult stubCodecEncodeImage(avifCodec * codec, + avifEncoder * encoder, + const avifImage * image, + avifBool alpha, + int tileRowsLog2, + int tileColsLog2, + int quality, + avifEncoderChanges encoderChanges, + avifBool disableLaggedOutput, + avifAddImageFlags addImageFlags, + avifCodecEncodeOutput * output) +{ + (void)codec; + (void)encoder; + (void)tileRowsLog2; + (void)tileColsLog2; + (void)quality; + (void)encoderChanges; + (void)disableLaggedOutput; + (void)addImageFlags; + + if (alpha || !stubImageMatches(image)) { + return AVIF_RESULT_UNKNOWN_ERROR; + } + const avifResult result = avifCodecEncodeOutputAddSample(output, stubBitstream, sizeof(stubBitstream), AVIF_TRUE); + if (result == AVIF_RESULT_OK) { + ++stats.encodedFrames; + } + return result; +} + +static avifBool stubCodecEncodeFinish(avifCodec * codec, avifCodecEncodeOutput * output) +{ + (void)codec; + (void)output; + return AVIF_TRUE; +} + +static avifBool stubCodecGetNextImage(avifCodec * codec, + const avifDecodeSample * sample, + avifBool alpha, + avifBool * isLimitedRangeAlpha, + avifImage * image) +{ + (void)codec; + (void)isLimitedRangeAlpha; + + if (alpha || !sample || sample->data.size != sizeof(stubBitstream) || + memcmp(sample->data.data, stubBitstream, sizeof(stubBitstream)) != 0) { + return AVIF_FALSE; + } + + avifImageFreePlanes(image, AVIF_PLANES_YUV); + image->width = AVIF_STUB_CODEC_WIDTH; + image->height = AVIF_STUB_CODEC_HEIGHT; + image->depth = AVIF_STUB_CODEC_DEPTH; + image->yuvFormat = AVIF_STUB_CODEC_FORMAT; + if (avifImageAllocatePlanes(image, AVIF_PLANES_YUV) != AVIF_RESULT_OK) { + return AVIF_FALSE; + } + for (int plane = AVIF_CHAN_Y; plane <= AVIF_CHAN_V; ++plane) { + const uint32_t width = avifImagePlaneWidth(image, plane); + const uint32_t height = avifImagePlaneHeight(image, plane); + const uint32_t rowBytes = avifImagePlaneRowBytes(image, plane); + uint8_t * pixels = avifImagePlane(image, plane); + for (uint32_t y = 0; y < height; ++y) { + for (uint32_t x = 0; x < width; ++x) { + pixels[y * rowBytes + x] = avifStubCodecPixel(plane, x, y); + } + } + } + ++stats.decodedFrames; + return AVIF_TRUE; +} + +static const char * stubCodecVersion(void) +{ + return "1.0"; +} + +static avifCodec * stubCodecCreate(void) +{ + // libavif releases the codec with avifFree(), so it must be allocated with avifAlloc(). + avifCodec * codec = (avifCodec *)avifAlloc(sizeof(avifCodec)); + if (!codec) { + return NULL; + } + memset(codec, 0, sizeof(*codec)); + codec->getNextImage = stubCodecGetNextImage; + codec->encodeImage = stubCodecEncodeImage; + codec->encodeFinish = stubCodecEncodeFinish; + return codec; +} + +avifResult avifStubCodecRegister(avifCodecChoice * choice) +{ + const avifCodecChoice existingChoice = avifCodecChoiceFromName(AVIF_STUB_CODEC_NAME); + if (existingChoice != AVIF_CODEC_CHOICE_AUTO) { + *choice = existingChoice; + return AVIF_RESULT_OK; + } + + avifCodecInformation info = { 0 }; + info.type = AVIF_CODEC_TYPE_AV1; + info.name = AVIF_STUB_CODEC_NAME; + info.version = stubCodecVersion; + info.create = stubCodecCreate; + info.flags = AVIF_CODEC_FLAG_CAN_DECODE | AVIF_CODEC_FLAG_CAN_ENCODE; + const avifResult result = avifRegisterCustomCodec(&info); + if (result == AVIF_RESULT_OK) { + *choice = info.choice; + } + return result; +} + +avifStubCodecStats avifStubCodecGetStats(void) +{ + return stats; +} diff --git a/tests/customcodec/stub_codec.h b/tests/customcodec/stub_codec.h new file mode 100644 index 0000000000..b7d55255d8 --- /dev/null +++ b/tests/customcodec/stub_codec.h @@ -0,0 +1,46 @@ +// Copyright 2026 Yuan Tong. All rights reserved. +// SPDX-License-Identifier: BSD-2-Clause + +// A stub custom codec used to test the custom codec registry. It only uses the public libavif API, +// so it can be linked statically into a test or built as a shared library loaded by the apps. +// +// The encoder only accepts the fixed input image described below and always emits the same +// hardcoded AV1 bitstream. The decoder only accepts that bitstream and always outputs the fixed +// image. A successful round trip therefore proves both requests reached the stub codec. + +#ifndef LIBAVIF_TESTS_CUSTOMCODEC_STUB_CODEC_H +#define LIBAVIF_TESTS_CUSTOMCODEC_STUB_CODEC_H + +#include "avif/codec.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define AVIF_STUB_CODEC_NAME "stub" + +// The fixed image: 8x8, 8-bit, YUV 4:2:0. +#define AVIF_STUB_CODEC_WIDTH 8 +#define AVIF_STUB_CODEC_HEIGHT 8 +#define AVIF_STUB_CODEC_DEPTH 8 +#define AVIF_STUB_CODEC_FORMAT AVIF_PIXEL_FORMAT_YUV420 +// Sample value of the fixed image at (x, y) of the given plane (AVIF_CHAN_Y, AVIF_CHAN_U or AVIF_CHAN_V). +uint8_t avifStubCodecPixel(int plane, uint32_t x, uint32_t y); + +typedef struct avifStubCodecStats +{ + uint32_t encodedFrames; + uint32_t decodedFrames; +} avifStubCodecStats; + +// Registers the stub codec, or returns the existing registration if a codec named +// AVIF_STUB_CODEC_NAME is already registered. +avifResult avifStubCodecRegister(avifCodecChoice * choice); +// Number of frames that went through the stub encoder and decoder so far. +avifStubCodecStats avifStubCodecGetStats(void); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // LIBAVIF_TESTS_CUSTOMCODEC_STUB_CODEC_H diff --git a/tests/customcodec/stub_plugin.c b/tests/customcodec/stub_plugin.c new file mode 100644 index 0000000000..985796c7e6 --- /dev/null +++ b/tests/customcodec/stub_plugin.c @@ -0,0 +1,25 @@ +// Copyright 2026 Yuan Tong. All rights reserved. +// SPDX-License-Identifier: BSD-2-Clause + +// Exposes the stub codec to avifenc/avifdec through --custom-codec. + +#include + +#include "avif/apps.h" +#include "stub_codec.h" + +avifResult avifAppsCustomCodecSetup(avifDiagnostics * diag) +{ + (void)diag; + avifCodecChoice choice; + return avifStubCodecRegister(&choice); +} + +avifResult avifAppsCustomCodecShutdown(avifDiagnostics * diag) +{ + (void)diag; + const avifStubCodecStats stats = avifStubCodecGetStats(); + // Checked by test_cmd_custom_codec.sh. + printf("Custom codec stub: encoded %u frame(s), decoded %u frame(s)\n", stats.encodedFrames, stats.decodedFrames); + return AVIF_RESULT_OK; +} diff --git a/tests/data/custom_codec_stub_8x8.y4m b/tests/data/custom_codec_stub_8x8.y4m new file mode 100644 index 0000000000..853b5bd757 --- /dev/null +++ b/tests/data/custom_codec_stub_8x8.y4m @@ -0,0 +1,3 @@ +YUV4MPEG2 W8 H8 F25:1 Ip A0:0 C420jpeg +FRAME + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNO`abcdefghijklmno ¡¢£¤¥¦§¨©ª«¬­®¯ \ No newline at end of file diff --git a/tests/gtest/avifcustomcodectest.cc b/tests/gtest/avifcustomcodectest.cc new file mode 100644 index 0000000000..5df9112450 --- /dev/null +++ b/tests/gtest/avifcustomcodectest.cc @@ -0,0 +1,128 @@ +// Copyright 2026 Yuan Tong. All rights reserved. +// SPDX-License-Identifier: BSD-2-Clause + +#include "avif/avif.h" +#include "avif/codec.h" +#include "aviftest_helpers.h" +#include "gtest/gtest.h" +#include "stub_codec.h" + +namespace avif { +namespace { + +// Custom codec registry is process global, so the stub codec is registered once +// for all tests. +class CustomCodecTest : public testing::Test { + protected: + static void SetUpTestSuite() { + ASSERT_EQ(avifStubCodecRegister(&stub_choice_), AVIF_RESULT_OK); + } + + static avifCodecChoice stub_choice_; +}; + +avifCodecChoice CustomCodecTest::stub_choice_ = AVIF_CODEC_CHOICE_AUTO; + +const char* DummyVersion() { return "0"; } +avifCodec* DummyCreate() { return nullptr; } + +TEST_F(CustomCodecTest, Register) { + // The stub codec got a custom choice and can be looked up by name. + EXPECT_GE(stub_choice_, AVIF_CODEC_CHOICE_CUSTOM_BASE); + EXPECT_EQ(avifCodecChoiceFromName(AVIF_STUB_CODEC_NAME), stub_choice_); + EXPECT_STREQ(avifCodecName(stub_choice_, AVIF_CODEC_FLAG_CAN_ENCODE), + AVIF_STUB_CODEC_NAME); + EXPECT_STREQ(avifCodecName(stub_choice_, AVIF_CODEC_FLAG_CAN_DECODE), + AVIF_STUB_CODEC_NAME); + + avifCodecInformation info = {}; + info.type = AVIF_CODEC_TYPE_AV1; + info.name = "encodeonly"; + info.version = DummyVersion; + info.create = DummyCreate; + info.flags = AVIF_CODEC_FLAG_CAN_ENCODE; + + // Missing fields. + EXPECT_EQ(avifRegisterCustomCodec(nullptr), AVIF_RESULT_INVALID_ARGUMENT); + avifCodecInformation invalid = info; + invalid.name = nullptr; + EXPECT_EQ(avifRegisterCustomCodec(&invalid), AVIF_RESULT_INVALID_ARGUMENT); + invalid = info; + invalid.version = nullptr; + EXPECT_EQ(avifRegisterCustomCodec(&invalid), AVIF_RESULT_INVALID_ARGUMENT); + invalid = info; + invalid.create = nullptr; + EXPECT_EQ(avifRegisterCustomCodec(&invalid), AVIF_RESULT_INVALID_ARGUMENT); + + // Duplicate names. + invalid = info; + invalid.name = AVIF_STUB_CODEC_NAME; + EXPECT_EQ(avifRegisterCustomCodec(&invalid), AVIF_RESULT_INVALID_ARGUMENT); + const char* built_in_name = avifCodecName(AVIF_CODEC_CHOICE_AUTO, 0); + if (built_in_name != nullptr) { + invalid.name = built_in_name; + EXPECT_EQ(avifRegisterCustomCodec(&invalid), AVIF_RESULT_INVALID_ARGUMENT); + } + + // The flags of a custom codec are honored. + info.choice = AVIF_CODEC_CHOICE_AUTO; + ASSERT_EQ(avifRegisterCustomCodec(&info), AVIF_RESULT_OK); + EXPECT_GE(info.choice, AVIF_CODEC_CHOICE_CUSTOM_BASE); + EXPECT_NE(info.choice, stub_choice_); + EXPECT_EQ(avifCodecChoiceFromName("encodeonly"), info.choice); + EXPECT_STREQ(avifCodecName(info.choice, AVIF_CODEC_FLAG_CAN_ENCODE), + "encodeonly"); + EXPECT_EQ(avifCodecName(info.choice, AVIF_CODEC_FLAG_CAN_DECODE), nullptr); +} + +TEST_F(CustomCodecTest, NotSelectedByAuto) { + for (avifCodecFlags flags : + {avifCodecFlags{0}, avifCodecFlags{AVIF_CODEC_FLAG_CAN_ENCODE}, + avifCodecFlags{AVIF_CODEC_FLAG_CAN_DECODE}}) { + const char* name = avifCodecName(AVIF_CODEC_CHOICE_AUTO, flags); + if (name != nullptr) { + EXPECT_STRNE(name, AVIF_STUB_CODEC_NAME); + } + } +} + +TEST_F(CustomCodecTest, EncodeDecode) { + ImagePtr image = testutil::CreateImage( + AVIF_STUB_CODEC_WIDTH, AVIF_STUB_CODEC_HEIGHT, AVIF_STUB_CODEC_DEPTH, + AVIF_STUB_CODEC_FORMAT, AVIF_PLANES_YUV); + ASSERT_NE(image, nullptr); + for (int plane = AVIF_CHAN_Y; plane <= AVIF_CHAN_V; ++plane) { + uint8_t* row = avifImagePlane(image.get(), plane); + for (uint32_t y = 0; y < avifImagePlaneHeight(image.get(), plane); ++y) { + for (uint32_t x = 0; x < avifImagePlaneWidth(image.get(), plane); ++x) { + row[x] = avifStubCodecPixel(plane, x, y); + } + row += avifImagePlaneRowBytes(image.get(), plane); + } + } + const avifStubCodecStats stats_before = avifStubCodecGetStats(); + + EncoderPtr encoder(avifEncoderCreate()); + ASSERT_NE(encoder, nullptr); + encoder->codecChoice = stub_choice_; + testutil::AvifRwData encoded; + ASSERT_EQ(avifEncoderWrite(encoder.get(), image.get(), &encoded), + AVIF_RESULT_OK); + EXPECT_EQ(avifStubCodecGetStats().encodedFrames, + stats_before.encodedFrames + 1); + + DecoderPtr decoder(avifDecoderCreate()); + ASSERT_NE(decoder, nullptr); + decoder->codecChoice = stub_choice_; + ImagePtr decoded(avifImageCreateEmpty()); + ASSERT_NE(decoded, nullptr); + ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data, + encoded.size), + AVIF_RESULT_OK); + EXPECT_EQ(avifStubCodecGetStats().decodedFrames, + stats_before.decodedFrames + 1); + EXPECT_TRUE(testutil::AreImagesEqual(*image, *decoded)); +} + +} // namespace +} // namespace avif diff --git a/tests/test_cmd_custom_codec.sh b/tests/test_cmd_custom_codec.sh new file mode 100644 index 0000000000..f3375d7f63 --- /dev/null +++ b/tests/test_cmd_custom_codec.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Copyright 2026 Yuan Tong. All rights reserved. +# SPDX-License-Identifier: BSD-2-Clause +# ------------------------------------------------------------------------------ +# +# Tests loading a custom codec shared library in avifenc and avifdec. +# AVIF_CUSTOM_CODEC_STUB must be set to the path of the avif_custom_codec_stub library. + +source $(dirname "$0")/cmd_test_common.sh || exit + +# Input file paths. This image is the only input accepted by the stub encoder, +# and the only output of the stub decoder. +INPUT_Y4M="${TESTDATA_DIR}/custom_codec_stub_8x8.y4m" +# Output file names. +ENCODED_FILE="avif_test_cmd_custom_codec_encoded.avif" +DECODED_FILE="avif_test_cmd_custom_codec_decoded.y4m" +OUT_MSG="avif_test_cmd_custom_codec_out_msg.txt" + +# Cleanup +cleanup() { + pushd ${TMP_DIR} + rm -f -- "${ENCODED_FILE}" "${DECODED_FILE}" "${OUT_MSG}" + popd +} +trap cleanup EXIT + +pushd ${TMP_DIR} + echo "Testing encoding with a custom codec" + "${AVIFENC}" --custom-codec "${AVIF_CUSTOM_CODEC_STUB}" -c stub "${INPUT_Y4M}" -o "${ENCODED_FILE}" > "${OUT_MSG}" + grep -F "Custom codec stub: encoded 1 frame(s), decoded 0 frame(s)" "${OUT_MSG}" + + echo "Testing decoding with a custom codec" + "${AVIFDEC}" --custom-codec "${AVIF_CUSTOM_CODEC_STUB}" -c stub "${ENCODED_FILE}" "${DECODED_FILE}" > "${OUT_MSG}" + grep -F "Custom codec stub: encoded 0 frame(s), decoded 1 frame(s)" "${OUT_MSG}" + "${ARE_IMAGES_EQUAL}" "${INPUT_Y4M}" "${DECODED_FILE}" 0 +popd + +exit 0