Changeset View
Changeset View
Standalone View
Standalone View
libbreezecommon/breezeboxshadowrenderer.cpp
- This file was added.
1 | /* | ||||
---|---|---|---|---|---|
2 | * Copyright (C) 2018 Vlad Zagorodniy <vladzzag@gmail.com> | ||||
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 "breezeboxshadowrenderer.h" | ||||
23 | | ||||
24 | // auto-generated | ||||
25 | #include "config-breezecommon.h" | ||||
26 | | ||||
27 | // Qt | ||||
28 | #include <QPainter> | ||||
29 | #include <QtMath> | ||||
30 | | ||||
31 | namespace Breeze | ||||
32 | { | ||||
33 | | ||||
34 | static inline int calculateBlurRadius(qreal stdDev) | ||||
35 | { | ||||
36 | // See https://www.w3.org/TR/SVG11/filters.html#feGaussianBlurElement | ||||
37 | const qreal gaussianScaleFactor = (3.0 * qSqrt(2.0 * M_PI) / 4.0) * 1.5; | ||||
38 | return qMax(2, qFloor(stdDev * gaussianScaleFactor + 0.5)); | ||||
39 | } | ||||
40 | | ||||
41 | static inline qreal calculateBlurStdDev(int radius) | ||||
42 | { | ||||
43 | // https://www.w3.org/TR/css-backgrounds-3/#shadow-blur says that the resulting | ||||
44 | // shadow must approximate the image that would be generated by applying to the | ||||
45 | // shadow a Gaussian blur with a standard deviation equal to half the blur radius, | ||||
46 | // but we had been using a slightly different (non-standard compliant) routine to | ||||
47 | // derive the standard deviation before, so in order to not break existing shadow | ||||
48 | // params, we're not following the standard. | ||||
49 | return radius * 0.43; // TODO: Multiply by 0.5 instead. | ||||
50 | } | ||||
51 | | ||||
52 | static inline QSize calculateBlurExtent(int radius) | ||||
53 | { | ||||
54 | const int blurRadius = calculateBlurRadius(calculateBlurStdDev(radius)); | ||||
55 | return QSize(blurRadius, blurRadius); | ||||
56 | } | ||||
57 | | ||||
58 | struct BoxLobes | ||||
59 | { | ||||
60 | int left; ///< how many pixels sample to the left | ||||
61 | int right; ///< how many pixels sample to the right | ||||
62 | }; | ||||
63 | | ||||
64 | /** | ||||
65 | * Compute box filter parameters. | ||||
66 | * | ||||
67 | * @param radius The blur radius. | ||||
68 | * @returns Parameters for three box filters. | ||||
69 | **/ | ||||
70 | static QVector<BoxLobes> computeLobes(int radius) | ||||
71 | { | ||||
72 | const int blurRadius = calculateBlurRadius(calculateBlurStdDev(radius)); | ||||
73 | const int z = blurRadius / 3; | ||||
74 | | ||||
75 | int major; | ||||
76 | int minor; | ||||
77 | int final; | ||||
78 | | ||||
79 | switch (blurRadius % 3) { | ||||
80 | case 0: | ||||
81 | major = z; | ||||
82 | minor = z; | ||||
83 | final = z; | ||||
84 | break; | ||||
85 | | ||||
86 | case 1: | ||||
87 | major = z + 1; | ||||
88 | minor = z; | ||||
89 | final = z; | ||||
90 | break; | ||||
91 | | ||||
92 | case 2: | ||||
93 | major = z + 1; | ||||
94 | minor = z; | ||||
95 | final = z + 1; | ||||
96 | break; | ||||
97 | | ||||
98 | default: | ||||
99 | Q_UNREACHABLE(); | ||||
100 | break; | ||||
101 | } | ||||
102 | | ||||
103 | Q_ASSERT(major + minor + final == blurRadius); | ||||
104 | | ||||
105 | return { | ||||
106 | {major, minor}, | ||||
107 | {minor, major}, | ||||
108 | {final, final} | ||||
109 | }; | ||||
110 | } | ||||
111 | | ||||
112 | /** | ||||
113 | * Process a row with a box filter. | ||||
114 | * | ||||
115 | * @param src The start of the row. | ||||
116 | * @param dst The destination. | ||||
117 | * @param width The width of the row, in pixels. | ||||
118 | * @param horizontalStride The number of bytes from one alpha value to the | ||||
119 | * next alpha value. | ||||
120 | * @param verticalStride The number of bytes from one row to the next row. | ||||
121 | * @param lobes Params of the box filter. | ||||
122 | * @param transposeInput Whether the input is transposed. | ||||
123 | * @param transposeOutput Whether the output should be transposed. | ||||
124 | **/ | ||||
125 | static inline void boxBlurRowAlpha(const uint8_t *src, uint8_t *dst, int width, int horizontalStride, | ||||
126 | int verticalStride, const BoxLobes &lobes, bool transposeInput, | ||||
127 | bool transposeOutput) | ||||
128 | { | ||||
129 | const int inputStep = transposeInput ? verticalStride : horizontalStride; | ||||
130 | const int outputStep = transposeOutput ? verticalStride : horizontalStride; | ||||
131 | | ||||
132 | const int boxSize = lobes.left + 1 + lobes.right; | ||||
133 | const int reciprocal = (1 << 24) / boxSize; | ||||
134 | | ||||
135 | uint32_t alphaSum = (boxSize + 1) / 2; | ||||
136 | | ||||
137 | const uint8_t *left = src; | ||||
138 | const uint8_t *right = src; | ||||
139 | uint8_t *out = dst; | ||||
140 | | ||||
141 | const uint8_t firstValue = src[0]; | ||||
142 | const uint8_t lastValue = src[(width - 1) * inputStep]; | ||||
143 | | ||||
144 | alphaSum += firstValue * lobes.left; | ||||
145 | | ||||
146 | const uint8_t *initEnd = src + (boxSize - lobes.left) * inputStep; | ||||
147 | while (right < initEnd) { | ||||
148 | alphaSum += *right; | ||||
149 | right += inputStep; | ||||
150 | } | ||||
151 | | ||||
152 | const uint8_t *leftEnd = src + boxSize * inputStep; | ||||
153 | while (right < leftEnd) { | ||||
154 | *out = (alphaSum * reciprocal) >> 24; | ||||
155 | alphaSum += *right - firstValue; | ||||
156 | right += inputStep; | ||||
157 | out += outputStep; | ||||
158 | } | ||||
159 | | ||||
160 | const uint8_t *centerEnd = src + width * inputStep; | ||||
161 | while (right < centerEnd) { | ||||
162 | *out = (alphaSum * reciprocal) >> 24; | ||||
163 | alphaSum += *right - *left; | ||||
164 | left += inputStep; | ||||
165 | right += inputStep; | ||||
166 | out += outputStep; | ||||
167 | } | ||||
168 | | ||||
169 | const uint8_t *rightEnd = dst + width * outputStep; | ||||
170 | while (out < rightEnd) { | ||||
171 | *out = (alphaSum * reciprocal) >> 24; | ||||
172 | alphaSum += lastValue - *left; | ||||
173 | left += inputStep; | ||||
174 | out += outputStep; | ||||
175 | } | ||||
176 | } | ||||
177 | | ||||
178 | /** | ||||
179 | * Blur the alpha channel of a given image. | ||||
180 | * | ||||
181 | * @param image The input image. | ||||
182 | * @param radius The blur radius. | ||||
183 | * @param rect Specifies what part of the image to blur. If nothing is provided, then | ||||
184 | * the whole alpha channel of the input image will be blurred. | ||||
185 | **/ | ||||
186 | static inline void boxBlurAlpha(QImage &image, int radius, const QRect &rect = {}) | ||||
187 | { | ||||
188 | if (radius < 2) { | ||||
189 | return; | ||||
190 | } | ||||
191 | | ||||
192 | const QVector<BoxLobes> lobes = computeLobes(radius); | ||||
193 | | ||||
194 | const QRect blurRect = rect.isNull() ? image.rect() : rect; | ||||
195 | | ||||
196 | const int alphaOffset = QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3; | ||||
197 | const int width = blurRect.width(); | ||||
198 | const int height = blurRect.height(); | ||||
199 | const int rowStride = image.bytesPerLine(); | ||||
200 | const int pixelStride = image.depth() >> 3; | ||||
201 | | ||||
202 | const int bufferStride = qMax(width, height) * pixelStride; | ||||
203 | QScopedPointer<uint8_t, QScopedPointerArrayDeleter<uint8_t> > buf(new uint8_t[2 * bufferStride]); | ||||
204 | uint8_t *buf1 = buf.data(); | ||||
205 | uint8_t *buf2 = buf1 + bufferStride; | ||||
206 | | ||||
207 | // Blur the image in horizontal direction. | ||||
208 | for (int i = 0; i < height; ++i) { | ||||
209 | uint8_t *row = image.scanLine(blurRect.y() + i) + blurRect.x() * pixelStride + alphaOffset; | ||||
210 | boxBlurRowAlpha(row, buf1, width, pixelStride, rowStride, lobes[0], false, false); | ||||
211 | boxBlurRowAlpha(buf1, buf2, width, pixelStride, rowStride, lobes[1], false, false); | ||||
212 | boxBlurRowAlpha(buf2, row, width, pixelStride, rowStride, lobes[2], false, false); | ||||
213 | } | ||||
214 | | ||||
215 | // Blur the image in vertical direction. | ||||
216 | for (int i = 0; i < width; ++i) { | ||||
217 | uint8_t *column = image.scanLine(blurRect.y()) + (blurRect.x() + i) * pixelStride + alphaOffset; | ||||
218 | boxBlurRowAlpha(column, buf1, height, pixelStride, rowStride, lobes[0], true, false); | ||||
219 | boxBlurRowAlpha(buf1, buf2, height, pixelStride, rowStride, lobes[1], false, false); | ||||
220 | boxBlurRowAlpha(buf2, column, height, pixelStride, rowStride, lobes[2], false, true); | ||||
221 | } | ||||
222 | } | ||||
223 | | ||||
224 | static inline void mirrorTopLeftQuadrant(QImage &image) | ||||
225 | { | ||||
226 | const int width = image.width(); | ||||
227 | const int height = image.height(); | ||||
228 | | ||||
229 | const int centerX = qCeil(width * 0.5); | ||||
230 | const int centerY = qCeil(height * 0.5); | ||||
231 | | ||||
232 | const int alphaOffset = QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3; | ||||
233 | const int stride = image.depth() >> 3; | ||||
234 | | ||||
235 | for (int y = 0; y < centerY; ++y) { | ||||
236 | uint8_t *in = image.scanLine(y) + alphaOffset; | ||||
237 | uint8_t *out = in + (width - 1) * stride; | ||||
238 | | ||||
239 | for (int x = 0; x < centerX; ++x, in += stride, out -= stride) { | ||||
240 | *out = *in; | ||||
241 | } | ||||
242 | } | ||||
243 | | ||||
244 | for (int y = 0; y < centerY; ++y) { | ||||
245 | const uint8_t *in = image.scanLine(y) + alphaOffset; | ||||
246 | uint8_t *out = image.scanLine(width - y - 1) + alphaOffset; | ||||
247 | | ||||
248 | for (int x = 0; x < width; ++x, in += stride, out += stride) { | ||||
249 | *out = *in; | ||||
250 | } | ||||
251 | } | ||||
252 | } | ||||
253 | | ||||
254 | static void renderShadow(QPainter *painter, const QRect &rect, qreal borderRadius, const QPoint &offset, int radius, const QColor &color) | ||||
255 | { | ||||
256 | const QSize inflation = calculateBlurExtent(radius); | ||||
257 | const QSize size = rect.size() + 2 * inflation; | ||||
258 | | ||||
259 | #if BREEZE_COMMON_USE_KDE4 | ||||
260 | const qreal dpr = 1.0; | ||||
261 | #else | ||||
262 | const qreal dpr = painter->device()->devicePixelRatioF(); | ||||
263 | #endif | ||||
264 | | ||||
265 | QImage shadow(size * dpr, QImage::Format_ARGB32_Premultiplied); | ||||
266 | #if !BREEZE_COMMON_USE_KDE4 | ||||
267 | shadow.setDevicePixelRatio(dpr); | ||||
268 | #endif | ||||
269 | shadow.fill(Qt::transparent); | ||||
270 | | ||||
271 | QRect boxRect(QPoint(0, 0), rect.size()); | ||||
272 | boxRect.moveCenter(shadow.rect().center()); | ||||
273 | | ||||
274 | const qreal xRadius = 2.0 * borderRadius / boxRect.width(); | ||||
275 | const qreal yRadius = 2.0 * borderRadius / boxRect.height(); | ||||
276 | | ||||
277 | QPainter shadowPainter; | ||||
278 | shadowPainter.begin(&shadow); | ||||
279 | shadowPainter.setRenderHint(QPainter::Antialiasing); | ||||
280 | shadowPainter.setPen(Qt::NoPen); | ||||
281 | shadowPainter.setBrush(Qt::black); | ||||
282 | shadowPainter.drawRoundedRect(boxRect, xRadius, yRadius); | ||||
283 | shadowPainter.end(); | ||||
284 | | ||||
285 | // Because the shadow texture is symmetrical, that's enough to blur | ||||
286 | // only the top-left quadrant and then mirror it. | ||||
287 | const QRect blurRect(0, 0, qCeil(shadow.width() * 0.5), qCeil(shadow.height() * 0.5)); | ||||
288 | const int scaledRadius = qRound(radius * dpr); | ||||
289 | boxBlurAlpha(shadow, scaledRadius, blurRect); | ||||
290 | mirrorTopLeftQuadrant(shadow); | ||||
291 | | ||||
292 | // Give the shadow a tint of the desired color. | ||||
293 | shadowPainter.begin(&shadow); | ||||
294 | shadowPainter.setCompositionMode(QPainter::CompositionMode_SourceIn); | ||||
295 | shadowPainter.fillRect(shadow.rect(), color); | ||||
296 | shadowPainter.end(); | ||||
297 | | ||||
298 | // Actually, present the shadow. | ||||
299 | QRect shadowRect = shadow.rect(); | ||||
300 | shadowRect.setSize(shadowRect.size() / dpr); | ||||
301 | shadowRect.moveCenter(rect.center() + offset); | ||||
302 | painter->drawImage(shadowRect, shadow); | ||||
303 | } | ||||
304 | | ||||
305 | void BoxShadowRenderer::setBoxSize(const QSize &size) | ||||
306 | { | ||||
307 | m_boxSize = size; | ||||
308 | } | ||||
309 | | ||||
310 | void BoxShadowRenderer::setBorderRadius(qreal radius) | ||||
311 | { | ||||
312 | m_borderRadius = radius; | ||||
313 | } | ||||
314 | | ||||
315 | void BoxShadowRenderer::setDevicePixelRatio(qreal dpr) | ||||
316 | { | ||||
317 | m_dpr = dpr; | ||||
318 | } | ||||
319 | | ||||
320 | void BoxShadowRenderer::addShadow(const QPoint &offset, int radius, const QColor &color) | ||||
321 | { | ||||
322 | Shadow shadow = {}; | ||||
323 | shadow.offset = offset; | ||||
324 | shadow.radius = radius; | ||||
325 | shadow.color = color; | ||||
326 | m_shadows.append(shadow); | ||||
327 | } | ||||
328 | | ||||
329 | QImage BoxShadowRenderer::render() const | ||||
330 | { | ||||
331 | if (m_shadows.isEmpty()) { | ||||
332 | return {}; | ||||
333 | } | ||||
334 | | ||||
335 | QSize canvasSize; | ||||
336 | for (const Shadow &shadow : qAsConst(m_shadows)) { | ||||
337 | canvasSize = canvasSize.expandedTo( | ||||
338 | calculateMinimumShadowTextureSize(m_boxSize, shadow.radius, shadow.offset)); | ||||
339 | } | ||||
340 | | ||||
341 | QImage canvas(canvasSize * m_dpr, QImage::Format_ARGB32_Premultiplied); | ||||
342 | #if !BREEZE_COMMON_USE_KDE4 | ||||
343 | canvas.setDevicePixelRatio(m_dpr); | ||||
344 | #endif | ||||
345 | canvas.fill(Qt::transparent); | ||||
346 | | ||||
347 | QRect boxRect(QPoint(0, 0), m_boxSize); | ||||
348 | boxRect.moveCenter(QRect(QPoint(0, 0), canvasSize).center()); | ||||
349 | | ||||
350 | QPainter painter(&canvas); | ||||
351 | for (const Shadow &shadow : qAsConst(m_shadows)) { | ||||
352 | renderShadow(&painter, boxRect, m_borderRadius, shadow.offset, shadow.radius, shadow.color); | ||||
353 | } | ||||
354 | painter.end(); | ||||
355 | | ||||
356 | return canvas; | ||||
357 | } | ||||
358 | | ||||
359 | QSize BoxShadowRenderer::calculateMinimumBoxSize(int radius) | ||||
360 | { | ||||
361 | const QSize blurExtent = calculateBlurExtent(radius); | ||||
362 | return 2 * blurExtent + QSize(1, 1); | ||||
363 | } | ||||
364 | | ||||
365 | QSize BoxShadowRenderer::calculateMinimumShadowTextureSize(const QSize &boxSize, int radius, const QPoint &offset) | ||||
366 | { | ||||
367 | return boxSize + 2 * calculateBlurExtent(radius) + QSize(qAbs(offset.x()), qAbs(offset.y())); | ||||
368 | } | ||||
369 | | ||||
370 | } // namespace Breeze |