File indexing completed on 2024-12-22 04:31:08
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 }