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