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