master ohmu/fastfetch-openbsd / src / logo / image / wic.cpp
ohmu  ·  2026-10-15
  1extern "C" {
  2#include "image.h"
  3#include "common/mallocHelper.h"
  4#include "common/windows/com.h"
  5#include "common/windows/nt.h"
  6}
  7
  8#include <initguid.h>
  9#include <windows.h>
 10#include <wincodec.h>
 11
 12// ---------------------------------------------------------------------------------------------
 13// Metadata
 14//
 15// `QueryInterface(IID_IWICMetadataQueryReader)` returns E_NOINTERFACE on this platform for every
 16// decoder and every frame, so the interface has to be reached through the method instead. Copying
 17// the QueryInterface out of Microsoft's own WicAnimatedGif.cpp sample reads no data at all here.
 18//
 19// The two readers are not interchangeable: `/logscrdesc/*` and `/appext/*` exist only on the
 20// decoder's reader, `/imgdesc/*` and `/grctlext/*` only on a frame's.
 21// ---------------------------------------------------------------------------------------------
 22
 23static bool readMetadata(IWICMetadataQueryReader* reader, const wchar_t* name, PROPVARIANT* out) {
 24    if (reader == nullptr) {
 25        return false;
 26    }
 27
 28    PropVariantInit(out);
 29    if (FAILED(reader->GetMetadataByName(name, out))) {
 30        PropVariantClear(out);
 31        return false;
 32    }
 33
 34    return true;
 35}
 36
 37// Every integer path used here is one of these; the width varies with the item.
 38static bool readUint(IWICMetadataQueryReader* reader, const wchar_t* name, uint32_t* out) {
 39    PROPVARIANT value;
 40    if (!readMetadata(reader, name, &value)) {
 41        return false;
 42    }
 43
 44    bool ok = true;
 45    switch (value.vt) {
 46        case VT_UI1: *out = value.bVal; break;
 47        case VT_UI2: *out = value.uiVal; break;
 48        case VT_UI4: *out = value.ulVal; break;
 49        case VT_I2: *out = (uint32_t) value.iVal; break;
 50        case VT_I4: *out = (uint32_t) value.lVal; break;
 51        default: ok = false; break;
 52    }
 53
 54    PropVariantClear(&value);
 55    return ok;
 56}
 57
 58// The loop count lives in the NETSCAPE2.0 application extension: `[0]=size=3`, `[1]=sub-block
 59// id=1`, `[2..3]=LE16 count`. `/logscrdesc/LoopCount` and `/logscrdesc/Iterations` do not exist.
 60// A source without the extension declares no loop count at all, which is -1, not 0.
 61static bool readLoopCount(IWICMetadataQueryReader* reader, int32_t* out) {
 62    PROPVARIANT value;
 63    if (!readMetadata(reader, L"/appext/Data", &value)) {
 64        return false;
 65    }
 66
 67    bool ok = false;
 68    if (value.vt == (VARTYPE) (VT_VECTOR | VT_UI1) && value.caub.cElems >= 4 &&
 69        value.caub.pElems[0] == 3 && value.caub.pElems[1] == 1) {
 70        *out = (int32_t) ((uint32_t) value.caub.pElems[2] | ((uint32_t) value.caub.pElems[3] << 8));
 71        ok = true;
 72    }
 73
 74    PropVariantClear(&value);
 75    return ok;
 76}
 77
 78// ---------------------------------------------------------------------------------------------
 79// Pixels
 80// ---------------------------------------------------------------------------------------------
 81
 82// Scales `source` to `outWidth` x `outHeight` and converts it to straight-alpha RGBA8, which is
 83// what kitty (f=32) and chafa (CHAFA_PIXEL_RGBA8_UNASSOCIATED) both consume. Ownership of
 84// `*outPixels` passes to the caller.
 85static bool convertToRGBA(IWICImagingFactory* factory, IWICBitmapSource* source,
 86    uint32_t outWidth, uint32_t outHeight, uint8_t** outPixels, const char** error) {
 87    UINT sourceWidth = 0, sourceHeight = 0;
 88    if (FAILED(source->GetSize(&sourceWidth, &sourceHeight))) {
 89        if (error) *error = "failed to query the image dimensions";
 90        return false;
 91    }
 92
 93    FF_AUTO_RELEASE_COM_OBJECT IWICBitmapScaler* scaler = nullptr;
 94    FF_AUTO_RELEASE_COM_OBJECT IWICFormatConverter* premultiplyConverter = nullptr;
 95    FF_AUTO_RELEASE_COM_OBJECT IWICFormatConverter* converter = nullptr;
 96
 97    IWICBitmapSource* sized = source;
 98    if (outWidth != sourceWidth || outHeight != sourceHeight) {
 99        // Same size: don't resample. ImageMagick clones the image in this case too,
100        // and resampling would only blur it.
101        //
102        // Otherwise scale premultiplied alpha to avoid transparent RGB values bleeding into
103        // the visible edge pixels. The final output is converted back to straight alpha below.
104        if (FAILED(factory->CreateFormatConverter(&premultiplyConverter)) ||
105            FAILED(premultiplyConverter->Initialize(source, GUID_WICPixelFormat32bppPBGRA,
106                WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom)) ||
107            FAILED(factory->CreateBitmapScaler(&scaler)) ||
108            FAILED(scaler->Initialize(premultiplyConverter, outWidth, outHeight,
109                (WICBitmapInterpolationMode) 0x4 /* WICBitmapInterpolationModeHighQualityCubic */))) {
110            if (error) *error = "image scaling failed";
111            return false;
112        }
113        sized = scaler;
114    }
115
116    // Normalize to straight-alpha RGBA8: kitty (f=32) and chafa
117    // (CHAFA_PIXEL_RGBA8_UNASSOCIATED) both consume exactly this
118    if (FAILED(factory->CreateFormatConverter(&converter)) ||
119        FAILED(converter->Initialize(sized, GUID_WICPixelFormat32bppRGBA,
120            WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom))) {
121        if (error) *error = "pixel format conversion failed";
122        return false;
123    }
124
125    // `stride` and the length WIC is told are both UINT, while the buffer is a size_t allocation, so
126    // computing either in UINT would let a large request wrap and have WIC fill a buffer it believes
127    // is smaller than it is. Not reachable today -- the scaler above rejects any dimension above
128    // 32767, which caps the stride at 131068 and the buffer at 4294705156, both inside UINT -- but
129    // the guard costs nothing and every other backend keeps its buffer size associative. Kept in
130    // size_t throughout so the check also covers the allocation overflowing size_t.
131    const size_t strideSize = (size_t) outWidth * 4;
132    const size_t bufferSize = strideSize * outHeight;
133    if (strideSize > UINT_MAX || bufferSize > UINT_MAX) {
134        if (error) *error = "the requested image dimensions are too large";
135        return false;
136    }
137    const UINT stride = (UINT) strideSize;
138
139    uint8_t* pixels = (uint8_t*) malloc(bufferSize);
140    if (pixels == nullptr) {
141        if (error) *error = "out of memory";
142        return false;
143    }
144
145    // prc == nullptr means the whole image; WIC fills the buffer using the stride we pass in
146    if (FAILED(converter->CopyPixels(nullptr, stride, (UINT) bufferSize, pixels))) {
147        free(pixels);
148        if (error) *error = "pixel copy failed";
149        return false;
150    }
151
152    *outPixels = pixels;
153    return true;
154}
155
156// Fills in the missing dimension, keeping the source aspect ratio (same as the IM path)
157static bool resolveTargetSize(FFLogoRequestData* requestData, uint32_t sourceWidth, uint32_t sourceHeight, const char** error) {
158    uint32_t width = requestData->logoPixelWidth;
159    uint32_t height = requestData->logoPixelHeight;
160    if (width == 0 && height == 0) {
161        width = sourceWidth;
162        height = sourceHeight;
163    } else if (width == 0) {
164        width = (uint32_t) ((double) sourceWidth / (double) sourceHeight * height);
165    } else if (height == 0) {
166        height = (uint32_t) ((double) sourceHeight / (double) sourceWidth * width);
167    }
168
169    if (width == 0 || height == 0) {
170        if (error) *error = "invalid target dimensions";
171        return false;
172    }
173
174    requestData->logoPixelWidth = width;
175    requestData->logoPixelHeight = height;
176    return true;
177}
178
179// The source path is UTF-8, WIC only accepts UTF-16
180static bool createDecoder(IWICImagingFactory* factory, IWICBitmapDecoder** out, const char** error) {
181    wchar_t widePath[MAX_PATH + 1];
182    if (!NT_SUCCESS(RtlUTF8ToUnicodeN(widePath, (ULONG) sizeof(widePath), nullptr,
183            instance.config.logo.source.chars, (ULONG) instance.config.logo.source.length + 1))) {
184        if (error) *error = "failed to convert the image path to UTF-16";
185        return false;
186    }
187
188    if (FAILED(factory->CreateDecoderFromFilename(widePath, nullptr, GENERIC_READ,
189            WICDecodeMetadataCacheOnDemand, out))) {
190        if (error) *error = "unsupported or unreadable image format";
191        return false;
192    }
193
194    return true;
195}
196
197bool ffImageCreateWIC(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) {
198    const char* comError = ffInitCom();
199    if (comError) {
200        if (error) *error = comError;
201        return false;
202    }
203
204    FF_AUTO_RELEASE_COM_OBJECT IWICImagingFactory* factory = nullptr;
205    FF_AUTO_RELEASE_COM_OBJECT IWICBitmapDecoder* decoder = nullptr;
206    FF_AUTO_RELEASE_COM_OBJECT IWICBitmapFrameDecode* frame = nullptr;
207
208    if (FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
209            IID_IWICImagingFactory, (void**) &factory))) {
210        if (error) *error = "WIC imaging factory is unavailable";
211        return false;
212    }
213
214    if (!createDecoder(factory, &decoder, error)) {
215        return false;
216    }
217
218    // Only the first frame, matching ImageMagick's ReadImage (neither handles GIF animation)
219    if (FAILED(decoder->GetFrame(0, &frame))) {
220        if (error) *error = "failed to get the first frame";
221        return false;
222    }
223
224    UINT sourceWidth = 0, sourceHeight = 0;
225    frame->GetSize(&sourceWidth, &sourceHeight);
226    if (sourceWidth == 0 || sourceHeight == 0) {
227        if (error) *error = "invalid image dimensions";
228        return false;
229    }
230
231    if (!resolveTargetSize(requestData, sourceWidth, sourceHeight, error)) {
232        return false;
233    }
234
235    uint8_t* pixels = nullptr;
236    if (!convertToRGBA(factory, frame, requestData->logoPixelWidth, requestData->logoPixelHeight, &pixels, error)) {
237        return false;
238    }
239
240    out->data = pixels;
241    out->width = requestData->logoPixelWidth;
242    out->height = requestData->logoPixelHeight;
243    return true;
244}
245
246// ---------------------------------------------------------------------------------------------
247// Animation
248//
249// WIC does not compose GIF frames: it hands out the raw sub-frame plus the metadata describing how
250// to place it, so the canvas and the disposal handling are ours. The loop below follows the GIF
251// model, which is also what Microsoft's own sample implements:
252//
253//   1. apply the *previous* frame's disposal to the canvas (0/1 keep, 2 clear its rectangle,
254//      3 restore the snapshot taken before it was drawn);
255//   2. if the current frame's disposal is 3, snapshot the canvas as it stands;
256//   3. draw the current frame's pixels at its own (left, top).
257//
258// The canvas is scaled once per frame on the way out, never per sub-frame: scaling in sub-frame
259// coordinates would introduce edge errors.
260// ---------------------------------------------------------------------------------------------
261
262struct FFWicAnimation {
263    IWICImagingFactory* factory;
264    IWICBitmapDecoder* decoder;
265    IWICMetadataQueryReader* decoderReader; // container level; nullptr for formats without one
266
267    uint32_t canvasWidth;
268    uint32_t canvasHeight;
269    uint8_t* canvas;   // RGBA8, the composed canvas
270    uint8_t* snapshot; // the canvas as it was before a disposal=3 frame was drawn
271
272    uint32_t outputWidth;
273    uint32_t outputHeight;
274
275    uint32_t previousLeft;
276    uint32_t previousTop;
277    uint32_t previousWidth;
278    uint32_t previousHeight;
279    uint32_t previousDisposal;
280    uint32_t previousRawDelay; // centiseconds, as the source declares it
281    bool hasPrevious;
282
283    uint32_t nextIndex; // frames are handed out in order; frame i depends on 0..i-1
284    int32_t minGap;     // 0, or 10 when every frame's raw delay is <= 0
285};
286
287static void clearRect(FFWicAnimation* animation, uint32_t left, uint32_t top, uint32_t width, uint32_t height) {
288    const size_t rowBytes = (size_t) width * 4;
289    for (uint32_t y = 0; y < height; ++y) {
290        memset(animation->canvas + (((size_t) (top + y) * animation->canvasWidth) + left) * 4, 0, rowBytes);
291    }
292}
293
294// Reads frame `index`'s metadata, applies the previous frame's disposal, snapshots if this frame
295// asks for it, and draws the frame onto the canvas. Reports the frame's gap in milliseconds.
296static bool composeFrame(FFWicAnimation* animation, uint32_t index, int32_t* delayMs, const char** error) {
297    FF_AUTO_RELEASE_COM_OBJECT IWICBitmapFrameDecode* frame = nullptr;
298    FF_AUTO_RELEASE_COM_OBJECT IWICMetadataQueryReader* reader = nullptr;
299
300    if (FAILED(animation->decoder->GetFrame(index, &frame)) ||
301        FAILED(frame->GetMetadataQueryReader(&reader))) {
302        *error = "failed to read the frame metadata";
303        return false;
304    }
305
306    uint32_t left = 0, top = 0, width = 0, height = 0, disposal = 0, rawDelay = 0;
307    if (!readUint(reader, L"/imgdesc/Left", &left) || !readUint(reader, L"/imgdesc/Top", &top) ||
308        !readUint(reader, L"/imgdesc/Width", &width) || !readUint(reader, L"/imgdesc/Height", &height)) {
309        // Not a GIF: there is no placement metadata at all and the frame is the whole canvas.
310        UINT frameWidth = 0, frameHeight = 0;
311        if (FAILED(frame->GetSize(&frameWidth, &frameHeight))) {
312            *error = "failed to read the frame rectangle";
313            return false;
314        }
315        left = 0;
316        top = 0;
317        width = frameWidth;
318        height = frameHeight;
319    }
320
321    if (width == 0 || height == 0 ||
322        left + width > animation->canvasWidth || top + height > animation->canvasHeight) {
323        *error = "the frame rectangle does not fit the canvas";
324        return false;
325    }
326
327    // Both are absent on anything that is not a GIF, in which case the frame covers the canvas and
328    // there is nothing to animate.
329    readUint(reader, L"/grctlext/Disposal", &disposal);
330    readUint(reader, L"/grctlext/Delay", &rawDelay);
331
332    // kitty's mapping, which the design settled on: the raw delay is in centiseconds, the 100 ms
333    // floor exists only for sources whose frames are *all* zero, and whatever is left at <= 0 means
334    // "gapless" rather than 100 ms. Not the "<90 -> 90" floor of the WIC sample, which its own
335    // comment admits destroys legitimate zero-gap frames.
336    const int32_t gap = (int32_t) (rawDelay > (uint32_t) animation->minGap ? rawDelay : (uint32_t) animation->minGap) * 10;
337    *delayMs = gap > 0 ? gap : -1;
338
339    if (animation->hasPrevious) {
340        if (animation->previousDisposal == 2 && animation->previousRawDelay != 0) {
341            // No colour table is reachable through WIC - only BackgroundColorIndex, never a palette
342            // - so "restore to background" can only mean transparent.
343            //
344            // A previous frame with a zero delay keeps its pixels instead: it was never displayed
345            // for any length of time, so erasing it would only flicker. This is the rule the
346            // reference compositor was validated against.
347            clearRect(animation, animation->previousLeft, animation->previousTop,
348                animation->previousWidth, animation->previousHeight);
349        } else if (animation->previousDisposal == 3) {
350            memcpy(animation->canvas, animation->snapshot,
351                (size_t) animation->canvasWidth * animation->canvasHeight * 4);
352        }
353    }
354
355    if (disposal == 3) {
356        const size_t canvasSize = (size_t) animation->canvasWidth * animation->canvasHeight * 4;
357        if (animation->snapshot == nullptr) {
358            animation->snapshot = (uint8_t*) malloc(canvasSize);
359            if (animation->snapshot == nullptr) {
360                *error = "out of memory";
361                return false;
362            }
363        }
364        memcpy(animation->snapshot, animation->canvas, canvasSize);
365    }
366
367    // Decode the sub-frame at its own size; the canvas is scaled on the way out instead.
368    FF_AUTO_FREE uint8_t* pixels = nullptr;
369    if (!convertToRGBA(animation->factory, frame, width, height, &pixels, error)) {
370        return false;
371    }
372
373    for (uint32_t y = 0; y < height; ++y) {
374        const uint8_t* source = pixels + (size_t) y * width * 4;
375        uint8_t* target = animation->canvas + (((size_t) (top + y) * animation->canvasWidth) + left) * 4;
376
377        for (uint32_t x = 0; x < width; ++x, source += 4, target += 4) {
378            const uint8_t alpha = source[3];
379            if (alpha == 0) {
380                // GIF transparency is one bit, so "keep" is exact and needs no arithmetic at all.
381                continue;
382            }
383            if (alpha == 255) {
384                memcpy(target, source, 4);
385                continue;
386            }
387
388            // Not a GIF. Composite over, so a source with real alpha still lands sanely; for the
389            // one-bit alpha above this branch is never taken.
390            for (uint32_t channel = 0; channel < 3; ++channel) {
391                target[channel] = (uint8_t) (((uint32_t) source[channel] * alpha +
392                    (uint32_t) target[channel] * (255 - alpha) + 127) / 255);
393            }
394            target[3] = (uint8_t) (alpha + (uint32_t) target[3] * (255 - alpha) / 255);
395        }
396    }
397
398    animation->previousLeft = left;
399    animation->previousTop = top;
400    animation->previousWidth = width;
401    animation->previousHeight = height;
402    animation->previousDisposal = disposal;
403    animation->previousRawDelay = rawDelay;
404    animation->hasPrevious = true;
405    return true;
406}
407
408// Scales the composed canvas to the output size and hands it over.
409static bool outputCanvas(FFWicAnimation* animation, FFImageFrame* out, const char** error) {
410    const UINT stride = animation->canvasWidth * 4;
411
412    FF_AUTO_RELEASE_COM_OBJECT IWICBitmap* bitmap = nullptr;
413    if (FAILED(animation->factory->CreateBitmapFromMemory(animation->canvasWidth, animation->canvasHeight,
414            GUID_WICPixelFormat32bppRGBA, stride, stride * animation->canvasHeight, animation->canvas, &bitmap))) {
415        *error = "failed to wrap the composed canvas";
416        return false;
417    }
418
419    return convertToRGBA(animation->factory, bitmap, animation->outputWidth, animation->outputHeight,
420        &out->data, error);
421}
422
423static bool wicGetFrame(FFImageAnimation* animation, uint32_t index, FFImageFrame* out, const char** error) {
424    FFWicAnimation* session = (FFWicAnimation*) ffImageAnimationGetImpl(animation);
425
426    // The composition of frame i depends on frames 0..i-1, so they are walked in order and the
427    // intermediate canvases are simply discarded.
428    if (index < session->nextIndex) {
429        *error = "animation frames must be taken in order";
430        return false;
431    }
432
433    int32_t delayMs = -1;
434    for (uint32_t i = session->nextIndex; i <= index; ++i) {
435        if (!composeFrame(session, i, &delayMs, error)) {
436            return false;
437        }
438        session->nextIndex = i + 1;
439    }
440
441    if (!outputCanvas(session, out, error)) {
442        return false;
443    }
444
445    out->delayMs = delayMs;
446    return true;
447}
448
449static void wicDestroyAnimation(FFImageAnimation* animation) {
450    FFWicAnimation* session = (FFWicAnimation*) ffImageAnimationGetImpl(animation);
451    if (session == nullptr) {
452        return;
453    }
454
455    free(session->canvas);
456    free(session->snapshot);
457    if (session->decoderReader) session->decoderReader->Release();
458    if (session->decoder) session->decoder->Release();
459    if (session->factory) session->factory->Release();
460    free(session);
461}
462
463// Reads one frame's raw delay, for the all-zero check that decides the 100 ms floor.
464static bool readFrameDelay(IWICBitmapDecoder* decoder, uint32_t index, uint32_t* out) {
465    FF_AUTO_RELEASE_COM_OBJECT IWICBitmapFrameDecode* frame = nullptr;
466    FF_AUTO_RELEASE_COM_OBJECT IWICMetadataQueryReader* reader = nullptr;
467
468    if (FAILED(decoder->GetFrame(index, &frame)) || FAILED(frame->GetMetadataQueryReader(&reader))) {
469        return false;
470    }
471
472    return readUint(reader, L"/grctlext/Delay", out);
473}
474
475bool ffImageAnimationOpenWIC(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error) {
476    const char* comError = ffInitCom();
477    if (comError) {
478        if (error) *error = comError;
479        return false;
480    }
481
482    FF_AUTO_RELEASE_COM_OBJECT IWICImagingFactory* factory = nullptr;
483    FF_AUTO_RELEASE_COM_OBJECT IWICBitmapDecoder* decoder = nullptr;
484    FF_AUTO_RELEASE_COM_OBJECT IWICBitmapFrameDecode* firstFrame = nullptr;
485    FF_AUTO_RELEASE_COM_OBJECT IWICMetadataQueryReader* decoderReader = nullptr;
486
487    if (FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
488            IID_IWICImagingFactory, (void**) &factory))) {
489        if (error) *error = "WIC imaging factory is unavailable";
490        return false;
491    }
492
493    if (!createDecoder(factory, &decoder, error)) {
494        return false;
495    }
496
497    // Cheap: WICDecodeMetadataCacheOnDemand reads only the header, so the frame count - which a
498    // negative --logo-animation-frame index is resolved against - needs no decoding.
499    UINT frameCount = 0;
500    if (FAILED(decoder->GetFrameCount(&frameCount)) || frameCount == 0) {
501        if (error) *error = "the image source has no frames";
502        return false;
503    }
504
505    if (FAILED(decoder->GetFrame(0, &firstFrame))) {
506        if (error) *error = "failed to get the first frame";
507        return false;
508    }
509
510    // Absent for every format without a query reader (PNG, JPEG, ...), which is not an error: the
511    // frame is the canvas then.
512    decoder->GetMetadataQueryReader(&decoderReader);
513
514    // GetSize() on a frame gives the *sub-frame* rectangle, never the canvas, so the canvas has to
515    // come from the logical screen descriptor.
516    uint32_t canvasWidth = 0, canvasHeight = 0;
517    if (!readUint(decoderReader, L"/logscrdesc/Width", &canvasWidth) ||
518        !readUint(decoderReader, L"/logscrdesc/Height", &canvasHeight)) {
519        UINT width = 0, height = 0;
520        if (FAILED(firstFrame->GetSize(&width, &height)) || width == 0 || height == 0) {
521            if (error) *error = "invalid image dimensions";
522            return false;
523        }
524        canvasWidth = width;
525        canvasHeight = height;
526    }
527
528    if (!resolveTargetSize(requestData, canvasWidth, canvasHeight, error)) {
529        return false;
530    }
531
532    // 0 means "loop forever", -1 that the source declares no loop count at all.
533    int32_t loopCount = -1;
534    readLoopCount(decoderReader, &loopCount);
535
536    // kitty's rule: the 100 ms floor applies only when every frame's raw delay is <= 0. One frame
537    // with a delay settles it, so the normal case stops after the first read.
538    int32_t minGap = 10;
539    for (UINT i = 0; i < frameCount; ++i) {
540        uint32_t rawDelay = 0;
541        if (!readFrameDelay(decoder, i, &rawDelay) || rawDelay > 0) {
542            minGap = 0;
543            break;
544        }
545    }
546
547    FFWicAnimation* session = (FFWicAnimation*) calloc(1, sizeof(*session));
548    if (session == nullptr) {
549        if (error) *error = "out of memory";
550        return false;
551    }
552
553    session->canvas = (uint8_t*) calloc((size_t) canvasWidth * canvasHeight * 4, 1);
554    if (session->canvas == nullptr) {
555        free(session);
556        if (error) *error = "out of memory";
557        return false;
558    }
559
560    session->factory = factory;
561    session->decoder = decoder;
562    session->decoderReader = decoderReader;
563    session->canvasWidth = canvasWidth;
564    session->canvasHeight = canvasHeight;
565    session->outputWidth = requestData->logoPixelWidth;
566    session->outputHeight = requestData->logoPixelHeight;
567    session->minGap = minGap;
568    session->nextIndex = 0;
569
570    FFImageAnimation* animation = ffImageAnimationCreate(frameCount, loopCount, session, wicGetFrame, wicDestroyAnimation);
571    if (animation == nullptr) {
572        free(session->canvas);
573        free(session);
574        if (error) *error = "out of memory";
575        return false;
576    }
577
578    // The session owns these from here on, so the cleanup guards must not release them.
579    factory = nullptr;
580    decoder = nullptr;
581    decoderReader = nullptr;
582    *out = animation;
583    return true;
584}