Added support for sizing SVG images arbitrarily (overriding the image aspect ratio by stretching and squashing).

This commit is contained in:
Leon Styhre 2022-09-02 20:48:45 +02:00
parent 41def158bb
commit 039c27fa8e
6 changed files with 55 additions and 54 deletions

View file

@ -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<size_t>(mSize.x),
static_cast<size_t>(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<ThemeData>& 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<glm::vec2>("size")};
@ -581,6 +548,8 @@ void ImageComponent::applyTheme(const std::shared_ptr<ThemeData>& 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<glm::vec2>("maxSize")};
@ -610,8 +579,15 @@ void ImageComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
setDefaultImage(elem->get<std::string>("default"));
if (properties & PATH && elem->has("path")) {
bool tile {elem->has("tile") && elem->get<bool>("tile")};
setImage(elem->get<std::string>("path"), tile);
const bool tile {elem->has("tile") && elem->get<bool>("tile")};
const std::string path {elem->get<std::string>("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")) {

View file

@ -118,7 +118,7 @@ private:
bool mFlipX;
bool mFlipY;
bool mTargetIsMax;
bool mTargetIsMin;
bool mScalableNonAspect;
float mTileWidth;
float mTileHeight;

View file

@ -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<float>(mHeight)};
const float aspectW {svgImage->width / static_cast<float>(mWidth)};
const float aspectH {svgImage->height / static_cast<float>(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<int>(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<float>(mWidth))
height = std::floor(height * (static_cast<float>(mWidth) / requiredWidth));
}
std::vector<unsigned char> tempVector;
tempVector.reserve(mWidth * mHeight * 4);
NSVGrasterizer* rast {nsvgCreateRasterizer()};
// Compensate for rounding losses for a slightly more accurate rasterization.
const float compScale {(static_cast<float>(mHeight) / svgImage->height) *
(mSourceHeight / static_cast<float>(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);

View file

@ -64,8 +64,12 @@ public:
}
glm::vec2 getSize() { return glm::vec2 {static_cast<int>(mWidth), static_cast<int>(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<float> mSourceWidth;
std::atomic<float> mSourceHeight;
std::atomic<bool> mScalable;
std::atomic<bool> mScalableNonAspect;
std::atomic<bool> mHasRGBAData;
std::atomic<bool> mPendingRasterization;
bool mLinearMagnify;

View file

@ -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<float>(width), static_cast<float>(height)};
data->setSourceSize(static_cast<float>(width), static_cast<float>(height));

View file

@ -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<std::string, bool, bool, bool, size_t, size_t>;