1// Copyright 2020 the Resvg Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4// Based on https://github.com/fschutt/fastblur
5
6#![allow(clippy::needless_range_loop)]
7
8use super::ImageRefMut;
9use rgb::RGBA8;
10use std::cmp;
11
12const STEPS: usize = 5;
13
14/// Applies a box blur.
15///
16/// Input image pixels should have a **premultiplied alpha**.
17///
18/// A negative or zero `sigma_x`/`sigma_y` will disable the blur along that axis.
19///
20/// # Allocations
21///
22/// This method will allocate a copy of the `src` image as a back buffer.
23pub fn apply(sigma_x: f64, sigma_y: f64, mut src: ImageRefMut) {
24 let boxes_horz: [i32; 5] = create_box_gauss(sigma_x as f32);
25 let boxes_vert: [i32; 5] = create_box_gauss(sigma_y as f32);
26 let mut backbuf: Vec> = src.data.to_vec();
27 let mut backbuf: ImageRefMut<'_> = ImageRefMut::new(src.width, src.height, &mut backbuf);
28
29 for (box_size_horz: &i32, box_size_vert: &i32) in boxes_horz.iter().zip(boxes_vert.iter()) {
30 let radius_horz: usize = ((box_size_horz - 1) / 2) as usize;
31 let radius_vert: usize = ((box_size_vert - 1) / 2) as usize;
32 box_blur_impl(radius_horz, radius_vert, &mut backbuf, &mut src);
33 }
34}
35
36#[inline(never)]
37fn create_box_gauss(sigma: f32) -> [i32; STEPS] {
38 if sigma > 0.0 {
39 let n_float = STEPS as f32;
40
41 // Ideal averaging filter width
42 let w_ideal = (12.0 * sigma * sigma / n_float).sqrt() + 1.0;
43 let mut wl = w_ideal.floor() as i32;
44 if wl % 2 == 0 {
45 wl -= 1;
46 }
47
48 let wu = wl + 2;
49
50 let wl_float = wl as f32;
51 let m_ideal = (12.0 * sigma * sigma
52 - n_float * wl_float * wl_float
53 - 4.0 * n_float * wl_float
54 - 3.0 * n_float)
55 / (-4.0 * wl_float - 4.0);
56 let m = m_ideal.round() as usize;
57
58 let mut sizes = [0; STEPS];
59 for i in 0..STEPS {
60 if i < m {
61 sizes[i] = wl;
62 } else {
63 sizes[i] = wu;
64 }
65 }
66
67 sizes
68 } else {
69 [1; STEPS]
70 }
71}
72
73#[inline]
74fn box_blur_impl(
75 blur_radius_horz: usize,
76 blur_radius_vert: usize,
77 backbuf: &mut ImageRefMut,
78 frontbuf: &mut ImageRefMut,
79) {
80 box_blur_vert(blur_radius_vert, backbuf:frontbuf, frontbuf:backbuf);
81 box_blur_horz(blur_radius_horz, backbuf, frontbuf);
82}
83
84#[inline]
85fn box_blur_vert(blur_radius: usize, backbuf: &ImageRefMut, frontbuf: &mut ImageRefMut) {
86 if blur_radius == 0 {
87 frontbuf.data.copy_from_slice(backbuf.data);
88 return;
89 }
90
91 let width = backbuf.width as usize;
92 let height = backbuf.height as usize;
93
94 let iarr = 1.0 / (blur_radius + blur_radius + 1) as f32;
95 let blur_radius_prev = blur_radius as isize - height as isize;
96 let blur_radius_next = blur_radius as isize + 1;
97
98 for i in 0..width {
99 let col_start = i; //inclusive
100 let col_end = i + width * (height - 1); //inclusive
101 let mut ti = i;
102 let mut li = ti;
103 let mut ri = ti + blur_radius * width;
104
105 let fv = RGBA8::default();
106 let lv = RGBA8::default();
107
108 let mut val_r = blur_radius_next * (fv.r as isize);
109 let mut val_g = blur_radius_next * (fv.g as isize);
110 let mut val_b = blur_radius_next * (fv.b as isize);
111 let mut val_a = blur_radius_next * (fv.a as isize);
112
113 // Get the pixel at the specified index, or the first pixel of the column
114 // if the index is beyond the top edge of the image
115 let get_top = |i| {
116 if i < col_start {
117 fv
118 } else {
119 backbuf.data[i]
120 }
121 };
122
123 // Get the pixel at the specified index, or the last pixel of the column
124 // if the index is beyond the bottom edge of the image
125 let get_bottom = |i| {
126 if i > col_end {
127 lv
128 } else {
129 backbuf.data[i]
130 }
131 };
132
133 for j in 0..cmp::min(blur_radius, height) {
134 let bb = backbuf.data[ti + j * width];
135 val_r += bb.r as isize;
136 val_g += bb.g as isize;
137 val_b += bb.b as isize;
138 val_a += bb.a as isize;
139 }
140 if blur_radius > height {
141 val_r += blur_radius_prev * (lv.r as isize);
142 val_g += blur_radius_prev * (lv.g as isize);
143 val_b += blur_radius_prev * (lv.b as isize);
144 val_a += blur_radius_prev * (lv.a as isize);
145 }
146
147 for _ in 0..cmp::min(height, blur_radius + 1) {
148 let bb = get_bottom(ri);
149 ri += width;
150 val_r += sub(bb.r, fv.r);
151 val_g += sub(bb.g, fv.g);
152 val_b += sub(bb.b, fv.b);
153 val_a += sub(bb.a, fv.a);
154
155 frontbuf.data[ti] = RGBA8 {
156 r: round(val_r as f32 * iarr) as u8,
157 g: round(val_g as f32 * iarr) as u8,
158 b: round(val_b as f32 * iarr) as u8,
159 a: round(val_a as f32 * iarr) as u8,
160 };
161 ti += width;
162 }
163
164 if height <= blur_radius {
165 // otherwise `(height - blur_radius)` will underflow
166 continue;
167 }
168
169 for _ in (blur_radius + 1)..(height - blur_radius) {
170 let bb1 = backbuf.data[ri];
171 ri += width;
172 let bb2 = backbuf.data[li];
173 li += width;
174
175 val_r += sub(bb1.r, bb2.r);
176 val_g += sub(bb1.g, bb2.g);
177 val_b += sub(bb1.b, bb2.b);
178 val_a += sub(bb1.a, bb2.a);
179
180 frontbuf.data[ti] = RGBA8 {
181 r: round(val_r as f32 * iarr) as u8,
182 g: round(val_g as f32 * iarr) as u8,
183 b: round(val_b as f32 * iarr) as u8,
184 a: round(val_a as f32 * iarr) as u8,
185 };
186 ti += width;
187 }
188
189 for _ in 0..cmp::min(height - blur_radius - 1, blur_radius) {
190 let bb = get_top(li);
191 li += width;
192
193 val_r += sub(lv.r, bb.r);
194 val_g += sub(lv.g, bb.g);
195 val_b += sub(lv.b, bb.b);
196 val_a += sub(lv.a, bb.a);
197
198 frontbuf.data[ti] = RGBA8 {
199 r: round(val_r as f32 * iarr) as u8,
200 g: round(val_g as f32 * iarr) as u8,
201 b: round(val_b as f32 * iarr) as u8,
202 a: round(val_a as f32 * iarr) as u8,
203 };
204 ti += width;
205 }
206 }
207}
208
209#[inline]
210fn box_blur_horz(blur_radius: usize, backbuf: &ImageRefMut, frontbuf: &mut ImageRefMut) {
211 if blur_radius == 0 {
212 frontbuf.data.copy_from_slice(backbuf.data);
213 return;
214 }
215
216 let width = backbuf.width as usize;
217 let height = backbuf.height as usize;
218
219 let iarr = 1.0 / (blur_radius + blur_radius + 1) as f32;
220 let blur_radius_prev = blur_radius as isize - width as isize;
221 let blur_radius_next = blur_radius as isize + 1;
222
223 for i in 0..height {
224 let row_start = i * width; // inclusive
225 let row_end = (i + 1) * width - 1; // inclusive
226 let mut ti = i * width; // VERTICAL: $i;
227 let mut li = ti;
228 let mut ri = ti + blur_radius;
229
230 let fv = RGBA8::default();
231 let lv = RGBA8::default();
232
233 let mut val_r = blur_radius_next * (fv.r as isize);
234 let mut val_g = blur_radius_next * (fv.g as isize);
235 let mut val_b = blur_radius_next * (fv.b as isize);
236 let mut val_a = blur_radius_next * (fv.a as isize);
237
238 // Get the pixel at the specified index, or the first pixel of the row
239 // if the index is beyond the left edge of the image
240 let get_left = |i| {
241 if i < row_start {
242 fv
243 } else {
244 backbuf.data[i]
245 }
246 };
247
248 // Get the pixel at the specified index, or the last pixel of the row
249 // if the index is beyond the right edge of the image
250 let get_right = |i| {
251 if i > row_end {
252 lv
253 } else {
254 backbuf.data[i]
255 }
256 };
257
258 for j in 0..cmp::min(blur_radius, width) {
259 let bb = backbuf.data[ti + j]; // VERTICAL: ti + j * width
260 val_r += bb.r as isize;
261 val_g += bb.g as isize;
262 val_b += bb.b as isize;
263 val_a += bb.a as isize;
264 }
265 if blur_radius > width {
266 val_r += blur_radius_prev * (lv.r as isize);
267 val_g += blur_radius_prev * (lv.g as isize);
268 val_b += blur_radius_prev * (lv.b as isize);
269 val_a += blur_radius_prev * (lv.a as isize);
270 }
271
272 // Process the left side where we need pixels from beyond the left edge
273 for _ in 0..cmp::min(width, blur_radius + 1) {
274 let bb = get_right(ri);
275 ri += 1;
276 val_r += sub(bb.r, fv.r);
277 val_g += sub(bb.g, fv.g);
278 val_b += sub(bb.b, fv.b);
279 val_a += sub(bb.a, fv.a);
280
281 frontbuf.data[ti] = RGBA8 {
282 r: round(val_r as f32 * iarr) as u8,
283 g: round(val_g as f32 * iarr) as u8,
284 b: round(val_b as f32 * iarr) as u8,
285 a: round(val_a as f32 * iarr) as u8,
286 };
287 ti += 1; // VERTICAL : ti += width, same with the other areas
288 }
289
290 if width <= blur_radius {
291 // otherwise `(width - blur_radius)` will underflow
292 continue;
293 }
294
295 // Process the middle where we know we won't bump into borders
296 // without the extra indirection of get_left/get_right. This is faster.
297 for _ in (blur_radius + 1)..(width - blur_radius) {
298 let bb1 = backbuf.data[ri];
299 ri += 1;
300 let bb2 = backbuf.data[li];
301 li += 1;
302
303 val_r += sub(bb1.r, bb2.r);
304 val_g += sub(bb1.g, bb2.g);
305 val_b += sub(bb1.b, bb2.b);
306 val_a += sub(bb1.a, bb2.a);
307
308 frontbuf.data[ti] = RGBA8 {
309 r: round(val_r as f32 * iarr) as u8,
310 g: round(val_g as f32 * iarr) as u8,
311 b: round(val_b as f32 * iarr) as u8,
312 a: round(val_a as f32 * iarr) as u8,
313 };
314 ti += 1;
315 }
316
317 // Process the right side where we need pixels from beyond the right edge
318 for _ in 0..cmp::min(width - blur_radius - 1, blur_radius) {
319 let bb = get_left(li);
320 li += 1;
321
322 val_r += sub(lv.r, bb.r);
323 val_g += sub(lv.g, bb.g);
324 val_b += sub(lv.b, bb.b);
325 val_a += sub(lv.a, bb.a);
326
327 frontbuf.data[ti] = RGBA8 {
328 r: round(val_r as f32 * iarr) as u8,
329 g: round(val_g as f32 * iarr) as u8,
330 b: round(val_b as f32 * iarr) as u8,
331 a: round(val_a as f32 * iarr) as u8,
332 };
333 ti += 1;
334 }
335 }
336}
337
338/// Fast rounding for x <= 2^23.
339/// This is orders of magnitude faster than built-in rounding intrinsic.
340///
341/// Source: https://stackoverflow.com/a/42386149/585725
342#[inline]
343fn round(mut x: f32) -> f32 {
344 x += 12582912.0;
345 x -= 12582912.0;
346 x
347}
348
349#[inline]
350fn sub(c1: u8, c2: u8) -> isize {
351 c1 as isize - c2 as isize
352}
353