From 039c27fa8ed476c603c2348c7121c1f08f0009a4 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Fri, 2 Sep 2022 20:48:45 +0200 Subject: [PATCH] Added support for sizing SVG images arbitrarily (overriding the image aspect ratio by stretching and squashing). --- es-core/src/components/ImageComponent.cpp | 66 ++++++++--------------- es-core/src/components/ImageComponent.h | 2 +- es-core/src/resources/TextureData.cpp | 27 +++++++--- es-core/src/resources/TextureData.h | 7 ++- es-core/src/resources/TextureResource.cpp | 3 ++ es-core/src/resources/TextureResource.h | 4 +- 6 files changed, 55 insertions(+), 54 deletions(-) diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 75c4907f6..3414ade19 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -35,7 +35,7 @@ ImageComponent::ImageComponent(bool forceLoad, bool dynamic) , mFlipX {false} , mFlipY {false} , mTargetIsMax {false} - , mTargetIsMin {false} + , mScalableNonAspect {false} , mTileWidth {0.0f} , mTileHeight {0.0f} , mColorShift {0xFFFFFFFF} @@ -68,20 +68,15 @@ void ImageComponent::resize(bool rasterize) mSize = mTargetSize; } else { - // 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. if (mTargetIsMax) { + // Maintain image aspect ratio. mSize = textureSize; - - glm::vec2 resizeScale {(mTargetSize.x / mSize.x), (mTargetSize.y / mSize.y)}; + glm::vec2 resizeScale {mTargetSize.x / mSize.x, mTargetSize.y / mSize.y}; if (resizeScale.x < resizeScale.y) { // This will be mTargetSize.x. We can't exceed it, nor be lower than it. mSize.x *= resizeScale.x; - // We need to make sure we're not creating an image larger than max size. - 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. @@ -89,34 +84,11 @@ void ImageComponent::resize(bool rasterize) mSize.x = std::min((mSize.y / textureSize.y) * textureSize.x, mTargetSize.x); } } - else if (mTargetIsMin) { - mSize = textureSize; - - glm::vec2 resizeScale {(mTargetSize.x / mSize.x), (mTargetSize.y / mSize.y)}; - - if (resizeScale.x > resizeScale.y) { - mSize.x *= resizeScale.x; - mSize.y *= resizeScale.x; - - float cropPercent {(mSize.y - mTargetSize.y) / (mSize.y * 2.0f)}; - crop(0.0f, cropPercent, 0.0f, cropPercent); - } - else { - mSize.x *= resizeScale.y; - mSize.y *= resizeScale.y; - - float cropPercent {(mSize.x - mTargetSize.x) / (mSize.x * 2.0f)}; - crop(cropPercent, 0.0f, cropPercent, 0.0f); - } - mSize.y = std::max(mSize.y, mTargetSize.y); - mSize.x = std::max((mSize.y / textureSize.y) * textureSize.x, mTargetSize.x); - } else { - // If both components are set, we just stretch. - // If no components are set, we don't resize at all. + // If both axes are set we just stretch or squash, if no axes are set we do nothing. mSize = mTargetSize == glm::vec2 {0.0f, 0.0f} ? textureSize : mTargetSize; - // If only one component is set, we resize in a way that maintains aspect ratio. + // If only one axis is set, we resize in a way that maintains aspect ratio. if (!mTargetSize.x && mTargetSize.y) { mSize.y = mTargetSize.y; mSize.x = (mSize.y / textureSize.y) * textureSize.x; @@ -181,6 +153,7 @@ void ImageComponent::setImage(const std::string& path, bool tile) mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic, mLinearInterpolation, static_cast(mSize.x), static_cast(mSize.y), mTileWidth, mTileHeight); + mTexture->setScalableNonAspect(mScalableNonAspect); mTexture->rasterizeAt(mSize.x, mSize.y); onSizeChanged(); } @@ -211,7 +184,6 @@ void ImageComponent::setResize(float width, float height) { mTargetSize = glm::vec2 {width, height}; mTargetIsMax = false; - mTargetIsMin = false; resize(); } @@ -219,7 +191,6 @@ void ImageComponent::setResize(float width, float height, bool rasterize) { mTargetSize = glm::vec2 {width, height}; mTargetIsMax = false; - mTargetIsMin = false; resize(rasterize); } @@ -227,7 +198,6 @@ void ImageComponent::setMaxSize(const float width, const float height) { mTargetSize = glm::vec2 {width, height}; mTargetIsMax = true; - mTargetIsMin = false; resize(); } @@ -460,12 +430,7 @@ void ImageComponent::render(const glm::mat4& parentTrans) return; glm::mat4 trans {parentTrans * getTransform()}; - - // Don't round vertices if scaled as it may lead to single-pixel alignment issues. - if (mScale == 1.0f) - mRenderer->setMatrix(trans, true); - else - mRenderer->setMatrix(trans, false); + mRenderer->setMatrix(trans, false); if (mTexture && mOpacity > 0.0f) { if (Settings::getInstance()->getBool("DebugImage")) { @@ -567,6 +532,8 @@ 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")}; @@ -581,6 +548,8 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, if (imageSize.y > 0.0f) imageSize.y = glm::clamp(imageSize.y, 0.001f, 2.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")}; @@ -610,8 +579,15 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, setDefaultImage(elem->get("default")); if (properties & PATH && elem->has("path")) { - bool tile {elem->has("tile") && elem->get("tile")}; - setImage(elem->get("path"), tile); + const bool tile {elem->has("tile") && elem->get("tile")}; + const std::string path {elem->get("path")}; + + if (!theme->isLegacyTheme() && noMax && path.length() > 4 && + Utils::String::toLower(path.substr(path.size() - 4, std::string::npos)) == ".svg") { + mScalableNonAspect = true; + } + + setImage(path, tile); } if (properties && elem->has("imageType")) { diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index e127c209c..122887a6b 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -118,7 +118,7 @@ private: bool mFlipX; bool mFlipY; bool mTargetIsMax; - bool mTargetIsMin; + bool mScalableNonAspect; float mTileWidth; float mTileHeight; diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 6ab7e877a..47391786d 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -35,6 +35,7 @@ TextureData::TextureData(bool tile) , mSourceWidth {0.0f} , mSourceHeight {0.0f} , mScalable {false} + , mScalableNonAspect {false} , mHasRGBAData {false} , mPendingRasterization {false} , mLinearMagnify {false} @@ -107,17 +108,31 @@ bool TextureData::initSVGFromMemory(const std::string& fileData) } if (rasterize) { + float height {static_cast(mHeight)}; + const float aspectW {svgImage->width / static_cast(mWidth)}; + const float aspectH {svgImage->height / static_cast(mHeight)}; + + if (mScalableNonAspect && aspectW != aspectH) { + // 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. + mWidth = static_cast(std::round(svgImage->width / aspectH)); + } + else { + // For very wide and short images we need to check that the target width is enough + // to fit the rasterized image, if not we'll decrease the height as needed. + const float requiredWidth {height * (svgImage->width / svgImage->height)}; + if (std::round(requiredWidth) > static_cast(mWidth)) + height = std::floor(height * (static_cast(mWidth) / requiredWidth)); + } + std::vector tempVector; tempVector.reserve(mWidth * mHeight * 4); NSVGrasterizer* rast {nsvgCreateRasterizer()}; - // Compensate for rounding losses for a slightly more accurate rasterization. - const float compScale {(static_cast(mHeight) / svgImage->height) * - (mSourceHeight / static_cast(mHeight))}; - - nsvgRasterize(rast, svgImage, 0.0f, 0.0f, compScale, tempVector.data(), mWidth, mHeight, - mWidth * 4); + nsvgRasterize(rast, svgImage, 0, 0, height / svgImage->height, tempVector.data(), mWidth, + mHeight, mWidth * 4); nsvgDeleteRasterizer(rast); diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 072f945ac..ebe118a8e 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -64,8 +64,12 @@ 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 setting) { mLinearMagnify = setting; } + void setLinearMagnify(bool state) { mLinearMagnify = state; } // Has the image been loaded but not yet been rasterized as the size was not known? const bool getPendingRasterization() { return mPendingRasterization; } @@ -90,6 +94,7 @@ private: std::atomic mSourceWidth; std::atomic mSourceHeight; std::atomic mScalable; + std::atomic mScalableNonAspect; std::atomic mHasRGBAData; std::atomic mPendingRasterization; bool mLinearMagnify; diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index 490a60f09..95fb7e110 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -23,6 +23,7 @@ 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()) { @@ -245,6 +246,8 @@ 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 597a34c5a..77f07d944 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -54,7 +54,8 @@ public: return (mTextureData != nullptr ? mTextureData->getScalable() : false); } - void setLinearMagnify(bool setting) { mTextureData->setLinearMagnify(setting); } + void setScalableNonAspect(bool state) { mScalableNonAspect = state; } + void setLinearMagnify(bool state) { mTextureData->setLinearMagnify(state); } std::string getTextureFilePath(); @@ -100,6 +101,7 @@ private: glm::ivec2 mSize; glm::vec2 mSourceSize; bool mForceLoad; + bool mScalableNonAspect; // File path, tile, linear interpolation, scalable/SVG, width, height. using TextureKeyType = std::tuple;