File indexing completed on 2024-12-22 04:31:07

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