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, ¤tPos, &remainingLength, false, nullptr);
706 while (remainingLength > 0) {
707 appendKittyChunk(&result, ¤tPos, &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, ¤tPos, &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, ¤tPos, &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}