Files
fastfetch/src/logo/image/image.c
T

1138 lines
43 KiB
C
Raw Normal View History

2022-04-13 23:04:42 +02:00
#include "image.h"
2026-01-06 09:48:38 +08:00
#include "common/io.h"
#include "common/mallocHelper.h"
2026-01-06 09:48:38 +08:00
#include "common/printing.h"
#include "common/processing.h"
2026-05-28 15:58:08 +08:00
#include "common/strutil.h"
2026-01-06 09:48:38 +08:00
#include "common/base64.h"
2024-09-03 21:31:43 +08:00
#include "detection/terminalsize/terminalsize.h"
2022-04-06 14:02:05 +02:00
2023-06-12 15:10:41 +02:00
#include <limits.h>
2023-08-22 15:31:10 +08:00
#include <math.h>
2023-06-12 15:10:41 +02:00
2023-01-09 13:42:58 +08:00
#ifdef __APPLE__
#include <sys/syslimits.h>
#elif _WIN32
#include <windows.h>
#elif __linux__
#include <sys/sendfile.h>
2024-07-03 15:12:48 +08:00
#elif __sun
#include <sys/termios.h>
2023-01-09 13:42:58 +08:00
#endif
2026-03-29 09:28:49 +08:00
static bool printImageIterm(bool printError) {
2023-10-25 14:43:46 +08:00
const FFOptionsLogo* options = &instance.config.logo;
FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
2026-03-29 09:28:49 +08:00
if (!ffAppendFileBuffer(options->source.chars, &buf)) {
if (printError) {
fputs("Logo (iterm): Failed to load image file\n", stderr);
2026-03-29 09:28:49 +08:00
}
return false;
2023-01-01 12:29:08 +08:00
}
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);
2026-03-29 09:28:49 +08:00
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) {
2023-09-15 10:53:09 +08:00
ffStrbufAppendNC(&buf, options->paddingTop, '\n');
ffStrbufAppendNC(&buf, options->paddingLeft, ' ');
2026-03-29 09:28:49 +08:00
} 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);
2026-03-29 09:28:49 +08:00
}
return false;
}
ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;9999999H\e[%uD", (unsigned) options->paddingTop + 1, (unsigned) options->paddingRight + options->width);
}
2026-03-29 09:28:49 +08:00
if (inTmux) {
ffStrbufAppendS(&buf, "\ePtmux;\e");
2026-03-29 09:28:49 +08:00
}
2026-03-29 09:28:49 +08:00
if (options->width) {
ffStrbufAppendF(&buf, "\e]1337;File=inline=1;width=%u:%s\a", (unsigned) options->width, base64.chars);
2026-03-29 09:28:49 +08:00
} else {
ffStrbufAppendF(&buf, "\e]1337;File=inline=1:%s\a", base64.chars);
2026-03-29 09:28:49 +08:00
}
if (inTmux) {
ffStrbufAppendS(&buf, "\e\\");
2026-03-29 09:28:49 +08:00
}
ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
2026-03-29 09:28:49 +08:00
if (options->position == FF_LOGO_POSITION_LEFT || options->position == FF_LOGO_POSITION_RIGHT) {
2023-09-03 22:20:11 +08:00
uint16_t X = 0, Y = 0;
const char* error = ffGetTerminalResponse("\e[6n", 2, "%*[^0-9]%hu;%huR", &Y, &X);
2026-03-29 09:28:49 +08:00
if (error) {
2023-09-03 22:20:11 +08:00
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
}
2026-03-29 09:28:49 +08:00
if (X < options->paddingLeft + options->width) {
2024-09-02 20:57:47 +08:00
X = (uint16_t) (options->paddingLeft + options->width);
2026-03-29 09:28:49 +08:00
}
if (options->position == FF_LOGO_POSITION_LEFT) {
instance.state.logoWidth = X + options->paddingRight - 1;
2026-03-29 09:28:49 +08:00
}
2023-09-03 22:20:11 +08:00
instance.state.logoHeight = Y;
fputs("\e[H", stdout);
2026-03-29 09:28:49 +08:00
} else if (options->position == FF_LOGO_POSITION_TOP) {
2023-09-03 22:20:11 +08:00
instance.state.logoWidth = instance.state.logoHeight = 0;
2023-09-15 10:53:09 +08:00
ffPrintCharTimes('\n', options->paddingRight);
}
2026-03-29 09:28:49 +08:00
} else {
2023-09-15 10:53:09 +08:00
ffStrbufAppendNC(&buf, options->paddingTop, '\n');
2026-03-29 09:28:49 +08:00
if (options->position == FF_LOGO_POSITION_RIGHT) {
ffStrbufAppendF(&buf, "\e[9999999C\e[%uD", (unsigned) options->paddingRight + options->width);
2026-03-29 09:28:49 +08:00
} else if (options->paddingLeft) {
ffStrbufAppendF(&buf, "\e[%uC", (unsigned) options->paddingLeft);
2026-03-29 09:28:49 +08:00
}
if (inTmux) {
ffStrbufAppendS(&buf, "\ePtmux;\e");
2026-03-29 09:28:49 +08:00
}
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\\");
2026-03-29 09:28:49 +08:00
}
ffStrbufAppendC(&buf, '\n');
2023-09-03 22:20:11 +08:00
2026-03-29 09:28:49 +08:00
if (options->position == FF_LOGO_POSITION_LEFT) {
2023-09-15 10:53:09 +08:00
instance.state.logoWidth = options->width + options->paddingLeft + options->paddingRight;
instance.state.logoHeight = options->paddingTop + options->height;
2023-09-03 22:20:11 +08:00
ffStrbufAppendF(&buf, "\e[%uA", (unsigned) instance.state.logoHeight);
2026-03-29 09:28:49 +08:00
} else if (options->position == FF_LOGO_POSITION_TOP) {
2023-09-03 22:20:11 +08:00
instance.state.logoWidth = instance.state.logoHeight = 0;
2023-09-15 10:53:09 +08:00
ffStrbufAppendNC(&buf, options->paddingRight, '\n');
2026-03-29 09:28:49 +08:00
} 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;
}
2026-09-15 00:44:02 +08:00
// `kitten icat` parses a cursor positioning sequence it may have written: `\e[<n>C` (relative) or
// `\e[<row>;<col>H` (absolute). Returns 1 for the first form, 2 for the second one, 0 for anything
// else, in which case nothing is written back.
static int parseIcatCsi(const char* data, uint32_t length, uint32_t pos, uint32_t* end, uint32_t* first, uint32_t* second) {
if (pos + 2 >= length || data[pos] != '\e' || data[pos + 1] != '[') {
return 0;
}
uint32_t values[2] = { 0, 0 };
uint32_t count = 0;
uint32_t i = pos + 2;
while (i < length) {
char c = data[i];
if (c >= '0' && c <= '9') {
if (count == 0) {
count = 1;
}
values[count - 1] = values[count - 1] * 10 + (uint32_t) (c - '0');
++i;
} else if (c == ';' && count == 1) {
count = 2;
++i;
} else {
break;
}
}
if (i >= length) {
return 0;
}
if (data[i] == 'C' && count == 1) {
*end = i + 1;
*first = values[0];
*second = 0;
return 1;
}
if (data[i] == 'H' && count == 2) {
*end = i + 1;
*first = values[0];
*second = values[1];
return 2;
}
return 0;
}
// `kitten icat` positions the image itself: it always writes a carriage return, followed by at most
// one positioning sequence, and then the graphics escape. The carriage return alone would pull the
// cursor back to column 1 and undo the padding, and the absolute cursor move `kitten` writes when
// `--place` is used points at screen coordinates it picked itself, which are not necessarily the ones
// the padding asks for. Both are dropped here and the position is left to the caller, which has
// already moved the cursor to where the padding asks for. The horizontal centering `kitten` applied
// inside the requested rectangle is kept, but re-applied relatively, so it still follows the caller's
// padding.
static void appendIcatOutput(FFstrbuf* buf, const FFstrbuf* output, const FFOptionsLogo* options) {
const char* data = output->chars;
uint32_t pos = 0; // Start of the graphics escape; 0 means "layout not recognised"
uint32_t offset = 0; // Column offset `kitten` applied inside the requested rectangle
if (output->length > 0 && data[0] == '\r') {
uint32_t cursor = 1;
uint32_t end = 0, first = 0, second = 0;
int kind = parseIcatCsi(data, output->length, cursor, &end, &first, &second);
if (kind == 1) { // Relative move, written instead of the absolute one when `--place` is not used
offset = first;
cursor = end;
} else if (kind == 2 && second >= options->paddingLeft + 1) { // Absolute move, produced by `--place`
// The row is dropped along with the move, so it does not matter which one `kitten` picked.
// The column is where the padding put it, plus the centering, which is all that is kept.
offset = second - options->paddingLeft - 1;
cursor = end;
}
// The graphics escape has to follow once the positioning is gone
if (cursor + 1 < output->length && data[cursor] == '\e' && data[cursor + 1] == '_') {
pos = cursor;
}
}
if (pos == 0) {
// Unrecognised layout: keep it as is, positioning included
ffStrbufAppend(buf, output);
return;
}
if (offset > 0) {
// The cursor is already at the position the padding asks for
ffStrbufAppendF(buf, "\e[%uC", (unsigned) offset);
}
ffStrbufAppendNS(buf, output->length - pos, data + pos);
}
2026-03-29 09:28:49 +08:00
static bool printImageKittyIcat(bool printError) {
const FFOptionsLogo* options = &instance.config.logo;
2026-03-29 09:28:49 +08:00
if (!ffPathExists(options->source.chars, FF_PATHTYPE_FILE)) {
if (printError) {
fputs("Logo (kitty-icat): Failed to load image file\n", stderr);
2026-03-29 09:28:49 +08:00
}
return false;
}
fflush(stdout);
FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
2026-03-29 09:28:49 +08:00
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, ' ');
2026-03-29 09:28:49 +08:00
} else {
if (printError) {
fputs("Logo (kitty-icat): position top is not supported when logo width is set\n", stderr);
2026-03-29 09:28:49 +08:00
}
return false;
}
2026-03-29 09:28:49 +08:00
} else if (options->position == FF_LOGO_POSITION_RIGHT) {
if (printError) {
fputs("Logo (kitty-icat): position right is not supported\n", stderr);
2026-03-29 09:28:49 +08:00
}
return false;
}
2026-09-15 00:44:02 +08:00
// `kitten icat` writes its own positioning, so its output is collected separately and only
// appended once that positioning has been dropped (see appendIcatOutput)
FF_STRBUF_AUTO_DESTROY icatOutput = ffStrbufCreate();
2026-07-10 18:08:04 +08:00
const char* error = nullptr;
2026-03-29 09:28:49 +08:00
if (options->width) {
2026-09-15 00:44:02 +08:00
// `--place` measures `left` and `top` from the top left corner of the screen, so the padding is
// passed through unchanged. The move `kitten` writes for it is dropped again in appendIcatOutput,
// which leaves the position to the caller, so this is not what puts the image at the padding in
// the normal case. It is what keeps the fallback there working: when the output layout is not
// recognised - as under tmux, where `kitten` switches to unicode placeholders - its own
// positioning is kept, and this is what then places the image at the padding.
char place[64];
snprintf(place,
ARRAY_SIZE(place),
"--place=%ux%u@%ux%u",
options->width,
options->height == 0 ? 9999 : options->height,
2026-09-15 00:44:02 +08:00
options->paddingLeft,
options->paddingTop);
2026-09-15 00:44:02 +08:00
error = ffProcessAppendStdOut(&icatOutput, (char*[]) {
2026-03-29 09:28:49 +08:00
"kitten",
"icat",
"-n",
"--stdin=no",
2026-03-29 09:28:49 +08:00
"--align=center",
place,
"--scale-up",
options->source.chars,
2026-07-10 18:08:04 +08:00
nullptr,
2026-03-29 09:28:49 +08:00
});
} else {
2026-09-15 00:44:02 +08:00
error = ffProcessAppendStdOut(&icatOutput, (char*[]) {
2026-03-29 09:28:49 +08:00
"kitten",
"icat",
"-n",
"--stdin=no",
2026-03-29 09:28:49 +08:00
"--align=left",
options->source.chars,
2026-07-10 18:08:04 +08:00
nullptr,
2026-03-29 09:28:49 +08:00
});
}
if (error) {
if (printError) {
fprintf(stderr, "Logo (kitty-icat): running `kitten icat` failed %s\n", error);
2026-03-29 09:28:49 +08:00
}
return false;
}
2026-09-15 00:44:02 +08:00
if (icatOutput.length == 0) {
2026-03-29 09:28:49 +08:00
if (printError) {
fputs("Logo (kitty-icat): `kitten icat` returned empty output\n", stderr);
2026-03-29 09:28:49 +08:00
}
return false;
}
2026-09-15 00:44:02 +08:00
appendIcatOutput(&buf, &icatOutput, options);
ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
2026-03-29 09:28:49 +08:00
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);
2026-03-29 09:28:49 +08:00
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
}
2026-03-29 09:28:49 +08:00
if (X < options->paddingLeft + options->width) {
X = (uint16_t) (options->paddingLeft + options->width);
2026-03-29 09:28:49 +08:00
}
if (options->position == FF_LOGO_POSITION_LEFT) {
instance.state.logoWidth = X + options->paddingRight - 1;
2026-03-29 09:28:49 +08:00
}
instance.state.logoHeight = Y;
fputs("\e[H", stdout);
2026-03-29 09:28:49 +08:00
} else if (options->position == FF_LOGO_POSITION_TOP) {
instance.state.logoWidth = instance.state.logoHeight = 0;
ffPrintCharTimes('\n', options->paddingRight);
}
return true;
}
2026-03-29 09:28:49 +08:00
static bool printImageKittyDirect(bool printError) {
2023-10-25 14:43:46 +08:00
const FFOptionsLogo* options = &instance.config.logo;
2026-03-29 09:28:49 +08:00
if (!ffPathExists(options->source.chars, FF_PATHTYPE_FILE)) {
if (printError) {
fputs("Logo (kitty-direct): Failed to load image file\n", stderr);
2026-03-29 09:28:49 +08:00
}
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();
2026-03-29 09:28:49 +08:00
if (!options->width || !options->height) {
if (options->position == FF_LOGO_POSITION_LEFT) {
2023-09-03 22:20:11 +08:00
// We must clear the entre screen to make sure that terminal buffer won't scroll up
2026-03-29 09:28:49 +08:00
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, ' ');
2026-03-29 09:28:49 +08:00
} 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);
2026-03-29 09:28:49 +08:00
}
return false;
}
ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;9999999H\e[%uD", (unsigned) options->paddingTop + 1, (unsigned) options->paddingRight + options->width);
}
2026-03-29 09:28:49 +08:00
if (inTmux) {
ffStrbufAppendS(&buf, "\ePtmux;\e");
2026-03-29 09:28:49 +08:00
}
if (options->width) {
ffStrbufAppendF(&buf, "\e_Ga=T,f=100,t=f,c=%u;%s", (unsigned) options->width, base64.chars);
2026-03-29 09:28:49 +08:00
} else {
ffStrbufAppendF(&buf, "\e_Ga=T,f=100,t=f;%s", base64.chars);
2026-03-29 09:28:49 +08:00
}
if (inTmux) {
ffStrbufAppendC(&buf, '\e');
2026-03-29 09:28:49 +08:00
}
ffStrbufAppendS(&buf, "\e\\");
2026-03-29 09:28:49 +08:00
if (inTmux) {
ffStrbufAppendS(&buf, "\e\\");
2026-03-29 09:28:49 +08:00
}
ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
2026-03-29 09:28:49 +08:00
if (options->position == FF_LOGO_POSITION_LEFT || options->position == FF_LOGO_POSITION_RIGHT) {
2023-09-03 22:20:11 +08:00
uint16_t X = 0, Y = 0;
const char* error = ffGetTerminalResponse("\e[6n", 2, "%*[^0-9]%hu;%huR", &Y, &X);
2026-03-29 09:28:49 +08:00
if (error) {
if (printError) {
fprintf(stderr, "\nLogo (kitty-direct): fail to query cursor position: %s\n", error);
2026-03-29 09:28:49 +08:00
}
2023-09-03 22:20:11 +08:00
return true; // We already printed image logo, don't print ascii logo then
}
2026-03-29 09:28:49 +08:00
if (X < options->paddingLeft + options->width) {
2024-09-02 20:57:47 +08:00
X = (uint16_t) (options->paddingLeft + options->width);
2026-03-29 09:28:49 +08:00
}
if (options->position == FF_LOGO_POSITION_LEFT) {
instance.state.logoWidth = X + options->paddingRight - 1;
2026-03-29 09:28:49 +08:00
}
2023-09-03 22:20:11 +08:00
instance.state.logoHeight = Y;
fputs("\e[H", stdout);
2026-03-29 09:28:49 +08:00
} else if (options->position == FF_LOGO_POSITION_TOP) {
2023-09-03 22:20:11 +08:00
instance.state.logoWidth = instance.state.logoHeight = 0;
2023-09-15 10:53:09 +08:00
ffPrintCharTimes('\n', options->paddingRight);
}
2026-03-29 09:28:49 +08:00
} else {
ffStrbufAppendNC(&buf, options->paddingTop, '\n');
2026-03-29 09:28:49 +08:00
if (options->position == FF_LOGO_POSITION_RIGHT) {
ffStrbufAppendF(&buf, "\e[9999999C\e[%uD", (unsigned) options->paddingRight + options->width);
2026-03-29 09:28:49 +08:00
} else if (options->paddingLeft) {
ffStrbufAppendF(&buf, "\e[%uC", (unsigned) options->paddingLeft);
2026-03-29 09:28:49 +08:00
}
2026-03-29 09:28:49 +08:00
if (inTmux) {
ffStrbufAppendS(&buf, "\ePtmux;\e");
2026-03-29 09:28:49 +08:00
}
2026-03-29 09:28:49 +08:00
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\\");
2026-03-29 09:28:49 +08:00
}
ffStrbufAppendC(&buf, '\n');
2026-03-29 09:28:49 +08:00
if (options->position == FF_LOGO_POSITION_LEFT) {
2023-09-15 10:53:09 +08:00
instance.state.logoWidth = options->width + options->paddingLeft + options->paddingRight;
instance.state.logoHeight = options->paddingTop + options->height;
ffStrbufAppendF(&buf, "\e[%uA", (unsigned) instance.state.logoHeight);
2026-03-29 09:28:49 +08:00
} else if (options->position == FF_LOGO_POSITION_TOP) {
2023-09-03 22:20:11 +08:00
instance.state.logoWidth = instance.state.logoHeight = 0;
ffStrbufAppendNC(&buf, options->paddingRight, '\n');
2026-03-29 09:28:49 +08:00
} 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);
2023-07-18 21:09:37 +08:00
}
2022-12-23 12:34:30 +08:00
return true;
}
#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(__APPLE__) || defined(FF_HAVE_SIXEL)
2022-04-06 14:02:05 +02:00
#define FF_KITTY_MAX_CHUNK_SIZE 4096
2022-04-14 11:36:57 +02:00
#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"
// Modification time of the image source the entry was produced from. Written last, so an
// entry that was interrupted mid-write is never mistaken for a complete one.
#define FF_CACHE_FILE_MTIME "mtime"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
2022-12-20 11:37:54 +08:00
#ifndef _WIN32
#include <sys/ioctl.h>
#else
#include <wincon.h>
#include "common/path.h"
#endif
#ifdef FF_HAVE_ZLIB
#include "common/library.h"
#include <zlib.h>
2022-04-14 15:11:14 +02:00
2026-03-29 09:28:49 +08:00
static bool compressBlob(void** blob, size_t* length) {
2024-09-04 15:28:35 +08:00
FF_LIBRARY_LOAD(zlib, false, "libz" FF_LIBRARY_EXTENSION, 2)
2022-04-14 15:11:14 +02:00
FF_LIBRARY_LOAD_SYMBOL(zlib, compressBound, false)
FF_LIBRARY_LOAD_SYMBOL(zlib, compress2, false)
#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);
2022-04-14 15:11:14 +02:00
void* compressed = malloc(compressedLength);
2026-07-10 18:08:04 +08:00
if (compressed == nullptr) {
2022-04-14 15:11:14 +02:00
return false;
2026-03-29 09:28:49 +08:00
}
2022-04-14 15:11:14 +02:00
if (ffcompress2(compressed, &compressedLength, *blob, (uLong) *length, Z_BEST_COMPRESSION) != Z_OK) {
2022-04-14 15:11:14 +02:00
free(compressed);
return false;
}
free(*blob);
*length = (size_t) compressedLength;
*blob = compressed;
return true;
}
#endif // FF_HAVE_ZLIB
2022-04-14 15:11:14 +02:00
static void writeCacheData(FFLogoRequestData* requestData, const void* value, size_t len, const char* cacheFileName) {
// Every payload file goes through here, including the ones written from the printing helpers,
// so this is the single place that keeps `--logo-cache false` from writing anything at all.
if (instance.config.logo.cache == FF_LOGO_CACHE_OFF) {
return;
}
2022-05-23 15:32:10 +02:00
uint32_t cacheDirLength = requestData->cacheDir.length;
ffStrbufAppendS(&requestData->cacheDir, cacheFileName);
ffWriteFileData(requestData->cacheDir.chars, len, value);
2022-05-23 15:32:10 +02:00
ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
}
2026-03-29 09:28:49 +08:00
static void printImagePixels(FFLogoRequestData* requestData, const FFstrbuf* result, const char* cacheFileName) {
2023-10-25 14:43:46 +08:00
const FFOptionsLogo* options = &instance.config.logo;
2026-03-29 09:28:49 +08:00
// Calculate character dimensions
2023-09-15 10:53:09 +08:00
instance.state.logoWidth = requestData->logoCharacterWidth + options->paddingLeft + options->paddingRight;
instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop - 1;
2022-04-28 19:15:46 +02:00
2026-03-29 09:28:49 +08:00
// Write cache files
writeCacheData(requestData, result->chars, result->length, cacheFileName);
2022-05-23 15:32:10 +02:00
2026-03-29 09:28:49 +08:00
if (options->width == 0) {
writeCacheData(requestData, &requestData->logoCharacterWidth, sizeof(requestData->logoCharacterWidth), FF_CACHE_FILE_WIDTH);
2026-03-29 09:28:49 +08:00
}
2022-05-23 15:32:10 +02:00
2026-03-29 09:28:49 +08:00
if (options->height == 0) {
writeCacheData(requestData, &requestData->logoCharacterHeight, sizeof(requestData->logoCharacterHeight), FF_CACHE_FILE_HEIGHT);
2026-03-29 09:28:49 +08:00
}
2022-04-14 11:36:57 +02:00
2026-03-29 09:28:49 +08:00
// Write result to stdout
2023-09-15 10:53:09 +08:00
ffPrintCharTimes('\n', options->paddingTop);
2026-03-29 09:28:49 +08:00
if (options->position == FF_LOGO_POSITION_RIGHT) {
2024-06-28 10:05:38 +08:00
printf("\e[9999999C\e[%uD", (unsigned) options->paddingRight + requestData->logoCharacterWidth);
2026-03-29 09:28:49 +08:00
} else if (options->paddingLeft) {
2024-06-28 10:05:38 +08:00
printf("\e[%uC", (unsigned) options->paddingLeft);
2026-03-29 09:28:49 +08:00
}
2022-04-22 13:20:40 +02:00
fflush(stdout);
2023-01-28 22:05:23 +08:00
ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), result);
2022-04-14 11:36:57 +02:00
2026-03-29 09:28:49 +08:00
if (options->position != FF_LOGO_POSITION_TOP) {
// Go to upper left corner
2024-06-28 10:05:38 +08:00
printf("\e[1G\e[%uA", instance.state.logoHeight);
}
2026-03-29 09:28:49 +08:00
if (options->position != FF_LOGO_POSITION_LEFT) {
2024-06-28 10:05:38 +08:00
instance.state.logoWidth = instance.state.logoHeight = 0;
2026-03-29 09:28:49 +08:00
}
2022-04-14 11:36:57 +02:00
}
// 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);
}
2022-04-21 15:50:45 +02:00
static bool printImageSixel(FFLogoRequestData* requestData, const FFstrbuf* result) {
if (result->length == 0) {
2022-04-22 13:20:40 +02:00
return false;
2026-03-29 09:28:49 +08:00
}
2022-04-21 15:50:45 +02:00
printImagePixels(requestData, result, FF_CACHE_FILE_SIXEL);
2022-04-22 13:20:40 +02:00
return true;
}
2022-04-21 15:50:45 +02:00
2026-03-29 09:28:49 +08:00
static void appendKittyChunk(FFstrbuf* result, const char** blob, size_t* length, bool printEscapeCode) {
2022-04-22 13:20:40 +02:00
uint32_t chunkSize = *length > FF_KITTY_MAX_CHUNK_SIZE ? FF_KITTY_MAX_CHUNK_SIZE : (uint32_t) *length;
2026-03-29 09:28:49 +08:00
if (printEscapeCode) {
2022-04-22 13:20:40 +02:00
ffStrbufAppendS(result, "\033_G");
2026-03-29 09:28:49 +08:00
} else {
2022-04-22 13:20:40 +02:00
ffStrbufAppendC(result, ',');
2026-03-29 09:28:49 +08:00
}
2022-04-22 13:20:40 +02:00
ffStrbufAppendS(result, chunkSize != *length ? "m=1" : "m=0");
ffStrbufAppendC(result, ';');
ffStrbufAppendNS(result, chunkSize, *blob);
ffStrbufAppendS(result, "\033\\");
*length -= chunkSize;
*blob += chunkSize;
2022-04-14 11:36:57 +02:00
}
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) {
2022-04-14 11:36:57 +02:00
return false;
2026-03-29 09:28:49 +08:00
}
memcpy(blob, buffer->data, length);
2022-04-14 11:36:57 +02:00
#ifdef FF_HAVE_ZLIB
2026-03-29 09:28:49 +08:00
bool isCompressed = compressBlob(&blob, &length);
#else
2026-03-29 09:28:49 +08:00
bool isCompressed = false;
#endif
2022-04-14 15:11:14 +02:00
// 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);
2022-04-14 11:36:57 +02:00
FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateA(base64.length + 1024);
2022-04-14 11:36:57 +02:00
const char* currentPos = base64.chars;
size_t remainingLength = base64.length;
2022-04-14 11:36:57 +02:00
ffStrbufAppendF(&result, "\033_Ga=T,f=32,s=%u,v=%u", buffer->width, buffer->height);
2026-03-29 09:28:49 +08:00
if (isCompressed) {
2022-04-22 13:20:40 +02:00
ffStrbufAppendS(&result, ",o=z");
2026-03-29 09:28:49 +08:00
}
2022-04-22 13:20:40 +02:00
appendKittyChunk(&result, &currentPos, &remainingLength, false);
2026-03-29 09:28:49 +08:00
while (remainingLength > 0) {
2022-04-22 13:20:40 +02:00
appendKittyChunk(&result, &currentPos, &remainingLength, true);
2026-03-29 09:28:49 +08:00
}
2022-04-14 11:36:57 +02:00
printImagePixels(requestData, &result, isCompressed ? FF_CACHE_FILE_KITTY_COMPRESSED : FF_CACHE_FILE_KITTY_UNCOMPRESSED);
2022-04-14 11:36:57 +02:00
return true;
}
#ifdef FF_HAVE_CHAFA
#include <chafa.h>
static bool printImageChafa(FFLogoRequestData* requestData, const FFImageBuffer* buffer) {
#if _WIN32
FF_LIBRARY_LOAD(chafa, false, "libchafa-0" FF_LIBRARY_EXTENSION, 0)
#else
FF_LIBRARY_LOAD(chafa, false, "libchafa" FF_LIBRARY_EXTENSION, 1)
#endif
2022-04-28 19:15:46 +02:00
FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_new, false)
2022-12-22 02:07:35 +08:00
FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_apply_selectors, false)
2022-04-28 19:15:46 +02:00
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)
2022-12-22 02:07:35 +08:00
2022-04-28 19:15:46 +02:00
ChafaSymbolMap* symbolMap = ffchafa_symbol_map_new();
2026-07-10 18:08:04 +08:00
GError* error = nullptr;
2026-03-29 09:28:49 +08:00
if (!ffchafa_symbol_map_apply_selectors(symbolMap, instance.config.logo.chafaSymbols.chars, &error)) {
2022-12-22 02:07:35 +08:00
fputs(error->message, stderr);
2026-03-29 09:28:49 +08:00
}
2022-04-28 19:15:46 +02:00
ChafaCanvasConfig* canvasConfig = ffchafa_canvas_config_new();
2022-05-23 15:32:10 +02:00
ffchafa_canvas_config_set_geometry(canvasConfig, (gint) requestData->logoCharacterWidth, (gint) requestData->logoCharacterHeight);
2022-04-28 19:15:46 +02:00
ffchafa_canvas_config_set_symbol_map(canvasConfig, symbolMap);
2022-12-22 11:36:13 +08:00
2026-03-29 09:28:49 +08:00
if (instance.config.logo.chafaFgOnly) {
2022-12-22 11:36:13 +08:00
FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_fg_only_enabled);
2026-03-29 09:28:49 +08:00
if (ffchafa_canvas_config_set_fg_only_enabled) {
2022-12-22 11:36:13 +08:00
ffchafa_canvas_config_set_fg_only_enabled(canvasConfig, true);
2026-03-29 09:28:49 +08:00
}
2022-12-22 11:36:13 +08:00
}
2026-03-29 09:28:49 +08:00
if (instance.config.logo.chafaCanvasMode < CHAFA_CANVAS_MODE_MAX) {
2022-12-22 11:36:13 +08:00
FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_canvas_mode);
2026-03-29 09:28:49 +08:00
if (ffchafa_canvas_config_set_canvas_mode) {
ffchafa_canvas_config_set_canvas_mode(canvasConfig, (ChafaCanvasMode) instance.config.logo.chafaCanvasMode);
2026-03-29 09:28:49 +08:00
}
2022-12-22 11:36:13 +08:00
}
2026-03-29 09:28:49 +08:00
if (instance.config.logo.chafaColorSpace < CHAFA_COLOR_SPACE_MAX) {
2022-12-22 11:36:13 +08:00
FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_color_space)
2026-03-29 09:28:49 +08:00
if (ffchafa_canvas_config_set_color_space) {
ffchafa_canvas_config_set_color_space(canvasConfig, (ChafaColorSpace) instance.config.logo.chafaColorSpace);
2026-03-29 09:28:49 +08:00
}
2022-12-22 11:36:13 +08:00
}
2026-03-29 09:28:49 +08:00
if (instance.config.logo.chafaDitherMode < CHAFA_DITHER_MODE_MAX) {
2022-12-22 11:36:13 +08:00
FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_dither_mode)
2026-03-29 09:28:49 +08:00
if (ffchafa_canvas_config_set_dither_mode) {
ffchafa_canvas_config_set_dither_mode(canvasConfig, (ChafaDitherMode) instance.config.logo.chafaDitherMode);
2026-03-29 09:28:49 +08:00
}
2022-12-22 11:36:13 +08:00
}
2022-04-28 19:15:46 +02:00
ChafaCanvas* canvas = ffchafa_canvas_new(canvasConfig);
ffchafa_canvas_draw_all_pixels(
canvas,
CHAFA_PIXEL_RGBA8_UNASSOCIATED,
buffer->data,
(gint) buffer->width,
(gint) buffer->height,
(gint) buffer->width * 4);
2022-04-28 19:15:46 +02:00
2026-07-10 18:08:04 +08:00
GString* str = ffchafa_canvas_print(canvas, nullptr);
2022-04-28 19:15:46 +02:00
FFstrbuf result;
2022-05-23 15:32:10 +02:00
result.allocated = (uint32_t) str->allocated_len;
2022-04-28 19:15:46 +02:00
result.length = (uint32_t) str->len;
2022-05-23 15:32:10 +02:00
result.chars = str->str;
2022-12-20 22:44:45 +08:00
ffLogoPrintChars(result.chars, false);
2026-09-15 20:11:15 +08:00
writeCacheData(requestData, result.chars, result.length, FF_CACHE_FILE_CHAFA);
2022-04-28 19:15:46 +02:00
2022-12-22 11:36:13 +08:00
// FIXME: These functions must be imported from `libglib` dlls on Windows
FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, g_string_free);
2026-03-29 09:28:49 +08:00
if (ffg_string_free) {
2022-12-22 11:36:13 +08:00
ffg_string_free(str, TRUE);
2026-03-29 09:28:49 +08:00
}
if (error) {
2022-12-22 11:36:13 +08:00
FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, g_error_free)
2026-03-29 09:28:49 +08:00
if (ffg_error_free) {
2022-12-22 11:36:13 +08:00
ffg_error_free(error);
2026-03-29 09:28:49 +08:00
}
2022-12-22 11:36:13 +08:00
}
2022-12-22 02:07:35 +08:00
2022-04-28 19:15:46 +02:00
ffchafa_canvas_unref(canvas);
ffchafa_canvas_config_unref(canvasConfig);
ffchafa_symbol_map_unref(symbolMap);
return true;
}
#endif
2022-04-28 19:15:46 +02:00
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)) {
return true;
}
#endif
#ifdef FF_HAVE_IMAGEMAGICK6
if (ffImageCreateIM6(requestData, out, error)) {
return true;
2022-04-06 14:02:05 +02:00
}
#endif
return false;
#endif
}
2022-04-06 14:02:05 +02:00
void ffImageDestroy(FFImageBuffer* buffer) {
free(buffer->data);
buffer->data = nullptr;
buffer->width = 0;
buffer->height = 0;
}
2022-04-06 14:02:05 +02:00
bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const char** error) {
// 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)) {
return false;
2026-03-29 09:28:49 +08:00
}
bool ok = ffSixelEncode(&buffer, out, error);
ffImageDestroy(&buffer);
return ok;
#else
FF_UNUSED(requestData, out);
if (error) {
*error = "sixel support is not compiled in";
2022-04-28 19:15:46 +02:00
}
return false;
#endif
#else
#ifdef FF_HAVE_IMAGEMAGICK7
if (ffImageSixelEncodeIM7(requestData, out, error)) {
return true;
2022-04-06 14:02:05 +02:00
}
#endif
#ifdef FF_HAVE_IMAGEMAGICK6
if (ffImageSixelEncodeIM6(requestData, out, error)) {
return true;
2022-04-06 14:02:05 +02:00
}
#endif
return false;
#endif
2022-04-06 14:02:05 +02:00
}
2022-04-13 23:04:42 +02:00
2026-03-29 09:28:49 +08:00
static FFNativeFD getCacheFD(FFLogoRequestData* requestData, const char* fileName) {
2022-04-21 15:50:45 +02:00
uint32_t cacheDirLength = requestData->cacheDir.length;
2022-04-22 13:20:40 +02:00
ffStrbufAppendS(&requestData->cacheDir, fileName);
#ifndef _WIN32
2023-12-04 20:32:46 +08:00
int fd = open(requestData->cacheDir.chars, O_RDONLY
#ifdef O_CLOEXEC
2023-12-04 20:32:46 +08:00
| O_CLOEXEC
#endif
2023-12-04 20:32:46 +08:00
);
#else
2026-07-10 18:08:04 +08:00
HANDLE fd = CreateFileA(requestData->cacheDir.chars, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
#endif
2022-04-21 15:50:45 +02:00
ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
2022-04-22 13:20:40 +02:00
return fd;
2022-04-21 15:50:45 +02:00
}
static bool readCachedStrbuf(FFLogoRequestData* requestData, FFstrbuf* result, const char* cacheFileName) {
2022-05-23 15:32:10 +02:00
uint32_t cacheDirLength = requestData->cacheDir.length;
ffStrbufAppendS(&requestData->cacheDir, cacheFileName);
bool res = ffAppendFileBuffer(requestData->cacheDir.chars, result);
2022-05-23 15:32:10 +02:00
ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
return res;
2022-05-23 15:32:10 +02:00
}
static bool readCachedData(FFLogoRequestData* requestData, void* buffer, size_t bufferSize, const char* cacheFileName) {
uint32_t cacheDirLength = requestData->cacheDir.length;
ffStrbufAppendS(&requestData->cacheDir, cacheFileName);
bool res = ffReadFileData(requestData->cacheDir.chars, bufferSize, buffer) == (ssize_t) bufferSize;
ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
return res;
}
2022-05-23 15:32:10 +02:00
static uint32_t readCachedUint32(FFLogoRequestData* requestData, const char* cacheFileName) {
2022-05-23 15:32:10 +02:00
uint32_t result = 0;
if (!readCachedData(requestData, &result, sizeof(result), cacheFileName)) {
2022-05-23 15:32:10 +02:00
return 0;
2026-03-29 09:28:49 +08:00
}
2022-04-21 15:50:45 +02:00
return result;
}
static uint64_t readCachedUint64(FFLogoRequestData* requestData, const char* cacheFileName) {
uint64_t result = 0;
if (!readCachedData(requestData, &result, sizeof(result), cacheFileName)) {
return 0;
}
2022-04-21 15:50:45 +02:00
2022-05-23 15:32:10 +02:00
return result;
}
2022-04-21 15:50:45 +02:00
// Drops everything a previous version of the source left in the entry directory.
// The directory is keyed on the source path and the pixel size only, so it is reused across
// edits; without this, a payload written for another logo type would be read back.
static void removeCachedFiles(FFLogoRequestData* requestData) {
static const char* const files[] = {
FF_CACHE_FILE_MTIME,
FF_CACHE_FILE_WIDTH,
FF_CACHE_FILE_HEIGHT,
FF_CACHE_FILE_SIXEL,
FF_CACHE_FILE_KITTY_COMPRESSED,
FF_CACHE_FILE_KITTY_UNCOMPRESSED,
FF_CACHE_FILE_CHAFA,
};
uint32_t cacheDirLength = requestData->cacheDir.length;
for (uint32_t i = 0; i < ARRAY_SIZE(files); ++i) {
ffStrbufAppendS(&requestData->cacheDir, files[i]);
ffRemoveFile(requestData->cacheDir.chars);
ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
}
}
static bool printCachedChars(FFLogoRequestData* requestData, const char* cacheFileName) {
FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate();
readCachedStrbuf(requestData, &content, cacheFileName);
2022-05-23 15:32:10 +02:00
2026-03-29 09:28:49 +08:00
if (content.length == 0) {
2022-05-23 15:32:10 +02:00
return false;
2026-03-29 09:28:49 +08:00
}
2022-05-23 15:32:10 +02:00
ffLogoPrintChars(content.chars, false);
2022-05-23 15:32:10 +02:00
return true;
}
2026-03-29 09:28:49 +08:00
static bool printCachedPixel(FFLogoRequestData* requestData) {
2024-06-28 10:05:38 +08:00
FFOptionsLogo* options = &instance.config.logo;
requestData->logoCharacterWidth = options->width;
2026-03-29 09:28:49 +08:00
if (requestData->logoCharacterWidth == 0) {
2022-05-23 15:32:10 +02:00
requestData->logoCharacterWidth = readCachedUint32(requestData, FF_CACHE_FILE_WIDTH);
2026-03-29 09:28:49 +08:00
if (requestData->logoCharacterWidth == 0) {
2022-05-23 15:32:10 +02:00
return false;
2026-03-29 09:28:49 +08:00
}
2022-05-23 15:32:10 +02:00
}
2022-04-21 15:50:45 +02:00
2024-06-28 10:05:38 +08:00
requestData->logoCharacterHeight = options->height;
2026-03-29 09:28:49 +08:00
if (requestData->logoCharacterHeight == 0) {
2022-05-23 15:32:10 +02:00
requestData->logoCharacterHeight = readCachedUint32(requestData, FF_CACHE_FILE_HEIGHT);
2026-03-29 09:28:49 +08:00
if (requestData->logoCharacterHeight == 0) {
2022-04-30 11:22:51 +02:00
return false;
2026-03-29 09:28:49 +08:00
}
2022-04-30 11:22:51 +02:00
}
2022-04-21 15:50:45 +02:00
2024-06-28 18:08:16 +08:00
FF_AUTO_CLOSE_FD FFNativeFD fd = FF_INVALID_FD;
2026-03-29 09:28:49 +08:00
if (requestData->type == FF_LOGO_TYPE_IMAGE_KITTY) {
2022-04-22 13:20:40 +02:00
fd = getCacheFD(requestData, FF_CACHE_FILE_KITTY_COMPRESSED);
2026-03-29 09:28:49 +08:00
if (!ffIsValidNativeFD(fd)) {
2022-04-22 13:20:40 +02:00
fd = getCacheFD(requestData, FF_CACHE_FILE_KITTY_UNCOMPRESSED);
2026-03-29 09:28:49 +08:00
}
} else if (requestData->type == FF_LOGO_TYPE_IMAGE_SIXEL) {
2022-04-22 13:20:40 +02:00
fd = getCacheFD(requestData, FF_CACHE_FILE_SIXEL);
2026-03-29 09:28:49 +08:00
}
2022-04-22 13:20:40 +02:00
2026-03-29 09:28:49 +08:00
if (!ffIsValidNativeFD(fd)) {
2022-04-22 13:20:40 +02:00
return false;
2026-03-29 09:28:49 +08:00
}
2022-04-22 13:20:40 +02:00
2024-06-28 10:05:38 +08:00
ffPrintCharTimes('\n', options->paddingTop);
2026-03-29 09:28:49 +08:00
if (options->position == FF_LOGO_POSITION_RIGHT) {
2024-06-28 10:05:38 +08:00
printf("\e[9999999C\e[%uD", (unsigned) options->paddingRight + requestData->logoCharacterWidth);
2026-03-29 09:28:49 +08:00
} else if (options->paddingLeft) {
2024-06-28 10:05:38 +08:00
printf("\e[%uC", (unsigned) options->paddingLeft);
2026-03-29 09:28:49 +08:00
}
2022-04-22 13:20:40 +02:00
fflush(stdout);
bool sent = false;
#ifdef __linux__
struct stat st;
2026-03-29 09:28:49 +08:00
if (fstat(fd, &st) >= 0) {
while (st.st_size > 0) {
2026-07-10 18:08:04 +08:00
ssize_t bytes = sendfile(STDOUT_FILENO, fd, nullptr, (size_t) st.st_size);
2026-03-29 09:28:49 +08:00
if (bytes > 0) {
sent = true;
st.st_size -= bytes;
2026-03-29 09:28:49 +08:00
} else {
break;
2026-03-29 09:28:49 +08:00
}
}
}
#endif
2026-03-29 09:28:49 +08:00
if (!sent) {
char buffer[32768];
ssize_t readBytes;
2026-03-29 09:28:49 +08:00
while ((readBytes = ffReadFDData(fd, sizeof(buffer), buffer)) > 0) {
ffWriteFDData(FFUnixFD2NativeFD(STDOUT_FILENO), (size_t) readBytes, buffer);
2026-03-29 09:28:49 +08:00
}
}
2022-04-22 13:20:40 +02:00
2024-06-28 10:05:38 +08:00
instance.state.logoWidth = requestData->logoCharacterWidth + options->paddingLeft + options->paddingRight;
instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop;
2022-04-22 13:20:40 +02:00
2026-03-29 09:28:49 +08:00
if (options->position != FF_LOGO_POSITION_TOP) {
// Go to upper left corner
2024-06-28 10:05:38 +08:00
printf("\e[1G\e[%uA", instance.state.logoHeight);
}
2022-05-23 15:32:10 +02:00
2026-03-29 09:28:49 +08:00
if (options->position != FF_LOGO_POSITION_LEFT) {
2024-06-28 10:05:38 +08:00
instance.state.logoWidth = instance.state.logoHeight = 0;
2026-03-29 09:28:49 +08:00
}
2022-04-22 13:20:40 +02:00
return true;
2022-04-21 15:50:45 +02:00
}
2026-03-29 09:28:49 +08:00
static bool getCharacterPixelDimensions(FFLogoRequestData* requestData) {
#ifdef _WIN32
2022-12-20 11:37:54 +08:00
2026-03-30 10:27:23 +08:00
CONSOLE_FONT_INFOEX cfi = { .cbSize = sizeof(cfi) };
2026-03-29 09:28:49 +08:00
if (GetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi)) // Only works for ConHost
2024-09-03 21:31:43 +08:00
{
requestData->characterPixelWidth = cfi.dwFontSize.X;
requestData->characterPixelHeight = cfi.dwFontSize.Y;
}
2026-03-29 09:28:49 +08:00
if (requestData->characterPixelWidth > 1.0 && requestData->characterPixelHeight > 1.0) {
2024-09-03 21:31:43 +08:00
return true;
2026-03-29 09:28:49 +08:00
}
#endif
2022-12-20 11:37:54 +08:00
2024-09-03 21:31:43 +08:00
FFTerminalSizeResult termSize = {};
2026-03-29 09:28:49 +08:00
if (ffDetectTerminalSize(&termSize)) {
2024-09-03 21:31:43 +08:00
requestData->characterPixelWidth = termSize.width / (double) termSize.columns;
requestData->characterPixelHeight = termSize.height / (double) termSize.rows;
}
2022-05-23 15:32:10 +02:00
return requestData->characterPixelWidth > 1.0 && requestData->characterPixelHeight > 1.0;
2022-04-21 15:50:45 +02:00
}
2026-03-29 09:28:49 +08:00
static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) {
2022-04-21 15:50:45 +02:00
FFLogoRequestData requestData;
requestData.type = type;
2022-05-23 15:32:10 +02:00
requestData.characterPixelWidth = 1;
requestData.characterPixelHeight = 1;
2026-03-29 09:28:49 +08:00
if (!getCharacterPixelDimensions(&requestData)) {
if (printError) {
fputs("Logo: getCharacterPixelDimensions() failed\n", stderr);
2026-03-29 09:28:49 +08:00
}
2022-06-08 10:59:33 +02:00
return false;
2022-12-23 12:44:19 +08:00
}
2022-04-21 15:50:45 +02:00
2023-08-22 15:31:10 +08:00
requestData.logoPixelWidth = (uint32_t) ceil((double) instance.config.logo.width * requestData.characterPixelWidth);
requestData.logoPixelHeight = (uint32_t) ceil((double) instance.config.logo.height * requestData.characterPixelHeight);
2022-04-30 11:22:51 +02:00
2023-01-15 15:57:26 +01:00
ffStrbufInit(&requestData.cacheDir);
ffStrbufAppend(&requestData.cacheDir, &instance.state.platform.cacheDir);
2023-01-28 11:28:05 +01:00
ffStrbufAppendS(&requestData.cacheDir, "fastfetch/images");
2022-04-21 15:50:45 +02:00
ffStrbufEnsureFree(&requestData.cacheDir, PATH_MAX);
char* filePath = requestData.cacheDir.chars + requestData.cacheDir.length;
2026-07-10 18:08:04 +08:00
if (realpath(instance.config.logo.source.chars, filePath) == nullptr) {
2026-03-29 09:28:49 +08:00
// We can safely return here, because if realpath failed, we surely won't be able to read the file
2022-04-21 15:50:45 +02:00
ffStrbufDestroy(&requestData.cacheDir);
2026-03-29 09:28:49 +08:00
if (printError) {
fputs("Logo: Querying realpath of the image source failed\n", stderr);
2026-03-29 09:28:49 +08:00
}
2022-06-08 10:59:33 +02:00
return false;
2022-04-21 15:50:45 +02:00
}
#ifdef _WIN32
filePath[1] = filePath[0]; // Drive Name
filePath[0] = '/';
#endif
2022-04-21 15:50:45 +02:00
ffStrbufRecalculateLength(&requestData.cacheDir);
ffStrbufEnsureEndsWithC(&requestData.cacheDir, '/');
ffStrbufAppendF(&requestData.cacheDir, "%ux%u/", requestData.logoPixelWidth, requestData.logoPixelHeight);
// The cached payload is a rendering, not a bit-exact artefact: every backend produces a
// valid one for the same source and pixel size, so the backend is deliberately not part of
// the key. What the key does have to capture is the content of the source, which the path
// can not: the same file can be replaced in place. Hence the recorded mtime.
// 0 means the mtime could not be read, in which case the entry is never trusted.
const uint64_t sourceMtime = ffPathGetMtime(instance.config.logo.source.chars);
2022-04-21 15:50:45 +02:00
if (instance.config.logo.cache == FF_LOGO_CACHE_ON &&
sourceMtime != 0 &&
readCachedUint64(&requestData, FF_CACHE_FILE_MTIME) == sourceMtime) {
bool cacheValid = requestData.type == FF_LOGO_TYPE_IMAGE_CHAFA
? printCachedChars(&requestData, FF_CACHE_FILE_CHAFA)
: printCachedPixel(&requestData);
if (cacheValid) {
ffStrbufDestroy(&requestData.cacheDir);
return true;
}
2022-04-21 15:50:45 +02:00
}
// Cache miss. The entry directory is keyed on the source path and the pixel size only, so
// it is reused when the source is edited; drop what the previous version left behind.
// With the cache turned off the directory is left alone entirely.
if (instance.config.logo.cache != FF_LOGO_CACHE_OFF) {
removeCachedFiles(&requestData);
}
const char* error = nullptr;
bool printSuccessful = false;
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
ffImageDestroy(&buffer);
}
2026-03-29 09:28:49 +08:00
}
2022-04-10 15:26:18 +02:00
if (printSuccessful) {
// Written last: an entry only becomes usable once its payload is complete
writeCacheData(&requestData, &sourceMtime, sizeof(sourceMtime), FF_CACHE_FILE_MTIME);
}
2022-04-21 15:50:45 +02:00
ffStrbufDestroy(&requestData.cacheDir);
2022-12-23 12:44:19 +08:00
if (printSuccessful) {
2023-01-28 11:14:16 +01:00
return true;
2026-03-29 09:28:49 +08:00
}
2023-01-28 11:14:16 +01:00
2026-03-29 09:28:49 +08:00
if (printError) {
fprintf(stderr, "Logo: %s\n", error ? error : "Failed to load / convert the image source");
2022-12-23 12:44:19 +08:00
}
2023-01-28 11:14:16 +01:00
return false;
2022-04-06 14:54:39 +02:00
}
2022-04-22 21:29:14 +02:00
2026-03-29 09:28:49 +08:00
#endif // FF_HAVE_IMAGEMAGICK{6, 7}
2022-12-23 12:44:19 +08:00
2026-03-29 09:28:49 +08:00
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);
2026-03-29 09:28:49 +08:00
}
return false;
}
2026-03-29 09:28:49 +08:00
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);
2026-03-29 09:28:49 +08:00
}
2022-12-23 12:44:19 +08:00
return false;
}
const char* term = getenv("TERM");
if (term && ffStrEquals(term, "screen")) {
2026-03-29 09:28:49 +08:00
if (printError) {
fputs("Logo: Image logo is not supported in terminal multiplexers\n", stderr);
2026-03-29 09:28:49 +08:00
}
return false;
}
2026-03-29 09:28:49 +08:00
if (type == FF_LOGO_TYPE_IMAGE_ITERM) {
return printImageIterm(printError);
2026-03-29 09:28:49 +08:00
}
2022-12-23 12:44:19 +08:00
2026-03-29 09:28:49 +08:00
if (type == FF_LOGO_TYPE_IMAGE_KITTY_DIRECT) {
return printImageKittyDirect(printError);
2026-03-29 09:28:49 +08:00
}
2022-12-23 12:44:19 +08:00
2026-03-29 09:28:49 +08:00
if (type == FF_LOGO_TYPE_IMAGE_KITTY_ICAT) {
return printImageKittyIcat(printError);
2026-03-29 09:28:49 +08:00
}
#if !FF_HAVE_CHAFA
2026-03-29 09:28:49 +08:00
if (type == FF_LOGO_TYPE_IMAGE_CHAFA) {
if (printError) {
fputs("Logo: Chafa support is not compiled in\n", stderr);
2022-12-23 12:44:19 +08:00
}
return false;
2026-03-29 09:28:49 +08:00
}
#endif
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(FF_HAVE_IMAGEMAGICK7) && !defined(FF_HAVE_IMAGEMAGICK6)
2026-03-29 09:28:49 +08:00
if (printError) {
fputs("Logo: Image Magick support is not compiled in\n", stderr);
}
return false;
#else
return printImageIfExistsSlowPath(type, printError);
#endif
2022-04-22 21:29:14 +02:00
}