mirror of
https://github.com/fastfetch-cli/fastfetch.git
synced 2026-10-28 07:44:06 +01:00
Image (Logo): uses native image decoder on Android
Codec (Android): uses `availability` feature test
This commit is contained in:
+6
-5
@@ -1,7 +1,7 @@
|
|||||||
# 2.69.0
|
# 2.69.0
|
||||||
|
|
||||||
Changes:
|
Changes:
|
||||||
* ImageMagick is no longer used for image logos on Windows and macOS, and has been replaced by the platform image frameworks (WIC on Windows, ImageIO on macOS). (Logo)
|
* ImageMagick is no longer used for image logos on Windows, macOS and Android, and has been replaced by the platform image frameworks (WIC on Windows, ImageIO on macOS, AImageDecoder on Android). (Logo)
|
||||||
* ImageMagick 6 support is deprecated. It is kept only for old Debian and Ubuntu releases that don't have ImageMagick 7 available.
|
* ImageMagick 6 support is deprecated. It is kept only for old Debian and Ubuntu releases that don't have ImageMagick 7 available.
|
||||||
* It is intended to be removed in a future release. Users are encouraged to upgrade to ImageMagick 7 when possible.
|
* It is intended to be removed in a future release. Users are encouraged to upgrade to ImageMagick 7 when possible.
|
||||||
|
|
||||||
@@ -17,9 +17,9 @@ Changes:
|
|||||||
Features:
|
Features:
|
||||||
* Improved image logo support
|
* Improved image logo support
|
||||||
* Backend rewritten
|
* Backend rewritten
|
||||||
* Added a native image decoding backend on Windows (WIC) and macOS (ImageIO).
|
* Added a native image decoding backend on Windows (WIC), macOS (ImageIO) and Android (AImageDecoder).
|
||||||
* Added an embedded libsixel encoder, used to produce sixel output on Windows and macOS. It is reported by `fastfetch --list-features` as "Embedded sixel".
|
* Added an embedded libsixel encoder, used to produce sixel output on Windows, macOS and Android. It is reported by `fastfetch --list-features` as "Embedded sixel".
|
||||||
* Enabled chafa image output on Windows and macOS independently of ImageMagick.
|
* Enabled chafa image output on Windows, macOS and Android independently of ImageMagick.
|
||||||
* As a result, `fastfetch --sixel X:\path\to\image` now works out of the box on Windows Terminal.
|
* As a result, `fastfetch --sixel X:\path\to\image` now works out of the box on Windows Terminal.
|
||||||
* Image logo cache entries are now validated against the modification time of the source image. (Logo)
|
* Image logo cache entries are now validated against the modification time of the source image. (Logo)
|
||||||
* Editing an image logo in place now invalidates its cached rendering.
|
* Editing an image logo in place now invalidates its cached rendering.
|
||||||
@@ -28,7 +28,8 @@ Features:
|
|||||||
* `--logo-animation-frame <0>` (`logo.animationFrame: 0` in the JSON config) plays a GIF or APNG. Only the `kitty` image protocol can play an animation; the frames are decoded and composed by fastfetch, so no external program is involved.
|
* `--logo-animation-frame <0>` (`logo.animationFrame: 0` in the JSON config) plays a GIF or APNG. Only the `kitty` image protocol can play an animation; the frames are decoded and composed by fastfetch, so no external program is involved.
|
||||||
* `--logo-animation-frame <N>` renders the Nth frame as a still image, and negative values count back from the end, so `-1` is the last frame. This works for the `sixel`, `kitty` and `chafa` logo types. Note that negative values can only be given in the JSON config, as the command line parser reads a leading `-` as another option.
|
* `--logo-animation-frame <N>` renders the Nth frame as a still image, and negative values count back from the end, so `-1` is the last frame. This works for the `sixel`, `kitty` and `chafa` logo types. Note that negative values can only be given in the JSON config, as the command line parser reads a leading `-` as another option.
|
||||||
* The default is `1`, which renders a still image, so nothing changes for anyone who does not opt in.
|
* The default is `1`, which renders a still image, so nothing changes for anyone who does not opt in.
|
||||||
* The frames come from the platform image framework (WIC on Windows, ImageIO on macOS, ImageMagick 7 on Linux). A single-frame GIF falls back to a still image. A build with none of those, or one built with ImageMagick 6, reports an error instead of silently showing a still image.
|
* The frames come from the platform image framework (WIC on Windows, ImageIO on macOS, AImageDecoder on Android, ImageMagick 7 on Linux). A single-frame GIF falls back to a still image.
|
||||||
|
* On Android an animation needs Android 12 (API 31), which is where AImageDecoder gained the ability to decode past the first frame. AImageDecoder composes the frames itself; its API does not expose a repeat count, so an animation is reported as looping forever. A build with none of those, or one built with ImageMagick 6, reports an error instead of silently showing a still image.
|
||||||
* A terminal that supports the kitty graphics protocol but not its animation frames, such as Konsole, shows the first frame.
|
* A terminal that supports the kitty graphics protocol but not its animation frames, such as Konsole, shows the first frame.
|
||||||
* Added the CMake option `ENABLE_IMAGE_LOGO`, which defaults to `ON`. Configure with `-DENABLE_IMAGE_LOGO=OFF` to build fastfetch without any image logo support. (Logo)
|
* Added the CMake option `ENABLE_IMAGE_LOGO`, which defaults to `ON`. Configure with `-DENABLE_IMAGE_LOGO=OFF` to build fastfetch without any image logo support. (Logo)
|
||||||
* Image logos are the only consumer of ImageMagick, chafa, and the embedded libsixel encoder, so none of the three is searched for at configure time, and no image decoding sources are compiled in.
|
* Image logos are the only consumer of ImageMagick, chafa, and the embedded libsixel encoder, so none of the three is searched for at configure time, and no image decoding sources are compiled in.
|
||||||
|
|||||||
+17
-3
@@ -99,10 +99,10 @@ cmake_dependent_option(ENABLE_THREADS "Enable multithreading" ON "Threads_FOUND"
|
|||||||
|
|
||||||
option(ENABLE_IMAGE_LOGO "Enable image logos (sixel / kitty / iTerm / chafa)" ON)
|
option(ENABLE_IMAGE_LOGO "Enable image logos (sixel / kitty / iTerm / chafa)" ON)
|
||||||
if(ENABLE_IMAGE_LOGO)
|
if(ENABLE_IMAGE_LOGO)
|
||||||
cmake_dependent_option(ENABLE_IMAGEMAGICK7 "Enable imagemagick 7" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR ANDROID OR SunOS OR Haiku OR GNU" OFF)
|
cmake_dependent_option(ENABLE_IMAGEMAGICK7 "Enable imagemagick 7" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR SunOS OR Haiku OR GNU" OFF)
|
||||||
cmake_dependent_option(ENABLE_IMAGEMAGICK6 "Enable imagemagick 6" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR SunOS OR GNU" OFF)
|
cmake_dependent_option(ENABLE_IMAGEMAGICK6 "Enable imagemagick 6" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR SunOS OR GNU" OFF)
|
||||||
cmake_dependent_option(ENABLE_SIXEL "Enable sixel logo output via the embedded libsixel encoder (Windows and macOS; replaces ImageMagick's SIXEL coder)" ON "WIN32 OR APPLE" OFF)
|
cmake_dependent_option(ENABLE_SIXEL "Enable sixel logo output via the embedded libsixel encoder (Windows, macOS and Android; replaces ImageMagick's SIXEL coder)" ON "WIN32 OR APPLE OR ANDROID" OFF)
|
||||||
cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7 OR WIN32 OR APPLE" OFF)
|
cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7 OR WIN32 OR APPLE OR ANDROID" OFF)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
option(ENABLE_ZLIB "Enable zlib" ON)
|
option(ENABLE_ZLIB "Enable zlib" ON)
|
||||||
@@ -695,6 +695,7 @@ elseif(ANDROID)
|
|||||||
src/detection/de/de_linux.c
|
src/detection/de/de_linux.c
|
||||||
src/detection/wmtheme/wmtheme_linux.c
|
src/detection/wmtheme/wmtheme_linux.c
|
||||||
src/detection/camera/camera_android.c
|
src/detection/camera/camera_android.c
|
||||||
|
src/logo/image/aid.c
|
||||||
)
|
)
|
||||||
elseif(FreeBSD)
|
elseif(FreeBSD)
|
||||||
list(APPEND LIBFASTFETCH_SRC
|
list(APPEND LIBFASTFETCH_SRC
|
||||||
@@ -2000,6 +2001,8 @@ elseif(GNU)
|
|||||||
elseif(ANDROID)
|
elseif(ANDROID)
|
||||||
target_link_libraries(libfastfetch
|
target_link_libraries(libfastfetch
|
||||||
PRIVATE "m"
|
PRIVATE "m"
|
||||||
|
PRIVATE jnigraphics
|
||||||
|
PRIVATE mediandk
|
||||||
)
|
)
|
||||||
if(ENABLE_WORDEXP)
|
if(ENABLE_WORDEXP)
|
||||||
# https://github.com/termux/termux-packages/pull/7056
|
# https://github.com/termux/termux-packages/pull/7056
|
||||||
@@ -2017,6 +2020,17 @@ elseif(ANDROID)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Both APIs (AImageDecoder, AMediaCodec) are newer than the API level this build targets --
|
||||||
|
# AImageDecoder is 30, and AMediaCodec_getName / AMediaCodec_releaseName are 28 -- while fastfetch
|
||||||
|
# keeps running on devices below those levels. The NDK headers mark the entry points unavailable, so
|
||||||
|
# they have to be weak references: otherwise the executable would fail to load on an older device.
|
||||||
|
# Guarding every use with __builtin_available() is what makes that safe, and turning the guard
|
||||||
|
# diagnostic into an error is what keeps a use from slipping past it.
|
||||||
|
set_source_files_properties(
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/logo/image/aid.c
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/detection/codec/codec_android.c
|
||||||
|
PROPERTIES COMPILE_OPTIONS "-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__;-Werror=unguarded-availability")
|
||||||
elseif(Haiku)
|
elseif(Haiku)
|
||||||
target_link_libraries(libfastfetch
|
target_link_libraries(libfastfetch
|
||||||
PRIVATE "network"
|
PRIVATE "network"
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// The NDK marks an API unavailable whenever it is newer than the API level the build targets, and
|
||||||
|
// fastfetch keeps running on devices below the levels it uses: AImageDecoder is 30, decoding past
|
||||||
|
// the first frame is 31, and AMediaCodec_getName is 28. CMake compiles the files that reach for
|
||||||
|
// them with -D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability, which turns
|
||||||
|
// those entry points into weak references and makes every unguarded use an error. Two things follow:
|
||||||
|
//
|
||||||
|
// * A weak reference resolves to null on an older device, so every use needs a run-time check --
|
||||||
|
// that is FF_API_AT_LEAST. Unguarded, the call would jump to a null pointer.
|
||||||
|
// * Annotating a helper with FF_REQUIRES_API keeps the check in one place: the compiler rejects
|
||||||
|
// any call of that helper which is not itself inside a guard.
|
||||||
|
//
|
||||||
|
// https://developer.android.com/ndk/guides/using-newer-apis
|
||||||
|
#define FF_REQUIRES_API(x) [[clang::availability(android, introduced = x)]]
|
||||||
|
#define FF_API_AT_LEAST(x) __builtin_available(android x, *)
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
#include "codec.h"
|
#include "codec.h"
|
||||||
|
|
||||||
#include "common/library.h"
|
#include "common/androidApi.h"
|
||||||
#include "common/strutil.h"
|
#include "common/strutil.h"
|
||||||
#undef __INTRODUCED_IN
|
|
||||||
#define __INTRODUCED_IN(...)
|
|
||||||
#include <media/NdkMediaCodec.h>
|
#include <media/NdkMediaCodec.h>
|
||||||
|
|
||||||
static const struct {
|
static const struct {
|
||||||
@@ -35,33 +33,25 @@ static bool ffCodecIsLikelySoftware(const char* codecName) {
|
|||||||
ffStrStartsWith(codecName, "OMX.PV.");
|
ffStrStartsWith(codecName, "OMX.PV.");
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool ffCodecIsHardwareAccelerated(
|
// Only the name query is newer than the API level this build targets: AMediaCodec_getName and
|
||||||
AMediaCodec* codec,
|
// AMediaCodec_releaseName are API 28, while creating and deleting a codec is API 21.
|
||||||
typeof(&AMediaCodec_getName) ffAMediaCodec_getName,
|
FF_REQUIRES_API(28) static bool ffCodecIsHardwareAccelerated(AMediaCodec* codec) {
|
||||||
typeof(&AMediaCodec_releaseName) ffAMediaCodec_releaseName) {
|
|
||||||
if (!codec) {
|
if (!codec) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
char* codecName = nullptr;
|
char* codecName = nullptr;
|
||||||
media_status_t status = ffAMediaCodec_getName(codec, &codecName);
|
media_status_t status = AMediaCodec_getName(codec, &codecName);
|
||||||
if (status != AMEDIA_OK || !codecName) {
|
if (status != AMEDIA_OK || !codecName) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isHardware = !ffCodecIsLikelySoftware(codecName);
|
bool isHardware = !ffCodecIsLikelySoftware(codecName);
|
||||||
ffAMediaCodec_releaseName(codec, codecName);
|
AMediaCodec_releaseName(codec, codecName);
|
||||||
return isHardware;
|
return isHardware;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /*list of FFCodecResult*/) {
|
FF_REQUIRES_API(28) static const char* ffDetectCodecNativeImpl(FFCodecOptions* options, FFlist* result /*list of FFCodecResult*/) {
|
||||||
FF_LIBRARY_LOAD_MESSAGE(mediandk, "libmediandk.so", 0)
|
|
||||||
FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mediandk, AMediaCodec_createDecoderByType)
|
|
||||||
FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mediandk, AMediaCodec_createEncoderByType)
|
|
||||||
FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mediandk, AMediaCodec_delete)
|
|
||||||
FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mediandk, AMediaCodec_getName)
|
|
||||||
FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mediandk, AMediaCodec_releaseName)
|
|
||||||
|
|
||||||
FFCodecType decoders = FF_CODEC_TYPE_NONE;
|
FFCodecType decoders = FF_CODEC_TYPE_NONE;
|
||||||
FFCodecType encoders = FF_CODEC_TYPE_NONE;
|
FFCodecType encoders = FF_CODEC_TYPE_NONE;
|
||||||
|
|
||||||
@@ -70,22 +60,22 @@ const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /*list o
|
|||||||
FFCodecType type = FF_CODEC_MIME_TO_TYPE[i].type;
|
FFCodecType type = FF_CODEC_MIME_TO_TYPE[i].type;
|
||||||
|
|
||||||
if ((options->showType & FF_CODEC_SHOW_TYPE_DECODER) && !(decoders & type)) {
|
if ((options->showType & FF_CODEC_SHOW_TYPE_DECODER) && !(decoders & type)) {
|
||||||
AMediaCodec* decoder = ffAMediaCodec_createDecoderByType(mime);
|
AMediaCodec* decoder = AMediaCodec_createDecoderByType(mime);
|
||||||
if (decoder) {
|
if (decoder) {
|
||||||
if (ffCodecIsHardwareAccelerated(decoder, ffAMediaCodec_getName, ffAMediaCodec_releaseName)) {
|
if (ffCodecIsHardwareAccelerated(decoder)) {
|
||||||
decoders |= type;
|
decoders |= type;
|
||||||
}
|
}
|
||||||
ffAMediaCodec_delete(decoder);
|
AMediaCodec_delete(decoder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((options->showType & FF_CODEC_SHOW_TYPE_ENCODER) && !(encoders & type)) {
|
if ((options->showType & FF_CODEC_SHOW_TYPE_ENCODER) && !(encoders & type)) {
|
||||||
AMediaCodec* encoder = ffAMediaCodec_createEncoderByType(mime);
|
AMediaCodec* encoder = AMediaCodec_createEncoderByType(mime);
|
||||||
if (encoder) {
|
if (encoder) {
|
||||||
if (ffCodecIsHardwareAccelerated(encoder, ffAMediaCodec_getName, ffAMediaCodec_releaseName)) {
|
if (ffCodecIsHardwareAccelerated(encoder)) {
|
||||||
encoders |= type;
|
encoders |= type;
|
||||||
}
|
}
|
||||||
ffAMediaCodec_delete(encoder);
|
AMediaCodec_delete(encoder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,3 +90,14 @@ const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /*list o
|
|||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /*list of FFCodecResult*/) {
|
||||||
|
if (FF_API_AT_LEAST(28)) {
|
||||||
|
return ffDetectCodecNativeImpl(options, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reading the codec name is the only way to tell a hardware codec from a software one. Without
|
||||||
|
// it there is nothing to report, and listing the codecs as if they were accelerated would be a
|
||||||
|
// guess, so say why instead.
|
||||||
|
return "AMediaCodec_getName() requires Android 9 (API 28)";
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,397 @@
|
|||||||
|
|
||||||
|
#include "image.h"
|
||||||
|
#include "common/androidApi.h"
|
||||||
|
#include "common/io.h"
|
||||||
|
#include "common/mallocHelper.h"
|
||||||
|
|
||||||
|
#include <android/bitmap.h>
|
||||||
|
#include <android/imagedecoder.h>
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static inline bool androidImageDecoderError(const char** error, const char* message) {
|
||||||
|
if (error) {
|
||||||
|
*error = message;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AImageDecoder decodes to premultiplied alpha, and it can not be asked for straight alpha once a
|
||||||
|
// target size is in effect: AImageDecoder_setUnpremultipliedRequired fails with
|
||||||
|
// ANDROID_IMAGE_DECODER_INVALID_CONVERSION, documented as "Unpremultiplied is not possible due to
|
||||||
|
// an existing scale set by AImageDecoder_setTargetSize". Premultiplied is the right domain to
|
||||||
|
// scale in anyway -- interpolating straight alpha averages the colour of fully transparent pixels
|
||||||
|
// into the edges next to them, which shows up as dark fringes -- so the decoder is left alone and
|
||||||
|
// this runs afterwards, exactly as the ImageIO backend does.
|
||||||
|
static void unPremultiplyRGBA(uint8_t* data, size_t pixelCount) {
|
||||||
|
for (size_t i = 0; i < pixelCount; ++i) {
|
||||||
|
uint8_t* const p = data + i * 4;
|
||||||
|
const uint8_t a = p[3];
|
||||||
|
if (a == 0) {
|
||||||
|
p[0] = p[1] = p[2] = 0;
|
||||||
|
} else if (a < 255) {
|
||||||
|
p[0] = (uint8_t) ((p[0] * 255 + a / 2) / a);
|
||||||
|
p[1] = (uint8_t) ((p[1] * 255 + a / 2) / a);
|
||||||
|
p[2] = (uint8_t) ((p[2] * 255 + a / 2) / a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy one decoded frame into a tightly packed, straight alpha buffer of its own.
|
||||||
|
//
|
||||||
|
// A copy rather than a hand-over: the animation path has to keep the decoder's own buffer intact,
|
||||||
|
// because AImageDecoder blends each frame into the one that is already there. The still path only
|
||||||
|
// needs the packed result, and going through here keeps the two identical.
|
||||||
|
//
|
||||||
|
// `premultiplied` is what the decoder reports about the pixels it produced, so a source that was
|
||||||
|
// decoded straight is copied and nothing else.
|
||||||
|
static uint8_t* androidPackFrame(const uint8_t* src, size_t stride, uint32_t width, uint32_t height, bool premultiplied) {
|
||||||
|
const size_t packedStride = (size_t) width * 4;
|
||||||
|
uint8_t* pixels = (uint8_t*) malloc(packedStride * height);
|
||||||
|
if (pixels == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint32_t y = 0; y < height; ++y) {
|
||||||
|
memcpy(pixels + (size_t) y * packedStride, src + (size_t) y * stride, packedStride);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (premultiplied) {
|
||||||
|
unPremultiplyRGBA(pixels, (size_t) width * height);
|
||||||
|
}
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything the still and the animation path share: read the source size, resolve the requested
|
||||||
|
// pixel size, ask for RGBA8888 and let the decoder do the scaling.
|
||||||
|
FF_REQUIRES_API(30) static bool androidResolveDecoder(AImageDecoder* decoder, FFLogoRequestData* requestData, size_t* outStride, bool* outPremultiplied, const char** error) {
|
||||||
|
const AImageDecoderHeaderInfo* header = AImageDecoder_getHeaderInfo(decoder);
|
||||||
|
// Both are int32_t, and anything <= 0 is not a usable source
|
||||||
|
const int32_t sourceWidth = AImageDecoderHeaderInfo_getWidth(header);
|
||||||
|
const int32_t sourceHeight = AImageDecoderHeaderInfo_getHeight(header);
|
||||||
|
if (sourceWidth <= 0 || sourceHeight <= 0) {
|
||||||
|
return androidImageDecoderError(error, "invalid image dimensions");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in the missing dimension, keeping the source aspect ratio (same as the IM path)
|
||||||
|
uint32_t width = requestData->logoPixelWidth;
|
||||||
|
uint32_t height = requestData->logoPixelHeight;
|
||||||
|
if (width == 0 && height == 0) {
|
||||||
|
width = (uint32_t) sourceWidth;
|
||||||
|
height = (uint32_t) sourceHeight;
|
||||||
|
} else if (width == 0) {
|
||||||
|
width = (uint32_t) ((double) sourceWidth / (double) sourceHeight * height);
|
||||||
|
} else if (height == 0) {
|
||||||
|
height = (uint32_t) ((double) sourceHeight / (double) sourceWidth * width);
|
||||||
|
}
|
||||||
|
if (width == 0 || height == 0) {
|
||||||
|
return androidImageDecoderError(error, "invalid target dimensions");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AImageDecoder_setAndroidBitmapFormat(decoder, ANDROID_BITMAP_FORMAT_RGBA_8888) != ANDROID_IMAGE_DECODER_SUCCESS) {
|
||||||
|
return androidImageDecoderError(error, "failed to select the RGBA8888 output format");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A source that is already the requested size needs no scaling at all, and leaving the target
|
||||||
|
// size unset buys one more thing: AImageDecoder refuses straight alpha only *because* of a
|
||||||
|
// scale, so unscaled output can be decoded straight and no conversion follows. That is also
|
||||||
|
// the more accurate of the two, having no integer rounding to recover from.
|
||||||
|
if (width == (uint32_t) sourceWidth && height == (uint32_t) sourceHeight) {
|
||||||
|
*outPremultiplied = AImageDecoder_setUnpremultipliedRequired(decoder, true) != ANDROID_IMAGE_DECODER_SUCCESS;
|
||||||
|
} else {
|
||||||
|
// Scaling is part of the decode: the decoder samples the source down (or up) as it goes, so
|
||||||
|
// there is neither a full-size intermediate nor a separate resize step.
|
||||||
|
if (AImageDecoder_setTargetSize(decoder, (int32_t) width, (int32_t) height) != ANDROID_IMAGE_DECODER_SUCCESS) {
|
||||||
|
return androidImageDecoderError(error, "failed to scale the image");
|
||||||
|
}
|
||||||
|
*outPremultiplied = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t stride = AImageDecoder_getMinimumStride(decoder);
|
||||||
|
if (stride < (size_t) width * 4 || stride > SIZE_MAX / height) {
|
||||||
|
return androidImageDecoderError(error, "invalid target dimensions");
|
||||||
|
}
|
||||||
|
|
||||||
|
requestData->logoPixelWidth = width;
|
||||||
|
requestData->logoPixelHeight = height;
|
||||||
|
*outStride = stride;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ffImageCreateAID(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) {
|
||||||
|
// AImageDecoder is API 30, and fastfetch still runs on devices below that, so this is a real
|
||||||
|
// run-time check and not a compile-time constant. See common/androidApi.h for why.
|
||||||
|
if (FF_API_AT_LEAST(30)) {
|
||||||
|
FF_AUTO_CLOSE_FD int fd = open(instance.config.logo.source.chars, O_RDONLY | O_CLOEXEC);
|
||||||
|
if (fd < 0) {
|
||||||
|
return androidImageDecoderError(error, "failed to open the image source");
|
||||||
|
}
|
||||||
|
|
||||||
|
AImageDecoder* decoder = nullptr;
|
||||||
|
if (AImageDecoder_createFromFd(fd, &decoder) != ANDROID_IMAGE_DECODER_SUCCESS) {
|
||||||
|
return androidImageDecoderError(error, "unsupported or unreadable image format");
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t stride = 0;
|
||||||
|
bool premultiplied = true;
|
||||||
|
if (!androidResolveDecoder(decoder, requestData, &stride, &premultiplied, error)) {
|
||||||
|
AImageDecoder_delete(decoder);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t width = requestData->logoPixelWidth;
|
||||||
|
const uint32_t height = requestData->logoPixelHeight;
|
||||||
|
const size_t packedStride = (size_t) width * 4;
|
||||||
|
const size_t bufferSize = stride * height;
|
||||||
|
|
||||||
|
FF_AUTO_FREE uint8_t* decoded = (uint8_t*) malloc(bufferSize);
|
||||||
|
if (decoded == nullptr) {
|
||||||
|
AImageDecoder_delete(decoder);
|
||||||
|
return androidImageDecoderError(error, "out of memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
const int result = AImageDecoder_decodeImage(decoder, decoded, stride, bufferSize);
|
||||||
|
// The decoder reads from the fd while decoding, so it outlives the decode but not the fd
|
||||||
|
AImageDecoder_delete(decoder);
|
||||||
|
if (result != ANDROID_IMAGE_DECODER_SUCCESS) {
|
||||||
|
return androidImageDecoderError(error, "failed to decode the image");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without row padding the decoder's buffer is already laid out the way the contract wants,
|
||||||
|
// so it is finished in place and handed over rather than copied.
|
||||||
|
if (stride == packedStride) {
|
||||||
|
if (premultiplied) {
|
||||||
|
unPremultiplyRGBA(decoded, (size_t) width * height);
|
||||||
|
}
|
||||||
|
out->data = decoded;
|
||||||
|
decoded = nullptr; // Ownership is transferred to `out`
|
||||||
|
} else {
|
||||||
|
out->data = androidPackFrame(decoded, stride, width, height, premultiplied);
|
||||||
|
if (out->data == nullptr) {
|
||||||
|
return androidImageDecoderError(error, "out of memory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out->width = width;
|
||||||
|
out->height = height;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return androidImageDecoderError(error, "AImageDecoder requires Android 11 (API 30) or newer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// Animation
|
||||||
|
//
|
||||||
|
// AImageDecoder composes the frames itself. Given the same buffer for every call, it decodes the
|
||||||
|
// part of the canvas a frame actually covers and blends it over what is already there, and it
|
||||||
|
// restores the buffer for a DISPOSE_OP_PREVIOUS frame on its own. So unlike the Windows backend,
|
||||||
|
// there is no canvas to maintain here: the session keeps the decoder's buffer and hands out a copy
|
||||||
|
// of it per frame.
|
||||||
|
//
|
||||||
|
// Two things follow from the decoder only ever walking forwards:
|
||||||
|
//
|
||||||
|
// * The frame count and every gap have to be collected up front by advancing through the whole
|
||||||
|
// animation and rewinding, which is what the session contract asks for anyway -- a negative
|
||||||
|
// --logo-animation-frame has to be resolved before the first frame is taken.
|
||||||
|
// * Reaching a frame that is not the next one means rewinding and walking forward again, which is
|
||||||
|
// slower than refusing it but keeps the iterator usable after the last frame.
|
||||||
|
//
|
||||||
|
// The loop count is the one thing AImageDecoder does not expose: it has no API for it at any
|
||||||
|
// level, so the animation is reported as looping forever, which is what an animated GIF with no
|
||||||
|
// NETSCAPE block does in every viewer anyway.
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
typedef struct FFAndroidAnimation {
|
||||||
|
AImageDecoder* decoder;
|
||||||
|
uint8_t* canvas; // the decoder's buffer; every frame is blended into what is already in it
|
||||||
|
size_t stride;
|
||||||
|
size_t size;
|
||||||
|
uint32_t width;
|
||||||
|
uint32_t height;
|
||||||
|
uint32_t nextIndex; // the frame the decoder is positioned on
|
||||||
|
bool premultiplied; // whether the decoder's frames need un-premultiplying
|
||||||
|
int32_t minGap;
|
||||||
|
int32_t* delaysCs; // one per frame, in centiseconds
|
||||||
|
} FFAndroidAnimation;
|
||||||
|
|
||||||
|
FF_REQUIRES_API(31) static bool androidAnimationGetFrame(FFImageAnimation* animation, uint32_t index, FFImageFrame* out, const char** error) {
|
||||||
|
FFAndroidAnimation* session = (FFAndroidAnimation*) ffImageAnimationGetImpl(animation);
|
||||||
|
|
||||||
|
// Frames come out of the decoder in order, so anything before the current one means starting
|
||||||
|
// over. AImageDecoder_rewind needs an animated source; it is reached here only once a frame
|
||||||
|
// has already been taken, which means the source is one.
|
||||||
|
if (index < session->nextIndex) {
|
||||||
|
if (AImageDecoder_rewind(session->decoder) != ANDROID_IMAGE_DECODER_SUCCESS) {
|
||||||
|
return androidImageDecoderError(error, "failed to rewind the animation");
|
||||||
|
}
|
||||||
|
session->nextIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skipped frames still have to be decoded: each frame is blended into the buffer that the
|
||||||
|
// following one builds on, so the buffer would be wrong without them.
|
||||||
|
while (session->nextIndex < index) {
|
||||||
|
if (AImageDecoder_decodeImage(session->decoder, session->canvas, session->stride, session->size) != ANDROID_IMAGE_DECODER_SUCCESS) {
|
||||||
|
return androidImageDecoderError(error, "failed to decode an animation frame");
|
||||||
|
}
|
||||||
|
if (AImageDecoder_advanceFrame(session->decoder) != ANDROID_IMAGE_DECODER_SUCCESS) {
|
||||||
|
return androidImageDecoderError(error, "failed to advance to the next animation frame");
|
||||||
|
}
|
||||||
|
++session->nextIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AImageDecoder_decodeImage(session->decoder, session->canvas, session->stride, session->size) != ANDROID_IMAGE_DECODER_SUCCESS) {
|
||||||
|
return androidImageDecoderError(error, "failed to decode the animation frame");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* frame = androidPackFrame(session->canvas, session->stride, session->width, session->height, session->premultiplied);
|
||||||
|
if (frame == nullptr) {
|
||||||
|
return androidImageDecoderError(error, "out of memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position on the following frame. Past the last one this reports FINISHED, which is expected
|
||||||
|
// and harmless: the frame just handed out is complete, and anything else the caller asks for
|
||||||
|
// restarts above.
|
||||||
|
AImageDecoder_advanceFrame(session->decoder);
|
||||||
|
++session->nextIndex;
|
||||||
|
|
||||||
|
// The same mapping the ImageIO backend settled on: the raw gap is in centiseconds, a floor of
|
||||||
|
// 100 ms applies only when every frame is zero, and what is left at <= 0 means gapless.
|
||||||
|
const int32_t gap = (session->delaysCs[index] > session->minGap ? session->delaysCs[index] : session->minGap) * 10;
|
||||||
|
out->data = frame;
|
||||||
|
out->delayMs = gap > 0 ? gap : -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FF_REQUIRES_API(31) static void androidAnimationDestroy(FFImageAnimation* animation) {
|
||||||
|
FFAndroidAnimation* session = (FFAndroidAnimation*) ffImageAnimationGetImpl(animation);
|
||||||
|
if (session == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AImageDecoder_delete(session->decoder);
|
||||||
|
free(session->canvas);
|
||||||
|
free(session->delaysCs);
|
||||||
|
free(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ffImageAnimationOpenAID(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error) {
|
||||||
|
// Decoding past the first frame needs API 31: AImageDecoder_advanceFrame and
|
||||||
|
// AImageDecoderFrameInfo are both introduced there, and AImageDecoder_decodeImage only
|
||||||
|
// documents decoding "all of the frames" from that level on.
|
||||||
|
if (FF_API_AT_LEAST(31)) {
|
||||||
|
FF_AUTO_CLOSE_FD int fd = open(instance.config.logo.source.chars, O_RDONLY | O_CLOEXEC);
|
||||||
|
if (fd < 0) {
|
||||||
|
return androidImageDecoderError(error, "failed to open the image source");
|
||||||
|
}
|
||||||
|
|
||||||
|
AImageDecoder* decoder = nullptr;
|
||||||
|
if (AImageDecoder_createFromFd(fd, &decoder) != ANDROID_IMAGE_DECODER_SUCCESS) {
|
||||||
|
return androidImageDecoderError(error, "unsupported or unreadable image format");
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t stride = 0;
|
||||||
|
bool premultiplied = true;
|
||||||
|
if (!androidResolveDecoder(decoder, requestData, &stride, &premultiplied, error)) {
|
||||||
|
AImageDecoder_delete(decoder);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AImageDecoderFrameInfo* frameInfo = AImageDecoderFrameInfo_create();
|
||||||
|
if (frameInfo == nullptr) {
|
||||||
|
AImageDecoder_delete(decoder);
|
||||||
|
return androidImageDecoderError(error, "out of memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk the animation once, recording every gap on the way, so that both the frame count
|
||||||
|
// and each frame's timing are known before the first frame is decoded.
|
||||||
|
uint32_t capacity = 16;
|
||||||
|
uint32_t frameCount = 0;
|
||||||
|
int32_t* delays = (int32_t*) malloc(capacity * sizeof(*delays));
|
||||||
|
if (delays == nullptr) {
|
||||||
|
AImageDecoderFrameInfo_delete(frameInfo);
|
||||||
|
AImageDecoder_delete(decoder);
|
||||||
|
return androidImageDecoderError(error, "out of memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t minGap = 10;
|
||||||
|
for (;;) {
|
||||||
|
if (frameCount == capacity) {
|
||||||
|
const uint32_t grown = capacity * 2;
|
||||||
|
int32_t* resized = (int32_t*) realloc(delays, grown * sizeof(*delays));
|
||||||
|
if (resized == nullptr) {
|
||||||
|
free(delays);
|
||||||
|
AImageDecoderFrameInfo_delete(frameInfo);
|
||||||
|
AImageDecoder_delete(decoder);
|
||||||
|
return androidImageDecoderError(error, "out of memory");
|
||||||
|
}
|
||||||
|
delays = resized;
|
||||||
|
capacity = grown;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t nanos = 0;
|
||||||
|
if (AImageDecoder_getFrameInfo(decoder, frameInfo) == ANDROID_IMAGE_DECODER_SUCCESS) {
|
||||||
|
nanos = AImageDecoderFrameInfo_getDuration(frameInfo);
|
||||||
|
}
|
||||||
|
const int32_t cs = nanos > 0 ? (int32_t) (nanos / 10000000) : 0;
|
||||||
|
delays[frameCount++] = cs;
|
||||||
|
if (cs > 0) {
|
||||||
|
minGap = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AImageDecoder_advanceFrame(decoder) != ANDROID_IMAGE_DECODER_SUCCESS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AImageDecoderFrameInfo_delete(frameInfo);
|
||||||
|
|
||||||
|
// A single frame source never advanced, so it is still positioned on its only frame and
|
||||||
|
// has nothing to rewind. Everything else stopped in the finished state.
|
||||||
|
if (frameCount > 1 && AImageDecoder_rewind(decoder) != ANDROID_IMAGE_DECODER_SUCCESS) {
|
||||||
|
free(delays);
|
||||||
|
AImageDecoder_delete(decoder);
|
||||||
|
return androidImageDecoderError(error, "failed to rewind the animation");
|
||||||
|
}
|
||||||
|
|
||||||
|
FFAndroidAnimation* session = (FFAndroidAnimation*) calloc(1, sizeof(*session));
|
||||||
|
if (session == nullptr) {
|
||||||
|
free(delays);
|
||||||
|
AImageDecoder_delete(decoder);
|
||||||
|
return androidImageDecoderError(error, "out of memory");
|
||||||
|
}
|
||||||
|
session->decoder = decoder;
|
||||||
|
session->stride = stride;
|
||||||
|
session->size = stride * requestData->logoPixelHeight;
|
||||||
|
session->width = requestData->logoPixelWidth;
|
||||||
|
session->height = requestData->logoPixelHeight;
|
||||||
|
session->premultiplied = premultiplied;
|
||||||
|
session->minGap = minGap;
|
||||||
|
session->delaysCs = delays;
|
||||||
|
|
||||||
|
session->canvas = (uint8_t*) calloc(1, session->size);
|
||||||
|
if (session->canvas == nullptr) {
|
||||||
|
free(session->delaysCs);
|
||||||
|
free(session);
|
||||||
|
AImageDecoder_delete(decoder);
|
||||||
|
return androidImageDecoderError(error, "out of memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0: AImageDecoder has no API for the loop count, and looping forever is what a viewer
|
||||||
|
// does with an animation that does not declare one.
|
||||||
|
FFImageAnimation* animation = ffImageAnimationCreate(frameCount, 0, session, androidAnimationGetFrame, androidAnimationDestroy);
|
||||||
|
if (animation == nullptr) {
|
||||||
|
free(session->canvas);
|
||||||
|
free(session->delaysCs);
|
||||||
|
free(session);
|
||||||
|
AImageDecoder_delete(decoder);
|
||||||
|
return androidImageDecoderError(error, "out of memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
*out = animation;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return androidImageDecoderError(error, "animation support requires Android 12 (API 31) or newer");
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-10
@@ -447,7 +447,7 @@ static bool printImageKittyDirect(bool printError) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(__APPLE__) || defined(FF_HAVE_SIXEL)
|
#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(__APPLE__) || defined(__ANDROID__) || defined(FF_HAVE_SIXEL)
|
||||||
|
|
||||||
#define FF_KITTY_MAX_CHUNK_SIZE 4096
|
#define FF_KITTY_MAX_CHUNK_SIZE 4096
|
||||||
|
|
||||||
@@ -807,6 +807,8 @@ bool ffImageCreate(FFLogoRequestData* requestData, FFImageBuffer* out, const cha
|
|||||||
return ffImageCreateWIC(requestData, out, error);
|
return ffImageCreateWIC(requestData, out, error);
|
||||||
#elif defined(__APPLE__)
|
#elif defined(__APPLE__)
|
||||||
return ffImageCreateImageIO(requestData, out, error);
|
return ffImageCreateImageIO(requestData, out, error);
|
||||||
|
#elif defined(__ANDROID__)
|
||||||
|
return ffImageCreateAID(requestData, out, error);
|
||||||
#else
|
#else
|
||||||
#ifdef FF_HAVE_IMAGEMAGICK7
|
#ifdef FF_HAVE_IMAGEMAGICK7
|
||||||
if (ffImageCreateIM7(requestData, out, error)) {
|
if (ffImageCreateIM7(requestData, out, error)) {
|
||||||
@@ -879,6 +881,8 @@ bool ffImageAnimationOpen(FFLogoRequestData* requestData, FFImageAnimation** out
|
|||||||
return ffImageAnimationOpenWIC(requestData, out, error);
|
return ffImageAnimationOpenWIC(requestData, out, error);
|
||||||
#elif defined(__APPLE__)
|
#elif defined(__APPLE__)
|
||||||
return ffImageAnimationOpenImageIO(requestData, out, error);
|
return ffImageAnimationOpenImageIO(requestData, out, error);
|
||||||
|
#elif defined(__ANDROID__)
|
||||||
|
return ffImageAnimationOpenAID(requestData, out, error);
|
||||||
#elif defined(FF_HAVE_IMAGEMAGICK7)
|
#elif defined(FF_HAVE_IMAGEMAGICK7)
|
||||||
return ffImageAnimationOpenIM7(requestData, out, error);
|
return ffImageAnimationOpenIM7(requestData, out, error);
|
||||||
#else
|
#else
|
||||||
@@ -915,10 +919,10 @@ void ffImageAnimationClose(FFImageAnimation* animation) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const char** error) {
|
bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const char** error) {
|
||||||
// Windows (WIC) and macOS (ImageIO) decode and resize to RGBA first, then the embedded
|
// Windows (WIC), macOS (ImageIO) and Android (AImageDecoder) decode and resize to RGBA first,
|
||||||
// libsixel encoder takes over. Other platforms let ImageMagick encode straight from the
|
// then the embedded libsixel encoder takes over. Other platforms let ImageMagick encode
|
||||||
// decoded image without an RGBA round trip.
|
// straight from the decoded image without an RGBA round trip.
|
||||||
#if defined(_WIN32) || defined(__APPLE__)
|
#if defined(_WIN32) || defined(__APPLE__) || defined(__ANDROID__)
|
||||||
#ifdef FF_HAVE_SIXEL
|
#ifdef FF_HAVE_SIXEL
|
||||||
FFImageBuffer buffer = {};
|
FFImageBuffer buffer = {};
|
||||||
if (!ffImageCreate(requestData, &buffer, error)) {
|
if (!ffImageCreate(requestData, &buffer, error)) {
|
||||||
@@ -950,10 +954,10 @@ bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const cha
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ffImageSixelEncodeBuffer(const FFImageBuffer* buffer, FFstrbuf* out, const char** error) {
|
bool ffImageSixelEncodeBuffer(const FFImageBuffer* buffer, FFstrbuf* out, const char** error) {
|
||||||
// Windows and macOS hand the pixels to the embedded encoder. ImageMagick has no way to encode
|
// Windows, macOS and Android hand the pixels to the embedded encoder. ImageMagick has no way to
|
||||||
// pixels it was not given an Image for, so its SIXEL coder is reached through a ConstituteImage
|
// encode pixels it was not given an Image for, so its SIXEL coder is reached through a
|
||||||
// round trip instead.
|
// ConstituteImage round trip instead.
|
||||||
#if defined(_WIN32) || defined(__APPLE__)
|
#if defined(_WIN32) || defined(__APPLE__) || defined(__ANDROID__)
|
||||||
#ifdef FF_HAVE_SIXEL
|
#ifdef FF_HAVE_SIXEL
|
||||||
return ffSixelEncode(buffer, out, error);
|
return ffSixelEncode(buffer, out, error);
|
||||||
#else
|
#else
|
||||||
@@ -1780,7 +1784,7 @@ bool ffLogoPrintImageIfExists(FFLogoType type, bool printError) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(FF_HAVE_IMAGEMAGICK7) && !defined(FF_HAVE_IMAGEMAGICK6)
|
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && !defined(FF_HAVE_IMAGEMAGICK7) && !defined(FF_HAVE_IMAGEMAGICK6)
|
||||||
if (printError) {
|
if (printError) {
|
||||||
fputs("Logo: Image Magick support is not compiled in\n", stderr);
|
fputs("Logo: Image Magick support is not compiled in\n", stderr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include "../logo.h"
|
#include "../logo.h"
|
||||||
|
|
||||||
#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(__APPLE__) || defined(FF_HAVE_SIXEL)
|
#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(__APPLE__) || defined(__ANDROID__) || defined(FF_HAVE_SIXEL)
|
||||||
|
|
||||||
typedef enum FFLogoImageResult: uint8_t {
|
typedef enum FFLogoImageResult: uint8_t {
|
||||||
FF_LOGO_IMAGE_RESULT_SUCCESS, // Logo printed
|
FF_LOGO_IMAGE_RESULT_SUCCESS, // Logo printed
|
||||||
@@ -119,6 +119,11 @@ bool ffImageCreateImageIO(FFLogoRequestData* requestData, FFImageBuffer* out, co
|
|||||||
bool ffImageAnimationOpenImageIO(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error);
|
bool ffImageAnimationOpenImageIO(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
bool ffImageCreateAID(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error);
|
||||||
|
bool ffImageAnimationOpenAID(FFLogoRequestData* requestData, FFImageAnimation** out, const char** error);
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef FF_HAVE_SIXEL
|
#ifdef FF_HAVE_SIXEL
|
||||||
bool ffSixelEncode(const FFImageBuffer* buffer, FFstrbuf* result, const char** error);
|
bool ffSixelEncode(const FFImageBuffer* buffer, FFstrbuf* result, const char** error);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user