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_MJPEG, V4L2_PIX_FMT_NV12, V4L2_PIX_FMT_YUYV};
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 {
71            width,
72            height,
73            data: Vec::new(),
74        };
75        b.resize(width, height);
76        b
77    }
78
79    pub fn resize(&mut self, width: usize, height: usize) {
80        self.width = width;
81        self.height = height;
82        self.data.resize(
83            width * height + 2 * (width.div_ceil(2) * height.div_ceil(2)),
84            0,
85        );
86    }
87
88    pub fn y_len(&self) -> usize {
89        self.width * self.height
90    }
91
92    pub fn uv_len(&self) -> usize {
93        self.width.div_ceil(2) * self.height.div_ceil(2)
94    }
95
96    pub fn view(&self, full_range: bool) -> I420View<'_> {
97        let (y, rest) = self.data.split_at(self.y_len());
98        let (u, v) = rest.split_at(self.uv_len());
99        I420View {
100            width: self.width,
101            height: self.height,
102            y,
103            u,
104            v,
105            y_stride: self.width,
106            uv_stride: self.width.div_ceil(2),
107            full_range,
108        }
109    }
110
111    /// Paint the whole buffer black in the given range (Y 16 limited, 0 full; chroma 128).
112    pub fn fill_black(&mut self, full_range: bool) {
113        let y_len = self.y_len();
114        self.data[..y_len].fill(if full_range { 0 } else { 16 });
115        self.data[y_len..].fill(128);
116    }
117}
118
119/// Full-range to limited-range lookup tables (BT.601/709 quantization; the matrix is unchanged).
120struct RangeLut {
121    luma: [u8; 256],
122    chroma: [u8; 256],
123    luma_up: [u8; 256],
124    chroma_up: [u8; 256],
125}
126
127fn range_lut() -> &'static RangeLut {
128    static LUT: std::sync::OnceLock<RangeLut> = std::sync::OnceLock::new();
129    LUT.get_or_init(|| {
130        let mut luma = [0u8; 256];
131        let mut chroma = [0u8; 256];
132        let mut luma_up = [0u8; 256];
133        let mut chroma_up = [0u8; 256];
134        for i in 0..256usize {
135            luma[i] = (16 + (i * 219 + 127) / 255) as u8;
136            let c = i as i32 - 128;
137            let scaled = if c >= 0 {
138                (c * 224 + 127) / 255
139            } else {
140                -((-c * 224 + 127) / 255)
141            };
142            chroma[i] = (128 + scaled) as u8;
143            let ly = i as i32 - 16;
144            luma_up[i] = ((ly * 255 + 109) / 219).clamp(0, 255) as u8;
145            let up = if c >= 0 {
146                (c * 255 + 112) / 224
147            } else {
148                -((-c * 255 + 112) / 224)
149            };
150            chroma_up[i] = (128 + up).clamp(0, 255) as u8;
151        }
152        RangeLut {
153            luma,
154            chroma,
155            luma_up,
156            chroma_up,
157        }
158    })
159}
160
161/// The LUTs that move `src` samples into the device's range: full-range JPEG into a
162/// limited-range raw device, limited-range video into the full-range MJPEG device, or nothing
163/// when the two agree.
164fn range_luts(
165    src_full: bool,
166    dev: &DeviceFormat,
167) -> (Option<&'static [u8; 256]>, Option<&'static [u8; 256]>) {
168    let lut = range_lut();
169    match (src_full, dev.full_range()) {
170        (true, false) => (Some(&lut.luma), Some(&lut.chroma)),
171        (false, true) => (Some(&lut.luma_up), Some(&lut.chroma_up)),
172        _ => (None, None),
173    }
174}
175
176/// Copy a plane row by row, optionally through a range LUT.
177fn copy_plane(
178    src: &[u8],
179    src_stride: usize,
180    width: usize,
181    height: usize,
182    dst: &mut [u8],
183    dst_stride: usize,
184    lut: Option<&[u8; 256]>,
185) {
186    for row in 0..height {
187        let s = &src[row * src_stride..row * src_stride + width];
188        let d = &mut dst[row * dst_stride..row * dst_stride + width];
189        match lut {
190            Some(t) => {
191                for (o, i) in d.iter_mut().zip(s) {
192                    *o = t[*i as usize];
193                }
194            }
195            None => d.copy_from_slice(s),
196        }
197    }
198}
199
200/// Average 2x2 blocks: exact halving, used before bilinear when the ratio exceeds 2 so strong
201/// downscales do not alias.
202fn halve_plane(
203    src: &[u8],
204    sw: usize,
205    sh: usize,
206    sstride: usize,
207    dst: &mut Vec<u8>,
208) -> (usize, usize) {
209    let dw = (sw / 2).max(1);
210    let dh = (sh / 2).max(1);
211    dst.resize(dw * dh, 0);
212    for y in 0..dh {
213        let r0 = &src[(2 * y).min(sh - 1) * sstride..];
214        let r1 = &src[(2 * y + 1).min(sh - 1) * sstride..];
215        let out = &mut dst[y * dw..(y + 1) * dw];
216        for (x, o) in out.iter_mut().enumerate() {
217            let x0 = (2 * x).min(sw - 1);
218            let x1 = (2 * x + 1).min(sw - 1);
219            *o = ((u32::from(r0[x0])
220                + u32::from(r0[x1])
221                + u32::from(r1[x0])
222                + u32::from(r1[x1])
223                + 2)
224                / 4) as u8;
225        }
226    }
227    (dw, dh)
228}
229
230/// Separable bilinear resample of one plane (fixed point 16.16).
231#[allow(clippy::too_many_arguments)]
232fn bilinear_plane(
233    src: &[u8],
234    sw: usize,
235    sh: usize,
236    sstride: usize,
237    dst: &mut [u8],
238    dw: usize,
239    dh: usize,
240    dstride: usize,
241) {
242    if sw == 0 || sh == 0 || dw == 0 || dh == 0 {
243        return;
244    }
245    if sw == dw && sh == dh {
246        copy_plane(src, sstride, sw, sh, dst, dstride, None);
247        return;
248    }
249    let xs: Vec<(usize, usize, u32)> = (0..dw)
250        .map(|x| {
251            let fx =
252                ((x as u64 * 2 + 1) * sw as u64 * 65536 / (dw as u64 * 2)).saturating_sub(32768);
253            let x0 = (fx >> 16) as usize;
254            let frac = (fx & 0xFFFF) as u32;
255            let x0 = x0.min(sw - 1);
256            let x1 = (x0 + 1).min(sw - 1);
257            (x0, x1, frac)
258        })
259        .collect();
260    for y in 0..dh {
261        let fy = ((y as u64 * 2 + 1) * sh as u64 * 65536 / (dh as u64 * 2)).saturating_sub(32768);
262        let y0 = ((fy >> 16) as usize).min(sh - 1);
263        let y1 = (y0 + 1).min(sh - 1);
264        let wy = (fy & 0xFFFF) as u32;
265        let r0 = &src[y0 * sstride..y0 * sstride + sw];
266        let r1 = &src[y1 * sstride..y1 * sstride + sw];
267        let out = &mut dst[y * dstride..y * dstride + dw];
268        for (o, &(x0, x1, wx)) in out.iter_mut().zip(&xs) {
269            let top = u32::from(r0[x0]) * (65536 - wx) + u32::from(r0[x1]) * wx;
270            let bot = u32::from(r1[x0]) * (65536 - wx) + u32::from(r1[x1]) * wx;
271            let v = ((top >> 8) * (65536 - wy) + (bot >> 8) * wy + (1 << 23)) >> 24;
272            *o = v.min(255) as u8;
273        }
274    }
275}
276
277/// Resample a plane into `dst` (tightly packed `dw` x `dh`), halving first while the source is more
278/// than twice the destination in either dimension.
279#[allow(clippy::too_many_arguments)]
280fn scale_plane(
281    src: &[u8],
282    sw: usize,
283    sh: usize,
284    sstride: usize,
285    dst: &mut [u8],
286    dw: usize,
287    dh: usize,
288    tmp: &mut [Vec<u8>; 2],
289) {
290    let mut cur: Option<(usize, usize, usize)> = None;
291    let mut which = 0;
292    let (mut w, mut h, mut stride) = (sw, sh, sstride);
293    while w >= 2 * dw && h >= 2 * dh && w > 1 && h > 1 {
294        let (nw, nh) = {
295            let (a, b) = tmp.split_at_mut(1);
296            let (out, input) = if which == 0 {
297                (&mut b[0], &a[0])
298            } else {
299                (&mut a[0], &b[0])
300            };
301            match cur {
302                None => halve_plane(src, w, h, stride, out),
303                Some(_) => halve_plane(input, w, h, stride, out),
304            }
305        };
306        which ^= 1;
307        w = nw;
308        h = nh;
309        stride = nw;
310        cur = Some((w, h, stride));
311    }
312    match cur {
313        None => bilinear_plane(src, sw, sh, sstride, dst, dw, dh, dw),
314        Some(_) => {
315            let input = if which == 0 { &tmp[0] } else { &tmp[1] };
316            bilinear_plane(input, w, h, stride, dst, dw, dh, dw);
317        }
318    }
319}
320
321/// Scratch state kept across frames so normalization allocates only on geometry changes.
322pub struct Normalizer {
323    fitted: I420Buffer,
324    fitted_geometry: Option<(usize, usize, usize, usize)>,
325    scaled: [Vec<u8>; 3],
326    halves: [Vec<u8>; 2],
327}
328
329impl Normalizer {
330    pub fn new() -> Self {
331        Normalizer {
332            fitted: I420Buffer::new(2, 2),
333            fitted_geometry: None,
334            scaled: [Vec::new(), Vec::new(), Vec::new()],
335            halves: [Vec::new(), Vec::new()],
336        }
337    }
338
339    /// Write `src` into `out` as a `dev` frame; returns the bytes written (0 if `out` is too small).
340    pub fn write_frame(&mut self, src: &I420View<'_>, dev: &DeviceFormat, out: &mut [u8]) -> usize {
341        let need = dev.frame_bytes();
342        if out.len() < need || src.width == 0 || src.height == 0 {
343            return 0;
344        }
345        if src.width == dev.width && src.height == dev.height {
346            emit(src, dev, out);
347            return need;
348        }
349        let (dw, dh, ox, oy) = fit(src.width, src.height, dev.width, dev.height);
350        if self.fitted.width != dev.width || self.fitted.height != dev.height {
351            self.fitted.resize(dev.width, dev.height);
352            self.fitted_geometry = None;
353        }
354        if self.fitted_geometry != Some((dw, dh, ox, oy)) {
355            self.fitted.fill_black(dev.full_range());
356            self.fitted_geometry = Some((dw, dh, ox, oy));
357        }
358        let cw = dw.div_ceil(2);
359        let ch = dh.div_ceil(2);
360        self.scaled[0].resize(dw * dh, 0);
361        self.scaled[1].resize(cw * ch, 0);
362        self.scaled[2].resize(cw * ch, 0);
363        scale_plane(
364            src.y,
365            src.width,
366            src.height,
367            src.y_stride,
368            &mut self.scaled[0],
369            dw,
370            dh,
371            &mut self.halves,
372        );
373        scale_plane(
374            src.u,
375            src.chroma_width(),
376            src.chroma_height(),
377            src.uv_stride,
378            &mut self.scaled[1],
379            cw,
380            ch,
381            &mut self.halves,
382        );
383        scale_plane(
384            src.v,
385            src.chroma_width(),
386            src.chroma_height(),
387            src.uv_stride,
388            &mut self.scaled[2],
389            cw,
390            ch,
391            &mut self.halves,
392        );
393        let (y_lut, c_lut) = range_luts(src.full_range, dev);
394        let fw = self.fitted.width;
395        let fcw = fw.div_ceil(2);
396        let y_len = self.fitted.y_len();
397        let uv_len = self.fitted.uv_len();
398        let (yp, rest) = self.fitted.data.split_at_mut(y_len);
399        let (up, vp) = rest.split_at_mut(uv_len);
400        copy_plane(
401            &self.scaled[0],
402            dw,
403            dw,
404            dh,
405            &mut yp[oy * fw + ox..],
406            fw,
407            y_lut,
408        );
409        copy_plane(
410            &self.scaled[1],
411            cw,
412            cw,
413            ch,
414            &mut up[(oy / 2) * fcw + ox / 2..],
415            fcw,
416            c_lut,
417        );
418        copy_plane(
419            &self.scaled[2],
420            cw,
421            cw,
422            ch,
423            &mut vp[(oy / 2) * fcw + ox / 2..],
424            fcw,
425            c_lut,
426        );
427        let fitted = self.fitted.view(dev.full_range());
428        emit(&fitted, dev, out);
429        need
430    }
431}
432
433impl Default for Normalizer {
434    fn default() -> Self {
435        Self::new()
436    }
437}
438
439/// One upright transform: clockwise quarter turns, then a horizontal mirror.
440///
441/// Clients encode the camera's pixels as captured and relay the upright
442/// transform with each frame; it is applied here, right after decode, where
443/// the fit step already absorbs the size swap of odd turns.
444#[derive(Clone, Copy, PartialEq, Eq, Debug)]
445pub struct Orientation {
446    /// Clockwise quarter turns (0..=3) that make the frame upright.
447    pub quarter_turns: u8,
448    /// Horizontal mirror, applied after the rotation.
449    pub hflip: bool,
450}
451
452impl Orientation {
453    pub const UPRIGHT: Orientation = Orientation {
454        quarter_turns: 0,
455        hflip: false,
456    };
457
458    pub fn is_upright(&self) -> bool {
459        self.quarter_turns.is_multiple_of(4) && !self.hflip
460    }
461}
462
463/// Destination square a quarter turn is taken in, so the source rows it gathers
464/// from stay in the innermost cache across the whole square.
465const ORIENT_TILE: usize = 32;
466
467/// Rotate one plane clockwise into a tightly packed destination (whose width is
468/// `h` for odd turn counts), mirroring each destination row for `hflip`.
469///
470/// Half turns keep rows as rows and are copied as such. A quarter turn gathers a
471/// source column per destination row, so it is taken in tiles: a whole-row pass
472/// touches every source row once per output row and spills the cache on any
473/// frame that does not fit in it.
474fn orient_plane(src: &[u8], stride: usize, w: usize, h: usize, o: Orientation, dst: &mut [u8]) {
475    let q = o.quarter_turns % 4;
476    let (dw, dh) = if q % 2 == 1 { (h, w) } else { (w, h) };
477    if q.is_multiple_of(2) {
478        let reversed = (q == 2) != o.hflip;
479        for dy in 0..dh {
480            let sy = if q == 2 { h - 1 - dy } else { dy };
481            let row = &src[sy * stride..sy * stride + w];
482            let out = &mut dst[dy * dw..dy * dw + dw];
483            if reversed {
484                for (dx, o) in out.iter_mut().enumerate() {
485                    *o = row[w - 1 - dx];
486                }
487            } else {
488                out.copy_from_slice(row);
489            }
490        }
491        return;
492    }
493    for ty in (0..dh).step_by(ORIENT_TILE) {
494        for tx in (0..dw).step_by(ORIENT_TILE) {
495            for dy in ty..(ty + ORIENT_TILE).min(dh) {
496                let row = &mut dst[dy * dw..(dy + 1) * dw];
497                for dx in tx..(tx + ORIENT_TILE).min(dw) {
498                    let (sx, sy) = if q == 1 {
499                        (dy, h - 1 - dx)
500                    } else {
501                        (w - 1 - dy, dx)
502                    };
503                    row[if o.hflip { dw - 1 - dx } else { dx }] = src[sy * stride + sx];
504                }
505            }
506        }
507    }
508}
509
510/// Apply `o` to `src`, producing a tightly packed upright copy in `out`. The
511/// chroma planes of the rotated picture are exactly the rotated chroma planes
512/// (odd-turn chroma dimensions transpose with them), so each plane is oriented
513/// independently.
514pub fn orient_i420(src: &I420View<'_>, o: Orientation, out: &mut I420Buffer) {
515    let q = o.quarter_turns % 4;
516    let (dw, dh) = if q % 2 == 1 {
517        (src.height, src.width)
518    } else {
519        (src.width, src.height)
520    };
521    out.resize(dw, dh);
522    let y_len = out.y_len();
523    let uv_len = out.uv_len();
524    let (yp, rest) = out.data.split_at_mut(y_len);
525    let (up, vp) = rest.split_at_mut(uv_len);
526    orient_plane(src.y, src.y_stride, src.width, src.height, o, yp);
527    orient_plane(
528        src.u,
529        src.uv_stride,
530        src.chroma_width(),
531        src.chroma_height(),
532        o,
533        up,
534    );
535    orient_plane(
536        src.v,
537        src.uv_stride,
538        src.chroma_width(),
539        src.chroma_height(),
540        o,
541        vp,
542    );
543}
544
545/// Largest even-aligned size of `sw` x `sh` that fits into `dw` x `dh` preserving aspect, and the
546/// even-aligned offsets that center it.
547pub fn fit(sw: usize, sh: usize, dw: usize, dh: usize) -> (usize, usize, usize, usize) {
548    let (mut w, mut h) = if sw * dh > dw * sh {
549        (dw, (sh * dw / sw).max(2))
550    } else {
551        ((sw * dh / sh).max(2), dh)
552    };
553    w = (w.min(dw) / 2) * 2;
554    h = (h.min(dh) / 2) * 2;
555    let ox = ((dw - w) / 2 / 2) * 2;
556    let oy = ((dh - h) / 2 / 2) * 2;
557    (w, h, ox, oy)
558}
559
560/// Emit `src` (already at the device size) as the device fourcc.
561fn emit(src: &I420View<'_>, dev: &DeviceFormat, out: &mut [u8]) {
562    let (y_lut, c_lut) = range_luts(src.full_range, dev);
563    let w = dev.width;
564    let h = dev.height;
565    let cw = w.div_ceil(2);
566    let ch = h.div_ceil(2);
567    match dev.fourcc {
568        V4L2_PIX_FMT_NV12 => {
569            let (yp, uvp) = out.split_at_mut(w * h);
570            copy_plane(src.y, src.y_stride, w, h, yp, w, y_lut);
571            for row in 0..ch {
572                let u = &src.u[row * src.uv_stride..row * src.uv_stride + cw];
573                let v = &src.v[row * src.uv_stride..row * src.uv_stride + cw];
574                let o = &mut uvp[row * cw * 2..(row + 1) * cw * 2];
575                for x in 0..cw {
576                    let (uu, vv) = match c_lut {
577                        Some(t) => (t[u[x] as usize], t[v[x] as usize]),
578                        None => (u[x], v[x]),
579                    };
580                    o[2 * x] = uu;
581                    o[2 * x + 1] = vv;
582                }
583            }
584        }
585        V4L2_PIX_FMT_YUYV => {
586            let planar = if src.full_range {
587                None
588            } else {
589                Some(YuvPlanarImage {
590                    y_plane: src.y,
591                    y_stride: src.y_stride as u32,
592                    u_plane: src.u,
593                    u_stride: src.uv_stride as u32,
594                    v_plane: src.v,
595                    v_stride: src.uv_stride as u32,
596                    width: w as u32,
597                    height: h as u32,
598                })
599            };
600            match planar {
601                Some(p) => {
602                    let mut packed = YuvPackedImageMut {
603                        yuy: BufferStoreMut::Borrowed(&mut out[..w * 2 * h]),
604                        yuy_stride: (w * 2) as u32,
605                        width: w as u32,
606                        height: h as u32,
607                    };
608                    if yuv::yuv420_to_yuyv422(&mut packed, &p).is_ok() {
609                        return;
610                    }
611                    yuyv_scalar(src, w, h, out, y_lut, c_lut);
612                }
613                None => yuyv_scalar(src, w, h, out, y_lut, c_lut),
614            }
615        }
616        _ => {
617            let (yp, rest) = out.split_at_mut(w * h);
618            let (up, vp) = rest.split_at_mut(cw * ch);
619            copy_plane(src.y, src.y_stride, w, h, yp, w, y_lut);
620            copy_plane(src.u, src.uv_stride, cw, ch, up, cw, c_lut);
621            copy_plane(src.v, src.uv_stride, cw, ch, vp, cw, c_lut);
622        }
623    }
624}
625
626fn yuyv_scalar(
627    src: &I420View<'_>,
628    w: usize,
629    h: usize,
630    out: &mut [u8],
631    y_lut: Option<&[u8; 256]>,
632    c_lut: Option<&[u8; 256]>,
633) {
634    let cw = w.div_ceil(2);
635    for row in 0..h {
636        let y = &src.y[row * src.y_stride..row * src.y_stride + w];
637        let u = &src.u[(row / 2) * src.uv_stride..(row / 2) * src.uv_stride + cw];
638        let v = &src.v[(row / 2) * src.uv_stride..(row / 2) * src.uv_stride + cw];
639        let o = &mut out[row * w * 2..(row + 1) * w * 2];
640        for x in 0..w {
641            let yy = match y_lut {
642                Some(t) => t[y[x] as usize],
643                None => y[x],
644            };
645            let c = if x % 2 == 0 { u[x / 2] } else { v[x / 2] };
646            let cc = match c_lut {
647                Some(t) => t[c as usize],
648                None => c,
649            };
650            o[2 * x] = yy;
651            o[2 * x + 1] = cc;
652        }
653    }
654}
655
656#[cfg(test)]
657mod tests {
658    use super::super::ring::V4L2_PIX_FMT_YUV420;
659    use super::*;
660
661    fn solid(width: usize, height: usize, y: u8, u: u8, v: u8) -> I420Buffer {
662        let mut b = I420Buffer::new(width, height);
663        let yl = b.y_len();
664        let ul = b.uv_len();
665        b.data[..yl].fill(y);
666        b.data[yl..yl + ul].fill(u);
667        b.data[yl + ul..].fill(v);
668        b
669    }
670
671    #[test]
672    fn mjpeg_device_takes_full_range_samples() {
673        let mjpeg = DeviceFormat {
674            width: 4,
675            height: 2,
676            fourcc: V4L2_PIX_FMT_MJPEG,
677        };
678        let raw = DeviceFormat {
679            width: 4,
680            height: 2,
681            fourcc: V4L2_PIX_FMT_YUV420,
682        };
683        let mut limited = I420Buffer::new(4, 2);
684        limited.data[..8].fill(16);
685        limited.data[8..].fill(128);
686        let mut out = vec![0u8; mjpeg.frame_bytes()];
687        let mut n = Normalizer::new();
688        assert_eq!(
689            n.write_frame(&limited.view(false), &mjpeg, &mut out),
690            mjpeg.frame_bytes()
691        );
692        assert_eq!((out[0], out[8]), (0, 128));
693        limited.data[..8].fill(235);
694        n.write_frame(&limited.view(false), &mjpeg, &mut out);
695        assert_eq!(out[0], 255);
696        let mut full = I420Buffer::new(4, 2);
697        full.data[..8].fill(200);
698        full.data[8..].fill(60);
699        n.write_frame(&full.view(true), &mjpeg, &mut out);
700        assert_eq!((out[0], out[8]), (200, 60));
701        n.write_frame(&full.view(true), &raw, &mut out);
702        assert_eq!(
703            (out[0], out[8]),
704            (range_lut().luma[200], range_lut().chroma[60])
705        );
706    }
707
708    #[test]
709    fn fit_preserves_aspect_and_alignment() {
710        assert_eq!(fit(1280, 720, 1280, 720), (1280, 720, 0, 0));
711        assert_eq!(fit(640, 480, 1280, 720), (960, 720, 160, 0));
712        assert_eq!(fit(720, 1280, 1280, 720), (404, 720, 438, 0));
713        assert_eq!(fit(1920, 1080, 640, 480), (640, 360, 0, 60));
714        let (w, h, ox, oy) = fit(333, 777, 1280, 720);
715        assert!(w % 2 == 0 && h % 2 == 0 && ox % 2 == 0 && oy % 2 == 0);
716        assert!(w + ox <= 1280 && h + oy <= 720);
717    }
718
719    #[test]
720    fn same_size_i420_is_a_plain_copy() {
721        let src = solid(8, 4, 100, 60, 200);
722        let dev = DeviceFormat {
723            width: 8,
724            height: 4,
725            fourcc: V4L2_PIX_FMT_YUV420,
726        };
727        let mut out = vec![0u8; dev.frame_bytes() + 7];
728        let n = Normalizer::new().write_frame(&src.view(false), &dev, &mut out);
729        assert_eq!(n, 8 * 4 * 3 / 2);
730        assert_eq!(&out[..n], &src.data[..]);
731    }
732
733    #[test]
734    fn full_range_is_compressed_to_limited() {
735        let src = solid(4, 2, 255, 0, 255);
736        let dev = DeviceFormat {
737            width: 4,
738            height: 2,
739            fourcc: V4L2_PIX_FMT_YUV420,
740        };
741        let mut out = vec![0u8; dev.frame_bytes()];
742        Normalizer::new().write_frame(&src.view(true), &dev, &mut out);
743        assert_eq!(out[0], 235);
744        assert_eq!(out[8], 16);
745        assert_eq!(out[10], 240);
746        let black = solid(4, 2, 0, 128, 128);
747        Normalizer::new().write_frame(&black.view(true), &dev, &mut out);
748        assert_eq!((out[0], out[8], out[10]), (16, 128, 128));
749    }
750
751    #[test]
752    fn nv12_and_yuyv_interleave() {
753        let src = solid(4, 2, 50, 60, 70);
754        let nv12 = DeviceFormat {
755            width: 4,
756            height: 2,
757            fourcc: V4L2_PIX_FMT_NV12,
758        };
759        let mut out = vec![0u8; nv12.frame_bytes()];
760        assert_eq!(
761            Normalizer::new().write_frame(&src.view(false), &nv12, &mut out),
762            12
763        );
764        assert_eq!(&out[..8], &[50; 8]);
765        assert_eq!(&out[8..], &[60, 70, 60, 70]);
766        let yuyv = DeviceFormat {
767            width: 4,
768            height: 2,
769            fourcc: V4L2_PIX_FMT_YUYV,
770        };
771        let mut out = vec![0u8; yuyv.frame_bytes()];
772        assert_eq!(
773            Normalizer::new().write_frame(&src.view(false), &yuyv, &mut out),
774            16
775        );
776        assert_eq!(&out[..8], &[50, 60, 50, 70, 50, 60, 50, 70]);
777        let mut out2 = vec![0u8; yuyv.frame_bytes()];
778        Normalizer::new().write_frame(&src.view(true), &yuyv, &mut out2);
779        assert_eq!(out2[0], range_lut().luma[50]);
780        assert_eq!(out2[1], range_lut().chroma[60]);
781    }
782
783    #[test]
784    fn letterbox_scales_and_pads_black() {
785        let src = solid(8, 8, 200, 90, 160);
786        let dev = DeviceFormat {
787            width: 16,
788            height: 8,
789            fourcc: V4L2_PIX_FMT_YUV420,
790        };
791        let mut out = vec![0u8; dev.frame_bytes()];
792        let mut n = Normalizer::new();
793        assert_eq!(
794            n.write_frame(&src.view(false), &dev, &mut out),
795            dev.frame_bytes()
796        );
797        assert_eq!(out[0], 16);
798        assert_eq!(out[4 + 3 * 16], 200);
799        assert_eq!(out[15], 16);
800        let uv = 16 * 8;
801        assert_eq!(out[uv], 128);
802        assert_eq!(out[uv + 2 + 8 * 2], 90);
803        assert_eq!(out[uv + 8 * 4 + 2 + 8 * 2], 160);
804        assert_eq!(
805            n.write_frame(&src.view(false), &dev, &mut out),
806            dev.frame_bytes()
807        );
808        assert_eq!(out[4 + 3 * 16], 200);
809    }
810
811    #[test]
812    fn orient_rotates_clockwise_then_mirrors() {
813        let mut src = I420Buffer::new(2, 2);
814        src.data[..4].copy_from_slice(&[1, 2, 3, 4]);
815        src.data[4] = 10;
816        src.data[5] = 20;
817        let mut out = I420Buffer::new(2, 2);
818        let v = src.view(false);
819        orient_i420(
820            &v,
821            Orientation {
822                quarter_turns: 1,
823                hflip: false,
824            },
825            &mut out,
826        );
827        assert_eq!(&out.data[..4], &[3, 1, 4, 2]);
828        orient_i420(
829            &v,
830            Orientation {
831                quarter_turns: 2,
832                hflip: false,
833            },
834            &mut out,
835        );
836        assert_eq!(&out.data[..4], &[4, 3, 2, 1]);
837        orient_i420(
838            &v,
839            Orientation {
840                quarter_turns: 3,
841                hflip: false,
842            },
843            &mut out,
844        );
845        assert_eq!(&out.data[..4], &[2, 4, 1, 3]);
846        orient_i420(
847            &v,
848            Orientation {
849                quarter_turns: 0,
850                hflip: true,
851            },
852            &mut out,
853        );
854        assert_eq!(&out.data[..4], &[2, 1, 4, 3]);
855        assert_eq!((out.data[4], out.data[5]), (10, 20));
856        assert!(Orientation::UPRIGHT.is_upright());
857        assert!(
858            !Orientation {
859                quarter_turns: 2,
860                hflip: false
861            }
862            .is_upright()
863        );
864    }
865
866    #[test]
867    fn orient_quarter_turn_spans_tiles() {
868        // Wider and taller than one tile, so the gather is checked across tile
869        // boundaries rather than inside a single square.
870        let (w, h) = (ORIENT_TILE * 2 + 6, ORIENT_TILE + 8);
871        let mut src = I420Buffer::new(w, h);
872        for (i, b) in src.data[..w * h].iter_mut().enumerate() {
873            *b = (i % 251) as u8;
874        }
875        let v = src.view(false);
876        let mut out = I420Buffer::new(2, 2);
877        for (q, hflip) in [(1u8, false), (1, true), (3, false), (3, true)] {
878            orient_i420(
879                &v,
880                Orientation {
881                    quarter_turns: q,
882                    hflip,
883                },
884                &mut out,
885            );
886            assert_eq!((out.width, out.height), (h, w));
887            for dy in 0..w {
888                for dx in 0..h {
889                    let (sx, sy) = if q == 1 {
890                        (dy, h - 1 - dx)
891                    } else {
892                        (w - 1 - dy, dx)
893                    };
894                    let dst_x = if hflip { h - 1 - dx } else { dx };
895                    assert_eq!(
896                        out.data[dy * h + dst_x],
897                        v.y[sy * v.y_stride + sx],
898                        "q={} hflip={} dx={} dy={}",
899                        q,
900                        hflip,
901                        dx,
902                        dy
903                    );
904                }
905            }
906        }
907    }
908
909    #[test]
910    fn orient_swaps_dimensions_for_odd_turns() {
911        let mut src = I420Buffer::new(4, 2);
912        for (i, b) in src.data[..8].iter_mut().enumerate() {
913            *b = i as u8;
914        }
915        let mut out = I420Buffer::new(2, 2);
916        orient_i420(
917            &src.view(true),
918            Orientation {
919                quarter_turns: 1,
920                hflip: false,
921            },
922            &mut out,
923        );
924        assert_eq!((out.width, out.height), (2, 4));
925        assert_eq!(&out.data[..8], &[4, 0, 5, 1, 6, 2, 7, 3]);
926        assert!(out.view(true).full_range);
927    }
928
929    #[test]
930    fn downscale_averages() {
931        let mut src = I420Buffer::new(64, 64);
932        for (i, b) in src.data.iter_mut().enumerate() {
933            *b = if i < 64 * 64 {
934                ((i % 64) * 4) as u8
935            } else {
936                128
937            };
938        }
939        let dev = DeviceFormat {
940            width: 16,
941            height: 16,
942            fourcc: V4L2_PIX_FMT_YUV420,
943        };
944        let mut out = vec![0u8; dev.frame_bytes()];
945        assert_eq!(
946            Normalizer::new().write_frame(&src.view(false), &dev, &mut out),
947            dev.frame_bytes()
948        );
949        assert!(out[0] < out[8] && out[8] < out[15]);
950        assert!(out[..256].iter().all(|v| *v <= 252));
951        assert!(out[256..].iter().all(|v| *v == 128));
952    }
953}