MauiKit Controls

windows/shadowhelper/boxshadowrenderer.cpp
1/*
2 * Copyright (C) 2018 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
3 *
4 * The box blur implementation is based on AlphaBoxBlur from Firefox.
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20
21// own
22#include "boxshadowrenderer.h"
23
24// Qt
25#include <QPainter>
26#include <QtMath>
27
28static inline int calculateBlurRadius(qreal stdDev)
29{
30 // See https://www.w3.org/TR/SVG11/filters.html#feGaussianBlurElement
31 const qreal gaussianScaleFactor = (3.0 * qSqrt(2.0 * M_PI) / 4.0) * 1.5;
32 return qMax(2, qFloor(stdDev * gaussianScaleFactor + 0.5));
33}
34
35static inline qreal calculateBlurStdDev(int radius)
36{
37 // See https://www.w3.org/TR/css-backgrounds-3/#shadow-blur
38 return radius * 0.5;
39}
40
41static inline QSize calculateBlurExtent(int radius)
42{
43 const int blurRadius = calculateBlurRadius(calculateBlurStdDev(radius));
44 return QSize(blurRadius, blurRadius);
45}
46
47struct BoxLobes
48{
49 int left; ///< how many pixels sample to the left
50 int right; ///< how many pixels sample to the right
51};
52
53/**
54 * Compute box filter parameters.
55 *
56 * @param radius The blur radius.
57 * @returns Parameters for three box filters.
58 **/
59static QVector<BoxLobes> computeLobes(int radius)
60{
61 const int blurRadius = calculateBlurRadius(calculateBlurStdDev(radius));
62 const int z = blurRadius / 3;
63
64 int major;
65 int minor;
66 int final;
67
68 switch (blurRadius % 3) {
69 case 0:
70 major = z;
71 minor = z;
72 final = z;
73 break;
74
75 case 1:
76 major = z + 1;
77 minor = z;
78 final = z;
79 break;
80
81 case 2:
82 major = z + 1;
83 minor = z;
84 final = z + 1;
85 break;
86
87 default:
88 Q_UNREACHABLE();
89 }
90
91 Q_ASSERT(major + minor + final == blurRadius);
92
93 return {
94 {major, minor},
95 {minor, major},
96 {final, final}
97 };
98}
99
100/**
101 * Process a row with a box filter.
102 *
103 * @param src The start of the row.
104 * @param dst The destination.
105 * @param width The width of the row, in pixels.
106 * @param horizontalStride The number of bytes from one alpha value to the
107 * next alpha value.
108 * @param verticalStride The number of bytes from one row to the next row.
109 * @param lobes Params of the box filter.
110 * @param transposeInput Whether the input is transposed.
111 * @param transposeOutput Whether the output should be transposed.
112 **/
113static inline void boxBlurRowAlpha(const uint8_t *src, uint8_t *dst, int width, int horizontalStride,
114 int verticalStride, const BoxLobes &lobes, bool transposeInput,
115 bool transposeOutput)
116{
117 const int inputStep = transposeInput ? verticalStride : horizontalStride;
118 const int outputStep = transposeOutput ? verticalStride : horizontalStride;
119
120 const int boxSize = lobes.left + 1 + lobes.right;
121 const int reciprocal = (1 << 24) / boxSize;
122
123 uint32_t alphaSum = (boxSize + 1) / 2;
124
125 const uint8_t *left = src;
126 const uint8_t *right = src;
127 uint8_t *out = dst;
128
129 const uint8_t firstValue = src[0];
130 const uint8_t lastValue = src[(width - 1) * inputStep];
131
132 alphaSum += firstValue * lobes.left;
133
134 const uint8_t *initEnd = src + (boxSize - lobes.left) * inputStep;
135 while (right < initEnd) {
136 alphaSum += *right;
137 right += inputStep;
138 }
139
140 const uint8_t *leftEnd = src + boxSize * inputStep;
141 while (right < leftEnd) {
142 *out = (alphaSum * reciprocal) >> 24;
143 alphaSum += *right - firstValue;
144 right += inputStep;
145 out += outputStep;
146 }
147
148 const uint8_t *centerEnd = src + width * inputStep;
149 while (right < centerEnd) {
150 *out = (alphaSum * reciprocal) >> 24;
151 alphaSum += *right - *left;
152 left += inputStep;
153 right += inputStep;
154 out += outputStep;
155 }
156
157 const uint8_t *rightEnd = dst + width * outputStep;
158 while (out < rightEnd) {
159 *out = (alphaSum * reciprocal) >> 24;
160 alphaSum += lastValue - *left;
161 left += inputStep;
162 out += outputStep;
163 }
164}
165
166/**
167 * Blur the alpha channel of a given image.
168 *
169 * @param image The input image.
170 * @param radius The blur radius.
171 * @param rect Specifies what part of the image to blur. If nothing is provided, then
172 * the whole alpha channel of the input image will be blurred.
173 **/
174static inline void boxBlurAlpha(QImage &image, int radius, const QRect &rect = {})
175{
176 if (radius < 2) {
177 return;
178 }
179
180 const QVector<BoxLobes> lobes = computeLobes(radius);
181
182 const QRect blurRect = rect.isNull() ? image.rect() : rect;
183
184 const int alphaOffset = QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3;
185 const int width = blurRect.width();
186 const int height = blurRect.height();
187 const int rowStride = image.bytesPerLine();
188 const int pixelStride = image.depth() >> 3;
189
190 const int bufferStride = qMax(width, height) * pixelStride;
191 QScopedPointer<uint8_t, QScopedPointerArrayDeleter<uint8_t> > buf(new uint8_t[2 * bufferStride]);
192 uint8_t *buf1 = buf.data();
193 uint8_t *buf2 = buf1 + bufferStride;
194
195 // Blur the image in horizontal direction.
196 for (int i = 0; i < height; ++i) {
197 uint8_t *row = image.scanLine(blurRect.y() + i) + blurRect.x() * pixelStride + alphaOffset;
198 boxBlurRowAlpha(row, buf1, width, pixelStride, rowStride, lobes[0], false, false);
199 boxBlurRowAlpha(buf1, buf2, width, pixelStride, rowStride, lobes[1], false, false);
200 boxBlurRowAlpha(buf2, row, width, pixelStride, rowStride, lobes[2], false, false);
201 }
202
203 // Blur the image in vertical direction.
204 for (int i = 0; i < width; ++i) {
205 uint8_t *column = image.scanLine(blurRect.y()) + (blurRect.x() + i) * pixelStride + alphaOffset;
206 boxBlurRowAlpha(column, buf1, height, pixelStride, rowStride, lobes[0], true, false);
207 boxBlurRowAlpha(buf1, buf2, height, pixelStride, rowStride, lobes[1], false, false);
208 boxBlurRowAlpha(buf2, column, height, pixelStride, rowStride, lobes[2], false, true);
209 }
210}
211
212static inline void mirrorTopLeftQuadrant(QImage &image)
213{
214 const int width = image.width();
215 const int height = image.height();
216
217 const int centerX = qCeil(width * 0.5);
218 const int centerY = qCeil(height * 0.5);
219
220 const int alphaOffset = QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3;
221 const int stride = image.depth() >> 3;
222
223 for (int y = 0; y < centerY; ++y) {
224 uint8_t *in = image.scanLine(y) + alphaOffset;
225 uint8_t *out = in + (width - 1) * stride;
226
227 for (int x = 0; x < centerX; ++x, in += stride, out -= stride) {
228 *out = *in;
229 }
230 }
231
232 for (int y = 0; y < centerY; ++y) {
233 const uint8_t *in = image.scanLine(y) + alphaOffset;
234 uint8_t *out = image.scanLine(width - y - 1) + alphaOffset;
235
236 for (int x = 0; x < width; ++x, in += stride, out += stride) {
237 *out = *in;
238 }
239 }
240}
241
242static void renderShadow(QPainter *painter, const QRect &rect, qreal borderRadius, const QPoint &offset, int radius, const QColor &color)
243{
244 const QSize inflation = calculateBlurExtent(radius);
245 const QSize size = rect.size() + 2 * inflation;
246
247 const qreal dpr = painter->device()->devicePixelRatioF();
248
250 shadow.setDevicePixelRatio(dpr);
251 shadow.fill(Qt::transparent);
252
253 QRect boxRect(QPoint(0, 0), rect.size());
254 boxRect.moveCenter(QRect(QPoint(0, 0), size).center());
255
256 const qreal xRadius = 2.0 * borderRadius / boxRect.width();
257 const qreal yRadius = 2.0 * borderRadius / boxRect.height();
258
259 QPainter shadowPainter;
260 shadowPainter.begin(&shadow);
262 shadowPainter.setPen(Qt::NoPen);
263 shadowPainter.setBrush(Qt::black);
264 shadowPainter.drawRoundedRect(boxRect, xRadius, yRadius);
265 shadowPainter.end();
266
267 // Because the shadow texture is symmetrical, that's enough to blur
268 // only the top-left quadrant and then mirror it.
269 const QRect blurRect(0, 0, qCeil(shadow.width() * 0.5), qCeil(shadow.height() * 0.5));
270 const int scaledRadius = qRound(radius * dpr);
271 boxBlurAlpha(shadow, scaledRadius, blurRect);
272 mirrorTopLeftQuadrant(shadow);
273
274 // Give the shadow a tint of the desired color.
275 shadowPainter.begin(&shadow);
277 shadowPainter.fillRect(shadow.rect(), color);
278 shadowPainter.end();
279
280 // Actually, present the shadow.
281 QRect shadowRect = shadow.rect();
282 shadowRect.setSize(shadowRect.size() / dpr);
283 shadowRect.moveCenter(rect.center() + offset);
284 painter->drawImage(shadowRect, shadow);
285}
286
287void BoxShadowRenderer::setBoxSize(const QSize &size)
288{
289 m_boxSize = size;
290}
291
292void BoxShadowRenderer::setBorderRadius(qreal radius)
293{
294 m_borderRadius = radius;
295}
296
297void BoxShadowRenderer::setDevicePixelRatio(qreal dpr)
298{
299 m_dpr = dpr;
300}
301
302void BoxShadowRenderer::addShadow(const QPoint &offset, int radius, const QColor &color)
303{
304 Shadow shadow = {};
305 shadow.offset = offset;
306 shadow.radius = radius;
307 shadow.color = color;
308 m_shadows.append(shadow);
309}
310
311QImage BoxShadowRenderer::render() const
312{
313 if (m_shadows.isEmpty()) {
314 return {};
315 }
316
317 QSize canvasSize;
318 for (const Shadow &shadow : qAsConst(m_shadows)) {
319 canvasSize = canvasSize.expandedTo(
320 calculateMinimumShadowTextureSize(m_boxSize, shadow.radius, shadow.offset));
321 }
322
323 QImage canvas(canvasSize * m_dpr, QImage::Format_ARGB32_Premultiplied);
324 canvas.setDevicePixelRatio(m_dpr);
325 canvas.fill(Qt::transparent);
326
327 QRect boxRect(QPoint(0, 0), m_boxSize);
328 boxRect.moveCenter(QRect(QPoint(0, 0), canvasSize).center());
329
330 QPainter painter(&canvas);
331 for (const Shadow &shadow : qAsConst(m_shadows)) {
332 renderShadow(&painter, boxRect, m_borderRadius, shadow.offset, shadow.radius, shadow.color);
333 }
334 painter.end();
335
336 return canvas;
337}
338
339QSize BoxShadowRenderer::calculateMinimumBoxSize(int radius)
340{
341 const QSize blurExtent = calculateBlurExtent(radius);
342 return 2 * blurExtent + QSize(1, 1);
343}
344
345QSize BoxShadowRenderer::calculateMinimumShadowTextureSize(const QSize &boxSize, int radius, const QPoint &offset)
346{
347 return boxSize + 2 * calculateBlurExtent(radius) + QSize(qAbs(offset.x()), qAbs(offset.y()));
348}
Format_ARGB32_Premultiplied
qsizetype bytesPerLine() const const
int depth() const const
int height() const const
QRect rect() const const
uchar * scanLine(int i)
int width() const const
qreal devicePixelRatioF() const const
CompositionMode_SourceIn
bool begin(QPaintDevice *device)
QPaintDevice * device() const const
void drawImage(const QPoint &point, const QImage &image)
void drawRoundedRect(const QRect &rect, qreal xRadius, qreal yRadius, Qt::SizeMode mode)
bool end()
void fillRect(const QRect &rectangle, QGradient::Preset preset)
void setBrush(Qt::BrushStyle style)
void setCompositionMode(CompositionMode mode)
void setPen(Qt::PenStyle style)
void setRenderHint(RenderHint hint, bool on)
int x() const const
int y() const const
QPoint center() const const
int height() const const
bool isNull() const const
void moveCenter(const QPoint &position)
void setSize(const QSize &size)
QSize size() const const
int width() const const
int x() const const
int y() const const
QSize expandedTo(const QSize &otherSize) const const
transparent
QTextStream & center(QTextStream &stream)
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 17:04:30 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.