Animated thumbnails now show a small play-arrow badge (bottom-right) so users can spot which images move before hovering; it's hidden while the image plays on hover. Detection is cheap — a new util::IsAnimatedImageFile probes without a full decode: animated WebP via WebPGetFeatures.has_animation, and a multi-frame GIF via a lightweight image-descriptor block walk (stops at the 2nd frame). The picker only probes .gif/.webp thumbnails and caches the result on the Thumb. Verified: animated GIF + animated WebP report animated; still GIF/WebP/PNG do not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
530 lines
19 KiB
C++
530 lines
19 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "texture_loader.h"
|
|
|
|
// stb_image — single-file image loader (public domain)
|
|
// Only compiled here; all other files just include the header.
|
|
#define STB_IMAGE_IMPLEMENTATION
|
|
// Formats stb decodes for us: PNG for app assets, plus the common user-avatar formats. WebP is handled
|
|
// separately (libwebp) below. Keep this list in sync with ImagePicker's accepted extensions.
|
|
#define STBI_ONLY_PNG
|
|
#define STBI_ONLY_JPEG
|
|
#define STBI_ONLY_BMP
|
|
#define STBI_ONLY_GIF
|
|
#define STBI_ONLY_TGA
|
|
#define STBI_ONLY_PSD
|
|
#define STBI_ONLY_PNM // .pnm/.ppm/.pgm/.pbm
|
|
#define STBI_ONLY_PIC
|
|
#define STBI_NO_STDIO // we do our own fread for portability
|
|
#include "stb_image.h"
|
|
|
|
#include <webp/decode.h>
|
|
#include <webp/demux.h> // WebPAnimDecoder — animated WebP
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
#ifdef DRAGONX_USE_DX11
|
|
#include <d3d11.h>
|
|
|
|
// Get D3D11 device from ImGui backend (same pattern as qr_code.cpp)
|
|
static ID3D11Device* GetImGuiD3D11Device()
|
|
{
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
if (!io.BackendRendererUserData) return nullptr;
|
|
return *reinterpret_cast<ID3D11Device**>(io.BackendRendererUserData);
|
|
}
|
|
#else
|
|
#ifdef DRAGONX_HAS_GLAD
|
|
#include <glad/gl.h>
|
|
#else
|
|
#include <SDL3/SDL_opengl.h>
|
|
#endif
|
|
#endif
|
|
|
|
#include "../util/logger.h"
|
|
|
|
namespace dragonx {
|
|
namespace util {
|
|
|
|
// True if the buffer is a WebP (RIFF....WEBP) container.
|
|
static bool IsWebP(const unsigned char* d, size_t n)
|
|
{
|
|
return n >= 12 && memcmp(d, "RIFF", 4) == 0 && memcmp(d + 8, "WEBP", 4) == 0;
|
|
}
|
|
|
|
// Decode any supported image to a fresh RGBA8 buffer. WebP goes through libwebp; everything else
|
|
// through stb. The returned buffer is always free()-able (stbi's default free is free(), and the WebP
|
|
// path decodes into a malloc'd buffer), so FreeRawPixels(stbi_image_free) releases either. NULL on fail.
|
|
static unsigned char* DecodeImageRGBA(const unsigned char* data, size_t len, int* outW, int* outH)
|
|
{
|
|
if (IsWebP(data, len)) {
|
|
int w = 0, h = 0;
|
|
if (!WebPGetInfo(data, len, &w, &h) || w <= 0 || h <= 0) return nullptr;
|
|
unsigned char* buf = (unsigned char*)malloc((size_t)w * h * 4);
|
|
if (!buf) return nullptr;
|
|
if (!WebPDecodeRGBAInto(data, len, buf, (size_t)w * h * 4, w * 4)) { free(buf); return nullptr; }
|
|
*outW = w; *outH = h;
|
|
return buf;
|
|
}
|
|
int channels = 0;
|
|
return stbi_load_from_memory(data, (int)len, outW, outH, &channels, 4);
|
|
}
|
|
|
|
// Read entire file into memory
|
|
static bool ReadFileToBuffer(const char* path, std::vector<unsigned char>& buf)
|
|
{
|
|
FILE* f = fopen(path, "rb");
|
|
if (!f) return false;
|
|
fseek(f, 0, SEEK_END);
|
|
long sz = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
if (sz <= 0) { fclose(f); return false; }
|
|
buf.resize((size_t)sz);
|
|
size_t rd = fread(buf.data(), 1, (size_t)sz, f);
|
|
fclose(f);
|
|
return rd == (size_t)sz;
|
|
}
|
|
|
|
bool LoadTextureFromFile(const char* path, ImTextureID* outTexID, int* outW, int* outH)
|
|
{
|
|
std::vector<unsigned char> fileData;
|
|
if (!ReadFileToBuffer(path, fileData)) {
|
|
DEBUG_LOGF("LoadTextureFromFile: failed to read '%s'\n", path);
|
|
return false;
|
|
}
|
|
|
|
int w = 0, h = 0;
|
|
unsigned char* pixels = DecodeImageRGBA(fileData.data(), fileData.size(), &w, &h);
|
|
if (!pixels) {
|
|
DEBUG_LOGF("LoadTextureFromFile: decode failed for '%s'\n", path);
|
|
return false;
|
|
}
|
|
|
|
#ifdef DRAGONX_USE_DX11
|
|
ID3D11Device* device = GetImGuiD3D11Device();
|
|
if (!device) {
|
|
stbi_image_free(pixels);
|
|
DEBUG_LOGF("LoadTextureFromFile: no D3D11 device available\n");
|
|
return false;
|
|
}
|
|
|
|
D3D11_TEXTURE2D_DESC desc = {};
|
|
desc.Width = w;
|
|
desc.Height = h;
|
|
desc.MipLevels = 1;
|
|
desc.ArraySize = 1;
|
|
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
desc.SampleDesc.Count = 1;
|
|
desc.Usage = D3D11_USAGE_DEFAULT;
|
|
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
|
|
|
D3D11_SUBRESOURCE_DATA initData = {};
|
|
initData.pSysMem = pixels;
|
|
initData.SysMemPitch = w * 4;
|
|
|
|
ID3D11Texture2D* texture = nullptr;
|
|
HRESULT hr = device->CreateTexture2D(&desc, &initData, &texture);
|
|
stbi_image_free(pixels);
|
|
if (FAILED(hr) || !texture) {
|
|
DEBUG_LOGF("LoadTextureFromFile: CreateTexture2D failed for '%s'\n", path);
|
|
return false;
|
|
}
|
|
|
|
ID3D11ShaderResourceView* srv = nullptr;
|
|
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
|
|
srvDesc.Format = desc.Format;
|
|
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
|
|
srvDesc.Texture2D.MipLevels = 1;
|
|
|
|
hr = device->CreateShaderResourceView(texture, &srvDesc, &srv);
|
|
texture->Release();
|
|
if (FAILED(hr) || !srv) {
|
|
DEBUG_LOGF("LoadTextureFromFile: CreateSRV failed for '%s'\n", path);
|
|
return false;
|
|
}
|
|
|
|
*outTexID = (ImTextureID)(intptr_t)srv;
|
|
*outW = w;
|
|
*outH = h;
|
|
DEBUG_LOGF("LoadTextureFromFile: loaded '%s' (%dx%d) -> DX11 SRV %p\n", path, w, h, (void*)srv);
|
|
return true;
|
|
#else
|
|
|
|
GLuint tex = 0;
|
|
glGenTextures(1, &tex);
|
|
glBindTexture(GL_TEXTURE_2D, tex);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0,
|
|
GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
|
glGenerateMipmap(GL_TEXTURE_2D);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
|
|
stbi_image_free(pixels);
|
|
|
|
*outTexID = (ImTextureID)(intptr_t)tex;
|
|
*outW = w;
|
|
*outH = h;
|
|
DEBUG_LOGF("LoadTextureFromFile: loaded '%s' (%dx%d) -> texture %u\n", path, w, h, tex);
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
bool LoadTextureFromMemory(const unsigned char* data, size_t dataSize,
|
|
ImTextureID* outTexID, int* outW, int* outH)
|
|
{
|
|
if (!data || dataSize == 0) {
|
|
DEBUG_LOGF("LoadTextureFromMemory: null/empty data\n");
|
|
return false;
|
|
}
|
|
|
|
int w = 0, h = 0;
|
|
unsigned char* pixels = DecodeImageRGBA(data, dataSize, &w, &h);
|
|
if (!pixels) {
|
|
DEBUG_LOGF("LoadTextureFromMemory: decode failed\n");
|
|
return false;
|
|
}
|
|
|
|
#ifdef DRAGONX_USE_DX11
|
|
ID3D11Device* device = GetImGuiD3D11Device();
|
|
if (!device) {
|
|
stbi_image_free(pixels);
|
|
DEBUG_LOGF("LoadTextureFromMemory: no D3D11 device available\n");
|
|
return false;
|
|
}
|
|
|
|
D3D11_TEXTURE2D_DESC desc = {};
|
|
desc.Width = w;
|
|
desc.Height = h;
|
|
desc.MipLevels = 1;
|
|
desc.ArraySize = 1;
|
|
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
desc.SampleDesc.Count = 1;
|
|
desc.Usage = D3D11_USAGE_DEFAULT;
|
|
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
|
|
|
D3D11_SUBRESOURCE_DATA initData = {};
|
|
initData.pSysMem = pixels;
|
|
initData.SysMemPitch = w * 4;
|
|
|
|
ID3D11Texture2D* texture = nullptr;
|
|
HRESULT hr = device->CreateTexture2D(&desc, &initData, &texture);
|
|
stbi_image_free(pixels);
|
|
if (FAILED(hr) || !texture) {
|
|
DEBUG_LOGF("LoadTextureFromMemory: CreateTexture2D failed\n");
|
|
return false;
|
|
}
|
|
|
|
ID3D11ShaderResourceView* srv = nullptr;
|
|
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
|
|
srvDesc.Format = desc.Format;
|
|
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
|
|
srvDesc.Texture2D.MipLevels = 1;
|
|
|
|
hr = device->CreateShaderResourceView(texture, &srvDesc, &srv);
|
|
texture->Release();
|
|
if (FAILED(hr) || !srv) {
|
|
DEBUG_LOGF("LoadTextureFromMemory: CreateSRV failed\n");
|
|
return false;
|
|
}
|
|
|
|
*outTexID = (ImTextureID)(intptr_t)srv;
|
|
*outW = w;
|
|
*outH = h;
|
|
DEBUG_LOGF("LoadTextureFromMemory: %dx%d -> DX11 SRV %p\n", w, h, (void*)srv);
|
|
return true;
|
|
#else
|
|
GLuint tex = 0;
|
|
glGenTextures(1, &tex);
|
|
glBindTexture(GL_TEXTURE_2D, tex);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0,
|
|
GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
|
glGenerateMipmap(GL_TEXTURE_2D);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
|
|
stbi_image_free(pixels);
|
|
|
|
*outTexID = (ImTextureID)(intptr_t)tex;
|
|
*outW = w;
|
|
*outH = h;
|
|
DEBUG_LOGF("LoadTextureFromMemory: %dx%d -> texture %u\n", w, h, tex);
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
bool CreateRawTexture(const unsigned char* pixels, int w, int h,
|
|
bool repeat, ImTextureID* outTexID)
|
|
{
|
|
if (!pixels || w <= 0 || h <= 0) return false;
|
|
|
|
#ifdef DRAGONX_USE_DX11
|
|
ID3D11Device* device = GetImGuiD3D11Device();
|
|
if (!device) return false;
|
|
|
|
D3D11_TEXTURE2D_DESC desc = {};
|
|
desc.Width = w;
|
|
desc.Height = h;
|
|
desc.MipLevels = 1;
|
|
desc.ArraySize = 1;
|
|
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
desc.SampleDesc.Count = 1;
|
|
desc.Usage = D3D11_USAGE_DEFAULT;
|
|
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
|
|
|
D3D11_SUBRESOURCE_DATA initData = {};
|
|
initData.pSysMem = pixels;
|
|
initData.SysMemPitch = w * 4;
|
|
|
|
ID3D11Texture2D* texture = nullptr;
|
|
HRESULT hr = device->CreateTexture2D(&desc, &initData, &texture);
|
|
if (FAILED(hr) || !texture) return false;
|
|
|
|
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
|
|
srvDesc.Format = desc.Format;
|
|
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
|
|
srvDesc.Texture2D.MipLevels = 1;
|
|
|
|
ID3D11ShaderResourceView* srv = nullptr;
|
|
hr = device->CreateShaderResourceView(texture, &srvDesc, &srv);
|
|
texture->Release();
|
|
if (FAILED(hr) || !srv) return false;
|
|
|
|
*outTexID = (ImTextureID)(intptr_t)srv;
|
|
return true;
|
|
#else
|
|
GLuint tex = 0;
|
|
glGenTextures(1, &tex);
|
|
glBindTexture(GL_TEXTURE_2D, tex);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0,
|
|
GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
if (repeat) {
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
|
} else {
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
}
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
|
|
*outTexID = (ImTextureID)(intptr_t)tex;
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
unsigned char* LoadRawPixelsFromFile(const char* path, int* outW, int* outH)
|
|
{
|
|
std::vector<unsigned char> fileData;
|
|
if (!ReadFileToBuffer(path, fileData)) return nullptr;
|
|
return DecodeImageRGBA(fileData.data(), fileData.size(), outW, outH);
|
|
}
|
|
|
|
unsigned char* LoadRawPixelsFromMemory(const unsigned char* data, size_t dataSize, int* outW, int* outH)
|
|
{
|
|
if (!data || dataSize == 0) return nullptr;
|
|
return DecodeImageRGBA(data, dataSize, outW, outH);
|
|
}
|
|
|
|
void FreeRawPixels(unsigned char* pixels)
|
|
{
|
|
if (pixels) stbi_image_free(pixels);
|
|
}
|
|
|
|
// Box-average downscale an RGBA image to dw*dh, appending the result as a new frame in `out`.
|
|
static void AppendDownscaledFrame(const unsigned char* src, int sw, int sh, int dw, int dh,
|
|
std::vector<std::vector<unsigned char>>& out)
|
|
{
|
|
out.emplace_back();
|
|
std::vector<unsigned char>& dst = out.back();
|
|
dst.resize((size_t)dw * dh * 4);
|
|
for (int y = 0; y < dh; y++) {
|
|
int sy0 = y * sh / dh, sy1 = std::max(sy0 + 1, (y + 1) * sh / dh);
|
|
for (int x = 0; x < dw; x++) {
|
|
int sx0 = x * sw / dw, sx1 = std::max(sx0 + 1, (x + 1) * sw / dw);
|
|
uint32_t r=0,g=0,b=0,a=0,n=0;
|
|
for (int yy = sy0; yy < sy1; yy++)
|
|
for (int xx = sx0; xx < sx1; xx++) {
|
|
const unsigned char* p = src + ((size_t)yy * sw + xx) * 4;
|
|
r+=p[0]; g+=p[1]; b+=p[2]; a+=p[3]; ++n;
|
|
}
|
|
unsigned char* d = dst.data() + ((size_t)y * dw + x) * 4;
|
|
d[0]=(unsigned char)(r/n); d[1]=(unsigned char)(g/n); d[2]=(unsigned char)(b/n); d[3]=(unsigned char)(a/n);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void FitDims(int sw, int sh, int maxDim, int* dw, int* dh)
|
|
{
|
|
*dw = sw; *dh = sh;
|
|
if (sw > maxDim || sh > maxDim) {
|
|
float s = (float)maxDim / (float)std::max(sw, sh);
|
|
*dw = std::max(1, (int)(sw * s));
|
|
*dh = std::max(1, (int)(sh * s));
|
|
}
|
|
}
|
|
|
|
bool LoadAnimatedRGBA(const char* path, int maxFrames, AnimFrames& out)
|
|
{
|
|
out.frames.clear();
|
|
out.delaysSec.clear();
|
|
out.w = out.h = 0;
|
|
std::vector<unsigned char> file;
|
|
if (!ReadFileToBuffer(path, file)) return false;
|
|
const unsigned char* data = file.data();
|
|
const size_t len = file.size();
|
|
if (maxFrames < 1) maxFrames = 1;
|
|
|
|
// Static max dimension is generous (an avatar renders <=112px); animations use a smaller cap so a
|
|
// long clip can't cost tens of MB of VRAM. We only know if it animates after peeking the frame count.
|
|
const int kStillMax = 256, kAnimMax = 128;
|
|
|
|
// ---- Animated (or still) WebP via libwebp's demux decoder ----
|
|
if (IsWebP(data, len)) {
|
|
WebPData wd; wd.bytes = data; wd.size = len;
|
|
WebPAnimDecoderOptions opt;
|
|
if (!WebPAnimDecoderOptionsInit(&opt)) return false;
|
|
opt.color_mode = MODE_RGBA;
|
|
WebPAnimDecoder* dec = WebPAnimDecoderNew(&wd, &opt);
|
|
if (!dec) {
|
|
// Not an animation container — fall back to a single still decode.
|
|
int w=0,h=0; unsigned char* px = DecodeImageRGBA(data, len, &w, &h);
|
|
if (!px) return false;
|
|
int dw,dh; FitDims(w, h, kStillMax, &dw, &dh);
|
|
AppendDownscaledFrame(px, w, h, dw, dh, out.frames);
|
|
out.delaysSec.push_back(0.0f); out.w = dw; out.h = dh;
|
|
free(px);
|
|
return true;
|
|
}
|
|
WebPAnimInfo info;
|
|
if (!WebPAnimDecoderGetInfo(dec, &info) || info.canvas_width <= 0 || info.canvas_height <= 0) {
|
|
WebPAnimDecoderDelete(dec); return false;
|
|
}
|
|
const int animated = (info.frame_count > 1);
|
|
int dw, dh; FitDims(info.canvas_width, info.canvas_height, animated ? kAnimMax : kStillMax, &dw, &dh);
|
|
int prevTs = 0, produced = 0;
|
|
while (WebPAnimDecoderHasMoreFrames(dec) && produced < maxFrames) {
|
|
uint8_t* buf = nullptr; int ts = 0;
|
|
if (!WebPAnimDecoderGetNext(dec, &buf, &ts)) break; // buf valid until next call/delete
|
|
AppendDownscaledFrame(buf, info.canvas_width, info.canvas_height, dw, dh, out.frames);
|
|
float d = (ts - prevTs) / 1000.0f; prevTs = ts;
|
|
out.delaysSec.push_back(std::max(0.02f, d));
|
|
++produced;
|
|
}
|
|
WebPAnimDecoderDelete(dec);
|
|
if (out.frames.empty()) return false;
|
|
out.w = dw; out.h = dh;
|
|
return true;
|
|
}
|
|
|
|
// ---- Animated (or still) GIF via stb ----
|
|
const bool isGif = len >= 6 && memcmp(data, "GIF8", 4) == 0;
|
|
if (isGif) {
|
|
int* delays = nullptr; int w=0,h=0,z=0,comp=0;
|
|
unsigned char* all = stbi_load_gif_from_memory(data, (int)len, &delays, &w, &h, &z, &comp, 4);
|
|
if (all && w > 0 && h > 0 && z > 0) {
|
|
const int animated = (z > 1);
|
|
int dw, dh; FitDims(w, h, animated ? kAnimMax : kStillMax, &dw, &dh);
|
|
int frames = std::min(z, maxFrames);
|
|
for (int i = 0; i < frames; i++) {
|
|
AppendDownscaledFrame(all + (size_t)i * w * h * 4, w, h, dw, dh, out.frames);
|
|
float d = (delays && delays[i] > 0) ? delays[i] / 1000.0f : 0.1f;
|
|
out.delaysSec.push_back(std::max(0.02f, d));
|
|
}
|
|
out.w = dw; out.h = dh;
|
|
stbi_image_free(all);
|
|
if (delays) stbi_image_free(delays);
|
|
return true;
|
|
}
|
|
if (all) stbi_image_free(all);
|
|
if (delays) stbi_image_free(delays);
|
|
// fall through to a plain decode if the GIF path failed
|
|
}
|
|
|
|
// ---- Everything else: a single still frame (PNG/JPEG/BMP/TGA/... and APNG's default image) ----
|
|
{
|
|
int w=0,h=0; unsigned char* px = DecodeImageRGBA(data, len, &w, &h);
|
|
if (!px) return false;
|
|
int dw,dh; FitDims(w, h, kStillMax, &dw, &dh);
|
|
AppendDownscaledFrame(px, w, h, dw, dh, out.frames);
|
|
out.delaysSec.push_back(0.0f);
|
|
out.w = dw; out.h = dh;
|
|
free(px);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Lightweight animation probe (no full decode): animated WebP via WebPGetFeatures.has_animation, or a
|
|
// GIF with >1 image-descriptor block. Walks the GIF block structure only until it finds a 2nd frame.
|
|
static bool IsAnimatedData(const unsigned char* data, size_t len)
|
|
{
|
|
if (IsWebP(data, len)) {
|
|
WebPBitstreamFeatures f;
|
|
if (WebPGetFeatures(data, len, &f) == VP8_STATUS_OK) return f.has_animation != 0;
|
|
return false;
|
|
}
|
|
if (len >= 13 && memcmp(data, "GIF8", 4) == 0) {
|
|
size_t p = 6; // after "GIF8?a"
|
|
unsigned char packed = data[p + 4]; // Logical Screen Descriptor packed byte
|
|
p += 7;
|
|
if (packed & 0x80) p += (size_t)(2 << (packed & 7)) * 3; // skip global color table
|
|
int frames = 0;
|
|
while (p < len) {
|
|
unsigned char b = data[p++];
|
|
if (b == 0x2C) { // image descriptor = one frame
|
|
if (++frames > 1) return true;
|
|
if (p + 9 > len) break;
|
|
unsigned char ip = data[p + 8];
|
|
p += 9;
|
|
if (ip & 0x80) p += (size_t)(2 << (ip & 7)) * 3; // skip local color table
|
|
if (p >= len) break;
|
|
++p; // LZW min code size
|
|
while (p < len) { unsigned char s = data[p++]; if (!s) break; p += s; } // image sub-blocks
|
|
} else if (b == 0x21) { // extension
|
|
if (p >= len) break;
|
|
++p; // label
|
|
while (p < len) { unsigned char s = data[p++]; if (!s) break; p += s; } // sub-blocks
|
|
} else { // trailer (0x3B) or unexpected
|
|
break;
|
|
}
|
|
}
|
|
return frames > 1;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool IsAnimatedImageFile(const char* path)
|
|
{
|
|
std::vector<unsigned char> file;
|
|
if (!ReadFileToBuffer(path, file)) return false;
|
|
return IsAnimatedData(file.data(), file.size());
|
|
}
|
|
|
|
void DestroyTexture(ImTextureID texID)
|
|
{
|
|
if (!texID) return;
|
|
#ifdef DRAGONX_USE_DX11
|
|
auto* srv = (ID3D11ShaderResourceView*)(intptr_t)texID;
|
|
srv->Release();
|
|
#else
|
|
GLuint tex = (GLuint)(intptr_t)texID;
|
|
glDeleteTextures(1, &tex);
|
|
#endif
|
|
}
|
|
|
|
} // namespace util
|
|
} // namespace dragonx
|