diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,8 +10,6 @@ include(WriteBasicConfigVersionFile) include(FeatureSummary) -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules") - if(USE_KDE4) find_package(KDE4 REQUIRED) include(KDE4Defaults) diff --git a/cmake/Modules/FindFFTW.cmake b/cmake/Modules/FindFFTW.cmake deleted file mode 100644 --- a/cmake/Modules/FindFFTW.cmake +++ /dev/null @@ -1,20 +0,0 @@ -# Find the FFTW library -# -# Usage: -# find_package(FFTW [REQUIRED]) -# -# It sets the following variables: -# FFTW_FOUND -# FFTW_INCLUDES -# FFTW_LIBRARIES - - -find_path(FFTW_INCLUDES fftw3.h) - -find_library(FFTW_LIBRARIES NAMES fftw3) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(FFTW DEFAULT_MSG - FFTW_INCLUDES FFTW_LIBRARIES) - -mark_as_advanced(FFTW_INCLUDES FFTW_LIBRARIES) diff --git a/kdecoration/breezedecoration.cpp b/kdecoration/breezedecoration.cpp --- a/kdecoration/breezedecoration.cpp +++ b/kdecoration/breezedecoration.cpp @@ -29,7 +29,7 @@ #include "breezebutton.h" #include "breezesizegrip.h" -#include "breezeboxshadowhelper.h" +#include "breezeboxshadowrenderer.h" #include #include @@ -729,43 +729,37 @@ return c; }; - // In order to properly render a box shadow with a given radius `shadowSize`, - // the box size should be at least `2 * QSize(shadowSize, shadowSize)`. - const int shadowSize = qMax(params.shadow1.radius, params.shadow2.radius); - const QRect box(shadowSize, shadowSize, 2 * shadowSize + 1, 2 * shadowSize + 1); - const QRect rect = box.adjusted(-shadowSize, -shadowSize, shadowSize, shadowSize); + const QSize boxSize = BoxShadowRenderer::minimumRequiredBoxSize(params.shadow1.radius) + .expandedTo(BoxShadowRenderer::minimumRequiredBoxSize(params.shadow2.radius)); - QImage shadow(rect.size(), QImage::Format_ARGB32_Premultiplied); - shadow.fill(Qt::transparent); - - QPainter painter(&shadow); - painter.setRenderHint(QPainter::Antialiasing); + BoxShadowRenderer shadowRenderer; + shadowRenderer.setBorderRadius(Metrics::Frame_FrameRadius + 0.5); + shadowRenderer.setBoxSize(boxSize); + shadowRenderer.setDevicePixelRatio(1.0); // TODO: Create HiDPI shadows? const qreal strength = static_cast(g_shadowStrength) / 255.0; - - // Draw the "shape" shadow. - BoxShadowHelper::boxShadow( - &painter, - box, - params.shadow1.offset, - params.shadow1.radius, + shadowRenderer.addShadow(params.shadow1.offset, params.shadow1.radius, withOpacity(g_shadowColor, params.shadow1.opacity * strength)); - - // Draw the "contrast" shadow. - BoxShadowHelper::boxShadow( - &painter, - box, - params.shadow2.offset, - params.shadow2.radius, + shadowRenderer.addShadow(params.shadow2.offset, params.shadow2.radius, withOpacity(g_shadowColor, params.shadow2.opacity * strength)); + QImage shadowTexture = shadowRenderer.render(); + + QPainter painter(&shadowTexture); + painter.setRenderHint(QPainter::Antialiasing); + + const QRect outerRect = shadowTexture.rect(); + + QRect boxRect(QPoint(0, 0), boxSize); + boxRect.moveCenter(outerRect.center()); + // Mask out inner rect. const QMargins padding = QMargins( - shadowSize - Metrics::Shadow_Overlap - params.offset.x(), - shadowSize - Metrics::Shadow_Overlap - params.offset.y(), - shadowSize - Metrics::Shadow_Overlap + params.offset.x(), - shadowSize - Metrics::Shadow_Overlap + params.offset.y()); - const QRect innerRect = rect - padding; + boxRect.left() - outerRect.left() - Metrics::Shadow_Overlap - params.offset.x(), + boxRect.top() - outerRect.top() - Metrics::Shadow_Overlap - params.offset.y(), + outerRect.right() - boxRect.right() - Metrics::Shadow_Overlap + params.offset.x(), + outerRect.bottom() - boxRect.bottom() - Metrics::Shadow_Overlap + params.offset.y()); + const QRect innerRect = outerRect - padding; painter.setPen(Qt::NoPen); painter.setBrush(Qt::black); @@ -788,8 +782,8 @@ g_sShadow = QSharedPointer::create(); g_sShadow->setPadding(padding); - g_sShadow->setInnerShadowRect(QRect(shadow.rect().center(), QSize(1, 1))); - g_sShadow->setShadow(shadow); + g_sShadow->setInnerShadowRect(QRect(outerRect.center(), QSize(1, 1))); + g_sShadow->setShadow(shadowTexture); } setShadow(g_sShadow); diff --git a/kstyle/breezeshadowhelper.cpp b/kstyle/breezeshadowhelper.cpp --- a/kstyle/breezeshadowhelper.cpp +++ b/kstyle/breezeshadowhelper.cpp @@ -20,7 +20,7 @@ #include "breezeshadowhelper.h" #include "breeze.h" -#include "breezeboxshadowhelper.h" +#include "breezeboxshadowrenderer.h" #include "breezehelper.h" #include "breezepropertynames.h" #include "breezestyleconfigdata.h" @@ -285,46 +285,46 @@ return c; }; - const int shadowSize = qMax(params.shadow1.radius, params.shadow2.radius); const QColor color = StyleConfigData::shadowColor(); const qreal strength = static_cast(StyleConfigData::shadowStrength()) / 255.0; - const QRect box( - shadowSize, - shadowSize, - 2 * shadowSize + 1, - 2 * shadowSize + 1); - const QRect outerRect = box.adjusted(-shadowSize, -shadowSize, shadowSize, shadowSize); + const QSize boxSize = BoxShadowRenderer::minimumRequiredBoxSize(params.shadow1.radius) + .expandedTo(BoxShadowRenderer::minimumRequiredBoxSize(params.shadow2.radius)); - QPixmap shadow = _helper.highDpiPixmap(outerRect.width(), outerRect.height()); - shadow.fill(Qt::transparent); + #if QT_VERSION >= 0x050300 + const qreal dpr = qApp->devicePixelRatio(); + #else + const qreal dpr = 1.0; + #endif - QPainter painter(&shadow); - painter.setRenderHint(QPainter::Antialiasing); + const qreal frameRadius = _helper.frameRadius(); - // Draw the "shape" shadow. - BoxShadowHelper::boxShadow( - &painter, - box, - params.shadow1.offset, - params.shadow1.radius, - withOpacity(color, params.shadow1.opacity * strength)); + BoxShadowRenderer shadowRenderer; + shadowRenderer.setBorderRadius(frameRadius); + shadowRenderer.setBoxSize(boxSize); + shadowRenderer.setDevicePixelRatio(dpr); - // Draw the "contrast" shadow. - BoxShadowHelper::boxShadow( - &painter, - box, - params.shadow2.offset, - params.shadow2.radius, + shadowRenderer.addShadow(params.shadow1.offset, params.shadow1.radius, + withOpacity(color, params.shadow1.opacity * strength)); + shadowRenderer.addShadow(params.shadow2.offset, params.shadow2.radius, withOpacity(color, params.shadow2.opacity * strength)); + QImage shadowTexture = shadowRenderer.render(); + + const QRect outerRect(QPoint(0, 0), shadowTexture.size() / dpr); + + QRect boxRect(QPoint(0, 0), boxSize); + boxRect.moveCenter(outerRect.center()); + // Mask out inner rect. + QPainter painter(&shadowTexture); + painter.setRenderHint(QPainter::Antialiasing); + const QMargins margins = QMargins( - shadowSize - Metrics::Shadow_Overlap - params.offset.x(), - shadowSize - Metrics::Shadow_Overlap - params.offset.y(), - shadowSize - Metrics::Shadow_Overlap + params.offset.x(), - shadowSize - Metrics::Shadow_Overlap + params.offset.y()); - const qreal frameRadius = _helper.frameRadius(); + boxRect.left() - outerRect.left() - Metrics::Shadow_Overlap - params.offset.x(), + boxRect.top() - outerRect.top() - Metrics::Shadow_Overlap - params.offset.y(), + outerRect.right() - boxRect.right() - Metrics::Shadow_Overlap + params.offset.x(), + outerRect.bottom() - boxRect.bottom() - Metrics::Shadow_Overlap + params.offset.y()); painter.setPen(Qt::NoPen); painter.setBrush(Qt::black); @@ -343,7 +343,7 @@ const QPoint innerRectTopLeft = outerRect.center(); _shadowTiles = TileSet( - shadow, + QPixmap::fromImage(shadowTexture), innerRectTopLeft.x(), innerRectTopLeft.y(), 1, 1); @@ -573,12 +573,22 @@ return QMargins(); } - const int shadowSize = qMax(params.shadow1.radius, params.shadow2.radius); + const QSize boxSize = BoxShadowRenderer::minimumRequiredBoxSize(params.shadow1.radius) + .expandedTo(BoxShadowRenderer::minimumRequiredBoxSize(params.shadow2.radius)); + + const QSize canvasSize = BoxShadowRenderer::minimumRequiredCanvasSize(boxSize, params.shadow1.radius, params.shadow1.offset) + .expandedTo(BoxShadowRenderer::minimumRequiredCanvasSize(boxSize, params.shadow2.radius, params.shadow2.offset)); + + const QRect canvasRect(QPoint(0, 0), canvasSize); + + QRect boxRect(QPoint(0, 0), boxSize); + boxRect.moveCenter(canvasRect.center()); + QMargins margins( - shadowSize - Metrics::Shadow_Overlap - params.offset.x(), - shadowSize - Metrics::Shadow_Overlap - params.offset.y(), - shadowSize - Metrics::Shadow_Overlap + params.offset.x(), - shadowSize - Metrics::Shadow_Overlap + params.offset.y()); + boxRect.left() - canvasRect.left() - Metrics::Shadow_Overlap - params.offset.x(), + boxRect.top() - canvasRect.top() - Metrics::Shadow_Overlap - params.offset.y(), + canvasRect.right() - boxRect.right() - Metrics::Shadow_Overlap + params.offset.x(), + canvasRect.bottom() - boxRect.right() - Metrics::Shadow_Overlap + params.offset.y()); if (widget->inherits("QBalloonTip")) { // Balloon tip needs special margins to deal with the arrow. diff --git a/libbreezecommon/CMakeLists.txt b/libbreezecommon/CMakeLists.txt --- a/libbreezecommon/CMakeLists.txt +++ b/libbreezecommon/CMakeLists.txt @@ -11,9 +11,6 @@ endif () ################# dependencies ################# -### FFTW -find_package(FFTW REQUIRED) - ### Qt/KDE if (NOT BREEZE_COMMON_USE_KDE4) find_package(Qt5 REQUIRED CONFIG COMPONENTS Widgets) @@ -24,7 +21,7 @@ ################# breezestyle target ################# set(breezecommon_LIB_SRCS - breezeboxshadowhelper.cpp + breezeboxshadowrenderer.cpp ) if (BREEZE_COMMON_USE_KDE4) @@ -35,7 +32,6 @@ EXPORT_FILE_NAME breezecommon_export.h) target_link_libraries(breezecommon4 ${KDE4_KDEUI_LIBS}) - target_link_libraries(breezecommon4 ${FFTW_LIBRARIES}) set_target_properties(breezecommon4 PROPERTIES VERSION ${PROJECT_VERSION} @@ -52,9 +48,7 @@ target_link_libraries(breezecommon5 PUBLIC Qt5::Core - Qt5::Gui - PRIVATE - ${FFTW_LIBRARIES}) + Qt5::Gui) set_target_properties(breezecommon5 PROPERTIES VERSION ${PROJECT_VERSION} diff --git a/libbreezecommon/breezeboxshadowhelper.h b/libbreezecommon/breezeboxshadowhelper.h deleted file mode 100644 --- a/libbreezecommon/breezeboxshadowhelper.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2018 Vlad Zagorodniy - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of - * the License or (at your option) version 3 or any later version - * accepted by the membership of KDE e.V. (or its successor approved - * by the membership of KDE e.V.), which shall act as a proxy - * defined in Section 14 of version 3 of the license. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef BREEZE_COMMON_BOXSHADOWHELPER_H -#define BREEZE_COMMON_BOXSHADOWHELPER_H - -#include "breezecommon_export.h" - -#include -#include -#include -#include - - -namespace Breeze { -namespace BoxShadowHelper { - -void BREEZECOMMON_EXPORT boxShadow(QPainter *p, const QRect &box, const QPoint &offset, - int radius, const QColor &color); - -} // BoxShadowHelper -} // Breeze - -#endif // BREEZE_COMMON_BOXSHADOWHELPER_H diff --git a/libbreezecommon/breezeboxshadowhelper.cpp b/libbreezecommon/breezeboxshadowhelper.cpp deleted file mode 100644 --- a/libbreezecommon/breezeboxshadowhelper.cpp +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright (C) 2018 Vlad Zagorodniy - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of - * the License or (at your option) version 3 or any later version - * accepted by the membership of KDE e.V. (or its successor approved - * by the membership of KDE e.V.), which shall act as a proxy - * defined in Section 14 of version 3 of the license. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "breezeboxshadowhelper.h" -#include "config-breezecommon.h" - -#include - -#include - -#include - - -namespace Breeze { -namespace BoxShadowHelper { - -namespace { - // FFT approach outperforms naive blur method when blur radius >= 64. - // (was discovered after doing a lot of benchmarks) - const int FFT_BLUR_RADIUS_THRESHOLD = 64; - - // According to the CSS Level 3 spec, standard deviation must be equal to - // half of the blur radius. https://www.w3.org/TR/css-backgrounds-3/#shadow-blur - // Current window size is too small for sigma equal to half of the blur radius. - // As a workaround, sigma blur scale is lowered. With the lowered sigma - // blur scale, area under the kernel equals to 0.98, which is pretty enough. - // Maybe, it should be changed in the future. - const double SIGMA_BLUR_SCALE = 0.4375; -} - -inline int kernelSizeToRadius(int kernelSize) -{ - return (kernelSize - 1) / 2; -} - -inline int radiusToKernelSize(int radius) -{ - return radius * 2 + 1; -} - -QVector computeGaussianKernel(int radius) -{ - QVector kernel; - const int kernelSize = radiusToKernelSize(radius); - kernel.reserve(kernelSize); - - const double sigma = SIGMA_BLUR_SCALE * radius; - const double den = std::sqrt(2.0) * sigma; - double kernelNorm = 0.0; - double lastInt = 0.5 * std::erf((-radius - 0.5) / den); - - for (int i = 0; i < kernelSize; i++) { - const double currInt = 0.5 * std::erf((i - radius + 0.5) / den); - const double w = currInt - lastInt; - kernel << w; - kernelNorm += w; - lastInt = currInt; - } - - for (auto &w : kernel) { - w /= kernelNorm; - } - - return kernel; -} - -// Do horizontal pass of the Gaussian filter. Please notice that the result -// is transposed. So, the dst image should have proper size, e.g. if the src -// image have (wxh) size then the dst image should have (hxw) size. The -// result is transposed so we read memory in linear order. -void blurAlphaNaivePass(const QImage &src, QImage &dst, const QVector &kernel) -{ - const int alphaOffset = QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3; - const int alphaStride = src.depth() >> 3; - const int radius = kernelSizeToRadius(kernel.size()); - - for (int y = 0; y < src.height(); y++) { - const uchar *in = src.scanLine(y) + alphaOffset; - uchar *out = dst.scanLine(0) + alphaOffset + y * alphaStride; - - for (int x = 0; x < radius; x++) { - const uchar *window = in; - double alpha = 0.0; - for (int k = radius - x; k < kernel.size(); k++) { - alpha += *window * kernel[k]; - window += alphaStride; - } - *out = static_cast(alpha); - out += dst.width() * alphaStride; - } - - for (int x = radius; x < src.width() - radius; x++) { - const uchar *window = in + (x - radius) * alphaStride; - double alpha = 0.0; - for (int k = 0; k < kernel.size(); k++) { - alpha += *window * kernel[k]; - window += alphaStride; - } - *out = static_cast(alpha); - out += dst.width() * alphaStride; - } - - for (int x = src.width() - radius; x < src.width(); x++) { - const uchar *window = in + (x - radius - 1) * alphaStride; - double alpha = 0.0; - const int outside = x + radius - src.width(); - for (int k = 0; k < kernel.size() - outside; k++) { - alpha += *window * kernel[k]; - window += alphaStride; - } - *out = static_cast(alpha); - out += dst.width() * alphaStride; - } - } -} - -// Blur alpha channel of the given image using separable convolution -// gaussian kernel. Not very efficient with big blur radii. -void blurAlphaNaive(QImage &img, int radius) -{ - const QVector kernel = computeGaussianKernel(radius); - QImage tmp(img.height(), img.width(), img.format()); - - blurAlphaNaivePass(img, tmp, kernel); // horizontal pass - blurAlphaNaivePass(tmp, img, kernel); // vertical pass -} - -// Blur alpha channel of the given image using Fourier Transform. -// It's somewhat efficient with big blur radii. -// -// It works as follows: -// - do FFT on given input image(it is expected, that the -// input image was padded before) -// - compute Gaussian kernel, pad it to the size of the input -// image, and do FFT on it -// - multiply the two in the frequency domain(element-wise) -// - transform the result back to "time domain" -// -void blurAlphaFFT(QImage &img, int radius) -{ - const int alphaOffset = QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3; - const int alphaStride = img.depth() >> 3; - const int size = img.width() * img.height(); - - // Use FFTW's malloc function so the returned pointer obeys any - // special alignment restrictions. (e.g. for SIMD acceleration, etc) - // See http://www.fftw.org/fftw3_doc/MekernelSizeToRadius(mory-Allocation.html - fftw_complex *imageIn = fftw_alloc_complex(size); - fftw_complex *imageOut = fftw_alloc_complex(size); - - uchar *data = img.scanLine(0) + alphaOffset; - for (int i = 0; i < size; i++) { - imageIn[i][0] = *data; - imageIn[i][1] = 0.0; - data += alphaStride; - } - - fftw_plan imageFFT = fftw_plan_dft_2d( - img.height(), img.width(), - imageIn, imageOut, - FFTW_FORWARD, FFTW_ESTIMATE); - - fftw_plan imageIFFT = fftw_plan_dft_2d( - img.height(), img.width(), - imageOut, imageIn, - FFTW_BACKWARD, FFTW_ESTIMATE); - - // The computed Gaussian kernel has to have the same size as the input image. - // Please note that the center of the computed Gaussian kernel is placed - // at the top-left corner and the whole kernel is wrapped around so we read - // result in linear order. - // Note: the kernel is computed by taking a product of two 1-D Gaussian kernels. - QVector kernel(size, 0); - const QVector kernel_ = computeGaussianKernel(radius); - for (int y = 0; y < kernel_.size(); y++) { - const int i = (img.height() + y - radius) % img.height(); - for (int x = 0; x < kernel_.size(); x++) { - const int j = (img.width() + x - radius) % img.width(); - kernel[j + i * img.width()] = kernel_[x] * kernel_[y]; - } - } - - fftw_complex *kernelIn = fftw_alloc_complex(kernel.size()); - fftw_complex *kernelOut = fftw_alloc_complex(kernel.size()); - - for (int i = 0; i < size; i++) { - kernelIn[i][0] = kernel[i]; - kernelIn[i][1] = 0.0; - } - - fftw_plan kernelFFT = fftw_plan_dft_2d( - img.height(), img.width(), - kernelIn, kernelOut, - FFTW_FORWARD, FFTW_ESTIMATE); - - // Do actual FFT. - fftw_execute(imageFFT); - fftw_execute(kernelFFT); - - for (int i = 0; i < size; i++) { - const double re = imageOut[i][0] * kernelOut[i][0] - imageOut[i][1] * kernelOut[i][1]; - const double im = imageOut[i][0] * kernelOut[i][1] + imageOut[i][1] * kernelOut[i][0]; - imageOut[i][0] = re; - imageOut[i][1] = im; - } - - fftw_execute(imageIFFT); - - // Copy result back. Please note, result is scaled by `width x height` so we need to scale it down. - const double invSize = 1.0 / size; - data = img.scanLine(0) + alphaOffset; - for (int i = 0; i < size; i++) { - *data = imageIn[i][0] * invSize; - data += alphaStride; - } - - fftw_destroy_plan(kernelFFT); - fftw_destroy_plan(imageFFT); - fftw_destroy_plan(imageIFFT); - - fftw_free(kernelIn); - fftw_free(kernelOut); - - fftw_free(imageIn); - fftw_free(imageOut); -} - -void boxShadow(QPainter *p, const QRect &box, const QPoint &offset, int radius, const QColor &color) -{ - const QSize size = box.size() + 2 * QSize(radius, radius); - -#if BREEZE_COMMON_USE_KDE4 - const qreal dpr = 1.0; -#else - const qreal dpr = p->device()->devicePixelRatioF(); -#endif - - QPainter painter; - - QImage shadow(size * dpr, QImage::Format_ARGB32_Premultiplied); -#if !BREEZE_COMMON_USE_KDE4 - shadow.setDevicePixelRatio(dpr); -#endif - shadow.fill(Qt::transparent); - - painter.begin(&shadow); - painter.fillRect(QRect(QPoint(radius, radius), box.size()), Qt::black); - painter.end(); - - // There is no need to blur RGB channels. Blur the alpha - // channel and then give the shadow a tint of the desired color. - const int radius_ = radius * dpr; - if (radius_ < FFT_BLUR_RADIUS_THRESHOLD) { - blurAlphaNaive(shadow, radius_); - } else { - blurAlphaFFT(shadow, radius_); - } - - painter.begin(&shadow); - painter.setCompositionMode(QPainter::CompositionMode_SourceIn); - painter.fillRect(shadow.rect(), color); - painter.end(); - - QRect shadowRect = shadow.rect(); - shadowRect.setSize(shadowRect.size() / dpr); - shadowRect.moveCenter(box.center() + offset); - p->drawImage(shadowRect, shadow); -} - -} // BoxShadowHelper -} // Breeze diff --git a/libbreezecommon/breezeboxshadowrenderer.h b/libbreezecommon/breezeboxshadowrenderer.h new file mode 100644 --- /dev/null +++ b/libbreezecommon/breezeboxshadowrenderer.h @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 Vlad Zagorodniy + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +// own +#include "breezecommon_export.h" + +// Qt +#include +#include +#include + +namespace Breeze +{ + +class BREEZECOMMON_EXPORT BoxShadowRenderer +{ +public: + // Compiler generated constructors & destructor are fine. + + /** + * Set the size of the box + * @param size The size of the box + **/ + void setBoxSize(const QSize &size); + + /** + * Set the radius of box' corners + * @param radius The border radius + **/ + void setBorderRadius(qreal radius); + + /** + * Set the device pixel ratio of the resulting shadow texture + * @param dpr The device pixel ratio + **/ + void setDevicePixelRatio(qreal dpr); + + /** + * Add a shadow + * @param offset The offset of the shadow + * @param radius The blur radius + * @param color The color of the shadow + **/ + void addShadow(const QPoint &offset, int radius, const QColor &color); + + /** + * Render the shadow + **/ + QImage render() const; + + /** + * Calculate minimum required size of the box + * + * @param radius The blur radius + **/ + static QSize minimumRequiredBoxSize(int radius); + + /** + * + * @param boxSize The size of the box + * @param radius The blur radius + * @param offset The offset of the shadow + **/ + static QSize minimumRequiredCanvasSize(const QSize &boxSize, int radius, const QPoint &offset); + +private: + QSize m_boxSize; + qreal m_borderRadius = 0.0; + qreal m_dpr = 1.0; + + struct Shadow { + QPoint offset; + int radius; + QColor color; + }; + + QVector m_shadows; +}; + +} // namespace Breeze diff --git a/libbreezecommon/breezeboxshadowrenderer.cpp b/libbreezecommon/breezeboxshadowrenderer.cpp new file mode 100644 --- /dev/null +++ b/libbreezecommon/breezeboxshadowrenderer.cpp @@ -0,0 +1,365 @@ +/* + * Copyright (C) 2018 Vlad Zagorodniy + * + * The box blur implementation is based on AlphaBoxBlur from Firefox. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// own +#include "breezeboxshadowrenderer.h" + +// Qt +#include +#include + +namespace Breeze +{ + +static inline int calculateBlurRadius(qreal stdDev) +{ + // See https://www.w3.org/TR/SVG11/filters.html#feGaussianBlurElement + const qreal gaussianScaleFactor = (3.0 * qSqrt(2.0 * M_PI) / 4.0) * 1.5; // * 0.9; + return qMax(2, qFloor(stdDev * gaussianScaleFactor + 0.5)); +} + +static inline qreal calculateBlurStdDev(int radius) +{ + // The resulting shadow must approximate the image that would be generated by + // applying to the shadow a Gaussian blur with a standard deviation equal to + // half the blur radius. See https://www.w3.org/TR/css-backgrounds-3/#shadow-blur + return radius / 2.0; +} + +static inline QSize calculateBlurExtent(int radius) +{ + const int blurRadius = calculateBlurRadius(calculateBlurStdDev(radius)); + return QSize(blurRadius, blurRadius); +} + +struct BoxLobes +{ + int left; + int right; +}; + +static QVector computeLobes(int radius) +{ + const int blurRadius = calculateBlurRadius(calculateBlurStdDev(radius)); + const int z = blurRadius / 3; + + int major; + int minor; + int final; + + switch (blurRadius % 3) { + case 0: + major = z; + minor = z; + final = z; + break; + + case 1: + major = z + 1; + minor = z; + final = z; + break; + + case 2: + major = z + 1; + minor = z; + final = z + 1; + break; + + default: + Q_UNREACHABLE(); + break; + } + + Q_ASSERT(major + minor + final == blurRadius); + + return { + {major, minor}, + {minor, major}, + {final, final} + }; + + // QVector lobes; + // lobes.reserve(3); + + // if (blurRadius % 2) { + // const int lobeSize = (blurRadius - 1) / 2; + // lobes.append({lobeSize, lobeSize}); + // lobes.append({lobeSize, lobeSize}); + // lobes.append({lobeSize, lobeSize}); + // } else { + // const int lobeSize = blurRadius / 2; + // lobes.append({lobeSize, lobeSize - 1}); + // lobes.append({lobeSize - 1, lobeSize}); + // lobes.append({lobeSize, lobeSize}); + // } + + // return lobes; +} + +static inline void boxBlurRowAlpha(const uint8_t *src, uint8_t *dst, int width, int horizontalStride, + int verticalStride, const BoxLobes &lobes, bool transposeInput, + bool transposeOutput) +{ + const int inputStep = transposeInput ? verticalStride : horizontalStride; + const int outputStep = transposeOutput ? verticalStride : horizontalStride; + + const int boxSize = lobes.left + 1 + lobes.right; + const int reciprocal = (1 << 24) / boxSize; + + uint32_t alphaSum = (boxSize + 1) / 2; + + const uint8_t *left = src; + const uint8_t *right = src; + uint8_t *out = dst; + + const uint8_t firstValue = src[0]; + const uint8_t lastValue = src[(width - 1) * inputStep]; + + alphaSum += firstValue * lobes.left; + + const uint8_t *initEnd = src + (boxSize - lobes.left) * inputStep; + while (right < initEnd) { + alphaSum += *right; + right += inputStep; + } + + const uint8_t *leftEnd = src + boxSize * inputStep; + while (right < leftEnd) { + *out = (alphaSum * reciprocal) >> 24; + alphaSum += *right - firstValue; + right += inputStep; + out += outputStep; + } + + const uint8_t *centerEnd = src + width * inputStep; + while (right < centerEnd) { + *out = (alphaSum * reciprocal) >> 24; + alphaSum += *right - *left; + left += inputStep; + right += inputStep; + out += outputStep; + } + + const uint8_t *rightEnd = dst + width * outputStep; + while (out < rightEnd) { + *out = (alphaSum * reciprocal) >> 24; + alphaSum += lastValue - *left; + left += inputStep; + out += outputStep; + } +} + +/** + * Blur the alpha channel of a given image + * + * @param image The input image + * @param radius The blur radius + * @param rect Specifies what part of the image to blur. If nothing is provided, then + * the whole alpha channel of the input image will be blurred + **/ +static inline void boxBlurAlpha(QImage &image, int radius, const QRect &rect = {}) +{ + if (radius < 2) { + return; + } + + const QVector lobes = computeLobes(radius); + + const QRect blurRect = rect.isNull() ? image.rect() : rect; + + const int alphaOffset = QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3; + const int width = blurRect.width(); + const int height = blurRect.height(); + const int rowStride = image.bytesPerLine(); + const int pixelStride = image.depth() >> 3; + + const int bufferStride = qMax(width, height) * pixelStride; + QScopedPointer > buf(new uint8_t[2 * bufferStride]); + uint8_t *buf1 = buf.data(); + uint8_t *buf2 = buf1 + bufferStride; + + // Blur the image in horizontal direction. + for (int i = 0; i < height; ++i) { + uint8_t *row = image.scanLine(blurRect.y() + i) + blurRect.x() * pixelStride + alphaOffset; + boxBlurRowAlpha(row, buf1, width, pixelStride, rowStride, lobes[0], false, false); + boxBlurRowAlpha(buf1, buf2, width, pixelStride, rowStride, lobes[1], false, false); + boxBlurRowAlpha(buf2, row, width, pixelStride, rowStride, lobes[2], false, false); + } + + // Blur the image in vertical direction. + for (int i = 0; i < width; ++i) { + uint8_t *column = image.scanLine(blurRect.y()) + (blurRect.x() + i) * pixelStride + alphaOffset; + boxBlurRowAlpha(column, buf1, height, pixelStride, rowStride, lobes[0], true, false); + boxBlurRowAlpha(buf1, buf2, height, pixelStride, rowStride, lobes[1], false, false); + boxBlurRowAlpha(buf2, column, height, pixelStride, rowStride, lobes[2], false, true); + } +} + +static inline void mirrorTopLeftQuadrant(QImage &image) +{ + const int width = image.width(); + const int height = image.height(); + + const int centerX = qCeil(width * 0.5); + const int centerY = qCeil(height * 0.5); + + const int alphaOffset = QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3; + const int stride = image.depth() >> 3; + + for (int y = 0; y < centerY; ++y) { + uint8_t *in = image.scanLine(y) + alphaOffset; + uint8_t *out = in + (width - 1) * stride; + + for (int x = 0; x < centerX; ++x, in += stride, out -= stride) { + *out = *in; + } + } + + for (int y = 0; y < centerY; ++y) { + const uint8_t *in = image.scanLine(y) + alphaOffset; + uint8_t *out = image.scanLine(width - y - 1) + alphaOffset; + + for (int x = 0; x < width; ++x, in += stride, out += stride) { + *out = *in; + } + } +} + +static void renderShadow(QPainter *painter, const QRect &rect, qreal borderRadius, const QPoint &offset, int radius, const QColor &color) +{ + const QSize inflation = calculateBlurExtent(radius); + const QSize size = rect.size() + 2 * inflation; + +#if BREEZE_COMMON_USE_KDE4 + const qreal dpr = 1.0; +#else + const qreal dpr = painter->device()->devicePixelRatioF(); +#endif + + QImage shadow(size * dpr, QImage::Format_ARGB32_Premultiplied); +#if !BREEZE_COMMON_USE_KDE4 + shadow.setDevicePixelRatio(dpr); +#endif + shadow.fill(Qt::transparent); + + QPainter shadowPainter; + + // Draw the box. + QRect boxRect(QPoint(0, 0), rect.size()); + boxRect.moveCenter(shadow.rect().center()); + + shadowPainter.begin(&shadow); + shadowPainter.setRenderHint(QPainter::Antialiasing); + shadowPainter.setPen(Qt::NoPen); + shadowPainter.setBrush(Qt::black); + if (borderRadius > 0.0) { + shadowPainter.drawRoundedRect(boxRect, borderRadius, borderRadius); + } else { + shadowPainter.drawRect(boxRect); + } + shadowPainter.end(); + + const QRect blurRect(0, 0, qCeil(shadow.width() * 0.5), qCeil(shadow.height() * 0.5)); + const int scaledRadius = qRound(radius * dpr); + boxBlurAlpha(shadow, scaledRadius, blurRect); + mirrorTopLeftQuadrant(shadow); + + // Give the shadow a tint of the desired color. + shadowPainter.begin(&shadow); + shadowPainter.setCompositionMode(QPainter::CompositionMode_SourceIn); + shadowPainter.fillRect(shadow.rect(), color); + shadowPainter.end(); + + // Actually, present the shadow. + QRect shadowRect = shadow.rect(); + shadowRect.setSize(shadowRect.size() / dpr); + shadowRect.moveCenter(rect.center() + offset); + painter->drawImage(shadowRect, shadow); +} + +void BoxShadowRenderer::setBoxSize(const QSize &size) +{ + m_boxSize = size; +} + +void BoxShadowRenderer::setBorderRadius(qreal radius) +{ + m_borderRadius = radius; +} + +void BoxShadowRenderer::setDevicePixelRatio(qreal dpr) +{ + m_dpr = dpr; +} + +void BoxShadowRenderer::addShadow(const QPoint &offset, int radius, const QColor &color) +{ + Shadow shadow = {}; + shadow.offset = offset; + shadow.radius = radius; + shadow.color = color; + m_shadows.append(shadow); +} + +QImage BoxShadowRenderer::render() const +{ + if (m_shadows.isEmpty()) { + return {}; + } + + QSize canvasSize; + for (const Shadow &shadow : qAsConst(m_shadows)) { + canvasSize = canvasSize.expandedTo( + minimumRequiredCanvasSize(m_boxSize, shadow.radius, shadow.offset)); + } + + QImage canvas(canvasSize * m_dpr, QImage::Format_ARGB32_Premultiplied); +#if !BREEZE_COMMON_USE_KDE4 + canvas.setDevicePixelRatio(m_dpr); +#endif + canvas.fill(Qt::transparent); + + QRect boxRect(QPoint(0, 0), m_boxSize); + boxRect.moveCenter(QRect(QPoint(0, 0), canvasSize).center()); + + QPainter painter(&canvas); + for (const Shadow &shadow : qAsConst(m_shadows)) { + renderShadow(&painter, boxRect, m_borderRadius, shadow.offset, shadow.radius, shadow.color); + } + painter.end(); + + return canvas; +} + +QSize BoxShadowRenderer::minimumRequiredBoxSize(int radius) +{ + const QSize blurExtent = calculateBlurExtent(radius); + return 2 * blurExtent + QSize(1, 1); +} + +QSize BoxShadowRenderer::minimumRequiredCanvasSize(const QSize &boxSize, int radius, const QPoint &offset) +{ + return boxSize + 2 * calculateBlurExtent(radius) + QSize(qAbs(offset.x()), qAbs(offset.y())); +} + +} // namespace Breeze