diff options
| author | sumuel <samuel@yakubos.org> | 2026-08-17 20:44:55 +0000 |
|---|---|---|
| committer | sumuel <samuel@yakubos.org> | 2026-08-17 20:44:55 +0000 |
| commit | 76424950e373d3b04ac3dd13019151bfba3e8423 (patch) | |
| tree | 4c3cfdbda039e592b9186be3e28f8d7cfd439e8a /src/logo/image | |
Add the files
Diffstat (limited to 'src/logo/image')
| -rw-r--r-- | src/logo/image/im6.c | 27 | ||||
| -rw-r--r-- | src/logo/image/im7.c | 28 | ||||
| -rw-r--r-- | src/logo/image/image.c | 1032 | ||||
| -rw-r--r-- | src/logo/image/image.h | 43 |
4 files changed, 1130 insertions, 0 deletions
diff --git a/src/logo/image/im6.c b/src/logo/image/im6.c new file mode 100644 index 0000000..3e9d722 --- /dev/null +++ b/src/logo/image/im6.c @@ -0,0 +1,27 @@ +#ifdef FF_HAVE_IMAGEMAGICK6 + + #include "image.h" + #include "common/library.h" + + #include <magick/MagickCore.h> + +static FF_LIBRARY_SYMBOL(ResizeImage) + + static void* logoResize(const void* image, size_t width, size_t height, void* exceptionInfo) { + return ffResizeImage(image, width, height, UndefinedFilter, 1.0, exceptionInfo); +} + +FFLogoImageResult ffLogoPrintImageIM6(FFLogoRequestData* requestData) { + FF_LIBRARY_LOAD(imageMagick, FF_LOGO_IMAGE_RESULT_INIT_ERROR, "libMagickCore-6.Q16HDRI" FF_LIBRARY_EXTENSION, 8, "libMagickCore-6.Q16" FF_LIBRARY_EXTENSION, 8) + FF_LIBRARY_LOAD_SYMBOL_ADDRESS(imageMagick, ffResizeImage, ResizeImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + + FFLogoImageResult result = ffLogoPrintImageImpl(requestData, &(FFIMData) { + .resizeFunc = logoResize, + .library = imageMagick, + }); + + imageMagick = NULL; // leak imageMagick to prevent fastfetch from crashing #552 + return result; +} + +#endif diff --git a/src/logo/image/im7.c b/src/logo/image/im7.c new file mode 100644 index 0000000..636a5fa --- /dev/null +++ b/src/logo/image/im7.c @@ -0,0 +1,28 @@ +#ifdef FF_HAVE_IMAGEMAGICK7 + + #include "image.h" + #include "common/library.h" + + #include <MagickCore/MagickCore.h> + +static FF_LIBRARY_SYMBOL(ResizeImage) + + static void* logoResize(const void* image, size_t width, size_t height, void* exceptionInfo) { + return ffResizeImage(image, width, height, UndefinedFilter, exceptionInfo); +} + +FFLogoImageResult ffLogoPrintImageIM7(FFLogoRequestData* requestData) { + FF_LIBRARY_LOAD(imageMagick, FF_LOGO_IMAGE_RESULT_INIT_ERROR, "libMagickCore-7.Q16HDRI" FF_LIBRARY_EXTENSION, 11, "libMagickCore-7.Q16" FF_LIBRARY_EXTENSION, 11, "libMagickCore-7.Q16HDRI-10" FF_LIBRARY_EXTENSION, -1 // Required for Windows + ) + FF_LIBRARY_LOAD_SYMBOL_ADDRESS(imageMagick, ffResizeImage, ResizeImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR) + + FFLogoImageResult result = ffLogoPrintImageImpl(requestData, &(FFIMData) { + .resizeFunc = logoResize, + .library = imageMagick, + }); + + imageMagick = NULL; // leak imageMagick to prevent fastfetch from crashing #552 + return result; +} + +#endif diff --git a/src/logo/image/image.c b/src/logo/image/image.c new file mode 100644 index 0000000..6dd3212 --- /dev/null +++ b/src/logo/image/image.c @@ -0,0 +1,1032 @@ +#include "image.h" +#include "common/io.h" +#include "common/printing.h" +#include "common/processing.h" +#include "common/strutil.h" +#include "common/base64.h" +#include "detection/terminalsize/terminalsize.h" + +#include <limits.h> +#include <math.h> + +#ifdef __APPLE__ + #include <sys/syslimits.h> +#elif _WIN32 + #include <windows.h> +#elif __linux__ + #include <sys/sendfile.h> +#elif __sun + #include <sys/termios.h> +#endif + +static bool printImageIterm(bool printError) { + const FFOptionsLogo* options = &instance.config.logo; + FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate(); + if (!ffAppendFileBuffer(options->source.chars, &buf)) { + if (printError) { + fputs("Logo (iterm): Failed to load image file\n", stderr); + } + return false; + } + + fflush(stdout); + + bool inTmux = false; + { + const char* term = getenv("TERM"); + inTmux = term && (ffStrStartsWith(term, "screen") || ffStrStartsWith(term, "tmux")); + } + + FF_STRBUF_AUTO_DESTROY base64 = ffBase64EncodeStrbuf(&buf); + ffStrbufClear(&buf); + + if (!options->width || !options->height) { + if (options->position == FF_LOGO_POSITION_LEFT) { + ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;%uH", (unsigned) options->paddingTop + 1, (unsigned) options->paddingLeft + 1); + } else if (options->position == FF_LOGO_POSITION_TOP) { + ffStrbufAppendNC(&buf, options->paddingTop, '\n'); + ffStrbufAppendNC(&buf, options->paddingLeft, ' '); + } else if (options->position == FF_LOGO_POSITION_RIGHT) { + if (!options->width) { + if (printError) { + fputs("Logo (iterm): Must set logo width when using position right\n", stderr); + } + return false; + } + ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;9999999H\e[%uD", (unsigned) options->paddingTop + 1, (unsigned) options->paddingRight + options->width); + } + if (inTmux) { + ffStrbufAppendS(&buf, "\ePtmux;\e"); + } + + if (options->width) { + ffStrbufAppendF(&buf, "\e]1337;File=inline=1;width=%u:%s\a", (unsigned) options->width, base64.chars); + } else { + ffStrbufAppendF(&buf, "\e]1337;File=inline=1:%s\a", base64.chars); + } + if (inTmux) { + ffStrbufAppendS(&buf, "\e\\"); + } + ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf); + + if (options->position == FF_LOGO_POSITION_LEFT || options->position == FF_LOGO_POSITION_RIGHT) { + uint16_t X = 0, Y = 0; + const char* error = ffGetTerminalResponse("\e[6n", 2, "%*[^0-9]%hu;%huR", &Y, &X); + if (error) { + fprintf(stderr, "\nLogo (iterm): fail to query cursor position: %s\n", error); + return true; // We already printed image logo, don't print ascii logo then + } + if (X < options->paddingLeft + options->width) { + X = (uint16_t) (options->paddingLeft + options->width); + } + if (options->position == FF_LOGO_POSITION_LEFT) { + instance.state.logoWidth = X + options->paddingRight - 1; + } + instance.state.logoHeight = Y; + fputs("\e[H", stdout); + } else if (options->position == FF_LOGO_POSITION_TOP) { + instance.state.logoWidth = instance.state.logoHeight = 0; + ffPrintCharTimes('\n', options->paddingRight); + } + } else { + ffStrbufAppendNC(&buf, options->paddingTop, '\n'); + if (options->position == FF_LOGO_POSITION_RIGHT) { + ffStrbufAppendF(&buf, "\e[9999999C\e[%uD", (unsigned) options->paddingRight + options->width); + } else if (options->paddingLeft) { + ffStrbufAppendF(&buf, "\e[%uC", (unsigned) options->paddingLeft); + } + if (inTmux) { + ffStrbufAppendS(&buf, "\ePtmux;\e"); + } + ffStrbufAppendF(&buf, "\e]1337;File=inline=1;width=%u;height=%u;preserveAspectRatio=%u:%s\a", (unsigned) options->width, (unsigned) options->height, (unsigned) options->preserveAspectRatio, base64.chars); + if (inTmux) { + ffStrbufAppendS(&buf, "\e\\"); + } + ffStrbufAppendC(&buf, '\n'); + + if (options->position == FF_LOGO_POSITION_LEFT) { + instance.state.logoWidth = options->width + options->paddingLeft + options->paddingRight; + instance.state.logoHeight = options->paddingTop + options->height; + ffStrbufAppendF(&buf, "\e[%uA", (unsigned) instance.state.logoHeight); + } else if (options->position == FF_LOGO_POSITION_TOP) { + instance.state.logoWidth = instance.state.logoHeight = 0; + ffStrbufAppendNC(&buf, options->paddingRight, '\n'); + } else if (options->position == FF_LOGO_POSITION_RIGHT) { + instance.state.logoWidth = instance.state.logoHeight = 0; + ffStrbufAppendF(&buf, "\e[1G\e[%uA", (unsigned) options->height); + } + ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf); + } + + return true; +} + +static bool printImageKittyIcat(bool printError) { + const FFOptionsLogo* options = &instance.config.logo; + + if (!ffPathExists(options->source.chars, FF_PATHTYPE_FILE)) { + if (printError) { + fputs("Logo (kitty-icat): Failed to load image file\n", stderr); + } + return false; + } + + fflush(stdout); + + FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate(); + + if (options->position == FF_LOGO_POSITION_LEFT) { + ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;%uH", (unsigned) options->paddingTop + 1, (unsigned) options->paddingLeft + 1); + } else if (options->position == FF_LOGO_POSITION_TOP) { + if (!options->width) { + ffStrbufAppendNC(&buf, options->paddingTop, '\n'); + ffStrbufAppendNC(&buf, options->paddingLeft, ' '); + } else { + if (printError) { + fputs("Logo (kitty-icat): position top is not supported when logo width is set\n", stderr); + } + return false; + } + } else if (options->position == FF_LOGO_POSITION_RIGHT) { + if (printError) { + fputs("Logo (kitty-icat): position right is not supported\n", stderr); + } + return false; + } + + uint32_t prevLength = buf.length; + + const char* error = NULL; + + if (options->width) { + char place[64]; + snprintf(place, + ARRAY_SIZE(place), + "--place=%ux%u@%ux%u", + options->width, + options->height == 0 ? 9999 : options->height, + options->paddingLeft + 1, + options->paddingTop + 1); + + error = ffProcessAppendStdOut(&buf, (char*[]) { + "kitten", + "icat", + "-n", + "--align=center", + place, + "--scale-up", + options->source.chars, + NULL, + }); + } else { + error = ffProcessAppendStdOut(&buf, (char*[]) { + "kitten", + "icat", + "-n", + "--align=left", + options->source.chars, + NULL, + }); + } + if (error) { + if (printError) { + fprintf(stderr, "Logo (kitty-icat): running `kitten icat` failed %s\n", error); + } + return false; + } + + if (buf.length == prevLength) { + if (printError) { + fputs("Logo (kitty-icat): `kitten icat` returned empty output\n", stderr); + } + return false; + } + + ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf); + + if (options->position == FF_LOGO_POSITION_LEFT || options->position == FF_LOGO_POSITION_RIGHT) { + uint16_t X = 0, Y = 0; + const char* error = ffGetTerminalResponse("\e[6n", 2, "%*[^0-9]%hu;%huR", &Y, &X); + if (error) { + fprintf(stderr, "\nLogo (kitty-icat): fail to query cursor position: %s\n", error); + return true; // We already printed image logo, don't print ascii logo then + } + if (X < options->paddingLeft + options->width) { + X = (uint16_t) (options->paddingLeft + options->width); + } + if (options->position == FF_LOGO_POSITION_LEFT) { + instance.state.logoWidth = X + options->paddingRight - 1; + } + instance.state.logoHeight = Y; + fputs("\e[H", stdout); + } else if (options->position == FF_LOGO_POSITION_TOP) { + instance.state.logoWidth = instance.state.logoHeight = 0; + ffPrintCharTimes('\n', options->paddingRight); + } + + return true; +} + +static bool printImageKittyDirect(bool printError) { + const FFOptionsLogo* options = &instance.config.logo; + + if (!ffPathExists(options->source.chars, FF_PATHTYPE_FILE)) { + if (printError) { + fputs("Logo (kitty-direct): Failed to load image file\n", stderr); + } + return false; + } + + fflush(stdout); + + bool inTmux = false; + { + const char* term = getenv("TERM"); + inTmux = term && (ffStrStartsWith(term, "screen") || ffStrStartsWith(term, "tmux")); + } + + FF_STRBUF_AUTO_DESTROY base64 = ffBase64EncodeStrbuf(&options->source); + FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate(); + + if (!options->width || !options->height) { + if (options->position == FF_LOGO_POSITION_LEFT) { + // We must clear the entre screen to make sure that terminal buffer won't scroll up + ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;%uH", (unsigned) options->paddingTop + 1, (unsigned) options->paddingLeft + 1); + } else if (options->position == FF_LOGO_POSITION_TOP) { + ffStrbufAppendNC(&buf, options->paddingTop, '\n'); + ffStrbufAppendNC(&buf, options->paddingLeft, ' '); + } else if (options->position == FF_LOGO_POSITION_RIGHT) { + if (!options->width) { + if (printError) { + fputs("Logo (iterm): Must set logo width when using position right\n", stderr); + } + return false; + } + ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;9999999H\e[%uD", (unsigned) options->paddingTop + 1, (unsigned) options->paddingRight + options->width); + } + + if (inTmux) { + ffStrbufAppendS(&buf, "\ePtmux;\e"); + } + if (options->width) { + ffStrbufAppendF(&buf, "\e_Ga=T,f=100,t=f,c=%u;%s", (unsigned) options->width, base64.chars); + } else { + ffStrbufAppendF(&buf, "\e_Ga=T,f=100,t=f;%s", base64.chars); + } + if (inTmux) { + ffStrbufAppendC(&buf, '\e'); + } + ffStrbufAppendS(&buf, "\e\\"); + if (inTmux) { + ffStrbufAppendS(&buf, "\e\\"); + } + + ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf); + + if (options->position == FF_LOGO_POSITION_LEFT || options->position == FF_LOGO_POSITION_RIGHT) { + uint16_t X = 0, Y = 0; + const char* error = ffGetTerminalResponse("\e[6n", 2, "%*[^0-9]%hu;%huR", &Y, &X); + if (error) { + if (printError) { + fprintf(stderr, "\nLogo (kitty-direct): fail to query cursor position: %s\n", error); + } + return true; // We already printed image logo, don't print ascii logo then + } + if (X < options->paddingLeft + options->width) { + X = (uint16_t) (options->paddingLeft + options->width); + } + if (options->position == FF_LOGO_POSITION_LEFT) { + instance.state.logoWidth = X + options->paddingRight - 1; + } + instance.state.logoHeight = Y; + fputs("\e[H", stdout); + } else if (options->position == FF_LOGO_POSITION_TOP) { + instance.state.logoWidth = instance.state.logoHeight = 0; + ffPrintCharTimes('\n', options->paddingRight); + } + } else { + ffStrbufAppendNC(&buf, options->paddingTop, '\n'); + + if (options->position == FF_LOGO_POSITION_RIGHT) { + ffStrbufAppendF(&buf, "\e[9999999C\e[%uD", (unsigned) options->paddingRight + options->width); + } else if (options->paddingLeft) { + ffStrbufAppendF(&buf, "\e[%uC", (unsigned) options->paddingLeft); + } + + if (inTmux) { + ffStrbufAppendS(&buf, "\ePtmux;\e"); + } + + ffStrbufAppendF(&buf, "\e_Ga=T,f=100,t=f,c=%u,r=%u;%s\e\\", (unsigned) options->width, (unsigned) options->height, base64.chars); + if (inTmux) { + ffStrbufAppendS(&buf, "\e\\"); + } + ffStrbufAppendC(&buf, '\n'); + if (options->position == FF_LOGO_POSITION_LEFT) { + instance.state.logoWidth = options->width + options->paddingLeft + options->paddingRight; + instance.state.logoHeight = options->paddingTop + options->height; + ffStrbufAppendF(&buf, "\e[%uA", (unsigned) instance.state.logoHeight); + } else if (options->position == FF_LOGO_POSITION_TOP) { + instance.state.logoWidth = instance.state.logoHeight = 0; + ffStrbufAppendNC(&buf, options->paddingRight, '\n'); + } else if (options->position == FF_LOGO_POSITION_RIGHT) { + instance.state.logoWidth = instance.state.logoHeight = 0; + ffStrbufAppendF(&buf, "\e[1G\e[%uA", (unsigned) options->height); + } + + ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf); + } + + return true; +} + +#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) + + #define FF_KITTY_MAX_CHUNK_SIZE 4096 + + #define FF_CACHE_FILE_HEIGHT "height" + #define FF_CACHE_FILE_WIDTH "width" + #define FF_CACHE_FILE_SIXEL "sixel" + #define FF_CACHE_FILE_KITTY_COMPRESSED "kittyc" + #define FF_CACHE_FILE_KITTY_UNCOMPRESSED "kittyu" + #define FF_CACHE_FILE_CHAFA "chafa" + + #include <string.h> + #include <unistd.h> + #include <fcntl.h> + + #ifndef _WIN32 + #include <sys/ioctl.h> + #else + #include <wincon.h> + #include "common/path.h" + #endif + + #ifdef FF_HAVE_ZLIB + #include "common/library.h" + #include <stdlib.h> + #include <zlib.h> + +static bool compressBlob(void** blob, size_t* length) { + FF_LIBRARY_LOAD(zlib, false, "libz" FF_LIBRARY_EXTENSION, 2) + FF_LIBRARY_LOAD_SYMBOL(zlib, compressBound, false) + FF_LIBRARY_LOAD_SYMBOL(zlib, compress2, false) + + uLong compressedLength = ffcompressBound(*length); + void* compressed = malloc(compressedLength); + if (compressed == NULL) { + return false; + } + + if (ffcompress2(compressed, &compressedLength, *blob, *length, Z_BEST_COMPRESSION) != Z_OK) { + free(compressed); + return false; + } + + free(*blob); + + *length = (size_t) compressedLength; + *blob = compressed; + return true; +} + + #endif // FF_HAVE_ZLIB + + // We use only the defines from here, that are exactly the same in both versions + #ifdef FF_HAVE_IMAGEMAGICK7 + #include <MagickCore/MagickCore.h> + #else + #include <magick/MagickCore.h> + #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 == NULL) { + 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); + ffWriteFileBuffer(requestData->cacheDir.chars, value); + ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength); +} + +static void writeCacheUint32(FFLogoRequestData* requestData, uint32_t value, const char* cacheFileName) { + FFstrbuf content; + content.chars = (char*) &value; + content.length = sizeof(value); + writeCacheStrbuf(requestData, &content, cacheFileName); +} + +static void printImagePixels(FFLogoRequestData* requestData, const FFstrbuf* result, const char* cacheFileName) { + const FFOptionsLogo* options = &instance.config.logo; + // Calculate character dimensions + instance.state.logoWidth = requestData->logoCharacterWidth + options->paddingLeft + options->paddingRight; + instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop - 1; + + // Write cache files + writeCacheStrbuf(requestData, result, cacheFileName); + + if (options->width == 0) { + writeCacheUint32(requestData, requestData->logoCharacterWidth, FF_CACHE_FILE_WIDTH); + } + + if (options->height == 0) { + writeCacheUint32(requestData, requestData->logoCharacterHeight, FF_CACHE_FILE_HEIGHT); + } + + // Write result to stdout + ffPrintCharTimes('\n', options->paddingTop); + if (options->position == FF_LOGO_POSITION_RIGHT) { + printf("\e[9999999C\e[%uD", (unsigned) options->paddingRight + requestData->logoCharacterWidth); + } else if (options->paddingLeft) { + printf("\e[%uC", (unsigned) options->paddingLeft); + } + fflush(stdout); + ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), result); + + if (options->position != FF_LOGO_POSITION_TOP) { + // Go to upper left corner + printf("\e[1G\e[%uA", instance.state.logoHeight); + } + + if (options->position != FF_LOGO_POSITION_LEFT) { + instance.state.logoWidth = instance.state.logoHeight = 0; + } +} + +static bool printImageSixel(FFLogoRequestData* requestData, const ImageData* imageData) { + imageData->ffCopyMagickString(imageData->imageInfo->magick, "SIXEL", 6); + + size_t length; + void* blob = imageData->ffImageToBlob(imageData->imageInfo, imageData->image, &length, imageData->exceptionInfo); + if (!checkAllocationResult(blob, length)) { + return false; + } + + FFstrbuf result; + result.chars = (char*) blob; + result.length = (uint32_t) length; + + printImagePixels(requestData, &result, FF_CACHE_FILE_SIXEL); + + free(blob); + return true; +} + +static void appendKittyChunk(FFstrbuf* result, const char** blob, size_t* length, bool printEscapeCode) { + uint32_t chunkSize = *length > FF_KITTY_MAX_CHUNK_SIZE ? FF_KITTY_MAX_CHUNK_SIZE : (uint32_t) *length; + + if (printEscapeCode) { + ffStrbufAppendS(result, "\033_G"); + } else { + ffStrbufAppendC(result, ','); + } + + ffStrbufAppendS(result, chunkSize != *length ? "m=1" : "m=0"); + ffStrbufAppendC(result, ';'); + ffStrbufAppendNS(result, chunkSize, *blob); + ffStrbufAppendS(result, "\033\\"); + *length -= chunkSize; + *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)) { + return false; + } + + #ifdef FF_HAVE_ZLIB + bool isCompressed = compressBlob(&blob, &length); + #else + bool isCompressed = false; + #endif + + char* chars = imageData->ffBase64Encode(blob, length, &length); + free(blob); + if (!checkAllocationResult(chars, length)) { + return false; + } + + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateA((uint32_t) (length + 1024)); + + const char* currentPos = chars; + size_t remainingLength = length; + + ffStrbufAppendF(&result, "\033_Ga=T,f=32,s=%u,v=%u", requestData->logoPixelWidth, requestData->logoPixelHeight); + if (isCompressed) { + ffStrbufAppendS(&result, ",o=z"); + } + appendKittyChunk(&result, ¤tPos, &remainingLength, false); + while (remainingLength > 0) { + appendKittyChunk(&result, ¤tPos, &remainingLength, true); + } + + printImagePixels(requestData, &result, isCompressed ? FF_CACHE_FILE_KITTY_COMPRESSED : FF_CACHE_FILE_KITTY_UNCOMPRESSED); + + free(chars); + return true; +} + + #ifdef FF_HAVE_CHAFA + #include <chafa.h> +static bool printImageChafa(FFLogoRequestData* requestData, const ImageData* imageData) { + FF_LIBRARY_LOAD(chafa, false, "libchafa" FF_LIBRARY_EXTENSION, 1, "libchafa-0" FF_LIBRARY_EXTENSION, -1 // Required for Windows + ) + 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) + FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_set_geometry, false) + FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_set_symbol_map, false) + FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_new, false) + FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_draw_all_pixels, false) + FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_print, false) + FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_unref, false) + 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 = NULL; + if (!ffchafa_symbol_map_apply_selectors(symbolMap, instance.config.logo.chafaSymbols.chars, &error)) { + fputs(error->message, stderr); + } + + ChafaCanvasConfig* canvasConfig = ffchafa_canvas_config_new(); + ffchafa_canvas_config_set_geometry(canvasConfig, (gint) requestData->logoCharacterWidth, (gint) requestData->logoCharacterHeight); + ffchafa_canvas_config_set_symbol_map(canvasConfig, symbolMap); + + if (instance.config.logo.chafaFgOnly) { + FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_fg_only_enabled); + if (ffchafa_canvas_config_set_fg_only_enabled) { + ffchafa_canvas_config_set_fg_only_enabled(canvasConfig, true); + } + } + if (instance.config.logo.chafaCanvasMode < CHAFA_CANVAS_MODE_MAX) { + FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_canvas_mode); + if (ffchafa_canvas_config_set_canvas_mode) { + ffchafa_canvas_config_set_canvas_mode(canvasConfig, (ChafaCanvasMode) instance.config.logo.chafaCanvasMode); + } + } + if (instance.config.logo.chafaColorSpace < CHAFA_COLOR_SPACE_MAX) { + FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_color_space) + if (ffchafa_canvas_config_set_color_space) { + ffchafa_canvas_config_set_color_space(canvasConfig, (ChafaColorSpace) instance.config.logo.chafaColorSpace); + } + } + if (instance.config.logo.chafaDitherMode < CHAFA_DITHER_MODE_MAX) { + FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_dither_mode) + if (ffchafa_canvas_config_set_dither_mode) { + ffchafa_canvas_config_set_dither_mode(canvasConfig, (ChafaDitherMode) instance.config.logo.chafaDitherMode); + } + } + + ChafaCanvas* canvas = ffchafa_canvas_new(canvasConfig); + ffchafa_canvas_draw_all_pixels( + canvas, + CHAFA_PIXEL_RGBA8_UNASSOCIATED, + blob, + (gint) imageData->image->columns, + (gint) imageData->image->rows, + (gint) imageData->image->columns * 4); + + GString* str = ffchafa_canvas_print(canvas, NULL); + FFstrbuf result; + result.allocated = (uint32_t) str->allocated_len; + result.length = (uint32_t) str->len; + result.chars = str->str; + + ffLogoPrintChars(result.chars, false); + writeCacheStrbuf(requestData, &result, FF_CACHE_FILE_CHAFA); + + // FIXME: These functions must be imported from `libglib` dlls on Windows + FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, g_string_free); + if (ffg_string_free) { + ffg_string_free(str, TRUE); + } + if (error) { + FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, g_error_free) + if (ffg_error_free) { + ffg_error_free(error); + } + } + + ffchafa_canvas_unref(canvas); + ffchafa_canvas_config_unref(canvasConfig); + ffchafa_symbol_map_unref(symbolMap); + + return true; +} + #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(NULL, MagickFalse); + + imageData.exceptionInfo = ffAcquireExceptionInfo(); + if (imageData.exceptionInfo == NULL) { + ffMagickCoreTerminus(); + return FF_LOGO_IMAGE_RESULT_RUN_ERROR; + } + + ImageInfo* imageInfoIn = ffAcquireImageInfo(); + if (imageInfoIn == NULL) { + ffDestroyExceptionInfo(imageData.exceptionInfo); + ffMagickCoreTerminus(); + return FF_LOGO_IMAGE_RESULT_RUN_ERROR; + } + + //+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 == NULL) { + 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 == NULL) { + ffDestroyExceptionInfo(imageData.exceptionInfo); + ffMagickCoreTerminus(); + return FF_LOGO_IMAGE_RESULT_RUN_ERROR; + } + imageData.image = resized; + + imageData.imageInfo = ffAcquireImageInfo(); + if (imageData.imageInfo == NULL) { + 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 + } 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); + } + + ffDestroyImageInfo(imageData.imageInfo); + ffDestroyImage(imageData.image); + ffDestroyExceptionInfo(imageData.exceptionInfo); + ffMagickCoreTerminus(); + + return printSuccessful ? FF_LOGO_IMAGE_RESULT_SUCCESS : FF_LOGO_IMAGE_RESULT_RUN_ERROR; +} + +static FFNativeFD getCacheFD(FFLogoRequestData* requestData, const char* fileName) { + uint32_t cacheDirLength = requestData->cacheDir.length; + ffStrbufAppendS(&requestData->cacheDir, fileName); + #ifndef _WIN32 + int fd = open(requestData->cacheDir.chars, O_RDONLY + #ifdef O_CLOEXEC + | O_CLOEXEC + #endif + ); + #else + HANDLE fd = CreateFileA(requestData->cacheDir.chars, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + #endif + ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength); + return fd; +} + +static void readCachedStrbuf(FFLogoRequestData* requestData, FFstrbuf* result, const char* cacheFileName) { + uint32_t cacheDirLength = requestData->cacheDir.length; + ffStrbufAppendS(&requestData->cacheDir, cacheFileName); + ffAppendFileBuffer(requestData->cacheDir.chars, result); + ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength); +} + +static uint32_t readCachedUint32(FFLogoRequestData* requestData, const char* cacheFileName) { + FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate(); + readCachedStrbuf(requestData, &content, cacheFileName); + + uint32_t result = 0; + + if (content.length != sizeof(result)) { + return 0; + } + + memcpy(&result, content.chars, sizeof(result)); + + return result; +} + +static bool printCachedChars(FFLogoRequestData* requestData, const char* cacheFileName) { + FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate(); + readCachedStrbuf(requestData, &content, cacheFileName); + + if (content.length == 0) { + return false; + } + + ffLogoPrintChars(content.chars, false); + return true; +} + +static bool printCachedPixel(FFLogoRequestData* requestData) { + FFOptionsLogo* options = &instance.config.logo; + + requestData->logoCharacterWidth = options->width; + if (requestData->logoCharacterWidth == 0) { + requestData->logoCharacterWidth = readCachedUint32(requestData, FF_CACHE_FILE_WIDTH); + if (requestData->logoCharacterWidth == 0) { + return false; + } + } + + requestData->logoCharacterHeight = options->height; + if (requestData->logoCharacterHeight == 0) { + requestData->logoCharacterHeight = readCachedUint32(requestData, FF_CACHE_FILE_HEIGHT); + if (requestData->logoCharacterHeight == 0) { + return false; + } + } + + FF_AUTO_CLOSE_FD FFNativeFD fd = FF_INVALID_FD; + if (requestData->type == FF_LOGO_TYPE_IMAGE_KITTY) { + fd = getCacheFD(requestData, FF_CACHE_FILE_KITTY_COMPRESSED); + if (!ffIsValidNativeFD(fd)) { + fd = getCacheFD(requestData, FF_CACHE_FILE_KITTY_UNCOMPRESSED); + } + } else if (requestData->type == FF_LOGO_TYPE_IMAGE_SIXEL) { + fd = getCacheFD(requestData, FF_CACHE_FILE_SIXEL); + } + + if (!ffIsValidNativeFD(fd)) { + return false; + } + + ffPrintCharTimes('\n', options->paddingTop); + if (options->position == FF_LOGO_POSITION_RIGHT) { + printf("\e[9999999C\e[%uD", (unsigned) options->paddingRight + requestData->logoCharacterWidth); + } else if (options->paddingLeft) { + printf("\e[%uC", (unsigned) options->paddingLeft); + } + fflush(stdout); + + bool sent = false; + #ifdef __linux__ + struct stat st; + if (fstat(fd, &st) >= 0) { + while (st.st_size > 0) { + ssize_t bytes = sendfile(STDOUT_FILENO, fd, NULL, (size_t) st.st_size); + if (bytes > 0) { + sent = true; + st.st_size -= bytes; + } else { + break; + } + } + } + #endif + + if (!sent) { + char buffer[32768]; + ssize_t readBytes; + while ((readBytes = ffReadFDData(fd, sizeof(buffer), buffer)) > 0) { + ffWriteFDData(FFUnixFD2NativeFD(STDOUT_FILENO), (size_t) readBytes, buffer); + } + } + + instance.state.logoWidth = requestData->logoCharacterWidth + options->paddingLeft + options->paddingRight; + instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop; + + if (options->position != FF_LOGO_POSITION_TOP) { + // Go to upper left corner + printf("\e[1G\e[%uA", instance.state.logoHeight); + } + + if (options->position != FF_LOGO_POSITION_LEFT) { + instance.state.logoWidth = instance.state.logoHeight = 0; + } + return true; +} + +static bool getCharacterPixelDimensions(FFLogoRequestData* requestData) { + #ifdef _WIN32 + + CONSOLE_FONT_INFOEX cfi = { .cbSize = sizeof(cfi) }; + if (GetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi)) // Only works for ConHost + { + requestData->characterPixelWidth = cfi.dwFontSize.X; + requestData->characterPixelHeight = cfi.dwFontSize.Y; + } + if (requestData->characterPixelWidth > 1.0 && requestData->characterPixelHeight > 1.0) { + return true; + } + #endif + + FFTerminalSizeResult termSize = {}; + if (ffDetectTerminalSize(&termSize)) { + requestData->characterPixelWidth = termSize.width / (double) termSize.columns; + requestData->characterPixelHeight = termSize.height / (double) termSize.rows; + } + + return requestData->characterPixelWidth > 1.0 && requestData->characterPixelHeight > 1.0; +} + +static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) { + FFLogoRequestData requestData; + requestData.type = type; + requestData.characterPixelWidth = 1; + requestData.characterPixelHeight = 1; + + if (!getCharacterPixelDimensions(&requestData)) { + if (printError) { + fputs("Logo: getCharacterPixelDimensions() failed\n", stderr); + } + return false; + } + + requestData.logoPixelWidth = (uint32_t) ceil((double) instance.config.logo.width * requestData.characterPixelWidth); + requestData.logoPixelHeight = (uint32_t) ceil((double) instance.config.logo.height * requestData.characterPixelHeight); + + ffStrbufInit(&requestData.cacheDir); + ffStrbufAppend(&requestData.cacheDir, &instance.state.platform.cacheDir); + ffStrbufAppendS(&requestData.cacheDir, "fastfetch/images"); + + ffStrbufEnsureFree(&requestData.cacheDir, PATH_MAX); + char* filePath = requestData.cacheDir.chars + requestData.cacheDir.length; + if (realpath(instance.config.logo.source.chars, filePath) == NULL) { + // We can safely return here, because if realpath failed, we surely won't be able to read the file + ffStrbufDestroy(&requestData.cacheDir); + if (printError) { + fputs("Logo: Querying realpath of the image source failed\n", stderr); + } + return false; + } + + #ifdef _WIN32 + filePath[1] = filePath[0]; // Drive Name + filePath[0] = '/'; + #endif + + ffStrbufRecalculateLength(&requestData.cacheDir); + ffStrbufEnsureEndsWithC(&requestData.cacheDir, '/'); + ffStrbufAppendF(&requestData.cacheDir, "%u*%u/", requestData.logoPixelWidth, requestData.logoPixelHeight); + + if (!instance.config.logo.recache) { + bool cacheValid = requestData.type == FF_LOGO_TYPE_IMAGE_CHAFA + ? printCachedChars(&requestData, FF_CACHE_FILE_CHAFA) + : printCachedPixel(&requestData); + if (cacheValid) { + ffStrbufDestroy(&requestData.cacheDir); + return true; + } + } + + FFLogoImageResult result = FF_LOGO_IMAGE_RESULT_INIT_ERROR; + + #ifdef FF_HAVE_IMAGEMAGICK7 + result = ffLogoPrintImageIM7(&requestData); + #endif + + #ifdef FF_HAVE_IMAGEMAGICK6 + if (result == FF_LOGO_IMAGE_RESULT_INIT_ERROR) { + result = ffLogoPrintImageIM6(&requestData); + } + #endif + + ffStrbufDestroy(&requestData.cacheDir); + + if (result == FF_LOGO_IMAGE_RESULT_SUCCESS) { + 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); + } + } + + return false; +} + +#endif // FF_HAVE_IMAGEMAGICK{6, 7} + +bool ffLogoPrintImageIfExists(FFLogoType type, bool printError) { + if (instance.config.display.pipe) { + if (printError) { + fputs("Logo: Image logo is not supported in pipe mode\n", stderr); + } + return false; + } + + if (!ffPathExists(instance.config.logo.source.chars, FF_PATHTYPE_FILE)) { + if (printError) { + fprintf(stderr, "Logo: Image source \"%s\" does not exist\n", instance.config.logo.source.chars); + } + return false; + } + + const char* term = getenv("TERM"); + if ((term && ffStrEquals(term, "screen")) || getenv("ZELLIJ")) { + if (printError) { + fputs("Logo: Image logo is not supported in terminal multiplexers\n", stderr); + } + return false; + } + + if (type == FF_LOGO_TYPE_IMAGE_ITERM) { + return printImageIterm(printError); + } + + if (type == FF_LOGO_TYPE_IMAGE_KITTY_DIRECT) { + return printImageKittyDirect(printError); + } + + if (type == FF_LOGO_TYPE_IMAGE_KITTY_ICAT) { + return printImageKittyIcat(printError); + } + +#if !FF_HAVE_CHAFA + if (type == FF_LOGO_TYPE_IMAGE_CHAFA) { + if (printError) { + fputs("Logo: Chafa support is not compiled in\n", stderr); + } + return false; + } +#endif + +#if !defined(FF_HAVE_IMAGEMAGICK7) && !defined(FF_HAVE_IMAGEMAGICK6) + if (printError) { + fputs("Logo: Image Magick support is not compiled in\n", stderr); + } + return false; +#else + return printImageIfExistsSlowPath(type, printError); +#endif +} diff --git a/src/logo/image/image.h b/src/logo/image/image.h new file mode 100644 index 0000000..da3b2d2 --- /dev/null +++ b/src/logo/image/image.h @@ -0,0 +1,43 @@ +#pragma once + +#include "../logo.h" + +#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) + +typedef enum FF_A_PACKED FFLogoImageResult { + FF_LOGO_IMAGE_RESULT_SUCCESS, // Logo printed + FF_LOGO_IMAGE_RESULT_INIT_ERROR, // Failed to load library, try again with next IM version + FF_LOGO_IMAGE_RESULT_RUN_ERROR // Failed to load / convert image, cancel whole sixel code +} FFLogoImageResult; + +typedef struct FFLogoRequestData { + FFLogoType type; + FFstrbuf cacheDir; + + double characterPixelWidth; + double characterPixelHeight; + + uint32_t logoPixelWidth; + uint32_t logoPixelHeight; + + uint32_t logoCharacterHeight; + uint32_t logoCharacterWidth; +} FFLogoRequestData; + +typedef struct FFIMData { + void* library; + void* (*resizeFunc)(const void* image, size_t width, size_t height, void* exceptionInfo); +} FFIMData; + +FFLogoImageResult ffLogoPrintImageImpl(FFLogoRequestData* requestData, const FFIMData* imData); + +#endif + +#ifdef FF_HAVE_IMAGEMAGICK7 +FFLogoImageResult ffLogoPrintImageIM7(FFLogoRequestData* requestData); +#endif + +#ifdef FF_HAVE_IMAGEMAGICK6 + #include <math.h> +FFLogoImageResult ffLogoPrintImageIM6(FFLogoRequestData* requestData); +#endif |