Skip to main content

pixelflux/webcam/
convert.rs

1//! Normalization of decoded frames into the virtual device's fixed format.
2//!
3//! The device is a fixed-function camera: one raw pixel format at one size. Whatever the client
4//! camera delivers (any size, full- or limited-range luma) is fitted into that: the frame is scaled
5//! to fit with black bars when its size differs, full-range (JPEG) samples are compressed into the
6//! limited range every V4L2 consumer assumes, and the planes are emitted as I420, NV12 or YUYV.
7//! The common path — a frame already at the device size, limited range, I420 device — is a plain
8//! plane copy straight into the ring slot.
9
10use yuv::{BufferStoreMut, YuvPackedImageMut, YuvPlanarImage};
11
12use super::ring::{V4L2_PIX_FMT_NV12, V4L2_PIX_FMT_YUYV, V4L2_PIX_FMT_MJPEG};
13
14/// Borrowed I420 planes with arbitrary strides.
15#[derive(Clone, Copy)]
16pub struct I420View<'a> {
17    pub width: usize,
18    pub height: usize,
19    pub y: &'a [u8],
20    pub u: &'a [u8],
21    pub v: &'a [u8],
22    pub y_stride: usize,
23    pub uv_stride: usize,
24    /// Samples use the full 0..255 range (JPEG) instead of the limited 16..235 / 16..240 range.
25    pub full_range: bool,
26}
27
28impl<'a> I420View<'a> {
29    pub fn chroma_width(&self) -> usize {
30        self.width.div_ceil(2)
31    }
32
33    pub fn chroma_height(&self) -> usize {
34        self.height.div_ceil(2)
35    }
36}
37
38/// Target geometry: the device format the ring advertises.
39#[derive(Clone, Copy, Debug)]
40pub struct DeviceFormat {
41    pub width: usize,
42    pub height: usize,
43    pub fourcc: u32,
44}
45
46impl DeviceFormat {
47    /// Bytes of one device frame; for MJPEG, of the I420 picture the JPEG is encoded from.
48    pub fn frame_bytes(&self) -> usize {
49        match self.fourcc {
50            V4L2_PIX_FMT_YUYV => self.width * 2 * self.height,
51            _ => self.width * self.height + 2 * (self.width.div_ceil(2) * self.height.div_ceil(2)),
52        }
53    }
54
55    /// JPEG (JFIF) carries full-range samples; every raw device format is limited range.
56    pub fn full_range(&self) -> bool {
57        self.fourcc == V4L2_PIX_FMT_MJPEG
58    }
59}
60
61/// Owned, tightly packed I420 buffer used as the normalization scratch.
62pub struct I420Buffer {
63    pub width: usize,
64    pub height: usize,
65    pub data: Vec<u8>,
66}
67
68impl I420Buffer {
69    pub fn new(width: usize, height: usize) -> Self {
70        let mut b = I420Buffer { width, height, data: Vec::new() };
71        b.resize(width, height);
72        b
73    }
74
75    pub fn resize(&mut self, width: usize, height: usize) {
76        self.width = width;
77        self.height = height;
78        self.data.resize(width * height + 2 * (width.div_ceil(2) * height.div_ceil(2)), 0);
79    }
80
81    pub fn y_len(&self) -> usize {
82        self.width * self.height
83    }
84
85    pub fn uv_len(&self) -> usize {
86        self.width.div_ceil(2) * self.height.div_ceil(2)
87    }
88
89    pub fn view(&self, full_range: bool) -> I420View<'_> {
90        let (y, rest) = self.data.split_at(self.y_len());
91        let (u, v) = rest.split_at(self.uv_len());
92        I420View {
93            width: self.width,
94            height: self.height,
95            y,
96            u,
97            v,
98            y_stride: self.width,
99            uv_stride: self.width.div_ceil(2),
100            full_range,
101        }
102    }
103
104    /// Paint the whole buffer black in the given range (Y 16 limited, 0 full; chroma 128).
105    pub fn fill_black(&mut self, full_range: bool) {
106        let y_len = self.y_len();
107        self.data[..y_len].fill(if full_range { 0 } else { 16 });
108        self.data[y_len..].fill(128);
109    }
110}
111
112/// Full-range to limited-range lookup tables (BT.601/709 quantization; the matrix is unchanged).
113struct RangeLut {
114    luma: [u8; 256],
115    chroma: [u8; 256],
116    luma_up: [u8; 256],
117    chroma_up: [u8; 256],
118}
119
120fn range_lut() -> &'static RangeLut {
121    static LUT: std::sync::OnceLock<RangeLut> = std::sync::OnceLock::new();
122    LUT.get_or_init(|| {
123        let mut luma = [0u8; 256];
124        let mut chroma = [0u8; 256];
125        let mut luma_up = [0u8; 256];
126        let mut chroma_up = [0u8; 256];
127        for i in 0..256usize {
128            luma[i] = (16 + (i * 219 + 127) / 255) as u8;
129            let c = i as i32 - 128;
130            let scaled = if c >= 0 { (c * 224 + 127) / 255 } else { -((-c * 224 + 127) / 255) };
131            chroma[i] = (128 + scaled) as u8;
132            let ly = i as i32 - 16;
133            luma_up[i] = ((ly * 255 + 109) / 219).clamp(0, 255) as u8;
134            let up = if c >= 0 { (c * 255 + 112) / 224 } else { -((-c * 255 + 112) / 224) };
135            chroma_up[i] = (128 + up).clamp(0, 255) as u8;
136        }
137        RangeLut { luma, chroma, luma_up, chroma_up }
138    })
139}
140
141/// The LUTs that move `src` samples into the device's range: full-range JPEG into a
142/// limited-range raw device, limited-range video into the full-range MJPEG device, or nothing
143/// when the two agree.
144fn range_luts(src_full: bool, dev: &DeviceFormat) -> (Option<&'static [u8; 256]>, Option<&'static [u8; 256]>) {
145    let lut = range_lut();
146    match (src_full, dev.full_range()) {
147        (true, false) => (Some(&lut.luma), Some(&lut.chroma)),
148        (false, true) => (Some(&lut.luma_up), Some(&lut.chroma_up)),
149        _ => (None, None),
150    }
151}
152
153/// Copy a plane row by row, optionally through a range LUT.
154fn copy_plane(src: &[u8], src_stride: usize, width: usize, height: usize, dst: &mut [u8], dst_stride: usize, lut: Option<&[u8; 256]>) {
155    for row in 0..height {
156        let s = &src[row * src_stride..row * src_stride + width];
157        let d = &mut dst[row * dst_stride..row * dst_stride + width];
158        match lut {
159            Some(t) => {
160                for (o, i) in d.iter_mut().zip(s) {
161                    *o = t[*i as usize];
162                }
163            }
164            None => d.copy_from_slice(s),
165        }
166    }
167}
168
169/// Average 2x2 blocks: exact halving, used before bilinear when the ratio exceeds 2 so strong
170/// downscales do not alias.
171fn halve_plane(src: &[u8], sw: usize, sh: usize, sstride: usize, dst: &mut Vec<u8>) -> (usize, usize) {
172    let dw = (sw / 2).max(1);
173    let dh = (sh / 2).max(1);
174    dst.resize(dw * dh, 0);
175    for y in 0..dh {
176        let r0 = &src[(2 * y).min(sh - 1) * sstride..];
177        let r1 = &src[(2 * y + 1).min(sh - 1) * sstride..];
178        let out = &mut dst[y * dw..(y + 1) * dw];
179        for (x, o) in out.iter_mut().enumerate() {
180            let x0 = (2 * x).min(sw - 1);
181            let x1 = (2 * x + 1).min(sw - 1);
182            *o = ((r0[x0] as u32 + r0[x1] as u32 + r1[x0] as u32 + r1[x1] as u32 + 2) / 4) as u8;
183        }
184    }
185    (dw, dh)
186}
187
188/// Separable bilinear resample of one plane (fixed point 16.16).
189fn bilinear_plane(src: &[u8], sw: usize, sh: usize, sstride: usize, dst: &mut [u8], dw: usize, dh: usize, dstride: usize) {
190    if sw == 0 || sh == 0 || dw == 0 || dh == 0 {
191        return;
192    }
193    if sw == dw && sh == dh {
194        copy_plane(src, sstride, sw, sh, dst, dstride, None);
195        return;
196    }
197    let xs: Vec<(usize, usize, u32)> = (0..dw)
198        .map(|x| {
199            let fx = ((x as u64 * 2 + 1) * sw as u64 * 65536 / (dw as u64 * 2)).saturating_sub(32768);
200            let x0 = (fx >> 16) as usize;
201            let frac = (fx & 0xFFFF) as u32;
202            let x0 = x0.min(sw - 1);
203            let x1 = (x0 + 1).min(sw - 1);
204            (x0, x1, frac)
205        })
206        .collect();
207    for y in 0..dh {
208        let fy = ((y as u64 * 2 + 1) * sh as u64 * 65536 / (dh as u64 * 2)).saturating_sub(32768);
209        let y0 = ((fy >> 16) as usize).min(sh - 1);
210        let y1 = (y0 + 1).min(sh - 1);
211        let wy = (fy & 0xFFFF) as u32;
212        let r0 = &src[y0 * sstride..y0 * sstride + sw];
213        let r1 = &src[y1 * sstride..y1 * sstride + sw];
214        let out = &mut dst[y * dstride..y * dstride + dw];
215        for (o, &(x0, x1, wx)) in out.iter_mut().zip(&xs) {
216            let top = r0[x0] as u32 * (65536 - wx) + r0[x1] as u32 * wx;
217            let bot = r1[x0] as u32 * (65536 - wx) + r1[x1] as u32 * wx;
218            let v = ((top >> 8) * (65536 - wy) + (bot >> 8) * wy + (1 << 23)) >> 24;
219            *o = v.min(255) as u8;
220        }
221    }
222}
223
224/// Resample a plane into `dst` (tightly packed `dw` x `dh`), halving first while the source is more
225/// than twice the destination in either dimension.
226fn scale_plane(src: &[u8], sw: usize, sh: usize, sstride: usize, dst: &mut [u8], dw: usize, dh: usize, tmp: &mut [Vec<u8>; 2]) {
227    let mut cur: Option<(usize, usize, usize)> = None;
228    let mut which = 0;
229    let (mut w, mut h, mut stride) = (sw, sh, sstride);
230    while w >= 2 * dw && h >= 2 * dh && w > 1 && h > 1 {
231        let (nw, nh) = {
232            let (a, b) = tmp.split_at_mut(1);
233            let (out, input) = if which == 0 { (&mut b[0], &a[0]) } else { (&mut a[0], &b[0]) };
234            match cur {
235                None => halve_plane(src, w, h, stride, out),
236                Some(_) => halve_plane(input, w, h, stride, out),
237            }
238        };
239        which ^= 1;
240        w = nw;
241        h = nh;
242        stride = nw;
243        cur = Some((w, h, stride));
244    }
245    match cur {
246        None => bilinear_plane(src, sw, sh, sstride, dst, dw, dh, dw),
247        Some(_) => {
248            let input = if which == 0 { &tmp[0] } else { &tmp[1] };
249            bilinear_plane(input, w, h, stride, dst, dw, dh, dw);
250        }
251    }
252}
253
254/// Scratch state kept across frames so normalization allocates only on geometry changes.
255pub struct Normalizer {
256    fitted: I420Buffer,
257    fitted_geometry: Option<(usize, usize, usize, usize)>,
258    scaled: [Vec<u8>; 3],
259    halves: [Vec<u8>; 2],
260}
261
262impl Normalizer {
263    pub fn new() -> Self {
264        Normalizer {
265            fitted: I420Buffer::new(2, 2),
266            fitted_geometry: None,
267            scaled: [Vec::new(), Vec::new(), Vec::new()],
268            halves: [Vec::new(), Vec::new()],
269        }
270    }
271
272    /// Write `src` into `out` as a `dev` frame; returns the bytes written (0 if `out` is too small).
273    pub fn write_frame(&mut self, src: &I420View<'_>, dev: &DeviceFormat, out: &mut [u8]) -> usize {
274        let need = dev.frame_bytes();
275        if out.len() < need || src.width == 0 || src.height == 0 {
276            return 0;
277        }
278        if src.width == dev.width && src.height == dev.height {
279            emit(src, dev, out);
280            return need;
281        }
282        let (dw, dh, ox, oy) = fit(src.width, src.height, dev.width, dev.height);
283        if self.fitted.width != dev.width || self.fitted.height != dev.height {
284            self.fitted.resize(dev.width, dev.height);
285            self.fitted_geometry = None;
286        }
287        if self.fitted_geometry != Some((dw, dh, ox, oy)) {
288            self.fitted.fill_black(dev.full_range());
289            self.fitted_geometry = Some((dw, dh, ox, oy));
290        }
291        let cw = dw.div_ceil(2);
292        let ch = dh.div_ceil(2);
293        self.scaled[0].resize(dw * dh, 0);
294        self.scaled[1].resize(cw * ch, 0);
295        self.scaled[2].resize(cw * ch, 0);
296        scale_plane(src.y, src.width, src.height, src.y_stride, &mut self.scaled[0], dw, dh, &mut self.halves);
297        scale_plane(src.u, src.chroma_width(), src.chroma_height(), src.uv_stride, &mut self.scaled[1], cw, ch, &mut self.halves);
298        scale_plane(src.v, src.chroma_width(), src.chroma_height(), src.uv_stride, &mut self.scaled[2], cw, ch, &mut self.halves);
299        let (y_lut, c_lut) = range_luts(src.full_range, dev);
300        let fw = self.fitted.width;
301        let fcw = fw.div_ceil(2);
302        let y_len = self.fitted.y_len();
303        let uv_len = self.fitted.uv_len();
304        let (yp, rest) = self.fitted.data.split_at_mut(y_len);
305        let (up, vp) = rest.split_at_mut(uv_len);
306        copy_plane(&self.scaled[0], dw, dw, dh, &mut yp[oy * fw + ox..], fw, y_lut);
307        copy_plane(&self.scaled[1], cw, cw, ch, &mut up[(oy / 2) * fcw + ox / 2..], fcw, c_lut);
308        copy_plane(&self.scaled[2], cw, cw, ch, &mut vp[(oy / 2) * fcw + ox / 2..], fcw, c_lut);
309        let fitted = self.fitted.view(dev.full_range());
310        emit(&fitted, dev, out);
311        need
312    }
313}
314
315impl Default for Normalizer {
316    fn default() -> Self {
317        Self::new()
318    }
319}
320
321/// One upright transform: clockwise quarter turns, then a horizontal mirror.
322///
323/// Clients encode the camera's pixels as captured and relay the upright
324/// transform with each frame; it is applied here, right after decode, where
325/// the fit step already absorbs the size swap of odd turns.
326#[derive(Clone, Copy, PartialEq, Eq, Debug)]
327pub struct Orientation {
328    /// Clockwise quarter turns (0..=3) that make the frame upright.
329    pub quarter_turns: u8,
330    /// Horizontal mirror, applied after the rotation.
331    pub hflip: bool,
332}
333
334impl Orientation {
335    pub const UPRIGHT: Orientation = Orientation { quarter_turns: 0, hflip: false };
336
337    pub fn is_upright(&self) -> bool {
338        self.quarter_turns % 4 == 0 && !self.hflip
339    }
340}
341
342/// Destination square a quarter turn is taken in, so the source rows it gathers
343/// from stay in the innermost cache across the whole square.
344const ORIENT_TILE: usize = 32;
345
346/// Rotate one plane clockwise into a tightly packed destination (whose width is
347/// `h` for odd turn counts), mirroring each destination row for `hflip`.
348///
349/// Half turns keep rows as rows and are copied as such. A quarter turn gathers a
350/// source column per destination row, so it is taken in tiles: a whole-row pass
351/// touches every source row once per output row and spills the cache on any
352/// frame that does not fit in it.
353fn orient_plane(src: &[u8], stride: usize, w: usize, h: usize, o: Orientation, dst: &mut [u8]) {
354    let q = o.quarter_turns % 4;
355    let (dw, dh) = if q % 2 == 1 { (h, w) } else { (w, h) };
356    if q % 2 == 0 {
357        let reversed = (q == 2) != o.hflip;
358        for dy in 0..dh {
359            let sy = if q == 2 { h - 1 - dy } else { dy };
360            let row = &src[sy * stride..sy * stride + w];
361            let out = &mut dst[dy * dw..dy * dw + dw];
362            if reversed {
363                for (dx, o) in out.iter_mut().enumerate() {
364                    *o = row[w - 1 - dx];
365                }
366            } else {
367                out.copy_from_slice(row);
368            }
369        }
370        return;
371    }
372    for ty in (0..dh).step_by(ORIENT_TILE) {
373        for tx in (0..dw).step_by(ORIENT_TILE) {
374            for dy in ty..(ty + ORIENT_TILE).min(dh) {
375                let row = &mut dst[dy * dw..(dy + 1) * dw];
376                for dx in tx..(tx + ORIENT_TILE).min(dw) {
377                    let (sx, sy) = if q == 1 { (dy, h - 1 - dx) } else { (w - 1 - dy, dx) };
378                    row[if o.hflip { dw - 1 - dx } else { dx }] = src[sy * stride + sx];
379                }
380            }
381        }
382    }
383}
384
385/// Apply `o` to `src`, producing a tightly packed upright copy in `out`. The
386/// chroma planes of the rotated picture are exactly the rotated chroma planes
387/// (odd-turn chroma dimensions transpose with them), so each plane is oriented
388/// independently.
389pub fn orient_i420(src: &I420View<'_>, o: Orientation, out: &mut I420Buffer) {
390    let q = o.quarter_turns % 4;
391    let (dw, dh) = if q % 2 == 1 { (src.height, src.width) } else { (src.width, src.height) };
392    out.resize(dw, dh);
393    let y_len = out.y_len();
394    let uv_len = out.uv_len();
395    let (yp, rest) = out.data.split_at_mut(y_len);
396    let (up, vp) = rest.split_at_mut(uv_len);
397    orient_plane(src.y, src.y_stride, src.width, src.height, o, yp);
398    orient_plane(src.u, src.uv_stride, src.chroma_width(), src.chroma_height(), o, up);
399    orient_plane(src.v, src.uv_stride, src.chroma_width(), src.chroma_height(), o, vp);
400}
401
402/// Largest even-aligned size of `sw` x `sh` that fits into `dw` x `dh` preserving aspect, and the
403/// even-aligned offsets that center it.
404pub fn fit(sw: usize, sh: usize, dw: usize, dh: usize) -> (usize, usize, usize, usize) {
405    let (mut w, mut h) = if sw * dh > dw * sh {
406        (dw, (sh * dw / sw).max(2))
407    } else {
408        ((sw * dh / sh).max(2), dh)
409    };
410    w = (w.min(dw) / 2) * 2;
411    h = (h.min(dh) / 2) * 2;
412    let ox = ((dw - w) / 2 / 2) * 2;
413    let oy = ((dh - h) / 2 / 2) * 2;
414    (w, h, ox, oy)
415}
416
417/// Emit `src` (already at the device size) as the device fourcc.
418fn emit(src: &I420View<'_>, dev: &DeviceFormat, out: &mut [u8]) {
419    let (y_lut, c_lut) = range_luts(src.full_range, dev);
420    let w = dev.width;
421    let h = dev.height;
422    let cw = w.div_ceil(2);
423    let ch = h.div_ceil(2);
424    match dev.fourcc {
425        V4L2_PIX_FMT_NV12 => {
426            let (yp, uvp) = out.split_at_mut(w * h);
427            copy_plane(src.y, src.y_stride, w, h, yp, w, y_lut);
428            for row in 0..ch {
429                let u = &src.u[row * src.uv_stride..row * src.uv_stride + cw];
430                let v = &src.v[row * src.uv_stride..row * src.uv_stride + cw];
431                let o = &mut uvp[row * cw * 2..(row + 1) * cw * 2];
432                for x in 0..cw {
433                    let (uu, vv) = match c_lut {
434                        Some(t) => (t[u[x] as usize], t[v[x] as usize]),
435                        None => (u[x], v[x]),
436                    };
437                    o[2 * x] = uu;
438                    o[2 * x + 1] = vv;
439                }
440            }
441        }
442        V4L2_PIX_FMT_YUYV => {
443            let planar = if src.full_range {
444                None
445            } else {
446                Some(YuvPlanarImage {
447                    y_plane: src.y,
448                    y_stride: src.y_stride as u32,
449                    u_plane: src.u,
450                    u_stride: src.uv_stride as u32,
451                    v_plane: src.v,
452                    v_stride: src.uv_stride as u32,
453                    width: w as u32,
454                    height: h as u32,
455                })
456            };
457            match planar {
458                Some(p) => {
459                    let mut packed = YuvPackedImageMut {
460                        yuy: BufferStoreMut::Borrowed(&mut out[..w * 2 * h]),
461                        yuy_stride: (w * 2) as u32,
462                        width: w as u32,
463                        height: h as u32,
464                    };
465                    if yuv::yuv420_to_yuyv422(&mut packed, &p).is_ok() {
466                        return;
467                    }
468                    yuyv_scalar(src, w, h, out, y_lut, c_lut);
469                }
470                None => yuyv_scalar(src, w, h, out, y_lut, c_lut),
471            }
472        }
473        _ => {
474            let (yp, rest) = out.split_at_mut(w * h);
475            let (up, vp) = rest.split_at_mut(cw * ch);
476            copy_plane(src.y, src.y_stride, w, h, yp, w, y_lut);
477            copy_plane(src.u, src.uv_stride, cw, ch, up, cw, c_lut);
478            copy_plane(src.v, src.uv_stride, cw, ch, vp, cw, c_lut);
479        }
480    }
481}
482
483fn yuyv_scalar(src: &I420View<'_>, w: usize, h: usize, out: &mut [u8], y_lut: Option<&[u8; 256]>, c_lut: Option<&[u8; 256]>) {
484    let cw = w.div_ceil(2);
485    for row in 0..h {
486        let y = &src.y[row * src.y_stride..row * src.y_stride + w];
487        let u = &src.u[(row / 2) * src.uv_stride..(row / 2) * src.uv_stride + cw];
488        let v = &src.v[(row / 2) * src.uv_stride..(row / 2) * src.uv_stride + cw];
489        let o = &mut out[row * w * 2..(row + 1) * w * 2];
490        for x in 0..w {
491            let yy = match y_lut { Some(t) => t[y[x] as usize], None => y[x] };
492            let c = if x % 2 == 0 { u[x / 2] } else { v[x / 2] };
493            let cc = match c_lut { Some(t) => t[c as usize], None => c };
494            o[2 * x] = yy;
495            o[2 * x + 1] = cc;
496        }
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::super::ring::V4L2_PIX_FMT_YUV420;
503    use super::*;
504
505    fn solid(width: usize, height: usize, y: u8, u: u8, v: u8) -> I420Buffer {
506        let mut b = I420Buffer::new(width, height);
507        let yl = b.y_len();
508        let ul = b.uv_len();
509        b.data[..yl].fill(y);
510        b.data[yl..yl + ul].fill(u);
511        b.data[yl + ul..].fill(v);
512        b
513    }
514
515    #[test]
516    fn mjpeg_device_takes_full_range_samples() {
517        let mjpeg = DeviceFormat { width: 4, height: 2, fourcc: V4L2_PIX_FMT_MJPEG };
518        let raw = DeviceFormat { width: 4, height: 2, fourcc: V4L2_PIX_FMT_YUV420 };
519        let mut limited = I420Buffer::new(4, 2);
520        limited.data[..8].fill(16);
521        limited.data[8..].fill(128);
522        let mut out = vec![0u8; mjpeg.frame_bytes()];
523        let mut n = Normalizer::new();
524        assert_eq!(n.write_frame(&limited.view(false), &mjpeg, &mut out), mjpeg.frame_bytes());
525        assert_eq!((out[0], out[8]), (0, 128));
526        limited.data[..8].fill(235);
527        n.write_frame(&limited.view(false), &mjpeg, &mut out);
528        assert_eq!(out[0], 255);
529        let mut full = I420Buffer::new(4, 2);
530        full.data[..8].fill(200);
531        full.data[8..].fill(60);
532        n.write_frame(&full.view(true), &mjpeg, &mut out);
533        assert_eq!((out[0], out[8]), (200, 60));
534        n.write_frame(&full.view(true), &raw, &mut out);
535        assert_eq!((out[0], out[8]), (range_lut().luma[200], range_lut().chroma[60]));
536    }
537
538    #[test]
539    fn fit_preserves_aspect_and_alignment() {
540        assert_eq!(fit(1280, 720, 1280, 720), (1280, 720, 0, 0));
541        assert_eq!(fit(640, 480, 1280, 720), (960, 720, 160, 0));
542        assert_eq!(fit(720, 1280, 1280, 720), (404, 720, 438, 0));
543        assert_eq!(fit(1920, 1080, 640, 480), (640, 360, 0, 60));
544        let (w, h, ox, oy) = fit(333, 777, 1280, 720);
545        assert!(w % 2 == 0 && h % 2 == 0 && ox % 2 == 0 && oy % 2 == 0);
546        assert!(w + ox <= 1280 && h + oy <= 720);
547    }
548
549    #[test]
550    fn same_size_i420_is_a_plain_copy() {
551        let src = solid(8, 4, 100, 60, 200);
552        let dev = DeviceFormat { width: 8, height: 4, fourcc: V4L2_PIX_FMT_YUV420 };
553        let mut out = vec![0u8; dev.frame_bytes() + 7];
554        let n = Normalizer::new().write_frame(&src.view(false), &dev, &mut out);
555        assert_eq!(n, 8 * 4 * 3 / 2);
556        assert_eq!(&out[..n], &src.data[..]);
557    }
558
559    #[test]
560    fn full_range_is_compressed_to_limited() {
561        let src = solid(4, 2, 255, 0, 255);
562        let dev = DeviceFormat { width: 4, height: 2, fourcc: V4L2_PIX_FMT_YUV420 };
563        let mut out = vec![0u8; dev.frame_bytes()];
564        Normalizer::new().write_frame(&src.view(true), &dev, &mut out);
565        assert_eq!(out[0], 235);
566        assert_eq!(out[8], 16);
567        assert_eq!(out[10], 240);
568        let black = solid(4, 2, 0, 128, 128);
569        Normalizer::new().write_frame(&black.view(true), &dev, &mut out);
570        assert_eq!((out[0], out[8], out[10]), (16, 128, 128));
571    }
572
573    #[test]
574    fn nv12_and_yuyv_interleave() {
575        let src = solid(4, 2, 50, 60, 70);
576        let nv12 = DeviceFormat { width: 4, height: 2, fourcc: V4L2_PIX_FMT_NV12 };
577        let mut out = vec![0u8; nv12.frame_bytes()];
578        assert_eq!(Normalizer::new().write_frame(&src.view(false), &nv12, &mut out), 12);
579        assert_eq!(&out[..8], &[50; 8]);
580        assert_eq!(&out[8..], &[60, 70, 60, 70]);
581        let yuyv = DeviceFormat { width: 4, height: 2, fourcc: V4L2_PIX_FMT_YUYV };
582        let mut out = vec![0u8; yuyv.frame_bytes()];
583        assert_eq!(Normalizer::new().write_frame(&src.view(false), &yuyv, &mut out), 16);
584        assert_eq!(&out[..8], &[50, 60, 50, 70, 50, 60, 50, 70]);
585        let mut out2 = vec![0u8; yuyv.frame_bytes()];
586        Normalizer::new().write_frame(&src.view(true), &yuyv, &mut out2);
587        assert_eq!(out2[0], range_lut().luma[50]);
588        assert_eq!(out2[1], range_lut().chroma[60]);
589    }
590
591    #[test]
592    fn letterbox_scales_and_pads_black() {
593        let src = solid(8, 8, 200, 90, 160);
594        let dev = DeviceFormat { width: 16, height: 8, fourcc: V4L2_PIX_FMT_YUV420 };
595        let mut out = vec![0u8; dev.frame_bytes()];
596        let mut n = Normalizer::new();
597        assert_eq!(n.write_frame(&src.view(false), &dev, &mut out), dev.frame_bytes());
598        assert_eq!(out[0], 16);
599        assert_eq!(out[4 + 3 * 16], 200);
600        assert_eq!(out[15], 16);
601        let uv = 16 * 8;
602        assert_eq!(out[uv], 128);
603        assert_eq!(out[uv + 2 + 8 * 2], 90);
604        assert_eq!(out[uv + 8 * 4 + 2 + 8 * 2], 160);
605        assert_eq!(n.write_frame(&src.view(false), &dev, &mut out), dev.frame_bytes());
606        assert_eq!(out[4 + 3 * 16], 200);
607    }
608
609    #[test]
610    fn orient_rotates_clockwise_then_mirrors() {
611        let mut src = I420Buffer::new(2, 2);
612        src.data[..4].copy_from_slice(&[1, 2, 3, 4]);
613        src.data[4] = 10;
614        src.data[5] = 20;
615        let mut out = I420Buffer::new(2, 2);
616        let v = src.view(false);
617        orient_i420(&v, Orientation { quarter_turns: 1, hflip: false }, &mut out);
618        assert_eq!(&out.data[..4], &[3, 1, 4, 2]);
619        orient_i420(&v, Orientation { quarter_turns: 2, hflip: false }, &mut out);
620        assert_eq!(&out.data[..4], &[4, 3, 2, 1]);
621        orient_i420(&v, Orientation { quarter_turns: 3, hflip: false }, &mut out);
622        assert_eq!(&out.data[..4], &[2, 4, 1, 3]);
623        orient_i420(&v, Orientation { quarter_turns: 0, hflip: true }, &mut out);
624        assert_eq!(&out.data[..4], &[2, 1, 4, 3]);
625        assert_eq!((out.data[4], out.data[5]), (10, 20));
626        assert!(Orientation::UPRIGHT.is_upright());
627        assert!(!Orientation { quarter_turns: 2, hflip: false }.is_upright());
628    }
629
630    #[test]
631    fn orient_quarter_turn_spans_tiles() {
632        // Wider and taller than one tile, so the gather is checked across tile
633        // boundaries rather than inside a single square.
634        let (w, h) = (ORIENT_TILE * 2 + 6, ORIENT_TILE + 8);
635        let mut src = I420Buffer::new(w, h);
636        for (i, b) in src.data[..w * h].iter_mut().enumerate() {
637            *b = (i % 251) as u8;
638        }
639        let v = src.view(false);
640        let mut out = I420Buffer::new(2, 2);
641        for (q, hflip) in [(1u8, false), (1, true), (3, false), (3, true)] {
642            orient_i420(&v, Orientation { quarter_turns: q, hflip }, &mut out);
643            assert_eq!((out.width, out.height), (h, w));
644            for dy in 0..w {
645                for dx in 0..h {
646                    let (sx, sy) = if q == 1 { (dy, h - 1 - dx) } else { (w - 1 - dy, dx) };
647                    let dst_x = if hflip { h - 1 - dx } else { dx };
648                    assert_eq!(out.data[dy * h + dst_x], v.y[sy * v.y_stride + sx],
649                               "q={} hflip={} dx={} dy={}", q, hflip, dx, dy);
650                }
651            }
652        }
653    }
654
655    #[test]
656    fn orient_swaps_dimensions_for_odd_turns() {
657        let mut src = I420Buffer::new(4, 2);
658        for (i, b) in src.data[..8].iter_mut().enumerate() {
659            *b = i as u8;
660        }
661        let mut out = I420Buffer::new(2, 2);
662        orient_i420(&src.view(true), Orientation { quarter_turns: 1, hflip: false }, &mut out);
663        assert_eq!((out.width, out.height), (2, 4));
664        assert_eq!(&out.data[..8], &[4, 0, 5, 1, 6, 2, 7, 3]);
665        assert!(out.view(true).full_range);
666    }
667
668    #[test]
669    fn downscale_averages() {
670        let mut src = I420Buffer::new(64, 64);
671        for (i, b) in src.data.iter_mut().enumerate() {
672            *b = if i < 64 * 64 { ((i % 64) * 4) as u8 } else { 128 };
673        }
674        let dev = DeviceFormat { width: 16, height: 16, fourcc: V4L2_PIX_FMT_YUV420 };
675        let mut out = vec![0u8; dev.frame_bytes()];
676        assert_eq!(Normalizer::new().write_frame(&src.view(false), &dev, &mut out), dev.frame_bytes());
677        assert!(out[0] < out[8] && out[8] < out[15]);
678        assert!(out[..256].iter().all(|v| *v <= 252));
679        assert!(out[256..].iter().all(|v| *v == 128));
680    }
681}