[vlc-commits] [Git][videolan/vlc][master] 8 commits: qml: handle DropShadowImage size from the QML side

Steve Lhomme (@robUx4) gitlab at videolan.org
Fri Feb 2 10:25:44 UTC 2024



Steve Lhomme pushed to branch master at VideoLAN / VLC


Commits:
d574dc2f by Pierre Lamot at 2024-02-02T09:56:34+00:00
qml: handle DropShadowImage size from the QML side

- - - - -
88a376ba by Pierre Lamot at 2024-02-02T09:56:34+00:00
qml: implement DoubleShadow as a single image

- - - - -
23fa7f35 by Pierre Lamot at 2024-02-02T09:56:34+00:00
qt: add guaussian blur CPU implementation

this version uses https://github.com/bfraboni/FastGaussianBlur/ under MIT license

- - - - -
690f23d6 by Pierre Lamot at 2024-02-02T09:56:34+00:00
qt: fix includes in gaussian blur implementation

- - - - -
ef6980ba by Pierre Lamot at 2024-02-02T09:56:34+00:00
qml: use better blur implementation for shadows

qt_blurImage is not a Gaussian blur, the result doesn't match the designs

this version uses https://github.com/bfraboni/FastGaussianBlur/ under MIT license

fix: #27102

- - - - -
6e660f7f by Pierre Lamot at 2024-02-02T09:56:34+00:00
qml: infer target size and radius from the shadow target

in most cases we can infer rectangle size & radius from the target

- - - - -
f129c72a by Pierre Lamot at 2024-02-02T09:56:34+00:00
qml: normalize drop shadow drawing

image provider requested size is dpr dependant, so drawing needs to be scaled
accordingly. All metrics provided to draw the shadow are provided in the same
referencial which is dpr independant (usually Qml referential). This referential
is expressed through the "viewport" variables which indicates the bounds of the
picture to draw.

for the effective drawing, The viewport is scaled  fit in the requested image
size, then we draw the effect by applying the computed scale factor to all
metrics.

One other benefit of this, is that we can limit the CPU & memory required for
the drawing by limitting the image sourceSize

- - - - -
352b88c1 by Pierre Lamot at 2024-02-02T09:56:34+00:00
qml: limit GridItem shadow size to 128x128

- - - - -


17 changed files:

- modules/gui/qt/Makefile.am
- modules/gui/qt/medialibrary/qml/MusicAlbumsGridExpandDelegate.qml
- modules/gui/qt/medialibrary/qml/VideoInfoExpandPanel.qml
- modules/gui/qt/meson.build
- modules/gui/qt/network/qml/NetworkThumbnailItem.qml
- modules/gui/qt/player/qml/controlbarcontrols/ArtworkInfoWidget.qml
- modules/gui/qt/player/qml/controlbarcontrols/PlayButton.qml
- modules/gui/qt/playlist/qml/PlaylistDelegate.qml
- modules/gui/qt/util/effects_image_provider.cpp
- modules/gui/qt/util/effects_image_provider.hpp
- + modules/gui/qt/util/fast_gaussian_blur_template.h
- modules/gui/qt/widgets/qml/DoubleShadow.qml
- modules/gui/qt/widgets/qml/DragItem.qml
- modules/gui/qt/widgets/qml/DropShadowImage.qml
- modules/gui/qt/widgets/qml/EmptyLabel.qml
- modules/gui/qt/widgets/qml/GridItem.qml
- modules/gui/qt/widgets/qml/TableColumns.qml


Changes:

=====================================
modules/gui/qt/Makefile.am
=====================================
@@ -320,6 +320,7 @@ libqt_plugin_la_SOURCES = \
 	util/vlctick.cpp \
 	util/vlctick.hpp \
 	util/shared_input_item.hpp \
+	util/fast_gaussian_blur_template.h \
 	util/effects_image_provider.cpp \
 	util/effects_image_provider.hpp \
 	util/qsgroundedrectangularimagenode.cpp \


=====================================
modules/gui/qt/medialibrary/qml/MusicAlbumsGridExpandDelegate.qml
=====================================
@@ -128,11 +128,11 @@ FocusScope {
                     : VLCStyle.noArtAlbumCover
 
                 Widgets.DefaultShadow {
-                    anchors.fill: parent
-                    visible: (parent.status === RoundImage.Ready)
+                    anchors.centerIn: parent
 
-                    xRadius: parent.radius
-                    yRadius: parent.radius
+                    sourceItem: parent
+
+                    visible: (parent.status === RoundImage.Ready)
                 }
             }
         }


=====================================
modules/gui/qt/medialibrary/qml/VideoInfoExpandPanel.qml
=====================================
@@ -118,11 +118,11 @@ FocusScope {
                             radius: VLCStyle.gridCover_radius
 
                             Widgets.DefaultShadow {
-                                anchors.fill: parent
-                                visible: (parent.status === RoundImage.Ready)
+                                anchors.centerIn: parent
+
+                                sourceItem: parent
 
-                                xRadius: parent.radius
-                                yRadius: parent.radius
+                                visible: (parent.status === RoundImage.Ready)
                             }
                         }
                     }


=====================================
modules/gui/qt/meson.build
=====================================
@@ -449,6 +449,7 @@ some_sources = files(
     'util/vlctick.cpp',
     'util/vlctick.hpp',
     'util/shared_input_item.hpp',
+    'util/fast_gaussian_blur_template.h',
     'util/effects_image_provider.cpp',
     'util/effects_image_provider.hpp',
     'util/qsgroundedrectangularimagenode.cpp',


=====================================
modules/gui/qt/network/qml/NetworkThumbnailItem.qml
=====================================
@@ -102,12 +102,11 @@ Row {
         height: artwork.height
 
         Widgets.DefaultShadow {
-            // clip shadows to only the painted area of cover
-            x: (artwork.x + artwork.width - artwork.paintedWidth) / 2
-            y: (artwork.y + artwork.height - artwork.paintedHeight) / 2
-            width: artwork.paintedWidth
-            height: artwork.paintedHeight
+            anchors.centerIn: artwork
 
+            // clip shadows to only the painted area of cover
+            rectWidth: artwork.paintedWidth
+            rectHeight: artwork.paintedHeight
         }
 
         NetworkCustomCover {


=====================================
modules/gui/qt/player/qml/controlbarcontrols/ArtworkInfoWidget.qml
=====================================
@@ -125,8 +125,8 @@ AbstractButton {
             Widgets.DefaultShadow {
                 anchors.centerIn: coverImage
 
-                width: coverImage.paintedWidth
-                height: coverImage.paintedHeight
+                sourceItem: coverImage
+
             }
         }
 


=====================================
modules/gui/qt/player/qml/controlbarcontrols/PlayButton.qml
=====================================
@@ -244,10 +244,10 @@ T.Control {
 
             color: VLCStyle.setColorAlpha(theme.accent, 0.29)
 
+            rectWidth: parent.width
+            rectHeight: parent.height
             xRadius: parent.width
             yRadius: xRadius
-
-            sourceSize: Qt.size(xRadius, yRadius)
         }
 
         Widgets.DropShadowImage {
@@ -263,10 +263,11 @@ T.Control {
 
             color: VLCStyle.setColorAlpha(theme.accent, 1.0)
 
+            rectWidth: parent.width
+            rectHeight: parent.height
+
             xRadius: parent.width
             yRadius: xRadius
-
-            sourceSize: Qt.size(xRadius, yRadius)
         }
 
         Widgets.ScaledImage {


=====================================
modules/gui/qt/playlist/qml/PlaylistDelegate.qml
=====================================
@@ -170,8 +170,9 @@ T.ItemDelegate {
 
                 Widgets.DefaultShadow {
                     anchors.centerIn: parent
-                    width: parent.paintedWidth
-                    height: parent.paintedHeight
+
+                    sourceItem: parent
+
                     visible: (artwork.status === Image.Ready)
                 }
             }


=====================================
modules/gui/qt/util/effects_image_provider.cpp
=====================================
@@ -25,13 +25,7 @@
 
 #include <memory>
 
-#include "qt.hpp" // VLC_WEAK
-
-// Qt private exported function
-QT_BEGIN_NAMESPACE
-extern void VLC_WEAK qt_blurImage(QImage &blurImage, qreal radius, bool quality, int transposed = 0);
-QT_END_NAMESPACE
-
+#include "fast_gaussian_blur_template.h"
 
 namespace {
 
@@ -47,58 +41,90 @@ class RectDropShadowEffect : public IEffect
 
 public:
     explicit RectDropShadowEffect(const QVariantMap& settings)
-        : m_blurRadius(settings["blurRadius"].toReal())
+        : m_viewport(settings["viewportWidth"].toReal(), settings["viewportHeight"].toReal())
         , m_color(settings["color"].value<QColor>())
+        , m_blurRadius(settings["blurRadius"].toReal())
         , m_xOffset(settings["xOffset"].toReal())
         , m_yOffset(settings["yOffset"].toReal())
-    { }
+        , m_recWidth(settings["rectWidth"].toReal())
+        , m_height(settings["rectHeight"].toReal())
+    {
+    }
 
-    QImage generate(const QSize& size) const override
+    virtual void draw(QPainter& painter, qreal xscale, qreal yscale) const
     {
-        QImage mask(size, QImage::Format_ARGB32_Premultiplied);
-        mask.fill(m_color);
-        return generate(mask);
+        QPainterPath path;
+        //center in window + offset
+        path.addRect(
+            ((m_viewport.width() / 2.)  - (m_recWidth / 2.) + m_xOffset) * xscale,
+            ((m_viewport.height() / 2.) - (m_height / 2.)   + m_yOffset) * yscale,
+            m_recWidth * xscale,
+            m_height * yscale);
+        painter.fillPath(path, m_color);
+        painter.drawPath(path);
     }
 
-    QImage generate(const QImage& mask) const
+    QImage generate(const QSize& size) const override
     {
-        if (Q_UNLIKELY(!&qt_blurImage))
-        {
-            qWarning("qt_blurImage() is not available! Drop shadow will not work!");
+        //scale image to fit in the requested size
+        QSize viewport = m_viewport.scaled(size, Qt::KeepAspectRatio);
+        qreal xscale = viewport.width() / (qreal)m_viewport.width();
+        qreal yscale = viewport.height() / (qreal)m_viewport.height();
+
+        //we need to generate packed image for the blur implementation
+        //default QImage constructor may not for enforce this
+        unsigned char* rawSource = (unsigned char*)malloc(viewport.width() * viewport.height() * 4);
+        if (!rawSource)
             return {};
-        }
 
-        // Create a new image with boundaries containing the mask and effect.
-        QImage ret(boundingSize(mask.size()), QImage::Format_ARGB32_Premultiplied);
-        ret.fill(0);
+        //don't make the QImage hold the rawbuffer, as fast_gaussian_blur may swap input and output buffers
+        QImage source(rawSource,
+            viewport.width(), viewport.height(), viewport.width() * 4,
+            QImage::Format_ARGB32_Premultiplied);
 
-        assert(!ret.isNull());
+        // Create a new image with boundaries containing the mask and effect.
+        source.fill(Qt::transparent);
         {
             // Copy the mask
-            QPainter painter(&ret);
+            QPainter painter(&source);
             painter.setCompositionMode(QPainter::CompositionMode_Source);
-            const auto radius = m_blurRadius;
-            painter.drawImage(radius + m_xOffset, radius + m_yOffset, mask);
+            //note: can we use painter.scale here?
+            draw(painter, xscale, yscale);
         }
 
-        // Blur the mask
-        qt_blurImage(ret, m_blurRadius, false);
+        unsigned char* rawDest = (unsigned char*)malloc(viewport.width() * viewport.height() * 4);
+        if (!rawDest)
+        {
+            free(rawSource);
+            return {};
+        }
 
-        return ret;
-    }
+        fast_gaussian_blur(
+            rawSource, rawDest,
+            viewport.width(), viewport.height(), 4, // 4 channels
+            m_blurRadius * xscale / 2, // sigma is radius/2, see https://drafts.csswg.org/css-backgrounds/#shadow-blur
+            3, Border::kMirror //3 passes
+            );
 
-    constexpr QSize boundingSize(const QSize& size) const
-    {
-        // Size of bounding rectangle of the effect
-        const qreal diameter = m_blurRadius * 2;
-        return size + QSize(qAbs(m_xOffset) + diameter, qAbs(m_yOffset) + diameter);
+        free(rawSource);
+        QImage dest(rawDest,
+            viewport.width(), viewport.height(), viewport.width() * 4,
+            QImage::Format_ARGB32_Premultiplied,
+            free, rawDest);
+
+        return dest;
     }
 
+
 protected:
-    qreal m_blurRadius = 1.0;
+    QSize m_viewport;
+
     QColor m_color {63, 63, 63, 180};
+    qreal m_blurRadius = 1.0;
     qreal m_xOffset = 0.0;
     qreal m_yOffset = 0.0;
+    qreal m_recWidth = 0.0;
+    qreal m_height = 0.0;
 };
 
 class RoundedRectDropShadowEffect : public RectDropShadowEffect
@@ -106,35 +132,71 @@ class RoundedRectDropShadowEffect : public RectDropShadowEffect
 public:
     explicit RoundedRectDropShadowEffect(const QVariantMap& settings)
         : RectDropShadowEffect(settings)
+
         , m_xRadius(settings["xRadius"].toReal())
         , m_yRadius(settings["yRadius"].toReal())
     { }
 
-    QImage generate(const QSize& size) const override
+    void draw(QPainter& painter, qreal xscale, qreal yscale) const override
     {
-        assert(!(qFuzzyIsNull(m_xRadius) && qFuzzyIsNull(m_yRadius))); // use RectDropShadowEffect instead
+        painter.setRenderHint(QPainter::Antialiasing);
+        painter.setPen(m_color);
+
+        QPainterPath path;
+        path.addRoundedRect(
+            ((m_viewport.width() / 2.)  - (m_recWidth / 2.) + m_xOffset) * xscale,
+            ((m_viewport.height() / 2.) - (m_height / 2.)   + m_yOffset) * yscale,
+            m_recWidth * xscale,
+            m_height * yscale,
+            m_xRadius * xscale,
+            m_yRadius * yscale);
+        painter.fillPath(path, m_color);
+        painter.drawPath(path);
+    }
 
-        QImage mask(size, QImage::Format_ARGB32_Premultiplied);
-        mask.fill(Qt::transparent);
+protected:
+    qreal m_xRadius = 0.0;
+    qreal m_yRadius = 0.0;
+};
 
-        assert(!mask.isNull());
-        {
-            QPainter painter(&mask);
-            painter.setRenderHint(QPainter::Antialiasing);
-            painter.setPen(m_color);
-
-            QPainterPath path;
-            path.addRoundedRect(mask.rect(), m_xRadius, m_yRadius);
-            painter.fillPath(path, m_color);
-            painter.drawPath(path);
-        }
+class DoubleShadowEffect : public IEffect
+{
+public:
+    explicit DoubleShadowEffect(const QVariantMap& settings)
+        : shadow1(adaptSettings(settings, "primary"))
+        , shadow2(adaptSettings(settings, "secondary"))
+    {
+    }
 
-        return RectDropShadowEffect::generate(mask);
+    static QVariantMap adaptSettings(const QVariantMap& settings, const QString& prefix)
+    {
+        QVariantMap ret;
+        ret["viewportWidth"]  = settings["viewportWidth"].toReal();
+        ret["viewportHeight"] = settings["viewportHeight"].toReal();
+        ret["blurRadius"]     = settings[prefix + "BlurRadius"].toReal();
+        ret["color"]          = settings[prefix + "Color"].value<QColor>();
+        ret["xOffset"]        = settings[prefix + "XOffset"].toReal();
+        ret["yOffset"]        = settings[prefix + "YOffset"].toReal();
+        ret["rectWidth"]      = settings["rectWidth"].toReal();
+        ret["rectHeight"]     = settings["rectHeight"].toReal();
+        ret["xRadius"]        = settings["xRadius"].toReal();
+        ret["yRadius"]        = settings["yRadius"].toReal();
+        return ret;
+    }
+
+    QImage generate(const QSize& size) const override
+    {
+        QImage firstImage = shadow1.generate(size);
+        QImage secondImage = shadow2.generate(size);
+        QPainter painter(&firstImage);
+        painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
+        painter.drawImage(firstImage.rect(), secondImage);
+        return firstImage;
     }
 
 protected:
-    qreal m_xRadius = 0.0;
-    qreal m_yRadius = 0.0;
+    RoundedRectDropShadowEffect shadow1;
+    RoundedRectDropShadowEffect shadow2;
 };
 
 }
@@ -176,6 +238,11 @@ QImage EffectsImageProvider::requestImage(const QString &id, QSize *size, const
             effect = std::make_unique<RoundedRectDropShadowEffect>(queryToVariantMap(query));
             break;
 
+
+        case EffectsImageProvider::DoubleRoundedRectDropShadow:
+            effect = std::make_unique<DoubleShadowEffect>(queryToVariantMap(query));
+            break;
+
         default:
             return {};
         }


=====================================
modules/gui/qt/util/effects_image_provider.hpp
=====================================
@@ -34,7 +34,8 @@ public:
     enum Effect
     {
         RectDropShadow = 1,
-        RoundedRectDropShadow
+        RoundedRectDropShadow,
+        DoubleRoundedRectDropShadow
     };
     Q_ENUM(Effect)
 


=====================================
modules/gui/qt/util/fast_gaussian_blur_template.h
=====================================
@@ -0,0 +1,848 @@
+// Copyright (C) 2017-2022 Basile Fraboni
+// Copyright (C) 2014 Ivan Kutskir (for the original fast blur implementation)
+// All Rights Reserved
+// You may use, distribute and modify this code under the
+// terms of the MIT license. For further details please refer
+// to : https://mit-license.org/
+//
+#pragma once
+
+#include <cmath>
+
+//!
+//! \file fast_gaussian_blur_template.h
+//! \author Basile Fraboni
+//! \date 2017 - 2022
+//!
+//! \brief This contains a C++ implementation of a fast Gaussian blur algorithm in linear time.
+//!
+//! The image buffer is supposed to be of size `w * h * c`, where `h` is height, `w` is width,
+//! and `c` is the number of channels.
+//! The default implementation only supports up to 4 channels images, but one can easily add support for any number of channels
+//! using either specific template cases or a generic function that takes the number of channels as an explicit parameter.
+//! This implementation is focused on learning and readability more than on performance.
+//! The fast blur algorithm is performed with several box blur passes over an image.
+//! The filter converges towards a true Gaussian blur after several passes thanks to the theorem central limit.
+//! In practice, three passes (biquadratic filter) are sufficient for good quality results.
+//! For further details please refer to:
+//!     - http://blog.ivank.net/fastest-gaussian-blur.html
+//!     - https://www.peterkovesi.com/papers/FastGaussianSmoothing.pdf
+//!     - https://github.com/bfraboni/FastGaussianBlur
+//!
+//! **Note:** The fast gaussian blur algorithm is not accurate on image boundaries.
+//! It performs a diffusion of the signal with several passes, each pass depending
+//! on the output of the preceding one. Some of the diffused signal is lost near borders and results in a slight
+//! loss of accuracy for next pass. This problem can be solved by increasing the image support of
+//! half the box kernel extent at each pass of the algorithm. The added padding would in this case
+//! capture the diffusion and make the next pass accurate.
+//! On contrary true Gaussian blur does not suffer this problem since the whole diffusion process
+//! is performed in one pass only.
+//! The extra padding is not performed in this implementation, however we provide several border
+//! policies resulting in dfferent approximations and accuracies.
+//!
+
+//!
+//! \brief Enumeration that decribes border policies for filters.
+//!
+//! For a detailed description of border policies please refer to:
+//! - https://en.wikipedia.org/wiki/Kernel_(image_processing)#Edge_Handling
+//! - https://www.intel.com/content/www/us/en/develop/documentation/ipp-dev-reference/top/volume-2-image-processing/filtering-functions-2/user-defined-border-types.html
+//! - https://docs.opencv.org/3.4/d2/de8/group__core__array.html#ga209f2f4869e304c82d07739337eae7c5
+//! - http://iihm.imag.fr/Docs/java/jai1_0guide/Image-enhance.doc.html
+//!
+enum Border
+{
+    kExtend,
+    kKernelCrop,
+    kMirror,
+    kWrap,
+};
+
+//!
+//! Sliding kernel accumulation has 4 cases:
+//! 1. left side out and right side in
+//! 2. left side in and right side in
+//! 3. left side in and right side out
+//! 4. left side out and right side out
+//!
+//! Small (S) kernels corresponds to kernels with radius < width; r < w
+//! Mid   (M) kernels corresponds to kernels with kernel size < width; 2r+1 < w
+//! Large (L) kernels corresponds to kernels with radius > width; r > w
+//!
+//! The fast version for (S) results in 3 loops for cases 1, 2 and 3.
+//! The fast version for (M) results in 3 loops for cases 1, 4 and 3.
+//! The fast version for (L) results in 1 loop for cases 4.
+//!
+enum Kernel
+{
+    kSmall,
+    kMid,
+    kLarge,
+};
+
+//!
+//! \brief This function performs a single separable horizontal box blur pass with border extend policy.
+//! Templated by buffer data type T, buffer number of channels C.
+//! Faster version for kernels that are smaller than the image width (r <= w).
+//!
+//! \param[in] in           source buffer
+//! \param[in,out] out      target buffer
+//! \param[in] w            image width
+//! \param[in] h            image height
+//! \param[in] r            box dimension
+//!
+template<typename T, int C, Kernel kernel = kSmall>
+inline void horizontal_blur_extend(const T * in, T * out, const int w, const int h, const int r)
+{
+    // change the local variable types depending on the template type for faster calculations
+    using calc_type = std::conditional_t<std::is_integral_v<T>, int, float>;
+
+    const float iarr = 1.f / (r+r+1);
+    #pragma omp parallel for
+    for(int i=0; i<h; i++)
+    {
+        const int begin = i*w;
+        const int end = begin+w;
+        calc_type fv[C], lv[C], acc[C];                 // first value, last value, sliding accumulator
+
+        // init fv, lv, acc by extending outside the image buffer
+        for(int ch=0; ch<C; ++ch)
+        {
+            fv[ch] =  in[begin*C+ch];
+            lv[ch] =  in[(end-1)*C+ch];
+            acc[ch] = (r+1)*fv[ch];
+        }
+
+        if constexpr(kernel == kLarge)
+        {
+            // initial acucmulation
+            for(int j=0; j<r; j++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                // prefilling the accumulator with the last value seems slower than/equal to this ternary
+                acc[ch] += j < w ? in[(begin+j)*C+ch] : lv[ch];
+            }
+
+            for(int ti = begin; ti < end; ti++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += lv[ch] - fv[ch];
+                // assert(acc[ch] >= 0);
+                out[ti*C+ch] = acc[ch]*iarr + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+        }
+        else if constexpr(kernel == kMid)
+        {
+            // current index, left index, right index
+            int ti = begin, li = begin-r-1, ri = begin+r;
+
+            // initial acucmulation
+            for(int j=ti; j<ri; j++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += in[j*C+ch];
+            }
+
+            // 1. left side out and right side in
+            for(; ri<end; ri++, ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += in[ri*C+ch] - fv[ch];
+                // assert(acc[ch] >= 0);
+                out[ti*C+ch] = acc[ch]*iarr + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+
+            // 4. left side out and right side out
+            for(; li<begin; ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += lv[ch] - fv[ch]; //! mid kernels
+                // assert(acc[ch] >= 0);
+                out[ti*C+ch] = acc[ch]*iarr + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+
+            // 3. left side in and right side out
+            for(; ti<end; ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += lv[ch] - in[li*C+ch];
+                // assert(acc[ch] >= 0);
+                out[ti*C+ch] = acc[ch]*iarr + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+        }
+        else if constexpr(kernel == kSmall)
+        {
+            // current index, left index, right index
+            int ti = begin, li = begin-r-1, ri = begin+r;
+
+            // initial acucmulation
+            for(int j=ti; j<ri; j++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += in[j*C+ch];
+            }
+
+            // 1. left side out and right side in
+            for(; li<begin; ri++, ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += in[ri*C+ch] - fv[ch];
+                // assert(acc[ch] >= 0);
+                out[ti*C+ch] = acc[ch]*iarr + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+
+            // 2. left side in and right side in
+            for(; ri<end; ri++, ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += in[ri*C+ch] - in[li*C+ch];
+                // assert(acc[ch] >= 0);
+                out[ti*C+ch] = acc[ch]*iarr + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+
+            // 3. left side in and right side out
+            for(; ti<end; ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += lv[ch] - in[li*C+ch];
+                // assert(acc[ch] >= 0);
+                out[ti*C+ch] = acc[ch]*iarr + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+        }
+    }
+}
+
+//!
+//! \brief This function performs a single separable horizontal box blur pass with kernel crop border policy.
+//! Templated by buffer data type T, buffer number of channels C.
+//! Faster version for kernels that are smaller than the image width (r <= w).
+//!
+//! \param[in] in           source buffer
+//! \param[in,out] out      target buffer
+//! \param[in] w            image width
+//! \param[in] h            image height
+//! \param[in] r            box dimension
+//!
+template<typename T, int C, Kernel kernel = kSmall>
+inline void horizontal_blur_kernel_crop(const T * in, T * out, const int w, const int h, const int r)
+{
+    // change the local variable types depending on the template type for faster calculations
+    using calc_type = std::conditional_t<std::is_integral_v<T>, int, float>;
+
+    const float iarr = 1.f / (r+r+1);
+    const float iwidth = 1.f / w;
+    #pragma omp parallel for
+    for(int i=0; i<h; i++)
+    {
+        const int begin = i*w;
+        const int end = begin+w;
+        calc_type acc[C] = { 0 };
+
+        if constexpr(kernel == kLarge)
+        {
+            // initial acucmulation
+            for(int j=begin; j<end; j++)
+            for(int ch=0; ch < C; ++ch)
+            {
+                acc[ch] += in[j*C+ch];
+            }
+
+            // this is constant
+            for(int j=begin; j<end; j++)
+            for(int ch=0; ch < C; ++ch)
+            {
+                out[j*C+ch] = acc[ch]*iwidth + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+        }
+        else if constexpr(kernel == kMid)
+        {
+            // current index, left index, right index
+            int ti = begin, li = begin-r-1, ri = begin+r;
+
+            // initial acucmulation
+            for(int j=ti; j<ri; j++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += in[j*C+ch];
+            }
+
+            // 1. left side out and right side in
+            for(; ri<end; ri++, ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += in[ri*C+ch];
+                // assert(acc[ch] >= 0);
+                const float inorm = 1.f / float(ri+1-begin);
+                out[ti*C+ch] = acc[ch]*inorm + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+
+            // 4. left side out and right side out
+            for(; li<begin; ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                out[ti*C+ch] = acc[ch]*iwidth + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+
+            // 3. left side in and right side out
+            for(; ti<end; ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] -= in[li*C+ch];
+                // assert(acc[ch] >= 0);
+                const float inorm = 1.f / float(end-li-1);
+                out[ti*C+ch] = acc[ch]*inorm + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+        }
+        else if constexpr(kernel == kSmall)
+        {
+            // current index, left index, right index
+            int ti = begin, li = begin-r-1, ri = begin+r;
+
+            // initial acucmulation
+            for(int j=ti; j<ri; j++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += in[j*C+ch];
+            }
+
+            // 1. left side out and right side in
+            for(; li<begin; ri++, ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += in[ri*C+ch];
+                // assert(acc[ch] >= 0);
+                const float inorm = 1.f / float(ri+1-begin);
+                out[ti*C+ch] = acc[ch]*inorm + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+
+            // 2. left side in and right side in
+            for(; ri<end; ri++, ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += in[ri*C+ch] - in[li*C+ch];
+                // assert(acc[ch] >= 0);
+                out[ti*C+ch] = acc[ch]*iarr + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+
+            // 3. left side in and right side out
+            for(; ti<end; ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] -= in[li*C+ch];
+                // assert(acc[ch] >= 0);
+                const float inorm = 1.f / float(end-li-1);
+                out[ti*C+ch] = acc[ch]*inorm + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+        }
+    }
+}
+
+//! Helper to compute array indices for mirror and wrap border policies.
+struct Index
+{
+    //! wrap index
+    static inline int wrap(const int begin, const int end, const int index)
+    {
+        const int length = end-begin;
+        const int repeat = std::abs(index / length)+1;
+        const int value = index + repeat * length;
+        return begin+(value%length);
+    }
+
+    //! mirror without repetition index
+    static inline int mirror(const int begin, const int end, const int index)
+    {
+        if(index >= begin && index < end)
+            return index;
+
+        const int length = end-begin, last = end-1, slength = length-1;
+        const int pindex = index < begin ? last-index+slength : index-begin;
+        const int repeat = pindex / slength;
+        const int mod = pindex % slength;
+        return repeat%2 ? slength-mod+begin : mod+begin;
+    }
+};
+
+//!
+//! \brief This function performs a single separable horizontal box blur pass with mirror border policy.
+//! Templated by buffer data type T, buffer number of channels C.
+//! Faster version for kernels that are smaller than the image width (r < w).
+//!
+//! \param[in] in           source buffer
+//! \param[in,out] out      target buffer
+//! \param[in] w            image width
+//! \param[in] h            image height
+//! \param[in] r            box dimension
+//!
+//! \todo Rework this one at some point.
+template<typename T, int C, Kernel kernel = kSmall>
+inline void horizontal_blur_mirror(const T* in, T* out, const int w, const int h, const int r)
+{
+    // change the local variable types depending on the template type for faster calculations
+    using calc_type = std::conditional_t<std::is_integral_v<T>, int, float>;
+
+    const double iarr = 1.f/(r+r+1);
+    #pragma omp parallel for
+    for (int i = 0; i < h; i++)
+    {
+        const int begin = i*w;
+        const int end = begin+w;
+        calc_type acc[C] = { 0 };
+
+        // current index, left index, right index
+        int ti = begin, li = begin-r-1, ri = begin+r;
+
+        if constexpr(kernel == kLarge) // generic but slow
+        {
+            // initial acucmulation
+            for(int j=li; j<ri; j++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                const int id = Index::mirror(begin, end, j); // mirrored id
+                acc[ch] += in[id*C+ch];
+            }
+
+            // perform filtering
+            for(int j=0; j<w; j++, ri++, ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                const int rid = Index::mirror(begin, end, ri); // right mirrored id
+                const int lid = Index::mirror(begin, end, li); // left mirrored id
+                acc[ch] += in[rid*C+ch] - in[lid*C+ch];
+                out[ti*C+ch] = acc[ch]*iarr + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+        }
+        else if constexpr(kernel == kMid)
+        {
+            for(int j=li; j<begin; j++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                const int lid = 2 * begin - j; // mirrored id
+                acc[ch] += in[lid*C+ch];
+            }
+
+            for(int j=begin; j<ri; j++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += in[j*C+ch];
+            }
+
+            // 1. left side out and right side in
+            for(; ri<end; ri++, ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                const int lid = 2 * begin - li; // left mirrored id
+                acc[ch] += in[ri*C+ch] - in[lid*C+ch];
+                // assert(acc[ch] >= 0);
+                out[ti*C+ch] = acc[ch]*iarr + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+
+            // 4. left side out and right side out
+            for(; li<begin; ri++, ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                const int rid = 2 * end - 2 - ri;   // right mirrored id
+                const int lid = 2 * begin - li;     // left mirrored id
+                acc[ch] += in[rid*C+ch] - in[lid*C+ch];
+                // assert(acc[ch] >= 0);
+                out[ti*C+ch] = acc[ch]*iarr + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+
+            // 3. left side in and right side out
+            for(; ti<end; ri++, ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                const int rid = 2*end-2-ri; // right mirrored id
+                acc[ch] += in[rid*C+ch] - in[li*C+ch];
+                // assert(acc[ch] >= 0);
+                out[ti*C+ch] = acc[ch]*iarr + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+        }
+        else if constexpr(kernel == kSmall)
+        {
+            for(int j=li; j<begin; j++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                const int lid = 2 * begin - j; // mirrored id
+                acc[ch] += in[lid*C+ch];
+            }
+
+            for(int j=begin; j<ri; j++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += in[j*C+ch];
+            }
+
+            // 1. left side out and right side in
+            for(; li<begin; ri++, ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                const int lid = 2 * begin - li; // left mirrored id
+                acc[ch] += in[ri*C+ch] - in[lid*C+ch];
+                // assert(acc[ch] >= 0);
+                out[ti*C+ch] = acc[ch]*iarr + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+
+            // 2. left side in and right side in
+            for(; ri<end; ri++, ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                acc[ch] += in[ri*C+ch] - in[li*C+ch];
+                // assert(acc[ch] >= 0);
+                out[ti*C+ch] = acc[ch]*iarr + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+
+            // 3. left side in and right side out
+            for(; ti<end; ri++, ti++, li++)
+            for(int ch=0; ch<C; ++ch)
+            {
+                const int rid = 2*end-2-ri; // right mirrored id
+                acc[ch] += in[rid*C+ch] - in[li*C+ch];
+                // assert(acc[ch] >= 0);
+                out[ti*C+ch] = acc[ch]*iarr + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+            }
+        }
+    }
+}
+
+//!
+//! \brief This function performs a single separable horizontal box blur pass with mirror border policy.
+//! Templated by buffer data type T, buffer number of channels C.
+//! Generic version for kernels that are larger than the image width (r >= w).
+//!
+//! \param[in] in           source buffer
+//! \param[in,out] out      target buffer
+//! \param[in] w            image width
+//! \param[in] h            image height
+//! \param[in] r            box dimension
+//!
+//! \todo Make a faster version for small kernels.
+template<typename T, int C>
+inline void horizontal_blur_wrap(const T* in, T* out, const int w, const int h, const int r)
+{
+    // change the local variable types depending on the template type for faster calculations
+    using calc_type = std::conditional_t<std::is_integral_v<T>, int, float>;
+
+    const float iarr = 1.f / (r+r+1);
+    #pragma omp parallel for
+    for(int i=0; i<h; i++)
+    {
+        const int begin = i*w;
+        const int end = begin+w;
+        int ti = begin, li = begin-r-1, ri = begin+r;   // current index, left index, right index
+        calc_type acc[C] = { 0 };                       // sliding accumulator
+
+        // initial acucmulation
+        for(int j=li; j<ri; j++)
+        for(int ch=0; ch<C; ++ch)
+        {
+            const int id = Index::wrap(begin, end, j); // wrapped id
+            acc[ch] += in[id*C+ch];
+        }
+
+        // perform filtering
+        for(int j=0; j<w; j++, ri++, ti++, li++)
+        for(int ch=0; ch<C; ++ch)
+        {
+            const int rid = Index::wrap(begin, end, ri); // right wrapped id
+            const int lid = Index::wrap(begin, end, li); // left wrapped id
+            acc[ch] += in[rid*C+ch] - in[lid*C+ch];
+            out[ti*C+ch] = acc[ch]*iarr + (std::is_integral_v<T> ? 0.5f : 0.f); // fixes darkening with integer types
+        }
+    }
+}
+
+//!
+//! \brief Utility template dispatcher function for horizontal_blur.
+//! Templated by buffer data type T, buffer number of channels C, and border policy P.
+//!
+//! \param[in] in           source buffer
+//! \param[in,out] out      target buffer
+//! \param[in] w            image width
+//! \param[in] h            image height
+//! \param[in] r            box dimension
+//!
+template<typename T, int C, Border P = kMirror>
+inline void horizontal_blur(const T * in, T * out, const int w, const int h, const int r)
+{
+    if constexpr(P == kExtend)
+    {
+        if( r < w/2 )       horizontal_blur_extend<T,C,Kernel::kSmall>(in, out, w, h, r);
+        else if( r < w )    horizontal_blur_extend<T,C,Kernel::kMid  >(in, out, w, h, r);
+        else                horizontal_blur_extend<T,C,Kernel::kLarge>(in, out, w, h, r);
+    }
+    else if constexpr(P == kKernelCrop)
+    {
+        if( r < w/2 )       horizontal_blur_kernel_crop<T,C,Kernel::kSmall>(in, out, w, h, r);
+        else if( r < w )    horizontal_blur_kernel_crop<T,C,Kernel::kMid  >(in, out, w, h, r);
+        else                horizontal_blur_kernel_crop<T,C,Kernel::kLarge>(in, out, w, h, r);
+    }
+    else if constexpr(P == kMirror)
+    {
+        if( r < w/2 )       horizontal_blur_mirror<T,C,Kernel::kSmall>(in, out, w, h, r);
+        else if( r < w )    horizontal_blur_mirror<T,C,Kernel::kMid  >(in, out, w, h, r);
+        else                horizontal_blur_mirror<T,C,Kernel::kLarge>(in, out, w, h, r);
+    }
+    else if constexpr(P == kWrap)
+    {
+        horizontal_blur_wrap<T,C>(in, out, w, h, r);
+    }
+}
+
+//!
+//! \brief Utility template dispatcher function for horizontal_blur. Templated by buffer data type T and border policy P.
+//!
+//! \param[in] in           source buffer
+//! \param[in,out] out      target buffer
+//! \param[in] w            image width
+//! \param[in] h            image height
+//! \param[in] c            image channels
+//! \param[in] r            box dimension
+//!
+template<typename T, Border P = kMirror>
+inline void horizontal_blur(const T * in, T * out, const int w, const int h, const int c, const int r)
+{
+    switch(c)
+    {
+        case 1: horizontal_blur<T,1,P>(in, out, w, h, r); break;
+        case 2: horizontal_blur<T,2,P>(in, out, w, h, r); break;
+        case 3: horizontal_blur<T,3,P>(in, out, w, h, r); break;
+        case 4: horizontal_blur<T,4,P>(in, out, w, h, r); break;
+        default: printf("horizontal_blur over %d channels is not supported yet. Add a specific case if possible or fall back to the generic version.\n", c); break;
+        // default: horizontal_blur<T>(in, out, w, h, c, r); break;
+    }
+}
+
+//!
+//! \brief This function performs a 2D tranposition of an image.
+//!
+//! The transposition is done per
+//! block to reduce the number of cache misses and improve cache coherency for large image buffers.
+//! Templated by buffer data type T and buffer number of channels C.
+//!
+//! \param[in] in           source buffer
+//! \param[in,out] out      target buffer
+//! \param[in] w            image width
+//! \param[in] h            image height
+//!
+template<typename T, int C>
+inline void flip_block(const T * in, T * out, const int w, const int h)
+{
+    constexpr int block = 256/C;
+    #pragma omp parallel for collapse(2)
+    for(int x= 0; x < w; x+= block)
+    for(int y= 0; y < h; y+= block)
+    {
+        const T * p = in + y*w*C + x*C;
+        T * q = out + y*C + x*h*C;
+
+        const int blockx= std::min(w, x+block) - x;
+        const int blocky= std::min(h, y+block) - y;
+        for(int xx= 0; xx < blockx; xx++)
+        {
+            for(int yy= 0; yy < blocky; yy++)
+            {
+                for(int k= 0; k < C; k++)
+                    q[k]= p[k];
+                p+= w*C;
+                q+= C;
+            }
+            p+= -blocky*w*C + C;
+            q+= -blocky*C + h*C;
+        }
+    }
+}
+//!
+//! \brief Utility template dispatcher function for flip_block. Templated by buffer data type T.
+//!
+//! \param[in] in           source buffer
+//! \param[in,out] out      target buffer
+//! \param[in] w            image width
+//! \param[in] h            image height
+//! \param[in] c            image channels
+//!
+template<typename T>
+inline void flip_block(const T * in, T * out, const int w, const int h, const int c)
+{
+    switch(c)
+    {
+        case 1: flip_block<T,1>(in, out, w, h); break;
+        case 2: flip_block<T,2>(in, out, w, h); break;
+        case 3: flip_block<T,3>(in, out, w, h); break;
+        case 4: flip_block<T,4>(in, out, w, h); break;
+        default: printf("flip_block over %d channels is not supported yet. Add a specific case if possible or fall back to the generic version.\n", c); break;
+        // default: flip_block<T>(in, out, w, h, c); break;
+    }
+}
+
+//!
+//! \brief This function converts the standard deviation of
+//! Gaussian blur into a box radius for each box blur pass.
+//! Returns the approximate sigma value achieved with the N box blur passes.
+//!
+//! For further details please refer to :
+//! - https://www.peterkovesi.com/papers/FastGaussianSmoothing.pdf
+//!
+//! \param[out] boxes   box radiis for kernel sizes of 2*boxes[i]+1
+//! \param[in] sigma    Gaussian standard deviation
+//! \param[in] n        number of box blur pass
+//!
+inline float sigma_to_box_radius(int boxes[], const float sigma, const int n)
+{
+    // ideal filter width
+    float wi = std::sqrt((12*sigma*sigma/n)+1);
+    int wl = wi; // no need std::floor
+    if(wl%2==0) wl--;
+    int wu = wl+2;
+
+    float mi = (12*sigma*sigma - n*wl*wl - 4*n*wl - 3*n)/(-4*wl - 4);
+    int m = mi+0.5f; // avoid std::round by adding 0.5f and cast to integer type
+
+    for(int i=0; i<n; i++)
+        boxes[i] = ((i < m ? wl : wu) - 1) / 2;
+
+    return std::sqrt((m*wl*wl+(n-m)*wu*wu-n)/12.f);
+}
+
+//!
+//! \brief This function performs a fast Gaussian blur. Templated by buffer data type T and number of passes N.
+//!
+//! Applying several times box blur tends towards a true Gaussian blur (thanks TCL). Three passes are sufficient
+//! for good results. Templated by buffer data type T and number of passes N. The input buffer is also used
+//! as temporary and modified during the process hence it can not be constant.
+//!
+//! Usually the process should alternate between horizontal and vertical passes
+//! as much times as we want box blur passes. However thanks to box blur properties
+//! the separable passes can be performed in any order without changing the result.
+//! Hence for performance purposes the algorithm is:
+//! - apply N times horizontal blur (horizontal passes)
+//! - flip the image buffer (transposition)
+//! - apply N times horizontal blur (vertical passes)
+//! - flip the image buffer (transposition)
+//!
+//! We provide two version of the function:
+//! - generic N passes (in which more std::swap are used)
+//! - specialized 3 passes only
+//!
+//! \param[in,out] in       source buffer reference ptr
+//! \param[in,out] out      target buffer reference ptr
+//! \param[in] w            image width
+//! \param[in] h            image height
+//! \param[in] c            image channels
+//! \param[in] sigma        Gaussian standard deviation
+//!
+template<typename T, unsigned int N, Border P>
+inline void fast_gaussian_blur(T *& in, T *& out, const int w, const int h, const int c, const float sigma)
+{
+    // compute box kernel sizes
+    int boxes[N];
+    sigma_to_box_radius(boxes, sigma, N);
+
+    // perform N horizontal blur passes
+    for(unsigned int i = 0; i < N; ++i)
+    {
+        horizontal_blur<T,P>(in, out, w, h, c, boxes[i]);
+        std::swap(in, out);
+    }
+
+    // flip buffer
+    flip_block(in, out, w, h, c);
+    std::swap(in, out);
+
+    // perform N horizontal blur passes on flipped image
+    for(unsigned int i = 0; i < N; ++i)
+    {
+        horizontal_blur<T,P>(in, out, h, w, c, boxes[i]);
+        std::swap(in, out);
+    }
+
+    // flip buffer
+    flip_block(in, out, h, w, c);
+}
+
+// specialized 3 passes
+template<typename T, Border P>
+inline void fast_gaussian_blur(T *& in, T *& out, const int w, const int h, const int c, const float sigma)
+{
+    // compute box kernel sizes
+    int boxes[3];
+    sigma_to_box_radius(boxes, sigma, 3);
+
+    // perform 3 horizontal blur passes
+    horizontal_blur<T,P>(in, out, w, h, c, boxes[0]);
+    horizontal_blur<T,P>(out, in, w, h, c, boxes[1]);
+    horizontal_blur<T,P>(in, out, w, h, c, boxes[2]);
+
+    // flip buffer
+    flip_block(out, in, w, h, c);
+
+    // perform 3 horizontal blur passes on flipped image
+    horizontal_blur<T,P>(in, out, h, w, c, boxes[0]);
+    horizontal_blur<T,P>(out, in, h, w, c, boxes[1]);
+    horizontal_blur<T,P>(in, out, h, w, c, boxes[2]);
+
+    // flip buffer
+    flip_block(out, in, h, w, c);
+
+    // swap pointers to get result in the ouput buffer
+    std::swap(in, out);
+}
+
+//!
+//! \brief Utility template dispatcher function for fast_gaussian_blur. Templated by buffer data type T and border policy P.
+//!
+//! This is the main exposed function and the one that should be used in programs.
+//!
+//! \param[in,out] in       source buffer reference ptr
+//! \param[in,out] out      target buffer reference ptr
+//! \param[in] w            image width
+//! \param[in] h            image height
+//! \param[in] c            image channels
+//! \param[in] sigma        Gaussian standard deviation
+//! \param[in] n            number of passes, should be > 0
+//!
+template<typename T, Border P = kMirror>
+void fast_gaussian_blur(T *& in, T *& out, const int w, const int h, const int c, const float sigma, const unsigned int n)
+{
+    switch(n)
+    {
+        case 1: fast_gaussian_blur<T,1,P>(in, out, w, h, c, sigma); break;
+        case 2: fast_gaussian_blur<T,2,P>(in, out, w, h, c, sigma); break;
+        case 3: fast_gaussian_blur<T,  P>(in, out, w, h, c, sigma); break; // specialized 3 passes version
+        case 4: fast_gaussian_blur<T,4,P>(in, out, w, h, c, sigma); break;
+        case 5: fast_gaussian_blur<T,5,P>(in, out, w, h, c, sigma); break;
+        case 6: fast_gaussian_blur<T,6,P>(in, out, w, h, c, sigma); break;
+        case 7: fast_gaussian_blur<T,7,P>(in, out, w, h, c, sigma); break;
+        case 8: fast_gaussian_blur<T,8,P>(in, out, w, h, c, sigma); break;
+        case 9: fast_gaussian_blur<T,9,P>(in, out, w, h, c, sigma); break;
+        case 10: fast_gaussian_blur<T,10,P>(in, out, w, h, c, sigma); break;
+        default: printf("fast_gaussian_blur with %d passes is not supported yet. Add a specific case if possible or fall back to the generic version.\n", n); break;
+        // default: fast_gaussian_blur<T,10>(in, out, w, h, c, sigma, n); break;
+    }
+}
+
+//!
+//! \brief Utility template dispatcher function for fast_gaussian_blur. Templated by buffer data type.
+//!
+//! This is the main exposed function and the one that should be used in programs.
+//!
+//! \param[in,out] in       source buffer reference ptr
+//! \param[in,out] out      target buffer reference ptr
+//! \param[in] w            image width
+//! \param[in] h            image height
+//! \param[in] c            image channels
+//! \param[in] sigma        Gaussian standard deviation
+//! \param[in] n            number of passes, should be > 0
+//! \param[in] p            border policy {kExtend, kMirror, kKernelCrop, kWrap}
+//!
+template<typename T>
+void fast_gaussian_blur(T *& in, T *& out, const int w, const int h, const int c, const float sigma, const unsigned int n, const Border p)
+{
+    switch(p)
+    {
+        case kExtend:       fast_gaussian_blur<T, kExtend>       (in, out, w, h, c, sigma, n); break;
+        case kMirror:       fast_gaussian_blur<T, kMirror>       (in, out, w, h, c, sigma, n); break;
+        case kKernelCrop:   fast_gaussian_blur<T, kKernelCrop>   (in, out, w, h, c, sigma, n); break;
+        case kWrap:         fast_gaussian_blur<T, kWrap>         (in, out, w, h, c, sigma, n); break;
+    }
+}


=====================================
modules/gui/qt/widgets/qml/DoubleShadow.qml
=====================================
@@ -18,63 +18,74 @@
 
 import QtQuick 2.12
 
+import org.videolan.vlc 0.1
+
 import "qrc:///style/"
 
 // A convenience file to encapsulate two drop shadow images stacked on top
 // of each other
-Item {
+ScaledImage {
     id: root
 
-    property var xRadius: null
-    property var yRadius: null
-
-    property alias primaryVerticalOffset: primaryShadow.yOffset
-    property alias primaryHorizontalOffset: primaryShadow.xOffset
-    property alias primaryColor: primaryShadow.color
-    property alias primaryBlurRadius: primaryShadow.blurRadius
-    property alias primaryXRadius: primaryShadow.xRadius
-    property alias primaryYRadius: primaryShadow.yRadius
-
-    property alias secondaryVerticalOffset: secondaryShadow.yOffset
-    property alias secondaryHorizontalOffset: secondaryShadow.xOffset
-    property alias secondaryColor: secondaryShadow.color
-    property alias secondaryBlurRadius: secondaryShadow.blurRadius
-    property alias secondaryXRadius: secondaryShadow.xRadius
-    property alias secondaryYRadius: secondaryShadow.yRadius
-
-    property alias cache: primaryShadow.cache
-
-    visible: (width > 0 && height > 0)
-
-    DropShadowImage {
-        id: primaryShadow
+    property Item sourceItem: null
 
-        anchors.centerIn: parent
-        anchors.alignWhenCentered: false
+    property real viewportWidth: rectWidth + (Math.max(Math.abs(primaryHorizontalOffset) + primaryBlurRadius, Math.abs(secondaryHorizontalOffset) + secondaryBlurRadius)) * 2
+    property real viewportHeight: rectHeight + (Math.max(Math.abs(primaryVerticalOffset) + primaryBlurRadius, Math.abs(secondaryVerticalOffset) + secondaryBlurRadius)) * 2
 
-        color: Qt.rgba(0, 0, 0, .18)
-        xOffset: 0
+    property real rectWidth: sourceItem ? sourceItem.width : 0
+    property real rectHeight: sourceItem ? sourceItem.height : 0
+    property real xRadius: (sourceItem && sourceItem.radius !== undefined ) ? sourceItem.radius : 0
+    property real yRadius: (sourceItem && sourceItem.radius !== undefined ) ? sourceItem.radius : 0
 
-        xRadius: root.xRadius
-        yRadius: root.yRadius
+    property color primaryColor: Qt.rgba(0, 0, 0, .18)
+    property real primaryVerticalOffset: 0
+    property real primaryHorizontalOffset: 0
+    property real primaryBlurRadius: 0
 
-        sourceSize: Qt.size(parent.width, parent.height)
-    }
+    property color secondaryColor: Qt.rgba(0, 0, 0, .22)
+    property real secondaryVerticalOffset: 0
+    property real secondaryHorizontalOffset: 0
+    property real secondaryBlurRadius: 0
 
-    DropShadowImage {
-        id: secondaryShadow
+    //by default we request
+    sourceSize: Qt.size(viewportWidth, viewportHeight)
 
-        anchors.centerIn: parent
-        anchors.alignWhenCentered: false
+    cache: true
+    asynchronous: true
 
-        color: Qt.rgba(0, 0, 0, .22)
-        xOffset: 0
+    fillMode: Image.Stretch
 
-        xRadius: root.xRadius
-        yRadius: root.yRadius
-
-        sourceSize: Qt.size(parent.width, parent.height)
+    visible: (width > 0 && height > 0)
 
-        cache: root.cache
+    z: -1
+
+    onSourceSizeChanged: {
+        // Do not load the image when size is not valid:
+        if (sourceSize.width > 0 && sourceSize.height > 0)
+            source = Qt.binding(() => {
+                return Effects.url(
+                    Effects.DoubleRoundedRectDropShadow,
+                    {
+                        "viewportWidth" : viewportWidth,
+                        "viewportHeight" :viewportHeight,
+
+                        "rectWidth": rectWidth,
+                        "rectHeight": rectHeight,
+                        "xRadius": xRadius,
+                        "yRadius": yRadius,
+
+                        "primaryColor": primaryColor,
+                        "primaryBlurRadius": primaryBlurRadius,
+                        "primaryXOffset": primaryHorizontalOffset,
+                        "primaryYOffset": primaryVerticalOffset,
+
+                        "secondaryColor": secondaryColor,
+                        "secondaryBlurRadius": secondaryBlurRadius,
+                        "secondaryXOffset": secondaryHorizontalOffset,
+                        "secondaryYOffset": secondaryVerticalOffset,
+                    })
+            })
+        else
+            source = ""
     }
 }


=====================================
modules/gui/qt/widgets/qml/DragItem.qml
=====================================
@@ -410,10 +410,9 @@ Item {
                 color: theme.bg.primary
 
                 DefaultShadow {
-                    anchors.fill: parent
+                    anchors.centerIn: parent
 
-                    xRadius: bg.radius
-                    yRadius: bg.radius
+                    sourceItem: bg
                 }
             }
 
@@ -493,10 +492,9 @@ Item {
         }
 
         DefaultShadow {
-            anchors.fill: parent
+            anchors.centerIn: parent
 
-            xRadius: extraCovers.radius
-            yRadius: extraCovers.radius
+            sourceItem: extraCovers
         }
     }
 


=====================================
modules/gui/qt/widgets/qml/DropShadowImage.qml
=====================================
@@ -21,17 +21,29 @@ import QtQuick 2.12
 import org.videolan.vlc 0.1
 
 ScaledImage {
-    property var blurRadius: null
-    property var color: null
-    property var xOffset: null
-    property var yOffset: null
-    property var xRadius: null
-    property var yRadius: null
+
+    property Item sourceItem: null
+
+    property real viewportWidth: rectWidth + (blurRadius + Math.abs(xOffset)) * 2
+    property real viewportHeight: rectHeight + (blurRadius + Math.abs(yOffset)) * 2
+
+    property real blurRadius: 0
+    property color color
+
+    property real rectWidth: sourceItem ? sourceItem.width : 0
+    property real rectHeight: sourceItem ? sourceItem.height : 0
+
+    property real xOffset: 0
+    property real yOffset: 0
+    property real xRadius: (sourceItem && sourceItem.radius !== undefined) ? sourceItem.radius : 0
+    property real yRadius: (sourceItem && sourceItem.radius !== undefined) ? sourceItem.radius : 0
+
+    sourceSize: Qt.size(viewportWidth, viewportHeight)
 
     cache: true
     asynchronous: true
 
-    fillMode: Image.Pad
+    fillMode: Image.Stretch
 
     onSourceSizeChanged: {
         // Do not load the image when size is not valid:
@@ -39,8 +51,14 @@ ScaledImage {
             source = Qt.binding(function() {
                 return Effects.url((xRadius > 0 || yRadius > 0) ? Effects.RoundedRectDropShadow
                                                                 : Effects.RectDropShadow,
-                                   {"blurRadius": blurRadius,
+                                   {
+                                    "viewportWidth" : viewportWidth,
+                                    "viewportHeight" :viewportHeight,
+
+                                    "blurRadius": blurRadius,
                                     "color": color,
+                                    "rectWidth": rectWidth,
+                                    "rectHeight": rectHeight,
                                     "xOffset": xOffset,
                                     "yOffset": yOffset,
                                     "xRadius": xRadius,


=====================================
modules/gui/qt/widgets/qml/EmptyLabel.qml
=====================================
@@ -85,7 +85,8 @@ T.Control {
                 }
 
                 Widgets.DefaultShadow {
-                    anchors.fill: cover
+                    anchors.centerIn: cover
+                    sourceItem: parent
                 }
             }
 


=====================================
modules/gui/qt/widgets/qml/GridItem.qml
=====================================
@@ -262,27 +262,31 @@ T.ItemDelegate {
                 DefaultShadow {
                     id: unselectedShadow
 
-                    anchors.fill: parent
-                    anchors.margins: VLCStyle.dp(1) // outside border (unselected)
+                    anchors.centerIn: parent
 
                     visible: opacity > 0
 
-                    xRadius: parent.radius
-                    yRadius: parent.radius
+                    sourceItem: parent
+
+                    width: viewportWidth
+                    height: viewportHeight
+                    sourceSize: Qt.size(128, 128)
                 }
 
                 DoubleShadow {
                     id: selectedShadow
 
-                    anchors.fill: parent
-                    anchors.margins: VLCStyle.dp(1)
-                    z: -1
+                    anchors.centerIn: parent
 
                     visible: opacity > 0
                     opacity: 0
 
-                    xRadius: parent.radius
-                    yRadius: parent.radius
+                    sourceItem: parent
+
+                    width: viewportWidth
+                    height: viewportHeight
+
+                    sourceSize: Qt.size(128, 128)
 
                     primaryVerticalOffset: VLCStyle.dp(6, VLCStyle.scale)
                     primaryBlurRadius: VLCStyle.dp(18, VLCStyle.scale)


=====================================
modules/gui/qt/widgets/qml/TableColumns.qml
=====================================
@@ -139,10 +139,9 @@ Item {
             }
 
             DefaultShadow {
-                anchors.fill: parent
+                anchors.centerIn: parent
 
-                xRadius: parent.radius
-                yRadius: parent.radius
+                sourceItem: parent
             }
         }
 



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/a8a57ba8c4a9b9cd717be22ad18c23e5db5aafde...352b88c10636b86f5aca08f610cb9cc8127df950

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/a8a57ba8c4a9b9cd717be22ad18c23e5db5aafde...352b88c10636b86f5aca08f610cb9cc8127df950
You're receiving this email because of your account on code.videolan.org.


VideoLAN code repository instance


More information about the vlc-commits mailing list