feat(convert): add BGRx to RGB24 conversion with downscaling
Box filter for integer scale factors, fixed-point area averaging otherwise. The source view is bounds-checked so a short or misdescribed PipeWire buffer is reported instead of read out of range.
This commit is contained in:
+501
@@ -0,0 +1,501 @@
|
|||||||
|
//! BGRx → RGB24 conversion with optional downscaling (box filter for integer factors,
|
||||||
|
//! area averaging otherwise).
|
||||||
|
|
||||||
|
/// Fixed-point unit of the area-average weights.
|
||||||
|
const WEIGHT_BITS: u32 = 16;
|
||||||
|
const WEIGHT_ONE: u32 = 1 << WEIGHT_BITS;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
|
||||||
|
pub enum ConvertError {
|
||||||
|
#[error("empty frame {width}x{height}")]
|
||||||
|
Empty { width: usize, height: usize },
|
||||||
|
#[error("stride {stride} is smaller than width*4 ({width}*4)")]
|
||||||
|
StrideTooSmall { stride: usize, width: usize },
|
||||||
|
#[error("{width}x{height} frame with stride {stride} at offset {offset} does not fit into {mapped} mapped bytes")]
|
||||||
|
OutOfBounds {
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
stride: usize,
|
||||||
|
offset: usize,
|
||||||
|
mapped: usize,
|
||||||
|
},
|
||||||
|
#[error("chunk size {chunk_size} is smaller than stride*height ({needed})")]
|
||||||
|
ChunkTooSmall { chunk_size: usize, needed: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A bounds-checked view of a packed BGRx frame.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct BgrxFrame<'a> {
|
||||||
|
/// Starts at the first pixel of row 0.
|
||||||
|
data: &'a [u8],
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
stride: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BgrxFrame<'a> {
|
||||||
|
/// Validates the frame geometry against the mapped memory and the chunk size.
|
||||||
|
pub fn new(
|
||||||
|
mapped: &'a [u8],
|
||||||
|
offset: usize,
|
||||||
|
chunk_size: usize,
|
||||||
|
stride: usize,
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
) -> Result<Self, ConvertError> {
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
return Err(ConvertError::Empty { width, height });
|
||||||
|
}
|
||||||
|
let row_bytes = width
|
||||||
|
.checked_mul(4)
|
||||||
|
.ok_or(ConvertError::StrideTooSmall { stride, width })?;
|
||||||
|
if stride < row_bytes {
|
||||||
|
return Err(ConvertError::StrideTooSmall { stride, width });
|
||||||
|
}
|
||||||
|
let out_of_bounds = ConvertError::OutOfBounds {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
stride,
|
||||||
|
offset,
|
||||||
|
mapped: mapped.len(),
|
||||||
|
};
|
||||||
|
let end = stride
|
||||||
|
.checked_mul(height - 1)
|
||||||
|
.and_then(|v| v.checked_add(row_bytes))
|
||||||
|
.and_then(|v| v.checked_add(offset))
|
||||||
|
.ok_or_else(|| out_of_bounds.clone())?;
|
||||||
|
if end > mapped.len() {
|
||||||
|
return Err(out_of_bounds);
|
||||||
|
}
|
||||||
|
match stride.checked_mul(height) {
|
||||||
|
Some(needed) if chunk_size >= needed => {}
|
||||||
|
needed => {
|
||||||
|
return Err(ConvertError::ChunkTooSmall {
|
||||||
|
chunk_size,
|
||||||
|
needed: needed.unwrap_or(usize::MAX),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
data: &mapped[offset..end],
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
stride,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(&self) -> usize {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(&self) -> usize {
|
||||||
|
self.height
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row(&self, y: usize) -> &'a [u8] {
|
||||||
|
let start = y * self.stride;
|
||||||
|
&self.data[start..start + self.width * 4]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Largest size that fits into `max_w`x`max_h` with the aspect ratio of `src_w`x`src_h`.
|
||||||
|
/// Never upscales.
|
||||||
|
pub fn fit_within(src_w: u32, src_h: u32, max_w: u32, max_h: u32) -> (u32, u32) {
|
||||||
|
if src_w <= max_w && src_h <= max_h {
|
||||||
|
return (src_w, src_h);
|
||||||
|
}
|
||||||
|
let scale = f64::min(
|
||||||
|
f64::from(max_w) / f64::from(src_w),
|
||||||
|
f64::from(max_h) / f64::from(src_h),
|
||||||
|
);
|
||||||
|
let w = (f64::from(src_w) * scale)
|
||||||
|
.round()
|
||||||
|
.clamp(1.0, f64::from(max_w)) as u32;
|
||||||
|
let h = (f64::from(src_h) * scale)
|
||||||
|
.round()
|
||||||
|
.clamp(1.0, f64::from(max_h)) as u32;
|
||||||
|
(w, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Source taps of one axis for area averaging: for destination index `i`,
|
||||||
|
/// `taps[starts[i]..starts[i + 1]]` holds `(source index, weight)` with weights summing
|
||||||
|
/// to `WEIGHT_ONE`.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct AxisTaps {
|
||||||
|
starts: Vec<usize>,
|
||||||
|
taps: Vec<(usize, u32)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AxisTaps {
|
||||||
|
fn build(src: usize, dst: usize) -> Self {
|
||||||
|
let scale = src as f64 / dst as f64;
|
||||||
|
let mut starts = Vec::with_capacity(dst + 1);
|
||||||
|
let mut taps = Vec::new();
|
||||||
|
for i in 0..dst {
|
||||||
|
starts.push(taps.len());
|
||||||
|
let begin = i as f64 * scale;
|
||||||
|
let end = (i + 1) as f64 * scale;
|
||||||
|
let first = begin.floor() as usize;
|
||||||
|
let last = (end.ceil() as usize).min(src);
|
||||||
|
let from = taps.len();
|
||||||
|
for j in first..last {
|
||||||
|
let overlap = end.min(j as f64 + 1.0) - begin.max(j as f64);
|
||||||
|
let weight = (overlap / scale * f64::from(WEIGHT_ONE)).round() as u32;
|
||||||
|
if weight > 0 {
|
||||||
|
taps.push((j, weight));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Make the weights sum to exactly WEIGHT_ONE so flat areas stay flat.
|
||||||
|
let sum: u32 = taps[from..].iter().map(|t| t.1).sum();
|
||||||
|
if let Some(max) = taps[from..].iter_mut().max_by_key(|t| t.1) {
|
||||||
|
max.1 = (max.1 + WEIGHT_ONE).saturating_sub(sum);
|
||||||
|
} else {
|
||||||
|
taps.push((first.min(src - 1), WEIGHT_ONE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
starts.push(taps.len());
|
||||||
|
Self { starts, taps }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, i: usize) -> &[(usize, u32)] {
|
||||||
|
&self.taps[self.starts[i]..self.starts[i + 1]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reusable converter; keeps its scratch buffers and filter taps between frames.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Converter {
|
||||||
|
geometry: (usize, usize, usize, usize),
|
||||||
|
x_taps: AxisTaps,
|
||||||
|
y_taps: AxisTaps,
|
||||||
|
row: Vec<u32>,
|
||||||
|
acc: Vec<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Converter {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts `src` to RGB24 of `dst_w`x`dst_h` into `out` (resized as needed).
|
||||||
|
pub fn convert(&mut self, src: &BgrxFrame<'_>, dst_w: usize, dst_h: usize, out: &mut Vec<u8>) {
|
||||||
|
assert!(dst_w > 0 && dst_h > 0, "destination must not be empty");
|
||||||
|
out.resize(dst_w * dst_h * 3, 0);
|
||||||
|
let (sw, sh) = (src.width, src.height);
|
||||||
|
if (sw, sh) == (dst_w, dst_h) {
|
||||||
|
copy_rgb(src, out);
|
||||||
|
} else if sw % dst_w == 0 && sh % dst_h == 0 {
|
||||||
|
self.box_filter(src, sw / dst_w, sh / dst_h, dst_w, out);
|
||||||
|
} else {
|
||||||
|
self.area_average(src, dst_w, dst_h, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn box_filter(
|
||||||
|
&mut self,
|
||||||
|
src: &BgrxFrame<'_>,
|
||||||
|
fx: usize,
|
||||||
|
fy: usize,
|
||||||
|
dst_w: usize,
|
||||||
|
out: &mut [u8],
|
||||||
|
) {
|
||||||
|
let n = (fx * fy) as u32;
|
||||||
|
let sums = &mut self.row;
|
||||||
|
sums.clear();
|
||||||
|
sums.resize(dst_w * 3, 0);
|
||||||
|
for (dy, out_row) in out.chunks_exact_mut(dst_w * 3).enumerate() {
|
||||||
|
sums.fill(0);
|
||||||
|
for sy in dy * fy..(dy + 1) * fy {
|
||||||
|
let row = src.row(sy);
|
||||||
|
for (block, sum) in row.chunks_exact(fx * 4).zip(sums.chunks_exact_mut(3)) {
|
||||||
|
for px in block.chunks_exact(4) {
|
||||||
|
sum[0] += u32::from(px[2]);
|
||||||
|
sum[1] += u32::from(px[1]);
|
||||||
|
sum[2] += u32::from(px[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (o, s) in out_row.iter_mut().zip(sums.iter()) {
|
||||||
|
*o = ((s + n / 2) / n) as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn area_average(&mut self, src: &BgrxFrame<'_>, dst_w: usize, dst_h: usize, out: &mut [u8]) {
|
||||||
|
let geometry = (src.width, src.height, dst_w, dst_h);
|
||||||
|
if self.geometry != geometry {
|
||||||
|
self.x_taps = AxisTaps::build(src.width, dst_w);
|
||||||
|
self.y_taps = AxisTaps::build(src.height, dst_h);
|
||||||
|
self.geometry = geometry;
|
||||||
|
}
|
||||||
|
self.row.clear();
|
||||||
|
self.row.resize(dst_w * 3, 0);
|
||||||
|
self.acc.clear();
|
||||||
|
self.acc.resize(dst_w * 3, 0);
|
||||||
|
|
||||||
|
let round = 1u64 << (2 * WEIGHT_BITS - 1);
|
||||||
|
for (dy, out_row) in out.chunks_exact_mut(dst_w * 3).enumerate() {
|
||||||
|
self.acc.fill(0);
|
||||||
|
for &(sy, wy) in self.y_taps.get(dy) {
|
||||||
|
let src_row = src.row(sy);
|
||||||
|
// Horizontal pass: values in [0, 255 * WEIGHT_ONE].
|
||||||
|
for (dx, rgb) in self.row.chunks_exact_mut(3).enumerate() {
|
||||||
|
let (mut r, mut g, mut b) = (0u32, 0u32, 0u32);
|
||||||
|
for &(sx, wx) in self.x_taps.get(dx) {
|
||||||
|
let px = &src_row[sx * 4..sx * 4 + 4];
|
||||||
|
r += wx * u32::from(px[2]);
|
||||||
|
g += wx * u32::from(px[1]);
|
||||||
|
b += wx * u32::from(px[0]);
|
||||||
|
}
|
||||||
|
rgb[0] = r;
|
||||||
|
rgb[1] = g;
|
||||||
|
rgb[2] = b;
|
||||||
|
}
|
||||||
|
let wy = u64::from(wy);
|
||||||
|
for (a, v) in self.acc.iter_mut().zip(self.row.iter()) {
|
||||||
|
*a += wy * u64::from(*v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (o, a) in out_row.iter_mut().zip(self.acc.iter()) {
|
||||||
|
*o = ((a + round) >> (2 * WEIGHT_BITS)) as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_rgb(src: &BgrxFrame<'_>, out: &mut [u8]) {
|
||||||
|
for (y, out_row) in out.chunks_exact_mut(src.width * 3).enumerate() {
|
||||||
|
for (px, rgb) in src.row(y).chunks_exact(4).zip(out_row.chunks_exact_mut(3)) {
|
||||||
|
rgb[0] = px[2];
|
||||||
|
rgb[1] = px[1];
|
||||||
|
rgb[2] = px[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Builds a BGRx buffer with padding and offset filled with garbage.
|
||||||
|
fn make_buffer(
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
stride: usize,
|
||||||
|
offset: usize,
|
||||||
|
pixel: impl Fn(usize, usize) -> [u8; 3],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut buf = vec![0xEE; offset + stride * height];
|
||||||
|
for y in 0..height {
|
||||||
|
for x in 0..width {
|
||||||
|
let [r, g, b] = pixel(x, y);
|
||||||
|
let i = offset + y * stride + x * 4;
|
||||||
|
buf[i..i + 4].copy_from_slice(&[b, g, r, 0xFF]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert(
|
||||||
|
buf: &[u8],
|
||||||
|
offset: usize,
|
||||||
|
stride: usize,
|
||||||
|
w: usize,
|
||||||
|
h: usize,
|
||||||
|
dw: usize,
|
||||||
|
dh: usize,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let frame = BgrxFrame::new(buf, offset, buf.len() - offset, stride, w, h).unwrap();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
Converter::new().convert(&frame, dw, dh, &mut out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reference area average in floating point.
|
||||||
|
fn reference(
|
||||||
|
w: usize,
|
||||||
|
h: usize,
|
||||||
|
dw: usize,
|
||||||
|
dh: usize,
|
||||||
|
pixel: impl Fn(usize, usize) -> [u8; 3],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let (sx, sy) = (w as f64 / dw as f64, h as f64 / dh as f64);
|
||||||
|
let overlap =
|
||||||
|
|a0: f64, a1: f64, j: usize| (a1.min(j as f64 + 1.0) - a0.max(j as f64)).max(0.0);
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for dy in 0..dh {
|
||||||
|
for dx in 0..dw {
|
||||||
|
let (x0, x1, y0, y1) = (
|
||||||
|
dx as f64 * sx,
|
||||||
|
(dx + 1) as f64 * sx,
|
||||||
|
dy as f64 * sy,
|
||||||
|
(dy + 1) as f64 * sy,
|
||||||
|
);
|
||||||
|
let mut acc = [0f64; 3];
|
||||||
|
let xs = x0.floor() as usize..(x1.ceil() as usize).min(w);
|
||||||
|
for y in y0.floor() as usize..(y1.ceil() as usize).min(h) {
|
||||||
|
for x in xs.clone() {
|
||||||
|
let wgt = overlap(x0, x1, x) * overlap(y0, y1, y);
|
||||||
|
let p = pixel(x, y);
|
||||||
|
for c in 0..3 {
|
||||||
|
acc[c] += wgt * f64::from(p[c]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for a in acc {
|
||||||
|
out.push((a / (sx * sy)).round() as u8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pattern(x: usize, y: usize) -> [u8; 3] {
|
||||||
|
[
|
||||||
|
(x * 37 + y * 11) as u8,
|
||||||
|
(x * 5 + y * 71) as u8,
|
||||||
|
((x ^ y) * 29) as u8,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identity_with_padding_and_offset() {
|
||||||
|
let (w, h, stride, offset) = (3, 2, 16, 5);
|
||||||
|
let buf = make_buffer(w, h, stride, offset, pattern);
|
||||||
|
let out = convert(&buf, offset, stride, w, h, w, h);
|
||||||
|
let expected: Vec<u8> = (0..h)
|
||||||
|
.flat_map(|y| (0..w).flat_map(move |x| pattern(x, y)))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(out, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identity_odd_size() {
|
||||||
|
let (w, h) = (7, 5);
|
||||||
|
let buf = make_buffer(w, h, w * 4, 0, pattern);
|
||||||
|
let out = convert(&buf, 0, w * 4, w, h, w, h);
|
||||||
|
let expected: Vec<u8> = (0..h)
|
||||||
|
.flat_map(|y| (0..w).flat_map(move |x| pattern(x, y)))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(out, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn box_filter_factor_2() {
|
||||||
|
let values = [
|
||||||
|
[0u8, 10, 20, 30],
|
||||||
|
[40, 50, 60, 70],
|
||||||
|
[100, 100, 0, 0],
|
||||||
|
[100, 101, 0, 0],
|
||||||
|
];
|
||||||
|
let buf = make_buffer(4, 4, 16, 0, |x, y| [values[y][x], 255 - values[y][x], 7]);
|
||||||
|
let out = convert(&buf, 0, 16, 4, 4, 2, 2);
|
||||||
|
// (0+10+40+50)/4 = 25, (20+30+60+70)/4 = 45, (100+100+100+101)/4 = 100.25, 0
|
||||||
|
let r: Vec<u8> = out.chunks(3).map(|p| p[0]).collect();
|
||||||
|
assert_eq!(r, [25, 45, 100, 0]);
|
||||||
|
let g: Vec<u8> = out.chunks(3).map(|p| p[1]).collect();
|
||||||
|
assert_eq!(g, [230, 210, 155, 255]);
|
||||||
|
assert!(out.chunks(3).all(|p| p[2] == 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn box_filter_factor_3_with_padding() {
|
||||||
|
let (w, h, stride, offset) = (6, 3, 28, 8);
|
||||||
|
let buf = make_buffer(w, h, stride, offset, |x, y| [(x + y * 6) as u8 * 10, 0, 0]);
|
||||||
|
let out = convert(&buf, offset, stride, w, h, 2, 1);
|
||||||
|
// Left block: x 0..3, all rows: mean of {0,10,20,60,70,80,120,130,140} = 70
|
||||||
|
// Right block: +30 = 100
|
||||||
|
assert_eq!(out, [70, 0, 0, 100, 0, 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn area_average_non_integer_known_values() {
|
||||||
|
let values = [0u8, 90, 180];
|
||||||
|
let buf = make_buffer(3, 1, 12, 0, |x, _| [values[x], values[x], values[x]]);
|
||||||
|
let out = convert(&buf, 0, 12, 3, 1, 2, 1);
|
||||||
|
assert_eq!(out, [30, 30, 30, 150, 150, 150]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn area_average_matches_reference() {
|
||||||
|
for &(w, h, dw, dh) in &[
|
||||||
|
(5, 3, 2, 2),
|
||||||
|
(322, 182, 320, 181),
|
||||||
|
(17, 9, 4, 3),
|
||||||
|
(10, 10, 3, 7),
|
||||||
|
] {
|
||||||
|
let buf = make_buffer(w, h, w * 4, 0, pattern);
|
||||||
|
let out = convert(&buf, 0, w * 4, w, h, dw, dh);
|
||||||
|
let expected = reference(w, h, dw, dh, pattern);
|
||||||
|
assert_eq!(out.len(), expected.len());
|
||||||
|
for (i, (a, b)) in out.iter().zip(expected.iter()).enumerate() {
|
||||||
|
assert!(
|
||||||
|
a.abs_diff(*b) <= 1,
|
||||||
|
"{w}x{h}->{dw}x{dh} byte {i}: {a} vs {b}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flat_color_stays_flat() {
|
||||||
|
let buf = make_buffer(641, 359, 641 * 4, 0, |_, _| [200, 17, 255]);
|
||||||
|
let out = convert(&buf, 0, 641 * 4, 641, 359, 320, 180);
|
||||||
|
assert!(out.chunks(3).all(|p| p == [200, 17, 255]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converter_reuse_across_geometries() {
|
||||||
|
let mut conv = Converter::new();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for &(w, h, dw, dh) in &[(9, 9, 4, 4), (8, 8, 4, 4), (9, 9, 4, 4), (5, 5, 5, 5)] {
|
||||||
|
let buf = make_buffer(w, h, w * 4, 0, pattern);
|
||||||
|
let frame = BgrxFrame::new(&buf, 0, buf.len(), w * 4, w, h).unwrap();
|
||||||
|
conv.convert(&frame, dw, dh, &mut out);
|
||||||
|
let expected = reference(w, h, dw, dh, pattern);
|
||||||
|
assert!(out
|
||||||
|
.iter()
|
||||||
|
.zip(expected.iter())
|
||||||
|
.all(|(a, b)| a.abs_diff(*b) <= 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bounds_violations_are_errors() {
|
||||||
|
let buf = vec![0u8; 64];
|
||||||
|
assert!(matches!(
|
||||||
|
BgrxFrame::new(&buf, 0, 64, 16, 0, 4),
|
||||||
|
Err(ConvertError::Empty { .. })
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
BgrxFrame::new(&buf, 0, 64, 15, 4, 4),
|
||||||
|
Err(ConvertError::StrideTooSmall { .. })
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
BgrxFrame::new(&buf, 1, 64, 16, 4, 4),
|
||||||
|
Err(ConvertError::OutOfBounds { .. })
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
BgrxFrame::new(&buf, 0, 63, 16, 4, 4),
|
||||||
|
Err(ConvertError::ChunkTooSmall { .. })
|
||||||
|
));
|
||||||
|
assert!(BgrxFrame::new(&buf, 0, 64, 16, 4, 4).is_ok());
|
||||||
|
// Last row without padding still fits.
|
||||||
|
assert!(BgrxFrame::new(&buf[..60], 0, 64, 16, 3, 4).is_ok());
|
||||||
|
// Overflowing geometry must not panic.
|
||||||
|
assert!(BgrxFrame::new(&buf, usize::MAX, 64, 16, 4, 4).is_err());
|
||||||
|
assert!(BgrxFrame::new(&buf, 0, usize::MAX, usize::MAX, 4, usize::MAX).is_err());
|
||||||
|
assert!(BgrxFrame::new(&buf, 0, 64, 16, usize::MAX, 1).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fit_within_keeps_aspect() {
|
||||||
|
assert_eq!(fit_within(3840, 2160, 320, 180), (320, 180));
|
||||||
|
assert_eq!(fit_within(640, 360, 320, 180), (320, 180));
|
||||||
|
assert_eq!(fit_within(320, 134, 320, 180), (320, 134));
|
||||||
|
assert_eq!(fit_within(322, 182, 321, 181), (320, 181));
|
||||||
|
assert_eq!(fit_within(1080, 1920, 320, 180), (101, 180));
|
||||||
|
assert_eq!(fit_within(100, 100, 320, 180), (100, 100));
|
||||||
|
assert_eq!(fit_within(10000, 1, 320, 180), (320, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
//! HyperHDR's flatbuffers server.
|
//! HyperHDR's flatbuffers server.
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod convert;
|
||||||
pub mod hyperhdr;
|
pub mod hyperhdr;
|
||||||
pub mod pacing;
|
pub mod pacing;
|
||||||
pub mod slot;
|
pub mod slot;
|
||||||
|
|||||||
Reference in New Issue
Block a user