File indexing completed on 2024-04-21 05:26:14

0001 /*
0002  * SPDX-FileCopyrightText: 2018 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
0003  *
0004  * The box blur implementation is based on AlphaBoxBlur from Firefox.
0005  *
0006  * SPDX-License-Identifier: GPL-2.0-or-later
0007  *
0008  * This program is distributed in the hope that it will be useful,
0009  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0010  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0011  * GNU General Public License for more details.
0012  *
0013  * You should have received a copy of the GNU General Public License
0014  * along with this program; if not, write to the Free Software
0015  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
0016  */
0017 
0018 // own
0019 #include "breezeboxshadowrenderer.h"
0020 
0021 // Qt
0022 #include <QPainter>
0023 #include <QtMath>
0024 
0025 namespace Breeze
0026 {
0027 static inline int calculateBlurRadius(qreal stdDev)
0028 {
0029     // See https://www.w3.org/TR/SVG11/filters.html#feGaussianBlurElement
0030     const qreal gaussianScaleFactor = (3.0 * qSqrt(2.0 * M_PI) / 4.0) * 1.5;
0031     return qMax(2, qFloor(stdDev * gaussianScaleFactor + 0.5));
0032 }
0033 
0034 static inline qreal calculateBlurStdDev(int radius)
0035 {
0036     // See https://www.w3.org/TR/css-backgrounds-3/#shadow-blur
0037     return radius * 0.5;
0038 }
0039 
0040 static inline QSize calculateBlurExtent(int radius)
0041 {
0042     const int blurRadius = calculateBlurRadius(calculateBlurStdDev(radius));
0043     return QSize(blurRadius, blurRadius);
0044 }
0045 
0046 struct BoxLobes {
0047     int left; ///< how many pixels sample to the left
0048     int right; ///< how many pixels sample to the right
0049 };
0050 
0051 /**
0052  * Compute box filter parameters.
0053  *
0054  * @param radius The blur radius.
0055  * @returns Parameters for three box filters.
0056  **/
0057 static QVector<BoxLobes> computeLobes(int radius)
0058 {
0059     const int blurRadius = calculateBlurRadius(calculateBlurStdDev(radius));
0060     const int z = blurRadius / 3;
0061 
0062     int major;
0063     int minor;
0064     int final;
0065 
0066     switch (blurRadius % 3) {
0067     case 0:
0068         major = z;
0069         minor = z;
0070         final = z;
0071         break;
0072 
0073     case 1:
0074         major = z + 1;
0075         minor = z;
0076         final = z;
0077         break;
0078 
0079     case 2:
0080         major = z + 1;
0081         minor = z;
0082         final = z + 1;
0083         break;
0084 
0085     default:
0086         Q_UNREACHABLE();
0087     }
0088 
0089     Q_ASSERT(major + minor + final == blurRadius);
0090 
0091     return {{major, minor}, {minor, major}, {final, final}};
0092 }
0093 
0094 /**
0095  * Process a row with a box filter.
0096  *
0097  * @param src The start of the row.
0098  * @param dst The destination.
0099  * @param width The width of the row, in pixels.
0100  * @param horizontalStride The number of bytes from one alpha value to the
0101  *    next alpha value.
0102  * @param verticalStride The number of bytes from one row to the next row.
0103  * @param lobes Params of the box filter.
0104  * @param transposeInput Whether the input is transposed.
0105  * @param transposeOutput Whether the output should be transposed.
0106  **/
0107 static inline void boxBlurRowAlpha(const uint8_t *src,
0108                                    uint8_t *dst,
0109                                    int width,
0110                                    int horizontalStride,
0111                                    int verticalStride,
0112                                    const BoxLobes &lobes,
0113                                    bool transposeInput,
0114                                    bool transposeOutput)
0115 {
0116     const int inputStep = transposeInput ? verticalStride : horizontalStride;
0117     const int outputStep = transposeOutput ? verticalStride : horizontalStride;
0118 
0119     const int boxSize = lobes.left + 1 + lobes.right;
0120     const int reciprocal = (1 << 24) / boxSize;
0121 
0122     uint32_t alphaSum = (boxSize + 1) / 2;
0123 
0124     const uint8_t *left = src;
0125     const uint8_t *right = src;
0126     uint8_t *out = dst;
0127 
0128     const uint8_t firstValue = src[0];
0129     const uint8_t lastValue = src[(width - 1) * inputStep];
0130 
0131     alphaSum += firstValue * lobes.left;
0132 
0133     const uint8_t *initEnd = src + (boxSize - lobes.left) * inputStep;
0134     while (right < initEnd) {
0135         alphaSum += *right;
0136         right += inputStep;
0137     }
0138 
0139     const uint8_t *leftEnd = src + boxSize * inputStep;
0140     while (right < leftEnd) {
0141         *out = (alphaSum * reciprocal) >> 24;
0142         alphaSum += *right - firstValue;
0143         right += inputStep;
0144         out += outputStep;
0145     }
0146 
0147     const uint8_t *centerEnd = src + width * inputStep;
0148     while (right < centerEnd) {
0149         *out = (alphaSum * reciprocal) >> 24;
0150         alphaSum += *right - *left;
0151         left += inputStep;
0152         right += inputStep;
0153         out += outputStep;
0154     }
0155 
0156     const uint8_t *rightEnd = dst + width * outputStep;
0157     while (out < rightEnd) {
0158         *out = (alphaSum * reciprocal) >> 24;
0159         alphaSum += lastValue - *left;
0160         left += inputStep;
0161         out += outputStep;
0162     }
0163 }
0164 
0165 /**
0166  * Blur the alpha channel of a given image.
0167  *
0168  * @param image The input image.
0169  * @param radius The blur radius.
0170  * @param rect Specifies what part of the image to blur. If nothing is provided, then
0171  *    the whole alpha channel of the input image will be blurred.
0172  **/
0173 static inline void boxBlurAlpha(QImage &image, int radius, const QRect &rect = {})
0174 {
0175     if (radius < 2) {
0176         return;
0177     }
0178 
0179     const QVector<BoxLobes> lobes = computeLobes(radius);
0180 
0181     const QRect blurRect = rect.isNull() ? image.rect() : rect;
0182 
0183     const int alphaOffset = QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3;
0184     const int width = blurRect.width();
0185     const int height = blurRect.height();
0186     const int rowStride = image.bytesPerLine();
0187     const int pixelStride = image.depth() >> 3;
0188 
0189     const int bufferStride = qMax(width, height) * pixelStride;
0190     QScopedPointer<uint8_t, QScopedPointerArrayDeleter<uint8_t>> buf(new uint8_t[2 * bufferStride]);
0191     uint8_t *buf1 = buf.data();
0192     uint8_t *buf2 = buf1 + bufferStride;
0193 
0194     // Blur the image in horizontal direction.
0195     for (int i = 0; i < height; ++i) {
0196         uint8_t *row = image.scanLine(blurRect.y() + i) + blurRect.x() * pixelStride + alphaOffset;
0197         boxBlurRowAlpha(row, buf1, width, pixelStride, rowStride, lobes[0], false, false);
0198         boxBlurRowAlpha(buf1, buf2, width, pixelStride, rowStride, lobes[1], false, false);
0199         boxBlurRowAlpha(buf2, row, width, pixelStride, rowStride, lobes[2], false, false);
0200     }
0201 
0202     // Blur the image in vertical direction.
0203     for (int i = 0; i < width; ++i) {
0204         uint8_t *column = image.scanLine(blurRect.y()) + (blurRect.x() + i) * pixelStride + alphaOffset;
0205         boxBlurRowAlpha(column, buf1, height, pixelStride, rowStride, lobes[0], true, false);
0206         boxBlurRowAlpha(buf1, buf2, height, pixelStride, rowStride, lobes[1], false, false);
0207         boxBlurRowAlpha(buf2, column, height, pixelStride, rowStride, lobes[2], false, true);
0208     }
0209 }
0210 
0211 static inline void mirrorTopLeftQuadrant(QImage &image)
0212 {
0213     const int width = image.width();
0214     const int height = image.height();
0215 
0216     const int centerX = qCeil(width * 0.5);
0217     const int centerY = qCeil(height * 0.5);
0218 
0219     const int alphaOffset = QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3;
0220     const int stride = image.depth() >> 3;
0221 
0222     for (int y = 0; y < centerY; ++y) {
0223         uint8_t *in = image.scanLine(y) + alphaOffset;
0224         uint8_t *out = in + (width - 1) * stride;
0225 
0226         for (int x = 0; x < centerX; ++x, in += stride, out -= stride) {
0227             *out = *in;
0228         }
0229     }
0230 
0231     for (int y = 0; y < centerY; ++y) {
0232         const uint8_t *in = image.scanLine(y) + alphaOffset;
0233         uint8_t *out = image.scanLine(width - y - 1) + alphaOffset;
0234 
0235         for (int x = 0; x < width; ++x, in += stride, out += stride) {
0236             *out = *in;
0237         }
0238     }
0239 }
0240 
0241 static void renderShadow(QPainter *painter, const QRect &rect, qreal borderRadius, const QPoint &offset, int radius, const QColor &color)
0242 {
0243     const QSize inflation = calculateBlurExtent(radius);
0244     const QSize size = rect.size() + 2 * inflation;
0245 
0246     const qreal dpr = painter->device()->devicePixelRatioF();
0247 
0248     QImage shadow(size * dpr, QImage::Format_ARGB32_Premultiplied);
0249     shadow.setDevicePixelRatio(dpr);
0250     shadow.fill(Qt::transparent);
0251 
0252     QRect boxRect(QPoint(0, 0), rect.size());
0253     boxRect.moveCenter(QRect(QPoint(0, 0), size).center());
0254 
0255     const qreal xRadius = 2.0 * borderRadius / boxRect.width();
0256     const qreal yRadius = 2.0 * borderRadius / boxRect.height();
0257 
0258     QPainter shadowPainter;
0259     shadowPainter.begin(&shadow);
0260     shadowPainter.setRenderHint(QPainter::Antialiasing);
0261     shadowPainter.setPen(Qt::NoPen);
0262     shadowPainter.setBrush(Qt::black);
0263     shadowPainter.drawRoundedRect(boxRect, xRadius, yRadius);
0264     shadowPainter.end();
0265 
0266     // Because the shadow texture is symmetrical, that's enough to blur
0267     // only the top-left quadrant and then mirror it.
0268     const QRect blurRect(0, 0, qCeil(shadow.width() * 0.5), qCeil(shadow.height() * 0.5));
0269     const int scaledRadius = qRound(radius * dpr);
0270     boxBlurAlpha(shadow, scaledRadius, blurRect);
0271     mirrorTopLeftQuadrant(shadow);
0272 
0273     // Give the shadow a tint of the desired color.
0274     shadowPainter.begin(&shadow);
0275     shadowPainter.setCompositionMode(QPainter::CompositionMode_SourceIn);
0276     shadowPainter.fillRect(shadow.rect(), color);
0277     shadowPainter.end();
0278 
0279     // Actually, present the shadow.
0280     QRect shadowRect = shadow.rect();
0281     shadowRect.setSize(shadowRect.size() / dpr);
0282     shadowRect.moveCenter(rect.center() + offset);
0283     painter->drawImage(shadowRect, shadow);
0284 }
0285 
0286 void BoxShadowRenderer::setBoxSize(const QSize &size)
0287 {
0288     m_boxSize = size;
0289 }
0290 
0291 void BoxShadowRenderer::setBorderRadius(qreal radius)
0292 {
0293     m_borderRadius = radius;
0294 }
0295 
0296 void BoxShadowRenderer::addShadow(const QPoint &offset, int radius, const QColor &color)
0297 {
0298     Shadow shadow = {};
0299     shadow.offset = offset;
0300     shadow.radius = radius;
0301     shadow.color = color;
0302     m_shadows.append(shadow);
0303 }
0304 
0305 QImage BoxShadowRenderer::render() const
0306 {
0307     if (m_shadows.isEmpty()) {
0308         return {};
0309     }
0310 
0311     QSize canvasSize;
0312     for (const Shadow &shadow : std::as_const(m_shadows)) {
0313         canvasSize = canvasSize.expandedTo(calculateMinimumShadowTextureSize(m_boxSize, shadow.radius, shadow.offset));
0314     }
0315 
0316     QImage canvas(canvasSize, QImage::Format_ARGB32_Premultiplied);
0317     canvas.fill(Qt::transparent);
0318 
0319     QRect boxRect(QPoint(0, 0), m_boxSize);
0320     boxRect.moveCenter(QRect(QPoint(0, 0), canvasSize).center());
0321 
0322     QPainter painter(&canvas);
0323     for (const Shadow &shadow : std::as_const(m_shadows)) {
0324         renderShadow(&painter, boxRect, m_borderRadius, shadow.offset, shadow.radius, shadow.color);
0325     }
0326     painter.end();
0327 
0328     return canvas;
0329 }
0330 
0331 QSize BoxShadowRenderer::calculateMinimumBoxSize(int radius)
0332 {
0333     const QSize blurExtent = calculateBlurExtent(radius);
0334     return 2 * blurExtent + QSize(1, 1);
0335 }
0336 
0337 QSize BoxShadowRenderer::calculateMinimumShadowTextureSize(const QSize &boxSize, int radius, const QPoint &offset)
0338 {
0339     return boxSize + 2 * calculateBlurExtent(radius) + QSize(qAbs(offset.x()), qAbs(offset.y()));
0340 }
0341 
0342 } // namespace Breeze