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 | |
9 | use super::ImageRefMut; |
10 | use rgb::RGBA8; |
11 | use std::cmp; |
12 | |
13 | const 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. |
24 | pub 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)] |
38 | fn 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 ] |
75 | fn 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 ] |
86 | fn 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 ] |
211 | fn 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 ] |
344 | fn round(mut x: f32) -> f32 { |
345 | x += 12582912.0; |
346 | x -= 12582912.0; |
347 | x |
348 | } |
349 | |
350 | #[inline ] |
351 | fn sub(c1: u8, c2: u8) -> isize { |
352 | c1 as isize - c2 as isize |
353 | } |
354 | |