Skip to main content

ithmb_core/
uyvy.rs

1//! UYVY (YUV 4:2:2) decoder.
2//!
3//! UYVY is a packed YUV format where each 4-byte group encodes two pixels
4//! that share U (Cb) and V (Cr) chroma values.
5//!
6//! ```text
7//! Byte 0: U0 (Cb)     — shared chroma blue-difference
8//! Byte 1: Y0          — luma for pixel 0
9//! Byte 2: V0 (Cr)     — shared chroma red-difference
10//! Byte 3: Y1          — luma for pixel 1
11//! ```
12//!
13//! Color conversion uses BT.601 coefficients via [`crate::yuv::yuv_to_bgra`].
14
15use crate::error::{DecodeError, DecodedImage};
16use crate::profile::Profile;
17use crate::yuv;
18use std::sync::atomic::AtomicBool;
19/// Decode a UYVY frame to BGRA8.
20///
21/// # Arguments
22///
23/// * `src` — Raw UYVY data (2 bytes per pixel in the format above).
24/// * `profile` — Frame profile providing dimensions and interlacing flag.
25///
26/// # Errors
27///
28/// | Variant | Condition |
29/// |---|---|
30/// | `InvalidFormat` | Non-positive width or height, or odd height with interlacing |
31pub fn decode(src: &[u8], profile: &Profile, canceled: &AtomicBool) -> Result<DecodedImage, DecodeError> {
32    let (w, h) = crate::decoder_helpers::validate_dimensions(src, profile, "width and height must be positive", 2)?;
33
34    let mut dst = vec![0u8; w * h * 4];
35
36    if profile.is_interlaced {
37        if h % 2 != 0 {
38            return Err(DecodeError::InvalidFormat(
39                "interlaced UYVY requires an even height".into(),
40            ));
41        }
42        decode_interlaced(src, w, h, &mut dst, canceled)?;
43    } else {
44        decode_progressive(src, w, h, &mut dst, canceled)?;
45    }
46
47    #[allow(clippy::cast_possible_truncation)]
48    Ok(DecodedImage {
49        data: dst,
50        width: w as u32,
51        height: h as u32,
52    })
53}
54
55/// Decode a non-interlaced UYVY frame row by row.
56fn decode_progressive(
57    src: &[u8],
58    w: usize,
59    h: usize,
60    dst: &mut [u8],
61    canceled: &AtomicBool,
62) -> Result<(), DecodeError> {
63    let row_stride = w * 2;
64    for y in 0..h {
65        crate::pixel_utils::check_canceled(canceled, "uyvy decode canceled")?;
66        let src_off = y * row_stride;
67        let dst_off = y * w * 4;
68        decode_row(&src[src_off..], w, &mut dst[dst_off..]);
69    }
70    Ok(())
71}
72
73/// Decode an interlaced UYVY frame with separate even/odd fields.
74///
75/// The first half of `src` holds even rows (0, 2, 4, …) and the second
76/// half holds odd rows (1, 3, 5, …). After decoding both fields the
77/// rows are weaved into the final frame.
78fn decode_interlaced(src: &[u8], w: usize, h: usize, dst: &mut [u8], canceled: &AtomicBool) -> Result<(), DecodeError> {
79    let half_rows = h / 2;
80    let field_bytes = w * half_rows * 2;
81
82    // Even rows (0, 2, 4, …) from the first field.
83    for i in 0..half_rows {
84        crate::pixel_utils::check_canceled(canceled, "uyvy decode canceled")?;
85        let src_off = i * w * 2;
86        let dst_row = i * 2;
87        decode_row(&src[src_off..], w, &mut dst[dst_row * w * 4..]);
88    }
89
90    // Odd rows (1, 3, 5, …) from the second field.
91    for i in 0..half_rows {
92        crate::pixel_utils::check_canceled(canceled, "uyvy decode canceled")?;
93        let src_off = field_bytes + i * w * 2;
94        let dst_row = i * 2 + 1;
95        decode_row(&src[src_off..], w, &mut dst[dst_row * w * 4..]);
96    }
97    Ok(())
98}
99
100/// Convert one row of UYVY data to BGRA.
101///
102/// Processes pixels in groups of 2 using the 4-byte UYVY group format.
103/// For odd widths the last pixel reads its Y and U from the trailing
104#[allow(clippy::similar_names)]
105fn decode_row(row_src: &[u8], w: usize, row_dst: &mut [u8]) {
106    let groups = w / 2;
107
108    #[cfg(feature = "simd")]
109    {
110        let row_bytes = groups * 4;
111        crate::simd::uyvy_row_to_bgra(&row_src[..row_bytes], &mut row_dst[..groups * 8]);
112    }
113    #[cfg(not(feature = "simd"))]
114    {
115        for g in 0..groups {
116            let src_idx = g * 4;
117            let px: [u8; 4] = [
118                row_src[src_idx],
119                row_src[src_idx + 1],
120                row_src[src_idx + 2],
121                row_src[src_idx + 3],
122            ];
123            let out = crate::simd::uyvy_quad_to_bgra(&px);
124            let d0 = g * 8;
125            row_dst[d0..d0 + 8].copy_from_slice(&out);
126        }
127    }
128
129    // Odd width: the incomplete trailing pair provides [U, Y] but no V.
130    if w % 2 != 0 {
131        let last_src = groups * 4;
132        let y = row_src[last_src + 1];
133        let u = row_src[last_src];
134        // Reuse V from the last complete group, or neutral if no groups.
135        let v = if groups > 0 { row_src[groups * 4 - 2] } else { 128 };
136        let px = yuv::yuv_to_bgra(y, u, v);
137        let d_off = groups * 8;
138        row_dst[d_off..d_off + 4].copy_from_slice(&px);
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use crate::profile::Encoding;
146    use std::sync::atomic::AtomicBool;
147
148    /// Helper: build a minimal UYVY profile.
149    fn make_profile(w: i32, h: i32, interlaced: bool) -> Profile {
150        Profile {
151            prefix: 0,
152            width: w,
153            height: h,
154            encoding: Encoding::Yuv422,
155            frame_byte_length: w * h * 2,
156            is_interlaced: interlaced,
157            ..Default::default()
158        }
159    }
160
161    // -----------------------------------------------------------------------
162    // Error cases
163    // -----------------------------------------------------------------------
164
165    #[test]
166    fn zero_width() {
167        let p = make_profile(0, 100, false);
168        match decode(&[], &p, &AtomicBool::new(false)) {
169            Err(DecodeError::InvalidFormat(_)) => {}
170            other => panic!("expected InvalidFormat, got {other:?}"),
171        }
172    }
173
174    #[test]
175    fn zero_height() {
176        let p = make_profile(100, 0, false);
177        match decode(&[], &p, &AtomicBool::new(false)) {
178            Err(DecodeError::InvalidFormat(_)) => {}
179            other => panic!("expected InvalidFormat, got {other:?}"),
180        }
181    }
182
183    #[test]
184    fn buffer_too_short() {
185        let p = make_profile(4, 4, false);
186        match decode(&[0u8; 4], &p, &AtomicBool::new(false)) {
187            Err(DecodeError::BufferTooShort { expected, actual }) => {
188                assert_eq!(expected, 4 * 4 * 2);
189                assert_eq!(actual, 4);
190            }
191            other => panic!("expected BufferTooShort, got {other:?}"),
192        }
193    }
194
195    #[test]
196    fn interlaced_odd_height() {
197        let p = make_profile(4, 3, true);
198        match decode(&[0u8; 4 * 3 * 2], &p, &AtomicBool::new(false)) {
199            Err(DecodeError::InvalidFormat(_)) => {}
200            other => panic!("expected InvalidFormat, got {other:?}"),
201        }
202    }
203
204    // -----------------------------------------------------------------------
205    // Basic decode — gray neutral chroma
206    // -----------------------------------------------------------------------
207
208    #[test]
209    fn gray_pair_2x1() {
210        // U=128, Y0=128, V=128, Y1=128  →  two gray pixels
211        let src = [128u8, 128, 128, 128];
212        let p = make_profile(2, 1, false);
213        let img = decode(&src, &p, &AtomicBool::new(false)).unwrap();
214        assert_eq!(img.height, 1);
215        let expected = [128u8, 128, 128, 255, 128, 128, 128, 255];
216        assert_eq!(img.data, expected);
217    }
218
219    #[test]
220    fn black_pair_2x1() {
221        // U=128, Y=0, V=128, Y=0  →  two black pixels
222        let src = [128u8, 0, 128, 0];
223        let p = make_profile(2, 1, false);
224        let _img = decode(&src, &p, &AtomicBool::new(false)).unwrap();
225    }
226
227    #[test]
228    fn white_pair_2x1() {
229        // U=128, Y=255, V=128, Y=255  →  two white pixels
230        let src = [128u8, 255, 128, 255];
231        let p = make_profile(2, 1, false);
232        let _img = decode(&src, &p, &AtomicBool::new(false)).unwrap();
233    }
234
235    // -----------------------------------------------------------------------
236    // 4x4 known pattern — verify BGRA alignment
237    // -----------------------------------------------------------------------
238
239    #[test]
240    fn four_by_four_pattern() {
241        // Build a 4×4 image where each pixel pair shares chroma.
242        // Row 0: U=100, Y=10, V=200, Y=20, U=110, Y=30, V=210, Y=40
243        // Row 1: U=120, Y=50, V=220, Y=60, U=130, Y=70, V=230, Y=80
244        // Row 2: U=140, Y=90, V=240, Y=100, U=150, Y=110, V=250, Y=120
245        // Row 3: U=160, Y=130, V=10, Y=140, U=170, Y=150, V=20, Y=160
246        let src: Vec<u8> = vec![
247            100, 10, 200, 20, 110, 30, 210, 40, 120, 50, 220, 60, 130, 70, 230, 80, 140, 90, 240, 100, 150, 110, 250,
248            120, 160, 130, 10, 140, 170, 150, 20, 160,
249        ];
250
251        let p = make_profile(4, 4, false);
252        let img = decode(&src, &p, &AtomicBool::new(false)).unwrap();
253
254        // Verify pixel (row=0, col=0): Y=10, U=100, V=200
255        let px00 = yuv::yuv_to_bgra(10, 100, 200);
256        assert_eq!(img.data[0..4], px00);
257
258        // Verify pixel (row=0, col=1): Y=20, U=100, V=200
259        let px01 = yuv::yuv_to_bgra(20, 100, 200);
260        assert_eq!(img.data[4..8], px01);
261
262        // Verify pixel (row=0, col=2): Y=30, U=110, V=210
263        let px02 = yuv::yuv_to_bgra(30, 110, 210);
264        assert_eq!(img.data[8..12], px02);
265
266        // Verify pixel (row=0, col=3): Y=40, U=110, V=210
267        let px03 = yuv::yuv_to_bgra(40, 110, 210);
268        assert_eq!(img.data[12..16], px03);
269
270        // Verify pixel (row=3, col=0): Y=130, U=160, V=10
271        let px30 = yuv::yuv_to_bgra(130, 160, 10);
272        assert_eq!(img.data[48..52], px30);
273
274        // Verify pixel (row=3, col=3): Y=160, U=170, V=20
275        let px33 = yuv::yuv_to_bgra(160, 170, 20);
276        assert_eq!(img.data[60..64], px33);
277    }
278
279    // -----------------------------------------------------------------------
280    // Interlaced decode
281    // -----------------------------------------------------------------------
282
283    #[test]
284    fn interlaced_4x2() {
285        // 4×2 interlaced: 2 fields of 1 row each (4 pixels per row).
286        // Field 0 (even rows — written to row 0):
287        //   [U=100, Y=10, V=200, Y=20, U=110, Y=30, V=210, Y=40]
288        // Field 1 (odd rows  — written to row 1):
289        //   [U=150, Y=50, V=250, Y=60, U=160, Y=70, V=10,  Y=80]
290        let even_field: Vec<u8> = vec![100, 10, 200, 20, 110, 30, 210, 40];
291        let odd_field: Vec<u8> = vec![150, 50, 250, 60, 160, 70, 10, 80];
292        let src: Vec<u8> = [even_field.as_slice(), odd_field.as_slice()].concat();
293
294        let p = make_profile(4, 2, true);
295        let img = decode(&src, &p, &AtomicBool::new(false)).unwrap();
296
297        // Row 0 should match the even field.
298        let px00 = yuv::yuv_to_bgra(10, 100, 200);
299        let px01 = yuv::yuv_to_bgra(20, 100, 200);
300        let px02 = yuv::yuv_to_bgra(30, 110, 210);
301        let px03 = yuv::yuv_to_bgra(40, 110, 210);
302        assert_eq!(img.data[0..16], [px00, px01, px02, px03].concat());
303
304        // Row 1 should match the odd field.
305        let px10 = yuv::yuv_to_bgra(50, 150, 250);
306        let px11 = yuv::yuv_to_bgra(60, 150, 250);
307        let px12 = yuv::yuv_to_bgra(70, 160, 10);
308        let px13 = yuv::yuv_to_bgra(80, 160, 10);
309        assert_eq!(img.data[16..32], [px10, px11, px12, px13].concat());
310    }
311
312    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_possible_wrap)]
313    #[test]
314    fn interlaced_matches_manual_weave() {
315        // 6×4 interlaced: 2 fields of 2 rows each.
316        // Manually decode each field with decode_progressive, then weave.
317        let w = 6usize;
318        let h = 4usize;
319        let half = h / 2;
320
321        let mut field0 = Vec::with_capacity(w * half * 2);
322        let mut field1 = Vec::with_capacity(w * half * 2);
323        for row in 0..half {
324            for px in 0..w / 2 {
325                let u = (row * 50 + px * 30 + 10) as u8;
326                let y0 = (row * 40 + px * 20 + 5) as u8;
327                let v = (row * 50 + px * 30 + 20) as u8;
328                let y1 = (row * 40 + px * 20 + 15) as u8;
329                field0.extend_from_slice(&[u, y0, v, y1]);
330            }
331        }
332        for row in 0..half {
333            for px in 0..w / 2 {
334                let u = (row * 70 + px * 40 + 100) as u8;
335                let y0 = (row * 60 + px * 30 + 50) as u8;
336                let v = (row * 70 + px * 40 + 120) as u8;
337                let y1 = (row * 60 + px * 30 + 60) as u8;
338                field1.extend_from_slice(&[u, y0, v, y1]);
339            }
340        }
341
342        let interlaced_src: Vec<u8> = [field0.as_slice(), field1.as_slice()].concat();
343        let p = make_profile(w as i32, h as i32, true);
344        let img = decode(&interlaced_src, &p, &AtomicBool::new(false)).unwrap();
345        // Manually weave: decode fields separately, interleave rows.
346        let mut manual = vec![0u8; w * h * 4];
347        let mut row0_dst = vec![0u8; w * half * 4];
348        let mut row1_dst = vec![0u8; w * half * 4];
349        decode_progressive(&field0, w, half, &mut row0_dst, &AtomicBool::new(false)).unwrap();
350        decode_progressive(&field1, w, half, &mut row1_dst, &AtomicBool::new(false)).unwrap();
351
352        for i in 0..half {
353            let dst_off_even = i * 2 * w * 4;
354            let dst_off_odd = (i * 2 + 1) * w * 4;
355            let src_off = i * w * 4;
356            manual[dst_off_even..dst_off_even + w * 4].copy_from_slice(&row0_dst[src_off..src_off + w * 4]);
357            manual[dst_off_odd..dst_off_odd + w * 4].copy_from_slice(&row1_dst[src_off..src_off + w * 4]);
358        }
359
360        assert_eq!(img.data, manual);
361    }
362
363    // -----------------------------------------------------------------------
364    // BGRA output alignment — each pixel is exactly 4 bytes
365    // -----------------------------------------------------------------------
366
367    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
368    #[test]
369    fn bgra_output_alpha_is_always_255() {
370        let mut src: Vec<u8> = Vec::new();
371        // 2×2 image = 2 rows × 4 bytes = 8 bytes
372        for y in 0..2i32 {
373            for x in 0..2 / 2 {
374                src.push(100); // U
375                src.push((y * 50 + x * 30) as u8); // Y0
376                src.push(200); // V
377                src.push((y * 50 + x * 30 + 20) as u8); // Y1
378            }
379        }
380        let p = make_profile(2, 2, false);
381        let img = decode(&src, &p, &AtomicBool::new(false)).unwrap();
382        for (i, chunk) in img.data.chunks_exact(4).enumerate() {
383            assert_eq!(chunk[3], 255, "alpha must be 255 at pixel {i}, got {}", chunk[3]);
384        }
385    }
386}