diff --git a/CMakeLists.txt b/CMakeLists.txt index cdd0f2941..98c005822 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,9 +88,10 @@ cmake_dependent_option(ENABLE_EET "Enable eet" ON "LINUX OR FreeBSD OR OpenBSD O cmake_dependent_option(ENABLE_DBUS "Enable dbus-1" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR ANDROID OR SunOS OR Haiku OR GNU" OFF) cmake_dependent_option(ENABLE_SQLITE3 "Enable sqlite3" ON "LINUX OR FreeBSD OR APPLE OR OpenBSD OR NetBSD OR SunOS OR GNU" OFF) cmake_dependent_option(ENABLE_RPM "Enable rpm" ON "LINUX OR GNU" OFF) -cmake_dependent_option(ENABLE_IMAGEMAGICK7 "Enable imagemagick 7" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR APPLE OR ANDROID OR WIN32 OR SunOS OR Haiku OR GNU" OFF) +cmake_dependent_option(ENABLE_IMAGEMAGICK7 "Enable imagemagick 7" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR APPLE OR ANDROID OR SunOS OR Haiku OR GNU" OFF) cmake_dependent_option(ENABLE_IMAGEMAGICK6 "Enable imagemagick 6" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR APPLE OR SunOS OR GNU" OFF) -cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7" OFF) +cmake_dependent_option(ENABLE_SIXEL "Enable sixel logo output via the embedded libsixel encoder (Windows only; replaces ImageMagick's SIXEL coder)" ON "WIN32" OFF) +cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7 OR WIN32" OFF) cmake_dependent_option(ENABLE_EGL "Enable egl" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR ANDROID OR WIN32 OR SunOS OR Haiku OR GNU" OFF) cmake_dependent_option(ENABLE_GLX "Enable glx" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR ANDROID OR SunOS OR GNU" OFF) cmake_dependent_option(ENABLE_OPENCL "Enable opencl" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR WIN32 OR ANDROID OR SunOS OR Haiku OR GNU" OFF) @@ -101,7 +102,6 @@ cmake_dependent_option(ENABLE_ELF "Enable libelf" ON "LINUX OR ANDROID OR Dragon cmake_dependent_option(ENABLE_THREADS "Enable multithreading" ON "Threads_FOUND" OFF) option(ENABLE_ZLIB "Enable zlib" ON) -option(ENABLE_SIXEL "Enable sixel image logo output (vendored libsixel encoder)" ON) option(ENABLE_SYSTEM_YYJSON "Use system provided (instead of fastfetch embedded) yyjson library" OFF) option(ENABLE_ASAN "Build fastfetch with ASAN (address sanitizer)" OFF) option(ENABLE_TRACER "Build fastfetch with function tracing" OFF) @@ -504,6 +504,7 @@ set(LIBFASTFETCH_SRC src/logo/image/im6.c src/logo/image/im7.c src/logo/image/image.c + src/logo/image/sixel.c src/logo/logo.c src/modules/modules.c src/options/display.c @@ -1115,6 +1116,7 @@ elseif(WIN32) src/detection/de/de_nosupport.c src/detection/wmtheme/wmtheme_windows.c src/detection/camera/camera_windows.cpp + src/logo/image/wic.cpp ) elseif(SunOS) list(APPEND LIBFASTFETCH_SRC @@ -1913,6 +1915,7 @@ elseif(WIN32) PRIVATE "cfgmgr32" PRIVATE "winbrand" PRIVATE "secur32" + PRIVATE "windowscodecs" ) if(NOT ENABLE_WIN81_COMPAT) target_link_libraries(libfastfetch @@ -2013,6 +2016,10 @@ if(ENABLE_SIXEL) # src/3rdparty/sixel/*.c include (angle brackets, so the directory itself # has to be on the include path) target_include_directories(libfastfetch PRIVATE ${PROJECT_SOURCE_DIR}/src/3rdparty/sixel) + + # The vendored subset is not a library to be detected, so FF_HAVE_SIXEL can't come + # from ff_lib_enable() and has to be defined here + target_compile_definitions(libfastfetch PUBLIC FF_HAVE_SIXEL=1) endif() target_link_libraries(libfastfetch diff --git a/src/common/impl/init.c b/src/common/impl/init.c index f4307e1f9..16932e1e2 100644 --- a/src/common/impl/init.c +++ b/src/common/impl/init.c @@ -281,6 +281,9 @@ void ffListFeatures(void) { #if FF_ENABLE_WCWIDTH "Embedded wcwidth\n" #endif +#if FF_HAVE_SIXEL + "Embedded sixel\n" +#endif #if FF_HAVE_WINRT "WinRT headers\n" #endif diff --git a/src/logo/image/im6.c b/src/logo/image/im6.c index b8743b7ec..f2c7bb0b3 100644 --- a/src/logo/image/im6.c +++ b/src/logo/image/im6.c @@ -2,8 +2,10 @@ #include "image.h" #include "common/library.h" + #include "common/mallocHelper.h" #include + #include static FF_LIBRARY_SYMBOL(ResizeImage) @@ -11,7 +13,10 @@ static FF_LIBRARY_SYMBOL(ResizeImage) return ffResizeImage(image, width, height, UndefinedFilter, 1.0, exceptionInfo); } -FFLogoImageResult ffLogoPrintImageIM6(FFLogoRequestData* requestData) { +// Decode the image source, resize it to the requested pixel size and export it as a blob +// in the given ImageMagick format ("RGBA" for kitty / chafa, "SIXEL" for sixel output). +// On success requestData->logoPixelWidth / logoPixelHeight hold the real image dimensions. +static FFLogoImageResult im6EncodeImage(FFLogoRequestData* requestData, const char* magick, uint32_t magickLength, void** outBlob, size_t* outLength) { // clang-format off FF_LIBRARY_LOAD(imageMagick, FF_LOGO_IMAGE_RESULT_INIT_ERROR, "libMagickCore-6.Q16HDRI" FF_LIBRARY_EXTENSION, 8, @@ -21,13 +26,137 @@ FFLogoImageResult ffLogoPrintImageIM6(FFLogoRequestData* requestData) { // clang-format on FF_LIBRARY_LOAD_SYMBOL_ADDRESS(imageMagick, ffResizeImage, ResizeImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FFLogoImageResult result = ffLogoPrintImageImpl(requestData, &(FFIMData) { - .resizeFunc = logoResize, - .library = imageMagick, - }); + FF_LIBRARY_LOAD_SYMBOL(imageMagick, MagickCoreGenesis, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, MagickCoreTerminus, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireExceptionInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyExceptionInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, ReadImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, CopyMagickString, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, ImageToBlob, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - imageMagick = nullptr; // leak imageMagick to prevent fastfetch from crashing #552 + FFLogoImageResult result = FF_LOGO_IMAGE_RESULT_RUN_ERROR; + ExceptionInfo* exceptionInfo = nullptr; + Image* image = nullptr; + ImageInfo* imageInfoOut = nullptr; + FF_AUTO_FREE void* blob = nullptr; + size_t length = 0; + + ffMagickCoreGenesis(nullptr, MagickFalse); + + exceptionInfo = ffAcquireExceptionInfo(); + if (exceptionInfo == nullptr) { + goto cleanup; + } + + { + ImageInfo* imageInfoIn = ffAcquireImageInfo(); + if (imageInfoIn == nullptr) { + goto cleanup; + } + + //+1, because we need to copy the null byte too + ffCopyMagickString(imageInfoIn->filename, instance.config.logo.source.chars, instance.config.logo.source.length + 1); + + image = ffReadImage(imageInfoIn, exceptionInfo); + ffDestroyImageInfo(imageInfoIn); + if (image == nullptr) { + goto cleanup; + } + } + + if (requestData->logoPixelWidth == 0 && requestData->logoPixelHeight == 0) { + requestData->logoPixelWidth = (uint32_t) image->columns; + requestData->logoPixelHeight = (uint32_t) image->rows; + } else if (requestData->logoPixelWidth == 0) { + requestData->logoPixelWidth = (uint32_t) ((double) image->columns / (double) image->rows * requestData->logoPixelHeight); + } else if (requestData->logoPixelHeight == 0) { + requestData->logoPixelHeight = (uint32_t) ((double) image->rows / (double) image->columns * requestData->logoPixelWidth); + } + + if (requestData->logoPixelWidth == 0 || requestData->logoPixelHeight == 0) { + goto cleanup; + } + + { + Image* resized = logoResize(image, requestData->logoPixelWidth, requestData->logoPixelHeight, exceptionInfo); + ffDestroyImage(image); + image = resized; + if (image == nullptr) { + goto cleanup; + } + } + + imageInfoOut = ffAcquireImageInfo(); + if (imageInfoOut == nullptr) { + goto cleanup; + } + + ffCopyMagickString(imageInfoOut->magick, magick, magickLength); + + blob = ffImageToBlob(imageInfoOut, image, &length, exceptionInfo); + if (blob == nullptr || length == 0) { + goto cleanup; + } + + *outBlob = blob; + *outLength = length; + blob = nullptr; // Ownership is transferred to the caller + result = FF_LOGO_IMAGE_RESULT_SUCCESS; + +cleanup: + if (imageInfoOut) { + ffDestroyImageInfo(imageInfoOut); + } + if (image) { + ffDestroyImage(image); + } + if (exceptionInfo) { + ffDestroyExceptionInfo(exceptionInfo); + } + ffMagickCoreTerminus(); + + // leak imageMagick to prevent fastfetch from crashing #552 + imageMagick = nullptr; return result; } +static void setError(FFLogoImageResult result, const char** error) { + if (error) { + *error = result == FF_LOGO_IMAGE_RESULT_INIT_ERROR + ? "Image Magick library not found" + : "Failed to load / convert the image source"; + } +} + +bool ffImageCreateIM6(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { + void* blob = nullptr; + size_t length = 0; + FFLogoImageResult result = im6EncodeImage(requestData, "RGBA", 5, &blob, &length); + if (result != FF_LOGO_IMAGE_RESULT_SUCCESS) { + setError(result, error); + return false; + } + + out->data = blob; + out->width = requestData->logoPixelWidth; + out->height = requestData->logoPixelHeight; + return true; +} + +bool ffImageSixelEncodeIM6(FFLogoRequestData* requestData, FFstrbuf* out, const char** error) { + FF_AUTO_FREE void* blob = nullptr; + size_t length = 0; + FFLogoImageResult result = im6EncodeImage(requestData, "SIXEL", 6, &blob, &length); + if (result != FF_LOGO_IMAGE_RESULT_SUCCESS) { + setError(result, error); + return false; + } + + ffStrbufSetNS(out, (uint32_t) length, (const char*) blob); + return true; +} + #endif diff --git a/src/logo/image/im7.c b/src/logo/image/im7.c index b44241ce5..ab8ae94bb 100644 --- a/src/logo/image/im7.c +++ b/src/logo/image/im7.c @@ -2,8 +2,10 @@ #include "image.h" #include "common/library.h" + #include "common/mallocHelper.h" #include + #include static FF_LIBRARY_SYMBOL(ResizeImage) @@ -11,7 +13,10 @@ static FF_LIBRARY_SYMBOL(ResizeImage) return ffResizeImage(image, width, height, UndefinedFilter, exceptionInfo); } -FFLogoImageResult ffLogoPrintImageIM7(FFLogoRequestData* requestData) { +// Decode the image source, resize it to the requested pixel size and export it as a blob +// in the given ImageMagick format ("RGBA" for kitty / chafa, "SIXEL" for sixel output). +// On success requestData->logoPixelWidth / logoPixelHeight hold the real image dimensions. +static FFLogoImageResult im7EncodeImage(FFLogoRequestData* requestData, const char* magick, uint32_t magickLength, void** outBlob, size_t* outLength) { // clang-format off #if _WIN32 FF_LIBRARY_LOAD(imageMagick, FF_LOGO_IMAGE_RESULT_INIT_ERROR, @@ -27,13 +32,137 @@ FFLogoImageResult ffLogoPrintImageIM7(FFLogoRequestData* requestData) { // clang-format on FF_LIBRARY_LOAD_SYMBOL_ADDRESS(imageMagick, ffResizeImage, ResizeImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FFLogoImageResult result = ffLogoPrintImageImpl(requestData, &(FFIMData) { - .resizeFunc = logoResize, - .library = imageMagick, - }); + FF_LIBRARY_LOAD_SYMBOL(imageMagick, MagickCoreGenesis, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, MagickCoreTerminus, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireExceptionInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyExceptionInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, ReadImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, CopyMagickString, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + FF_LIBRARY_LOAD_SYMBOL(imageMagick, ImageToBlob, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - imageMagick = nullptr; // leak imageMagick to prevent fastfetch from crashing #552 + FFLogoImageResult result = FF_LOGO_IMAGE_RESULT_RUN_ERROR; + ExceptionInfo* exceptionInfo = nullptr; + Image* image = nullptr; + ImageInfo* imageInfoOut = nullptr; + FF_AUTO_FREE void* blob = nullptr; + size_t length = 0; + + ffMagickCoreGenesis(nullptr, MagickFalse); + + exceptionInfo = ffAcquireExceptionInfo(); + if (exceptionInfo == nullptr) { + goto cleanup; + } + + { + ImageInfo* imageInfoIn = ffAcquireImageInfo(); + if (imageInfoIn == nullptr) { + goto cleanup; + } + + //+1, because we need to copy the null byte too + ffCopyMagickString(imageInfoIn->filename, instance.config.logo.source.chars, instance.config.logo.source.length + 1); + + image = ffReadImage(imageInfoIn, exceptionInfo); + ffDestroyImageInfo(imageInfoIn); + if (image == nullptr) { + goto cleanup; + } + } + + if (requestData->logoPixelWidth == 0 && requestData->logoPixelHeight == 0) { + requestData->logoPixelWidth = (uint32_t) image->columns; + requestData->logoPixelHeight = (uint32_t) image->rows; + } else if (requestData->logoPixelWidth == 0) { + requestData->logoPixelWidth = (uint32_t) ((double) image->columns / (double) image->rows * requestData->logoPixelHeight); + } else if (requestData->logoPixelHeight == 0) { + requestData->logoPixelHeight = (uint32_t) ((double) image->rows / (double) image->columns * requestData->logoPixelWidth); + } + + if (requestData->logoPixelWidth == 0 || requestData->logoPixelHeight == 0) { + goto cleanup; + } + + { + Image* resized = logoResize(image, requestData->logoPixelWidth, requestData->logoPixelHeight, exceptionInfo); + ffDestroyImage(image); + image = resized; + if (image == nullptr) { + goto cleanup; + } + } + + imageInfoOut = ffAcquireImageInfo(); + if (imageInfoOut == nullptr) { + goto cleanup; + } + + ffCopyMagickString(imageInfoOut->magick, magick, magickLength); + + blob = ffImageToBlob(imageInfoOut, image, &length, exceptionInfo); + if (blob == nullptr || length == 0) { + goto cleanup; + } + + *outBlob = blob; + *outLength = length; + blob = nullptr; // Ownership is transferred to the caller + result = FF_LOGO_IMAGE_RESULT_SUCCESS; + +cleanup: + if (imageInfoOut) { + ffDestroyImageInfo(imageInfoOut); + } + if (image) { + ffDestroyImage(image); + } + if (exceptionInfo) { + ffDestroyExceptionInfo(exceptionInfo); + } + ffMagickCoreTerminus(); + + // leak imageMagick to prevent fastfetch from crashing #552 + imageMagick = nullptr; return result; } +static void setError(FFLogoImageResult result, const char** error) { + if (error) { + *error = result == FF_LOGO_IMAGE_RESULT_INIT_ERROR + ? "Image Magick library not found" + : "Failed to load / convert the image source"; + } +} + +bool ffImageCreateIM7(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { + void* blob = nullptr; + size_t length = 0; + FFLogoImageResult result = im7EncodeImage(requestData, "RGBA", 5, &blob, &length); + if (result != FF_LOGO_IMAGE_RESULT_SUCCESS) { + setError(result, error); + return false; + } + + out->data = blob; + out->width = requestData->logoPixelWidth; + out->height = requestData->logoPixelHeight; + return true; +} + +bool ffImageSixelEncodeIM7(FFLogoRequestData* requestData, FFstrbuf* out, const char** error) { + FF_AUTO_FREE void* blob = nullptr; + size_t length = 0; + FFLogoImageResult result = im7EncodeImage(requestData, "SIXEL", 6, &blob, &length); + if (result != FF_LOGO_IMAGE_RESULT_SUCCESS) { + setError(result, error); + return false; + } + + ffStrbufSetNS(out, (uint32_t) length, (const char*) blob); + return true; +} + #endif diff --git a/src/logo/image/image.c b/src/logo/image/image.c index c97e1f7ba..69973f1c8 100644 --- a/src/logo/image/image.c +++ b/src/logo/image/image.c @@ -1,5 +1,6 @@ #include "image.h" #include "common/io.h" +#include "common/mallocHelper.h" #include "common/printing.h" #include "common/processing.h" #include "common/strutil.h" @@ -340,7 +341,7 @@ static bool printImageKittyDirect(bool printError) { return true; } -#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) +#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(FF_HAVE_SIXEL) #define FF_KITTY_MAX_CHUNK_SIZE 4096 @@ -351,6 +352,7 @@ static bool printImageKittyDirect(bool printError) { #define FF_CACHE_FILE_KITTY_UNCOMPRESSED "kittyu" #define FF_CACHE_FILE_CHAFA "chafa" + #include #include #include #include @@ -364,7 +366,6 @@ static bool printImageKittyDirect(bool printError) { #ifdef FF_HAVE_ZLIB #include "common/library.h" - #include #include static bool compressBlob(void** blob, size_t* length) { @@ -372,13 +373,21 @@ static bool compressBlob(void** blob, size_t* length) { FF_LIBRARY_LOAD_SYMBOL(zlib, compressBound, false) FF_LIBRARY_LOAD_SYMBOL(zlib, compress2, false) - uLong compressedLength = ffcompressBound(*length); + #if _WIN32 + // zlib's uLong is 32-bit on Windows (LLP64), so a >4 GiB source can't be + // compressed through this API; reject it instead of silently truncating + if (*length > (size_t) ULONG_MAX) { + return false; + } + #endif + + uLong compressedLength = ffcompressBound((uLong) *length); void* compressed = malloc(compressedLength); if (compressed == nullptr) { return false; } - if (ffcompress2(compressed, &compressedLength, *blob, *length, Z_BEST_COMPRESSION) != Z_OK) { + if (ffcompress2(compressed, &compressedLength, *blob, (uLong) *length, Z_BEST_COMPRESSION) != Z_OK) { free(compressed); return false; } @@ -392,36 +401,6 @@ static bool compressBlob(void** blob, size_t* length) { #endif // FF_HAVE_ZLIB - // We use only the defines from here, that are exactly the same in both versions - #ifdef FF_HAVE_IMAGEMAGICK7 - #include - #else - #include - #endif - -typedef struct ImageData { - FF_LIBRARY_SYMBOL(CopyMagickString) - FF_LIBRARY_SYMBOL(ImageToBlob) - FF_LIBRARY_SYMBOL(Base64Encode) - - ImageInfo* imageInfo; - Image* image; - ExceptionInfo* exceptionInfo; -} ImageData; - -static inline bool checkAllocationResult(void* data, size_t length) { - if (data == nullptr) { - return false; - } - - if (length == 0) { - free(data); - return false; - } - - return true; -} - static void writeCacheStrbuf(FFLogoRequestData* requestData, const FFstrbuf* value, const char* cacheFileName) { uint32_t cacheDirLength = requestData->cacheDir.length; ffStrbufAppendS(&requestData->cacheDir, cacheFileName); @@ -473,22 +452,18 @@ static void printImagePixels(FFLogoRequestData* requestData, const FFstrbuf* res } } -static bool printImageSixel(FFLogoRequestData* requestData, const ImageData* imageData) { - imageData->ffCopyMagickString(imageData->imageInfo->magick, "SIXEL", 6); +// The backends report the real pixel dimensions; the character dimensions are derived here +static void fillCharacterDimensions(FFLogoRequestData* requestData) { + requestData->logoCharacterWidth = (uint32_t) ceil((double) requestData->logoPixelWidth / requestData->characterPixelWidth); + requestData->logoCharacterHeight = (uint32_t) ceil((double) requestData->logoPixelHeight / requestData->characterPixelHeight); +} - size_t length; - void* blob = imageData->ffImageToBlob(imageData->imageInfo, imageData->image, &length, imageData->exceptionInfo); - if (!checkAllocationResult(blob, length)) { +static bool printImageSixel(FFLogoRequestData* requestData, const FFstrbuf* result) { + if (result->length == 0) { return false; } - FFstrbuf result; - result.chars = (char*) blob; - result.length = (uint32_t) length; - - printImagePixels(requestData, &result, FF_CACHE_FILE_SIXEL); - - free(blob); + printImagePixels(requestData, result, FF_CACHE_FILE_SIXEL); return true; } @@ -509,14 +484,13 @@ static void appendKittyChunk(FFstrbuf* result, const char** blob, size_t* length *blob += chunkSize; } -static bool printImageKitty(FFLogoRequestData* requestData, const ImageData* imageData) { - imageData->ffCopyMagickString(imageData->imageInfo->magick, "RGBA", 5); - - size_t length; - void* blob = imageData->ffImageToBlob(imageData->imageInfo, imageData->image, &length, imageData->exceptionInfo); - if (!checkAllocationResult(blob, length)) { +static bool printImageKitty(FFLogoRequestData* requestData, const FFImageBuffer* buffer) { + size_t length = (size_t) buffer->width * buffer->height * 4; + FF_AUTO_FREE void* blob = malloc(length); + if (blob == nullptr) { return false; } + memcpy(blob, buffer->data, length); #ifdef FF_HAVE_ZLIB bool isCompressed = compressBlob(&blob, &length); @@ -524,18 +498,16 @@ static bool printImageKitty(FFLogoRequestData* requestData, const ImageData* ima bool isCompressed = false; #endif - char* chars = imageData->ffBase64Encode(blob, length, &length); - free(blob); - if (!checkAllocationResult(chars, length)) { - return false; - } + // base64 output is 4 * ceil(length / 3) bytes, plus the terminating null byte + FF_STRBUF_AUTO_DESTROY base64 = ffStrbufCreateA((uint32_t) (10 + length * 4 / 3)); + ffBase64EncodeRaw((uint32_t) length, (const char*) blob, &base64.length, base64.chars); - FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateA((uint32_t) (length + 1024)); + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateA(base64.length + 1024); - const char* currentPos = chars; - size_t remainingLength = length; + const char* currentPos = base64.chars; + size_t remainingLength = base64.length; - ffStrbufAppendF(&result, "\033_Ga=T,f=32,s=%u,v=%u", requestData->logoPixelWidth, requestData->logoPixelHeight); + ffStrbufAppendF(&result, "\033_Ga=T,f=32,s=%u,v=%u", buffer->width, buffer->height); if (isCompressed) { ffStrbufAppendS(&result, ",o=z"); } @@ -545,19 +517,17 @@ static bool printImageKitty(FFLogoRequestData* requestData, const ImageData* ima } printImagePixels(requestData, &result, isCompressed ? FF_CACHE_FILE_KITTY_COMPRESSED : FF_CACHE_FILE_KITTY_UNCOMPRESSED); - - free(chars); return true; } #ifdef FF_HAVE_CHAFA #include -static bool printImageChafa(FFLogoRequestData* requestData, const ImageData* imageData) { - #if _WIN32 +static bool printImageChafa(FFLogoRequestData* requestData, const FFImageBuffer* buffer) { + #if _WIN32 FF_LIBRARY_LOAD(chafa, false, "libchafa-0" FF_LIBRARY_EXTENSION, 0) - #else + #else FF_LIBRARY_LOAD(chafa, false, "libchafa" FF_LIBRARY_EXTENSION, 1) - #endif + #endif FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_new, false) FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_apply_selectors, false) FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_new, false) @@ -570,13 +540,6 @@ static bool printImageChafa(FFLogoRequestData* requestData, const ImageData* ima FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_unref, false) FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_unref, false) - imageData->ffCopyMagickString(imageData->imageInfo->magick, "RGBA", 5); - size_t length; - void* blob = imageData->ffImageToBlob(imageData->imageInfo, imageData->image, &length, imageData->exceptionInfo); - if (!checkAllocationResult(blob, length)) { - return false; - } - ChafaSymbolMap* symbolMap = ffchafa_symbol_map_new(); GError* error = nullptr; if (!ffchafa_symbol_map_apply_selectors(symbolMap, instance.config.logo.chafaSymbols.chars, &error)) { @@ -616,10 +579,10 @@ static bool printImageChafa(FFLogoRequestData* requestData, const ImageData* ima ffchafa_canvas_draw_all_pixels( canvas, CHAFA_PIXEL_RGBA8_UNASSOCIATED, - blob, - (gint) imageData->image->columns, - (gint) imageData->image->rows, - (gint) imageData->image->columns * 4); + buffer->data, + (gint) buffer->width, + (gint) buffer->height, + (gint) buffer->width * 4); GString* str = ffchafa_canvas_print(canvas, nullptr); FFstrbuf result; @@ -650,101 +613,62 @@ static bool printImageChafa(FFLogoRequestData* requestData, const ImageData* ima } #endif -FFLogoImageResult ffLogoPrintImageImpl(FFLogoRequestData* requestData, const FFIMData* imData) { - FF_LIBRARY_LOAD_SYMBOL(imData->library, MagickCoreGenesis, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL(imData->library, MagickCoreTerminus, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL(imData->library, AcquireExceptionInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL(imData->library, DestroyExceptionInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL(imData->library, AcquireImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL(imData->library, DestroyImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL(imData->library, ReadImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL(imData->library, DestroyImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - - ImageData imageData; - - FF_LIBRARY_LOAD_SYMBOL_VAR(imData->library, imageData, CopyMagickString, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL_VAR(imData->library, imageData, ImageToBlob, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - FF_LIBRARY_LOAD_SYMBOL_VAR(imData->library, imageData, Base64Encode, FF_LOGO_IMAGE_RESULT_INIT_ERROR) - - ffMagickCoreGenesis(nullptr, MagickFalse); - - imageData.exceptionInfo = ffAcquireExceptionInfo(); - if (imageData.exceptionInfo == nullptr) { - ffMagickCoreTerminus(); - return FF_LOGO_IMAGE_RESULT_RUN_ERROR; +bool ffImageCreate(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { + #ifdef _WIN32 + return ffImageCreateWIC(requestData, out, error); + #else + #ifdef FF_HAVE_IMAGEMAGICK7 + if (ffImageCreateIM7(requestData, out, error)) { + return true; } - - ImageInfo* imageInfoIn = ffAcquireImageInfo(); - if (imageInfoIn == nullptr) { - ffDestroyExceptionInfo(imageData.exceptionInfo); - ffMagickCoreTerminus(); - return FF_LOGO_IMAGE_RESULT_RUN_ERROR; + #endif + #ifdef FF_HAVE_IMAGEMAGICK6 + if (ffImageCreateIM6(requestData, out, error)) { + return true; } - - //+1, because we need to copy the null byte too - imageData.ffCopyMagickString(imageInfoIn->filename, instance.config.logo.source.chars, instance.config.logo.source.length + 1); - - imageData.image = ffReadImage(imageInfoIn, imageData.exceptionInfo); - ffDestroyImageInfo(imageInfoIn); - if (imageData.image == nullptr) { - ffDestroyExceptionInfo(imageData.exceptionInfo); - ffMagickCoreTerminus(); - return FF_LOGO_IMAGE_RESULT_RUN_ERROR; - } - - if (requestData->logoPixelWidth == 0 && requestData->logoPixelHeight == 0) { - requestData->logoPixelWidth = (uint32_t) imageData.image->columns; - requestData->logoPixelHeight = (uint32_t) imageData.image->rows; - } else if (requestData->logoPixelWidth == 0) { - requestData->logoPixelWidth = (uint32_t) ((double) imageData.image->columns / (double) imageData.image->rows * requestData->logoPixelHeight); - } else if (requestData->logoPixelHeight == 0) { - requestData->logoPixelHeight = (uint32_t) ((double) imageData.image->rows / (double) imageData.image->columns * requestData->logoPixelWidth); - } - - requestData->logoCharacterWidth = (uint32_t) ceil((double) requestData->logoPixelWidth / requestData->characterPixelWidth); - requestData->logoCharacterHeight = (uint32_t) ceil((double) requestData->logoPixelHeight / requestData->characterPixelHeight); - - if (requestData->logoPixelWidth == 0 || requestData->logoPixelHeight == 0 || requestData->logoCharacterWidth == 0 || requestData->logoCharacterHeight == 0) { - ffDestroyImage(imageData.image); - ffDestroyExceptionInfo(imageData.exceptionInfo); - ffMagickCoreTerminus(); - return FF_LOGO_IMAGE_RESULT_RUN_ERROR; - } - - Image* resized = imData->resizeFunc(imageData.image, requestData->logoPixelWidth, requestData->logoPixelHeight, imageData.exceptionInfo); - ffDestroyImage(imageData.image); - if (resized == nullptr) { - ffDestroyExceptionInfo(imageData.exceptionInfo); - ffMagickCoreTerminus(); - return FF_LOGO_IMAGE_RESULT_RUN_ERROR; - } - imageData.image = resized; - - imageData.imageInfo = ffAcquireImageInfo(); - if (imageData.imageInfo == nullptr) { - ffDestroyImage(imageData.image); - ffDestroyExceptionInfo(imageData.exceptionInfo); - ffMagickCoreTerminus(); - return FF_LOGO_IMAGE_RESULT_RUN_ERROR; - } - - bool printSuccessful = false; - if (requestData->type == FF_LOGO_TYPE_IMAGE_CHAFA) { - #if FF_HAVE_CHAFA - printSuccessful = printImageChafa(requestData, &imageData); + #endif + return false; #endif - } else if (requestData->type == FF_LOGO_TYPE_IMAGE_KITTY) { - printSuccessful = printImageKitty(requestData, &imageData); - } else if (requestData->type == FF_LOGO_TYPE_IMAGE_SIXEL) { - printSuccessful = printImageSixel(requestData, &imageData); +} + +void ffImageDestroy(FFImageBuffer* buffer) { + free(buffer->data); + buffer->data = nullptr; + buffer->width = 0; + buffer->height = 0; +} + +bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const char** error) { + #ifdef _WIN32 + // Windows: WIC decodes and resizes to RGBA, then the embedded libsixel encoder takes over + #ifdef FF_HAVE_SIXEL + FFImageBuffer buffer = {}; + if (!ffImageCreate(requestData, &buffer, error)) { + return false; } - - ffDestroyImageInfo(imageData.imageInfo); - ffDestroyImage(imageData.image); - ffDestroyExceptionInfo(imageData.exceptionInfo); - ffMagickCoreTerminus(); - - return printSuccessful ? FF_LOGO_IMAGE_RESULT_SUCCESS : FF_LOGO_IMAGE_RESULT_RUN_ERROR; + bool ok = ffSixelEncode(&buffer, out, error); + ffImageDestroy(&buffer); + return ok; + #else + if (error) { + *error = "sixel support is not compiled in"; + } + return false; + #endif + #else + // Off Windows: ImageMagick encodes straight from the decoded image, without an RGBA round trip + #ifdef FF_HAVE_IMAGEMAGICK7 + if (ffImageSixelEncodeIM7(requestData, out, error)) { + return true; + } + #endif + #ifdef FF_HAVE_IMAGEMAGICK6 + if (ffImageSixelEncodeIM6(requestData, out, error)) { + return true; + } + #endif + return false; + #endif } static FFNativeFD getCacheFD(FFLogoRequestData* requestData, const char* fileName) { @@ -937,6 +861,17 @@ static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) { ffStrbufRecalculateLength(&requestData.cacheDir); ffStrbufEnsureEndsWithC(&requestData.cacheDir, '/'); + + // The cache is indexed by source path and pixel size only, so it has to be namespaced + // by backend: different backends (and different sixel encoders) produce different bytes + #ifdef _WIN32 + ffStrbufAppendS(&requestData.cacheDir, "wic/"); + #elif defined(FF_HAVE_IMAGEMAGICK7) + ffStrbufAppendS(&requestData.cacheDir, "im7/"); + #elif defined(FF_HAVE_IMAGEMAGICK6) + ffStrbufAppendS(&requestData.cacheDir, "im6/"); + #endif + ffStrbufAppendF(&requestData.cacheDir, "%u*%u/", requestData.logoPixelWidth, requestData.logoPixelHeight); if (!instance.config.logo.recache) { @@ -949,30 +884,40 @@ static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) { } } - FFLogoImageResult result = FF_LOGO_IMAGE_RESULT_INIT_ERROR; + const char* error = nullptr; + bool printSuccessful = false; - #ifdef FF_HAVE_IMAGEMAGICK7 - result = ffLogoPrintImageIM7(&requestData); + if (requestData.type == FF_LOGO_TYPE_IMAGE_SIXEL) { + // The sixel encoder belongs to the backend, so it is not fed through ffImageCreate + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + if (ffImageSixelEncode(&requestData, &result, &error)) { + fillCharacterDimensions(&requestData); + printSuccessful = printImageSixel(&requestData, &result); + } + } else { + FFImageBuffer buffer = {}; + if (ffImageCreate(&requestData, &buffer, &error)) { + fillCharacterDimensions(&requestData); + if (requestData.type == FF_LOGO_TYPE_IMAGE_KITTY) { + printSuccessful = printImageKitty(&requestData, &buffer); + } + #if FF_HAVE_CHAFA + else if (requestData.type == FF_LOGO_TYPE_IMAGE_CHAFA) { + printSuccessful = printImageChafa(&requestData, &buffer); + } #endif - - #ifdef FF_HAVE_IMAGEMAGICK6 - if (result == FF_LOGO_IMAGE_RESULT_INIT_ERROR) { - result = ffLogoPrintImageIM6(&requestData); + ffImageDestroy(&buffer); + } } - #endif ffStrbufDestroy(&requestData.cacheDir); - if (result == FF_LOGO_IMAGE_RESULT_SUCCESS) { + if (printSuccessful) { return true; } if (printError) { - if (result == FF_LOGO_IMAGE_RESULT_INIT_ERROR) { - fputs("Logo: Image Magick library not found\n", stderr); - } else { - fputs("Logo: Failed to load / convert the image source\n", stderr); - } + fprintf(stderr, "Logo: %s\n", error ? error : "Failed to load / convert the image source"); } return false; @@ -1024,7 +969,7 @@ bool ffLogoPrintImageIfExists(FFLogoType type, bool printError) { } #endif -#if !defined(FF_HAVE_IMAGEMAGICK7) && !defined(FF_HAVE_IMAGEMAGICK6) +#if !defined(_WIN32) && !defined(FF_HAVE_IMAGEMAGICK7) && !defined(FF_HAVE_IMAGEMAGICK6) if (printError) { fputs("Logo: Image Magick support is not compiled in\n", stderr); } diff --git a/src/logo/image/image.h b/src/logo/image/image.h index 5fbd39a00..ff4d4dbcf 100644 --- a/src/logo/image/image.h +++ b/src/logo/image/image.h @@ -2,7 +2,7 @@ #include "../logo.h" -#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) +#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(FF_HAVE_SIXEL) typedef enum FFLogoImageResult: uint8_t { FF_LOGO_IMAGE_RESULT_SUCCESS, // Logo printed @@ -24,20 +24,42 @@ typedef struct FFLogoRequestData { uint32_t logoCharacterWidth; } FFLogoRequestData; -typedef struct FFIMData { - void* library; - void* (*resizeFunc)(const void* image, size_t width, size_t height, void* exceptionInfo); -} FFIMData; +// Decoded and resized image: RGBA8, straight (unassociated) alpha, no row padding +typedef struct FFImageBuffer { + uint8_t* data; + uint32_t width; + uint32_t height; +} FFImageBuffer; -FFLogoImageResult ffLogoPrintImageImpl(FFLogoRequestData* requestData, const FFIMData* imData); +// Backend contract. Both functions update requestData->logoPixelWidth / logoPixelHeight +// with the real dimensions of the produced image, so the caller can derive the character +// dimensions afterwards. + +// Decode the image source and resize it to the requested pixel size +bool ffImageCreate(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error); +void ffImageDestroy(FFImageBuffer* buffer); + +// Encode the image source as a sixel byte stream (including the DCS envelope). +// The encoder belongs to the backend: ImageMagick's SIXEL coder off Windows, +// the embedded libsixel on Windows. +bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const char** error); #endif #ifdef FF_HAVE_IMAGEMAGICK7 -FFLogoImageResult ffLogoPrintImageIM7(FFLogoRequestData* requestData); +bool ffImageCreateIM7(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error); +bool ffImageSixelEncodeIM7(FFLogoRequestData* requestData, FFstrbuf* out, const char** error); #endif #ifdef FF_HAVE_IMAGEMAGICK6 - #include -FFLogoImageResult ffLogoPrintImageIM6(FFLogoRequestData* requestData); +bool ffImageCreateIM6(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error); +bool ffImageSixelEncodeIM6(FFLogoRequestData* requestData, FFstrbuf* out, const char** error); +#endif + +#ifdef _WIN32 +bool ffImageCreateWIC(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error); +#endif + +#ifdef FF_HAVE_SIXEL +bool ffSixelEncode(const FFImageBuffer* buffer, FFstrbuf* result, const char** error); #endif diff --git a/src/logo/image/sixel.c b/src/logo/image/sixel.c new file mode 100644 index 000000000..3b0c0ae83 --- /dev/null +++ b/src/logo/image/sixel.c @@ -0,0 +1,54 @@ +#include "image.h" + +#ifdef FF_HAVE_SIXEL + + #include // src/3rdparty/sixel/sixel.h + +static int sixelWriteCallback(char* data, int size, void* priv) { + ffStrbufAppendNS((FFstrbuf*) priv, (uint32_t) size, data); + return 1; // non-zero means "keep going" +} + +bool ffSixelEncode(const FFImageBuffer* buffer, FFstrbuf* result, const char** error) { + sixel_dither_t* dither = nullptr; + if (sixel_dither_new(&dither, 256, nullptr) != SIXEL_OK) { + if (error) { + *error = "sixel_dither_new() failed"; + } + return false; + } + + // Feed RGBA8 directly and let libsixel do the palette quantization and dithering + if (sixel_dither_initialize(dither, buffer->data, (int) buffer->width, (int) buffer->height, SIXEL_PIXELFORMAT_RGBA8888, SIXEL_LARGE_AUTO, SIXEL_REP_AUTO, SIXEL_QUALITY_HIGH) != SIXEL_OK) { + sixel_dither_unref(dither); + if (error) { + *error = "sixel_dither_initialize() failed"; + } + return false; + } + + sixel_output_t* output = nullptr; + if (sixel_output_new(&output, sixelWriteCallback, result, nullptr) != SIXEL_OK) { + sixel_dither_unref(dither); + if (error) { + *error = "sixel_output_new() failed"; + } + return false; + } + + // The depth parameter is unused by libsixel; the DCS envelope is emitted by default + SIXELSTATUS status = sixel_encode(buffer->data, (int) buffer->width, (int) buffer->height, 4, dither, output); + + sixel_output_unref(output); + sixel_dither_unref(dither); + + if (status != SIXEL_OK || result->length == 0) { + if (error) { + *error = "sixel_encode() failed"; + } + return false; + } + return true; +} + +#endif diff --git a/src/logo/image/wic.cpp b/src/logo/image/wic.cpp new file mode 100644 index 000000000..cac0b2b1d --- /dev/null +++ b/src/logo/image/wic.cpp @@ -0,0 +1,127 @@ +extern "C" { +#include "image.h" +#include "common/mallocHelper.h" +#include "common/windows/com.h" +#include "common/windows/nt.h" +} + +#include +#include +#include + +bool ffImageCreateWIC(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { + const char* comError = ffInitCom(); + if (comError) { + if (error) *error = comError; + return false; + } + + FF_AUTO_RELEASE_COM_OBJECT IWICImagingFactory* factory = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICBitmapDecoder* decoder = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICBitmapFrameDecode* frame = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICBitmapScaler* scaler = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICFormatConverter* premultiplyConverter = nullptr; + FF_AUTO_RELEASE_COM_OBJECT IWICFormatConverter* converter = nullptr; + + if (FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, + IID_IWICImagingFactory, (void**) &factory))) { + if (error) *error = "WIC imaging factory is unavailable"; + return false; + } + + // The source path is UTF-8, WIC only accepts UTF-16 + wchar_t widePath[MAX_PATH + 1]; + if (!NT_SUCCESS(RtlUTF8ToUnicodeN(widePath, (ULONG) sizeof(widePath), nullptr, + instance.config.logo.source.chars, (ULONG) instance.config.logo.source.length + 1))) { + if (error) *error = "failed to convert the image path to UTF-16"; + return false; + } + + if (FAILED(factory->CreateDecoderFromFilename(widePath, nullptr, GENERIC_READ, + WICDecodeMetadataCacheOnDemand, &decoder))) { + if (error) *error = "unsupported or unreadable image format"; + return false; + } + + // Only the first frame, matching ImageMagick's ReadImage (neither handles GIF animation) + if (FAILED(decoder->GetFrame(0, &frame))) { + if (error) *error = "failed to get the first frame"; + return false; + } + + UINT sourceWidth = 0, sourceHeight = 0; + frame->GetSize(&sourceWidth, &sourceHeight); + if (sourceWidth == 0 || sourceHeight == 0) { + if (error) *error = "invalid image dimensions"; + return false; + } + + // Fill in the missing dimension, keeping the source aspect ratio (same as the IM path) + uint32_t width = requestData->logoPixelWidth; + uint32_t height = requestData->logoPixelHeight; + if (width == 0 && height == 0) { + width = sourceWidth; + height = sourceHeight; + } else if (width == 0) { + width = (uint32_t) ((double) sourceWidth / (double) sourceHeight * height); + } else if (height == 0) { + height = (uint32_t) ((double) sourceHeight / (double) sourceWidth * width); + } + + if (width == 0 || height == 0) { + if (error) *error = "invalid target dimensions"; + return false; + } + + requestData->logoPixelWidth = width; + requestData->logoPixelHeight = height; + + IWICBitmapSource* source = nullptr; + if (width == sourceWidth && height == sourceHeight) { + // Same size: don't resample. ImageMagick clones the image in this case too, + // and resampling would only blur it + source = frame; + } else { + // Scale premultiplied alpha to avoid transparent RGB values bleeding into + // the visible edge pixels. The final output is converted back to straight + // alpha below, matching FFImageBuffer's RGBA8 contract. + if (FAILED(factory->CreateFormatConverter(&premultiplyConverter)) || + FAILED(premultiplyConverter->Initialize(frame, GUID_WICPixelFormat32bppPBGRA, + WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom)) || + FAILED(factory->CreateBitmapScaler(&scaler)) || + FAILED(scaler->Initialize(premultiplyConverter, width, height, + (WICBitmapInterpolationMode) 0x4 /* WICBitmapInterpolationModeHighQualityCubic */))) { + if (error) *error = "image scaling failed"; + return false; + } + source = scaler; + } + + // Normalize to straight-alpha RGBA8: kitty (f=32) and chafa + // (CHAFA_PIXEL_RGBA8_UNASSOCIATED) both consume exactly this + if (FAILED(factory->CreateFormatConverter(&converter)) || + FAILED(converter->Initialize(source, GUID_WICPixelFormat32bppRGBA, + WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom))) { + if (error) *error = "pixel format conversion failed"; + return false; + } + + UINT stride = width * 4; + FF_AUTO_FREE uint8_t* pixels = (uint8_t*) malloc((size_t) stride * height); + if (pixels == nullptr) { + if (error) *error = "out of memory"; + return false; + } + + // prc == nullptr means the whole image; WIC fills the buffer using the stride we pass in + if (FAILED(converter->CopyPixels(nullptr, stride, stride * height, pixels))) { + if (error) *error = "pixel copy failed"; + return false; + } + + out->data = pixels; + out->width = width; + out->height = height; + pixels = nullptr; // Ownership is transferred to `out` + return true; +}