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