Skip to main content

rawshift_image/transforms/
orientation.rs

1//! Orientation and crop transforms for RGB images.
2//!
3//! Applies EXIF orientation tags and rectangular crop regions to
4//! fully demosaiced RGB images.
5
6use crate::core::image::{Rect, RgbImage, Size};
7
8/// Apply EXIF orientation transform to correct image display.
9///
10/// TIFF orientation values encode how the stored image should be rotated/flipped
11/// to produce the correct upright display. This function applies the corresponding
12/// pixel transform so the output is always orientation-1 (Normal/upright).
13///
14/// Orientation values:
15/// - 1: Normal (no transform)
16/// - 2: Mirror horizontal
17/// - 3: Rotate 180°
18/// - 4: Mirror vertical
19/// - 5: Transpose (mirror horizontal + rotate 90° CCW)
20/// - 6: Rotate 90° CW
21/// - 7: Transverse (mirror horizontal + rotate 90° CW)
22/// - 8: Rotate 90° CCW
23pub fn apply_orientation(image: &mut RgbImage, orientation: u16) {
24    match orientation {
25        1 => {} // No transform needed
26        2 => flip_horizontal(image),
27        3 => rotate_180(image),
28        4 => flip_vertical(image),
29        5 => {
30            // Transpose = flip horizontal then rotate 90° CCW
31            flip_horizontal(image);
32            rotate_90_ccw(image);
33        }
34        6 => rotate_90_cw(image),
35        7 => {
36            // Transverse = flip horizontal then rotate 90° CW
37            flip_horizontal(image);
38            rotate_90_cw(image);
39        }
40        8 => rotate_90_ccw(image),
41        _ => tracing::warn!(
42            "Unknown orientation value: {}, skipping transform",
43            orientation
44        ),
45    }
46}
47
48/// Mirror image horizontally (left ↔ right).
49pub fn flip_horizontal(image: &mut RgbImage) {
50    let w = image.width() as usize;
51    let h = image.height() as usize;
52    for row in 0..h {
53        for col in 0..w / 2 {
54            let a = (row * w + col) * 3;
55            let b = (row * w + (w - 1 - col)) * 3;
56            image.data.swap(a, b);
57            image.data.swap(a + 1, b + 1);
58            image.data.swap(a + 2, b + 2);
59        }
60    }
61}
62
63/// Mirror image vertically (top ↔ bottom).
64pub fn flip_vertical(image: &mut RgbImage) {
65    let w = image.width() as usize;
66    let h = image.height() as usize;
67    for row in 0..h / 2 {
68        for col in 0..w {
69            let a = (row * w + col) * 3;
70            let b = ((h - 1 - row) * w + col) * 3;
71            image.data.swap(a, b);
72            image.data.swap(a + 1, b + 1);
73            image.data.swap(a + 2, b + 2);
74        }
75    }
76}
77
78/// Rotate image 180°.
79pub fn rotate_180(image: &mut RgbImage) {
80    let n = image.data.len();
81    let mut i = 0;
82    let mut j = n - 3;
83    while i < j {
84        image.data.swap(i, j);
85        image.data.swap(i + 1, j + 1);
86        image.data.swap(i + 2, j + 2);
87        i += 3;
88        j -= 3;
89    }
90}
91
92/// Rotate image 90° clockwise.
93///
94/// New dimensions: `new_width = old_height`, `new_height = old_width`.
95pub fn rotate_90_cw(image: &mut RgbImage) {
96    let old_w = image.width() as usize;
97    let old_h = image.height() as usize;
98    let new_w = old_h;
99    let new_h = old_w;
100    let mut new_data = vec![0u16; new_w * new_h * 3];
101    for old_row in 0..old_h {
102        for old_col in 0..old_w {
103            let new_row = old_col;
104            let new_col = old_h - 1 - old_row;
105            let src = (old_row * old_w + old_col) * 3;
106            let dst = (new_row * new_w + new_col) * 3;
107            new_data[dst] = image.data[src];
108            new_data[dst + 1] = image.data[src + 1];
109            new_data[dst + 2] = image.data[src + 2];
110        }
111    }
112    image.data = new_data;
113    image.set_size(Size::new(new_w as u32, new_h as u32));
114}
115
116/// Rotate image 90° counter-clockwise.
117///
118/// New dimensions: `new_width = old_height`, `new_height = old_width`.
119pub fn rotate_90_ccw(image: &mut RgbImage) {
120    let old_w = image.width() as usize;
121    let old_h = image.height() as usize;
122    let new_w = old_h;
123    let new_h = old_w;
124    let mut new_data = vec![0u16; new_w * new_h * 3];
125    for old_row in 0..old_h {
126        for old_col in 0..old_w {
127            let new_row = old_w - 1 - old_col;
128            let new_col = old_row;
129            let src = (old_row * old_w + old_col) * 3;
130            let dst = (new_row * new_w + new_col) * 3;
131            new_data[dst] = image.data[src];
132            new_data[dst + 1] = image.data[src + 1];
133            new_data[dst + 2] = image.data[src + 2];
134        }
135    }
136    image.data = new_data;
137    image.set_size(Size::new(new_w as u32, new_h as u32));
138}
139
140/// Crop an RGB image to the given rectangle.
141///
142/// If the crop region extends beyond image bounds, a warning is logged and
143/// the image is left unchanged.
144pub fn apply_crop(image: &mut RgbImage, crop: Rect) {
145    let x = crop.origin.x as usize;
146    let y = crop.origin.y as usize;
147    let w = crop.size.width as usize;
148    let h = crop.size.height as usize;
149
150    if x + w <= image.width() as usize && y + h <= image.height() as usize {
151        let img_width = image.width() as usize;
152        let mut new_data = Vec::with_capacity(w * h * 3);
153        for row in 0..h {
154            let src_base = ((y + row) * img_width + x) * 3;
155            new_data.extend_from_slice(&image.data[src_base..src_base + w * 3]);
156        }
157        image.set_size(Size::new(w as u32, h as u32));
158        image.data = new_data;
159    } else {
160        tracing::warn!(
161            "Crop region out of bounds: {:?} vs {}x{}",
162            crop,
163            image.width(),
164            image.height()
165        );
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use crate::core::image::{Point, Size};
173
174    fn make_image(w: u32, h: u32, data: Vec<u16>) -> RgbImage {
175        RgbImage::new(w, h, data)
176    }
177
178    #[test]
179    fn test_flip_horizontal_2x2() {
180        // 2x2 image: [R0,G0,B0, R1,G1,B1,  R2,G2,B2, R3,G3,B3]
181        let mut img = make_image(2, 2, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
182        flip_horizontal(&mut img);
183        assert_eq!(img.data, vec![4, 5, 6, 1, 2, 3, 10, 11, 12, 7, 8, 9]);
184    }
185
186    #[test]
187    fn test_flip_vertical_2x2() {
188        let mut img = make_image(2, 2, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
189        flip_vertical(&mut img);
190        assert_eq!(img.data, vec![7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6]);
191    }
192
193    #[test]
194    fn test_rotate_180_2x2() {
195        let mut img = make_image(2, 2, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
196        rotate_180(&mut img);
197        assert_eq!(img.data, vec![10, 11, 12, 7, 8, 9, 4, 5, 6, 1, 2, 3]);
198    }
199
200    #[test]
201    fn test_rotate_90_cw_2x2() {
202        // Original:  [A B]    After 90 CW:  [C A]
203        //            [C D]                   [D B]
204        let mut img = make_image(2, 2, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
205        rotate_90_cw(&mut img);
206        assert_eq!(img.width(), 2);
207        assert_eq!(img.height(), 2);
208        assert_eq!(img.data, vec![7, 8, 9, 1, 2, 3, 10, 11, 12, 4, 5, 6]);
209    }
210
211    #[test]
212    fn test_rotate_90_ccw_2x2() {
213        // Original:  [A B]    After 90 CCW: [B D]
214        //            [C D]                   [A C]
215        let mut img = make_image(2, 2, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
216        rotate_90_ccw(&mut img);
217        assert_eq!(img.width(), 2);
218        assert_eq!(img.height(), 2);
219        assert_eq!(img.data, vec![4, 5, 6, 10, 11, 12, 1, 2, 3, 7, 8, 9]);
220    }
221
222    #[test]
223    fn test_rotate_90_cw_non_square() {
224        // 3x2 → 2x3
225        let mut img = make_image(
226            3,
227            2,
228            vec![1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 0, 6, 0, 0],
229        );
230        rotate_90_cw(&mut img);
231        assert_eq!(img.width(), 2);
232        assert_eq!(img.height(), 3);
233    }
234
235    #[test]
236    fn test_apply_orientation_identity() {
237        let mut img = make_image(2, 2, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
238        let original = img.data.clone();
239        apply_orientation(&mut img, 1);
240        assert_eq!(img.data, original);
241    }
242
243    #[test]
244    fn test_crop_basic() {
245        // 4x4 image, crop to 2x2 at (1,1)
246        let mut data = Vec::with_capacity(4 * 4 * 3);
247        for i in 0..16 {
248            data.push(i as u16);
249            data.push(0);
250            data.push(0);
251        }
252        let mut img = make_image(4, 4, data);
253        apply_crop(&mut img, Rect::new(Point::new(1, 1), Size::new(2, 2)));
254        assert_eq!(img.width(), 2);
255        assert_eq!(img.height(), 2);
256        // Row 1 of original: pixels 4,5,6,7 → crop cols 1..3 → pixels 5,6
257        assert_eq!(img.data[0], 5); // pixel(1,1).r
258        assert_eq!(img.data[3], 6); // pixel(2,1).r
259    }
260
261    #[test]
262    fn test_crop_out_of_bounds() {
263        let mut img = make_image(4, 4, vec![0u16; 4 * 4 * 3]);
264        let original_size = img.size();
265        apply_crop(&mut img, Rect::new(Point::new(3, 3), Size::new(2, 2)));
266        // Should be unchanged
267        assert_eq!(img.size(), original_size);
268    }
269}