1 | /* GSK - The GIMP Toolkit |
2 | * |
3 | * Copyright (C) 2014 Red Hat |
4 | * |
5 | * This library is free software; you can redistribute it and/or |
6 | * modify it under the terms of the GNU Library General Public |
7 | * License as published by the Free Software Foundation; either |
8 | * version 2 of the License, or (at your option) any later version. |
9 | * |
10 | * This library is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
13 | * Library General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU Library General Public |
16 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
17 | * |
18 | * Written by: |
19 | * Jasper St. Pierre <jstpierre@mecheye.net> |
20 | * Owen Taylor <otaylor@redhat.com> |
21 | */ |
22 | |
23 | #include "gskcairoblurprivate.h" |
24 | |
25 | #include <math.h> |
26 | #include <string.h> |
27 | |
28 | /* |
29 | * Gets the size for a single box blur. |
30 | * |
31 | * Much of this, the 3 * sqrt(2 * pi) / 4, is the known value for |
32 | * approximating a Gaussian using box blurs. This yields quite a good |
33 | * approximation for a Gaussian. For more details, see: |
34 | * http://www.w3.org/TR/SVG11/filters.html#feGaussianBlurElement |
35 | * https://bugzilla.mozilla.org/show_bug.cgi?id=590039#c19 |
36 | */ |
37 | #define GAUSSIAN_SCALE_FACTOR ((3.0 * sqrt(2 * G_PI) / 4)) |
38 | |
39 | #define get_box_filter_size(radius) ((int)(GAUSSIAN_SCALE_FACTOR * (radius))) |
40 | |
41 | /* Sadly, clang is picky about get_box_filter_size(2) not being a |
42 | * constant expression, thus we have to use precomputed values. |
43 | */ |
44 | #define BOX_FILTER_SIZE_2 3 |
45 | #define BOX_FILTER_SIZE_3 5 |
46 | #define BOX_FILTER_SIZE_4 7 |
47 | #define BOX_FILTER_SIZE_5 9 |
48 | #define BOX_FILTER_SIZE_6 11 |
49 | #define BOX_FILTER_SIZE_7 13 |
50 | #define BOX_FILTER_SIZE_8 15 |
51 | #define BOX_FILTER_SIZE_9 16 |
52 | #define BOX_FILTER_SIZE_10 18 |
53 | |
54 | /* This applies a single box blur pass to a horizontal range of pixels; |
55 | * since the box blur has the same weight for all pixels, we can |
56 | * implement an efficient sliding window algorithm where we add |
57 | * in pixels coming into the window from the right and remove |
58 | * them when they leave the windw to the left. |
59 | * |
60 | * d is the filter width; for even d shift indicates how the blurred |
61 | * result is aligned with the original - does ' x ' go to ' yy' (shift=1) |
62 | * or 'yy ' (shift=-1) |
63 | */ |
64 | static void |
65 | blur_xspan (guchar *row, |
66 | guchar *tmp_buffer, |
67 | int row_width, |
68 | int d, |
69 | int shift) |
70 | { |
71 | int offset; |
72 | int sum = 0; |
73 | int i; |
74 | |
75 | if (d % 2 == 1) |
76 | offset = d / 2; |
77 | else |
78 | offset = (d - shift) / 2; |
79 | |
80 | /* All the conditionals in here look slow, but the branches will |
81 | * be well predicted and there are enough different possibilities |
82 | * that trying to write this as a series of unconditional loops |
83 | * is hard and not an obvious win. The main slow down here seems |
84 | * to be the integer division per pixel; one possible optimization |
85 | * would be to accumulate into two 16-bit integer buffers and |
86 | * only divide down after all three passes. (SSE parallel implementation |
87 | * of the divide step is possible.) |
88 | */ |
89 | |
90 | #define BLUR_ROW_KERNEL(D) \ |
91 | for (i = -(D) + offset; i < row_width + offset; i++) \ |
92 | { \ |
93 | if (i >= 0 && i < row_width) \ |
94 | sum += row[i]; \ |
95 | \ |
96 | if (i >= offset) \ |
97 | { \ |
98 | if (i >= (D)) \ |
99 | sum -= row[i - (D)]; \ |
100 | \ |
101 | tmp_buffer[i - offset] = (sum + (D) / 2) / (D); \ |
102 | } \ |
103 | } \ |
104 | break; |
105 | |
106 | /* We unroll the values for d for radius 2-10 to avoid a generic |
107 | * divide operation (not radius 1, because its a no-op) */ |
108 | switch (d) |
109 | { |
110 | case BOX_FILTER_SIZE_2: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_2); |
111 | case BOX_FILTER_SIZE_3: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_3); |
112 | case BOX_FILTER_SIZE_4: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_4); |
113 | case BOX_FILTER_SIZE_5: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_5); |
114 | case BOX_FILTER_SIZE_6: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_6); |
115 | case BOX_FILTER_SIZE_7: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_7); |
116 | case BOX_FILTER_SIZE_8: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_8); |
117 | case BOX_FILTER_SIZE_9: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_9); |
118 | case BOX_FILTER_SIZE_10: BLUR_ROW_KERNEL (BOX_FILTER_SIZE_10); |
119 | default: BLUR_ROW_KERNEL (d); |
120 | } |
121 | |
122 | memcpy (dest: row, src: tmp_buffer, n: row_width); |
123 | } |
124 | |
125 | static void |
126 | blur_rows (guchar *dst_buffer, |
127 | guchar *tmp_buffer, |
128 | int buffer_width, |
129 | int buffer_height, |
130 | int d) |
131 | { |
132 | int i; |
133 | |
134 | for (i = 0; i < buffer_height; i++) |
135 | { |
136 | guchar *row = dst_buffer + i * buffer_width; |
137 | |
138 | /* We want to produce a symmetric blur that spreads a pixel |
139 | * equally far to the left and right. If d is odd that happens |
140 | * naturally, but for d even, we approximate by using a blur |
141 | * on either side and then a centered blur of size d + 1. |
142 | * (technique also from the SVG specification) |
143 | */ |
144 | if (d % 2 == 1) |
145 | { |
146 | blur_xspan (row, tmp_buffer, row_width: buffer_width, d, shift: 0); |
147 | blur_xspan (row, tmp_buffer, row_width: buffer_width, d, shift: 0); |
148 | blur_xspan (row, tmp_buffer, row_width: buffer_width, d, shift: 0); |
149 | } |
150 | else |
151 | { |
152 | blur_xspan (row, tmp_buffer, row_width: buffer_width, d, shift: 1); |
153 | blur_xspan (row, tmp_buffer, row_width: buffer_width, d, shift: -1); |
154 | blur_xspan (row, tmp_buffer, row_width: buffer_width, d: d + 1, shift: 0); |
155 | } |
156 | } |
157 | } |
158 | |
159 | /* Swaps width and height. |
160 | */ |
161 | static void |
162 | flip_buffer (guchar *dst_buffer, |
163 | guchar *src_buffer, |
164 | int width, |
165 | int height) |
166 | { |
167 | /* Working in blocks increases cache efficiency, compared to reading |
168 | * or writing an entire column at once |
169 | */ |
170 | #define BLOCK_SIZE 16 |
171 | |
172 | int i0, j0; |
173 | |
174 | for (i0 = 0; i0 < width; i0 += BLOCK_SIZE) |
175 | for (j0 = 0; j0 < height; j0 += BLOCK_SIZE) |
176 | { |
177 | int max_j = MIN(j0 + BLOCK_SIZE, height); |
178 | int max_i = MIN(i0 + BLOCK_SIZE, width); |
179 | int i, j; |
180 | |
181 | for (i = i0; i < max_i; i++) |
182 | for (j = j0; j < max_j; j++) |
183 | dst_buffer[i * height + j] = src_buffer[j * width + i]; |
184 | } |
185 | #undef BLOCK_SIZE |
186 | } |
187 | |
188 | static void |
189 | _boxblur (guchar *buffer, |
190 | int width, |
191 | int height, |
192 | int radius, |
193 | GskBlurFlags flags) |
194 | { |
195 | guchar *flipped_buffer; |
196 | int d = get_box_filter_size (radius); |
197 | |
198 | flipped_buffer = g_malloc (n_bytes: width * height); |
199 | |
200 | if (flags & GSK_BLUR_Y) |
201 | { |
202 | /* Step 1: swap rows and columns */ |
203 | flip_buffer (dst_buffer: flipped_buffer, src_buffer: buffer, width, height); |
204 | |
205 | /* Step 2: blur rows (really columns) */ |
206 | blur_rows (dst_buffer: flipped_buffer, tmp_buffer: buffer, buffer_width: height, buffer_height: width, d); |
207 | |
208 | /* Step 3: swap rows and columns */ |
209 | flip_buffer (dst_buffer: buffer, src_buffer: flipped_buffer, width: height, height: width); |
210 | } |
211 | |
212 | if (flags & GSK_BLUR_X) |
213 | { |
214 | /* Step 4: blur rows */ |
215 | blur_rows (dst_buffer: buffer, tmp_buffer: flipped_buffer, buffer_width: width, buffer_height: height, d); |
216 | } |
217 | |
218 | g_free (mem: flipped_buffer); |
219 | } |
220 | |
221 | /* |
222 | * _gsk_cairo_blur_surface: |
223 | * @surface: a cairo image surface. |
224 | * @radius: the blur radius. |
225 | * |
226 | * Blurs the cairo image surface at the given radius. |
227 | */ |
228 | void |
229 | gsk_cairo_blur_surface (cairo_surface_t* surface, |
230 | double radius_d, |
231 | GskBlurFlags flags) |
232 | { |
233 | int radius = radius_d; |
234 | |
235 | g_return_if_fail (surface != NULL); |
236 | g_return_if_fail (cairo_surface_get_type (surface) == CAIRO_SURFACE_TYPE_IMAGE); |
237 | g_return_if_fail (cairo_image_surface_get_format (surface) == CAIRO_FORMAT_A8); |
238 | |
239 | /* The code doesn't actually do any blurring for radius 1, as it |
240 | * ends up with box filter size 1 */ |
241 | if (radius <= 1) |
242 | return; |
243 | |
244 | if ((flags & (GSK_BLUR_X|GSK_BLUR_Y)) == 0) |
245 | return; |
246 | |
247 | /* Before we mess with the surface, execute any pending drawing. */ |
248 | cairo_surface_flush (surface); |
249 | |
250 | _boxblur (buffer: cairo_image_surface_get_data (surface), |
251 | width: cairo_image_surface_get_stride (surface), |
252 | height: cairo_image_surface_get_height (surface), |
253 | radius, flags); |
254 | |
255 | /* Inform cairo we altered the surface contents. */ |
256 | cairo_surface_mark_dirty (surface); |
257 | } |
258 | |
259 | /*<private> |
260 | * gsk_cairo_blur_compute_pixels: |
261 | * @radius: the radius to compute the pixels for |
262 | * |
263 | * Computes the number of pixels necessary to extend an image in one |
264 | * direction to hold the image with shadow. |
265 | * |
266 | * This is just the number of pixels added by the blur radius, shadow |
267 | * offset and spread are not included. |
268 | * |
269 | * Much of this, the 3 * sqrt(2 * pi) / 4, is the known value for |
270 | * approximating a Gaussian using box blurs. This yields quite a good |
271 | * approximation for a Gaussian. Then we multiply this by 1.5 since our |
272 | * code wants the radius of the entire triple-box-blur kernel instead of |
273 | * the diameter of an individual box blur. For more details, see: |
274 | * http://www.w3.org/TR/SVG11/filters.html#feGaussianBlurElement |
275 | * https://bugzilla.mozilla.org/show_bug.cgi?id=590039#c19 |
276 | */ |
277 | int |
278 | gsk_cairo_blur_compute_pixels (double radius) |
279 | { |
280 | return floor (x: radius * GAUSSIAN_SCALE_FACTOR * 1.5 + 0.5); |
281 | } |
282 | |
283 | static gboolean |
284 | needs_blur (float radius, |
285 | GskBlurFlags blur_flags) |
286 | { |
287 | /* Neither blurring horizontal nor vertical means no blurring at all. */ |
288 | if ((blur_flags & (GSK_BLUR_X | GSK_BLUR_Y)) == 0) |
289 | return FALSE; |
290 | |
291 | /* The code doesn't actually do any blurring for radius 1, as it |
292 | * ends up with box filter size 1 */ |
293 | if (radius <= 1.0) |
294 | return FALSE; |
295 | |
296 | return TRUE; |
297 | } |
298 | |
299 | static const cairo_user_data_key_t original_cr_key; |
300 | |
301 | cairo_t * |
302 | gsk_cairo_blur_start_drawing (cairo_t *cr, |
303 | float radius, |
304 | GskBlurFlags blur_flags) |
305 | { |
306 | double clip_x1, clip_x2, clip_y1, clip_y2, clip_width, clip_height; |
307 | cairo_surface_t *surface; |
308 | cairo_t *blur_cr; |
309 | double clip_radius; |
310 | double x_scale, y_scale; |
311 | gboolean blur_x = (blur_flags & GSK_BLUR_X) != 0; |
312 | gboolean blur_y = (blur_flags & GSK_BLUR_Y) != 0; |
313 | |
314 | if (!needs_blur (radius, blur_flags)) |
315 | return cr; |
316 | |
317 | cairo_clip_extents (cr, x1: &clip_x1, y1: &clip_y1, x2: &clip_x2, y2: &clip_y2); |
318 | clip_width = clip_x2 - clip_x1; |
319 | clip_height = clip_y2 - clip_y1; |
320 | |
321 | clip_radius = gsk_cairo_blur_compute_pixels (radius); |
322 | |
323 | x_scale = y_scale = 1; |
324 | cairo_surface_get_device_scale (surface: cairo_get_target (cr), x_scale: &x_scale, y_scale: &y_scale); |
325 | |
326 | if (blur_flags & GSK_BLUR_REPEAT) |
327 | { |
328 | if (!blur_x) |
329 | clip_width = 1; |
330 | if (!blur_y) |
331 | clip_height = 1; |
332 | } |
333 | |
334 | /* Create a larger surface to center the blur. */ |
335 | surface = cairo_surface_create_similar_image (other: cairo_get_target (cr), |
336 | format: CAIRO_FORMAT_A8, |
337 | width: x_scale * (clip_width + (blur_x ? 2 * clip_radius : 0)), |
338 | height: y_scale * (clip_height + (blur_y ? 2 * clip_radius : 0))); |
339 | cairo_surface_set_device_scale (surface, x_scale, y_scale); |
340 | cairo_surface_set_device_offset (surface, |
341 | x_offset: x_scale * ((blur_x ? clip_radius : 0) - clip_x1), |
342 | y_offset: y_scale * ((blur_y ? clip_radius : 0) - clip_y1)); |
343 | |
344 | blur_cr = cairo_create (target: surface); |
345 | cairo_set_user_data (cr: blur_cr, key: &original_cr_key, user_data: cairo_reference (cr), destroy: (cairo_destroy_func_t) cairo_destroy); |
346 | |
347 | if (cairo_has_current_point (cr)) |
348 | { |
349 | double x, y; |
350 | |
351 | cairo_get_current_point (cr, x: &x, y: &y); |
352 | cairo_move_to (cr: blur_cr, x, y); |
353 | } |
354 | |
355 | return blur_cr; |
356 | } |
357 | |
358 | static void |
359 | mask_surface_repeat (cairo_t *cr, |
360 | cairo_surface_t *surface) |
361 | { |
362 | cairo_pattern_t *pattern; |
363 | |
364 | pattern = cairo_pattern_create_for_surface (surface); |
365 | cairo_pattern_set_extend (pattern, extend: CAIRO_EXTEND_REPEAT); |
366 | |
367 | cairo_mask (cr, pattern); |
368 | |
369 | cairo_pattern_destroy (pattern); |
370 | } |
371 | |
372 | cairo_t * |
373 | gsk_cairo_blur_finish_drawing (cairo_t *cr, |
374 | float radius, |
375 | const GdkRGBA *color, |
376 | GskBlurFlags blur_flags) |
377 | { |
378 | cairo_t *original_cr; |
379 | cairo_surface_t *surface; |
380 | double x_scale; |
381 | |
382 | if (!needs_blur (radius, blur_flags)) |
383 | return cr; |
384 | |
385 | original_cr = cairo_get_user_data (cr, key: &original_cr_key); |
386 | |
387 | /* Blur the surface. */ |
388 | surface = cairo_get_target (cr); |
389 | |
390 | x_scale = 1; |
391 | cairo_surface_get_device_scale (surface: cairo_get_target (cr), x_scale: &x_scale, NULL); |
392 | |
393 | gsk_cairo_blur_surface (surface, radius_d: x_scale * radius, flags: blur_flags); |
394 | |
395 | gdk_cairo_set_source_rgba (cr: original_cr, rgba: color); |
396 | if (blur_flags & GSK_BLUR_REPEAT) |
397 | mask_surface_repeat (cr: original_cr, surface); |
398 | else |
399 | cairo_mask_surface (cr: original_cr, surface, surface_x: 0, surface_y: 0); |
400 | |
401 | cairo_destroy (cr); |
402 | |
403 | cairo_surface_destroy (surface); |
404 | |
405 | return original_cr; |
406 | } |
407 | |
408 | |