master ohmu/fastfetch-openbsd / src / logo / image / im7.c
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