/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 | | } |