Changeset View
Changeset View
Standalone View
Standalone View
kdecoration/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 "breezeboxshadowhelper.h" | ||||
22 | | ||||
23 | #include <cmath> | ||||
24 | | ||||
25 | #include <QPainter> | ||||
26 | #include <QVector> | ||||
27 | | ||||
28 | #include <fftw3.h> | ||||
29 | | ||||
30 | | ||||
31 | namespace Breeze { | ||||
32 | namespace BoxShadowHelper { | ||||
33 | | ||||
34 | QVector<double> computeGaussianKernel(double radius, double sigma) | ||||
35 | { | ||||
36 | QVector<double> kernel; | ||||
37 | const int kernelSize = static_cast<int>(radius) * 2 + 1; | ||||
38 | | ||||
39 | const double den = sqrt(2.0) * sigma; | ||||
40 | double kernelNorm = 0.0; | ||||
41 | double lastInt = 0.5 * erf((-radius - 0.5) / den); | ||||
42 | | ||||
43 | for (int i = 0; i < kernelSize; i++) { | ||||
44 | const double currInt = 0.5 * erf((i - radius + 0.5) / den); | ||||
45 | const double w = currInt - lastInt; | ||||
46 | kernel << w; | ||||
47 | kernelNorm += w; | ||||
48 | lastInt = currInt; | ||||
49 | } | ||||
50 | | ||||
51 | for (auto &w : kernel) { | ||||
52 | w /= kernelNorm; | ||||
53 | } | ||||
54 | | ||||
55 | return kernel; | ||||
56 | } | ||||
57 | | ||||
58 | // Blur alpha channel of the given image using separable convolution | ||||
59 | // gaussian kernel. Not very efficient with big blur radii. | ||||
60 | void blurAlphaSeparable(QImage &img, double radius, double sigma) | ||||
61 | { | ||||
62 | const auto kernel = computeGaussianKernel(radius, sigma); | ||||
63 | | ||||
64 | QImage tmp(img.size().transposed(), img.format()); | ||||
65 | | ||||
66 | QRgb *imgData = reinterpret_cast<QRgb *>(img.scanLine(0)); | ||||
67 | QRgb *tmpData = reinterpret_cast<QRgb *>(tmp.scanLine(0)); | ||||
68 | const int imgStride = img.width(); | ||||
69 | const int tmpStride = tmp.width(); | ||||
70 | | ||||
71 | const int shift = static_cast<int>(radius); | ||||
72 | | ||||
73 | // Blur in X direction. Please note, the result is stored | ||||
74 | // in a temporary transposed temporary buffer. The result | ||||
75 | // is transposed in order to read memory in linear order. | ||||
76 | for (int y = 0; y < img.height(); y++) { | ||||
77 | for (int x = 0; x < img.width(); x++) { | ||||
78 | double alpha = 0.0; | ||||
79 | | ||||
80 | for (int i = 0; i < kernel.size(); i++) { | ||||
81 | const int idx = y * imgStride + qBound(0, x + i - shift, img.width() - 1); | ||||
82 | alpha += qAlpha(imgData[idx]) * kernel[i]; | ||||
83 | } | ||||
84 | | ||||
85 | const int idx = x * tmpStride + y; | ||||
86 | tmpData[idx] = qRgba(0, 0, 0, static_cast<int>(alpha)); | ||||
87 | } | ||||
88 | } | ||||
89 | | ||||
90 | // Blur in Y direction. The result is transposed again so size | ||||
91 | // matches original image size. | ||||
92 | for (int y = 0; y < tmp.height(); y++) { | ||||
93 | for (int x = 0; x < tmp.width(); x++) { | ||||
94 | double alpha = 0.0; | ||||
95 | | ||||
96 | for (int i = 0; i < kernel.size(); i++) { | ||||
97 | const int idx = y * tmpStride + qBound(0, x + i - shift, tmp.width() - 1); | ||||
98 | alpha += qAlpha(tmpData[idx]) * kernel[i]; | ||||
99 | } | ||||
100 | | ||||
101 | const int idx = x * imgStride + y; | ||||
102 | imgData[idx] = qRgba(0, 0, 0, static_cast<int>(alpha)); | ||||
103 | } | ||||
104 | } | ||||
105 | } | ||||
106 | | ||||
107 | // Blur alpha channel of the given image using Fourier Transform. | ||||
108 | // It's somewhat efficient with big blur radii. | ||||
109 | // | ||||
110 | // It works as follows: | ||||
111 | // - do FFT on given input image(it is expected, that the | ||||
112 | // input image was padded before) | ||||
113 | // - compute Gaussian kernel, pad it to the size of the input | ||||
114 | // image, and do FFT on it | ||||
115 | // - multiply the two in the frequency domain(element-wise) | ||||
116 | // - transform the result back to "time domain" | ||||
117 | // | ||||
118 | // Please notice that in order to omit several(4, more precisely) | ||||
119 | // memory copy ops, the Gaussian kernel is wrapped around and not centered. | ||||
120 | void blurAlphaFFT(QImage &img, double radius, double sigma) | ||||
121 | { | ||||
122 | int size = img.width() * img.height(); | ||||
123 | | ||||
124 | fftw_complex *imageIn; | ||||
125 | imageIn = reinterpret_cast<fftw_complex *>(fftw_malloc(sizeof(fftw_complex) * size)); | ||||
126 | | ||||
127 | QRgb *imgData = reinterpret_cast<QRgb *>(img.scanLine(0)); | ||||
128 | for (int i = 0; i < size; i++) { | ||||
129 | imageIn[i][0] = qAlpha(imgData[i]); | ||||
130 | imageIn[i][1] = 0.0; | ||||
131 | } | ||||
132 | | ||||
133 | fftw_complex *imageOut; | ||||
134 | imageOut = reinterpret_cast<fftw_complex *>(fftw_malloc(sizeof(fftw_complex) * size)); | ||||
135 | | ||||
136 | QVector<double> kernel_ = computeGaussianKernel(radius, sigma); | ||||
137 | QVector<double> kernel; | ||||
138 | kernel.resize(size); | ||||
139 | | ||||
140 | const int shift = -static_cast<int>(radius); | ||||
141 | const int kernelSize = kernel_.size(); | ||||
142 | for (int y = 0; y < kernelSize; y++) { | ||||
143 | for (int x = 0; x < kernelSize; x++) { | ||||
144 | int j = (img.width() + x + shift) % img.width(); | ||||
145 | int i = (img.height() + y + shift) % img.height(); | ||||
146 | kernel[j + i * img.width()] = kernel_[x] * kernel_[y]; | ||||
147 | } | ||||
148 | } | ||||
149 | | ||||
150 | fftw_complex *kernelIn; | ||||
151 | kernelIn = reinterpret_cast<fftw_complex *>(fftw_malloc(sizeof(fftw_complex) * kernel.size())); | ||||
152 | | ||||
153 | for (int i = 0; i < size; i++) { | ||||
154 | kernelIn[i][0] = kernel[i]; | ||||
155 | kernelIn[i][1] = 0.0; | ||||
156 | } | ||||
157 | | ||||
158 | fftw_complex *kernelOut; | ||||
159 | kernelOut = reinterpret_cast<fftw_complex *>(fftw_malloc(sizeof(fftw_complex) * kernel.size())); | ||||
160 | | ||||
161 | fftw_plan planImageFFT; | ||||
162 | planImageFFT = fftw_plan_dft_2d(img.height(), img.width(), | ||||
163 | imageIn, imageOut, | ||||
164 | FFTW_FORWARD, FFTW_ESTIMATE); | ||||
165 | fftw_execute(planImageFFT); | ||||
166 | | ||||
167 | fftw_plan planKernelFFT; | ||||
168 | planKernelFFT = fftw_plan_dft_2d(img.height(), img.width(), | ||||
169 | kernelIn, kernelOut, | ||||
170 | FFTW_FORWARD, FFTW_ESTIMATE); | ||||
171 | fftw_execute(planKernelFFT); | ||||
172 | | ||||
173 | for (int i = 0; i < size; i++) { | ||||
174 | double re = imageOut[i][0] * kernelOut[i][0] - imageOut[i][1] * kernelOut[i][1]; | ||||
175 | double im = imageOut[i][0] * kernelOut[i][1] + imageOut[i][1] * kernelOut[i][0]; | ||||
176 | imageOut[i][0] = re; | ||||
177 | imageOut[i][1] = im; | ||||
178 | } | ||||
179 | | ||||
180 | fftw_plan planImageIFFT; | ||||
181 | planImageIFFT = fftw_plan_dft_2d(img.height(), img.width(), | ||||
182 | imageOut, imageIn, | ||||
183 | FFTW_BACKWARD, FFTW_ESTIMATE); | ||||
184 | fftw_execute(planImageIFFT); | ||||
185 | | ||||
186 | for (int i = 0; i < size; i++) { | ||||
187 | imgData[i] = qRgba(0, 0, 0, imageIn[i][0] / size); | ||||
188 | } | ||||
189 | | ||||
190 | fftw_free(kernelIn); | ||||
191 | fftw_free(kernelOut); | ||||
192 | | ||||
193 | fftw_free(imageIn); | ||||
194 | fftw_free(imageOut); | ||||
195 | | ||||
196 | fftw_destroy_plan(planKernelFFT); | ||||
197 | fftw_destroy_plan(planImageFFT); | ||||
198 | fftw_destroy_plan(planImageIFFT); | ||||
199 | } | ||||
200 | | ||||
201 | namespace { | ||||
202 | // FFT approach outperforms separable convolution kernels when blur radius >= 64. | ||||
203 | // (was discovered after doing a lot of benchmarks) | ||||
204 | const int FFT_BLUR_RADIUS_THRESHOLD = 64; | ||||
205 | | ||||
206 | // With big sigma scale, it is not enough to pad box image with blur radius. | ||||
207 | // As a workaround, we could lower the scale value. According to the CSS | ||||
208 | // Level 3 spec, standard deviation must be equal to half of the blur radius. | ||||
209 | // https://www.w3.org/TR/css-backgrounds-3/#shadow-blur | ||||
210 | const double SIGMA_BLUR_SCALE = 0.4375; | ||||
211 | } | ||||
212 | | ||||
213 | inline double radiusToSigma(double radius) | ||||
214 | { | ||||
215 | return SIGMA_BLUR_SCALE * radius + 0.5; | ||||
216 | } | ||||
217 | | ||||
218 | void boxShadow(QPainter *p, const QRect &box, int radius, const QColor &color) | ||||
219 | { | ||||
220 | QSize size = box.size() + 2 * QSize(radius, radius); | ||||
221 | | ||||
222 | QPainter painter; | ||||
223 | | ||||
224 | QImage shadow(size, QImage::Format_ARGB32_Premultiplied); | ||||
225 | shadow.fill(Qt::transparent); | ||||
226 | painter.begin(&shadow); | ||||
227 | painter.fillRect(QRect(QPoint(radius, radius), box.size()), Qt::black); | ||||
228 | painter.end(); | ||||
229 | | ||||
230 | double sigma = radiusToSigma(radius); | ||||
231 | | ||||
232 | // There is no need to blur RGB channels. Blur the alpha | ||||
233 | // channel and do compositing stuff later. | ||||
234 | if (radius < FFT_BLUR_RADIUS_THRESHOLD) { | ||||
235 | blurAlphaSeparable(shadow, radius, sigma); | ||||
236 | } else { | ||||
237 | blurAlphaFFT(shadow, radius, sigma); | ||||
238 | } | ||||
239 | | ||||
240 | painter.begin(&shadow); | ||||
241 | painter.setCompositionMode(QPainter::CompositionMode_SourceIn); | ||||
242 | painter.fillRect(shadow.rect(), color); | ||||
243 | painter.end(); | ||||
244 | | ||||
245 | QRect shadowRect = shadow.rect(); | ||||
246 | shadowRect.moveCenter(box.center()); | ||||
247 | p->drawImage(shadowRect, shadow); | ||||
248 | } | ||||
249 | | ||||
250 | } // BoxShadowHelper | ||||
251 | } // Breeze |