Skip to main content

moq_video/
convert.rs

1//! CPU conversion of native video surfaces to packed pixels.
2//!
3//! [`Surface::into_rgba`](crate::Surface::into_rgba) is the portable rendering
4//! exit: it downloads a GPU surface when necessary, applies the surface's color
5//! space, and returns owned RGBA pixels for an image or UI toolkit.
6
7use yuv::{YuvPlanarImage, yuv420_to_rgba};
8
9use crate::{Color, Error, Size, Surface};
10
11/// CPU surface conversion options.
12///
13/// `#[non_exhaustive]`: build via [`Config::new`] (or `default()`) and set the
14/// fields you care about, so future output options stay additive.
15#[derive(Clone, Debug, Default)]
16#[non_exhaustive]
17pub struct Config {
18	/// How to interpret the source's YUV samples, overriding its own metadata.
19	///
20	/// `None` uses [`Surface::color`] and falls back to [`Color::infer`] when the
21	/// decoder or native surface carries no color description.
22	pub color: Option<Color>,
23}
24
25impl Config {
26	/// A default config that honors surface metadata and otherwise infers color.
27	pub fn new() -> Self {
28		Self::default()
29	}
30}
31
32/// Owned, tightly packed RGBA8 pixels in row-major order.
33#[derive(Clone)]
34pub struct Rgba {
35	width: u32,
36	height: u32,
37	stride: usize,
38	data: Vec<u8>,
39}
40
41impl Rgba {
42	/// Image width in pixels.
43	pub fn width(&self) -> u32 {
44		self.width
45	}
46
47	/// Image height in pixels.
48	pub fn height(&self) -> u32 {
49		self.height
50	}
51
52	/// Bytes between adjacent rows, always `width * 4`.
53	pub fn stride(&self) -> usize {
54		self.stride
55	}
56
57	/// Tightly packed RGBA8 pixels.
58	pub fn data(&self) -> &[u8] {
59		&self.data
60	}
61
62	/// Consume the image and return its tightly packed RGBA8 pixels.
63	pub fn into_data(self) -> Vec<u8> {
64		self.data
65	}
66}
67
68pub(crate) fn rgba(surface: Surface, config: &Config) -> Result<Rgba, Error> {
69	let size = Size::new(surface.width(), surface.height());
70	let color = config
71		.color
72		.or_else(|| surface.color())
73		.unwrap_or_else(|| Color::infer(size));
74	let i420 = surface.into_i420()?;
75	let luma = usize::try_from(size.pixels())
76		.map_err(|_| Error::Codec(anyhow::anyhow!("RGBA frame {size}: dimensions too large to represent")))?;
77	let stride = size.width.checked_mul(4).ok_or_else(|| {
78		Error::Codec(anyhow::anyhow!(
79			"RGBA frame {size}: row stride is too large to represent"
80		))
81	})?;
82	let stride_usize = usize::try_from(stride).map_err(|_| {
83		Error::Codec(anyhow::anyhow!(
84			"RGBA frame {size}: row stride is too large to represent"
85		))
86	})?;
87	let len = stride_usize.checked_mul(size.height as usize).ok_or_else(|| {
88		Error::Codec(anyhow::anyhow!(
89			"RGBA frame {size}: byte length is too large to represent"
90		))
91	})?;
92	let chroma = luma / 4;
93	let planar = YuvPlanarImage {
94		y_plane: &i420[..luma],
95		y_stride: size.width,
96		u_plane: &i420[luma..luma + chroma],
97		u_stride: size.width / 2,
98		v_plane: &i420[luma + chroma..],
99		v_stride: size.width / 2,
100		width: size.width,
101		height: size.height,
102	};
103	let mut data = vec![0; len];
104	let (range, matrix) = color.yuv();
105	yuv420_to_rgba(&planar, &mut data, stride, range, matrix)
106		.map_err(|e| Error::Codec(anyhow::anyhow!("yuv420_to_rgba failed for {size}: {e}")))?;
107
108	Ok(Rgba {
109		width: size.width,
110		height: size.height,
111		stride: stride_usize,
112		data,
113	})
114}
115
116#[cfg(test)]
117mod tests {
118	use super::*;
119	use crate::I420;
120
121	/// A resized surface keeps its original matrix even after crossing the
122	/// standard-definition boundary. Ignoring that metadata tints saturated
123	/// colors while leaving grayscale test images apparently correct.
124	#[test]
125	fn conversion_uses_the_surface_color() {
126		let source_size = Size::new(64, 64);
127		let red = [255u8, 0, 0, 255].repeat(source_size.pixels() as usize);
128		let source = I420::from_rgba(&red, source_size.width * 4, source_size.width, source_size.height).unwrap();
129		let source = source.resize(1280, 720).unwrap();
130		assert_eq!(source.color(), Some(Color::Bt601Limited));
131		assert_eq!(Color::infer(Size::new(1280, 720)), Color::Bt709Limited);
132
133		let image = rgba(Surface::I420(source), &Config::default()).unwrap();
134		let center = (image.height as usize / 2 * image.stride) + image.width as usize / 2 * 4;
135		let pixel = &image.data[center..center + 4];
136		assert!(pixel[0] >= 250, "red channel drifted: {pixel:?}");
137		assert!(pixel[1] <= 2 && pixel[2] <= 2, "surface matrix was ignored: {pixel:?}");
138		assert_eq!(pixel[3], 255);
139	}
140
141	#[test]
142	fn conversion_reports_a_tightly_packed_layout() {
143		let size = Size::new(64, 32);
144		let surface = Surface::I420(I420::new(size.width, size.height, vec![128; I420::len(64, 32)]).unwrap());
145
146		let image = rgba(surface, &Config::default()).unwrap();
147		assert_eq!(image.width(), size.width);
148		assert_eq!(image.height(), size.height);
149		assert_eq!(image.stride(), size.width as usize * 4);
150		assert_eq!(image.data().len(), image.stride() * size.height as usize);
151	}
152}