Coverage Report

Created: 2026-04-12 07:31

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
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<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>>>
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
/// ```no_run
212
/// use image::RgbaImage;
213
///
214
/// let mut img = RgbaImage::new(1920, 1080);
215
/// let tile = image::open("tile.png").unwrap();
216
///
217
/// image::imageops::tile(&mut img, &tile);
218
/// img.save("tiled_wallpaper.png").unwrap();
219
/// ```
220
0
pub fn tile<I, J>(bottom: &mut I, top: &J)
221
0
where
222
0
    I: GenericImage,
223
0
    J: GenericImageView<Pixel = I::Pixel>,
224
{
225
0
    for x in (0..bottom.width()).step_by(top.width() as usize) {
226
0
        for y in (0..bottom.height()).step_by(top.height() as usize) {
227
0
            overlay(bottom, top, i64::from(x), i64::from(y));
228
0
        }
229
    }
230
0
}
231
232
/// Fill the image with a linear vertical gradient
233
///
234
/// This function assumes a linear color space.
235
///
236
/// # Examples
237
/// ```no_run
238
/// use image::{Rgba, RgbaImage, Pixel};
239
///
240
/// let mut img = RgbaImage::new(100, 100);
241
/// let start = Rgba::from_slice(&[0, 128, 0, 0]);
242
/// let end = Rgba::from_slice(&[255, 255, 255, 255]);
243
///
244
/// image::imageops::vertical_gradient(&mut img, start, end);
245
/// img.save("vertical_gradient.png").unwrap();
246
0
pub fn vertical_gradient<S, P, I>(img: &mut I, start: &P, stop: &P)
247
0
where
248
0
    I: GenericImage<Pixel = P>,
249
0
    P: Pixel<Subpixel = S>,
250
0
    S: Primitive + Lerp,
251
{
252
0
    for y in 0..img.height() {
253
0
        let pixel = start.map2(stop, |a, b| {
254
0
            let y = <S::Ratio as num_traits::NumCast>::from(y).unwrap();
255
0
            let height = <S::Ratio as num_traits::NumCast>::from(img.height() - 1).unwrap();
256
0
            S::lerp(a, b, y / height)
257
0
        });
258
259
0
        for x in 0..img.width() {
260
0
            img.put_pixel(x, y, pixel);
261
0
        }
262
    }
263
0
}
264
265
/// Fill the image with a linear horizontal gradient
266
///
267
/// This function assumes a linear color space.
268
///
269
/// # Examples
270
/// ```no_run
271
/// use image::{Rgba, RgbaImage, Pixel};
272
///
273
/// let mut img = RgbaImage::new(100, 100);
274
/// let start = Rgba::from_slice(&[0, 128, 0, 0]);
275
/// let end = Rgba::from_slice(&[255, 255, 255, 255]);
276
///
277
/// image::imageops::horizontal_gradient(&mut img, start, end);
278
/// img.save("horizontal_gradient.png").unwrap();
279
0
pub fn horizontal_gradient<S, P, I>(img: &mut I, start: &P, stop: &P)
280
0
where
281
0
    I: GenericImage<Pixel = P>,
282
0
    P: Pixel<Subpixel = S>,
283
0
    S: Primitive + Lerp,
284
{
285
0
    for x in 0..img.width() {
286
0
        let pixel = start.map2(stop, |a, b| {
287
0
            let x = <S::Ratio as num_traits::NumCast>::from(x).unwrap();
288
0
            let width = <S::Ratio as num_traits::NumCast>::from(img.width() - 1).unwrap();
289
0
            S::lerp(a, b, x / width)
290
0
        });
291
292
0
        for y in 0..img.height() {
293
0
            img.put_pixel(x, y, pixel);
294
0
        }
295
    }
296
0
}
297
298
/// Replace the contents of an image at a given coordinate (x, y)
299
0
pub fn replace<I, J>(bottom: &mut I, top: &J, x: i64, y: i64)
300
0
where
301
0
    I: GenericImage,
302
0
    J: GenericImageView<Pixel = I::Pixel>,
303
{
304
0
    let bottom_dims = bottom.dimensions();
305
0
    let top_dims = top.dimensions();
306
307
    // Crop our top image if we're going out of bounds
308
0
    let (origin_bottom_x, origin_bottom_y, origin_top_x, origin_top_y, range_width, range_height) =
309
0
        overlay_bounds_ext(bottom_dims, top_dims, x, y);
310
311
0
    for y in 0..range_height {
312
0
        for x in 0..range_width {
313
0
            let p = top.get_pixel(origin_top_x + x, origin_top_y + y);
314
0
            bottom.put_pixel(origin_bottom_x + x, origin_bottom_y + y, p);
315
0
        }
316
    }
317
0
}
318
319
#[cfg(test)]
320
mod tests {
321
322
    use super::*;
323
    use crate::color::Rgb;
324
    use crate::GrayAlphaImage;
325
    use crate::GrayImage;
326
    use crate::ImageBuffer;
327
    use crate::Rgb32FImage;
328
    use crate::RgbImage;
329
    use crate::RgbaImage;
330
331
    #[test]
332
    fn test_overlay_bounds_ext() {
333
        assert_eq!(
334
            overlay_bounds_ext((10, 10), (10, 10), 0, 0),
335
            (0, 0, 0, 0, 10, 10)
336
        );
337
        assert_eq!(
338
            overlay_bounds_ext((10, 10), (10, 10), 1, 0),
339
            (1, 0, 0, 0, 9, 10)
340
        );
341
        assert_eq!(
342
            overlay_bounds_ext((10, 10), (10, 10), 0, 11),
343
            (0, 0, 0, 0, 0, 0)
344
        );
345
        assert_eq!(
346
            overlay_bounds_ext((10, 10), (10, 10), -1, 0),
347
            (0, 0, 1, 0, 9, 10)
348
        );
349
        assert_eq!(
350
            overlay_bounds_ext((10, 10), (10, 10), -10, 0),
351
            (0, 0, 0, 0, 0, 0)
352
        );
353
        assert_eq!(
354
            overlay_bounds_ext((10, 10), (10, 10), 1i64 << 50, 0),
355
            (0, 0, 0, 0, 0, 0)
356
        );
357
        assert_eq!(
358
            overlay_bounds_ext((10, 10), (10, 10), -(1i64 << 50), 0),
359
            (0, 0, 0, 0, 0, 0)
360
        );
361
        assert_eq!(
362
            overlay_bounds_ext((10, 10), (u32::MAX, 10), 10 - i64::from(u32::MAX), 0),
363
            (0, 0, u32::MAX - 10, 0, 10, 10)
364
        );
365
    }
366
367
    #[test]
368
    /// Test that images written into other images works
369
    fn test_image_in_image() {
370
        let mut target = ImageBuffer::new(32, 32);
371
        let source = ImageBuffer::from_pixel(16, 16, Rgb([255u8, 0, 0]));
372
        overlay(&mut target, &source, 0, 0);
373
        assert!(*target.get_pixel(0, 0) == Rgb([255u8, 0, 0]));
374
        assert!(*target.get_pixel(15, 0) == Rgb([255u8, 0, 0]));
375
        assert!(*target.get_pixel(16, 0) == Rgb([0u8, 0, 0]));
376
        assert!(*target.get_pixel(0, 15) == Rgb([255u8, 0, 0]));
377
        assert!(*target.get_pixel(0, 16) == Rgb([0u8, 0, 0]));
378
    }
379
380
    #[test]
381
    /// Test that images written outside of a frame doesn't blow up
382
    fn test_image_in_image_outside_of_bounds() {
383
        let mut target = ImageBuffer::new(32, 32);
384
        let source = ImageBuffer::from_pixel(32, 32, Rgb([255u8, 0, 0]));
385
        overlay(&mut target, &source, 1, 1);
386
        assert!(*target.get_pixel(0, 0) == Rgb([0, 0, 0]));
387
        assert!(*target.get_pixel(1, 1) == Rgb([255u8, 0, 0]));
388
        assert!(*target.get_pixel(31, 31) == Rgb([255u8, 0, 0]));
389
    }
390
391
    #[test]
392
    /// Test that images written to coordinates out of the frame doesn't blow up
393
    /// (issue came up in #848)
394
    fn test_image_outside_image_no_wrap_around() {
395
        let mut target = ImageBuffer::new(32, 32);
396
        let source = ImageBuffer::from_pixel(32, 32, Rgb([255u8, 0, 0]));
397
        overlay(&mut target, &source, 33, 33);
398
        assert!(*target.get_pixel(0, 0) == Rgb([0, 0, 0]));
399
        assert!(*target.get_pixel(1, 1) == Rgb([0, 0, 0]));
400
        assert!(*target.get_pixel(31, 31) == Rgb([0, 0, 0]));
401
    }
402
403
    #[test]
404
    /// Test that overlaying a transparent image doesn't change the bottom image
405
    /// (issue #2533)
406
    fn test_image_overlay_transparent() {
407
        let color = crate::Rgba([45, 57, 82, 200]);
408
        let mut target = RgbaImage::from_pixel(3, 3, color);
409
        let source = RgbaImage::new(3, 3);
410
        overlay(&mut target, &source, 0, 0);
411
        let color = *target.get_pixel(0, 0);
412
413
        assert_eq!(*target.get_pixel(0, 0), color);
414
    }
415
416
    #[test]
417
    /// Test that images written to coordinates with overflow works
418
    fn test_image_coordinate_overflow() {
419
        let mut target = ImageBuffer::new(16, 16);
420
        let source = ImageBuffer::from_pixel(32, 32, Rgb([255u8, 0, 0]));
421
        // Overflows to 'sane' coordinates but top is larger than bot.
422
        overlay(
423
            &mut target,
424
            &source,
425
            i64::from(u32::MAX - 31),
426
            i64::from(u32::MAX - 31),
427
        );
428
        assert!(*target.get_pixel(0, 0) == Rgb([0, 0, 0]));
429
        assert!(*target.get_pixel(1, 1) == Rgb([0, 0, 0]));
430
        assert!(*target.get_pixel(15, 15) == Rgb([0, 0, 0]));
431
    }
432
433
    use super::{horizontal_gradient, vertical_gradient};
434
435
    #[test]
436
    /// Test that horizontal gradients are correctly generated
437
    fn test_image_horizontal_gradient_limits() {
438
        let mut img = ImageBuffer::new(100, 1);
439
440
        let start = Rgb([0u8, 128, 0]);
441
        let end = Rgb([255u8, 255, 255]);
442
443
        horizontal_gradient(&mut img, &start, &end);
444
445
        assert_eq!(img.get_pixel(0, 0), &start);
446
        assert_eq!(img.get_pixel(img.width() - 1, 0), &end);
447
    }
448
449
    #[test]
450
    /// Test that vertical gradients are correctly generated
451
    fn test_image_vertical_gradient_limits() {
452
        let mut img = ImageBuffer::new(1, 100);
453
454
        let start = Rgb([0u8, 128, 0]);
455
        let end = Rgb([255u8, 255, 255]);
456
457
        vertical_gradient(&mut img, &start, &end);
458
459
        assert_eq!(img.get_pixel(0, 0), &start);
460
        assert_eq!(img.get_pixel(0, img.height() - 1), &end);
461
    }
462
463
    #[test]
464
    /// Test blur doesn't panic when passed 0.0
465
    fn test_blur_zero() {
466
        let image = RgbaImage::new(50, 50);
467
        let _ = blur(&image, 0.);
468
    }
469
470
    #[test]
471
    /// Test fast blur doesn't panic when passed 0.0
472
    fn test_fast_blur_zero() {
473
        let image = RgbaImage::new(50, 50);
474
        let _ = fast_blur(&image, 0.0);
475
    }
476
477
    #[test]
478
    /// Test fast blur doesn't panic when passed negative numbers
479
    fn test_fast_blur_negative() {
480
        let image = RgbaImage::new(50, 50);
481
        let _ = fast_blur(&image, -1.0);
482
    }
483
484
    #[test]
485
    /// Test fast blur doesn't panic when sigma produces boxes larger than the image
486
    fn test_fast_large_sigma() {
487
        let image = RgbaImage::new(1, 1);
488
        let _ = fast_blur(&image, 50.0);
489
    }
490
491
    #[test]
492
    /// Test blur doesn't panic when passed an empty image (any direction)
493
    fn test_fast_blur_empty() {
494
        let image = RgbaImage::new(0, 0);
495
        let _ = fast_blur(&image, 1.0);
496
        let image = RgbaImage::new(20, 0);
497
        let _ = fast_blur(&image, 1.0);
498
        let image = RgbaImage::new(0, 20);
499
        let _ = fast_blur(&image, 1.0);
500
    }
501
502
    #[test]
503
    /// Test fast blur works with 3 channels
504
    fn test_fast_blur_3_channels() {
505
        let image = RgbImage::new(50, 50);
506
        let _ = fast_blur(&image, 1.0);
507
    }
508
509
    #[test]
510
    /// Test fast blur works with 2 channels
511
    fn test_fast_blur_2_channels() {
512
        let image = GrayAlphaImage::new(50, 50);
513
        let _ = fast_blur(&image, 1.0);
514
    }
515
516
    #[test]
517
    /// Test fast blur works with 1 channel
518
    fn test_fast_blur_1_channels() {
519
        let image = GrayImage::new(50, 50);
520
        let _ = fast_blur(&image, 1.0);
521
    }
522
523
    #[test]
524
    #[cfg(feature = "tiff")]
525
    fn fast_blur_approximates_gaussian_blur_well() {
526
        let path = concat!(
527
            env!("CARGO_MANIFEST_DIR"),
528
            "/tests/images/tiff/testsuite/rgb-3c-16b.tiff"
529
        );
530
        let image = crate::open(path).unwrap();
531
        let image_blurred_gauss = image
532
            .blur_advanced(GaussianBlurParameters::new_from_sigma(50.0))
533
            .to_rgb8();
534
        let image_blurred_gauss_samples = image_blurred_gauss.as_flat_samples();
535
        let image_blurred_gauss_bytes = image_blurred_gauss_samples.as_slice();
536
        let image_blurred_fast = image.fast_blur(50.0).to_rgb8();
537
        let image_blurred_fast_samples = image_blurred_fast.as_flat_samples();
538
        let image_blurred_fast_bytes = image_blurred_fast_samples.as_slice();
539
540
        let error = image_blurred_gauss_bytes
541
            .iter()
542
            .zip(image_blurred_fast_bytes.iter())
543
            .map(|(a, b)| (f32::from(*a) - f32::from(*b)) / f32::from(*a))
544
            .sum::<f32>()
545
            / (image_blurred_gauss_bytes.len() as f32);
546
        assert!(error < 0.05);
547
    }
548
549
    /// Test that thumbnails are created without with correct color rounding.
550
    #[test]
551
    fn test_image_thumbnail() {
552
        let all_black_u8 = GrayImage::new(16, 16);
553
        assert_eq!(thumbnail(&all_black_u8, 1, 1).get_pixel(0, 0).0, [0_u8]);
554
555
        let all_black_f32 = Rgb32FImage::new(16, 16);
556
        assert_eq!(
557
            thumbnail(&all_black_f32, 1, 1).get_pixel(0, 0).0,
558
            [0.0_f32, 0.0_f32, 0.0_f32]
559
        );
560
561
        // this has an average of 0.5 which should round up to 1
562
        let checker = GrayImage::from_vec(2, 2, vec![0, 1, 0, 1]).unwrap();
563
        assert_eq!(thumbnail(&checker, 1, 1).get_pixel(0, 0).0, [1_u8]);
564
    }
565
}