master ohmu/fastfetch-openbsd / src / logo / image / image.c
ohmu  ·  2026-10-15
   1#include "image.h"
   2#include "common/io.h"
   3#include "common/library.h"
   4#include "common/mallocHelper.h"
   5#include "common/printing.h"
   6#include "common/processing.h"
   7#include "common/strutil.h"
   8#include "common/base64.h"
   9#include "detection/terminalsize/terminalsize.h"
  10
  11#include <limits.h>
  12#include <math.h>
  13
  14#ifdef __APPLE__
  15    #include <sys/syslimits.h>
  16#elif _WIN32
  17    #include <windows.h>
  18#elif __linux__
  19    #include <sys/sendfile.h>
  20#elif __sun
  21    #include <sys/termios.h>
  22#endif
  23
  24static void resolveImageAutoPosition(uint32_t logoWidth) {
  25    const FFOptionsLogo* options = &instance.config.logo;
  26    ffLogoResolveAutoPosition(logoWidth + options->paddingLeft + options->paddingRight);
  27}
  28
  29static bool printImageIterm(bool printError) {
  30    const FFOptionsLogo* options = &instance.config.logo;
  31    FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
  32    if (!ffAppendFileBuffer(options->source.chars, &buf)) {
  33        if (printError) {
  34            fputs("Logo (iterm): Failed to load image file\n", stderr);
  35        }
  36        return false;
  37    }
  38
  39    fflush(stdout);
  40
  41    bool inTmux = false;
  42    {
  43        const char* term = getenv("TERM");
  44        inTmux = term && (ffStrStartsWith(term, "screen") || ffStrStartsWith(term, "tmux"));
  45    }
  46
  47    FF_STRBUF_AUTO_DESTROY base64 = ffBase64EncodeStrbuf(&buf);
  48    ffStrbufClear(&buf);
  49
  50    if (!options->width || !options->height) {
  51        if (options->position == FF_LOGO_POSITION_LEFT) {
  52            ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;%uH", (unsigned) options->paddingTop + 1, (unsigned) options->paddingLeft + 1);
  53        } else if (options->position == FF_LOGO_POSITION_TOP) {
  54            ffStrbufAppendNC(&buf, options->paddingTop, '\n');
  55            ffStrbufAppendNC(&buf, options->paddingLeft, ' ');
  56        } else if (options->position == FF_LOGO_POSITION_RIGHT) {
  57            if (!options->width) {
  58                if (printError) {
  59                    fputs("Logo (iterm): Must set logo width when using position right\n", stderr);
  60                }
  61                return false;
  62            }
  63            ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;9999999H\e[%uD", (unsigned) options->paddingTop + 1, (unsigned) options->paddingRight + options->width);
  64        }
  65        if (inTmux) {
  66            ffStrbufAppendS(&buf, "\ePtmux;\e");
  67        }
  68
  69        if (options->width) {
  70            ffStrbufAppendF(&buf, "\e]1337;File=inline=1;width=%u:%s\a", (unsigned) options->width, base64.chars);
  71        } else {
  72            ffStrbufAppendF(&buf, "\e]1337;File=inline=1:%s\a", base64.chars);
  73        }
  74        if (inTmux) {
  75            ffStrbufAppendS(&buf, "\e\\");
  76        }
  77        ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
  78
  79        if (options->position == FF_LOGO_POSITION_LEFT || options->position == FF_LOGO_POSITION_RIGHT) {
  80            uint16_t X = 0, Y = 0;
  81            const char* error = ffGetTerminalResponse("\e[6n", 2, "%*[^0-9]%hu;%huR", &Y, &X);
  82            if (error) {
  83                fprintf(stderr, "\nLogo (iterm): fail to query cursor position: %s\n", error);
  84                return true; // We already printed image logo, don't print ascii logo then
  85            }
  86            if (X < options->paddingLeft + options->width) {
  87                X = (uint16_t) (options->paddingLeft + options->width);
  88            }
  89            if (options->position == FF_LOGO_POSITION_LEFT) {
  90                instance.state.logoWidth = X + options->paddingRight - 1;
  91            }
  92            instance.state.logoHeight = Y;
  93            // Work around an iTerm bug. Without the leading whitespace the image logo may be moved out of view
  94            // Found in iTerm 3.7.2
  95            fputs(" \e[H", stdout);
  96        } else if (options->position == FF_LOGO_POSITION_TOP) {
  97            instance.state.logoWidth = instance.state.logoHeight = 0;
  98            ffPrintCharTimes('\n', options->paddingRight);
  99        }
 100    } else {
 101        ffStrbufAppendNC(&buf, options->paddingTop, '\n');
 102        if (options->position == FF_LOGO_POSITION_RIGHT) {
 103            ffStrbufAppendF(&buf, "\e[9999999C\e[%uD", (unsigned) options->paddingRight + options->width);
 104        } else if (options->paddingLeft) {
 105            ffStrbufAppendF(&buf, "\e[%uC", (unsigned) options->paddingLeft);
 106        }
 107        if (inTmux) {
 108            ffStrbufAppendS(&buf, "\ePtmux;\e");
 109        }
 110        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);
 111        if (inTmux) {
 112            ffStrbufAppendS(&buf, "\e\\");
 113        }
 114        ffStrbufAppendC(&buf, '\n');
 115
 116        if (options->position == FF_LOGO_POSITION_LEFT) {
 117            instance.state.logoWidth = options->width + options->paddingLeft + options->paddingRight;
 118            instance.state.logoHeight = options->paddingTop + options->height;
 119            ffStrbufAppendF(&buf, "\e[%uA", (unsigned) instance.state.logoHeight);
 120        } else if (options->position == FF_LOGO_POSITION_TOP) {
 121            instance.state.logoWidth = instance.state.logoHeight = 0;
 122            ffStrbufAppendNC(&buf, options->paddingRight, '\n');
 123        } else if (options->position == FF_LOGO_POSITION_RIGHT) {
 124            instance.state.logoWidth = instance.state.logoHeight = 0;
 125            ffStrbufAppendF(&buf, "\e[1G\e[%uA", (unsigned) options->height);
 126        }
 127        ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
 128    }
 129
 130    return true;
 131}
 132
 133// `kitten icat` parses a cursor positioning sequence it may have written: `\e[<n>C` (relative) or
 134// `\e[<row>;<col>H` (absolute). Returns 1 for the first form, 2 for the second one, 0 for anything
 135// else, in which case nothing is written back.
 136static int parseIcatCsi(const char* data, uint32_t length, uint32_t pos, uint32_t* end, uint32_t* first, uint32_t* second) {
 137    if (pos + 2 >= length || data[pos] != '\e' || data[pos + 1] != '[') {
 138        return 0;
 139    }
 140
 141    uint32_t values[2] = { 0, 0 };
 142    uint32_t count = 0;
 143    uint32_t i = pos + 2;
 144    while (i < length) {
 145        char c = data[i];
 146        if (c >= '0' && c <= '9') {
 147            if (count == 0) {
 148                count = 1;
 149            }
 150            values[count - 1] = values[count - 1] * 10 + (uint32_t) (c - '0');
 151            ++i;
 152        } else if (c == ';' && count == 1) {
 153            count = 2;
 154            ++i;
 155        } else {
 156            break;
 157        }
 158    }
 159
 160    if (i >= length) {
 161        return 0;
 162    }
 163
 164    if (data[i] == 'C' && count == 1) {
 165        *end = i + 1;
 166        *first = values[0];
 167        *second = 0;
 168        return 1;
 169    }
 170
 171    if (data[i] == 'H' && count == 2) {
 172        *end = i + 1;
 173        *first = values[0];
 174        *second = values[1];
 175        return 2;
 176    }
 177
 178    return 0;
 179}
 180
 181// `kitten icat` positions the image itself: it always writes a carriage return, followed by at most
 182// one positioning sequence, and then the graphics escape. The carriage return alone would pull the
 183// cursor back to column 1 and undo the padding, and the absolute cursor move `kitten` writes when
 184// `--place` is used points at screen coordinates it picked itself, which are not necessarily the ones
 185// the padding asks for. Both are dropped here and the position is left to the caller, which has
 186// already moved the cursor to where the padding asks for. The horizontal centering `kitten` applied
 187// inside the requested rectangle is kept, but re-applied relatively, so it still follows the caller's
 188// padding.
 189static void appendIcatOutput(FFstrbuf* buf, const FFstrbuf* output, const FFOptionsLogo* options) {
 190    const char* data = output->chars;
 191    uint32_t pos = 0;    // Start of the graphics escape; 0 means "layout not recognised"
 192    uint32_t offset = 0; // Column offset `kitten` applied inside the requested rectangle
 193
 194    if (output->length > 0 && data[0] == '\r') {
 195        uint32_t cursor = 1;
 196        uint32_t end = 0, first = 0, second = 0;
 197        int kind = parseIcatCsi(data, output->length, cursor, &end, &first, &second);
 198        if (kind == 1) { // Relative move, written instead of the absolute one when `--place` is not used
 199            offset = first;
 200            cursor = end;
 201        } else if (kind == 2 && second >= options->paddingLeft + 1) { // Absolute move, produced by `--place`
 202            // The row is dropped along with the move, so it does not matter which one `kitten` picked.
 203            // The column is where the padding put it, plus the centering, which is all that is kept.
 204            offset = second - options->paddingLeft - 1;
 205            cursor = end;
 206        }
 207
 208        // The graphics escape has to follow once the positioning is gone
 209        if (cursor + 1 < output->length && data[cursor] == '\e' && data[cursor + 1] == '_') {
 210            pos = cursor;
 211        }
 212    }
 213
 214    if (pos == 0) {
 215        // Unrecognised layout: keep it as is, positioning included
 216        ffStrbufAppend(buf, output);
 217        return;
 218    }
 219
 220    if (offset > 0) {
 221        // The cursor is already at the position the padding asks for
 222        ffStrbufAppendF(buf, "\e[%uC", (unsigned) offset);
 223    }
 224    ffStrbufAppendNS(buf, output->length - pos, data + pos);
 225}
 226
 227static bool printImageKittyIcat(bool printError) {
 228    const FFOptionsLogo* options = &instance.config.logo;
 229
 230    if (!ffPathExists(options->source.chars, FF_PATHTYPE_FILE)) {
 231        if (printError) {
 232            fputs("Logo (kitty-icat): Failed to load image file\n", stderr);
 233        }
 234        return false;
 235    }
 236
 237    fflush(stdout);
 238
 239    FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
 240
 241    if (options->position == FF_LOGO_POSITION_LEFT) {
 242        ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;%uH", (unsigned) options->paddingTop + 1, (unsigned) options->paddingLeft + 1);
 243    } else if (options->position == FF_LOGO_POSITION_TOP) {
 244        if (!options->width) {
 245            ffStrbufAppendNC(&buf, options->paddingTop, '\n');
 246            ffStrbufAppendNC(&buf, options->paddingLeft, ' ');
 247        } else {
 248            if (printError) {
 249                fputs("Logo (kitty-icat): position top is not supported when logo width is set\n", stderr);
 250            }
 251            return false;
 252        }
 253    } else if (options->position == FF_LOGO_POSITION_RIGHT) {
 254        if (printError) {
 255            fputs("Logo (kitty-icat): position right is not supported\n", stderr);
 256        }
 257        return false;
 258    }
 259
 260    // `kitten icat` writes its own positioning, so its output is collected separately and only
 261    // appended once that positioning has been dropped (see appendIcatOutput)
 262    FF_STRBUF_AUTO_DESTROY icatOutput = ffStrbufCreate();
 263
 264    const char* error = nullptr;
 265
 266    if (options->width) {
 267        // `--place` measures `left` and `top` from the top left corner of the screen, so the padding is
 268        // passed through unchanged. The move `kitten` writes for it is dropped again in appendIcatOutput,
 269        // which leaves the position to the caller, so this is not what puts the image at the padding in
 270        // the normal case. It is what keeps the fallback there working: when the output layout is not
 271        // recognised - as under tmux, where `kitten` switches to unicode placeholders - its own
 272        // positioning is kept, and this is what then places the image at the padding.
 273        char place[64];
 274        snprintf(place,
 275            ARRAY_SIZE(place),
 276            "--place=%ux%u@%ux%u",
 277            options->width,
 278            options->height == 0 ? 9999 : options->height,
 279            options->paddingLeft,
 280            options->paddingTop);
 281
 282        error = ffProcessAppendStdOut(&icatOutput, (char*[]) {
 283                                                "kitten",
 284                                                "icat",
 285                                                "-n",
 286                                                "--stdin=no",
 287                                                "--align=center",
 288                                                place,
 289                                                "--scale-up",
 290                                                options->source.chars,
 291                                                nullptr,
 292                                            });
 293    } else {
 294        error = ffProcessAppendStdOut(&icatOutput, (char*[]) {
 295                                                "kitten",
 296                                                "icat",
 297                                                "-n",
 298                                                "--stdin=no",
 299                                                "--align=left",
 300                                                options->source.chars,
 301                                                nullptr,
 302                                            });
 303    }
 304    if (error) {
 305        if (printError) {
 306            fprintf(stderr, "Logo (kitty-icat): running `kitten icat` failed %s\n", error);
 307        }
 308        return false;
 309    }
 310
 311    if (icatOutput.length == 0) {
 312        if (printError) {
 313            fputs("Logo (kitty-icat): `kitten icat` returned empty output\n", stderr);
 314        }
 315        return false;
 316    }
 317
 318    appendIcatOutput(&buf, &icatOutput, options);
 319
 320    ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
 321
 322    if (options->position == FF_LOGO_POSITION_LEFT || options->position == FF_LOGO_POSITION_RIGHT) {
 323        uint16_t X = 0, Y = 0;
 324        const char* error = ffGetTerminalResponse("\e[6n", 2, "%*[^0-9]%hu;%huR", &Y, &X);
 325        if (error) {
 326            fprintf(stderr, "\nLogo (kitty-icat): fail to query cursor position: %s\n", error);
 327            return true; // We already printed image logo, don't print ascii logo then
 328        }
 329        if (X < options->paddingLeft + options->width) {
 330            X = (uint16_t) (options->paddingLeft + options->width);
 331        }
 332        if (options->position == FF_LOGO_POSITION_LEFT) {
 333            instance.state.logoWidth = X + options->paddingRight - 1;
 334        }
 335        instance.state.logoHeight = Y;
 336        fputs("\e[H", stdout);
 337    } else if (options->position == FF_LOGO_POSITION_TOP) {
 338        instance.state.logoWidth = instance.state.logoHeight = 0;
 339        ffPrintCharTimes('\n', options->paddingRight);
 340    }
 341
 342    return true;
 343}
 344
 345static bool printImageKittyDirect(bool printError) {
 346    const FFOptionsLogo* options = &instance.config.logo;
 347
 348    if (!ffPathExists(options->source.chars, FF_PATHTYPE_FILE)) {
 349        if (printError) {
 350            fputs("Logo (kitty-direct): Failed to load image file\n", stderr);
 351        }
 352        return false;
 353    }
 354
 355    fflush(stdout);
 356
 357    bool inTmux = false;
 358    {
 359        const char* term = getenv("TERM");
 360        inTmux = term && (ffStrStartsWith(term, "screen") || ffStrStartsWith(term, "tmux"));
 361    }
 362
 363    FF_STRBUF_AUTO_DESTROY base64 = ffBase64EncodeStrbuf(&options->source);
 364    FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
 365
 366    if (!options->width || !options->height) {
 367        if (options->position == FF_LOGO_POSITION_LEFT) {
 368            // We must clear the entre screen to make sure that terminal buffer won't scroll up
 369            ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;%uH", (unsigned) options->paddingTop + 1, (unsigned) options->paddingLeft + 1);
 370        } else if (options->position == FF_LOGO_POSITION_TOP) {
 371            ffStrbufAppendNC(&buf, options->paddingTop, '\n');
 372            ffStrbufAppendNC(&buf, options->paddingLeft, ' ');
 373        } else if (options->position == FF_LOGO_POSITION_RIGHT) {
 374            if (!options->width) {
 375                if (printError) {
 376                    fputs("Logo (iterm): Must set logo width when using position right\n", stderr);
 377                }
 378                return false;
 379            }
 380            ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;9999999H\e[%uD", (unsigned) options->paddingTop + 1, (unsigned) options->paddingRight + options->width);
 381        }
 382
 383        if (inTmux) {
 384            ffStrbufAppendS(&buf, "\ePtmux;\e");
 385        }
 386        if (options->width) {
 387            ffStrbufAppendF(&buf, "\e_Ga=T,f=100,t=f,c=%u;%s", (unsigned) options->width, base64.chars);
 388        } else {
 389            ffStrbufAppendF(&buf, "\e_Ga=T,f=100,t=f;%s", base64.chars);
 390        }
 391        if (inTmux) {
 392            ffStrbufAppendC(&buf, '\e');
 393        }
 394        ffStrbufAppendS(&buf, "\e\\");
 395        if (inTmux) {
 396            ffStrbufAppendS(&buf, "\e\\");
 397        }
 398
 399        ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
 400
 401        if (options->position == FF_LOGO_POSITION_LEFT || options->position == FF_LOGO_POSITION_RIGHT) {
 402            uint16_t X = 0, Y = 0;
 403            const char* error = ffGetTerminalResponse("\e[6n", 2, "%*[^0-9]%hu;%huR", &Y, &X);
 404            if (error) {
 405                if (printError) {
 406                    fprintf(stderr, "\nLogo (kitty-direct): fail to query cursor position: %s\n", error);
 407                }
 408                return true; // We already printed image logo, don't print ascii logo then
 409            }
 410            if (X < options->paddingLeft + options->width) {
 411                X = (uint16_t) (options->paddingLeft + options->width);
 412            }
 413            if (options->position == FF_LOGO_POSITION_LEFT) {
 414                instance.state.logoWidth = X + options->paddingRight - 1;
 415            }
 416            instance.state.logoHeight = Y;
 417            fputs("\e[H", stdout);
 418        } else if (options->position == FF_LOGO_POSITION_TOP) {
 419            instance.state.logoWidth = instance.state.logoHeight = 0;
 420            ffPrintCharTimes('\n', options->paddingRight);
 421        }
 422    } else {
 423        ffStrbufAppendNC(&buf, options->paddingTop, '\n');
 424
 425        if (options->position == FF_LOGO_POSITION_RIGHT) {
 426            ffStrbufAppendF(&buf, "\e[9999999C\e[%uD", (unsigned) options->paddingRight + options->width);
 427        } else if (options->paddingLeft) {
 428            ffStrbufAppendF(&buf, "\e[%uC", (unsigned) options->paddingLeft);
 429        }
 430
 431        if (inTmux) {
 432            ffStrbufAppendS(&buf, "\ePtmux;\e");
 433        }
 434
 435        ffStrbufAppendF(&buf, "\e_Ga=T,f=100,t=f,c=%u,r=%u;%s\e\\", (unsigned) options->width, (unsigned) options->height, base64.chars);
 436        if (inTmux) {
 437            ffStrbufAppendS(&buf, "\e\\");
 438        }
 439        ffStrbufAppendC(&buf, '\n');
 440        if (options->position == FF_LOGO_POSITION_LEFT) {
 441            instance.state.logoWidth = options->width + options->paddingLeft + options->paddingRight;
 442            instance.state.logoHeight = options->paddingTop + options->height;
 443            ffStrbufAppendF(&buf, "\e[%uA", (unsigned) instance.state.logoHeight);
 444        } else if (options->position == FF_LOGO_POSITION_TOP) {
 445            instance.state.logoWidth = instance.state.logoHeight = 0;
 446            ffStrbufAppendNC(&buf, options->paddingRight, '\n');
 447        } else if (options->position == FF_LOGO_POSITION_RIGHT) {
 448            instance.state.logoWidth = instance.state.logoHeight = 0;
 449            ffStrbufAppendF(&buf, "\e[1G\e[%uA", (unsigned) options->height);
 450        }
 451
 452        ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
 453    }
 454
 455    return true;
 456}
 457
 458#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(__APPLE__) || defined(__ANDROID__) || defined(FF_HAVE_SIXEL)
 459
 460    #define FF_KITTY_MAX_CHUNK_SIZE 4096
 461
 462    #define FF_CACHE_FILE_HEIGHT "height"
 463    #define FF_CACHE_FILE_WIDTH "width"
 464    #define FF_CACHE_FILE_SIXEL "sixel"
 465    #define FF_CACHE_FILE_KITTY_COMPRESSED "kittyc"
 466    #define FF_CACHE_FILE_KITTY_UNCOMPRESSED "kittyu"
 467    // Animation payload. The envelope can not be cached because it carries the image id, and that
 468    // id has to differ on every run; this entry holds only what is id-independent (see §7.2).
 469    #define FF_CACHE_FILE_KITTY_ANIMATION "kittyanim"
 470    #define FF_CACHE_FILE_CHAFA "chafa"
 471    // The frame the payloads in the same directory were rendered from. A selected frame is stored
 472    // exactly like the first frame is -- same entries, same payload format -- so this sidecar is
 473    // what tells the two apart, and what makes a change of --logo-animation-frame refresh the entry
 474    // instead of being served the frame that was asked for last time. It can not be part of the
 475    // entry name: the frame number ranges over the whole int32, and removeCachedFiles works from a
 476    // fixed list that could never enumerate it.
 477    // The entry sits next to every payload of the directory, so it describes whichever of them was
 478    // written last. Switching --logo-protocol between two protocols that both have a payload here,
 479    // and asking for different frames, can therefore pair one protocol's payload with the other's
 480    // frame number; the cache has to be refreshed after such a switch (`--logo-cache regen`).
 481    #define FF_CACHE_FILE_FRAME "frame"
 482    // Modification time of the image source the entry was produced from. Written last, so an
 483    // entry that was interrupted mid-write is never mistaken for a complete one.
 484    #define FF_CACHE_FILE_MTIME "mtime"
 485
 486    #include <stdlib.h>
 487    #include <string.h>
 488    #include <unistd.h>
 489    #include <fcntl.h>
 490
 491    #include "common/time.h"
 492    #include "common/debug.h"
 493
 494    #ifndef _WIN32
 495        #include <sys/ioctl.h>
 496    #else
 497        #include <wincon.h>
 498        #include "common/path.h"
 499    #endif
 500
 501    #ifdef FF_HAVE_ZLIB
 502        #include "common/zlib.h"
 503
 504static bool compressBlob(void** blob, size_t* length) {
 505    const char* error = ffZlibLoad(FF_ZLIB_COMPRESS);
 506    if (error != nullptr) {
 507        FF_DEBUG("The logo stays uncompressed: %s", error);
 508        return false;
 509    }
 510
 511        #if _WIN32
 512    // zlib's uLong is 32-bit on Windows (LLP64), so a >4 GiB source can't be
 513    // compressed through this API; reject it instead of silently truncating
 514    if (*length > (size_t) ULONG_MAX) {
 515        return false;
 516    }
 517        #endif
 518
 519    uLong compressedLength = compressBound((uLong) *length);
 520    void* compressed = malloc(compressedLength);
 521    if (compressed == nullptr) {
 522        return false;
 523    }
 524
 525    // Level 6, not 9. Measured on a 44 frame 400x400 GIF (2026-09-16): the level 9 encode is 573 ms
 526    // of the 720 ms the whole kitty animation path takes, i.e. it dwarfs decoding and the per frame
 527    // export combined. Level 6 does the same job in a fifth of the time for 4.9% more bytes, which is
 528    // also the level kitten icat uses. Level 1 would halve it again but doubles the payload, and past
 529    // that point the terminal is the slow one, not us.
 530    // The gap is far wider on Windows, where the same frames cost 37.7 ms each at level 9 against
 531    // 6.4 at level 6: raising this back to 9 now that the DLL name above actually loads is a 4.2x
 532    // slowdown there (398 ms -> 1692 ms). Re-measure both platforms before touching it.
 533    if (compress2(compressed, &compressedLength, *blob, (uLong) *length, Z_DEFAULT_COMPRESSION) != Z_OK) {
 534        free(compressed);
 535        return false;
 536    }
 537
 538    free(*blob);
 539
 540    *length = (size_t) compressedLength;
 541    *blob = compressed;
 542    return true;
 543}
 544
 545    #endif // FF_HAVE_ZLIB
 546
 547static void writeCacheData(FFLogoRequestData* requestData, const void* value, size_t len, const char* cacheFileName) {
 548    // Every payload file goes through here, including the ones written from the printing helpers,
 549    // so this is the single place that keeps `--logo-cache false` from writing anything at all.
 550    if (instance.config.logo.cache == FF_LOGO_CACHE_OFF) {
 551        return;
 552    }
 553
 554    uint32_t cacheDirLength = requestData->cacheDir.length;
 555    ffStrbufAppendS(&requestData->cacheDir, cacheFileName);
 556    ffWriteFileData(requestData->cacheDir.chars, len, value);
 557    ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
 558}
 559
 560// The character dimensions describe the source, not one particular rendering of it, so they live
 561// in their own entries. The animation path reuses them without writing a payload of its own.
 562static void writeImageSizeCache(FFLogoRequestData* requestData) {
 563    const FFOptionsLogo* options = &instance.config.logo;
 564    if (options->width == 0) {
 565        writeCacheData(requestData, &requestData->logoCharacterWidth, sizeof(requestData->logoCharacterWidth), FF_CACHE_FILE_WIDTH);
 566    }
 567
 568    if (options->height == 0) {
 569        writeCacheData(requestData, &requestData->logoCharacterHeight, sizeof(requestData->logoCharacterHeight), FF_CACHE_FILE_HEIGHT);
 570    }
 571}
 572
 573// Records which frame the payload written alongside it came from. Every payload write is followed
 574// by this one, so asking for another frame refreshes the entry instead of being served the frame
 575// that was asked for last time. What goes in is the *requested* selector, not the index it resolves
 576// to: on a single-frame source every selector resolves to frame 0, and storing that would make
 577// every selector but one miss on every run.
 578static void writeFrameCache(FFLogoRequestData* requestData) {
 579    const int32_t frame = instance.config.logo.animationFrame;
 580    writeCacheData(requestData, &frame, sizeof(frame), FF_CACHE_FILE_FRAME);
 581}
 582
 583static void printImageResult(FFLogoRequestData* requestData, const FFstrbuf* result) {
 584    const FFOptionsLogo* options = &instance.config.logo;
 585    // Write result to stdout
 586    ffPrintCharTimes('\n', options->paddingTop);
 587    if (options->position == FF_LOGO_POSITION_RIGHT) {
 588        printf("\e[9999999C\e[%uD", (unsigned) options->paddingRight + requestData->logoCharacterWidth);
 589    } else if (options->paddingLeft) {
 590        printf("\e[%uC", (unsigned) options->paddingLeft);
 591    }
 592    fflush(stdout);
 593    ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), result);
 594
 595    if (options->position != FF_LOGO_POSITION_TOP) {
 596        // Go to upper left corner
 597        printf("\e[1G\e[%uA", instance.state.logoHeight);
 598    }
 599
 600    if (options->position != FF_LOGO_POSITION_LEFT) {
 601        instance.state.logoWidth = instance.state.logoHeight = 0;
 602    }
 603}
 604
 605static void printImagePixels(FFLogoRequestData* requestData, const FFstrbuf* result, const char* cacheFileName) {
 606    const FFOptionsLogo* options = &instance.config.logo;
 607    // Calculate character dimensions
 608    instance.state.logoWidth = requestData->logoCharacterWidth + options->paddingLeft + options->paddingRight;
 609    instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop - 1;
 610
 611    // Write cache files
 612    writeCacheData(requestData, result->chars, result->length, cacheFileName);
 613    writeImageSizeCache(requestData);
 614
 615    printImageResult(requestData, result);
 616}
 617
 618// Same as printImagePixels, but the payload is not written to the cache: the bytes handed to the
 619// terminal are not the cached ones. Used by the animation path, whose envelope carries the image
 620// id and therefore differs on every run, and which stores its frames in the `kittyanim` entry
 621// instead.
 622static void printImagePixelsNoCache(FFLogoRequestData* requestData, const FFstrbuf* result) {
 623    const FFOptionsLogo* options = &instance.config.logo;
 624    // Calculate character dimensions
 625    instance.state.logoWidth = requestData->logoCharacterWidth + options->paddingLeft + options->paddingRight;
 626    instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop - 1;
 627
 628    writeImageSizeCache(requestData);
 629
 630    printImageResult(requestData, result);
 631}
 632
 633// The backends report the real pixel dimensions; the character dimensions are derived here
 634static void fillCharacterDimensions(FFLogoRequestData* requestData) {
 635    requestData->logoCharacterWidth = (uint32_t) ceil((double) requestData->logoPixelWidth / requestData->characterPixelWidth);
 636    requestData->logoCharacterHeight = (uint32_t) ceil((double) requestData->logoPixelHeight / requestData->characterPixelHeight);
 637    resolveImageAutoPosition(requestData->logoCharacterWidth);
 638}
 639
 640static bool printImageSixel(FFLogoRequestData* requestData, const FFstrbuf* result) {
 641    if (result->length == 0) {
 642        return false;
 643    }
 644
 645    printImagePixels(requestData, result, FF_CACHE_FILE_SIXEL);
 646    writeFrameCache(requestData);
 647    return true;
 648}
 649
 650// Appends one chunk of a chunked graphics command. `printEscapeCode` is false for the first chunk
 651// only, because the caller has already written the escape introducer and the control data.
 652// `controlPrefix` adds keys in front of `m`; animation frame data needs it on continuation chunks
 653// ("When sending animation frame data, subsequent chunks must also specify the a=f key"), the
 654// static path passes nullptr and is therefore unchanged.
 655static void appendKittyChunk(FFstrbuf* result, const char** blob, size_t* length, bool printEscapeCode, const char* controlPrefix) {
 656    uint32_t chunkSize = *length > FF_KITTY_MAX_CHUNK_SIZE ? FF_KITTY_MAX_CHUNK_SIZE : (uint32_t) *length;
 657
 658    if (printEscapeCode) {
 659        ffStrbufAppendS(result, "\033_G");
 660    } else {
 661        ffStrbufAppendC(result, ',');
 662    }
 663
 664    if (controlPrefix) {
 665        ffStrbufAppendS(result, controlPrefix);
 666        ffStrbufAppendC(result, ',');
 667    }
 668
 669    ffStrbufAppendS(result, chunkSize != *length ? "m=1" : "m=0");
 670    ffStrbufAppendC(result, ';');
 671    ffStrbufAppendNS(result, chunkSize, *blob);
 672    ffStrbufAppendS(result, "\033\\");
 673    *length -= chunkSize;
 674    *blob += chunkSize;
 675}
 676
 677// The compressed and the uncompressed entry hold the same rendering and share one frame sidecar.
 678static bool printImageKitty(FFLogoRequestData* requestData, const FFImageBuffer* buffer) {
 679    size_t length = (size_t) buffer->width * buffer->height * 4;
 680    FF_AUTO_FREE void* blob = malloc(length);
 681    if (blob == nullptr) {
 682        return false;
 683    }
 684    memcpy(blob, buffer->data, length);
 685
 686    #ifdef FF_HAVE_ZLIB
 687    bool isCompressed = compressBlob(&blob, &length);
 688    #else
 689    bool isCompressed = false;
 690    #endif
 691
 692    // base64 output is 4 * ceil(length / 3) bytes, plus the terminating null byte
 693    FF_STRBUF_AUTO_DESTROY base64 = ffStrbufCreateA((uint32_t) (10 + length * 4 / 3));
 694    ffBase64EncodeRaw((uint32_t) length, (const char*) blob, &base64.length, base64.chars);
 695
 696    FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateA(base64.length + 1024);
 697
 698    const char* currentPos = base64.chars;
 699    size_t remainingLength = base64.length;
 700
 701    ffStrbufAppendF(&result, "\033_Ga=T,f=32,s=%u,v=%u", buffer->width, buffer->height);
 702    if (isCompressed) {
 703        ffStrbufAppendS(&result, ",o=z");
 704    }
 705    appendKittyChunk(&result, &currentPos, &remainingLength, false, nullptr);
 706    while (remainingLength > 0) {
 707        appendKittyChunk(&result, &currentPos, &remainingLength, true, nullptr);
 708    }
 709
 710    printImagePixels(requestData, &result, isCompressed ? FF_CACHE_FILE_KITTY_COMPRESSED : FF_CACHE_FILE_KITTY_UNCOMPRESSED);
 711    writeFrameCache(requestData);
 712    return true;
 713}
 714
 715    #ifdef FF_HAVE_CHAFA
 716        #include <chafa.h>
 717static bool printImageChafa(FFLogoRequestData* requestData, const FFImageBuffer* buffer) {
 718        #if _WIN32
 719    FF_LIBRARY_LOAD(chafa, false, "libchafa-0" FF_LIBRARY_EXTENSION, 0)
 720        #else
 721    FF_LIBRARY_LOAD(chafa, false, "libchafa" FF_LIBRARY_EXTENSION, 1)
 722        #endif
 723    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_new, false)
 724    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_apply_selectors, false)
 725    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_new, false)
 726    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_set_geometry, false)
 727    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_set_symbol_map, false)
 728    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_new, false)
 729    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_draw_all_pixels, false)
 730    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_print, false)
 731    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_unref, false)
 732    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_unref, false)
 733    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_unref, false)
 734
 735    ChafaSymbolMap* symbolMap = ffchafa_symbol_map_new();
 736    GError* error = nullptr;
 737    if (!ffchafa_symbol_map_apply_selectors(symbolMap, instance.config.logo.chafaSymbols.chars, &error)) {
 738        fputs(error->message, stderr);
 739    }
 740
 741    ChafaCanvasConfig* canvasConfig = ffchafa_canvas_config_new();
 742    ffchafa_canvas_config_set_geometry(canvasConfig, (gint) requestData->logoCharacterWidth, (gint) requestData->logoCharacterHeight);
 743    ffchafa_canvas_config_set_symbol_map(canvasConfig, symbolMap);
 744
 745    if (instance.config.logo.chafaFgOnly) {
 746        FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_fg_only_enabled);
 747        if (ffchafa_canvas_config_set_fg_only_enabled) {
 748            ffchafa_canvas_config_set_fg_only_enabled(canvasConfig, true);
 749        }
 750    }
 751    if (instance.config.logo.chafaCanvasMode < CHAFA_CANVAS_MODE_MAX) {
 752        FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_canvas_mode);
 753        if (ffchafa_canvas_config_set_canvas_mode) {
 754            ffchafa_canvas_config_set_canvas_mode(canvasConfig, (ChafaCanvasMode) instance.config.logo.chafaCanvasMode);
 755        }
 756    }
 757    if (instance.config.logo.chafaColorSpace < CHAFA_COLOR_SPACE_MAX) {
 758        FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_color_space)
 759        if (ffchafa_canvas_config_set_color_space) {
 760            ffchafa_canvas_config_set_color_space(canvasConfig, (ChafaColorSpace) instance.config.logo.chafaColorSpace);
 761        }
 762    }
 763    if (instance.config.logo.chafaDitherMode < CHAFA_DITHER_MODE_MAX) {
 764        FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_dither_mode)
 765        if (ffchafa_canvas_config_set_dither_mode) {
 766            ffchafa_canvas_config_set_dither_mode(canvasConfig, (ChafaDitherMode) instance.config.logo.chafaDitherMode);
 767        }
 768    }
 769
 770    ChafaCanvas* canvas = ffchafa_canvas_new(canvasConfig);
 771    ffchafa_canvas_draw_all_pixels(
 772        canvas,
 773        CHAFA_PIXEL_RGBA8_UNASSOCIATED,
 774        buffer->data,
 775        (gint) buffer->width,
 776        (gint) buffer->height,
 777        (gint) buffer->width * 4);
 778
 779    GString* str = ffchafa_canvas_print(canvas, nullptr);
 780    FFstrbuf result;
 781    result.allocated = (uint32_t) str->allocated_len;
 782    result.length = (uint32_t) str->len;
 783    result.chars = str->str;
 784
 785    ffLogoPrintChars(result.chars, false);
 786    writeCacheData(requestData, result.chars, result.length, FF_CACHE_FILE_CHAFA);
 787    writeFrameCache(requestData);
 788
 789    // FIXME: These functions must be imported from `libglib` dlls on Windows
 790    FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, g_string_free);
 791    if (ffg_string_free) {
 792        ffg_string_free(str, TRUE);
 793    }
 794    if (error) {
 795        FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, g_error_free)
 796        if (ffg_error_free) {
 797            ffg_error_free(error);
 798        }
 799    }
 800
 801    ffchafa_canvas_unref(canvas);
 802    ffchafa_canvas_config_unref(canvasConfig);
 803    ffchafa_symbol_map_unref(symbolMap);
 804
 805    return true;
 806}
 807    #endif
 808
 809bool ffImageCreate(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) {
 810    #ifdef _WIN32
 811    return ffImageCreateWIC(requestData, out, error);
 812    #elif defined(__APPLE__)
 813    return ffImageCreateImageIO(requestData, out, error);
 814    #elif defined(__ANDROID__)
 815    return ffImageCreateAID(requestData, out, error);
 816    #else
 817        #ifdef FF_HAVE_IMAGEMAGICK7
 818    if (ffImageCreateIM7(requestData, out, error)) {
 819        return true;
 820    }
 821        #endif
 822        #ifdef FF_HAVE_IMAGEMAGICK6
 823    if (ffImageCreateIM6(requestData, out, error)) {
 824        return true;
 825    }
 826        #endif
 827    return false;
 828    #endif
 829}
 830
 831void ffImageDestroy(FFImageBuffer* buffer) {
 832    free(buffer->data);
 833    buffer->data = nullptr;
 834    buffer->width = 0;
 835    buffer->height = 0;
 836}
 837
 838void ffImageFrameDestroy(FFImageFrame* frame) {
 839    free(frame->data);
 840    frame->data = nullptr;
 841    frame->delayMs = 0;
 842}
 843
 844// The session itself is platform independent; a backend only has to fill it in through
 845// ffImageAnimationCreate and implement the two callbacks.
 846struct FFImageAnimation {
 847    uint32_t frameCount;
 848    int32_t loopCount;
 849    void* impl;
 850    bool (*getFrame)(FFImageAnimation* animation, uint32_t index, FFImageFrame* out, const char** error);
 851    void (*destroy)(FFImageAnimation* animation);
 852};
 853
 854FFImageAnimation* ffImageAnimationCreate(uint32_t frameCount, int32_t loopCount, void* impl,
 855    bool (*getFrame)(FFImageAnimation* animation, uint32_t index, FFImageFrame* out, const char** error),
 856    void (*destroy)(FFImageAnimation* animation)) {
 857    FFImageAnimation* animation = malloc(sizeof(*animation));
 858    if (animation == nullptr) {
 859        return nullptr;
 860    }
 861
 862    *animation = (FFImageAnimation) {
 863        .frameCount = frameCount,
 864        .loopCount = loopCount,
 865        .impl = impl,
 866        .getFrame = getFrame,
 867        .destroy = destroy,
 868    };
 869    return animation;
 870}
 871
 872void* ffImageAnimationGetImpl(const FFImageAnimation* animation) {
 873    return animation->impl;
 874}
 875
 876bool ffImageAnimationOpen(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error) {
 877    // The sessions belong to the backends: ImageIO composes frames internally, WIC does not and
 878    // has to keep a canvas, ImageMagick needs CoalesceImages. Windows, macOS and ImageMagick 7 are
 879    // wired up; ImageMagick 6 is deliberately left out (see image.h), and a build with none of them
 880    // keeps failing loudly rather than falling back to a still image.
 881    //
 882    // The order mirrors ffImageCreate: on macOS ImageIO is the decoder even when ImageMagick is
 883    // available too, so the animation has to come from the same place the stills do.
 884    #ifdef _WIN32
 885    return ffImageAnimationOpenWIC(requestData, out, error);
 886    #elif defined(__APPLE__)
 887    return ffImageAnimationOpenImageIO(requestData, out, error);
 888    #elif defined(__ANDROID__)
 889    return ffImageAnimationOpenAID(requestData, out, error);
 890    #elif defined(FF_HAVE_IMAGEMAGICK7)
 891    return ffImageAnimationOpenIM7(requestData, out, error);
 892    #else
 893    FF_UNUSED(requestData, out);
 894    *error = "the image source can not be animated by this build";
 895    return false;
 896    #endif
 897}
 898
 899uint32_t ffImageAnimationFrameCount(const FFImageAnimation* animation) {
 900    return animation->frameCount;
 901}
 902
 903int32_t ffImageAnimationLoopCount(const FFImageAnimation* animation) {
 904    return animation->loopCount;
 905}
 906
 907bool ffImageAnimationGetFrame(FFImageAnimation* animation, uint32_t index, FFImageFrame* out, const char** error) {
 908    if (index >= animation->frameCount) {
 909        *error = "the requested frame is out of range";
 910        return false;
 911    }
 912
 913    return animation->getFrame(animation, index, out, error);
 914}
 915
 916void ffImageAnimationClose(FFImageAnimation* animation) {
 917    if (animation == nullptr) {
 918        return;
 919    }
 920
 921    animation->destroy(animation);
 922    free(animation);
 923}
 924
 925bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const char** error) {
 926    // Windows (WIC), macOS (ImageIO) and Android (AImageDecoder) decode and resize to RGBA first,
 927    // then the embedded libsixel encoder takes over. Other platforms let ImageMagick encode
 928    // straight from the decoded image without an RGBA round trip.
 929    #if defined(_WIN32) || defined(__APPLE__) || defined(__ANDROID__)
 930        #ifdef FF_HAVE_SIXEL
 931    FFImageBuffer buffer = {};
 932    if (!ffImageCreate(requestData, &buffer, error)) {
 933        return false;
 934    }
 935    bool ok = ffSixelEncode(&buffer, out, error);
 936    ffImageDestroy(&buffer);
 937    return ok;
 938        #else
 939    FF_UNUSED(requestData, out);
 940    if (error) {
 941        *error = "sixel support is not compiled in";
 942    }
 943    return false;
 944        #endif
 945    #else
 946        #ifdef FF_HAVE_IMAGEMAGICK7
 947    if (ffImageSixelEncodeIM7(requestData, out, error)) {
 948        return true;
 949    }
 950        #endif
 951        #ifdef FF_HAVE_IMAGEMAGICK6
 952    if (ffImageSixelEncodeIM6(requestData, out, error)) {
 953        return true;
 954    }
 955        #endif
 956    return false;
 957    #endif
 958}
 959
 960bool ffImageSixelEncodeBuffer(const FFImageBuffer* buffer, FFstrbuf* out, const char** error) {
 961    // Windows, macOS and Android hand the pixels to the embedded encoder. ImageMagick has no way to
 962    // encode pixels it was not given an Image for, so its SIXEL coder is reached through a
 963    // ConstituteImage round trip instead.
 964    #if defined(_WIN32) || defined(__APPLE__) || defined(__ANDROID__)
 965        #ifdef FF_HAVE_SIXEL
 966    return ffSixelEncode(buffer, out, error);
 967        #else
 968    FF_UNUSED(buffer, out);
 969    if (error) {
 970        *error = "sixel support is not compiled in";
 971    }
 972    return false;
 973        #endif
 974    #else
 975        #ifdef FF_HAVE_IMAGEMAGICK7
 976    return ffImageSixelEncodeBufferIM7(buffer, out, error);
 977        #else
 978    // Only the animation path calls this, and only ImageMagick 7 has an animation backend, so a
 979    // build without it never gets here with something to encode.
 980    FF_UNUSED(buffer, out);
 981    if (error) {
 982        *error = "sixel support is not compiled in";
 983    }
 984    return false;
 985        #endif
 986    #endif
 987}
 988
 989static FFNativeFD getCacheFD(FFLogoRequestData* requestData, const char* fileName) {
 990    uint32_t cacheDirLength = requestData->cacheDir.length;
 991    ffStrbufAppendS(&requestData->cacheDir, fileName);
 992    #ifndef _WIN32
 993    int fd = open(requestData->cacheDir.chars, O_RDONLY
 994        #ifdef O_CLOEXEC
 995            | O_CLOEXEC
 996        #endif
 997    );
 998    #else
 999    wchar_t widePath[PATH_MAX];
1000    if (!NT_SUCCESS(RtlUTF8ToUnicodeN(widePath, sizeof(widePath), nullptr, requestData->cacheDir.chars, requestData->cacheDir.length + 1))) {
1001        return nullptr;
1002    }
1003    HANDLE fd = CreateFileW(widePath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
1004    #endif
1005    ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
1006    return fd;
1007}
1008
1009static bool readCachedStrbuf(FFLogoRequestData* requestData, FFstrbuf* result, const char* cacheFileName) {
1010    uint32_t cacheDirLength = requestData->cacheDir.length;
1011    ffStrbufAppendS(&requestData->cacheDir, cacheFileName);
1012    bool res = ffAppendFileBuffer(requestData->cacheDir.chars, result);
1013    ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
1014    return res;
1015}
1016
1017static bool readCachedData(FFLogoRequestData* requestData, void* buffer, size_t bufferSize, const char* cacheFileName) {
1018    uint32_t cacheDirLength = requestData->cacheDir.length;
1019    ffStrbufAppendS(&requestData->cacheDir, cacheFileName);
1020    bool res = ffReadFileData(requestData->cacheDir.chars, bufferSize, buffer) == (ssize_t) bufferSize;
1021    ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
1022    return res;
1023}
1024
1025static uint32_t readCachedUint32(FFLogoRequestData* requestData, const char* cacheFileName) {
1026    uint32_t result = 0;
1027    if (!readCachedData(requestData, &result, sizeof(result), cacheFileName)) {
1028        return 0;
1029    }
1030
1031    return result;
1032}
1033
1034static uint64_t readCachedUint64(FFLogoRequestData* requestData, const char* cacheFileName) {
1035    uint64_t result = 0;
1036    if (!readCachedData(requestData, &result, sizeof(result), cacheFileName)) {
1037        return 0;
1038    }
1039
1040    return result;
1041}
1042
1043// The frame the payload in the same directory was rendered from. A payload with no sidecar predates
1044// the sidecar and holds the first frame, which is exactly what the default selector asks for, so
1045// treating it as such keeps the caches of older builds usable.
1046static int32_t readCachedFrame(FFLogoRequestData* requestData, const char* frameFileName) {
1047    int32_t frame = FF_LOGO_ANIMATION_FRAME_FIRST;
1048    readCachedData(requestData, &frame, sizeof(frame), frameFileName);
1049    return frame;
1050}
1051
1052// Drops everything a previous version of the source left in the entry directory.
1053// The directory is keyed on the source path and the pixel size only, so it is reused across
1054// edits; without this, a payload written for another logo type would be read back.
1055static void removeCachedFiles(FFLogoRequestData* requestData) {
1056    static const char* const files[] = {
1057        FF_CACHE_FILE_MTIME,
1058        FF_CACHE_FILE_WIDTH,
1059        FF_CACHE_FILE_HEIGHT,
1060        FF_CACHE_FILE_SIXEL,
1061        FF_CACHE_FILE_KITTY_COMPRESSED,
1062        FF_CACHE_FILE_KITTY_UNCOMPRESSED,
1063        FF_CACHE_FILE_KITTY_ANIMATION,
1064        FF_CACHE_FILE_CHAFA,
1065        FF_CACHE_FILE_FRAME,
1066    };
1067
1068    // Every entry that is ever written is listed here; an entry left out would survive a source
1069    // change and be read back for the new source.
1070    uint32_t cacheDirLength = requestData->cacheDir.length;
1071    for (uint32_t i = 0; i < ARRAY_SIZE(files); ++i) {
1072        ffStrbufAppendS(&requestData->cacheDir, files[i]);
1073        ffRemoveFile(requestData->cacheDir.chars);
1074        ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
1075    }
1076}
1077
1078static bool printCachedChars(FFLogoRequestData* requestData, const char* cacheFileName) {
1079    if (instance.config.logo.animationFrame != readCachedFrame(requestData, FF_CACHE_FILE_FRAME)) {
1080        // The entry holds the rendering of another frame, or -- for the animation selector -- of
1081        // no single frame at all. Reporting a miss here is what sends the slow path off to render
1082        // the one that was asked for.
1083        return false;
1084    }
1085
1086    FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate();
1087    readCachedStrbuf(requestData, &content, cacheFileName);
1088
1089    if (content.length == 0) {
1090        return false;
1091    }
1092
1093    ffLogoPrintChars(content.chars, false);
1094    return true;
1095}
1096
1097// ---------------------------------------------------------------------------------------------
1098// kitty animations
1099//
1100// A `kittyanim` entry holds only what is independent of the image id: the pixel size, the frame
1101// count, each frame's gap, the loop count, and the frames' base64 payloads. The envelope is
1102// rebuilt on every run, because it carries the id and that id has to differ between runs.
1103// ---------------------------------------------------------------------------------------------
1104
1105static const char FF_KITTY_ANIMATION_MAGIC[4] = { 'F', 'K', 'A', '1' };
1106
1107typedef struct FFKittyAnimationFrame {
1108    int32_t delayMs;
1109    uint32_t payloadLength; // base64 bytes this frame occupies inside the payload block
1110    bool compressed;        // the frame's base64 decodes to a zlib stream, so it needs `o=z`
1111} FFKittyAnimationFrame;
1112
1113typedef struct FFKittyAnimation {
1114    uint32_t width;
1115    uint32_t height;
1116    uint32_t frameCount;
1117    int32_t loopCount; // 0 = infinite; -1 = the source declares none
1118    FFKittyAnimationFrame* frames;
1119    char* payload; // base64 of every frame, concatenated in frame order
1120    uint32_t payloadLength;
1121} FFKittyAnimation;
1122
1123static void destroyKittyAnimation(FFKittyAnimation* animation) {
1124    free(animation->frames);
1125    free(animation->payload);
1126    animation->frames = nullptr;
1127    animation->payload = nullptr;
1128}
1129
1130// The entry is written and read by the same machine, so the host's own representation is used.
1131static void appendRaw(FFstrbuf* result, const void* value, uint32_t size) {
1132    ffStrbufAppendNS(result, size, (const char*) value);
1133}
1134
1135static void appendUint32(FFstrbuf* result, uint32_t value) {
1136    appendRaw(result, &value, sizeof(value));
1137}
1138
1139static void appendInt32(FFstrbuf* result, int32_t value) {
1140    appendRaw(result, &value, sizeof(value));
1141}
1142
1143typedef struct FFKittyAnimationReader {
1144    const char* data;
1145    uint32_t length;
1146    uint32_t offset;
1147} FFKittyAnimationReader;
1148
1149static bool readRaw(FFKittyAnimationReader* reader, void* out, uint32_t size) {
1150    if (size > reader->length - reader->offset) {
1151        return false;
1152    }
1153
1154    memcpy(out, reader->data + reader->offset, size);
1155    reader->offset += size;
1156    return true;
1157}
1158
1159static bool readUint32(FFKittyAnimationReader* reader, uint32_t* out) {
1160    return readRaw(reader, out, sizeof(*out));
1161}
1162
1163static bool readInt32(FFKittyAnimationReader* reader, int32_t* out) {
1164    return readRaw(reader, out, sizeof(*out));
1165}
1166
1167static void serializeKittyAnimation(FFstrbuf* result, const FFKittyAnimation* animation) {
1168    appendRaw(result, FF_KITTY_ANIMATION_MAGIC, sizeof(FF_KITTY_ANIMATION_MAGIC));
1169    appendUint32(result, animation->width);
1170    appendUint32(result, animation->height);
1171    appendUint32(result, animation->frameCount);
1172    appendInt32(result, animation->loopCount);
1173
1174    for (uint32_t i = 0; i < animation->frameCount; ++i) {
1175        const FFKittyAnimationFrame* frame = &animation->frames[i];
1176        uint8_t flags = frame->compressed ? 1 : 0;
1177        appendInt32(result, frame->delayMs);
1178        appendUint32(result, frame->payloadLength);
1179        appendRaw(result, &flags, sizeof(flags));
1180    }
1181
1182    appendRaw(result, animation->payload, animation->payloadLength);
1183}
1184
1185static bool parseKittyAnimation(const char* data, uint32_t length, FFKittyAnimation* out, const char** error) {
1186    FFKittyAnimationReader reader = { .data = data, .length = length, .offset = 0 };
1187
1188    char magic[sizeof(FF_KITTY_ANIMATION_MAGIC)];
1189    if (!readRaw(&reader, magic, sizeof(magic)) || memcmp(magic, FF_KITTY_ANIMATION_MAGIC, sizeof(magic)) != 0) {
1190        *error = "the cached animation has an unknown format";
1191        return false;
1192    }
1193
1194    if (!readUint32(&reader, &out->width) || !readUint32(&reader, &out->height) ||
1195        !readUint32(&reader, &out->frameCount) || !readInt32(&reader, &out->loopCount)) {
1196        *error = "the cached animation is truncated";
1197        return false;
1198    }
1199
1200    if (out->frameCount == 0) {
1201        *error = "the cached animation has no frames";
1202        return false;
1203    }
1204
1205    out->frames = calloc(out->frameCount, sizeof(*out->frames));
1206    if (out->frames == nullptr) {
1207        *error = "out of memory";
1208        return false;
1209    }
1210
1211    uint32_t totalPayloadLength = 0;
1212    for (uint32_t i = 0; i < out->frameCount; ++i) {
1213        FFKittyAnimationFrame* frame = &out->frames[i];
1214        uint8_t flags = 0;
1215        if (!readInt32(&reader, &frame->delayMs) || !readUint32(&reader, &frame->payloadLength) ||
1216            !readRaw(&reader, &flags, sizeof(flags))) {
1217            *error = "the cached animation is truncated";
1218            return false;
1219        }
1220
1221        frame->compressed = flags != 0;
1222
1223        if (totalPayloadLength > UINT32_MAX - frame->payloadLength) {
1224            *error = "the cached animation is too large";
1225            return false;
1226        }
1227        totalPayloadLength += frame->payloadLength;
1228    }
1229
1230    if (totalPayloadLength > reader.length - reader.offset) {
1231        *error = "the cached animation is truncated";
1232        return false;
1233    }
1234
1235    out->payloadLength = totalPayloadLength;
1236    out->payload = malloc((size_t) totalPayloadLength + 1);
1237    if (out->payload == nullptr) {
1238        *error = "out of memory";
1239        return false;
1240    }
1241
1242    memcpy(out->payload, reader.data + reader.offset, totalPayloadLength);
1243    out->payload[totalPayloadLength] = '\0';
1244    return true;
1245}
1246
1247// The id has to differ on every run: reusing one makes the terminal append the new frames to the
1248// image the previous run left in its storage, which doubles the frame count. The low 24 bits take
1249// part in the colour / decoration encoding, so they are kept non-zero (kitten icat does the same
1250// in next_random()). The entropy comes from the clock, the pid and a stack address, which is
1251// plenty for a process that runs once.
1252static uint32_t getKittyImageId(void) {
1253    uint32_t id = (uint32_t) ffTimeGetNow();
1254    id ^= (uint32_t) getpid() * 2654435761u;
1255    id ^= (uint32_t) ((uintptr_t) &id >> 4);
1256    id &= 0xFFFFFF;
1257    return id ?: 1;
1258}
1259
1260// The envelope, per the kitty protocol: the root frame is transmitted with `a=T` and has no gap
1261// of its own, so its gap is set with a separate `a=a` command; every further frame is an `a=f`
1262// command that replaces the pixels (`X=1`) because the backend already composed a full canvas.
1263// Every command carries the image id and `q=2`, so nothing is ever written back to the tty.
1264//
1265// `z` is a non-negative millisecond gap, so the backend's -1 ("gapless", see FFImageFrame) has to
1266// be clamped: emitting it verbatim is out of range, and a terminal that validates the key drops
1267// the whole command rather than the one key. -1 and 0 mean the same thing to a terminal that does
1268// not validate.
1269static int32_t getKittyGap(int32_t delayMs) {
1270    return delayMs > 0 ? delayMs : 0;
1271}
1272
1273static void emitKittyAnimation(FFstrbuf* result, const FFKittyAnimation* animation, uint32_t imageId) {
1274    const char* currentPos = animation->payload;
1275
1276    for (uint32_t i = 0; i < animation->frameCount; ++i) {
1277        const FFKittyAnimationFrame* frame = &animation->frames[i];
1278        size_t frameLength = frame->payloadLength;
1279
1280        if (i == 0) {
1281            // Here s / v are the source rectangle, not the animation state
1282            ffStrbufAppendF(result, "\033_Ga=T,f=32,s=%u,v=%u,i=%u,q=2", animation->width, animation->height, imageId);
1283        } else {
1284            // A frame that covers the whole image is transmitted exactly like image data, plus the
1285            // frame keys. s / v are not optional here even though they always repeat the image size:
1286            // the terminal sizes the frame's canvas from them and rejects the command outright when
1287            // they are missing.
1288            ffStrbufAppendF(result, "\033_Ga=f,f=32,s=%u,v=%u,i=%u,z=%d,X=1,q=2",
1289                animation->width, animation->height, imageId, (int) getKittyGap(frame->delayMs));
1290        }
1291
1292        if (frame->compressed) {
1293            ffStrbufAppendS(result, ",o=z");
1294        }
1295
1296        appendKittyChunk(result, &currentPos, &frameLength, false, nullptr);
1297        while (frameLength > 0) {
1298            // "When sending animation frame data, subsequent chunks must also specify the a=f key"
1299            appendKittyChunk(result, &currentPos, &frameLength, true, i == 0 ? "q=2" : "a=f,q=2");
1300        }
1301
1302        if (i == 0) {
1303            // "the first frame or root frame is created with the base image data and has no gap,
1304            // so its gap must be set using this control code"
1305            ffStrbufAppendF(result, "\033_Ga=a,i=%u,r=1,z=%d,q=2\033\\", imageId, (int) getKittyGap(frame->delayMs));
1306        }
1307    }
1308
1309    // v is the loop count here: 1 loops forever, N loops N-1 times
1310    int32_t loops = 1;
1311    if (animation->loopCount > 0) {
1312        loops = animation->loopCount < INT32_MAX ? animation->loopCount + 1 : INT32_MAX;
1313    }
1314    ffStrbufAppendF(result, "\033_Ga=a,i=%u,v=%d,q=2\033\\", imageId, (int) loops);
1315    ffStrbufAppendF(result, "\033_Ga=a,i=%u,s=3,q=2\033\\", imageId);
1316}
1317
1318static bool printCachedKittyAnimation(FFLogoRequestData* requestData) {
1319    FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate();
1320    if (!readCachedStrbuf(requestData, &content, FF_CACHE_FILE_KITTY_ANIMATION) || content.length == 0) {
1321        return false;
1322    }
1323
1324    FFKittyAnimation animation = {};
1325    const char* error = "the cached animation is corrupt";
1326    if (!parseKittyAnimation(content.chars, content.length, &animation, &error)) {
1327        // The parser allocates the frame table before it can still fail, so what it got to allocate
1328        // belongs to the caller either way.
1329        destroyKittyAnimation(&animation);
1330        return false;
1331    }
1332
1333    FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateA(animation.payloadLength + animation.frameCount * 128 + 512);
1334    emitKittyAnimation(&result, &animation, getKittyImageId());
1335
1336    const FFOptionsLogo* options = &instance.config.logo;
1337    instance.state.logoWidth = requestData->logoCharacterWidth + options->paddingLeft + options->paddingRight;
1338    instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop - 1;
1339    printImageResult(requestData, &result);
1340
1341    destroyKittyAnimation(&animation);
1342    return true;
1343}
1344
1345// Decodes the source frame by frame, encodes every frame, and stores the result as a `kittyanim`
1346// entry. Frames are taken, encoded and released one at a time: keeping them all would cost
1347// frames * W * H * 4 bytes, which a tool that prints a logo once can not afford.
1348static bool encodeKittyAnimation(FFLogoRequestData* requestData, const char** error) {
1349    FFImageAnimation* animation = nullptr;
1350    if (!ffImageAnimationOpen(requestData, &animation, error)) {
1351        return false;
1352    }
1353
1354    fillCharacterDimensions(requestData);
1355
1356    const uint32_t frameCount = ffImageAnimationFrameCount(animation);
1357    const int32_t loopCount = ffImageAnimationLoopCount(animation);
1358
1359    if (frameCount == 1) {
1360        // Nothing to animate, but the caller asked for an animation and that is what the cache has to
1361        // hold: the reader for an animation never falls back to a still entry (see §7.1), so a
1362        // `kittyanim` is what it looks for whoever wrote one. Writing the still pair instead would
1363        // leave the entry functionally complete -- the mtime goes down, removeCachedFiles() is
1364        // skipped from then on -- while every run re-decodes and re-encodes the source, because the
1365        // entry the reader wants is missing. One frame, encoded the same way an animation of one
1366        // frame would be, keeps both the bytes sent and the cache honest.
1367        FFImageFrame frame = {};
1368        bool ok = ffImageAnimationGetFrame(animation, 0, &frame, error);
1369        ffImageAnimationClose(animation);
1370        if (!ok) {
1371            return false;
1372        }
1373
1374        FFImageBuffer buffer = {
1375            .data = frame.data,
1376            .width = requestData->logoPixelWidth,
1377            .height = requestData->logoPixelHeight,
1378        };
1379
1380        const size_t frameSize = (size_t) requestData->logoPixelWidth * requestData->logoPixelHeight * 4;
1381        FF_AUTO_FREE void* blob = malloc(frameSize);
1382        if (blob == nullptr) {
1383            ffImageDestroy(&buffer);
1384            *error = "out of memory";
1385            return false;
1386        }
1387        memcpy(blob, frame.data, frameSize);
1388
1389        size_t blobLength = frameSize;
1390        bool compressed = false;
1391        #ifdef FF_HAVE_ZLIB
1392        compressed = compressBlob(&blob, &blobLength);
1393        #endif
1394
1395        FF_STRBUF_AUTO_DESTROY base64 = ffStrbufCreateA((uint32_t) (10 + blobLength * 4 / 3));
1396        ffBase64EncodeRaw((uint32_t) blobLength, (const char*) blob, &base64.length, base64.chars);
1397
1398        FFKittyAnimationFrame single = { .delayMs = frame.delayMs, .compressed = compressed, .payloadLength = base64.length };
1399        FFKittyAnimation built = {
1400            .width = requestData->logoPixelWidth,
1401            .height = requestData->logoPixelHeight,
1402            .frameCount = 1,
1403            .loopCount = loopCount,
1404            .frames = &single,
1405            .payload = base64.chars,
1406            .payloadLength = base64.length,
1407        };
1408
1409        FF_STRBUF_AUTO_DESTROY serialized = ffStrbufCreate();
1410        serializeKittyAnimation(&serialized, &built);
1411        writeCacheData(requestData, serialized.chars, serialized.length, FF_CACHE_FILE_KITTY_ANIMATION);
1412
1413        // The envelope carries the image id, so the bytes handed to the terminal are not the cached
1414        // ones and go out without a payload write. The still pair is written by printImageKitty --
1415        // it is what a still selector reads, and this frame is the frame every still selector asks
1416        // for. It runs before the buffer is released for that reason.
1417        printImageKitty(requestData, &buffer);
1418        ffImageDestroy(&buffer);
1419        return true;
1420    }
1421
1422    FFKittyAnimationFrame* frames = calloc(frameCount, sizeof(*frames));
1423    if (frames == nullptr) {
1424        ffImageAnimationClose(animation);
1425        *error = "out of memory";
1426        return false;
1427    }
1428
1429    const size_t frameSize = (size_t) requestData->logoPixelWidth * requestData->logoPixelHeight * 4;
1430    FF_STRBUF_AUTO_DESTROY payload = ffStrbufCreate();
1431    bool ok = true;
1432
1433    for (uint32_t i = 0; i < frameCount && ok; ++i) {
1434        FFImageFrame frame = {};
1435        if (!ffImageAnimationGetFrame(animation, i, &frame, error)) {
1436            ok = false;
1437            break;
1438        }
1439
1440        frames[i].delayMs = frame.delayMs;
1441
1442        FF_AUTO_FREE void* blob = malloc(frameSize);
1443        if (blob == nullptr) {
1444            ffImageFrameDestroy(&frame);
1445            *error = "out of memory";
1446            ok = false;
1447            break;
1448        }
1449        memcpy(blob, frame.data, frameSize);
1450        ffImageFrameDestroy(&frame);
1451
1452        size_t blobLength = frameSize;
1453        #ifdef FF_HAVE_ZLIB
1454        frames[i].compressed = compressBlob(&blob, &blobLength);
1455        #else
1456        frames[i].compressed = false;
1457        #endif
1458
1459        FF_STRBUF_AUTO_DESTROY base64 = ffStrbufCreateA((uint32_t) (10 + blobLength * 4 / 3));
1460        ffBase64EncodeRaw((uint32_t) blobLength, (const char*) blob, &base64.length, base64.chars);
1461        frames[i].payloadLength = base64.length;
1462        ffStrbufAppend(&payload, &base64);
1463    }
1464
1465    if (ok) {
1466        FFKittyAnimation built = {
1467            .width = requestData->logoPixelWidth,
1468            .height = requestData->logoPixelHeight,
1469            .frameCount = frameCount,
1470            .loopCount = loopCount,
1471            .frames = frames,
1472            .payload = payload.chars,
1473            .payloadLength = payload.length,
1474        };
1475
1476        FF_STRBUF_AUTO_DESTROY serialized = ffStrbufCreate();
1477        serializeKittyAnimation(&serialized, &built);
1478        writeCacheData(requestData, serialized.chars, serialized.length, FF_CACHE_FILE_KITTY_ANIMATION);
1479
1480        FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateA(serialized.length + frameCount * 128 + 512);
1481        emitKittyAnimation(&result, &built, getKittyImageId());
1482        printImagePixelsNoCache(requestData, &result);
1483    }
1484
1485    free(frames);
1486    ffImageAnimationClose(animation);
1487    return ok;
1488}
1489
1490// Maps --logo-animation-frame to a 0-based frame index. Negative values count back from the end,
1491// so -1 is the last frame. Anything out of range clamps instead of failing: the frame count of the
1492// source is only known once it has been opened, and a typo in a logo option is not worth aborting
1493// the run over (see §10.2).
1494static uint32_t getAnimationFrameIndex(int32_t requested, uint32_t frameCount) {
1495    if (frameCount == 0) {
1496        return 0;
1497    }
1498
1499    if (requested < 0) {
1500        uint32_t back = (uint32_t) (-(int64_t) requested);
1501        return back <= frameCount ? frameCount - back : 0;
1502    }
1503
1504    uint32_t index = (uint32_t) (requested - 1);
1505    return index < frameCount ? index : frameCount - 1;
1506}
1507
1508// Renders one frame of an animated source as a still image. `N` is 1-based and negative values
1509// count from the end, matching --logo-animation-frame. Every protocol that goes through this slow
1510// path is served: the decoded frame is handed to the same renderers the static path uses, and the
1511// result is cached exactly like the first frame is (see FF_CACHE_FILE_FRAME).
1512static bool printAnimationFrame(FFLogoRequestData* requestData, const char** error) {
1513    FFImageAnimation* animation = nullptr;
1514    if (!ffImageAnimationOpen(requestData, &animation, error)) {
1515        return false;
1516    }
1517
1518    fillCharacterDimensions(requestData);
1519
1520    const uint32_t frameCount = ffImageAnimationFrameCount(animation);
1521    FFImageFrame frame = {};
1522    bool ok = ffImageAnimationGetFrame(animation, getAnimationFrameIndex(instance.config.logo.animationFrame, frameCount), &frame, error);
1523    ffImageAnimationClose(animation);
1524    if (!ok) {
1525        return false;
1526    }
1527
1528    FFImageBuffer buffer = {
1529        .data = frame.data,
1530        .width = requestData->logoPixelWidth,
1531        .height = requestData->logoPixelHeight,
1532    };
1533
1534    if (requestData->type == FF_LOGO_TYPE_IMAGE_SIXEL) {
1535        // The sixel encoder belongs to the backend, so it does not go through ffImageCreate -- but
1536        // it does have to be handed the frame that was just composed. ffImageSixelEncode would
1537        // re-read the source and encode the first frame instead of the selected one.
1538        FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate();
1539        ok = ffImageSixelEncodeBuffer(&buffer, &result, error) && printImageSixel(requestData, &result);
1540    } else if (requestData->type == FF_LOGO_TYPE_IMAGE_KITTY) {
1541        ok = printImageKitty(requestData, &buffer);
1542    }
1543#if FF_HAVE_CHAFA
1544    else if (requestData->type == FF_LOGO_TYPE_IMAGE_CHAFA) {
1545        ok = printImageChafa(requestData, &buffer);
1546    }
1547#endif
1548    else {
1549        *error = "this image protocol can not render a selected frame";
1550        ok = false;
1551    }
1552
1553    ffImageDestroy(&buffer);
1554    return ok;
1555}
1556
1557static bool printCachedPixel(FFLogoRequestData* requestData) {
1558    FFOptionsLogo* options = &instance.config.logo;
1559
1560    requestData->logoCharacterWidth = options->width;
1561    if (requestData->logoCharacterWidth == 0) {
1562        requestData->logoCharacterWidth = readCachedUint32(requestData, FF_CACHE_FILE_WIDTH);
1563        if (requestData->logoCharacterWidth == 0) {
1564            return false;
1565        }
1566    }
1567
1568    requestData->logoCharacterHeight = options->height;
1569    if (requestData->logoCharacterHeight == 0) {
1570        requestData->logoCharacterHeight = readCachedUint32(requestData, FF_CACHE_FILE_HEIGHT);
1571        if (requestData->logoCharacterHeight == 0) {
1572            return false;
1573        }
1574    }
1575
1576    resolveImageAutoPosition(requestData->logoCharacterWidth);
1577
1578    if (requestData->type == FF_LOGO_TYPE_IMAGE_KITTY &&
1579        options->animationFrame == FF_LOGO_ANIMATION_FRAME_ANIMATE) {
1580        // An animation entry holds payloads, not an envelope, so it can not be streamed out like
1581        // the static ones. It is also never allowed to fall back to a static entry: doing so would
1582        // silently show a still logo to a user who asked for an animation.
1583        return printCachedKittyAnimation(requestData);
1584    }
1585
1586    // A selected frame shares the entry with the first frame; the sidecar is what tells the two
1587    // apart. This also covers the animation selector: no still rendering may answer it.
1588    if (options->animationFrame != readCachedFrame(requestData, FF_CACHE_FILE_FRAME)) {
1589        return false;
1590    }
1591
1592    FF_AUTO_CLOSE_FD FFNativeFD fd = FF_INVALID_FD;
1593    if (requestData->type == FF_LOGO_TYPE_IMAGE_KITTY) {
1594        fd = getCacheFD(requestData, FF_CACHE_FILE_KITTY_COMPRESSED);
1595        if (!ffIsValidNativeFD(fd)) {
1596            // The pre-existing pair. Falling back between them is fine, they hold the same
1597            // rendering; falling back to anything else is not (see §7.1).
1598            fd = getCacheFD(requestData, FF_CACHE_FILE_KITTY_UNCOMPRESSED);
1599        }
1600    } else if (requestData->type == FF_LOGO_TYPE_IMAGE_SIXEL) {
1601        fd = getCacheFD(requestData, FF_CACHE_FILE_SIXEL);
1602    }
1603
1604    if (!ffIsValidNativeFD(fd)) {
1605        return false;
1606    }
1607
1608    ffPrintCharTimes('\n', options->paddingTop);
1609    if (options->position == FF_LOGO_POSITION_RIGHT) {
1610        printf("\e[9999999C\e[%uD", (unsigned) options->paddingRight + requestData->logoCharacterWidth);
1611    } else if (options->paddingLeft) {
1612        printf("\e[%uC", (unsigned) options->paddingLeft);
1613    }
1614    fflush(stdout);
1615
1616    bool sent = false;
1617    #ifdef __linux__
1618    struct stat st;
1619    if (fstat(fd, &st) >= 0) {
1620        while (st.st_size > 0) {
1621            ssize_t bytes = sendfile(STDOUT_FILENO, fd, nullptr, (size_t) st.st_size);
1622            if (bytes > 0) {
1623                sent = true;
1624                st.st_size -= bytes;
1625            } else {
1626                break;
1627            }
1628        }
1629    }
1630    #endif
1631
1632    if (!sent) {
1633        char buffer[32768];
1634        ssize_t readBytes;
1635        while ((readBytes = ffReadFDData(fd, sizeof(buffer), buffer)) > 0) {
1636            ffWriteFDData(FFUnixFD2NativeFD(STDOUT_FILENO), (size_t) readBytes, buffer);
1637        }
1638    }
1639
1640    // The `- 1` matches printImagePixels and printImagePixelsNoCache, which print the image
1641    // themselves: the cursor ends on the last row of the image rather than past it. A cached run and
1642    // the run that filled the cache have to report the same height, or the logo shifts by one line
1643    // between the first and every later run.
1644    instance.state.logoWidth = requestData->logoCharacterWidth + options->paddingLeft + options->paddingRight;
1645    instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop - 1;
1646
1647    if (options->position != FF_LOGO_POSITION_TOP) {
1648        // Go to upper left corner
1649        printf("\e[1G\e[%uA", instance.state.logoHeight);
1650    }
1651
1652    if (options->position != FF_LOGO_POSITION_LEFT) {
1653        instance.state.logoWidth = instance.state.logoHeight = 0;
1654    }
1655    return true;
1656}
1657
1658static bool getCharacterPixelDimensions(FFLogoRequestData* requestData) {
1659    #ifdef _WIN32
1660
1661    CONSOLE_FONT_INFOEX cfi = { .cbSize = sizeof(cfi) };
1662    if (GetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi)) // Only works for ConHost
1663    {
1664        requestData->characterPixelWidth = cfi.dwFontSize.X;
1665        requestData->characterPixelHeight = cfi.dwFontSize.Y;
1666    }
1667    if (requestData->characterPixelWidth > 1.0 && requestData->characterPixelHeight > 1.0) {
1668        return true;
1669    }
1670    #endif
1671
1672    FFTerminalSizeResult termSize = {};
1673    if (ffDetectTerminalSize(&termSize, false)) {
1674        requestData->characterPixelWidth = termSize.width / (double) termSize.columns;
1675        requestData->characterPixelHeight = termSize.height / (double) termSize.rows;
1676    }
1677
1678    return requestData->characterPixelWidth > 1.0 && requestData->characterPixelHeight > 1.0;
1679}
1680
1681static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) {
1682    FFLogoRequestData requestData;
1683    requestData.type = type;
1684    requestData.characterPixelWidth = 1;
1685    requestData.characterPixelHeight = 1;
1686
1687    if (!getCharacterPixelDimensions(&requestData)) {
1688        if (printError) {
1689            fputs("Logo: getCharacterPixelDimensions() failed\n", stderr);
1690        }
1691        return false;
1692    }
1693
1694    requestData.logoPixelWidth = (uint32_t) ceil((double) instance.config.logo.width * requestData.characterPixelWidth);
1695    requestData.logoPixelHeight = (uint32_t) ceil((double) instance.config.logo.height * requestData.characterPixelHeight);
1696
1697    ffStrbufInit(&requestData.cacheDir);
1698    ffStrbufAppend(&requestData.cacheDir, &instance.state.platform.cacheDir);
1699    ffStrbufAppendS(&requestData.cacheDir, "fastfetch/images");
1700
1701    ffStrbufEnsureFree(&requestData.cacheDir, PATH_MAX);
1702    char* filePath = requestData.cacheDir.chars + requestData.cacheDir.length;
1703    if (realpath(instance.config.logo.source.chars, filePath) == nullptr) {
1704        // We can safely return here, because if realpath failed, we surely won't be able to read the file
1705        ffStrbufDestroy(&requestData.cacheDir);
1706        if (printError) {
1707            fputs("Logo: Querying realpath of the image source failed\n", stderr);
1708        }
1709        return false;
1710    }
1711
1712    #ifdef _WIN32
1713    filePath[1] = filePath[0]; // Drive Name
1714    filePath[0] = '/';
1715    #endif
1716
1717    ffStrbufRecalculateLength(&requestData.cacheDir);
1718    ffStrbufEnsureEndsWithC(&requestData.cacheDir, '/');
1719
1720    ffStrbufAppendF(&requestData.cacheDir, "%ux%u/", requestData.logoPixelWidth, requestData.logoPixelHeight);
1721
1722    // The cached payload is a rendering, not a bit-exact artefact: every backend produces a
1723    // valid one for the same source and pixel size, so the backend is deliberately not part of
1724    // the key. What the key does have to capture is the content of the source, which the path
1725    // can not: the same file can be replaced in place. Hence the recorded mtime.
1726    // 0 means the mtime could not be read, in which case the entry is never trusted.
1727    const uint64_t sourceMtime = ffPathGetMtime(instance.config.logo.source.chars);
1728
1729    bool sourceUnchanged = false;
1730    if (instance.config.logo.cache == FF_LOGO_CACHE_ON && sourceMtime != 0) {
1731        sourceUnchanged = readCachedUint64(&requestData, FF_CACHE_FILE_MTIME) == sourceMtime;
1732        if (sourceUnchanged) {
1733            bool cacheValid = requestData.type == FF_LOGO_TYPE_IMAGE_CHAFA
1734                ? printCachedChars(&requestData, FF_CACHE_FILE_CHAFA)
1735                : printCachedPixel(&requestData);
1736            if (cacheValid) {
1737                ffStrbufDestroy(&requestData.cacheDir);
1738                return true;
1739            }
1740        }
1741    }
1742
1743    // Cache miss. The entry directory is keyed on the source path and the pixel size only, so it
1744    // is reused when the source is edited; drop what the previous version left behind. This only
1745    // happens when the source actually changed: a miss on one entry of an unchanged source just
1746    // means that rendering was never asked for before, and clearing the directory would throw
1747    // away the sibling entries every time the requested rendering changes.
1748    // With the cache turned off the directory is left alone entirely.
1749    if (instance.config.logo.cache != FF_LOGO_CACHE_OFF && !sourceUnchanged) {
1750        removeCachedFiles(&requestData);
1751    }
1752
1753    const char* error = nullptr;
1754    bool printSuccessful = false;
1755
1756    if (instance.config.logo.animationFrame == FF_LOGO_ANIMATION_FRAME_ANIMATE &&
1757        requestData.type != FF_LOGO_TYPE_IMAGE_KITTY) {
1758        // Only the kitty graphics protocol can play an animation. Falling through to the static
1759        // path would silently show a still logo to a user who asked for an animation.
1760        error = "the kitty graphics protocol is the only one that can play an animation";
1761    } else if (instance.config.logo.animationFrame == FF_LOGO_ANIMATION_FRAME_ANIMATE) {
1762        // Decided before anything is decoded: the static path never enters a loop over frames,
1763        // and this path never goes through ffImageCreate.
1764        printSuccessful = encodeKittyAnimation(&requestData, &error);
1765    } else if (instance.config.logo.animationFrame != FF_LOGO_ANIMATION_FRAME_FIRST) {
1766        // A frame other than the first. This must not fall through to the static path: that one
1767        // only ever produces the first frame, so it would silently hand back the wrong image.
1768        printSuccessful = printAnimationFrame(&requestData, &error);
1769    } else if (requestData.type == FF_LOGO_TYPE_IMAGE_SIXEL) {
1770        // The sixel encoder belongs to the backend, so it is not fed through ffImageCreate
1771        FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate();
1772        if (ffImageSixelEncode(&requestData, &result, &error)) {
1773            fillCharacterDimensions(&requestData);
1774            printSuccessful = printImageSixel(&requestData, &result);
1775        }
1776    } else {
1777        FFImageBuffer buffer = {};
1778        if (ffImageCreate(&requestData, &buffer, &error)) {
1779            fillCharacterDimensions(&requestData);
1780            if (requestData.type == FF_LOGO_TYPE_IMAGE_KITTY) {
1781                printSuccessful = printImageKitty(&requestData, &buffer);
1782            }
1783    #if FF_HAVE_CHAFA
1784            else if (requestData.type == FF_LOGO_TYPE_IMAGE_CHAFA) {
1785                printSuccessful = printImageChafa(&requestData, &buffer);
1786            }
1787    #endif
1788            ffImageDestroy(&buffer);
1789        }
1790    }
1791
1792    if (printSuccessful) {
1793        // Written last: an entry only becomes usable once its payload is complete
1794        writeCacheData(&requestData, &sourceMtime, sizeof(sourceMtime), FF_CACHE_FILE_MTIME);
1795    }
1796
1797    ffStrbufDestroy(&requestData.cacheDir);
1798
1799    if (printSuccessful) {
1800        return true;
1801    }
1802
1803    if (printError) {
1804        fprintf(stderr, "Logo: %s\n", error ? error : "Failed to load / convert the image source");
1805    }
1806
1807    return false;
1808}
1809
1810#endif // FF_HAVE_IMAGEMAGICK{6, 7}
1811
1812bool ffLogoPrintImageIfExists(FFLogoType type, bool printError) {
1813    if (instance.config.display.pipe) {
1814        if (printError) {
1815            fputs("Logo: Image logo is not supported in pipe mode\n", stderr);
1816        }
1817        return false;
1818    }
1819
1820    if (!ffPathExists(instance.config.logo.source.chars, FF_PATHTYPE_FILE)) {
1821        if (printError) {
1822            fprintf(stderr, "Logo: Image source \"%s\" does not exist\n", instance.config.logo.source.chars);
1823        }
1824        return false;
1825    }
1826
1827    const char* term = getenv("TERM");
1828    if (term && ffStrEquals(term, "screen")) {
1829        if (printError) {
1830            fputs("Logo: Image logo is not supported in terminal multiplexers\n", stderr);
1831        }
1832        return false;
1833    }
1834
1835    if (type == FF_LOGO_TYPE_IMAGE_ITERM) {
1836        resolveImageAutoPosition(instance.config.logo.width);
1837        return printImageIterm(printError);
1838    }
1839
1840    if (type == FF_LOGO_TYPE_IMAGE_KITTY_DIRECT) {
1841        resolveImageAutoPosition(instance.config.logo.width);
1842        return printImageKittyDirect(printError);
1843    }
1844
1845    if (type == FF_LOGO_TYPE_IMAGE_KITTY_ICAT) {
1846        resolveImageAutoPosition(instance.config.logo.width);
1847        return printImageKittyIcat(printError);
1848    }
1849
1850#if !FF_HAVE_CHAFA
1851    if (type == FF_LOGO_TYPE_IMAGE_CHAFA) {
1852        if (printError) {
1853            fputs("Logo: Chafa support is not compiled in\n", stderr);
1854        }
1855        return false;
1856    }
1857#endif
1858
1859#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && !defined(FF_HAVE_IMAGEMAGICK7) && !defined(FF_HAVE_IMAGEMAGICK6)
1860    if (printError) {
1861        fputs("Logo: Image Magick support is not compiled in\n", stderr);
1862    }
1863    return false;
1864#else
1865    return printImageIfExistsSlowPath(type, printError);
1866#endif
1867}