diff --git a/CMakeLists.txt b/CMakeLists.txt index d1e23ac..da34d0b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 # , (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) diff --git a/src/ui/windows/contacts_tab.cpp b/src/ui/windows/contacts_tab.cpp index a2ba0e1..b8ac673 100644 --- a/src/ui/windows/contacts_tab.cpp +++ b/src/ui/windows/contacts_tab.cpp @@ -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(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; } diff --git a/src/ui/windows/image_picker.h b/src/ui/windows/image_picker.h index f67b898..f65c76b 100644 --- a/src/ui/windows/image_picker.h +++ b/src/ui/windows/image_picker.h @@ -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) diff --git a/src/util/texture_loader.cpp b/src/util/texture_loader.cpp index ddf4be5..4fdaa4c 100644 --- a/src/util/texture_loader.cpp +++ b/src/util/texture_loader.cpp @@ -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 +#include // WebPAnimDecoder — animated WebP + #include #include +#include #include #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& 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 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)