resvg/filter/
box_blur.rs

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 = create_box_gauss(sigma_x as f32);
25    let boxes_vert = create_box_gauss(sigma_y as f32);
26    let mut backbuf = src.data.to_vec();
27    let mut backbuf = ImageRefMut::new(src.width, src.height, &mut backbuf);
28
29    for (box_size_horz, box_size_vert) in boxes_horz.iter().zip(boxes_vert.iter()) {
30        let radius_horz = ((box_size_horz - 1) / 2) as usize;
31        let radius_vert = ((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, 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}