From 35ffdb3995a860a40baef87fb9f2c6b71afdf1ae Mon Sep 17 00:00:00 2001 From: Carter Li Date: Mon, 14 Sep 2026 10:59:03 +0800 Subject: [PATCH] Logo (Image): drops IM in favor of ImageIO on macOS --- CMakeLists.txt | 11 ++- src/logo/image/image.c | 16 ++-- src/logo/image/image.h | 6 +- src/logo/image/imageio.c | 185 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 src/logo/image/imageio.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 98c005822..25e2e4dc0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,10 +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 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_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_IMAGEMAGICK7 "Enable imagemagick 7" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD 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 SunOS OR GNU" OFF) +cmake_dependent_option(ENABLE_SIXEL "Enable sixel logo output via the embedded libsixel encoder (Windows and macOS; replaces ImageMagick's SIXEL coder)" ON "WIN32 OR APPLE" OFF) +cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7 OR WIN32 OR APPLE" 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) @@ -1023,6 +1023,7 @@ elseif(APPLE) src/detection/de/de_nosupport.c src/detection/wmtheme/wmtheme_apple.c src/detection/camera/camera_apple.m + src/logo/image/imageio.c ) # CMAKE_SYSTEM_PROCESSOR has been normalized before if(CMAKE_SYSTEM_PROCESSOR STREQUAL "amd64") @@ -1887,6 +1888,8 @@ elseif(APPLE) PRIVATE "-framework CoreAudio" PRIVATE "-framework CoreMedia" PRIVATE "-framework CoreVideo" + PRIVATE "-framework ImageIO" + PRIVATE "-framework Accelerate" PRIVATE "-framework CoreWLAN" PRIVATE "-framework IOBluetooth" PRIVATE "-framework IOKit" diff --git a/src/logo/image/image.c b/src/logo/image/image.c index 69973f1c8..21a46ae89 100644 --- a/src/logo/image/image.c +++ b/src/logo/image/image.c @@ -341,7 +341,7 @@ static bool printImageKittyDirect(bool printError) { return true; } -#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(FF_HAVE_SIXEL) +#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(__APPLE__) || defined(FF_HAVE_SIXEL) #define FF_KITTY_MAX_CHUNK_SIZE 4096 @@ -616,6 +616,8 @@ static bool printImageChafa(FFLogoRequestData* requestData, const FFImageBuffer* bool ffImageCreate(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { #ifdef _WIN32 return ffImageCreateWIC(requestData, out, error); + #elif defined(__APPLE__) + return ffImageCreateImageIO(requestData, out, error); #else #ifdef FF_HAVE_IMAGEMAGICK7 if (ffImageCreateIM7(requestData, out, error)) { @@ -639,8 +641,10 @@ void ffImageDestroy(FFImageBuffer* buffer) { } 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 + // Windows (WIC) and macOS (ImageIO) decode and resize to RGBA first, then the embedded + // libsixel encoder takes over. Other platforms let ImageMagick encode straight from the + // decoded image without an RGBA round trip. + #if defined(_WIN32) || defined(__APPLE__) #ifdef FF_HAVE_SIXEL FFImageBuffer buffer = {}; if (!ffImageCreate(requestData, &buffer, error)) { @@ -650,13 +654,13 @@ bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const cha ffImageDestroy(&buffer); return ok; #else + FF_UNUSED(requestData, out); 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; @@ -866,6 +870,8 @@ static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) { // by backend: different backends (and different sixel encoders) produce different bytes #ifdef _WIN32 ffStrbufAppendS(&requestData.cacheDir, "wic/"); + #elif defined(__APPLE__) + ffStrbufAppendS(&requestData.cacheDir, "imageio/"); #elif defined(FF_HAVE_IMAGEMAGICK7) ffStrbufAppendS(&requestData.cacheDir, "im7/"); #elif defined(FF_HAVE_IMAGEMAGICK6) @@ -969,7 +975,7 @@ bool ffLogoPrintImageIfExists(FFLogoType type, bool printError) { } #endif -#if !defined(_WIN32) && !defined(FF_HAVE_IMAGEMAGICK7) && !defined(FF_HAVE_IMAGEMAGICK6) +#if !defined(_WIN32) && !defined(__APPLE__) && !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 ff4d4dbcf..f096dd7a8 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) || defined(_WIN32) || defined(FF_HAVE_SIXEL) +#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(__APPLE__) || defined(FF_HAVE_SIXEL) typedef enum FFLogoImageResult: uint8_t { FF_LOGO_IMAGE_RESULT_SUCCESS, // Logo printed @@ -60,6 +60,10 @@ bool ffImageSixelEncodeIM6(FFLogoRequestData* requestData, FFstrbuf* out, const bool ffImageCreateWIC(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error); #endif +#ifdef __APPLE__ +bool ffImageCreateImageIO(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/imageio.c b/src/logo/image/imageio.c new file mode 100644 index 000000000..a86e0ccc8 --- /dev/null +++ b/src/logo/image/imageio.c @@ -0,0 +1,185 @@ +#include "image.h" +#include "common/mallocHelper.h" +#include "common/apple/cf_helpers.h" + +#include +#include +#include + +bool ffImageCreateImageIO(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) { + FF_CFTYPE_AUTO_RELEASE CFURLRef url = CFURLCreateFromFileSystemRepresentation( + kCFAllocatorDefault, + (const UInt8*) instance.config.logo.source.chars, + (CFIndex) instance.config.logo.source.length, + false); + if (url == nullptr) { + if (error) { + *error = "failed to create the image URL"; + } + return false; + } + + FF_CFTYPE_AUTO_RELEASE CGImageSourceRef source = CGImageSourceCreateWithURL(url, nullptr); + if (source == nullptr) { + if (error) { + *error = "unsupported or unreadable image format"; + } + return false; + } + + // Only the first frame, matching ImageMagick's ReadImage (neither handles GIF animation) + FF_CFTYPE_AUTO_RELEASE CGImageRef image = CGImageSourceCreateImageAtIndex(source, 0, nullptr); + if (image == nullptr) { + if (error) { + *error = "failed to get the first frame"; + } + return false; + } + + size_t sourceWidth = CGImageGetWidth(image); + size_t sourceHeight = CGImageGetHeight(image); + 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 = (uint32_t) sourceWidth; + height = (uint32_t) 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; + + const bool sameSize = width == sourceWidth && height == sourceHeight; + + // Decode straight into RGBA for the same-size fast path. For resizing, decode + // into premultiplied ARGB so interpolation does not bleed transparent RGB into + // visible edge pixels. + // vImageBuffer_InitWithCGImage handles format conversion, color management and + // byte order in one call. NULL colorSpace means sRGB, matching kCGColorSpaceSRGB. + const vImage_CGImageFormat format = { + .bitsPerComponent = 8, + .bitsPerPixel = 32, + .colorSpace = nullptr, + .bitmapInfo = (CGBitmapInfo) ((sameSize ? kCGImageAlphaLast : kCGImageAlphaPremultipliedFirst) | kCGImageByteOrder32Big), + .version = 0, + .decode = nullptr, + .renderingIntent = kCGRenderingIntentDefault + }; + + vImage_Buffer src = {}; + vImage_Error vErr = vImageBuffer_InitWithCGImage(&src, (vImage_CGImageFormat*) &format, nullptr, image, kvImageNoFlags); + if (vErr != kvImageNoError) { + if (error) { + *error = "failed to decode the image with vImage"; + } + return false; + } + + // Final output buffer: straight (unassociated) RGBA8888 at the target size. + const size_t dstStride = (size_t) width * 4; + if (sameSize) { + // The requested format is already straight RGBA. If vImage did not add + // row padding, transfer its allocation directly to the caller; otherwise + // copy rows into the required tightly packed FFImageBuffer allocation. + if (src.rowBytes == dstStride) { + out->data = (uint8_t*) src.data; + out->width = width; + out->height = height; + src.data = nullptr; + return true; + } + + FF_AUTO_FREE uint8_t* pixels = (uint8_t*) malloc(dstStride * (size_t) height); + if (pixels == nullptr) { + free(src.data); + if (error) { + *error = "out of memory"; + } + return false; + } + for (uint32_t y = 0; y < height; ++y) { + memcpy(pixels + (size_t) y * dstStride, + (const uint8_t*) src.data + (size_t) y * src.rowBytes, + dstStride); + } + free(src.data); + out->data = pixels; + out->width = width; + out->height = height; + pixels = nullptr; + return true; + } else { + FF_AUTO_FREE uint8_t* pixels = (uint8_t*) malloc(dstStride * (size_t) height); + if (pixels == nullptr) { + free(src.data); + if (error) { + *error = "out of memory"; + } + return false; + } + + const vImage_Buffer dst = { + .data = pixels, + .width = width, + .height = height, + .rowBytes = dstStride + }; + + // Resample while still premultiplied: interpolating premultiplied data is the + // correct way to scale (it avoids the dark fringes you get from averaging + // straight-alpha RGB). NULL temp buffer lets vImage allocate internally. + vErr = vImageScale_ARGB8888(&src, &dst, nullptr, kvImageHighQualityResampling); + free(src.data); + if (vErr != kvImageNoError) { + if (error) { + *error = "failed to scale the image"; + } + return false; + } + + // Un-premultiply in place (pointwise, alpha == 0 is handled safely) so the result + // matches the kitty f=32 / chafa CHAFA_PIXEL_RGBA8_UNASSOCIATED contract. + vErr = vImageUnpremultiplyData_ARGB8888(&dst, &dst, kvImageNoFlags); + if (vErr != kvImageNoError) { + if (error) { + *error = "failed to un-premultiply the image"; + } + return false; + } + + // Reorder ARGB -> RGBA in place (permute supports in-place when data/rowBytes match). + // The downstream consumers and the WIC backend all expect R,G,B,A byte order. + const uint8_t permuteMap[4] = { 1, 2, 3, 0 }; // A,R,G,B -> R,G,B,A + vErr = vImagePermuteChannels_ARGB8888(&dst, &dst, permuteMap, kvImageNoFlags); + if (vErr != kvImageNoError) { + if (error) { + *error = "failed to reorder image channels"; + } + return false; + } + + out->data = pixels; + out->width = width; + out->height = height; + pixels = nullptr; // Ownership is transferred to `out` + return true; + } +}