v_frame 0.7.0

Video Frame data structures, originally part of rav1e
Documentation
// Copyright (c) 2018-2025, The rav1e contributors. All rights reserved
//
// This source code is subject to the terms of the BSD 2 Clause License and
// the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License
// was not distributed with this source code in the LICENSE file, you can
// obtain it at www.aomedia.org/license/software. If the Alliance for Open
// Media Patent License 1.0 was not distributed with this source code in the
// PATENTS file, you can obtain it at www.aomedia.org/license/patent.

//! YUV video frame structures and builders.
//!
//! This module provides the [`Frame`] type, which represents a complete YUV video frame
//! consisting of one luma (Y) plane and optionally two chroma (U and V) planes. Frames
//! are constructed using the [`FrameBuilder`] pattern to ensure type safety and correct
//! configuration.
//!
//! # Frame Structure
//!
//! A YUV frame contains:
//! - **Y plane**: Luma (brightness) information, always present
//! - **U plane**: First chroma component (Cb), present unless monochrome
//! - **V plane**: Second chroma component (Cr), present unless monochrome
//!
//! The relative dimensions of the chroma planes are determined by the
//! [`ChromaSubsampling`] format.
//!
//! # Type Safety
//!
//! Frames are generic over the pixel type `T: Pixel`:
//! - Use `Frame<u8>` for 8-bit video
//! - Use `Frame<u16>` for high bit-depth (9-16 bit) video
//!
//! The builder validates that the pixel type matches the specified bit depth,
//! returning [`FrameError::DataTypeMismatch`] if they don't align.
//!
//! # Padding
//!
//! Frames support optional padding around the luma plane, which is automatically
//! propagated to the chroma planes according to the subsampling ratio. Padding is
//! useful for video codec algorithms that need to access pixels beyond the visible
//! frame boundaries.
//!
//! # Example
//!
//! ```rust
//! use v_frame::frame::FrameBuilder;
//! use v_frame::chroma::ChromaSubsampling;
//!
//! // Create a 1920x1080 YUV420 8-bit frame
//! let frame = FrameBuilder::new(1920, 1080, ChromaSubsampling::Yuv420, 8)
//!     .build::<u8>()
//!     .unwrap();
//!
//! // Access the planes
//! assert_eq!(frame.y_plane.width(), 1920);
//! assert_eq!(frame.y_plane.height(), 1080);
//!
//! // Chroma planes are half size for YUV420
//! let u_plane = frame.u_plane.as_ref().unwrap();
//! assert_eq!(u_plane.width(), 960);
//! assert_eq!(u_plane.height(), 540);
//! ```
//!
//! # Creating Frames with Padding
//!
//! ```rust
//! use v_frame::frame::FrameBuilder;
//! use v_frame::chroma::ChromaSubsampling;
//!
//! let frame = FrameBuilder::new(1920, 1080, ChromaSubsampling::Yuv420, 10)
//! .luma_padding_left(16)
//! .luma_padding_right(16)
//! .luma_padding_top(16)
//! .luma_padding_bottom(16)
//! .build::<u16>().unwrap();
//! ```

mod error;
pub use error::FrameError;

#[cfg(test)]
mod tests;

use core::num::NonZeroU8;

use crate::{
    chroma::ChromaSubsampling,
    pixel::Pixel,
    plane::{Plane, PlaneGeometry},
};

/// Contains the data representing one YUV video frame.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Frame<T: Pixel> {
    /// The luma plane for this frame
    pub y_plane: Plane<T>,
    /// The first chroma plane for this frame, or `None` if this is a grayscale frame
    pub u_plane: Option<Plane<T>>,
    /// The second chroma plane for this frame, or `None` if this is a grayscale frame
    pub v_plane: Option<Plane<T>>,
    /// The chroma subsampling for this frame
    pub subsampling: ChromaSubsampling,
    /// The number of bits per pixel in this frame
    pub bit_depth: NonZeroU8,
}

impl<T: Pixel> Frame<T> {
    /// Returns a reference to the plane at the given 0-based index,
    /// if it exists in the frame. Otherwise, returns `None`.
    #[inline]
    #[must_use]
    pub fn plane(&self, index: usize) -> Option<&Plane<T>> {
        match index {
            0 => Some(&self.y_plane),
            1 => self.u_plane.as_ref(),
            2 => self.v_plane.as_ref(),
            _ => None,
        }
    }

    /// Returns a mutable reference to the plane at the given 0-based index,
    /// if it exists in the frame. Otherwise, returns `None`.
    #[inline]
    #[must_use]
    pub fn plane_mut(&mut self, index: usize) -> Option<&mut Plane<T>> {
        match index {
            0 => Some(&mut self.y_plane),
            1 => self.u_plane.as_mut(),
            2 => self.v_plane.as_mut(),
            _ => None,
        }
    }
}

/// A builder for constructing [`Frame`] instances with validation.
///
/// `FrameBuilder` uses the builder pattern to construct frames safely, validating
/// that all parameters are compatible (bit depth matches pixel type, dimensions are
/// compatible with chroma subsampling, padding is properly aligned, etc.).
///
/// # Required Parameters
///
/// The following parameters must be provided when creating a new builder:
/// - `width`: Frame width in pixels
/// - `height`: Frame height in pixels
/// - `subsampling`: Chroma subsampling format
/// - `bit_depth`: Bit depth (8 for `u8` pixels, 9-16 for `u16` pixels)
///
/// # Optional Parameters
///
/// Luma padding can be set via setter methods. When padding is set, it is automatically
/// propagated to the chroma planes according to the subsampling ratio.
///
/// # Example
///
/// ```rust
/// use v_frame::frame::FrameBuilder;
/// use v_frame::chroma::ChromaSubsampling;
///
/// let frame = FrameBuilder::new(1920, 1080, ChromaSubsampling::Yuv420, 8)
/// .luma_padding_left(8)
/// .luma_padding_right(8)
/// .build::<u8>().unwrap();
/// ```
pub struct FrameBuilder {
    /// Visible width in pixels.
    width: usize,
    /// Visible height in pixels.
    height: usize,
    /// Chroma subsampling format.
    subsampling: ChromaSubsampling,
    /// Bit depth of the frame's pixels (8-16).
    bit_depth: u8,
    /// Number of padding pixels on the left of the luma plane.
    luma_padding_left: usize,
    /// Number of padding pixels on the right of the luma plane.
    luma_padding_right: usize,
    /// Number of padding pixels on the top of the luma plane.
    luma_padding_top: usize,
    /// Number of padding pixels on the bottom of the luma plane.
    luma_padding_bottom: usize,
}

impl FrameBuilder {
    /// Creates a new frame builder, taking the parameters that are required for all frames.
    /// The builder then allows for setting additional, optional parameters.
    #[inline]
    #[must_use]
    pub fn new(width: usize, height: usize, subsampling: ChromaSubsampling, bit_depth: u8) -> Self {
        Self {
            width,
            height,
            subsampling,
            bit_depth,
            luma_padding_left: 0,
            luma_padding_right: 0,
            luma_padding_top: 0,
            luma_padding_bottom: 0,
        }
    }

    /// Set the `luma_padding_left` for the frame builder.
    #[inline]
    #[must_use]
    pub fn luma_padding_left(mut self, luma_padding_left: usize) -> Self {
        self.luma_padding_left = luma_padding_left;
        self
    }

    /// Set the `luma_padding_right` for the frame builder.
    #[inline]
    #[must_use]
    pub fn luma_padding_right(mut self, luma_padding_right: usize) -> Self {
        self.luma_padding_right = luma_padding_right;
        self
    }

    /// Set the `luma_padding_top` for the frame builder.
    #[inline]
    #[must_use]
    pub fn luma_padding_top(mut self, luma_padding_top: usize) -> Self {
        self.luma_padding_top = luma_padding_top;
        self
    }

    /// Set the `luma_padding_bottom` for the frame builder.
    #[inline]
    #[must_use]
    pub fn luma_padding_bottom(mut self, luma_padding_bottom: usize) -> Self {
        self.luma_padding_bottom = luma_padding_bottom;
        self
    }

    /// Constructs a `Frame` from the current builder.
    ///
    /// # Errors
    /// - Returns `FrameError::UnsupportedBitDepth` if the input bit depth is unsupported
    ///   (currently 8-16 bit inputs are supported)
    /// - Returns `FrameError::DataTypeMismatch` if the size of `T` does not match the
    ///   input bit depth
    /// - Returns `FrameError::UnsupportedResolution` if the resolution or padding dimensions
    ///   do not support the requested subsampling
    #[inline]
    pub fn build<T: Pixel>(self) -> Result<Frame<T>, FrameError> {
        let byte_width = const {
            let sz = size_of::<T>();
            assert!(sz > 0 && sz <= 2, "T must have a size of 1 or 2 bytes");
            sz
        };

        if self.bit_depth < 8 || self.bit_depth > 16 {
            return Err(FrameError::UnsupportedBitDepth {
                found: self.bit_depth,
            });
        }

        if (byte_width == 1 && self.bit_depth != 8) || (byte_width == 2 && self.bit_depth <= 8) {
            return Err(FrameError::DataTypeMismatch);
        }

        let Some(luma_geometry) = PlaneGeometry::new(
            self.width,
            self.height,
            self.luma_padding_left,
            self.luma_padding_right,
            self.luma_padding_top,
            self.luma_padding_bottom,
            1,
            1,
        ) else {
            return Err(FrameError::UnsupportedResolution);
        };

        let (u_plane, v_plane) = match luma_geometry.for_subsampling(self.subsampling) {
            Ok(Some(g)) => (Some(Plane::new(g)), Some(Plane::new(g))),
            Ok(None) => (None, None),
            Err(_) => return Err(FrameError::UnsupportedResolution),
        };

        Ok(Frame {
            y_plane: Plane::new(luma_geometry),
            u_plane,
            v_plane,
            subsampling: self.subsampling,
            bit_depth: NonZeroU8::new(self.bit_depth)
                .ok_or(FrameError::UnsupportedBitDepth { found: 0 })?,
        })
    }
}