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 | |
8 | use super::ImageRefMut; |
9 | use rgb::RGBA8; |
10 | use std::cmp; |
11 | |
12 | const 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. |
23 | pub fn apply(sigma_x: f64, sigma_y: f64, mut src: ImageRefMut) { |
24 | let boxes_horz: [i32; 5] = create_box_gauss(sigma_x as f32); |
25 | let boxes_vert: [i32; 5] = create_box_gauss(sigma_y as f32); |
26 | let mut backbuf: Vec> = src.data.to_vec(); |
27 | let mut backbuf: ImageRefMut<'_> = ImageRefMut::new(src.width, src.height, &mut backbuf); |
28 | |
29 | for (box_size_horz: &i32, box_size_vert: &i32) in boxes_horz.iter().zip(boxes_vert.iter()) { |
30 | let radius_horz: usize = ((box_size_horz - 1) / 2) as usize; |
31 | let radius_vert: usize = ((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)] |
37 | fn 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 ] |
74 | fn 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, backbuf:frontbuf, frontbuf:backbuf); |
81 | box_blur_horz(blur_radius_horz, backbuf, frontbuf); |
82 | } |
83 | |
84 | #[inline ] |
85 | fn 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 ] |
210 | fn 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 ] |
343 | fn round(mut x: f32) -> f32 { |
344 | x += 12582912.0; |
345 | x -= 12582912.0; |
346 | x |
347 | } |
348 | |
349 | #[inline ] |
350 | fn sub(c1: u8, c2: u8) -> isize { |
351 | c1 as isize - c2 as isize |
352 | } |
353 | |