pixelutil_image/
view.rs

1use std::ops::Deref;
2
3use image::{
4    flat::{View, ViewMut},
5    DynamicImage, GenericImageView, ImageBuffer, Pixel,
6};
7
8pub use crate::{coordinate::ImageCoordinate, index::ImageAxisIndex};
9
10/// A trait that extends the standard [`GenericImageView`] with additional
11/// convenience methods for coordinate-based image operations like getting pixel
12/// optionally at specific coordinates or clamped to image bounds, allowing to use negative values
13/// as coordinates.
14pub trait ExtendedImageView: GenericImageView {
15    /// Right and bottom index edges of the image.
16    #[inline]
17    fn edges(&self) -> (u32, u32) {
18        let (width, height) = self.dimensions();
19        (width - 1, height - 1)
20    }
21
22    /// Returns `true` if the given coordinates are within the bounds of the image.
23    #[inline]
24    fn within_bounds<C>(&self, coords: C) -> bool
25    where
26        C: ImageCoordinate,
27    {
28        coords
29            .image_coordinate()
30            .map(|(x, y)| self.in_bounds(x, y))
31            .unwrap_or(false)
32    }
33
34    /// Returns the pixel at the given coordinates if it is within the bounds of the image.
35    #[inline]
36    fn get_pixel_at<C>(&self, coords: C) -> Option<Self::Pixel>
37    where
38        C: ImageCoordinate,
39    {
40        coords
41            .image_coordinate()
42            .filter(|(x, y)| self.in_bounds(*x, *y))
43            .map(|(x, y)| unsafe { self.unsafe_get_pixel(x, y) })
44    }
45
46    /// Returns the pixel at the given coordinates, clamping the coordinates to the image bounds.
47    #[inline]
48    fn get_pixel_clamped<C>(&self, coords: C) -> Self::Pixel
49    where
50        C: ImageCoordinate,
51    {
52        let (right, bottom) = self.edges();
53        let (x, y) = coords.image_coordinate_clamped(right, bottom);
54        unsafe { self.unsafe_get_pixel(x, y) }
55    }
56}
57
58impl ExtendedImageView for DynamicImage {}
59impl<P: Pixel, Container> ExtendedImageView for ImageBuffer<P, Container> where
60    Container: Deref<Target = [P::Subpixel]>
61{
62}
63impl<P: Pixel, Buffer> ExtendedImageView for View<Buffer, P> where Buffer: AsRef<[P::Subpixel]> {}
64impl<P: Pixel, Buffer> ExtendedImageView for ViewMut<Buffer, P> where
65    Buffer: AsRef<[P::Subpixel]> + AsMut<[P::Subpixel]>
66{
67}
68
69#[cfg(test)]
70mod tests {
71    use image::{GrayImage, Luma};
72
73    use super::*;
74
75    #[test]
76    fn in_bounds_for_empty_image() {
77        let image = GrayImage::new(0, 0);
78        for (x, y) in [(0, 0), (-1, -1), (1, 1), (1, 0), (0, 1), (-1, 0), (0, -1)] {
79            assert!(!image.within_bounds((x, y)));
80        }
81    }
82
83    #[test]
84    fn in_bounds_for_non_empty_image() {
85        let image = GrayImage::new(1, 1);
86
87        assert!(image.within_bounds((0, 0)));
88        for (x, y) in [(-1, -1), (1, 1), (1, 0), (0, 1), (-1, 0), (0, -1)] {
89            assert!(!image.within_bounds((x, y)));
90        }
91    }
92
93    #[test]
94    fn lookup_pixel_for_empty_image() {
95        let image = GrayImage::new(0, 0);
96        for (x, y) in [(0, 0), (-1, -1), (1, 1), (1, 0), (0, 1), (-1, 0), (0, -1)] {
97            assert!(image.get_pixel_at((x, y)).is_none());
98        }
99    }
100
101    #[test]
102    fn lookup_pixel_for_non_empty_image() {
103        let image = GrayImage::from_pixel(1, 1, [255].into());
104
105        assert!(image.get_pixel_at((-1, -1)).is_none());
106        assert!(image.get_pixel_at((1, 1)).is_none());
107        assert!(image.get_pixel_at((0, 0)).is_some());
108        assert_eq!(
109            image.get_pixel_at((0, 0)),
110            image.get_pixel_checked(0, 0).copied()
111        );
112    }
113
114    #[test]
115    #[should_panic]
116    fn clamp_pixel_for_empty_image() {
117        let image = GrayImage::new(0, 0);
118        image.get_pixel_clamped((0, 0));
119    }
120
121    #[test]
122    fn clamp_pixel_for_non_empty_image() {
123        let image = GrayImage::from_vec(2, 2, vec![32, 64, 128, 255]).unwrap();
124        let (w, h) = (image.width() as i32, image.height() as i32);
125        let (b, r) = (h - 1, w - 1);
126
127        // near top-left corner
128        assert_eq!(&image.get_pixel_clamped((-1, -1)), image.get_pixel(0, 0));
129        assert_eq!(&image.get_pixel_clamped((0, -1)), image.get_pixel(0, 0));
130        assert_eq!(&image.get_pixel_clamped((-1, 0)), image.get_pixel(0, 0));
131
132        // near bottom-right corner
133        assert_eq!(&image.get_pixel_clamped((w, b)), image.get_pixel(1, 1));
134        assert_eq!(&image.get_pixel_clamped((w, b)), image.get_pixel(1, 1));
135        assert_eq!(&image.get_pixel_clamped((r, h)), image.get_pixel(1, 1));
136
137        // near top-right corner
138        assert_eq!(&image.get_pixel_clamped((w, 0)), image.get_pixel(1, 0));
139        assert_eq!(&image.get_pixel_clamped((r, -1)), image.get_pixel(1, 0));
140        assert_eq!(&image.get_pixel_clamped((w, -1)), image.get_pixel(1, 0));
141
142        // near bottom-left corner
143        assert_eq!(&image.get_pixel_clamped((-1, b)), image.get_pixel(0, 1));
144        assert_eq!(&image.get_pixel_clamped((-1, h)), image.get_pixel(0, 1));
145        assert_eq!(&image.get_pixel_clamped((0, h)), image.get_pixel(0, 1));
146
147        // corners of the image
148        assert_eq!(&image.get_pixel_clamped((0, 0)), image.get_pixel(0, 0));
149        assert_eq!(&image.get_pixel_clamped((r, 0)), image.get_pixel(1, 0));
150        assert_eq!(&image.get_pixel_clamped((0, b)), image.get_pixel(0, 1));
151        assert_eq!(&image.get_pixel_clamped((r, b)), image.get_pixel(1, 1));
152    }
153
154    #[test]
155    fn view_from_flat_samples() {
156        use image::flat::FlatSamples;
157
158        // Create sample data for a 2x2 grayscale image
159        let samples = vec![32u8, 64, 128, 255];
160
161        // Create FlatSamples
162        let flat_samples = FlatSamples {
163            samples,
164            layout: image::flat::SampleLayout {
165                channels: 1,
166                channel_stride: 1,
167                width: 2,
168                height: 2,
169                width_stride: 1,
170                height_stride: 2,
171            },
172            color_hint: None,
173        };
174
175        // Create a View from the FlatSamples
176        let view = flat_samples
177            .as_view::<Luma<u8>>()
178            .expect("Failed to create view from flat samples");
179
180        let (w, h) = (view.width() as i32, view.height() as i32);
181        let (b, r) = (h - 1, w - 1);
182
183        // Test bounds checking
184        assert!(view.within_bounds((0, 0)));
185        assert!(view.within_bounds((1, 1)));
186        assert!(!view.within_bounds((-1, 0)));
187        assert!(!view.within_bounds((2, 0)));
188
189        // Test get_pixel_at
190        assert!(view.get_pixel_at((0, 0)).is_some());
191        assert_eq!(view.get_pixel_at((0, 0)).unwrap(), Luma([32]));
192        assert_eq!(view.get_pixel_at((1, 0)).unwrap(), Luma([64]));
193        assert_eq!(view.get_pixel_at((0, 1)).unwrap(), Luma([128]));
194        assert_eq!(view.get_pixel_at((1, 1)).unwrap(), Luma([255]));
195        assert!(view.get_pixel_at((-1, -1)).is_none());
196        assert!(view.get_pixel_at((2, 2)).is_none());
197
198        // Test clamping functionality
199        // near top-left corner
200        assert_eq!(&view.get_pixel_clamped((-1, -1)), &Luma([32]));
201        assert_eq!(&view.get_pixel_clamped((0, -1)), &Luma([32]));
202        assert_eq!(&view.get_pixel_clamped((-1, 0)), &Luma([32]));
203
204        // near bottom-right corner
205        assert_eq!(&view.get_pixel_clamped((w, b)), &Luma([255]));
206        assert_eq!(&view.get_pixel_clamped((r, h)), &Luma([255]));
207
208        // near top-right corner
209        assert_eq!(&view.get_pixel_clamped((w, 0)), &Luma([64]));
210        assert_eq!(&view.get_pixel_clamped((r, -1)), &Luma([64]));
211        assert_eq!(&view.get_pixel_clamped((w, -1)), &Luma([64]));
212
213        // near bottom-left corner
214        assert_eq!(&view.get_pixel_clamped((-1, b)), &Luma([128]));
215        assert_eq!(&view.get_pixel_clamped((-1, h)), &Luma([128]));
216        assert_eq!(&view.get_pixel_clamped((0, h)), &Luma([128]));
217
218        // corners of the image
219        assert_eq!(&view.get_pixel_clamped((0, 0)), &Luma([32]));
220        assert_eq!(&view.get_pixel_clamped((r, 0)), &Luma([64]));
221        assert_eq!(&view.get_pixel_clamped((0, b)), &Luma([128]));
222        assert_eq!(&view.get_pixel_clamped((r, b)), &Luma([255]));
223    }
224
225    #[test]
226    fn coordinate_trait_usage() {
227        let image = GrayImage::from_vec(2, 2, vec![32, 64, 128, 255]).unwrap();
228
229        // Test with tuple coordinates
230        let tuple_coord = (0i32, 1i32);
231        assert!(image.within_bounds(tuple_coord));
232        assert_eq!(image.get_pixel_at(tuple_coord).unwrap(), Luma([128]));
233
234        // Test with array coordinates
235        let array_coord = [1i32, 0i32];
236        assert!(image.within_bounds(array_coord));
237        assert_eq!(image.get_pixel_at(array_coord).unwrap(), Luma([64]));
238
239        // Test clamping with different coordinate types
240        let out_of_bounds_tuple = (-1i32, -1i32);
241        assert_eq!(&image.get_pixel_clamped(out_of_bounds_tuple), &Luma([32]));
242
243        let out_of_bounds_array = [5i32, 5i32];
244        assert_eq!(&image.get_pixel_clamped(out_of_bounds_array), &Luma([255]));
245    }
246
247    #[cfg(feature = "nalgebra")]
248    #[test]
249    fn nalgebra_point_usage() {
250        use nalgebra::Point2;
251
252        let image = GrayImage::from_vec(2, 2, vec![32, 64, 128, 255]).unwrap();
253
254        let point = Point2::new(0i32, 1i32);
255        assert!(image.within_bounds(*point));
256        assert_eq!(image.get_pixel_at(*point).unwrap(), Luma([128]));
257
258        let out_of_bounds_point = Point2::new(-1i32, -1i32);
259        assert!(!image.within_bounds(out_of_bounds_point));
260        assert_eq!(image.get_pixel_clamped(out_of_bounds_point), Luma([32]));
261    }
262}