Coverage Report

Created: 2026-09-02 07:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/image/src/imageops/mod.rs
Line
Count
Source
1
//! Image Processing Functions
2
use crate::math::Rect;
3
use crate::traits::{Lerp, Pixel, Primitive};
4
use crate::{GenericImage, GenericImageView, SubImage};
5
6
/// Affine transformations
7
pub use self::affine::{
8
    flip_horizontal, flip_horizontal_in, flip_horizontal_in_place, flip_vertical, flip_vertical_in,
9
    flip_vertical_in_place, rotate180, rotate180_in, rotate180_in_place, rotate270, rotate270_in,
10
    rotate90, rotate90_in,
11
};
12
13
pub use self::fast_blur::fast_blur;
14
15
pub use self::sample::{
16
    blur, blur_advanced, filter3x3, interpolate_bilinear, interpolate_nearest, resize,
17
    sample_bilinear, sample_nearest, thumbnail, unsharpen, FilterType, GaussianBlurParameters,
18
};
19
pub(crate) use sample::gaussian_blur_dyn_image;
20
21
/// Color operations
22
pub use self::colorops::{
23
    brighten, brighten_in_place, contrast, contrast_in_place, dither, grayscale, grayscale_alpha,
24
    grayscale_with_type, grayscale_with_type_alpha, huerotate, huerotate_in_place, index_colors,
25
    invert, BiLevel, ColorMap,
26
};
27
28
mod affine;
29
mod colorops;
30
pub(crate) mod fast_blur;
31
mod filter_1d;
32
pub(crate) mod resize;
33
mod sample;
34
35
/// Return a mutable view into an image
36
/// The coordinates set the position of the top left corner of the crop.
37
0
pub fn crop_mut<I: GenericImageView>(image: &mut I, rect: Rect) -> SubImage<&mut I> {
38
0
    SubImage::new(image, rect.shrink_to_bounds_of(image))
39
0
}
40
41
/// Return an immutable view into an image
42
/// The coordinates set the position of the top left corner of the crop.
43
0
pub fn crop<I: GenericImageView>(image: &I, rect: Rect) -> SubImage<&I> {
44
0
    SubImage::new(image, rect.shrink_to_bounds_of(image))
45
0
}
Unexecuted instantiation: image::imageops::crop::<image::images::buffer::ImageBuffer<image::color::Rgb<f32>, alloc::vec::Vec<f32>>>
Unexecuted instantiation: image::imageops::crop::<image::images::buffer::ImageBuffer<image::color::Rgb<u8>, alloc::vec::Vec<u8>>>
Unexecuted instantiation: image::imageops::crop::<image::images::buffer::ImageBuffer<image::color::Rgb<u16>, alloc::vec::Vec<u16>>>
Unexecuted instantiation: image::imageops::crop::<image::images::buffer::ImageBuffer<image::color::Luma<f32>, alloc::vec::Vec<f32>>>
Unexecuted instantiation: image::imageops::crop::<image::images::buffer::ImageBuffer<image::color::Luma<u8>, alloc::vec::Vec<u8>>>
Unexecuted instantiation: image::imageops::crop::<image::images::buffer::ImageBuffer<image::color::Luma<u16>, alloc::vec::Vec<u16>>>
Unexecuted instantiation: image::imageops::crop::<image::images::buffer::ImageBuffer<image::color::Rgba<f32>, alloc::vec::Vec<f32>>>
Unexecuted instantiation: image::imageops::crop::<image::images::buffer::ImageBuffer<image::color::Rgba<u8>, alloc::vec::Vec<u8>>>
Unexecuted instantiation: image::imageops::crop::<image::images::buffer::ImageBuffer<image::color::Rgba<u16>, alloc::vec::Vec<u16>>>
Unexecuted instantiation: image::imageops::crop::<image::images::buffer::ImageBuffer<image::color::LumaA<f32>, alloc::vec::Vec<f32>>>
Unexecuted instantiation: image::imageops::crop::<image::images::buffer::ImageBuffer<image::color::LumaA<u8>, alloc::vec::Vec<u8>>>
Unexecuted instantiation: image::imageops::crop::<image::images::buffer::ImageBuffer<image::color::LumaA<u16>, alloc::vec::Vec<u16>>>
46
47
/// Calculate the region that can be copied from top to bottom.
48
///
49
/// Given image size of bottom and top image, and a point at which we want to place the top image
50
/// onto the bottom image, how large can we be? Have to wary of the following issues:
51
/// * Top might be larger than bottom
52
/// * Overflows in the computation
53
/// * Coordinates could be completely out of bounds
54
///
55
/// The main idea is to make use of inequalities provided by the nature of `saturating_add` and
56
/// `saturating_sub`. These intrinsically validate that all resulting coordinates will be in bounds
57
/// for both images.
58
///
59
/// We want that all these coordinate accesses are safe:
60
/// 1. `bottom.get_pixel(x + [0..x_range), y + [0..y_range))`
61
/// 2. `top.get_pixel([0..x_range), [0..y_range))`
62
///
63
/// Proof that the function provides the necessary bounds for width. Note that all unaugmented math
64
/// operations are to be read in standard arithmetic, not integer arithmetic. Since no direct
65
/// integer arithmetic occurs in the implementation, this is unambiguous.
66
///
67
/// ```text
68
/// Three short notes/lemmata:
69
/// - Iff `(a - b) <= 0` then `a.saturating_sub(b) = 0`
70
/// - Iff `(a - b) >= 0` then `a.saturating_sub(b) = a - b`
71
/// - If  `a <= c` then `a.saturating_sub(b) <= c.saturating_sub(b)`
72
///
73
/// 1.1 We show that if `bottom_width <= x`, then `x_range = 0` therefore `x + [0..x_range)` is empty.
74
///
75
/// x_range
76
///  = (top_width.saturating_add(x).min(bottom_width)).saturating_sub(x)
77
/// <= bottom_width.saturating_sub(x)
78
///
79
/// bottom_width <= x
80
/// <==> bottom_width - x <= 0
81
/// <==> bottom_width.saturating_sub(x) = 0
82
///  ==> x_range <= 0
83
///  ==> x_range  = 0
84
///
85
/// 1.2 If `x < bottom_width` then `x + x_range < bottom_width`
86
///
87
/// x + x_range
88
/// <= x + bottom_width.saturating_sub(x)
89
///  = x + (bottom_width - x)
90
///  = bottom_width
91
///
92
/// 2. We show that `x_range <= top_width`
93
///
94
/// x_range
95
///  = (top_width.saturating_add(x).min(bottom_width)).saturating_sub(x)
96
/// <= top_width.saturating_add(x).saturating_sub(x)
97
/// <= (top_wdith + x).saturating_sub(x)
98
///  = top_width (due to `top_width >= 0` and `x >= 0`)
99
/// ```
100
///
101
/// Proof is the same for height.
102
#[must_use]
103
0
pub fn overlay_bounds(
104
0
    (bottom_width, bottom_height): (u32, u32),
105
0
    (top_width, top_height): (u32, u32),
106
0
    x: u32,
107
0
    y: u32,
108
0
) -> (u32, u32) {
109
0
    let x_range = top_width
110
0
        .saturating_add(x) // Calculate max coordinate
111
0
        .min(bottom_width) // Restrict to lower width
112
0
        .saturating_sub(x); // Determinate length from start `x`
113
0
    let y_range = top_height
114
0
        .saturating_add(y)
115
0
        .min(bottom_height)
116
0
        .saturating_sub(y);
117
0
    (x_range, y_range)
118
0
}
119
120
/// Calculate the region that can be copied from top to bottom.
121
///
122
/// Given image size of bottom and top image, and a point at which we want to place the top image
123
/// onto the bottom image, how large can we be? Have to wary of the following issues:
124
/// * Top might be larger than bottom
125
/// * Overflows in the computation
126
/// * Coordinates could be completely out of bounds
127
///
128
/// The returned value is of the form:
129
///
130
/// `(origin_bottom_x, origin_bottom_y, origin_top_x, origin_top_y, x_range, y_range)`
131
///
132
/// The main idea is to do computations on i64's and then clamp to image dimensions.
133
/// In particular, we want to ensure that all these coordinate accesses are safe:
134
/// 1. `bottom.get_pixel(origin_bottom_x + [0..x_range), origin_bottom_y + [0..y_range))`
135
/// 2. `top.get_pixel(origin_top_y + [0..x_range), origin_top_y + [0..y_range))`
136
0
fn overlay_bounds_ext(
137
0
    (bottom_width, bottom_height): (u32, u32),
138
0
    (top_width, top_height): (u32, u32),
139
0
    x: i64,
140
0
    y: i64,
141
0
) -> (u32, u32, u32, u32, u32, u32) {
142
    // Return a predictable value if the two images don't overlap at all.
143
0
    if x > i64::from(bottom_width)
144
0
        || y > i64::from(bottom_height)
145
0
        || x.saturating_add(i64::from(top_width)) <= 0
146
0
        || y.saturating_add(i64::from(top_height)) <= 0
147
    {
148
0
        return (0, 0, 0, 0, 0, 0);
149
0
    }
150
151
    // Find the maximum x and y coordinates in terms of the bottom image.
152
0
    let max_x = x.saturating_add(i64::from(top_width));
153
0
    let max_y = y.saturating_add(i64::from(top_height));
154
155
    // Clip the origin and maximum coordinates to the bounds of the bottom image.
156
    // Casting to a u32 is safe because both 0 and `bottom_{width,height}` fit
157
    // into 32-bits.
158
0
    let max_inbounds_x = max_x.clamp(0, i64::from(bottom_width)) as u32;
159
0
    let max_inbounds_y = max_y.clamp(0, i64::from(bottom_height)) as u32;
160
0
    let origin_bottom_x = x.clamp(0, i64::from(bottom_width)) as u32;
161
0
    let origin_bottom_y = y.clamp(0, i64::from(bottom_height)) as u32;
162
163
    // The range is the difference between the maximum inbounds coordinates and
164
    // the clipped origin. Unchecked subtraction is safe here because both are
165
    // always positive and `max_inbounds_{x,y}` >= `origin_{x,y}` due to
166
    // `top_{width,height}` being >= 0.
167
0
    let x_range = max_inbounds_x - origin_bottom_x;
168
0
    let y_range = max_inbounds_y - origin_bottom_y;
169
170
    // If x (or y) is negative, then the origin of the top image is shifted by -x (or -y).
171
0
    let origin_top_x = x.saturating_mul(-1).clamp(0, i64::from(top_width)) as u32;
172
0
    let origin_top_y = y.saturating_mul(-1).clamp(0, i64::from(top_height)) as u32;
173
174
0
    (
175
0
        origin_bottom_x,
176
0
        origin_bottom_y,
177
0
        origin_top_x,
178
0
        origin_top_y,
179
0
        x_range,
180
0
        y_range,
181
0
    )
182
0
}
183
184
/// Overlay an image at a given coordinate (x, y)
185
0
pub fn overlay<I, J>(bottom: &mut I, top: &J, x: i64, y: i64)
186
0
where
187
0
    I: GenericImage,
188
0
    J: GenericImageView<Pixel = I::Pixel>,
189
{
190
0
    let bottom_dims = bottom.dimensions();
191
0
    let top_dims = top.dimensions();
192
193
    // Crop our top image if we're going out of bounds
194
0
    let (origin_bottom_x, origin_bottom_y, origin_top_x, origin_top_y, range_width, range_height) =
195
0
        overlay_bounds_ext(bottom_dims, top_dims, x, y);
196
197
0
    for y in 0..range_height {
198
0
        for x in 0..range_width {
199
0
            let p = top.get_pixel(origin_top_x + x, origin_top_y + y);
200
0
            let mut bottom_pixel = bottom.get_pixel(origin_bottom_x + x, origin_bottom_y + y);
201
0
            bottom_pixel.blend(&p);
202
0
203
0
            bottom.put_pixel(origin_bottom_x + x, origin_bottom_y + y, bottom_pixel);
204
0
        }
205
    }
206
0
}
207
208
/// Tile an image by repeating it multiple times
209
///
210
/// # Examples
211
///
212
/// ```no_run
213
/// use image::RgbaImage;
214
///
215
/// let mut img = RgbaImage::new(1920, 1080);
216
/// let tile = image::open("tile.png").unwrap();
217
///
218
/// image::imageops::tile(&mut img, &tile);
219
/// img.save("tiled_wallpaper.png").unwrap();
220
/// ```
221
///
222
/// # Panics
223
///
224
/// Panics if either dimension of the top image is zero.
225
0
pub fn tile<I, J>(bottom: &mut I, top: &J)
226
0
where
227
0
    I: GenericImage,
228
0
    J: GenericImageView<Pixel = I::Pixel>,
229
{
230
0
    let (top_width, top_height) = top.dimensions();
231
0
    if top_width == 0 || top_height == 0 {
232
0
        panic!("Cannot tile with an image with zero width or height");
233
0
    }
234
235
0
    for x in (0..bottom.width()).step_by(top_width as usize) {
236
0
        for y in (0..bottom.height()).step_by(top_height as usize) {
237
0
            overlay(bottom, top, i64::from(x), i64::from(y));
238
0
        }
239
    }
240
0
}
241
242
/// Fill the image with a linear vertical gradient
243
///
244
/// This function assumes a linear color space.
245
///
246
/// # Examples
247
/// ```no_run
248
/// use image::{Rgba, RgbaImage, Pixel};
249
///
250
/// let mut img = RgbaImage::new(100, 100);
251
/// let start = Rgba::from_slice(&[0, 128, 0, 0]);
252
/// let end = Rgba::from_slice(&[255, 255, 255, 255]);
253
///
254
/// image::imageops::vertical_gradient(&mut img, start, end);
255
/// img.save("vertical_gradient.png").unwrap();
256
0
pub fn vertical_gradient<S, P, I>(img: &mut I, start: &P, stop: &P)
257
0
where
258
0
    I: GenericImage<Pixel = P>,
259
0
    P: Pixel<Subpixel = S>,
260
0
    S: Primitive + Lerp,
261
{
262
0
    for y in 0..img.height() {
263
0
        let pixel = start.map2(stop, |a, b| {
264
0
            let y = <S::Ratio as num_traits::NumCast>::from(y).unwrap();
265
0
            let height = <S::Ratio as num_traits::NumCast>::from(img.height() - 1).unwrap();
266
0
            S::lerp(a, b, y / height)
267
0
        });
268
269
0
        for x in 0..img.width() {
270
0
            img.put_pixel(x, y, pixel);
271
0
        }
272
    }
273
0
}
274
275
/// Fill the image with a linear horizontal gradient
276
///
277
/// This function assumes a linear color space.
278
///
279
/// # Examples
280
/// ```no_run
281
/// use image::{Rgba, RgbaImage, Pixel};
282
///
283
/// let mut img = RgbaImage::new(100, 100);
284
/// let start = Rgba::from_slice(&[0, 128, 0, 0]);
285
/// let end = Rgba::from_slice(&[255, 255, 255, 255]);
286
///
287
/// image::imageops::horizontal_gradient(&mut img, start, end);
288
/// img.save("horizontal_gradient.png").unwrap();
289
0
pub fn horizontal_gradient<S, P, I>(img: &mut I, start: &P, stop: &P)
290
0
where
291
0
    I: GenericImage<Pixel = P>,
292
0
    P: Pixel<Subpixel = S>,
293
0
    S: Primitive + Lerp,
294
{
295
0
    for x in 0..img.width() {
296
0
        let pixel = start.map2(stop, |a, b| {
297
0
            let x = <S::Ratio as num_traits::NumCast>::from(x).unwrap();
298
0
            let width = <S::Ratio as num_traits::NumCast>::from(img.width() - 1).unwrap();
299
0
            S::lerp(a, b, x / width)
300
0
        });
301
302
0
        for y in 0..img.height() {
303
0
            img.put_pixel(x, y, pixel);
304
0
        }
305
    }
306
0
}
307
308
/// Replace the contents of an image at a given coordinate (x, y)
309
0
pub fn replace<I, J>(bottom: &mut I, top: &J, x: i64, y: i64)
310
0
where
311
0
    I: GenericImage,
312
0
    J: GenericImageView<Pixel = I::Pixel>,
313
{
314
0
    let bottom_dims = bottom.dimensions();
315
0
    let top_dims = top.dimensions();
316
317
    // Crop our top image if we're going out of bounds
318
0
    let (origin_bottom_x, origin_bottom_y, origin_top_x, origin_top_y, range_width, range_height) =
319
0
        overlay_bounds_ext(bottom_dims, top_dims, x, y);
320
321
0
    for y in 0..range_height {
322
0
        for x in 0..range_width {
323
0
            let p = top.get_pixel(origin_top_x + x, origin_top_y + y);
324
0
            bottom.put_pixel(origin_bottom_x + x, origin_bottom_y + y, p);
325
0
        }
326
    }
327
0
}
328
329
#[cfg(test)]
330
mod tests {
331
332
    use super::*;
333
    use crate::color::Rgb;
334
    use crate::GrayAlphaImage;
335
    use crate::GrayImage;
336
    use crate::ImageBuffer;
337
    use crate::Rgb32FImage;
338
    use crate::RgbImage;
339
    use crate::RgbaImage;
340
341
    #[test]
342
    fn test_overlay_bounds_ext() {
343
        assert_eq!(
344
            overlay_bounds_ext((10, 10), (10, 10), 0, 0),
345
            (0, 0, 0, 0, 10, 10)
346
        );
347
        assert_eq!(
348
            overlay_bounds_ext((10, 10), (10, 10), 1, 0),
349
            (1, 0, 0, 0, 9, 10)
350
        );
351
        assert_eq!(
352
            overlay_bounds_ext((10, 10), (10, 10), 0, 11),
353
            (0, 0, 0, 0, 0, 0)
354
        );
355
        assert_eq!(
356
            overlay_bounds_ext((10, 10), (10, 10), -1, 0),
357
            (0, 0, 1, 0, 9, 10)
358
        );
359
        assert_eq!(
360
            overlay_bounds_ext((10, 10), (10, 10), -10, 0),
361
            (0, 0, 0, 0, 0, 0)
362
        );
363
        assert_eq!(
364
            overlay_bounds_ext((10, 10), (10, 10), 1i64 << 50, 0),
365
            (0, 0, 0, 0, 0, 0)
366
        );
367
        assert_eq!(
368
            overlay_bounds_ext((10, 10), (10, 10), -(1i64 << 50), 0),
369
            (0, 0, 0, 0, 0, 0)
370
        );
371
        assert_eq!(
372
            overlay_bounds_ext((10, 10), (u32::MAX, 10), 10 - i64::from(u32::MAX), 0),
373
            (0, 0, u32::MAX - 10, 0, 10, 10)
374
        );
375
    }
376
377
    #[test]
378
    /// Test that images written into other images works
379
    fn test_image_in_image() {
380
        let mut target = ImageBuffer::new(32, 32);
381
        let source = ImageBuffer::from_pixel(16, 16, Rgb([255u8, 0, 0]));
382
        overlay(&mut target, &source, 0, 0);
383
        assert!(*target.get_pixel(0, 0) == Rgb([255u8, 0, 0]));
384
        assert!(*target.get_pixel(15, 0) == Rgb([255u8, 0, 0]));
385
        assert!(*target.get_pixel(16, 0) == Rgb([0u8, 0, 0]));
386
        assert!(*target.get_pixel(0, 15) == Rgb([255u8, 0, 0]));
387
        assert!(*target.get_pixel(0, 16) == Rgb([0u8, 0, 0]));
388
    }
389
390
    #[test]
391
    /// Test that images written outside of a frame doesn't blow up
392
    fn test_image_in_image_outside_of_bounds() {
393
        let mut target = ImageBuffer::new(32, 32);
394
        let source = ImageBuffer::from_pixel(32, 32, Rgb([255u8, 0, 0]));
395
        overlay(&mut target, &source, 1, 1);
396
        assert!(*target.get_pixel(0, 0) == Rgb([0, 0, 0]));
397
        assert!(*target.get_pixel(1, 1) == Rgb([255u8, 0, 0]));
398
        assert!(*target.get_pixel(31, 31) == Rgb([255u8, 0, 0]));
399
    }
400
401
    #[test]
402
    /// Test that images written to coordinates out of the frame doesn't blow up
403
    /// (issue came up in #848)
404
    fn test_image_outside_image_no_wrap_around() {
405
        let mut target = ImageBuffer::new(32, 32);
406
        let source = ImageBuffer::from_pixel(32, 32, Rgb([255u8, 0, 0]));
407
        overlay(&mut target, &source, 33, 33);
408
        assert!(*target.get_pixel(0, 0) == Rgb([0, 0, 0]));
409
        assert!(*target.get_pixel(1, 1) == Rgb([0, 0, 0]));
410
        assert!(*target.get_pixel(31, 31) == Rgb([0, 0, 0]));
411
    }
412
413
    #[test]
414
    /// Test that overlaying a transparent image doesn't change the bottom image
415
    /// (issue #2533)
416
    fn test_image_overlay_transparent() {
417
        let color = crate::Rgba([45, 57, 82, 200]);
418
        let mut target = RgbaImage::from_pixel(3, 3, color);
419
        let source = RgbaImage::new(3, 3);
420
        overlay(&mut target, &source, 0, 0);
421
        let color = *target.get_pixel(0, 0);
422
423
        assert_eq!(*target.get_pixel(0, 0), color);
424
    }
425
426
    #[test]
427
    /// Test that images written to coordinates with overflow works
428
    fn test_image_coordinate_overflow() {
429
        let mut target = ImageBuffer::new(16, 16);
430
        let source = ImageBuffer::from_pixel(32, 32, Rgb([255u8, 0, 0]));
431
        // Overflows to 'sane' coordinates but top is larger than bot.
432
        overlay(
433
            &mut target,
434
            &source,
435
            i64::from(u32::MAX - 31),
436
            i64::from(u32::MAX - 31),
437
        );
438
        assert!(*target.get_pixel(0, 0) == Rgb([0, 0, 0]));
439
        assert!(*target.get_pixel(1, 1) == Rgb([0, 0, 0]));
440
        assert!(*target.get_pixel(15, 15) == Rgb([0, 0, 0]));
441
    }
442
443
    use super::{horizontal_gradient, vertical_gradient};
444
445
    #[test]
446
    /// Test that horizontal gradients are correctly generated
447
    fn test_image_horizontal_gradient_limits() {
448
        let mut img = ImageBuffer::new(100, 1);
449
450
        let start = Rgb([0u8, 128, 0]);
451
        let end = Rgb([255u8, 255, 255]);
452
453
        horizontal_gradient(&mut img, &start, &end);
454
455
        assert_eq!(img.get_pixel(0, 0), &start);
456
        assert_eq!(img.get_pixel(img.width() - 1, 0), &end);
457
    }
458
459
    #[test]
460
    /// Test that vertical gradients are correctly generated
461
    fn test_image_vertical_gradient_limits() {
462
        let mut img = ImageBuffer::new(1, 100);
463
464
        let start = Rgb([0u8, 128, 0]);
465
        let end = Rgb([255u8, 255, 255]);
466
467
        vertical_gradient(&mut img, &start, &end);
468
469
        assert_eq!(img.get_pixel(0, 0), &start);
470
        assert_eq!(img.get_pixel(0, img.height() - 1), &end);
471
    }
472
473
    #[test]
474
    /// Test blur doesn't panic when passed 0.0
475
    fn test_blur_zero() {
476
        let image = RgbaImage::new(50, 50);
477
        let _ = blur(&image, 0.);
478
    }
479
480
    #[test]
481
    /// Test fast blur doesn't panic when passed 0.0
482
    fn test_fast_blur_zero() {
483
        let image = RgbaImage::new(50, 50);
484
        let _ = fast_blur(&image, 0.0);
485
    }
486
487
    #[test]
488
    /// Test fast blur doesn't panic when passed negative numbers
489
    fn test_fast_blur_negative() {
490
        let image = RgbaImage::new(50, 50);
491
        let _ = fast_blur(&image, -1.0);
492
    }
493
494
    #[test]
495
    /// Test fast blur doesn't panic when sigma produces boxes larger than the image
496
    fn test_fast_large_sigma() {
497
        let image = RgbaImage::new(1, 1);
498
        let _ = fast_blur(&image, 50.0);
499
    }
500
501
    #[test]
502
    /// Test blur doesn't panic when passed an empty image (any direction)
503
    fn test_fast_blur_empty() {
504
        let image = RgbaImage::new(0, 0);
505
        let _ = fast_blur(&image, 1.0);
506
        let image = RgbaImage::new(20, 0);
507
        let _ = fast_blur(&image, 1.0);
508
        let image = RgbaImage::new(0, 20);
509
        let _ = fast_blur(&image, 1.0);
510
    }
511
512
    #[test]
513
    /// Test fast blur works with 3 channels
514
    fn test_fast_blur_3_channels() {
515
        let image = RgbImage::new(50, 50);
516
        let _ = fast_blur(&image, 1.0);
517
    }
518
519
    #[test]
520
    /// Test fast blur works with 2 channels
521
    fn test_fast_blur_2_channels() {
522
        let image = GrayAlphaImage::new(50, 50);
523
        let _ = fast_blur(&image, 1.0);
524
    }
525
526
    #[test]
527
    /// Test fast blur works with 1 channel
528
    fn test_fast_blur_1_channels() {
529
        let image = GrayImage::new(50, 50);
530
        let _ = fast_blur(&image, 1.0);
531
    }
532
533
    #[test]
534
    #[cfg(feature = "tiff")]
535
    fn fast_blur_approximates_gaussian_blur_well() {
536
        let path = concat!(
537
            env!("CARGO_MANIFEST_DIR"),
538
            "/tests/images/tiff/testsuite/rgb-3c-16b.tiff"
539
        );
540
        let image = crate::open(path).unwrap();
541
        let image_blurred_gauss = image
542
            .blur_advanced(GaussianBlurParameters::new_from_sigma(50.0))
543
            .to_rgb8();
544
        let image_blurred_gauss_bytes = image_blurred_gauss.subpixels();
545
        let image_blurred_fast = image.fast_blur(50.0).to_rgb8();
546
        let image_blurred_fast_bytes = image_blurred_fast.subpixels();
547
548
        let error = image_blurred_gauss_bytes
549
            .iter()
550
            .zip(image_blurred_fast_bytes.iter())
551
            .map(|(a, b)| (f32::from(*a) - f32::from(*b)) / f32::from(*a))
552
            .sum::<f32>()
553
            / (image_blurred_gauss_bytes.len() as f32);
554
        assert!(error < 0.05);
555
    }
556
557
    /// Test that thumbnails are created without with correct color rounding.
558
    #[test]
559
    fn test_image_thumbnail() {
560
        let all_black_u8 = GrayImage::new(16, 16);
561
        assert_eq!(thumbnail(&all_black_u8, 1, 1).get_pixel(0, 0).0, [0_u8]);
562
563
        let all_black_f32 = Rgb32FImage::new(16, 16);
564
        assert_eq!(
565
            thumbnail(&all_black_f32, 1, 1).get_pixel(0, 0).0,
566
            [0.0_f32, 0.0_f32, 0.0_f32]
567
        );
568
569
        // this has an average of 0.5 which should round up to 1
570
        let checker = GrayImage::from_vec(2, 2, vec![0, 1, 0, 1]).unwrap();
571
        assert_eq!(thumbnail(&checker, 1, 1).get_pixel(0, 0).0, [1_u8]);
572
    }
573
}