svgfilters/
box_blur.rs

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