1use yuv::{YuvPlanarImage, yuv420_to_rgba};
8
9use crate::{Color, Error, Size, Surface};
10
11#[derive(Clone, Debug, Default)]
16#[non_exhaustive]
17pub struct Config {
18 pub color: Option<Color>,
23}
24
25impl Config {
26 pub fn new() -> Self {
28 Self::default()
29 }
30}
31
32#[derive(Clone)]
34pub struct Rgba {
35 width: u32,
36 height: u32,
37 stride: usize,
38 data: Vec<u8>,
39}
40
41impl Rgba {
42 pub fn width(&self) -> u32 {
44 self.width
45 }
46
47 pub fn height(&self) -> u32 {
49 self.height
50 }
51
52 pub fn stride(&self) -> usize {
54 self.stride
55 }
56
57 pub fn data(&self) -> &[u8] {
59 &self.data
60 }
61
62 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 #[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}