Coverage Report

Created: 2026-03-10 07:34

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