1 | //! Image Processing Functions |
2 | use std::cmp; |
3 | |
4 | use crate::image::{GenericImage, GenericImageView, SubImage}; |
5 | use crate::traits::{Lerp, Pixel, Primitive}; |
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 | /// Image sampling |
19 | pub use self::sample::{ |
20 | blur, filter3x3, interpolate_bilinear, interpolate_nearest, resize, sample_bilinear, |
21 | sample_nearest, thumbnail, unsharpen, |
22 | }; |
23 | |
24 | /// Color operations |
25 | pub use self::colorops::{ |
26 | brighten, contrast, dither, grayscale, grayscale_alpha, grayscale_with_type, |
27 | grayscale_with_type_alpha, huerotate, index_colors, invert, BiLevel, ColorMap, |
28 | }; |
29 | |
30 | mod affine; |
31 | // Public only because of Rust bug: |
32 | // https://github.com/rust-lang/rust/issues/18241 |
33 | pub mod colorops; |
34 | mod sample; |
35 | |
36 | /// Return a mutable view into an image |
37 | /// The coordinates set the position of the top left corner of the crop. |
38 | pub fn crop<I: GenericImageView>( |
39 | image: &mut I, |
40 | x: u32, |
41 | y: u32, |
42 | width: u32, |
43 | height: u32, |
44 | ) -> SubImage<&mut I> { |
45 | let (x: u32, y: u32, width: u32, height: u32) = crop_dimms(image, x, y, width, height); |
46 | SubImage::new(image, x, y, width, height) |
47 | } |
48 | |
49 | /// Return an immutable view into an image |
50 | /// The coordinates set the position of the top left corner of the crop. |
51 | pub fn crop_imm<I: GenericImageView>( |
52 | image: &I, |
53 | x: u32, |
54 | y: u32, |
55 | width: u32, |
56 | height: u32, |
57 | ) -> SubImage<&I> { |
58 | let (x: u32, y: u32, width: u32, height: u32) = crop_dimms(image, x, y, width, height); |
59 | SubImage::new(image, x, y, width, height) |
60 | } |
61 | |
62 | fn crop_dimms<I: GenericImageView>( |
63 | image: &I, |
64 | x: u32, |
65 | y: u32, |
66 | width: u32, |
67 | height: u32, |
68 | ) -> (u32, u32, u32, u32) { |
69 | let (iwidth: u32, iheight: u32) = image.dimensions(); |
70 | |
71 | let x: u32 = cmp::min(v1:x, v2:iwidth); |
72 | let y: u32 = cmp::min(v1:y, v2:iheight); |
73 | |
74 | let height: u32 = cmp::min(v1:height, v2:iheight - y); |
75 | let width: u32 = cmp::min(v1:width, v2:iwidth - x); |
76 | |
77 | (x, y, width, height) |
78 | } |
79 | |
80 | /// Calculate the region that can be copied from top to bottom. |
81 | /// |
82 | /// Given image size of bottom and top image, and a point at which we want to place the top image |
83 | /// onto the bottom image, how large can we be? Have to wary of the following issues: |
84 | /// * Top might be larger than bottom |
85 | /// * Overflows in the computation |
86 | /// * Coordinates could be completely out of bounds |
87 | /// |
88 | /// The main idea is to make use of inequalities provided by the nature of `saturating_add` and |
89 | /// `saturating_sub`. These intrinsically validate that all resulting coordinates will be in bounds |
90 | /// for both images. |
91 | /// |
92 | /// We want that all these coordinate accesses are safe: |
93 | /// 1. `bottom.get_pixel(x + [0..x_range), y + [0..y_range))` |
94 | /// 2. `top.get_pixel([0..x_range), [0..y_range))` |
95 | /// |
96 | /// Proof that the function provides the necessary bounds for width. Note that all unaugmented math |
97 | /// operations are to be read in standard arithmetic, not integer arithmetic. Since no direct |
98 | /// integer arithmetic occurs in the implementation, this is unambiguous. |
99 | /// |
100 | /// ```text |
101 | /// Three short notes/lemmata: |
102 | /// - Iff `(a - b) <= 0` then `a.saturating_sub(b) = 0` |
103 | /// - Iff `(a - b) >= 0` then `a.saturating_sub(b) = a - b` |
104 | /// - If `a <= c` then `a.saturating_sub(b) <= c.saturating_sub(b)` |
105 | /// |
106 | /// 1.1 We show that if `bottom_width <= x`, then `x_range = 0` therefore `x + [0..x_range)` is empty. |
107 | /// |
108 | /// x_range |
109 | /// = (top_width.saturating_add(x).min(bottom_width)).saturating_sub(x) |
110 | /// <= bottom_width.saturating_sub(x) |
111 | /// |
112 | /// bottom_width <= x |
113 | /// <==> bottom_width - x <= 0 |
114 | /// <==> bottom_width.saturating_sub(x) = 0 |
115 | /// ==> x_range <= 0 |
116 | /// ==> x_range = 0 |
117 | /// |
118 | /// 1.2 If `x < bottom_width` then `x + x_range < bottom_width` |
119 | /// |
120 | /// x + x_range |
121 | /// <= x + bottom_width.saturating_sub(x) |
122 | /// = x + (bottom_width - x) |
123 | /// = bottom_width |
124 | /// |
125 | /// 2. We show that `x_range <= top_width` |
126 | /// |
127 | /// x_range |
128 | /// = (top_width.saturating_add(x).min(bottom_width)).saturating_sub(x) |
129 | /// <= top_width.saturating_add(x).saturating_sub(x) |
130 | /// <= (top_wdith + x).saturating_sub(x) |
131 | /// = top_width (due to `top_width >= 0` and `x >= 0`) |
132 | /// ``` |
133 | /// |
134 | /// Proof is the same for height. |
135 | pub fn overlay_bounds( |
136 | (bottom_width: u32, bottom_height: u32): (u32, u32), |
137 | (top_width: u32, top_height: u32): (u32, u32), |
138 | x: u32, |
139 | y: u32, |
140 | ) -> (u32, u32) { |
141 | let x_range: u32 = top_widthu32 |
142 | .saturating_add(x) // Calculate max coordinate |
143 | .min(bottom_width) // Restrict to lower width |
144 | .saturating_sub(x); // Determinate length from start `x` |
145 | let y_range: u32 = top_heightu32 |
146 | .saturating_add(y) |
147 | .min(bottom_height) |
148 | .saturating_sub(y); |
149 | (x_range, y_range) |
150 | } |
151 | |
152 | /// Calculate the region that can be copied from top to bottom. |
153 | /// |
154 | /// Given image size of bottom and top image, and a point at which we want to place the top image |
155 | /// onto the bottom image, how large can we be? Have to wary of the following issues: |
156 | /// * Top might be larger than bottom |
157 | /// * Overflows in the computation |
158 | /// * Coordinates could be completely out of bounds |
159 | /// |
160 | /// The returned value is of the form: |
161 | /// |
162 | /// `(origin_bottom_x, origin_bottom_y, origin_top_x, origin_top_y, x_range, y_range)` |
163 | /// |
164 | /// The main idea is to do computations on i64's and then clamp to image dimensions. |
165 | /// In particular, we want to ensure that all these coordinate accesses are safe: |
166 | /// 1. `bottom.get_pixel(origin_bottom_x + [0..x_range), origin_bottom_y + [0..y_range))` |
167 | /// 2. `top.get_pixel(origin_top_y + [0..x_range), origin_top_y + [0..y_range))` |
168 | /// |
169 | fn overlay_bounds_ext( |
170 | (bottom_width: u32, bottom_height: u32): (u32, u32), |
171 | (top_width: u32, top_height: u32): (u32, u32), |
172 | x: i64, |
173 | y: i64, |
174 | ) -> (u32, u32, u32, u32, u32, u32) { |
175 | // Return a predictable value if the two images don't overlap at all. |
176 | if x > i64::from(bottom_width) |
177 | || y > i64::from(bottom_height) |
178 | || x.saturating_add(i64::from(top_width)) <= 0 |
179 | || y.saturating_add(i64::from(top_height)) <= 0 |
180 | { |
181 | return (0, 0, 0, 0, 0, 0); |
182 | } |
183 | |
184 | // Find the maximum x and y coordinates in terms of the bottom image. |
185 | let max_x = x.saturating_add(i64::from(top_width)); |
186 | let max_y = y.saturating_add(i64::from(top_height)); |
187 | |
188 | // Clip the origin and maximum coordinates to the bounds of the bottom image. |
189 | // Casting to a u32 is safe because both 0 and `bottom_{width,height}` fit |
190 | // into 32-bits. |
191 | let max_inbounds_x = max_x.clamp(0, i64::from(bottom_width)) as u32; |
192 | let max_inbounds_y = max_y.clamp(0, i64::from(bottom_height)) as u32; |
193 | let origin_bottom_x = x.clamp(0, i64::from(bottom_width)) as u32; |
194 | let origin_bottom_y = y.clamp(0, i64::from(bottom_height)) as u32; |
195 | |
196 | // The range is the difference between the maximum inbounds coordinates and |
197 | // the clipped origin. Unchecked subtraction is safe here because both are |
198 | // always positive and `max_inbounds_{x,y}` >= `origin_{x,y}` due to |
199 | // `top_{width,height}` being >= 0. |
200 | let x_range = max_inbounds_x - origin_bottom_x; |
201 | let y_range = max_inbounds_y - origin_bottom_y; |
202 | |
203 | // If x (or y) is negative, then the origin of the top image is shifted by -x (or -y). |
204 | let origin_top_x = x.saturating_mul(-1).clamp(0, i64::from(top_width)) as u32; |
205 | let origin_top_y = y.saturating_mul(-1).clamp(0, i64::from(top_height)) as u32; |
206 | |
207 | ( |
208 | origin_bottom_x, |
209 | origin_bottom_y, |
210 | origin_top_x, |
211 | origin_top_y, |
212 | x_range, |
213 | y_range, |
214 | ) |
215 | } |
216 | |
217 | /// Overlay an image at a given coordinate (x, y) |
218 | pub fn overlay<I, J>(bottom: &mut I, top: &J, x: i64, y: i64) |
219 | where |
220 | I: GenericImage, |
221 | J: GenericImageView<Pixel = I::Pixel>, |
222 | { |
223 | let bottom_dims: (u32, u32) = bottom.dimensions(); |
224 | let top_dims: (u32, u32) = top.dimensions(); |
225 | |
226 | // Crop our top image if we're going out of bounds |
227 | let (origin_bottom_x: u32, origin_bottom_y: u32, origin_top_x: u32, origin_top_y: u32, range_width: u32, range_height: u32) = |
228 | overlay_bounds_ext(bottom_dims, top_dims, x, y); |
229 | |
230 | for y: u32 in 0..range_height { |
231 | for x: u32 in 0..range_width { |
232 | let p: ::Pixel = top.get_pixel(x:origin_top_x + x, y:origin_top_y + y); |
233 | let mut bottom_pixel: ::Pixel = bottom.get_pixel(x:origin_bottom_x + x, y:origin_bottom_y + y); |
234 | bottom_pixel.blend(&p); |
235 | |
236 | bottom.put_pixel(x:origin_bottom_x + x, y:origin_bottom_y + y, bottom_pixel); |
237 | } |
238 | } |
239 | } |
240 | |
241 | /// Tile an image by repeating it multiple times |
242 | /// |
243 | /// # Examples |
244 | /// ```no_run |
245 | /// use image::{RgbaImage}; |
246 | /// |
247 | /// let mut img = RgbaImage::new(1920, 1080); |
248 | /// let tile = image::open("tile.png" ).unwrap(); |
249 | /// |
250 | /// image::imageops::tile(&mut img, &tile); |
251 | /// img.save("tiled_wallpaper.png" ).unwrap(); |
252 | /// ``` |
253 | pub fn tile<I, J>(bottom: &mut I, top: &J) |
254 | where |
255 | I: GenericImage, |
256 | J: GenericImageView<Pixel = I::Pixel>, |
257 | { |
258 | for x: u32 in (0..bottom.width()).step_by(step:top.width() as usize) { |
259 | for y: u32 in (0..bottom.height()).step_by(step:top.height() as usize) { |
260 | overlay(bottom, top, x:i64::from(x), y:i64::from(y)); |
261 | } |
262 | } |
263 | } |
264 | |
265 | /// Fill the image with a linear vertical 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::vertical_gradient(&mut img, start, end); |
278 | /// img.save("vertical_gradient.png" ).unwrap(); |
279 | pub fn vertical_gradient<S, P, I>(img: &mut I, start: &P, stop: &P) |
280 | where |
281 | I: GenericImage<Pixel = P>, |
282 | P: Pixel<Subpixel = S> + 'static, |
283 | S: Primitive + Lerp + 'static, |
284 | { |
285 | for y: u32 in 0..img.height() { |
286 | let pixel: P = start.map2(other:stop, |a: S, b: S| { |
287 | let y: ::Ratio = <S::Ratio as num_traits::NumCast>::from(y).unwrap(); |
288 | let height: ::Ratio = <S::Ratio as num_traits::NumCast>::from(img.height() - 1).unwrap(); |
289 | S::lerp(a, b, ratio:y / height) |
290 | }); |
291 | |
292 | for x: u32 in 0..img.width() { |
293 | img.put_pixel(x, y, pixel); |
294 | } |
295 | } |
296 | } |
297 | |
298 | /// Fill the image with a linear horizontal gradient |
299 | /// |
300 | /// This function assumes a linear color space. |
301 | /// |
302 | /// # Examples |
303 | /// ```no_run |
304 | /// use image::{Rgba, RgbaImage, Pixel}; |
305 | /// |
306 | /// let mut img = RgbaImage::new(100, 100); |
307 | /// let start = Rgba::from_slice(&[0, 128, 0, 0]); |
308 | /// let end = Rgba::from_slice(&[255, 255, 255, 255]); |
309 | /// |
310 | /// image::imageops::horizontal_gradient(&mut img, start, end); |
311 | /// img.save("horizontal_gradient.png" ).unwrap(); |
312 | pub fn horizontal_gradient<S, P, I>(img: &mut I, start: &P, stop: &P) |
313 | where |
314 | I: GenericImage<Pixel = P>, |
315 | P: Pixel<Subpixel = S> + 'static, |
316 | S: Primitive + Lerp + 'static, |
317 | { |
318 | for x: u32 in 0..img.width() { |
319 | let pixel: P = start.map2(other:stop, |a: S, b: S| { |
320 | let x: ::Ratio = <S::Ratio as num_traits::NumCast>::from(x).unwrap(); |
321 | let width: ::Ratio = <S::Ratio as num_traits::NumCast>::from(img.width() - 1).unwrap(); |
322 | S::lerp(a, b, ratio:x / width) |
323 | }); |
324 | |
325 | for y: u32 in 0..img.height() { |
326 | img.put_pixel(x, y, pixel); |
327 | } |
328 | } |
329 | } |
330 | |
331 | /// Replace the contents of an image at a given coordinate (x, y) |
332 | pub fn replace<I, J>(bottom: &mut I, top: &J, x: i64, y: i64) |
333 | where |
334 | I: GenericImage, |
335 | J: GenericImageView<Pixel = I::Pixel>, |
336 | { |
337 | let bottom_dims: (u32, u32) = bottom.dimensions(); |
338 | let top_dims: (u32, u32) = top.dimensions(); |
339 | |
340 | // Crop our top image if we're going out of bounds |
341 | let (origin_bottom_x: u32, origin_bottom_y: u32, origin_top_x: u32, origin_top_y: u32, range_width: u32, range_height: u32) = |
342 | overlay_bounds_ext(bottom_dims, top_dims, x, y); |
343 | |
344 | for y: u32 in 0..range_height { |
345 | for x: u32 in 0..range_width { |
346 | let p: ::Pixel = top.get_pixel(x:origin_top_x + x, y:origin_top_y + y); |
347 | bottom.put_pixel(x:origin_bottom_x + x, y:origin_bottom_y + y, pixel:p); |
348 | } |
349 | } |
350 | } |
351 | |
352 | #[cfg (test)] |
353 | mod tests { |
354 | |
355 | use super::{overlay, overlay_bounds_ext}; |
356 | use crate::color::Rgb; |
357 | use crate::ImageBuffer; |
358 | use crate::RgbaImage; |
359 | |
360 | #[test ] |
361 | fn test_overlay_bounds_ext() { |
362 | assert_eq!( |
363 | overlay_bounds_ext((10, 10), (10, 10), 0, 0), |
364 | (0, 0, 0, 0, 10, 10) |
365 | ); |
366 | assert_eq!( |
367 | overlay_bounds_ext((10, 10), (10, 10), 1, 0), |
368 | (1, 0, 0, 0, 9, 10) |
369 | ); |
370 | assert_eq!( |
371 | overlay_bounds_ext((10, 10), (10, 10), 0, 11), |
372 | (0, 0, 0, 0, 0, 0) |
373 | ); |
374 | assert_eq!( |
375 | overlay_bounds_ext((10, 10), (10, 10), -1, 0), |
376 | (0, 0, 1, 0, 9, 10) |
377 | ); |
378 | assert_eq!( |
379 | overlay_bounds_ext((10, 10), (10, 10), -10, 0), |
380 | (0, 0, 0, 0, 0, 0) |
381 | ); |
382 | assert_eq!( |
383 | overlay_bounds_ext((10, 10), (10, 10), 1i64 << 50, 0), |
384 | (0, 0, 0, 0, 0, 0) |
385 | ); |
386 | assert_eq!( |
387 | overlay_bounds_ext((10, 10), (10, 10), -(1i64 << 50), 0), |
388 | (0, 0, 0, 0, 0, 0) |
389 | ); |
390 | assert_eq!( |
391 | overlay_bounds_ext((10, 10), (u32::MAX, 10), 10 - i64::from(u32::MAX), 0), |
392 | (0, 0, u32::MAX - 10, 0, 10, 10) |
393 | ); |
394 | } |
395 | |
396 | #[test ] |
397 | /// Test that images written into other images works |
398 | fn test_image_in_image() { |
399 | let mut target = ImageBuffer::new(32, 32); |
400 | let source = ImageBuffer::from_pixel(16, 16, Rgb([255u8, 0, 0])); |
401 | overlay(&mut target, &source, 0, 0); |
402 | assert!(*target.get_pixel(0, 0) == Rgb([255u8, 0, 0])); |
403 | assert!(*target.get_pixel(15, 0) == Rgb([255u8, 0, 0])); |
404 | assert!(*target.get_pixel(16, 0) == Rgb([0u8, 0, 0])); |
405 | assert!(*target.get_pixel(0, 15) == Rgb([255u8, 0, 0])); |
406 | assert!(*target.get_pixel(0, 16) == Rgb([0u8, 0, 0])); |
407 | } |
408 | |
409 | #[test ] |
410 | /// Test that images written outside of a frame doesn't blow up |
411 | fn test_image_in_image_outside_of_bounds() { |
412 | let mut target = ImageBuffer::new(32, 32); |
413 | let source = ImageBuffer::from_pixel(32, 32, Rgb([255u8, 0, 0])); |
414 | overlay(&mut target, &source, 1, 1); |
415 | assert!(*target.get_pixel(0, 0) == Rgb([0, 0, 0])); |
416 | assert!(*target.get_pixel(1, 1) == Rgb([255u8, 0, 0])); |
417 | assert!(*target.get_pixel(31, 31) == Rgb([255u8, 0, 0])); |
418 | } |
419 | |
420 | #[test ] |
421 | /// Test that images written to coordinates out of the frame doesn't blow up |
422 | /// (issue came up in #848) |
423 | fn test_image_outside_image_no_wrap_around() { |
424 | let mut target = ImageBuffer::new(32, 32); |
425 | let source = ImageBuffer::from_pixel(32, 32, Rgb([255u8, 0, 0])); |
426 | overlay(&mut target, &source, 33, 33); |
427 | assert!(*target.get_pixel(0, 0) == Rgb([0, 0, 0])); |
428 | assert!(*target.get_pixel(1, 1) == Rgb([0, 0, 0])); |
429 | assert!(*target.get_pixel(31, 31) == Rgb([0, 0, 0])); |
430 | } |
431 | |
432 | #[test ] |
433 | /// Test that images written to coordinates with overflow works |
434 | fn test_image_coordinate_overflow() { |
435 | let mut target = ImageBuffer::new(16, 16); |
436 | let source = ImageBuffer::from_pixel(32, 32, Rgb([255u8, 0, 0])); |
437 | // Overflows to 'sane' coordinates but top is larger than bot. |
438 | overlay( |
439 | &mut target, |
440 | &source, |
441 | i64::from(u32::max_value() - 31), |
442 | i64::from(u32::max_value() - 31), |
443 | ); |
444 | assert!(*target.get_pixel(0, 0) == Rgb([0, 0, 0])); |
445 | assert!(*target.get_pixel(1, 1) == Rgb([0, 0, 0])); |
446 | assert!(*target.get_pixel(15, 15) == Rgb([0, 0, 0])); |
447 | } |
448 | |
449 | use super::{horizontal_gradient, vertical_gradient}; |
450 | |
451 | #[test ] |
452 | /// Test that horizontal gradients are correctly generated |
453 | fn test_image_horizontal_gradient_limits() { |
454 | let mut img = ImageBuffer::new(100, 1); |
455 | |
456 | let start = Rgb([0u8, 128, 0]); |
457 | let end = Rgb([255u8, 255, 255]); |
458 | |
459 | horizontal_gradient(&mut img, &start, &end); |
460 | |
461 | assert_eq!(img.get_pixel(0, 0), &start); |
462 | assert_eq!(img.get_pixel(img.width() - 1, 0), &end); |
463 | } |
464 | |
465 | #[test ] |
466 | /// Test that vertical gradients are correctly generated |
467 | fn test_image_vertical_gradient_limits() { |
468 | let mut img = ImageBuffer::new(1, 100); |
469 | |
470 | let start = Rgb([0u8, 128, 0]); |
471 | let end = Rgb([255u8, 255, 255]); |
472 | |
473 | vertical_gradient(&mut img, &start, &end); |
474 | |
475 | assert_eq!(img.get_pixel(0, 0), &start); |
476 | assert_eq!(img.get_pixel(0, img.height() - 1), &end); |
477 | } |
478 | |
479 | #[test ] |
480 | /// Test blur doesn't panick when passed 0.0 |
481 | fn test_blur_zero() { |
482 | let image = RgbaImage::new(50, 50); |
483 | let _ = super::blur(&image, 0.0); |
484 | } |
485 | } |
486 | |