diff --git a/src/hello_imgui/image_from_asset.h b/src/hello_imgui/image_from_asset.h index 373f9daf..9fceda17 100644 --- a/src/hello_imgui/image_from_asset.h +++ b/src/hello_imgui/image_from_asset.h @@ -41,21 +41,30 @@ bool ImageButtonFromAsset(const char *assetPath, const ImVec2& size = ImVec2(0, // `ImTextureID HelloImGui::ImTextureIdFromAsset(assetPath)`: // will return a texture ID for an image loaded from the assets. -ImTextureID ImTextureIdFromAsset(const char *assetPath); +// The size param is used for SVG images, otherwise it is ignored: +// leave empty to use default SVG image size, otherwise if only one dimension is provided, +// the other will be computed to keep the aspect ratio. +ImTextureID ImTextureIdFromAsset(const char *assetPath, ImVec2 svgSize = ImVec2(0, 0)); // `ImVec2 HelloImGui::ImageSizeFromAsset(assetPath)`: // will return the size of an image loaded from the assets. -ImVec2 ImageSizeFromAsset(const char *assetPath); +// The size param is used for SVG images, otherwise it is ignored: +// leave empty to use default SVG image size, otherwise if only one dimension is provided, +// the other will be computed to keep the aspect ratio. +ImVec2 ImageSizeFromAsset(const char *assetPath, ImVec2 svgSize = ImVec2(0, 0)); // `HelloImGui::ImageAndSize HelloImGui::ImageAndSizeFromAsset(assetPath)`: // will return the texture ID and the size of an image loaded from the assets. +// The size param is used for SVG images, otherwise it is ignored: +// leave empty to use default SVG image size, otherwise if only one dimension is provided, +// the other will be computed to keep the aspect ratio. struct ImageAndSize { ImTextureID textureId = ImTextureID(0); ImVec2 size = ImVec2(0.f, 0.f); }; -ImageAndSize ImageAndSizeFromAsset(const char *assetPath); +ImageAndSize ImageAndSizeFromAsset(const char *assetPath, ImVec2 svgSize = ImVec2(0, 0)); // `ImVec2 HelloImGui::ImageProportionalSize(askedSize, imageSize)`: @@ -72,6 +81,7 @@ ImVec2 ImageProportionalSize(const ImVec2& askedSize, const ImVec2& imageSize); namespace internal { + // Frees the internal texture cache void Free_ImageFromAssetMap(); } } diff --git a/src/hello_imgui/internal/image_from_asset.cpp b/src/hello_imgui/internal/image_from_asset.cpp index 22c41dbb..18c2b62a 100644 --- a/src/hello_imgui/internal/image_from_asset.cpp +++ b/src/hello_imgui/internal/image_from_asset.cpp @@ -20,6 +20,19 @@ #include +struct SvgRgbaImage +{ + std::vector data; + int width; + int height; +}; + +static SvgRgbaImage priv_SvgToRgba( + const HelloImGui::AssetFileData& data, + ImVec2 size = ImVec2(0.f, 0.f) // Use intrinsic width if 0, Use intrinsic height if 0. Preserve aspect ratio if only one is provided +); + + namespace HelloImGui { ImVec2 ImageProportionalSize(const ImVec2& askedSize, const ImVec2& imageSize) @@ -35,13 +48,30 @@ namespace HelloImGui return r; } + static std::string priv_FilenameAndSizeKey(const char* assetPath, ImVec2 size) + { + std::string key = assetPath; + key += "_"; + key += std::to_string(size.x); + key += "_"; + key += std::to_string(size.y); + return key; + } + + static bool priv_IsFilenameSvg(const std::string& filename) + { + return filename.size() > 4 && filename.substr(filename.size() - 4) == ".svg"; + } + static std::unordered_map gImageFromAssetMap; - static ImageAbstractPtr _GetCachedImage(const char*assetPath) + static ImageAbstractPtr _GetCachedImage(const char*assetPath, ImVec2 sizeForSvg) { - if (gImageFromAssetMap.find(assetPath) != gImageFromAssetMap.end()) - return gImageFromAssetMap.at(assetPath); + std::string key = priv_FilenameAndSizeKey(assetPath, sizeForSvg); + + if (gImageFromAssetMap.find(key) != gImageFromAssetMap.end()) + return gImageFromAssetMap.at(key); HelloImGui::RendererBackendType rendererBackendType = HelloImGui::GetRunnerParams()->rendererBackendType; ImageAbstractPtr concreteImage; @@ -65,40 +95,65 @@ namespace HelloImGui if (concreteImage == nullptr) { HelloImGui::Log(LogLevel::Warning, "ImageFromAsset: not implemented for this rendering backend!"); - gImageFromAssetMap[assetPath] = nullptr; // Cache the failure + gImageFromAssetMap[key] = nullptr; // Cache the failure return nullptr; } - unsigned char* image_data_rgba; - { - // Load the image using stbi_load_from_memory - auto assetData = LoadAssetFileData(assetPath); - IM_ASSERT(assetData.data != nullptr); - image_data_rgba = stbi_load_from_memory( - (unsigned char *)assetData.data, (int)assetData.dataSize, - &concreteImage->Width, &concreteImage->Height, NULL, 4); - FreeAssetFileData(&assetData); - } + bool isSvg = priv_IsFilenameSvg(assetPath); - if (image_data_rgba == NULL) + if (! isSvg) { - IM_ASSERT(false && "_GetCachedImage: Failed to load image!"); - throw std::runtime_error("_GetCachedImage: Failed to load image!"); + // Load with stbimage + unsigned char* image_data_rgba; + { + // Load the image using stbi_load_from_memory + auto assetData = LoadAssetFileData(assetPath); + IM_ASSERT(assetData.data != nullptr); + image_data_rgba = stbi_load_from_memory( + (unsigned char *)assetData.data, (int)assetData.dataSize, + &concreteImage->Width, &concreteImage->Height, NULL, 4); + FreeAssetFileData(&assetData); + } + + if (image_data_rgba == NULL) + { + IM_ASSERT(false && "_GetCachedImage: Failed to load image!"); + throw std::runtime_error("_GetCachedImage: Failed to load image!"); + } + concreteImage->_impl_StoreTexture(concreteImage->Width, concreteImage->Height, image_data_rgba); + stbi_image_free(image_data_rgba); + + gImageFromAssetMap[key] = concreteImage; + return concreteImage; } - concreteImage->_impl_StoreTexture(concreteImage->Width, concreteImage->Height, image_data_rgba); - stbi_image_free(image_data_rgba); + else + { + // Load SVG + SvgRgbaImage svgRgbaImage; + { + // Load the image using stbi_load_from_memory + auto assetData = LoadAssetFileData(assetPath); + IM_ASSERT(assetData.data != nullptr); + svgRgbaImage = priv_SvgToRgba(assetData, sizeForSvg); + FreeAssetFileData(&assetData); + concreteImage->Height = svgRgbaImage.height; + concreteImage->Width = svgRgbaImage.width; + } + concreteImage->_impl_StoreTexture(concreteImage->Width, concreteImage->Height, svgRgbaImage.data.data()); - gImageFromAssetMap[assetPath] = concreteImage; - return concreteImage; + gImageFromAssetMap[key] = concreteImage; + return concreteImage; + } } + void ImageFromAsset( const char *assetPath, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col) { - auto cachedImage = _GetCachedImage(assetPath); + auto cachedImage = _GetCachedImage(assetPath, size); if (cachedImage == nullptr) { ImGui::TextColored(ImVec4(1.f, 0.f, 0.f, 1.f), "ImageFromAsset: fail!"); @@ -112,7 +167,7 @@ namespace HelloImGui bool ImageButtonFromAsset(const char *assetPath, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, int frame_padding, const ImVec4& bg_col, const ImVec4& tint_col) { - auto cachedImage = _GetCachedImage(assetPath); + auto cachedImage = _GetCachedImage(assetPath, size); if (cachedImage == nullptr) { ImGui::TextColored(ImVec4(1.f, 0.f, 0.f, 1.f), "ImageButtonFromAsset: fail!"); @@ -125,25 +180,25 @@ namespace HelloImGui return clicked; } - ImTextureID ImTextureIdFromAsset(const char *assetPath) + ImTextureID ImTextureIdFromAsset(const char *assetPath, ImVec2 svgSize) { - auto cachedImage = _GetCachedImage(assetPath); + auto cachedImage = _GetCachedImage(assetPath, svgSize); if (cachedImage == nullptr) return ImTextureID(0); return cachedImage->TextureID(); } - ImVec2 ImageSizeFromAsset(const char *assetPath) + ImVec2 ImageSizeFromAsset(const char *assetPath, ImVec2 svgSize) { - auto cachedImage = _GetCachedImage(assetPath); + auto cachedImage = _GetCachedImage(assetPath, svgSize); if (cachedImage == nullptr) return ImVec2(0.f, 0.f); return ImVec2((float)cachedImage->Width, (float)cachedImage->Height); } - ImageAndSize ImageAndSizeFromAsset(const char *assetPath) + ImageAndSize ImageAndSizeFromAsset(const char *assetPath, ImVec2 svgSize) { - auto cachedImage = _GetCachedImage(assetPath); + auto cachedImage = _GetCachedImage(assetPath, svgSize); if (cachedImage == nullptr) return {}; return {cachedImage->TextureID(), ImVec2((float)cachedImage->Width, (float)cachedImage->Height)}; @@ -158,3 +213,99 @@ namespace HelloImGui } } + + +#include "plutosvg.h" + +static SvgRgbaImage priv_SvgToRgba( + const HelloImGui::AssetFileData& assetFileData, + ImVec2 size // Use intrinsic width if 0, Use intrinsic height if 0. Preserve aspect ratio if only one is provided +) +{ + // Load SVG document + plutosvg_document_t* svg = + plutosvg_document_load_from_data((const char*)assetFileData.data, assetFileData.dataSize, 0, 0, NULL, NULL); + if (!svg) { + IM_ASSERT(false && "SvgToRgba: Failed to load SVG document."); + } + + // Get intrinsic dimensions + float intrinsic_width = plutosvg_document_get_width(svg); + float intrinsic_height = plutosvg_document_get_height(svg); + if (intrinsic_width == 0 || intrinsic_height == 0) { + plutosvg_document_destroy(svg); + IM_ASSERT(false && "SvgToRgba: SVG document has invalid dimensions."); + } + + // Determine render dimensions + float width = size.x; + float height = size.y; + float render_width = width; + float render_height = height; + + if (width == 0.0f && height == 0.0f) { + render_width = intrinsic_width; + render_height = intrinsic_height; + } else if (width == 0.0f) { + // Calculate width to preserve aspect ratio + render_width = (height / intrinsic_height) * intrinsic_width; + } else if (height == 0.0f) { + // Calculate height to preserve aspect ratio + render_height = (width / intrinsic_width) * intrinsic_height; + } + + // Create a surface with the determined dimensions + plutovg_surface_t* surface = plutovg_surface_create(static_cast(render_width), static_cast(render_height)); + if (!surface) { + plutosvg_document_destroy(svg); + IM_ASSERT(false && "SvgToRgba: Failed to create Plutovg surface."); + } + + // Create a canvas for drawing + plutovg_canvas_t* canvas = plutovg_canvas_create(surface); + if (!canvas) { + plutovg_surface_destroy(surface); + plutosvg_document_destroy(svg); + IM_ASSERT(false && "SvgToRgba: Failed to create Plutovg canvas."); + } + + // Clear the surface with transparent color + plutovg_color_t transparent = PLUTOVG_MAKE_COLOR(1, 1, 1, 0); + //plutovg_surface_clear(surface, &transparent); + + // Set up palette callback (optional, can be nullptr if not using CSS variables) + // For simplicity, we'll use nullptr here. + bool render_success = plutosvg_document_render(svg, nullptr, canvas, &transparent, nullptr, nullptr); + if (!render_success) { + plutovg_canvas_destroy(canvas); + plutovg_surface_destroy(surface); + plutosvg_document_destroy(svg); + IM_ASSERT(false && "SvgToRgba: Failed to render SVG document."); + } + + // Access pixel data + unsigned char* pixel_data = plutovg_surface_get_data(surface); + if (!pixel_data) { + plutovg_canvas_destroy(canvas); + plutovg_surface_destroy(surface); + plutosvg_document_destroy(svg); + IM_ASSERT(false && "SvgToRgba: Failed to access surface pixel data."); + } + + int stride = plutovg_surface_get_stride(surface); + int height_px = plutovg_surface_get_height(surface); + int width_px = plutovg_surface_get_width(surface); + IM_ASSERT(stride == width_px * 4 && "SvgToRgba: Unexpected stride."); + + // Convert ARGB Premultiplied to RGBA Plain + // Plutovg uses premultiplied ARGB, but we need plain RGBA for ImGui + std::vector rgba_data(width_px * height_px * 4); + plutovg_convert_argb_to_rgba(rgba_data.data(), pixel_data, width_px, height_px, stride); + + // Cleanup + plutovg_canvas_destroy(canvas); + plutovg_surface_destroy(surface); + plutosvg_document_destroy(svg); + + return SvgRgbaImage{rgba_data, width_px, height_px}; +}