use crate::frame::{FrameAccessError, FrameEnvelope, PixelFormat};
#[derive(Debug, thiserror::Error)]
pub enum ConvertError {
#[error("source format already matches target")]
SameFormat,
#[error("unsupported conversion: {from:?} -> {to:?}")]
Unsupported {
from: PixelFormat,
to: PixelFormat,
},
#[error("frame data not accessible: {0}")]
Access(#[from] FrameAccessError),
}
pub fn convert(frame: &FrameEnvelope, target: PixelFormat) -> Result<FrameEnvelope, ConvertError> {
if frame.format() == target {
return Err(ConvertError::SameFormat);
}
let host_bytes = frame.require_host_data()?;
let w = frame.width();
let h = frame.height();
let src_stride = frame.stride();
let src_bpp = match frame.format() {
PixelFormat::Gray8 => 1u32,
PixelFormat::Rgb8 | PixelFormat::Bgr8 => 3,
PixelFormat::Rgba8 => 4,
_ => {
return Err(ConvertError::Unsupported {
from: frame.format(),
to: target,
});
}
};
let required = checked_frame_size(w, h, src_stride, src_bpp).ok_or_else(|| {
ConvertError::Access(FrameAccessError::MaterializationFailed {
detail: format!(
"dimension overflow: {}x{} stride={} bpp={}",
w, h, src_stride, src_bpp,
),
})
})?;
if host_bytes.len() < required {
return Err(ConvertError::Access(
FrameAccessError::MaterializationFailed {
detail: format!(
"frame data too short: {} bytes for {}x{} stride={} bpp={}",
host_bytes.len(),
w,
h,
src_stride,
src_bpp,
),
},
));
}
let converted = match (frame.format(), target) {
(PixelFormat::Bgr8, PixelFormat::Rgb8) | (PixelFormat::Rgb8, PixelFormat::Bgr8) => {
swap_rb(&host_bytes, w, h, src_stride)
}
(PixelFormat::Rgba8, PixelFormat::Rgb8) => rgba_to_rgb(&host_bytes, w, h, src_stride),
(PixelFormat::Rgb8, PixelFormat::Gray8) => rgb_to_gray(&host_bytes, w, h, src_stride),
_ => {
return Err(ConvertError::Unsupported {
from: frame.format(),
to: target,
});
}
};
let out_stride = match target {
PixelFormat::Gray8 => w,
PixelFormat::Rgb8 | PixelFormat::Bgr8 => w.checked_mul(3).ok_or_else(|| {
ConvertError::Access(FrameAccessError::MaterializationFailed {
detail: format!("output stride overflow: width={} bpp=3", w),
})
})?,
PixelFormat::Rgba8 => w.checked_mul(4).ok_or_else(|| {
ConvertError::Access(FrameAccessError::MaterializationFailed {
detail: format!("output stride overflow: width={} bpp=4", w),
})
})?,
_ => {
return Err(ConvertError::Unsupported {
from: frame.format(),
to: target,
});
}
};
Ok(FrameEnvelope::new_owned(
frame.feed_id(),
frame.seq(),
frame.ts(),
frame.wall_ts(),
w,
h,
target,
out_stride,
converted,
frame.metadata().clone(),
))
}
fn checked_frame_size(width: u32, height: u32, stride: u32, bpp: u32) -> Option<usize> {
if height == 0 {
return Some(0);
}
let last_row_bytes = (width as usize).checked_mul(bpp as usize)?;
let prefix_rows = (height as usize).checked_sub(1)?;
let prefix_bytes = prefix_rows.checked_mul(stride as usize)?;
prefix_bytes.checked_add(last_row_bytes)
}
fn pixel_rows(data: &[u8], width: u32, height: u32, stride: u32, bpp: u32) -> Vec<&[u8]> {
let row_bytes = (width as usize) * (bpp as usize);
let stride = stride as usize;
(0..height as usize)
.map(|y| {
let start = y * stride;
&data[start..start + row_bytes]
})
.collect()
}
fn swap_rb(data: &[u8], width: u32, height: u32, stride: u32) -> Vec<u8> {
let rows = pixel_rows(data, width, height, stride, 3);
let mut out = Vec::with_capacity((width * height * 3) as usize);
for row in rows {
for pixel in row.chunks_exact(3) {
out.extend_from_slice(&[pixel[2], pixel[1], pixel[0]]);
}
}
out
}
fn rgba_to_rgb(data: &[u8], width: u32, height: u32, stride: u32) -> Vec<u8> {
let rows = pixel_rows(data, width, height, stride, 4);
let mut out = Vec::with_capacity((width * height * 3) as usize);
for row in rows {
for px in row.chunks_exact(4) {
out.extend_from_slice(&[px[0], px[1], px[2]]);
}
}
out
}
fn rgb_to_gray(data: &[u8], width: u32, height: u32, stride: u32) -> Vec<u8> {
let rows = pixel_rows(data, width, height, stride, 3);
let mut out = Vec::with_capacity((width * height) as usize);
for row in rows {
for px in row.chunks_exact(3) {
let r = px[0] as f32;
let g = px[1] as f32;
let b = px[2] as f32;
out.push((0.299 * r + 0.587 * g + 0.114 * b).round() as u8);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frame::PixelFormat;
use nv_core::{FeedId, MonotonicTs, TypedMetadata, WallTs};
fn make_frame(format: PixelFormat, data: Vec<u8>, w: u32, h: u32) -> FrameEnvelope {
let stride = match format {
PixelFormat::Rgb8 | PixelFormat::Bgr8 => w * 3,
PixelFormat::Rgba8 => w * 4,
PixelFormat::Gray8 => w,
_ => w,
};
FrameEnvelope::new_owned(
FeedId::new(1),
0,
MonotonicTs::ZERO,
WallTs::from_micros(0),
w,
h,
format,
stride,
data,
TypedMetadata::new(),
)
}
#[test]
fn same_format_returns_error() {
let f = make_frame(PixelFormat::Rgb8, vec![0; 12], 2, 2);
assert!(matches!(
convert(&f, PixelFormat::Rgb8),
Err(ConvertError::SameFormat)
));
}
#[test]
fn bgr_to_rgb() {
let data = vec![10, 20, 30, 40, 50, 60];
let f = make_frame(PixelFormat::Bgr8, data, 2, 1);
let converted = convert(&f, PixelFormat::Rgb8).unwrap();
assert_eq!(converted.format(), PixelFormat::Rgb8);
assert_eq!(converted.host_data().unwrap(), &[30, 20, 10, 60, 50, 40]);
}
#[test]
fn bgr_to_rgb_with_stride_padding() {
let data = vec![
10, 20, 30, 40, 50, 60, 0xAA, 0xBB, 70, 80, 90, 11, 22, 33, 0xCC, 0xDD, ];
let f = FrameEnvelope::new_owned(
FeedId::new(1),
0,
MonotonicTs::ZERO,
WallTs::from_micros(0),
2,
2,
PixelFormat::Bgr8,
8, data,
TypedMetadata::new(),
);
let converted = convert(&f, PixelFormat::Rgb8).unwrap();
assert_eq!(
converted.host_data().unwrap(),
&[30, 20, 10, 60, 50, 40, 90, 80, 70, 33, 22, 11]
);
assert_eq!(converted.stride(), 6); }
#[test]
fn conversion_preserves_metadata() {
#[derive(Clone, Debug, PartialEq)]
struct Tag(u32);
let mut meta = TypedMetadata::new();
meta.insert(Tag(42));
let f = FrameEnvelope::new_owned(
FeedId::new(1),
7,
MonotonicTs::ZERO,
WallTs::from_micros(0),
2,
1,
PixelFormat::Bgr8,
6,
vec![10, 20, 30, 40, 50, 60],
meta,
);
let converted = convert(&f, PixelFormat::Rgb8).unwrap();
assert_eq!(converted.metadata().get::<Tag>(), Some(&Tag(42)));
assert_eq!(converted.seq(), 7);
}
}