diff --git a/libbreezecommon/breezeboxshadowrenderer.cpp b/libbreezecommon/breezeboxshadowrenderer.cpp index b551fd25..8203b5b3 100644 --- a/libbreezecommon/breezeboxshadowrenderer.cpp +++ b/libbreezecommon/breezeboxshadowrenderer.cpp @@ -1,380 +1,385 @@ /* * 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) any later version. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ // own #include "breezeboxshadowrenderer.h" // auto-generated #include "config-breezecommon.h" // Qt #include + +#ifdef BREEZE_COMMON_USE_KDE4 +#include +#else #include +#endif 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; return qMax(2, qFloor(stdDev * gaussianScaleFactor + 0.5)); } static inline qreal calculateBlurStdDev(int radius) { // https://www.w3.org/TR/css-backgrounds-3/#shadow-blur says that 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, // but we had been using a slightly different (non-standard compliant) routine to // derive the standard deviation before, so in order to not break existing shadow // params, we're not following the standard. return radius * 0.43; // TODO: Multiply by 0.5 instead. } static inline QSize calculateBlurExtent(int radius) { const int blurRadius = calculateBlurRadius(calculateBlurStdDev(radius)); return QSize(blurRadius, blurRadius); } struct BoxLobes { int left; ///< how many pixels sample to the left int right; ///< how many pixels sample to the right }; /** * Compute box filter parameters. * * @param radius The blur radius. * @returns Parameters for three box filters. **/ 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: #if !BREEZE_COMMON_USE_KDE4 Q_UNREACHABLE(); #endif break; } Q_ASSERT(major + minor + final == blurRadius); return { {major, minor}, {minor, major}, {final, final} }; } /** * Process a row with a box filter. * * @param src The start of the row. * @param dst The destination. * @param width The width of the row, in pixels. * @param horizontalStride The number of bytes from one alpha value to the * next alpha value. * @param verticalStride The number of bytes from one row to the next row. * @param lobes Params of the box filter. * @param transposeInput Whether the input is transposed. * @param transposeOutput Whether the output should be transposed. **/ 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); QRect boxRect(QPoint(0, 0), rect.size()); boxRect.moveCenter(QRect(QPoint(0, 0), size).center()); const qreal xRadius = 2.0 * borderRadius / boxRect.width(); const qreal yRadius = 2.0 * borderRadius / boxRect.height(); QPainter shadowPainter; shadowPainter.begin(&shadow); shadowPainter.setRenderHint(QPainter::Antialiasing); shadowPainter.setPen(Qt::NoPen); shadowPainter.setBrush(Qt::black); shadowPainter.drawRoundedRect(boxRect, xRadius, yRadius); shadowPainter.end(); // Because the shadow texture is symmetrical, that's enough to blur // only the top-left quadrant and then mirror it. 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; #if BREEZE_COMMON_USE_KDE4 foreach (const Shadow &shadow, m_shadows) { #else for (const Shadow &shadow : qAsConst(m_shadows)) { #endif canvasSize = canvasSize.expandedTo( calculateMinimumShadowTextureSize(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); #if BREEZE_COMMON_USE_KDE4 foreach (const Shadow &shadow, m_shadows) { #else for (const Shadow &shadow : qAsConst(m_shadows)) { #endif renderShadow(&painter, boxRect, m_borderRadius, shadow.offset, shadow.radius, shadow.color); } painter.end(); return canvas; } QSize BoxShadowRenderer::calculateMinimumBoxSize(int radius) { const QSize blurExtent = calculateBlurExtent(radius); return 2 * blurExtent + QSize(1, 1); } QSize BoxShadowRenderer::calculateMinimumShadowTextureSize(const QSize &boxSize, int radius, const QPoint &offset) { return boxSize + 2 * calculateBlurExtent(radius) + QSize(qAbs(offset.x()), qAbs(offset.y())); } } // namespace Breeze diff --git a/libbreezecommon/breezeboxshadowrenderer.h b/libbreezecommon/breezeboxshadowrenderer.h index 4c271bee..f363df2a 100644 --- a/libbreezecommon/breezeboxshadowrenderer.h +++ b/libbreezecommon/breezeboxshadowrenderer.h @@ -1,104 +1,105 @@ /* * 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) any later version. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once // own #include "breezecommon_export.h" // Qt +#include #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, in pixels. **/ 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 the minimum size of the box. * * This helper computes the minimum size of the box so the shadow behind it has * full its strength. * * @param radius The blur radius of the shadow. **/ static QSize calculateMinimumBoxSize(int radius); /** * Calculate the minimum size of the shadow texture. * * This helper computes the minimum size of the resulting texture so the shadow * is not clipped. * * @param boxSize The size of the box. * @param radius The blur radius. * @param offset The offset of the shadow. **/ static QSize calculateMinimumShadowTextureSize(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