ohmu
·
2026-10-15
1#ifdef FF_HAVE_IMAGEMAGICK7
2
3 #include "image.h"
4 #include "common/library.h"
5 #include "common/mallocHelper.h"
6 #include "common/strutil.h"
7
8 #include <MagickCore/MagickCore.h>
9 #include <stdlib.h>
10
11static FF_LIBRARY_SYMBOL(ResizeImage)
12
13 static void* logoResize(const void* image, size_t width, size_t height, void* exceptionInfo) {
14 return ffResizeImage(image, width, height, UndefinedFilter, exceptionInfo);
15}
16
17// Decode the image source, resize it to the requested pixel size and export it as a blob
18// in the given ImageMagick format ("RGBA" for kitty / chafa, "SIXEL" for sixel output).
19// On success requestData->logoPixelWidth / logoPixelHeight hold the real image dimensions.
20static FFLogoImageResult im7EncodeImage(FFLogoRequestData* requestData, const char* magick, uint32_t magickLength, void** outBlob, size_t* outLength) {
21 // clang-format off
22 #if _WIN32
23 FF_LIBRARY_LOAD(imageMagick, FF_LOGO_IMAGE_RESULT_INIT_ERROR,
24 "libMagickCore-7.Q16HDRI-10" FF_LIBRARY_EXTENSION, 0
25 )
26 #else
27 FF_LIBRARY_LOAD(imageMagick, FF_LOGO_IMAGE_RESULT_INIT_ERROR,
28 "libMagickCore-7.Q16HDRI" FF_LIBRARY_EXTENSION, 11,
29 "libMagickCore-7.Q16" FF_LIBRARY_EXTENSION, 11,
30 "libMagickCore-7" FF_LIBRARY_EXTENSION, 11
31 )
32 #endif
33 // clang-format on
34 FF_LIBRARY_LOAD_SYMBOL_ADDRESS(imageMagick, ffResizeImage, ResizeImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
35
36 FF_LIBRARY_LOAD_SYMBOL(imageMagick, MagickCoreGenesis, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
37 FF_LIBRARY_LOAD_SYMBOL(imageMagick, MagickCoreTerminus, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
38 FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireExceptionInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
39 FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyExceptionInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
40 FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
41 FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
42 FF_LIBRARY_LOAD_SYMBOL(imageMagick, ReadImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
43 FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImageList, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
44 FF_LIBRARY_LOAD_SYMBOL(imageMagick, CopyMagickString, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
45 FF_LIBRARY_LOAD_SYMBOL(imageMagick, ImageToBlob, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
46
47 FFLogoImageResult result = FF_LOGO_IMAGE_RESULT_RUN_ERROR;
48 ExceptionInfo* exceptionInfo = nullptr;
49 Image* image = nullptr;
50 ImageInfo* imageInfoOut = nullptr;
51 FF_AUTO_FREE void* blob = nullptr;
52 size_t length = 0;
53
54 ffMagickCoreGenesis(nullptr, MagickFalse);
55
56 exceptionInfo = ffAcquireExceptionInfo();
57 if (exceptionInfo == nullptr) {
58 goto cleanup;
59 }
60
61 {
62 ImageInfo* imageInfoIn = ffAcquireImageInfo();
63 if (imageInfoIn == nullptr) {
64 goto cleanup;
65 }
66
67 //+1, because we need to copy the null byte too
68 ffCopyMagickString(imageInfoIn->filename, instance.config.logo.source.chars, instance.config.logo.source.length + 1);
69
70 image = ffReadImage(imageInfoIn, exceptionInfo);
71 ffDestroyImageInfo(imageInfoIn);
72 if (image == nullptr) {
73 goto cleanup;
74 }
75 }
76
77 if (requestData->logoPixelWidth == 0 && requestData->logoPixelHeight == 0) {
78 requestData->logoPixelWidth = (uint32_t) image->columns;
79 requestData->logoPixelHeight = (uint32_t) image->rows;
80 } else if (requestData->logoPixelWidth == 0) {
81 requestData->logoPixelWidth = (uint32_t) ((double) image->columns / (double) image->rows * requestData->logoPixelHeight);
82 } else if (requestData->logoPixelHeight == 0) {
83 requestData->logoPixelHeight = (uint32_t) ((double) image->rows / (double) image->columns * requestData->logoPixelWidth);
84 }
85
86 if (requestData->logoPixelWidth == 0 || requestData->logoPixelHeight == 0) {
87 goto cleanup;
88 }
89
90 {
91 Image* resized = logoResize(image, requestData->logoPixelWidth, requestData->logoPixelHeight, exceptionInfo);
92 // ReadImage may return a list of images (e.g. for multi-frame formats like GIF).
93 // We only need the first frame, so destroy the whole list to avoid leaking the rest.
94 ffDestroyImageList(image);
95 image = resized;
96 if (image == nullptr) {
97 goto cleanup;
98 }
99 }
100
101 imageInfoOut = ffAcquireImageInfo();
102 if (imageInfoOut == nullptr) {
103 goto cleanup;
104 }
105
106 ffCopyMagickString(imageInfoOut->magick, magick, magickLength);
107
108 // The raw pixel coders write image->depth bits per sample, not 8: a 1-bit grayscale source comes
109 // back as columns*4/8 bytes per row and a 16-bit one as columns*8, while the RGBA caller hands
110 // the blob on as RGBA8 and derives its length from width*height*4. Pin the depth for the raw
111 // formats only -- the SIXEL coder quantises on its own, so it keeps the source depth and its
112 // output stays byte identical.
113 if (ffStrEquals(magick, "RGBA")) {
114 image->depth = 8;
115 }
116
117 blob = ffImageToBlob(imageInfoOut, image, &length, exceptionInfo);
118 if (blob == nullptr || length == 0) {
119 goto cleanup;
120 }
121
122 *outBlob = blob;
123 *outLength = length;
124 blob = nullptr; // Ownership is transferred to the caller
125 result = FF_LOGO_IMAGE_RESULT_SUCCESS;
126
127cleanup:
128 if (imageInfoOut) {
129 ffDestroyImageInfo(imageInfoOut);
130 }
131 if (image) {
132 ffDestroyImageList(image);
133 }
134 if (exceptionInfo) {
135 ffDestroyExceptionInfo(exceptionInfo);
136 }
137 ffMagickCoreTerminus();
138
139 // leak imageMagick to prevent fastfetch from crashing #552
140 imageMagick = nullptr;
141 return result;
142}
143
144static void setError(FFLogoImageResult result, const char** error) {
145 if (error) {
146 *error = result == FF_LOGO_IMAGE_RESULT_INIT_ERROR
147 ? "Image Magick library not found"
148 : "Failed to load / convert the image source";
149 }
150}
151
152bool ffImageCreateIM7(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) {
153 void* blob = nullptr;
154 size_t length = 0;
155 FFLogoImageResult result = im7EncodeImage(requestData, "RGBA", 5, &blob, &length);
156 if (result != FF_LOGO_IMAGE_RESULT_SUCCESS) {
157 setError(result, error);
158 return false;
159 }
160
161 // FFImageBuffer carries no length, so every consumer derives it from width*height*4. Refuse any
162 // other size rather than let them read past the blob -- the raw coder's depth scaling used to
163 // produce one (see the depth pin in im7EncodeImage).
164 if (length != (size_t) requestData->logoPixelWidth * requestData->logoPixelHeight * 4) {
165 if (error) {
166 *error = "Image Magick did not return an RGBA8 buffer";
167 }
168 free(blob);
169 return false;
170 }
171
172 out->data = blob;
173 out->width = requestData->logoPixelWidth;
174 out->height = requestData->logoPixelHeight;
175 return true;
176}
177
178bool ffImageSixelEncodeIM7(FFLogoRequestData* requestData, FFstrbuf* out, const char** error) {
179 FF_AUTO_FREE void* blob = nullptr;
180 size_t length = 0;
181 FFLogoImageResult result = im7EncodeImage(requestData, "SIXEL", 6, &blob, &length);
182 if (result != FF_LOGO_IMAGE_RESULT_SUCCESS) {
183 setError(result, error);
184 return false;
185 }
186
187 ffStrbufSetNS(out, (uint32_t) length, (const char*) blob);
188 return true;
189}
190
191// Encodes pixels the caller already has. The selected-frame path composes its frame itself, so it
192// can not go through ffImageSixelEncodeIM7 -- that one re-reads the source and would encode
193// whatever the still path would have shown instead of the frame that was asked for.
194bool ffImageSixelEncodeBufferIM7(const FFImageBuffer* buffer, FFstrbuf* out, const char** error) {
195 // clang-format off
196 #if _WIN32
197 FF_LIBRARY_LOAD(imageMagick, false,
198 "libMagickCore-7.Q16HDRI-10" FF_LIBRARY_EXTENSION, 0
199 )
200 #else
201 FF_LIBRARY_LOAD(imageMagick, false,
202 "libMagickCore-7.Q16HDRI" FF_LIBRARY_EXTENSION, 11,
203 "libMagickCore-7.Q16" FF_LIBRARY_EXTENSION, 11,
204 "libMagickCore-7" FF_LIBRARY_EXTENSION, 11
205 )
206 #endif
207 // clang-format on
208
209 FF_LIBRARY_LOAD_SYMBOL(imageMagick, MagickCoreGenesis, false)
210 FF_LIBRARY_LOAD_SYMBOL(imageMagick, MagickCoreTerminus, false)
211 FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireExceptionInfo, false)
212 FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyExceptionInfo, false)
213 FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireImageInfo, false)
214 FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImageInfo, false)
215 FF_LIBRARY_LOAD_SYMBOL(imageMagick, DestroyImage, false)
216 FF_LIBRARY_LOAD_SYMBOL(imageMagick, CopyMagickString, false)
217 FF_LIBRARY_LOAD_SYMBOL(imageMagick, ConstituteImage, false)
218 FF_LIBRARY_LOAD_SYMBOL(imageMagick, ImageToBlob, false)
219
220 ffMagickCoreGenesis(nullptr, MagickFalse);
221
222 ExceptionInfo* exceptionInfo = ffAcquireExceptionInfo();
223 if (exceptionInfo == nullptr) {
224 if (error) *error = "failed to acquire an ImageMagick exception";
225 ffMagickCoreTerminus();
226 return false;
227 }
228
229 Image* image = ffConstituteImage(buffer->width, buffer->height, "RGBA", CharPixel,
230 buffer->data, exceptionInfo);
231 if (image == nullptr) {
232 if (error) *error = "failed to wrap the animation frame";
233 ffDestroyExceptionInfo(exceptionInfo);
234 ffMagickCoreTerminus();
235 return false;
236 }
237
238 ImageInfo* imageInfoOut = ffAcquireImageInfo();
239 if (imageInfoOut == nullptr) {
240 if (error) *error = "failed to acquire an ImageMagick image info";
241 ffDestroyImage(image);
242 ffDestroyExceptionInfo(exceptionInfo);
243 ffMagickCoreTerminus();
244 return false;
245 }
246 ffCopyMagickString(imageInfoOut->magick, "SIXEL", 6);
247
248 size_t length = 0;
249 FF_AUTO_FREE void* blob = ffImageToBlob(imageInfoOut, image, &length, exceptionInfo);
250
251 ffDestroyImageInfo(imageInfoOut);
252 ffDestroyImage(image);
253 ffDestroyExceptionInfo(exceptionInfo);
254 ffMagickCoreTerminus();
255
256 if (blob == nullptr || length == 0) {
257 if (error) *error = "failed to encode the animation frame as sixel";
258 return false;
259 }
260
261 ffStrbufSetNS(out, (uint32_t) length, (const char*) blob);
262
263 // leak imageMagick to prevent fastfetch from crashing #552, as the static path does
264 imageMagick = nullptr;
265 return true;
266}
267
268// ---------------------------------------------------------------------------------------------
269// Animation
270//
271// ImageMagick reads an animated GIF as a list of *sub-frames*: every image holds only the frame's
272// own rectangle in image->columns x image->rows, and where it belongs on the logical screen in
273// image->page. CoalesceImages turns that into canvas-sized frames, which is the shape the session
274// contract asks for.
275//
276// Its idea of composition is not a browser's, though, so three things are steered before it runs.
277// All three were measured against an independent reference compositor rather than guessed, and so
278// was the fourth difference, which cannot be steered and is normalised after the fact instead. The
279// measurements and the memory cost that comes with reusing the composition are in
280// doc/kitty-animation.md §11-E.
281// ---------------------------------------------------------------------------------------------
282
283// The entry points the session calls have to outlive ffImageAnimationOpenIM7, so unlike the
284// locals im7EncodeImage loads they belong to the session.
285typedef struct FFIm7Animation {
286 FF_LIBRARY_SYMBOL(ResizeImage)
287 FF_LIBRARY_SYMBOL(ImageToBlob)
288 FF_LIBRARY_SYMBOL(DestroyImageList)
289 FF_LIBRARY_SYMBOL(DestroyImageInfo)
290 FF_LIBRARY_SYMBOL(DestroyExceptionInfo)
291 FF_LIBRARY_SYMBOL(MagickCoreTerminus)
292 FF_LIBRARY_SYMBOL(SetImageAlphaChannel)
293
294 Image* images; // coalesced, so every frame is canvas sized
295 Image* cursor; // the next frame to hand out
296 ImageInfo* blobInfo; // magick "RGBA", reused for every frame
297 ExceptionInfo* exceptionInfo;
298 uint32_t nextIndex;
299 uint32_t outputWidth;
300 uint32_t outputHeight;
301 int32_t minGap;
302} FFIm7Animation;
303
304// The aspect-ratio rule the static path applies inline, needed here for the canvas rather than
305// for a decoded frame.
306static bool im7ResolveTargetSize(FFLogoRequestData* requestData, uint32_t sourceWidth, uint32_t sourceHeight, const char** error) {
307 uint32_t width = requestData->logoPixelWidth;
308 uint32_t height = requestData->logoPixelHeight;
309 if (width == 0 && height == 0) {
310 width = sourceWidth;
311 height = sourceHeight;
312 } else if (width == 0) {
313 width = (uint32_t) ((double) sourceWidth / (double) sourceHeight * height);
314 } else if (height == 0) {
315 height = (uint32_t) ((double) sourceHeight / (double) sourceWidth * width);
316 }
317
318 if (width == 0 || height == 0) {
319 if (error) *error = "invalid target dimensions";
320 return false;
321 }
322
323 requestData->logoPixelWidth = width;
324 requestData->logoPixelHeight = height;
325 return true;
326}
327
328// Three measured differences between ImageMagick's composition and a browser's, all fixed on the
329// input side because the composition itself is the part worth reusing:
330//
331// 1. ImageMagick fills the canvas with the logical screen's background colour, opaque. Browsers
332// start transparent and never paint it. -> make every frame's background colour transparent.
333// 2. A frame with no transparent index carries no alpha channel, and the canvas CoalesceImages
334// clones from it inherits that, so the transparent fill above would be discarded. -> give
335// those frames an opaque alpha channel, which is what having no transparent index means.
336// 3. A frame that is never displayed (delay 0) must not erase the one before it, which is what
337// browsers and kitty do. ImageMagick honours the disposal anyway. -> drop the disposal.
338static void im7SteerComposition(FFIm7Animation* session, Image* images) {
339 for (Image* image = images; image != nullptr; image = image->next) {
340 image->background_color.red = 0;
341 image->background_color.green = 0;
342 image->background_color.blue = 0;
343 image->background_color.alpha = 0;
344 image->background_color.alpha_trait = BlendPixelTrait;
345
346 if (image->alpha_trait == UndefinedPixelTrait) {
347 session->ffSetImageAlphaChannel(image, OpaqueAlphaChannel, session->exceptionInfo);
348 }
349
350 if (image->delay == 0 && image->dispose == BackgroundDispose) {
351 image->dispose = NoneDispose;
352 }
353 }
354}
355
356// Where a BackgroundDispose region is cleared, ImageMagick uses the background colour it read from
357// the source and only forces the alpha to 0. The RGB under a zero alpha is unused by kitty and by
358// chafa, but the sixel encoder does look at it, and the Windows backend clears to zero -- so the
359// pixels are normalised here rather than left to differ between the two.
360static void im7ClearTransparentPixels(uint8_t* pixels, size_t length) {
361 for (size_t i = 0; i + 3 < length; i += 4) {
362 if (pixels[i + 3] == 0) {
363 pixels[i] = 0;
364 pixels[i + 1] = 0;
365 pixels[i + 2] = 0;
366 }
367 }
368}
369
370// The source's delay in centiseconds. GIFs are read with 100 ticks per second, so the conversion
371// is a no-op there, but nothing guarantees that for every format the coder may hand back.
372static uint32_t im7FrameDelayCs(const Image* image) {
373 if (image->ticks_per_second <= 0) {
374 return (uint32_t) image->delay;
375 }
376
377 return (uint32_t) ((uint64_t) image->delay * 100u / (uint64_t) image->ticks_per_second);
378}
379
380// ImageMagick counts how often the animation is *played*; the source counts how often it repeats
381// after the first play. 0 means forever and 1 means the source declared no loop count at all,
382// which is the -1 the session contract uses. Checked against the Windows backend on the same
383// files: no NETSCAPE block -> 1 here and -1 there, NETSCAPE 0 -> 0 and 0, NETSCAPE 3 -> 4 and 3.
384static int32_t im7LoopCount(const Image* image) {
385 if (image->iterations == 0) {
386 return 0;
387 }
388 if (image->iterations == 1) {
389 return -1;
390 }
391
392 const size_t loops = image->iterations - 1;
393 return loops > (size_t) INT32_MAX ? INT32_MAX : (int32_t) loops;
394}
395
396static bool im7GetFrame(FFImageAnimation* animation, uint32_t index, FFImageFrame* out, const char** error) {
397 FFIm7Animation* session = (FFIm7Animation*) ffImageAnimationGetImpl(animation);
398
399 // CoalesceImages has already composed every frame, so a frame is only ever walked past once,
400 // in order -- and taking one does not depend on having taken the previous one.
401 if (index < session->nextIndex) {
402 *error = "animation frames must be taken in order";
403 return false;
404 }
405
406 while (session->nextIndex < index && session->cursor != nullptr) {
407 session->cursor = session->cursor->next;
408 ++session->nextIndex;
409 }
410
411 if (session->cursor == nullptr) {
412 *error = "the animation has no such frame";
413 return false;
414 }
415
416 const uint32_t rawDelay = im7FrameDelayCs(session->cursor);
417
418 // kitty's mapping, which the design settled on: the raw delay is in centiseconds, the 100 ms
419 // floor exists only for sources whose frames are *all* zero, and whatever is left at <= 0 means
420 // "gapless" rather than 100 ms.
421 const int32_t gap = (int32_t) (rawDelay > (uint32_t) session->minGap ? rawDelay : (uint32_t) session->minGap) * 10;
422 out->delayMs = gap > 0 ? gap : -1;
423
424 // Each coalesced frame is canvas sized already; the scaling to the output size is per frame,
425 // exactly as it is for a still image.
426 Image* resized = session->ffResizeImage(session->cursor, session->outputWidth, session->outputHeight,
427 UndefinedFilter, session->exceptionInfo);
428 if (resized == nullptr) {
429 *error = "failed to resize the animation frame";
430 return false;
431 }
432
433 size_t length = 0;
434 void* blob = session->ffImageToBlob(session->blobInfo, resized, &length, session->exceptionInfo);
435 session->ffDestroyImageList(resized);
436 if (blob == nullptr || length != (size_t) session->outputWidth * session->outputHeight * 4) {
437 if (blob != nullptr) free(blob);
438 *error = "failed to export the animation frame";
439 return false;
440 }
441
442 im7ClearTransparentPixels((uint8_t*) blob, length);
443
444 out->data = (uint8_t*) blob;
445 session->cursor = session->cursor->next;
446 ++session->nextIndex;
447 return true;
448}
449
450static void im7FreeSession(FFIm7Animation* session) {
451 if (session->images) session->ffDestroyImageList(session->images);
452 if (session->blobInfo) session->ffDestroyImageInfo(session->blobInfo);
453 if (session->exceptionInfo) session->ffDestroyExceptionInfo(session->exceptionInfo);
454 // Only ever reached once MagickCoreGenesis has run, and the symbol load in front of that has
455 // already been checked, so this pointer is non-null here.
456 session->ffMagickCoreTerminus();
457
458 // The library handle is leaked on purpose, exactly as the static path does it: unloading
459 // libMagickCore while the process still exits through it crashes (#552).
460 free(session);
461}
462
463static void im7DestroyAnimation(FFImageAnimation* animation) {
464 FFIm7Animation* session = (FFIm7Animation*) ffImageAnimationGetImpl(animation);
465 if (session != nullptr) {
466 im7FreeSession(session);
467 }
468}
469
470bool ffImageAnimationOpenIM7(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error) {
471 // clang-format off
472 #if _WIN32
473 FF_LIBRARY_LOAD(imageMagick, false,
474 "libMagickCore-7.Q16HDRI-10" FF_LIBRARY_EXTENSION, 0
475 )
476 #else
477 FF_LIBRARY_LOAD(imageMagick, false,
478 "libMagickCore-7.Q16HDRI" FF_LIBRARY_EXTENSION, 11,
479 "libMagickCore-7.Q16" FF_LIBRARY_EXTENSION, 11,
480 "libMagickCore-7" FF_LIBRARY_EXTENSION, 11
481 )
482 #endif
483 // clang-format on
484
485 FF_LIBRARY_LOAD_SYMBOL(imageMagick, MagickCoreGenesis, false)
486 FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireExceptionInfo, false)
487 FF_LIBRARY_LOAD_SYMBOL(imageMagick, AcquireImageInfo, false)
488 FF_LIBRARY_LOAD_SYMBOL(imageMagick, ReadImage, false)
489 FF_LIBRARY_LOAD_SYMBOL(imageMagick, CoalesceImages, false)
490 FF_LIBRARY_LOAD_SYMBOL(imageMagick, CopyMagickString, false)
491
492 FFIm7Animation* session = (FFIm7Animation*) calloc(1, sizeof(*session));
493 if (session == nullptr) {
494 if (error) *error = "out of memory";
495 return false;
496 }
497
498 // The session owns these, so every failure below has to free the session by hand rather than
499 // go through im7DestroyAnimation, which expects the whole thing to be in place.
500 session->ffResizeImage = (typeof(&ResizeImage)) dlsym(imageMagick, "ResizeImage");
501 session->ffImageToBlob = (typeof(&ImageToBlob)) dlsym(imageMagick, "ImageToBlob");
502 session->ffDestroyImageList = (typeof(&DestroyImageList)) dlsym(imageMagick, "DestroyImageList");
503 session->ffDestroyImageInfo = (typeof(&DestroyImageInfo)) dlsym(imageMagick, "DestroyImageInfo");
504 session->ffDestroyExceptionInfo = (typeof(&DestroyExceptionInfo)) dlsym(imageMagick, "DestroyExceptionInfo");
505 session->ffMagickCoreTerminus = (typeof(&MagickCoreTerminus)) dlsym(imageMagick, "MagickCoreTerminus");
506 session->ffSetImageAlphaChannel = (typeof(&SetImageAlphaChannel)) dlsym(imageMagick, "SetImageAlphaChannel");
507 if (session->ffResizeImage == nullptr || session->ffImageToBlob == nullptr ||
508 session->ffDestroyImageList == nullptr || session->ffDestroyImageInfo == nullptr ||
509 session->ffDestroyExceptionInfo == nullptr || session->ffMagickCoreTerminus == nullptr ||
510 session->ffSetImageAlphaChannel == nullptr) {
511 if (error) *error = "the ImageMagick library is incomplete";
512 free(session);
513 return false;
514 }
515
516 // The core stays initialised for the whole session: the Image objects below belong to it, and
517 // terminating it while they are alive would leave them dangling. The static path can afford to
518 // initialise and terminate within one call because it never keeps an Image across calls.
519 ffMagickCoreGenesis(nullptr, MagickFalse);
520
521 session->exceptionInfo = ffAcquireExceptionInfo();
522 if (session->exceptionInfo == nullptr) {
523 if (error) *error = "failed to acquire an ImageMagick exception";
524 im7FreeSession(session);
525 return false;
526 }
527
528 ImageInfo* imageInfoIn = ffAcquireImageInfo();
529 if (imageInfoIn == nullptr) {
530 if (error) *error = "failed to acquire an ImageMagick image info";
531 im7FreeSession(session);
532 return false;
533 }
534
535 //+1, because we need to copy the null byte too
536 ffCopyMagickString(imageInfoIn->filename, instance.config.logo.source.chars, instance.config.logo.source.length + 1);
537
538 Image* raw = ffReadImage(imageInfoIn, session->exceptionInfo);
539 session->ffDestroyImageInfo(imageInfoIn);
540 if (raw == nullptr) {
541 if (error) *error = "failed to load the image source";
542 im7FreeSession(session);
543 return false;
544 }
545
546 im7SteerComposition(session, raw);
547
548 session->images = ffCoalesceImages(raw, session->exceptionInfo);
549 // CoalesceImages built a new list; the one it was given is still ours to destroy.
550 session->ffDestroyImageList(raw);
551 if (session->images == nullptr) {
552 if (error) *error = "failed to compose the animation frames";
553 im7FreeSession(session);
554 return false;
555 }
556
557 // The coalesced frames are canvas sized, which is what the target size is derived from.
558 uint32_t frameCount = 0;
559 for (Image* image = session->images; image != nullptr; image = image->next) {
560 ++frameCount;
561 }
562
563 if (frameCount == 0 ||
564 !im7ResolveTargetSize(requestData, (uint32_t) session->images->columns, (uint32_t) session->images->rows, error)) {
565 if (frameCount == 0 && error) *error = "the image source has no frames";
566 im7FreeSession(session);
567 return false;
568 }
569
570 session->blobInfo = ffAcquireImageInfo();
571 if (session->blobInfo == nullptr) {
572 if (error) *error = "failed to acquire an ImageMagick image info";
573 im7FreeSession(session);
574 return false;
575 }
576 ffCopyMagickString(session->blobInfo->magick, "RGBA", 5);
577
578 // kitty's rule: the 100 ms floor applies only when every frame's raw delay is <= 0. One frame
579 // with a delay settles it, so the normal case stops after the first read.
580 session->minGap = 10;
581 for (Image* image = session->images; image != nullptr; image = image->next) {
582 if (im7FrameDelayCs(image) > 0) {
583 session->minGap = 0;
584 break;
585 }
586 }
587
588 session->cursor = session->images;
589 session->nextIndex = 0;
590 session->outputWidth = requestData->logoPixelWidth;
591 session->outputHeight = requestData->logoPixelHeight;
592
593 FFImageAnimation* animation = ffImageAnimationCreate(frameCount, im7LoopCount(session->images),
594 session, im7GetFrame, im7DestroyAnimation);
595 if (animation == nullptr) {
596 if (error) *error = "out of memory";
597 im7FreeSession(session);
598 return false;
599 }
600
601 // leak imageMagick to prevent fastfetch from crashing #552, as the static path does
602 imageMagick = nullptr;
603 *out = animation;
604 return true;
605}
606
607#endif