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