ohmu
·
2026-10-15
1
2#include "image.h"
3#include "common/android/api.h"
4#include "common/io.h"
5#include "common/mallocHelper.h"
6
7#include <android/bitmap.h>
8#include <android/imagedecoder.h>
9
10#include <fcntl.h>
11#include <stdint.h>
12#include <string.h>
13#include <unistd.h>
14
15static inline bool androidImageDecoderError(const char** error, const char* message) {
16 if (error) {
17 *error = message;
18 }
19 return false;
20}
21
22// AImageDecoder decodes to premultiplied alpha, and it can not be asked for straight alpha once a
23// target size is in effect: AImageDecoder_setUnpremultipliedRequired fails with
24// ANDROID_IMAGE_DECODER_INVALID_CONVERSION, documented as "Unpremultiplied is not possible due to
25// an existing scale set by AImageDecoder_setTargetSize". Premultiplied is the right domain to
26// scale in anyway -- interpolating straight alpha averages the colour of fully transparent pixels
27// into the edges next to them, which shows up as dark fringes -- so the decoder is left alone and
28// this runs afterwards, exactly as the ImageIO backend does.
29static void unPremultiplyRGBA(uint8_t* data, size_t pixelCount) {
30 for (size_t i = 0; i < pixelCount; ++i) {
31 uint8_t* const p = data + i * 4;
32 const uint8_t a = p[3];
33 if (a == 0) {
34 p[0] = p[1] = p[2] = 0;
35 } else if (a < 255) {
36 p[0] = (uint8_t) ((p[0] * 255 + a / 2) / a);
37 p[1] = (uint8_t) ((p[1] * 255 + a / 2) / a);
38 p[2] = (uint8_t) ((p[2] * 255 + a / 2) / a);
39 }
40 }
41}
42
43// Copy one decoded frame into a tightly packed, straight alpha buffer of its own.
44//
45// A copy rather than a hand-over: the animation path has to keep the decoder's own buffer intact,
46// because AImageDecoder blends each frame into the one that is already there. The still path only
47// needs the packed result, and going through here keeps the two identical.
48//
49// `premultiplied` is what the decoder reports about the pixels it produced, so a source that was
50// decoded straight is copied and nothing else.
51static uint8_t* androidPackFrame(const uint8_t* src, size_t stride, uint32_t width, uint32_t height, bool premultiplied) {
52 const size_t packedStride = (size_t) width * 4;
53 uint8_t* pixels = (uint8_t*) malloc(packedStride * height);
54 if (pixels == nullptr) {
55 return nullptr;
56 }
57
58 for (uint32_t y = 0; y < height; ++y) {
59 memcpy(pixels + (size_t) y * packedStride, src + (size_t) y * stride, packedStride);
60 }
61
62 if (premultiplied) {
63 unPremultiplyRGBA(pixels, (size_t) width * height);
64 }
65 return pixels;
66}
67
68// Everything the still and the animation path share: read the source size, resolve the requested
69// pixel size, ask for RGBA8888 and let the decoder do the scaling.
70FF_ANDROID_REQUIRES_API(30) static bool androidResolveDecoder(AImageDecoder* decoder, FFLogoRequestData* requestData, size_t* outStride, bool* outPremultiplied, const char** error) {
71 const AImageDecoderHeaderInfo* header = AImageDecoder_getHeaderInfo(decoder);
72 // Both are int32_t, and anything <= 0 is not a usable source
73 const int32_t sourceWidth = AImageDecoderHeaderInfo_getWidth(header);
74 const int32_t sourceHeight = AImageDecoderHeaderInfo_getHeight(header);
75 if (sourceWidth <= 0 || sourceHeight <= 0) {
76 return androidImageDecoderError(error, "invalid image dimensions");
77 }
78
79 // Fill in the missing dimension, keeping the source aspect ratio (same as the IM path)
80 uint32_t width = requestData->logoPixelWidth;
81 uint32_t height = requestData->logoPixelHeight;
82 if (width == 0 && height == 0) {
83 width = (uint32_t) sourceWidth;
84 height = (uint32_t) sourceHeight;
85 } else if (width == 0) {
86 width = (uint32_t) ((double) sourceWidth / (double) sourceHeight * height);
87 } else if (height == 0) {
88 height = (uint32_t) ((double) sourceHeight / (double) sourceWidth * width);
89 }
90 if (width == 0 || height == 0) {
91 return androidImageDecoderError(error, "invalid target dimensions");
92 }
93
94 if (AImageDecoder_setAndroidBitmapFormat(decoder, ANDROID_BITMAP_FORMAT_RGBA_8888) != ANDROID_IMAGE_DECODER_SUCCESS) {
95 return androidImageDecoderError(error, "failed to select the RGBA8888 output format");
96 }
97
98 // A source that is already the requested size needs no scaling at all, and leaving the target
99 // size unset buys one more thing: AImageDecoder refuses straight alpha only *because* of a
100 // scale, so unscaled output can be decoded straight and no conversion follows. That is also
101 // the more accurate of the two, having no integer rounding to recover from.
102 if (width == (uint32_t) sourceWidth && height == (uint32_t) sourceHeight) {
103 *outPremultiplied = AImageDecoder_setUnpremultipliedRequired(decoder, true) != ANDROID_IMAGE_DECODER_SUCCESS;
104 } else {
105 // Scaling is part of the decode: the decoder samples the source down (or up) as it goes, so
106 // there is neither a full-size intermediate nor a separate resize step.
107 if (AImageDecoder_setTargetSize(decoder, (int32_t) width, (int32_t) height) != ANDROID_IMAGE_DECODER_SUCCESS) {
108 return androidImageDecoderError(error, "failed to scale the image");
109 }
110 *outPremultiplied = true;
111 }
112
113 const size_t stride = AImageDecoder_getMinimumStride(decoder);
114 if (stride < (size_t) width * 4 || stride > SIZE_MAX / height) {
115 return androidImageDecoderError(error, "invalid target dimensions");
116 }
117
118 requestData->logoPixelWidth = width;
119 requestData->logoPixelHeight = height;
120 *outStride = stride;
121 return true;
122}
123
124bool ffImageCreateAID(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) {
125 // AImageDecoder is API 30, and fastfetch still runs on devices below that, so this is a real
126 // run-time check and not a compile-time constant. See common/android/api.h for why.
127 if (FF_ANDROID_API_AT_LEAST(30)) {
128 FF_AUTO_CLOSE_FD int fd = open(instance.config.logo.source.chars, O_RDONLY | O_CLOEXEC);
129 if (fd < 0) {
130 return androidImageDecoderError(error, "failed to open the image source");
131 }
132
133 AImageDecoder* decoder = nullptr;
134 if (AImageDecoder_createFromFd(fd, &decoder) != ANDROID_IMAGE_DECODER_SUCCESS) {
135 return androidImageDecoderError(error, "unsupported or unreadable image format");
136 }
137
138 size_t stride = 0;
139 bool premultiplied = true;
140 if (!androidResolveDecoder(decoder, requestData, &stride, &premultiplied, error)) {
141 AImageDecoder_delete(decoder);
142 return false;
143 }
144
145 const uint32_t width = requestData->logoPixelWidth;
146 const uint32_t height = requestData->logoPixelHeight;
147 const size_t packedStride = (size_t) width * 4;
148 const size_t bufferSize = stride * height;
149
150 FF_AUTO_FREE uint8_t* decoded = (uint8_t*) malloc(bufferSize);
151 if (decoded == nullptr) {
152 AImageDecoder_delete(decoder);
153 return androidImageDecoderError(error, "out of memory");
154 }
155
156 const int result = AImageDecoder_decodeImage(decoder, decoded, stride, bufferSize);
157 // The decoder reads from the fd while decoding, so it outlives the decode but not the fd
158 AImageDecoder_delete(decoder);
159 if (result != ANDROID_IMAGE_DECODER_SUCCESS) {
160 return androidImageDecoderError(error, "failed to decode the image");
161 }
162
163 // Without row padding the decoder's buffer is already laid out the way the contract wants,
164 // so it is finished in place and handed over rather than copied.
165 if (stride == packedStride) {
166 if (premultiplied) {
167 unPremultiplyRGBA(decoded, (size_t) width * height);
168 }
169 out->data = decoded;
170 decoded = nullptr; // Ownership is transferred to `out`
171 } else {
172 out->data = androidPackFrame(decoded, stride, width, height, premultiplied);
173 if (out->data == nullptr) {
174 return androidImageDecoderError(error, "out of memory");
175 }
176 }
177
178 out->width = width;
179 out->height = height;
180 return true;
181 } else {
182 return androidImageDecoderError(error, "AImageDecoder requires Android 11 (API 30) or newer");
183 }
184}
185
186// ---------------------------------------------------------------------------------------------
187// Animation
188//
189// AImageDecoder composes the frames itself. Given the same buffer for every call, it decodes the
190// part of the canvas a frame actually covers and blends it over what is already there, and it
191// restores the buffer for a DISPOSE_OP_PREVIOUS frame on its own. So unlike the Windows backend,
192// there is no canvas to maintain here: the session keeps the decoder's buffer and hands out a copy
193// of it per frame.
194//
195// Two things follow from the decoder only ever walking forwards:
196//
197// * The frame count and every gap have to be collected up front by advancing through the whole
198// animation and rewinding, which is what the session contract asks for anyway -- a negative
199// --logo-animation-frame has to be resolved before the first frame is taken.
200// * Reaching a frame that is not the next one means rewinding and walking forward again, which is
201// slower than refusing it but keeps the iterator usable after the last frame.
202//
203// The loop count is the one thing AImageDecoder does not expose: it has no API for it at any
204// level, so the animation is reported as looping forever, which is what an animated GIF with no
205// NETSCAPE block does in every viewer anyway.
206// ---------------------------------------------------------------------------------------------
207
208typedef struct FFAndroidAnimation {
209 AImageDecoder* decoder;
210 int fd; // the source the decoder keeps reading from while frames are decoded
211 uint8_t* canvas; // the decoder's buffer; every frame is blended into what is already in it
212 size_t stride;
213 size_t size;
214 uint32_t width;
215 uint32_t height;
216 uint32_t nextIndex; // the frame the decoder is positioned on
217 bool premultiplied; // whether the decoder's frames need un-premultiplying
218 int32_t minGap;
219 int32_t* delaysCs; // one per frame, in centiseconds
220} FFAndroidAnimation;
221
222FF_ANDROID_REQUIRES_API(31) static bool androidAnimationGetFrame(FFImageAnimation* animation, uint32_t index, FFImageFrame* out, const char** error) {
223 FFAndroidAnimation* session = (FFAndroidAnimation*) ffImageAnimationGetImpl(animation);
224
225 // Frames come out of the decoder in order, so anything before the current one means starting
226 // over. AImageDecoder_rewind needs an animated source; it is reached here only once a frame
227 // has already been taken, which means the source is one.
228 if (index < session->nextIndex) {
229 if (AImageDecoder_rewind(session->decoder) != ANDROID_IMAGE_DECODER_SUCCESS) {
230 return androidImageDecoderError(error, "failed to rewind the animation");
231 }
232 session->nextIndex = 0;
233 }
234
235 // Skipped frames still have to be decoded: each frame is blended into the buffer that the
236 // following one builds on, so the buffer would be wrong without them.
237 while (session->nextIndex < index) {
238 if (AImageDecoder_decodeImage(session->decoder, session->canvas, session->stride, session->size) != ANDROID_IMAGE_DECODER_SUCCESS) {
239 return androidImageDecoderError(error, "failed to decode an animation frame");
240 }
241 if (AImageDecoder_advanceFrame(session->decoder) != ANDROID_IMAGE_DECODER_SUCCESS) {
242 return androidImageDecoderError(error, "failed to advance to the next animation frame");
243 }
244 ++session->nextIndex;
245 }
246
247 if (AImageDecoder_decodeImage(session->decoder, session->canvas, session->stride, session->size) != ANDROID_IMAGE_DECODER_SUCCESS) {
248 return androidImageDecoderError(error, "failed to decode the animation frame");
249 }
250
251 uint8_t* frame = androidPackFrame(session->canvas, session->stride, session->width, session->height, session->premultiplied);
252 if (frame == nullptr) {
253 return androidImageDecoderError(error, "out of memory");
254 }
255
256 // Position on the following frame. Past the last one this reports FINISHED, which is expected
257 // and harmless: the frame just handed out is complete, and anything else the caller asks for
258 // restarts above.
259 AImageDecoder_advanceFrame(session->decoder);
260 ++session->nextIndex;
261
262 // The same mapping the ImageIO backend settled on: the raw gap is in centiseconds, a floor of
263 // 100 ms applies only when every frame is zero, and what is left at <= 0 means gapless.
264 const int32_t gap = (session->delaysCs[index] > session->minGap ? session->delaysCs[index] : session->minGap) * 10;
265 out->data = frame;
266 out->delayMs = gap > 0 ? gap : -1;
267 return true;
268}
269
270FF_ANDROID_REQUIRES_API(31) static void androidAnimationDestroy(FFImageAnimation* animation) {
271 FFAndroidAnimation* session = (FFAndroidAnimation*) ffImageAnimationGetImpl(animation);
272 if (session == nullptr) {
273 return;
274 }
275
276 AImageDecoder_delete(session->decoder);
277 // The decoder reads from the source, so the fd has to outlive every decode, which is why the
278 // session holds it rather than the function that opened it.
279 close(session->fd);
280 free(session->canvas);
281 free(session->delaysCs);
282 free(session);
283}
284
285bool ffImageAnimationOpenAID(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error) {
286 // Decoding past the first frame needs API 31: AImageDecoder_advanceFrame and
287 // AImageDecoderFrameInfo are both introduced there, and AImageDecoder_decodeImage only
288 // documents decoding "all of the frames" from that level on.
289 if (FF_ANDROID_API_AT_LEAST(31)) {
290 FF_AUTO_CLOSE_FD int fd = open(instance.config.logo.source.chars, O_RDONLY | O_CLOEXEC);
291 if (fd < 0) {
292 return androidImageDecoderError(error, "failed to open the image source");
293 }
294
295 AImageDecoder* decoder = nullptr;
296 if (AImageDecoder_createFromFd(fd, &decoder) != ANDROID_IMAGE_DECODER_SUCCESS) {
297 return androidImageDecoderError(error, "unsupported or unreadable image format");
298 }
299
300 size_t stride = 0;
301 bool premultiplied = true;
302 if (!androidResolveDecoder(decoder, requestData, &stride, &premultiplied, error)) {
303 AImageDecoder_delete(decoder);
304 return false;
305 }
306
307 AImageDecoderFrameInfo* frameInfo = AImageDecoderFrameInfo_create();
308 if (frameInfo == nullptr) {
309 AImageDecoder_delete(decoder);
310 return androidImageDecoderError(error, "out of memory");
311 }
312
313 // Walk the animation once, recording every gap on the way, so that both the frame count
314 // and each frame's timing are known before the first frame is decoded.
315 uint32_t capacity = 16;
316 uint32_t frameCount = 0;
317 int32_t* delays = (int32_t*) malloc(capacity * sizeof(*delays));
318 if (delays == nullptr) {
319 AImageDecoderFrameInfo_delete(frameInfo);
320 AImageDecoder_delete(decoder);
321 return androidImageDecoderError(error, "out of memory");
322 }
323
324 int32_t minGap = 10;
325 for (;;) {
326 if (frameCount == capacity) {
327 const uint32_t grown = capacity * 2;
328 int32_t* resized = (int32_t*) realloc(delays, grown * sizeof(*delays));
329 if (resized == nullptr) {
330 free(delays);
331 AImageDecoderFrameInfo_delete(frameInfo);
332 AImageDecoder_delete(decoder);
333 return androidImageDecoderError(error, "out of memory");
334 }
335 delays = resized;
336 capacity = grown;
337 }
338
339 int64_t nanos = 0;
340 if (AImageDecoder_getFrameInfo(decoder, frameInfo) == ANDROID_IMAGE_DECODER_SUCCESS) {
341 nanos = AImageDecoderFrameInfo_getDuration(frameInfo);
342 }
343 const int32_t cs = nanos > 0 ? (int32_t) (nanos / 10000000) : 0;
344 delays[frameCount++] = cs;
345 if (cs > 0) {
346 minGap = 0;
347 }
348
349 if (AImageDecoder_advanceFrame(decoder) != ANDROID_IMAGE_DECODER_SUCCESS) {
350 break;
351 }
352 }
353 AImageDecoderFrameInfo_delete(frameInfo);
354
355 // A single frame source never advanced, so it is still positioned on its only frame and
356 // has nothing to rewind. Everything else stopped in the finished state.
357 if (frameCount > 1 && AImageDecoder_rewind(decoder) != ANDROID_IMAGE_DECODER_SUCCESS) {
358 free(delays);
359 AImageDecoder_delete(decoder);
360 return androidImageDecoderError(error, "failed to rewind the animation");
361 }
362
363 FFAndroidAnimation* session = (FFAndroidAnimation*) calloc(1, sizeof(*session));
364 if (session == nullptr) {
365 free(delays);
366 AImageDecoder_delete(decoder);
367 return androidImageDecoderError(error, "out of memory");
368 }
369 session->decoder = decoder;
370 session->stride = stride;
371 session->size = stride * requestData->logoPixelHeight;
372 session->width = requestData->logoPixelWidth;
373 session->height = requestData->logoPixelHeight;
374 session->premultiplied = premultiplied;
375 session->minGap = minGap;
376 session->delaysCs = delays;
377
378 session->canvas = (uint8_t*) calloc(1, session->size);
379 if (session->canvas == nullptr) {
380 free(session->delaysCs);
381 free(session);
382 AImageDecoder_delete(decoder);
383 return androidImageDecoderError(error, "out of memory");
384 }
385
386 // 0: AImageDecoder has no API for the loop count, and looping forever is what a viewer
387 // does with an animation that does not declare one.
388 FFImageAnimation* animation = ffImageAnimationCreate(frameCount, 0, session, androidAnimationGetFrame, androidAnimationDestroy);
389 if (animation == nullptr) {
390 free(session->canvas);
391 free(session->delaysCs);
392 free(session);
393 AImageDecoder_delete(decoder);
394 return androidImageDecoderError(error, "out of memory");
395 }
396
397 // Frames are decoded after this function returns, and the decoder reads from the source every
398 // time it does. The fd therefore has to outlive the function: it is handed to the session,
399 // which closes it in androidAnimationDestroy. Clearing the local is what keeps the cleanup
400 // attribute from closing it here -- every path above this line still relies on that attribute.
401 session->fd = fd;
402 fd = -1;
403
404 *out = animation;
405 return true;
406 } else {
407 return androidImageDecoderError(error, "animation support requires Android 12 (API 31) or newer");
408 }
409}