Skip to main content

falcon_video/
pixels.rs

1//! Pixel conversions: the decoder's BGRA rows (with padding and a track
2//! rotation) into tightly packed RGBA.
3
4pub use falcon_ffmpeg::Frame;
5
6/// Clockwise quarter turns (0 to 3) for a track's affine transform
7/// `[a b c d]`: an iPhone portrait clip carries `[0 1 -1 0]`, one turn.
8pub fn quarter_turns(a: f64, b: f64) -> u8 {
9    if a == 0.0 && b == 0.0 {
10        return 0;
11    }
12    let turns = (b.atan2(a) / std::f64::consts::FRAC_PI_2).round() as i64;
13    turns.rem_euclid(4) as u8
14}
15
16/// The size a system player's output asks for so that a picture shown at
17/// `width` by `height`, once turned `turns` quarter turns, fits `max_side`
18/// on its long side: the size before the turn, never enlarged.
19pub fn output_size(width: u32, height: u32, turns: u8, max_side: u32) -> (u32, u32) {
20    let (w, h) = if turns % 2 == 1 { (height, width) } else { (width, height) };
21    let long = w.max(h).max(1);
22    if long <= max_side {
23        return (w.max(1), h.max(1));
24    }
25    let scale = |v: u32| ((f64::from(v) * f64::from(max_side) / f64::from(long)).round() as u32).max(1);
26    (scale(w), scale(h))
27}
28
29/// Packs `height` rows of `width` BGRA pixels, `stride` bytes apart, into
30/// RGBA, turned clockwise `turns` quarter turns. Rows missing from `src`
31/// come out black rather than panicking.
32pub fn from_bgra(src: &[u8], width: usize, height: usize, stride: usize, turns: u8) -> Frame {
33    let (w, h) = (width, height);
34    let (ow, oh) = if turns % 2 == 1 { (h, w) } else { (w, h) };
35    if w == 0 || h == 0 {
36        return Frame { width: ow, height: oh, rgba: Vec::new() };
37    }
38    let mut rgba = vec![0u8; ow * oh * 4];
39    if turns.is_multiple_of(4) {
40        // The common case, a row at a time.
41        for (y, out) in rgba.chunks_exact_mut(w * 4).enumerate() {
42            let row = src.get(y * stride..).unwrap_or(&[]);
43            for (d, px) in out.as_chunks_mut::<4>().0.iter_mut().enumerate() {
44                if let Some(s) = row.get(d * 4..d * 4 + 4) {
45                    *px = [s[2], s[1], s[0], 255];
46                } else {
47                    px[3] = 255;
48                }
49            }
50        }
51        return Frame { width: ow, height: oh, rgba };
52    }
53    for oy in 0..oh {
54        for ox in 0..ow {
55            // The source pixel that lands at (ox, oy) after the turn.
56            let (sx, sy) = match turns % 4 {
57                1 => (oy, h - 1 - ox),
58                2 => (w - 1 - ox, h - 1 - oy),
59                3 => (w - 1 - oy, ox),
60                _ => (ox, oy),
61            };
62            let s = sy * stride + sx * 4;
63            let d = (oy * ow + ox) * 4;
64            if let Some(px) = src.get(s..s + 4) {
65                rgba[d] = px[2];
66                rgba[d + 1] = px[1];
67                rgba[d + 2] = px[0];
68            }
69            rgba[d + 3] = 255;
70        }
71    }
72    Frame { width: ow, height: oh, rgba }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    /// A 3×2 BGRA picture with 4 bytes of row padding; pixel n has blue n.
80    fn sample() -> Vec<u8> {
81        let mut v = Vec::new();
82        for y in 0..2u8 {
83            for x in 0..3u8 {
84                v.extend_from_slice(&[y * 3 + x, 0, 200, 0]);
85            }
86            v.extend_from_slice(&[9, 9, 9, 9]);
87        }
88        v
89    }
90
91    fn blues(f: &Frame) -> Vec<u8> {
92        f.rgba.as_chunks::<4>().0.iter().map(|p| p[2]).collect()
93    }
94
95    #[test]
96    fn bgra_rows_become_rgba() {
97        let f = from_bgra(&sample(), 3, 2, 16, 0);
98        assert_eq!((f.width, f.height), (3, 2));
99        assert_eq!(f.pixel(1, 1), [200, 0, 4, 255]);
100        assert_eq!(blues(&f), vec![0, 1, 2, 3, 4, 5]);
101        assert_eq!(f.mean(), [200.0, 0.0, 2.5]);
102    }
103
104    #[test]
105    fn turns_rotate_clockwise() {
106        // 0 1 2      turned once:  3 0
107        // 3 4 5                    4 1
108        //                          5 2
109        let f = from_bgra(&sample(), 3, 2, 16, 1);
110        assert_eq!((f.width, f.height), (2, 3));
111        assert_eq!(blues(&f), vec![3, 0, 4, 1, 5, 2]);
112        assert_eq!(blues(&from_bgra(&sample(), 3, 2, 16, 2)), vec![5, 4, 3, 2, 1, 0]);
113        assert_eq!(blues(&from_bgra(&sample(), 3, 2, 16, 3)), vec![2, 5, 1, 4, 0, 3]);
114    }
115
116    #[test]
117    fn short_buffers_do_not_panic() {
118        let f = from_bgra(&sample()[..18], 3, 2, 16, 0);
119        assert_eq!(f.pixel(0, 1), [0, 0, 0, 255]);
120        assert_eq!(from_bgra(&[], 0, 0, 0, 1).rgba.len(), 0);
121        assert_eq!(from_bgra(&[], 0, 5, 0, 0).rgba.len(), 0);
122    }
123
124    #[test]
125    fn output_sizes_fit_the_long_side() {
126        assert_eq!(output_size(1920, 1080, 0, 640), (640, 360));
127        assert_eq!(output_size(1080, 1920, 0, 640), (360, 640));
128        assert_eq!(output_size(320, 180, 0, 640), (320, 180), "never enlarged");
129        // A portrait clip: shown 1080x1920, stored 1920x1080 and turned once.
130        assert_eq!(output_size(1080, 1920, 1, 640), (640, 360));
131        assert_eq!(output_size(4000, 2, 0, 64), (64, 1), "never empty");
132        assert_eq!(output_size(0, 0, 0, 64), (1, 1));
133    }
134
135    #[test]
136    fn transforms_map_to_turns() {
137        assert_eq!(quarter_turns(1.0, 0.0), 0);
138        assert_eq!(quarter_turns(0.0, 1.0), 1);
139        assert_eq!(quarter_turns(-1.0, 0.0), 2);
140        assert_eq!(quarter_turns(0.0, -1.0), 3);
141        assert_eq!(quarter_turns(0.0, 0.0), 0);
142    }
143}