// 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 #include // WebPAnimDecoder — animated WebP #include #include #include #include #include #include #ifdef DRAGONX_USE_DX11 #include // 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(io.BackendRendererUserData); } #else #ifdef DRAGONX_HAS_GLAD #include #else #include #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& 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 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 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>& out) { out.emplace_back(); std::vector& 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 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 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