Changeset View
Changeset View
Standalone View
Standalone View
libbreezecommon/breezeboxshadowhelper.cpp
- This file was added.
1 | /* | ||||
---|---|---|---|---|---|
2 | * Copyright (C) 2018 Vlad Zagorodniy <vladzzag@gmail.com> | ||||
3 | * | ||||
4 | * This program is free software; you can redistribute it and/or | ||||
5 | * modify it under the terms of the GNU General Public License as | ||||
6 | * published by the Free Software Foundation; either version 2 of | ||||
7 | * the License or (at your option) version 3 or any later version | ||||
8 | * accepted by the membership of KDE e.V. (or its successor approved | ||||
9 | * by the membership of KDE e.V.), which shall act as a proxy | ||||
10 | * defined in Section 14 of version 3 of the license. | ||||
11 | * | ||||
12 | * This program is distributed in the hope that it will be useful, | ||||
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
15 | * GNU General Public License for more details. | ||||
16 | * | ||||
17 | * You should have received a copy of the GNU General Public License | ||||
18 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
19 | */ | ||||
20 | | ||||
21 | #include "config-breezecommon.h" | ||||
22 | | ||||
23 | #include "breezeboxshadowhelper.h" | ||||
24 | | ||||
25 | #include <cmath> | ||||
26 | | ||||
27 | #include <QPainter> | ||||
28 | #include <QVector> | ||||
29 | | ||||
30 | #include <fftw3.h> | ||||
31 | | ||||
32 | | ||||
33 | namespace Breeze { | ||||
34 | namespace BoxShadowHelper { | ||||
35 | | ||||
36 | QVector<double> computeGaussianKernel(double radius, double sigma) | ||||
37 | { | ||||
38 | QVector<double> kernel; | ||||
39 | const int kernelSize = static_cast<int>(radius) * 2 + 1; | ||||
40 | | ||||
41 | const double den = sqrt(2.0) * sigma; | ||||
42 | double kernelNorm = 0.0; | ||||
43 | double lastInt = 0.5 * erf((-radius - 0.5) / den); | ||||
44 | | ||||
45 | for (int i = 0; i < kernelSize; i++) { | ||||
46 | const double currInt = 0.5 * erf((i - radius + 0.5) / den); | ||||
47 | const double w = currInt - lastInt; | ||||
48 | kernel << w; | ||||
49 | kernelNorm += w; | ||||
50 | lastInt = currInt; | ||||
51 | } | ||||
52 | | ||||
53 | for (auto &w : kernel) { | ||||
54 | w /= kernelNorm; | ||||
55 | } | ||||
56 | | ||||
57 | return kernel; | ||||
58 | } | ||||
59 | | ||||
60 | // Blur alpha channel of the given image using separable convolution | ||||
61 | // gaussian kernel. Not very efficient with big blur radii. | ||||
62 | void blurAlphaSeparable(QImage &img, double radius, double sigma) | ||||
63 | { | ||||
64 | const auto kernel = computeGaussianKernel(radius, sigma); | ||||
65 | | ||||
66 | QImage tmp(img.height(), img.width(), img.format()); | ||||
67 | | ||||
68 | QRgb *imgData = reinterpret_cast<QRgb *>(img.scanLine(0)); | ||||
69 | QRgb *tmpData = reinterpret_cast<QRgb *>(tmp.scanLine(0)); | ||||
70 | const int imgStride = img.width(); | ||||
71 | const int tmpStride = tmp.width(); | ||||
72 | | ||||
73 | const int shift = static_cast<int>(radius); | ||||
74 | | ||||
75 | // Blur in X direction. Please note, the result is stored in a temporary | ||||
76 | // transposed buffer. The result is transposed to read memory in linear order. | ||||
77 | for (int y = 0; y < img.height(); y++) { | ||||
78 | for (int x = 0; x < img.width(); x++) { | ||||
79 | double alpha = 0.0; | ||||
80 | | ||||
81 | for (int i = 0; i < kernel.size(); i++) { | ||||
82 | const int idx = y * imgStride + qBound(0, x + i - shift, img.width() - 1); | ||||
83 | alpha += qAlpha(imgData[idx]) * kernel[i]; | ||||
84 | } | ||||
85 | | ||||
86 | const int idx = x * tmpStride + y; | ||||
87 | tmpData[idx] = qRgba(0, 0, 0, static_cast<int>(alpha)); | ||||
88 | } | ||||
89 | } | ||||
90 | | ||||
91 | // Blur in Y direction. The result is transposed again so size | ||||
92 | // matches original image size. | ||||
93 | for (int y = 0; y < tmp.height(); y++) { | ||||
94 | for (int x = 0; x < tmp.width(); x++) { | ||||
95 | double alpha = 0.0; | ||||
96 | | ||||
97 | for (int i = 0; i < kernel.size(); i++) { | ||||
98 | const int idx = y * tmpStride + qBound(0, x + i - shift, tmp.width() - 1); | ||||
99 | alpha += qAlpha(tmpData[idx]) * kernel[i]; | ||||
100 | } | ||||
101 | | ||||
102 | const int idx = x * imgStride + y; | ||||
103 | imgData[idx] = qRgba(0, 0, 0, static_cast<int>(alpha)); | ||||
104 | } | ||||
105 | } | ||||
106 | } | ||||
107 | | ||||
108 | // Blur alpha channel of the given image using Fourier Transform. | ||||
109 | // It's somewhat efficient with big blur radii. | ||||
110 | // | ||||
111 | // It works as follows: | ||||
112 | // - do FFT on given input image(it is expected, that the | ||||
113 | // input image was padded before) | ||||
114 | // - compute Gaussian kernel, pad it to the size of the input | ||||
115 | // image, and do FFT on it | ||||
116 | // - multiply the two in the frequency domain(element-wise) | ||||
117 | // - transform the result back to "time domain" | ||||
118 | // | ||||
119 | // Please notice that in order to omit several(4, more precisely) | ||||
120 | // memory copy ops, the Gaussian kernel is wrapped around and not centered. | ||||
121 | void blurAlphaFFT(QImage &img, double radius, double sigma) | ||||
122 | { | ||||
123 | const int size = img.width() * img.height(); | ||||
124 | | ||||
125 | fftw_complex *imageIn; | ||||
126 | imageIn = reinterpret_cast<fftw_complex *>(fftw_malloc(sizeof(fftw_complex) * size)); | ||||
127 | | ||||
128 | QRgb *imgData = reinterpret_cast<QRgb *>(img.scanLine(0)); | ||||
129 | for (int i = 0; i < size; i++) { | ||||
130 | imageIn[i][0] = qAlpha(imgData[i]); | ||||
131 | imageIn[i][1] = 0.0; | ||||
132 | } | ||||
133 | | ||||
134 | fftw_complex *imageOut; | ||||
135 | imageOut = reinterpret_cast<fftw_complex *>(fftw_malloc(sizeof(fftw_complex) * size)); | ||||
136 | | ||||
137 | const QVector<double> kernel_ = computeGaussianKernel(radius, sigma); | ||||
138 | QVector<double> kernel; | ||||
139 | kernel.resize(size); | ||||
140 | | ||||
141 | const int shift = -static_cast<int>(radius); | ||||
142 | const int kernelSize = kernel_.size(); | ||||
143 | for (int y = 0; y < kernelSize; y++) { | ||||
144 | for (int x = 0; x < kernelSize; x++) { | ||||
145 | const int j = (img.width() + x + shift) % img.width(); | ||||
146 | const int i = (img.height() + y + shift) % img.height(); | ||||
147 | kernel[j + i * img.width()] = kernel_[x] * kernel_[y]; | ||||
148 | } | ||||
149 | } | ||||
150 | | ||||
151 | fftw_complex *kernelIn; | ||||
152 | kernelIn = reinterpret_cast<fftw_complex *>(fftw_malloc(sizeof(fftw_complex) * kernel.size())); | ||||
153 | | ||||
154 | for (int i = 0; i < size; i++) { | ||||
155 | kernelIn[i][0] = kernel[i]; | ||||
156 | kernelIn[i][1] = 0.0; | ||||
157 | } | ||||
158 | | ||||
159 | fftw_complex *kernelOut; | ||||
160 | kernelOut = reinterpret_cast<fftw_complex *>(fftw_malloc(sizeof(fftw_complex) * kernel.size())); | ||||
161 | | ||||
162 | fftw_plan planImageFFT; | ||||
163 | planImageFFT = fftw_plan_dft_2d(img.height(), img.width(), | ||||
164 | imageIn, imageOut, | ||||
165 | FFTW_FORWARD, FFTW_ESTIMATE); | ||||
166 | fftw_execute(planImageFFT); | ||||
167 | | ||||
168 | fftw_plan planKernelFFT; | ||||
169 | planKernelFFT = fftw_plan_dft_2d(img.height(), img.width(), | ||||
170 | kernelIn, kernelOut, | ||||
171 | FFTW_FORWARD, FFTW_ESTIMATE); | ||||
172 | fftw_execute(planKernelFFT); | ||||
173 | | ||||
174 | for (int i = 0; i < size; i++) { | ||||
175 | const double re = imageOut[i][0] * kernelOut[i][0] - imageOut[i][1] * kernelOut[i][1]; | ||||
176 | const double im = imageOut[i][0] * kernelOut[i][1] + imageOut[i][1] * kernelOut[i][0]; | ||||
177 | imageOut[i][0] = re; | ||||
178 | imageOut[i][1] = im; | ||||
179 | } | ||||
180 | | ||||
181 | fftw_plan planImageIFFT; | ||||
182 | planImageIFFT = fftw_plan_dft_2d(img.height(), img.width(), | ||||
183 | imageOut, imageIn, | ||||
184 | FFTW_BACKWARD, FFTW_ESTIMATE); | ||||
185 | fftw_execute(planImageIFFT); | ||||
186 | | ||||
187 | for (int i = 0; i < size; i++) { | ||||
188 | imgData[i] = qRgba(0, 0, 0, imageIn[i][0] / size); | ||||
189 | } | ||||
190 | | ||||
191 | fftw_free(kernelIn); | ||||
192 | fftw_free(kernelOut); | ||||
193 | | ||||
194 | fftw_free(imageIn); | ||||
195 | fftw_free(imageOut); | ||||
196 | | ||||
197 | fftw_destroy_plan(planKernelFFT); | ||||
198 | fftw_destroy_plan(planImageFFT); | ||||
199 | fftw_destroy_plan(planImageIFFT); | ||||
200 | } | ||||
201 | | ||||
202 | namespace { | ||||
203 | // FFT approach outperforms separable convolution kernels when blur radius >= 64. | ||||
204 | // (was discovered after doing a lot of benchmarks) | ||||
205 | const int FFT_BLUR_RADIUS_THRESHOLD = 64; | ||||
206 | | ||||
207 | // According to the CSS Level 3 spec, standard deviation must be equal to | ||||
208 | // half of the blur radius. https://www.w3.org/TR/css-backgrounds-3/#shadow-blur | ||||
209 | // Current window size is too small for sigma equal to half of the blur radius. | ||||
210 | // As a workaround, sigma blur scale is lowered. With the lowered sigma | ||||
211 | // blur scale, area under the kernel equals to 0.98, which is pretty enough. | ||||
212 | // Maybe, it should be changed in the future. | ||||
213 | const double SIGMA_BLUR_SCALE = 0.4375; | ||||
214 | } | ||||
215 | | ||||
216 | inline double radiusToSigma(double radius) | ||||
217 | { | ||||
218 | return SIGMA_BLUR_SCALE * radius; | ||||
219 | } | ||||
220 | | ||||
221 | void boxShadow(QPainter *p, const QRect &box, const QPoint &offset, int radius, const QColor &color) | ||||
222 | { | ||||
223 | const QSize size = box.size() + 2 * QSize(radius, radius); | ||||
224 | | ||||
225 | #if BREEZE_COMMON_USE_KDE4 | ||||
226 | const qreal dpr = 1.0; | ||||
227 | #else | ||||
228 | const qreal dpr = p->device()->devicePixelRatioF(); | ||||
229 | #endif | ||||
230 | | ||||
231 | QPainter painter; | ||||
232 | | ||||
233 | QImage shadow(size * dpr, QImage::Format_ARGB32_Premultiplied); | ||||
234 | #if !BREEZE_COMMON_USE_KDE4 | ||||
235 | shadow.setDevicePixelRatio(dpr); | ||||
236 | #endif | ||||
237 | shadow.fill(Qt::transparent); | ||||
238 | | ||||
239 | painter.begin(&shadow); | ||||
240 | painter.fillRect(QRect(QPoint(radius, radius), box.size()), Qt::black); | ||||
241 | painter.end(); | ||||
242 | | ||||
243 | // There is no need to blur RGB channels. Blur the alpha | ||||
244 | // channel and do compositing stuff later. | ||||
245 | const double radius_ = radius * dpr; | ||||
246 | const double sigma = radiusToSigma(radius_); | ||||
247 | if (radius_ < FFT_BLUR_RADIUS_THRESHOLD) { | ||||
248 | blurAlphaSeparable(shadow, radius_, sigma); | ||||
249 | } else { | ||||
250 | blurAlphaFFT(shadow, radius_, sigma); | ||||
251 | } | ||||
252 | | ||||
253 | painter.begin(&shadow); | ||||
254 | painter.setCompositionMode(QPainter::CompositionMode_SourceIn); | ||||
255 | painter.fillRect(shadow.rect(), color); | ||||
256 | painter.end(); | ||||
257 | | ||||
258 | QRect shadowRect = shadow.rect(); | ||||
259 | shadowRect.setSize(shadowRect.size() / dpr); | ||||
260 | shadowRect.moveCenter(box.center() + offset); | ||||
261 | p->drawImage(shadowRect, shadow); | ||||
262 | } | ||||
263 | | ||||
264 | } // BoxShadowHelper | ||||
265 | } // Breeze |