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: [i32; 5] = create_box_gauss(sigma_x as f32);
26 let boxes_vert: [i32; 5] = create_box_gauss(sigma_y as f32);
27 let mut backbuf: Vec> = src.data.to_vec();
28 let mut backbuf: ImageRefMut<'_> = ImageRefMut::new(src.width, src.height, &mut backbuf);
29
30 for (box_size_horz: &i32, box_size_vert: &i32) in boxes_horz.iter().zip(boxes_vert.iter()) {
31 let radius_horz: usize = ((box_size_horz - 1) / 2) as usize;
32 let radius_vert: usize = ((box_size_vert - 1) / 2) as usize;
33 box_blur_impl(blur_radius_horz:radius_horz, blur_radius_vert: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, backbuf:frontbuf, 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}
354