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