feat(images): WebP decode via libwebp + more stb formats

Broaden avatar/image support:

- Add libwebp (FetchContent, static, decode-only) so WebP loads. Built from
  source for Linux / mingw-Windows / macOS-osxcross identically — the cross
  sysroots have no webp, so vendoring from source is the one portable path;
  encode/tool builds are disabled to avoid pulling in libpng/zlib. Linked as
  webp + webpdemux (the latter for animated WebP, wired next).
- texture_loader routes all decode through DecodeImageRGBA: sniffs the RIFF/
  WEBP header and uses libwebp (WebPDecodeRGBAInto into a free()-able buffer),
  else stb — so every existing caller (avatars, QR, thumbnails) gains WebP for
  free with no allocator mismatch.
- Enable stb's TGA / PSD / PNM / PIC decoders and add .webp/.tga/.psd/.pnm/
  .ppm/.pgm/.pic to the image-picker + avatar-library extension lists.

Verified: libwebp builds static and a real .webp decodes to correct RGBA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 10:57:01 -05:00
parent 6ac8f66644
commit 923d086092
4 changed files with 72 additions and 17 deletions

View File

@@ -295,6 +295,29 @@ else()
set(CURL_INCLUDE_DIRS ${CURL_INCLUDE_DIR})
endif()
# libwebp - WebP decode (still + animated via WebPAnimDecoder). Built from source, static, decode-only
# so Linux / mingw-Windows / macOS-osxcross all build it identically (the mingw/osx sysroots have no
# webp). Encode tools are disabled to avoid pulling in libpng/zlib that the cross sysroots lack.
message(STATUS "Fetching libwebp (decode-only, static)...")
FetchContent_Declare(
libwebp
GIT_REPOSITORY https://github.com/webmproject/libwebp.git
GIT_TAG v1.4.0
GIT_SHALLOW TRUE
)
set(WEBP_LINK_STATIC ON CACHE BOOL "" FORCE)
set(WEBP_BUILD_ANIM_UTILS OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_CWEBP OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_DWEBP OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_GIF2WEBP OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_IMG2WEBP OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_VWEBP OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_WEBPINFO OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_LIBWEBPMUX OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_WEBPMUX OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_EXTRAS OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(libwebp)
# libsodium - platform-specific
# Search order per platform:
# 1. Local pre-built in libs/libsodium{-mac,-win}/ (downloaded by scripts/fetch-libsodium.sh)
@@ -735,6 +758,7 @@ target_include_directories(ObsidianDragon PRIVATE
${GLAD_INCLUDE}
${CURL_INCLUDE_DIRS}
${MINIZ_DIR}
${libwebp_SOURCE_DIR}/src # <webp/decode.h>, <webp/demux.h> (FetchContent build tree)
)
target_link_libraries(ObsidianDragon PRIVATE
@@ -744,6 +768,8 @@ target_link_libraries(ObsidianDragon PRIVATE
sqlite3_amalgamation
${CURL_LIBRARIES}
${SODIUM_LIBRARY}
webp
webpdemux # WebPAnimDecoder (animated WebP); transitively pulls in webp + sharpyuv
)
if(DRAGONX_LITE_BACKEND_READY)

View File

@@ -202,7 +202,8 @@ static bool s_avatarLibraryDirty = true;
static bool isImageFileName(const std::string& name) {
std::string lo = name;
for (char& c : lo) c = static_cast<char>(std::tolower((unsigned char)c));
for (const char* e : { ".png", ".jpg", ".jpeg", ".bmp", ".gif" }) {
for (const char* e : { ".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp",
".tga", ".psd", ".pnm", ".ppm", ".pgm", ".pic" }) {
const std::string ext(e);
if (lo.size() > ext.size() && lo.compare(lo.size() - ext.size(), ext.size(), ext) == 0) return true;
}

View File

@@ -297,7 +297,8 @@ private:
static bool isImageName(const std::string& name) {
auto lower = name;
for (char& c : lower) c = (char)std::tolower((unsigned char)c);
static const char* kExt[] = { ".png", ".jpg", ".jpeg", ".bmp", ".gif" };
static const char* kExt[] = { ".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp",
".tga", ".psd", ".pnm", ".ppm", ".pgm", ".pic" };
for (const char* e : kExt) {
const std::string ext(e);
if (lower.size() > ext.size() && lower.compare(lower.size()-ext.size(), ext.size(), ext) == 0)

View File

@@ -7,17 +7,25 @@
// stb_image — single-file image loader (public domain)
// Only compiled here; all other files just include the header.
#define STB_IMAGE_IMPLEMENTATION
// Formats we actually decode: PNG for app assets, plus JPEG/BMP/GIF so user-chosen contact avatars
// (which are commonly JPEGs) load — must stay in sync with ImagePicker's accepted extensions.
// 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 <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
#ifdef DRAGONX_USE_DX11
@@ -43,6 +51,30 @@ static ID3D11Device* GetImGuiD3D11Device()
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)
{
@@ -66,11 +98,10 @@ bool LoadTextureFromFile(const char* path, ImTextureID* outTexID, int* outW, int
return false;
}
int w = 0, h = 0, channels = 0;
unsigned char* pixels = stbi_load_from_memory(
fileData.data(), (int)fileData.size(), &w, &h, &channels, 4);
int w = 0, h = 0;
unsigned char* pixels = DecodeImageRGBA(fileData.data(), fileData.size(), &w, &h);
if (!pixels) {
DEBUG_LOGF("LoadTextureFromFile: stbi failed for '%s'\n", path);
DEBUG_LOGF("LoadTextureFromFile: decode failed for '%s'\n", path);
return false;
}
@@ -154,11 +185,10 @@ bool LoadTextureFromMemory(const unsigned char* data, size_t dataSize,
return false;
}
int w = 0, h = 0, channels = 0;
unsigned char* pixels = stbi_load_from_memory(
data, (int)dataSize, &w, &h, &channels, 4);
int w = 0, h = 0;
unsigned char* pixels = DecodeImageRGBA(data, dataSize, &w, &h);
if (!pixels) {
DEBUG_LOGF("LoadTextureFromMemory: stbi decode failed\n");
DEBUG_LOGF("LoadTextureFromMemory: decode failed\n");
return false;
}
@@ -298,16 +328,13 @@ unsigned char* LoadRawPixelsFromFile(const char* path, int* outW, int* outH)
{
std::vector<unsigned char> fileData;
if (!ReadFileToBuffer(path, fileData)) return nullptr;
int channels = 0;
return stbi_load_from_memory(fileData.data(), (int)fileData.size(),
outW, outH, &channels, 4);
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;
int channels = 0;
return stbi_load_from_memory(data, (int)dataSize, outW, outH, &channels, 4);
return DecodeImageRGBA(data, dataSize, outW, outH);
}
void FreeRawPixels(unsigned char* pixels)