From 4768331b3b881573bf82781668c704c63b662e29 Mon Sep 17 00:00:00 2001 From: Hermet Park Date: Tue, 22 Jul 2025 10:49:17 +0900 Subject: [PATCH] sw_engine: support hue, color, saturation, luminosity, hardmix blends - Hue: Creates a result color with the luminance and saturation of the base color and the hue of the blend color. - Color: Creates a result color with the luminance of the base color and the hue and saturation of the blend color. This preserves the gray levels in the image and is useful for coloring monochrome images and for tinting color images. - Luminosity: reates a result color with the hue and saturation of the base color and the luminance of the blend color. This mode creates the inverse effect of Color mode. - Saturation: Creates a result color with the luminance and hue of the base color and the saturation of the blend color. Painting with this mode in an area with no (0) saturation (gray) causes no change. - HardMix: Adds the bottom & top. If the resulting sum for a channel is 255 or greater, it receives a value of 255; if less than 255, a value of 0. issue: https://github.com/thorvg/thorvg/issues/2701 --- examples/Blending.cpp | 10 +-- inc/thorvg.h | 10 +-- src/common/tvgColor.cpp | 8 +-- src/loaders/svg/tvgSvgLoader.cpp | 2 +- src/renderer/sw_engine/tvgSwCommon.h | 77 ++++++++++++++++++++++++ src/renderer/sw_engine/tvgSwRaster.cpp | 38 +++++++++++- src/renderer/sw_engine/tvgSwRenderer.cpp | 15 +++++ src/renderer/tvgPaint.cpp | 5 +- 8 files changed, 143 insertions(+), 22 deletions(-) diff --git a/examples/Blending.cpp b/examples/Blending.cpp index 4c18c76f..857d8208 100644 --- a/examples/Blending.cpp +++ b/examples/Blending.cpp @@ -162,12 +162,12 @@ struct UserExample : tvgexam::Example blender(canvas, "SoftLight", tvg::BlendMethod::SoftLight, 900.0f, 0.0f, data); blender(canvas, "Difference", tvg::BlendMethod::Difference, 900.0f, 150.0f, data); blender(canvas, "Exclusion", tvg::BlendMethod::Exclusion, 900.0f, 300.0f, data); - blender(canvas, "Hue (Not Supported)", tvg::BlendMethod::Hue, 900.0f, 450.0f, data); - blender(canvas, "Saturation (Not Supported)", tvg::BlendMethod::Saturation, 900.0f, 600.0f, data); - blender(canvas, "Color (Not Supported)", tvg::BlendMethod::Color, 900.0f, 750.0f, data); - blender(canvas, "Luminosity (Not Supported)", tvg::BlendMethod::Luminosity, 900.0f, 900.0f, data); + blender(canvas, "Hue", tvg::BlendMethod::Hue, 900.0f, 450.0f, data); + blender(canvas, "Saturation", tvg::BlendMethod::Saturation, 900.0f, 600.0f, data); + blender(canvas, "Color", tvg::BlendMethod::Color, 900.0f, 750.0f, data); + blender(canvas, "Luminosity", tvg::BlendMethod::Luminosity, 900.0f, 900.0f, data); blender(canvas, "Add", tvg::BlendMethod::Add, 900.0f, 1050.0f, data); - blender(canvas, "HardMix (Not Supported)", tvg::BlendMethod::HardMix, 900.0f, 1200.0f, data); + blender(canvas, "HardMix", tvg::BlendMethod::HardMix, 900.0f, 1200.0f, data); free(data); diff --git a/inc/thorvg.h b/inc/thorvg.h index 8c5e3807..af7c9407 100644 --- a/inc/thorvg.h +++ b/inc/thorvg.h @@ -209,12 +209,12 @@ enum class BlendMethod : uint8_t SoftLight, ///< The same as Overlay but with applying pure black or white does not result in pure black or white. (255 - 2 * S) * (D * D) + (2 * S * D) Difference, ///< Subtracts the bottom layer from the top layer or the other way around, to always get a non-negative value. (S - D) if (S > D), otherwise (D - S) Exclusion, ///< The result is twice the product of the top and bottom layers, subtracted from their sum. S + D - (2 * S * D) - Hue, ///< Reserved. Not supported. - Saturation, ///< Reserved. Not supported. - Color, ///< Reserved. Not supported. - Luminosity, ///< Reserved. Not supported. + Hue, ///< Combine with HSL(Sh + Ds + Dl) then convert it to RGB. + Saturation, ///< Combine with HSL(Dh + Ss + Dl) then convert it to RGB. + Color, ///< Combine with HSL(Sh + Ss + Dl) then convert it to RGB. + Luminosity, ///< Combine with HSL(Dh + Ds + Sl) then convert it to RGB. Add, ///< Simply adds pixel values of one layer with the other. (S + D) - HardMix, ///< Reserved. Not supported. + HardMix, ///< Adds S and D; result is 255 if the sum is greater than or equal to 255, otherwise 0. Composition = 255 ///< Used for intermediate composition. @since 1.0 }; diff --git a/src/common/tvgColor.cpp b/src/common/tvgColor.cpp index 7ef104a4..8ab24f4c 100644 --- a/src/common/tvgColor.cpp +++ b/src/common/tvgColor.cpp @@ -27,11 +27,6 @@ namespace tvg void hsl2rgb(float h, float s, float l, uint8_t& r, uint8_t& g, uint8_t& b) { - s = tvg::clamp(s, 0.0f, 1.0f); - l = tvg::clamp(l, 0.0f, 1.0f); - - auto tr = 0.0f, tg = 0.0f, tb = 0.0f; - if (tvg::zero(s)) { r = g = b = (uint8_t)nearbyint(l * 255.0f); return; @@ -53,6 +48,7 @@ void hsl2rgb(float h, float s, float l, uint8_t& r, uint8_t& g, uint8_t& b) auto vsf = v * sv * f; auto t = p + vsf; auto q = v - vsf; + float tr, tg, tb; switch (i) { case 0: tr = v; tg = t; tb = p; break; @@ -61,7 +57,7 @@ void hsl2rgb(float h, float s, float l, uint8_t& r, uint8_t& g, uint8_t& b) case 3: tr = p; tg = q; tb = v; break; case 4: tr = t; tg = p; tb = v; break; case 5: tr = v; tg = p; tb = q; break; - default: break; + default: tr = tg = tb = 0.0f; break; } r = (uint8_t)nearbyint(tr * 255.0f); g = (uint8_t)nearbyint(tg * 255.0f); diff --git a/src/loaders/svg/tvgSvgLoader.cpp b/src/loaders/svg/tvgSvgLoader.cpp index b101889c..18eefcf4 100644 --- a/src/loaders/svg/tvgSvgLoader.cpp +++ b/src/loaders/svg/tvgSvgLoader.cpp @@ -645,7 +645,7 @@ static bool _toColor(const char* str, uint8_t& r, uint8_t&g, uint8_t& b, char** hsl.l /= 100.0f; brightness = _skipSpace(brightness + 1, nullptr); if (brightness && brightness[0] == ')' && brightness[1] == '\0') { - hsl2rgb(hsl.h, hsl.s, hsl.l, r, g, b); + hsl2rgb(hsl.h, tvg::clamp(hsl.s, 0.0f, 1.0f), tvg::clamp(hsl.l, 0.0f, 1.0f), r, g, b); return true; } } diff --git a/src/renderer/sw_engine/tvgSwCommon.h b/src/renderer/sw_engine/tvgSwCommon.h index 5c1e9ca6..de85cbb4 100644 --- a/src/renderer/sw_engine/tvgSwCommon.h +++ b/src/renderer/sw_engine/tvgSwCommon.h @@ -26,6 +26,7 @@ #include #include "tvgCommon.h" #include "tvgMath.h" +#include "tvgColor.h" #include "tvgRender.h" #define SW_CURVE_TYPE_POINT 0 @@ -564,6 +565,80 @@ static inline uint32_t opBlendSoftLight(uint32_t s, uint32_t d) return BLEND_PRE(JOIN(255, f(C1(s), o.r), f(C2(s), o.g), f(C3(s), o.b)), s, o.a); } +void rasterRGB2HSL(uint8_t r, uint8_t g, uint8_t b, float* h, float* s, float* l); + +static inline uint32_t opBlendHue(uint32_t s, uint32_t d) +{ + RenderColor o; + if (!BLEND_UPRE(d, o)) return s; + + float sh, ds, dl; + rasterRGB2HSL(C1(s), C2(s), C3(s), &sh, 0, 0); + rasterRGB2HSL(o.r, o.g, o.b, 0, &ds, &dl); + + uint8_t r, g, b; + hsl2rgb(sh, ds, dl, r, g, b); + + return BLEND_PRE(JOIN(255, r, g, b), s, o.a); +} + +static inline uint32_t opBlendSaturation(uint32_t s, uint32_t d) +{ + RenderColor o; + if (!BLEND_UPRE(d, o)) return s; + + float dh, ss, dl; + rasterRGB2HSL(C1(s), C2(s), C3(s), 0, &ss, 0); + rasterRGB2HSL(o.r, o.g, o.b, &dh, 0, &dl); + + uint8_t r, g, b; + hsl2rgb(dh, ss, dl, r, g, b); + + return BLEND_PRE(JOIN(255, r, g, b), s, o.a); +} + +static inline uint32_t opBlendColor(uint32_t s, uint32_t d) +{ + RenderColor o; + if (!BLEND_UPRE(d, o)) return s; + + float sh, ss, dl; + rasterRGB2HSL(C1(s), C2(s), C3(s), &sh, &ss, 0); + rasterRGB2HSL(o.r, o.g, o.b, 0, 0, &dl); + + uint8_t r, g, b; + hsl2rgb(sh, ss, dl, r, g, b); + + return BLEND_PRE(JOIN(255, r, g, b), s, o.a); +} + +static inline uint32_t opBlendLuminosity(uint32_t s, uint32_t d) +{ + RenderColor o; + if (!BLEND_UPRE(d, o)) return s; + + float dh, ds, sl; + rasterRGB2HSL(C1(s), C2(s), C3(s), 0, 0, &sl); + rasterRGB2HSL(o.r, o.g, o.b, &dh, &ds, 0); + + uint8_t r, g, b; + hsl2rgb(dh, ds, sl, r, g, b); + + return BLEND_PRE(JOIN(255, r, g, b), s, o.a); +} + +static inline uint32_t opBlendHardMix(uint32_t s, uint32_t d) +{ + RenderColor o; + if (!BLEND_UPRE(d, o)) return s; + + auto f = [](uint8_t s, uint8_t d) { + return (s + d >= 255) ? 255 : 0; + }; + + return BLEND_PRE(JOIN(255, f(C1(s), o.r), f(C2(s), o.g), f(C3(s), o.b)), s, o.a); +} + int64_t mathMultiply(int64_t a, int64_t b); int64_t mathDivide(int64_t a, int64_t b); @@ -679,4 +754,6 @@ bool effectTint(SwCompositor* cmp, const RenderEffectTint* params, bool direct); void effectTritoneUpdate(RenderEffectTritone* effect); bool effectTritone(SwCompositor* cmp, const RenderEffectTritone* params, bool direct); + + #endif /* _TVG_SW_COMMON_H_ */ diff --git a/src/renderer/sw_engine/tvgSwRaster.cpp b/src/renderer/sw_engine/tvgSwRaster.cpp index 37b46349..55e57764 100644 --- a/src/renderer/sw_engine/tvgSwRaster.cpp +++ b/src/renderer/sw_engine/tvgSwRaster.cpp @@ -1769,4 +1769,40 @@ void rasterXYFlip(uint32_t* src, uint32_t* dst, int32_t stride, int32_t w, int32 } } } -} \ No newline at end of file +} + + +//TODO: can be moved in tvgColor +void rasterRGB2HSL(uint8_t r, uint8_t g, uint8_t b, float* h, float* s, float* l) +{ + auto rf = r / 255.0f; + auto gf = g / 255.0f; + auto bf = b / 255.0f; + auto maxVal = std::max(std::max(rf, gf), bf); + auto minVal = std::min(std::min(rf, gf), bf); + auto delta = maxVal - minVal; + + //lightness + float t; + if (l || s) { + t = (maxVal + minVal) * 0.5f; + if (l) *l = t; + } + + if (tvg::zero(delta)) { + if (h) *h = 0.0f; + if (s) *s = 0.0f; + } else { + //saturation + if (s) { + *s = (t < 0.5f) ? (delta / (maxVal + minVal)) : (delta / (2.0f - maxVal - minVal)); + } + //hue + if (h) { + if (maxVal == rf) *h = (gf - bf) / delta + (gf < bf ? 6.0f : 0.0f); + else if (maxVal == gf) *h = (bf - rf) / delta + 2.0f; + else *h = (rf - gf) / delta + 4.0f; + *h *= 60.0f; //directly convert to degrees + } + } +} diff --git a/src/renderer/sw_engine/tvgSwRenderer.cpp b/src/renderer/sw_engine/tvgSwRenderer.cpp index 7e375b04..265fc59f 100644 --- a/src/renderer/sw_engine/tvgSwRenderer.cpp +++ b/src/renderer/sw_engine/tvgSwRenderer.cpp @@ -564,9 +564,24 @@ bool SwRenderer::blend(BlendMethod method) case BlendMethod::Exclusion: surface->blender = opBlendExclusion; break; + case BlendMethod::Hue: + surface->blender = opBlendHue; + break; + case BlendMethod::Saturation: + surface->blender = opBlendSaturation; + break; + case BlendMethod::Color: + surface->blender = opBlendColor; + break; + case BlendMethod::Luminosity: + surface->blender = opBlendLuminosity; + break; case BlendMethod::Add: surface->blender = opBlendAdd; break; + case BlendMethod::HardMix: + surface->blender = opBlendHardMix; + break; default: TVGLOG("SW_ENGINE", "Non supported blending option = %d", (int) method); surface->blender = nullptr; diff --git a/src/renderer/tvgPaint.cpp b/src/renderer/tvgPaint.cpp index a3d5c6b7..42ed7074 100644 --- a/src/renderer/tvgPaint.cpp +++ b/src/renderer/tvgPaint.cpp @@ -429,10 +429,7 @@ uint8_t Paint::opacity() const noexcept Result Paint::blend(BlendMethod method) noexcept { - //TODO: Remove later - if (method == BlendMethod::Hue || method == BlendMethod::Saturation || method == BlendMethod::Color || method == BlendMethod::Luminosity || method == BlendMethod::HardMix) return Result::NonSupport; - - if (method == BlendMethod::Composition || method <= BlendMethod::HardMix) { + if (method <= BlendMethod::HardMix || method == BlendMethod::Composition) { pImpl->blend(method); return Result::Success; }