1use crate::frame::{FrameAccessError, FrameEnvelope, PixelFormat};
8
9#[derive(Debug, thiserror::Error)]
11pub enum ConvertError {
12 #[error("source format already matches target")]
14 SameFormat,
15 #[error("unsupported conversion: {from:?} -> {to:?}")]
17 Unsupported {
18 from: PixelFormat,
20 to: PixelFormat,
22 },
23 #[error("frame data not accessible: {0}")]
25 Access(#[from] FrameAccessError),
26}
27
28pub fn convert(frame: &FrameEnvelope, target: PixelFormat) -> Result<FrameEnvelope, ConvertError> {
48 if frame.format() == target {
49 return Err(ConvertError::SameFormat);
50 }
51
52 let host_bytes = frame.require_host_data()?;
53
54 let w = frame.width();
55 let h = frame.height();
56 let src_stride = frame.stride();
57
58 let src_bpp = match frame.format() {
62 PixelFormat::Gray8 => 1u32,
63 PixelFormat::Rgb8 | PixelFormat::Bgr8 => 3,
64 PixelFormat::Rgba8 => 4,
65 _ => {
66 return Err(ConvertError::Unsupported {
67 from: frame.format(),
68 to: target,
69 });
70 }
71 };
72 let required = checked_frame_size(w, h, src_stride, src_bpp).ok_or_else(|| {
73 ConvertError::Access(FrameAccessError::MaterializationFailed {
74 detail: format!(
75 "dimension overflow: {}x{} stride={} bpp={}",
76 w, h, src_stride, src_bpp,
77 ),
78 })
79 })?;
80 if host_bytes.len() < required {
81 return Err(ConvertError::Access(
82 FrameAccessError::MaterializationFailed {
83 detail: format!(
84 "frame data too short: {} bytes for {}x{} stride={} bpp={}",
85 host_bytes.len(),
86 w,
87 h,
88 src_stride,
89 src_bpp,
90 ),
91 },
92 ));
93 }
94
95 let converted = match (frame.format(), target) {
96 (PixelFormat::Bgr8, PixelFormat::Rgb8) | (PixelFormat::Rgb8, PixelFormat::Bgr8) => {
97 swap_rb(&host_bytes, w, h, src_stride)
98 }
99 (PixelFormat::Rgba8, PixelFormat::Rgb8) => rgba_to_rgb(&host_bytes, w, h, src_stride),
100 (PixelFormat::Rgb8, PixelFormat::Gray8) => rgb_to_gray(&host_bytes, w, h, src_stride),
101 _ => {
102 return Err(ConvertError::Unsupported {
103 from: frame.format(),
104 to: target,
105 });
106 }
107 };
108
109 let out_stride = match target {
110 PixelFormat::Gray8 => w,
111 PixelFormat::Rgb8 | PixelFormat::Bgr8 => w.checked_mul(3).ok_or_else(|| {
112 ConvertError::Access(FrameAccessError::MaterializationFailed {
113 detail: format!("output stride overflow: width={} bpp=3", w),
114 })
115 })?,
116 PixelFormat::Rgba8 => w.checked_mul(4).ok_or_else(|| {
117 ConvertError::Access(FrameAccessError::MaterializationFailed {
118 detail: format!("output stride overflow: width={} bpp=4", w),
119 })
120 })?,
121 _ => {
122 return Err(ConvertError::Unsupported {
123 from: frame.format(),
124 to: target,
125 });
126 }
127 };
128
129 Ok(FrameEnvelope::new_owned(
130 frame.feed_id(),
131 frame.seq(),
132 frame.ts(),
133 frame.wall_ts(),
134 w,
135 h,
136 target,
137 out_stride,
138 converted,
139 frame.metadata().clone(),
140 ))
141}
142
143fn checked_frame_size(width: u32, height: u32, stride: u32, bpp: u32) -> Option<usize> {
146 if height == 0 {
147 return Some(0);
148 }
149 let last_row_bytes = (width as usize).checked_mul(bpp as usize)?;
150 let prefix_rows = (height as usize).checked_sub(1)?;
151 let prefix_bytes = prefix_rows.checked_mul(stride as usize)?;
152 prefix_bytes.checked_add(last_row_bytes)
153}
154
155fn pixel_rows(data: &[u8], width: u32, height: u32, stride: u32, bpp: u32) -> Vec<&[u8]> {
167 let row_bytes = (width as usize) * (bpp as usize);
168 let stride = stride as usize;
169 (0..height as usize)
170 .map(|y| {
171 let start = y * stride;
172 &data[start..start + row_bytes]
173 })
174 .collect()
175}
176
177fn swap_rb(data: &[u8], width: u32, height: u32, stride: u32) -> Vec<u8> {
179 let rows = pixel_rows(data, width, height, stride, 3);
180 let mut out = Vec::with_capacity((width * height * 3) as usize);
181 for row in rows {
182 for pixel in row.chunks_exact(3) {
183 out.extend_from_slice(&[pixel[2], pixel[1], pixel[0]]);
184 }
185 }
186 out
187}
188
189fn rgba_to_rgb(data: &[u8], width: u32, height: u32, stride: u32) -> Vec<u8> {
191 let rows = pixel_rows(data, width, height, stride, 4);
192 let mut out = Vec::with_capacity((width * height * 3) as usize);
193 for row in rows {
194 for px in row.chunks_exact(4) {
195 out.extend_from_slice(&[px[0], px[1], px[2]]);
196 }
197 }
198 out
199}
200
201fn rgb_to_gray(data: &[u8], width: u32, height: u32, stride: u32) -> Vec<u8> {
203 let rows = pixel_rows(data, width, height, stride, 3);
204 let mut out = Vec::with_capacity((width * height) as usize);
205 for row in rows {
206 for px in row.chunks_exact(3) {
207 let r = px[0] as f32;
208 let g = px[1] as f32;
209 let b = px[2] as f32;
210 out.push((0.299 * r + 0.587 * g + 0.114 * b).round() as u8);
211 }
212 }
213 out
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::frame::PixelFormat;
220 use nv_core::{FeedId, MonotonicTs, TypedMetadata, WallTs};
221
222 fn make_frame(format: PixelFormat, data: Vec<u8>, w: u32, h: u32) -> FrameEnvelope {
223 let stride = match format {
224 PixelFormat::Rgb8 | PixelFormat::Bgr8 => w * 3,
225 PixelFormat::Rgba8 => w * 4,
226 PixelFormat::Gray8 => w,
227 _ => w,
228 };
229 FrameEnvelope::new_owned(
230 FeedId::new(1),
231 0,
232 MonotonicTs::ZERO,
233 WallTs::from_micros(0),
234 w,
235 h,
236 format,
237 stride,
238 data,
239 TypedMetadata::new(),
240 )
241 }
242
243 #[test]
244 fn same_format_returns_error() {
245 let f = make_frame(PixelFormat::Rgb8, vec![0; 12], 2, 2);
246 assert!(matches!(
247 convert(&f, PixelFormat::Rgb8),
248 Err(ConvertError::SameFormat)
249 ));
250 }
251
252 #[test]
253 fn bgr_to_rgb() {
254 let data = vec![10, 20, 30, 40, 50, 60];
255 let f = make_frame(PixelFormat::Bgr8, data, 2, 1);
256 let converted = convert(&f, PixelFormat::Rgb8).unwrap();
257 assert_eq!(converted.format(), PixelFormat::Rgb8);
258 assert_eq!(converted.host_data().unwrap(), &[30, 20, 10, 60, 50, 40]);
259 }
260
261 #[test]
262 fn bgr_to_rgb_with_stride_padding() {
263 let data = vec![
265 10, 20, 30, 40, 50, 60, 0xAA, 0xBB, 70, 80, 90, 11, 22, 33, 0xCC, 0xDD, ];
268 let f = FrameEnvelope::new_owned(
269 FeedId::new(1),
270 0,
271 MonotonicTs::ZERO,
272 WallTs::from_micros(0),
273 2,
274 2,
275 PixelFormat::Bgr8,
276 8, data,
278 TypedMetadata::new(),
279 );
280 let converted = convert(&f, PixelFormat::Rgb8).unwrap();
281 assert_eq!(
283 converted.host_data().unwrap(),
284 &[30, 20, 10, 60, 50, 40, 90, 80, 70, 33, 22, 11]
285 );
286 assert_eq!(converted.stride(), 6); }
288
289 #[test]
290 fn conversion_preserves_metadata() {
291 #[derive(Clone, Debug, PartialEq)]
292 struct Tag(u32);
293
294 let mut meta = TypedMetadata::new();
295 meta.insert(Tag(42));
296
297 let f = FrameEnvelope::new_owned(
298 FeedId::new(1),
299 7,
300 MonotonicTs::ZERO,
301 WallTs::from_micros(0),
302 2,
303 1,
304 PixelFormat::Bgr8,
305 6,
306 vec![10, 20, 30, 40, 50, 60],
307 meta,
308 );
309 let converted = convert(&f, PixelFormat::Rgb8).unwrap();
310 assert_eq!(converted.metadata().get::<Tag>(), Some(&Tag(42)));
311 assert_eq!(converted.seq(), 7);
312 }
313}