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}