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