From bf5cce31c6c6978fe615209698e38d6ae8c872a2 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 3 Oct 2022 18:43:30 +0200 Subject: [PATCH] Replaced the NanoSVG library with LunaSVG. --- CMakeLists.txt | 7 ++- es-core/src/components/ImageComponent.cpp | 19 +------ es-core/src/components/ImageComponent.h | 1 - es-core/src/resources/TextureData.cpp | 67 +++++++++-------------- es-core/src/resources/TextureData.h | 6 +- es-core/src/resources/TextureResource.cpp | 3 - es-core/src/resources/TextureResource.h | 2 - external/CMakeLists.txt | 25 +++++++-- 8 files changed, 54 insertions(+), 76 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 104e0b20e..0192c5dec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -380,6 +380,7 @@ endif() set(COMMON_INCLUDE_DIRS ${CURL_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/external/CImg ${CMAKE_CURRENT_SOURCE_DIR}/external/glm + ${CMAKE_CURRENT_SOURCE_DIR}/external/lunasvg/include ${CMAKE_CURRENT_SOURCE_DIR}/external/nanosvg/src ${CMAKE_CURRENT_SOURCE_DIR}/external/rapidjson/include ${CMAKE_CURRENT_SOURCE_DIR}/external/rlottie/inc @@ -462,6 +463,7 @@ elseif(WIN32) ${PROJECT_SOURCE_DIR}/glew32.lib ${PROJECT_SOURCE_DIR}/libcurl-x64.lib ${PROJECT_SOURCE_DIR}/freetype.lib + ${PROJECT_SOURCE_DIR}/lunasvg.lib ${PROJECT_SOURCE_DIR}/pugixml.lib ${PROJECT_SOURCE_DIR}/rlottie.lib ${PROJECT_SOURCE_DIR}/SDL2main.lib @@ -478,6 +480,7 @@ elseif(WIN32) ${PROJECT_SOURCE_DIR}/glew32.dll ${PROJECT_SOURCE_DIR}/libcurl-x64.dll ${PROJECT_SOURCE_DIR}/libfreetype.dll + ${PROJECT_SOURCE_DIR}/liblunasvg.a ${PROJECT_SOURCE_DIR}/libpugixml.dll ${PROJECT_SOURCE_DIR}/libSDL2main.a ${PROJECT_SOURCE_DIR}/librlottie.dll @@ -527,8 +530,10 @@ else() ${SDL2_LIBRARY}) endif() -# Lottie animation library rlottie. if(NOT WIN32) + # SVG rendering library LunaSVG. + set(COMMON_LIBRARIES ${COMMON_LIBRARIES} ${PROJECT_SOURCE_DIR}/liblunasvg.a) + # Lottie animation library rlottie. set(COMMON_LIBRARIES ${COMMON_LIBRARIES} ${PROJECT_SOURCE_DIR}/librlottie.a) endif() diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index ba516edc5..56e470b6a 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -22,7 +22,6 @@ ImageComponent::ImageComponent(bool forceLoad, bool dynamic) , mFlipX {false} , mFlipY {false} , mTargetIsMax {false} - , mScalableNonAspect {false} , mTileWidth {0.0f} , mTileHeight {0.0f} , mColorShift {0xFFFFFFFF} @@ -88,7 +87,6 @@ void ImageComponent::setImage(const std::string& path, bool tile) mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic, mLinearInterpolation, mMipmapping, static_cast(mSize.x), static_cast(mSize.y), mTileWidth, mTileHeight); - mTexture->setScalableNonAspect(mScalableNonAspect); mTexture->rasterizeAt(mSize.x, mSize.y); onSizeChanged(); } @@ -404,8 +402,6 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, getParent()->getSize() : glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight())}; - bool noMax {false}; - if (properties & ThemeFlags::SIZE) { if (elem->has("size")) { glm::vec2 imageSize {elem->get("size")}; @@ -420,8 +416,6 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, if (imageSize.y > 0.0f) imageSize.y = glm::clamp(imageSize.y, 0.001f, 3.0f); setResize(imageSize * scale); - if (imageSize.x != 0.0f && imageSize.y != 0.0f) - noMax = true; } else if (elem->has("maxSize")) { glm::vec2 imageMaxSize {elem->get("maxSize")}; @@ -455,11 +449,6 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, if (properties & PATH && elem->has("path")) { const std::string path {elem->get("path")}; - if (!tile && !theme->isLegacyTheme() && noMax && path.length() > 4 && - Utils::String::toLower(path.substr(path.size() - 4, std::string::npos)) == ".svg") { - mScalableNonAspect = true; - } - if (tile && elem->has("tileSize")) { glm::vec2 tileSize {elem->get("tileSize")}; if (tileSize.x == 0.0f && tileSize.y == 0.0f) { @@ -474,8 +463,6 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, tileSize.y = glm::clamp(tileSize.y, 0.0f, 1.0f); mTileWidth = tileSize.x * scale.x; mTileHeight = tileSize.y * scale.y; - if (mTileWidth != 0.0f && mTileHeight != 0.0f) - mScalableNonAspect = true; } } @@ -614,12 +601,8 @@ void ImageComponent::resize(bool rasterize) glm::vec2 resizeScale {mTargetSize.x / mSize.x, mTargetSize.y / mSize.y}; if (resizeScale.x < resizeScale.y) { - // SVG rasterization is determined by height and rasterization is done in terms of - // pixels. If rounding is off enough in the rasterization step (for images with - // extreme aspect ratios), it can cause cutoff when the aspect ratio breaks. - // So we always make sure to round accordingly to avoid such issues. mSize.x *= resizeScale.x; - mSize.y = floorf(std::min(mSize.y * resizeScale.x, mTargetSize.y)); + mSize.y = std::min(mSize.y * resizeScale.x, mTargetSize.y); } else { // This will be mTargetSize.y(). We can't exceed it. diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index e9401d5bc..2a8eb2e8c 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -119,7 +119,6 @@ private: bool mFlipX; bool mFlipY; bool mTargetIsMax; - bool mScalableNonAspect; float mTileWidth; float mTileHeight; diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index afad1fe2d..232ad60b2 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -6,9 +6,6 @@ // Low-level texture data functions. // -#define NANOSVG_IMPLEMENTATION -#define NANOSVGRAST_IMPLEMENTATION - #include "resources/TextureData.h" #include "ImageIO.h" @@ -16,13 +13,10 @@ #include "resources/ResourceManager.h" #include "utils/StringUtil.h" -#include "nanosvg.h" -#include "nanosvgrast.h" +#include "lunasvg.h" #include -#define DPI 96 - TextureData::TextureData(bool tile) : mRenderer {Renderer::getInstance()} , mTile {tile} @@ -34,10 +28,10 @@ TextureData::TextureData(bool tile) , mSourceWidth {0.0f} , mSourceHeight {0.0f} , mScalable {false} - , mScalableNonAspect {false} , mHasRGBAData {false} , mPendingRasterization {false} , mMipmapping {false} + , mInvalidSVGFile {false} , mLinearMagnify {false} { } @@ -64,20 +58,23 @@ bool TextureData::initSVGFromMemory(const std::string& fileData) if (!mDataRGBA.empty() && !mPendingRasterization) return true; - NSVGimage* svgImage {nsvgParse(const_cast(fileData.c_str()), "px", DPI)}; + auto svgImage = lunasvg::Document::loadFromData(fileData); - if (!svgImage || svgImage->width == 0 || svgImage->height == 0) { - LOG(LogError) << "Couldn't parse SVG image"; + if (svgImage == nullptr) { + // LOG(LogError) << "Couldn't parse SVG image:" << mPath; + mInvalidSVGFile = true; return false; } + float svgWidth {static_cast(svgImage->width())}; + float svgHeight {static_cast(svgImage->height())}; bool rasterize {true}; if (mTile) { if (mTileWidth == 0.0f && mTileHeight == 0.0f) { rasterize = false; - mSourceWidth = svgImage->width; - mSourceHeight = svgImage->height; + mSourceWidth = svgWidth; + mSourceHeight = svgHeight; } else { mSourceWidth = static_cast(mTileWidth); @@ -90,7 +87,7 @@ bool TextureData::initSVGFromMemory(const std::string& fileData) rasterize = false; // Set a small temporary size that maintains the image aspect ratio. mSourceWidth = 64.0f; - mSourceHeight = 64.0f * (svgImage->height / svgImage->width); + mSourceHeight = 64.0f * (svgHeight / svgWidth); } mWidth = static_cast(std::round(mSourceWidth)); @@ -98,38 +95,25 @@ bool TextureData::initSVGFromMemory(const std::string& fileData) if (mWidth == 0) { // Auto scale width to keep aspect ratio. - mWidth = static_cast( - std::round((static_cast(mHeight) / svgImage->height) * svgImage->width)); + mWidth = + static_cast(std::round((static_cast(mHeight) / svgHeight) * svgWidth)); } else if (mHeight == 0) { // Auto scale height to keep aspect ratio. - mHeight = static_cast( - std::round((static_cast(mWidth) / svgImage->width) * svgImage->height)); + mHeight = + static_cast(std::round((static_cast(mWidth) / svgWidth) * svgHeight)); } if (rasterize) { - const float aspectW {svgImage->width / static_cast(mWidth)}; - const float aspectH {svgImage->height / static_cast(mHeight)}; + // For very short and wide images, make a slight scale adjustment to prevent pixels + // to the right of the image from being cut off. + if (svgWidth / svgHeight > 4.0f) + svgImage->scale(0.998, 1.0); - // If the size property has been used to override the aspect ratio of the SVG image, - // then we need to rasterize at a lower resolution and let the GPU scale the texture. - // This is necessary as the rasterization always maintains the image aspect ratio. - if (mScalableNonAspect && aspectW != aspectH) - mWidth = static_cast(std::round(svgImage->width / aspectH)); - - std::vector tempVector; - tempVector.reserve(mWidth * mHeight * 4); - - NSVGrasterizer* rast {nsvgCreateRasterizer()}; - - nsvgRasterize(rast, svgImage, 0, 0, static_cast(mHeight) / svgImage->height, - tempVector.data(), mWidth, mHeight, mWidth * 4); - - nsvgDeleteRasterizer(rast); - - mDataRGBA.insert(mDataRGBA.begin(), std::make_move_iterator(tempVector.data()), - std::make_move_iterator(tempVector.data() + (mWidth * mHeight * 4))); - tempVector.erase(tempVector.begin(), tempVector.end()); + auto bitmap = svgImage->renderToBitmap(mWidth, mHeight); + bitmap.convertToRGBA(); + mDataRGBA.insert(mDataRGBA.begin(), std::move(bitmap.data()), + std::move(bitmap.data() + mWidth * mHeight * 4)); ImageIO::flipPixelsVert(mDataRGBA.data(), mWidth, mHeight); mPendingRasterization = false; @@ -141,8 +125,6 @@ bool TextureData::initSVGFromMemory(const std::string& fileData) mPendingRasterization = true; } - nsvgDelete(svgImage); - return true; } @@ -196,6 +178,9 @@ bool TextureData::initFromRGBA(const unsigned char* dataRGBA, size_t width, size bool TextureData::load() { + if (mInvalidSVGFile) + return false; + bool retval {false}; // Need to load. See if there is a file. diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 57f435d29..5c382650c 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -64,10 +64,6 @@ public: } glm::vec2 getSize() { return glm::vec2 {static_cast(mWidth), static_cast(mHeight)}; } - // Whether to stretch or squash SVG images if the size property has been used to override - // the aspect ratio (accomplished by rasterizing at a lower resolution and letting the GPU - // scale the texture). - void setScalableNonAspect(bool state) { mScalableNonAspect = state; } // Whether to use linear filtering when magnifying the texture. void setLinearMagnify(bool state) { mLinearMagnify = state; } // Whether to use mipmapping and trilinear filtering. @@ -96,10 +92,10 @@ private: std::atomic mSourceWidth; std::atomic mSourceHeight; std::atomic mScalable; - std::atomic mScalableNonAspect; std::atomic mHasRGBAData; std::atomic mPendingRasterization; std::atomic mMipmapping; + std::atomic mInvalidSVGFile; bool mLinearMagnify; bool mReloadable; }; diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index 7500acdd3..e0b3362e7 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -24,7 +24,6 @@ TextureResource::TextureResource(const std::string& path, bool scalable) : mTextureData {nullptr} , mForceLoad {false} - , mScalableNonAspect {false} { // Create a texture data object for this texture. if (!path.empty()) { @@ -251,8 +250,6 @@ void TextureResource::rasterizeAt(float width, float height) else data = sTextureDataManager.get(this); - data->setScalableNonAspect(mScalableNonAspect); - if (mTextureData && mTextureData.get()->getScalable()) mSourceSize = glm::vec2 {static_cast(width), static_cast(height)}; data->setSourceSize(static_cast(width), static_cast(height)); diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index 5d2ca7a91..c0dd80a3d 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -55,7 +55,6 @@ public: return (mTextureData != nullptr ? mTextureData->getScalable() : false); } - void setScalableNonAspect(bool state) { mScalableNonAspect = state; } void setLinearMagnify(bool state) { mTextureData->setLinearMagnify(state); } std::string getTextureFilePath(); @@ -103,7 +102,6 @@ private: glm::ivec2 mSize; glm::vec2 mSourceSize; bool mForceLoad; - bool mScalableNonAspect; // File path, tile, linear interpolation, scalable/SVG, width, height. using TextureKeyType = std::tuple; diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 518a6cabb..44584ba39 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -6,10 +6,8 @@ # CMake configuration for bundled dependencies built in-tree. # -# On Windows, rlottie is built as a DLL file. -if(NOT WIN32) - set(BUILD_SHARED_LIBS OFF) -endif() +# Always build the external libraries with optimizations enabled and without debug info. +set (CMAKE_BUILD_TYPE "Release") # Disabled threading support for rlottie as this functionality actually leads to far worse # performance. As well there is a bug on Windows that makes rlottie hang forever on application @@ -18,12 +16,29 @@ option(LOTTIE_THREAD OFF) option(LOTTIE_MODULE OFF) -# Only use the compiler and linker flags defined by rlottie. +# Only use the compiler and linker flags defined by LunaSVG and rlottie. unset(CMAKE_CXX_FLAGS) unset(CMAKE_EXE_LINKER_FLAGS) +add_subdirectory(lunasvg) + +# On Windows, rlottie is built as a DLL file. +if(NOT WIN32) + set(BUILD_SHARED_LIBS OFF) +endif() + if(EMSCRIPTEN) set(CMAKE_CXX_FLAGS -pthread) endif() +# rlottie generates a lot of annoying compiler warnings that we don't need to show. +if(CMAKE_CXX_COMPILER_ID MATCHES MSVC) + set(CMAKE_CXX_FLAGS "/wd4244 /wd4251 /wd4267 /wd4530 /wd4996") +else() + set(CMAKE_CXX_FLAGS "-w") +endif() + add_subdirectory(rlottie EXCLUDE_FROM_ALL) + +# Build LunaSVG before rlottie. +add_dependencies(rlottie lunasvg)