#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum Error {
#[error("CFA split needs even dimensions, got {width}×{height}")]
OddDimensions { width: u32, height: u32 },
#[error("expected {expected} bytes of mosaic data, got {actual}")]
WrongLength { expected: usize, actual: usize },
}
pub type Result<T> = std::result::Result<T, Error>;
pub const PLANE_COUNT: usize = 4;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Planes {
pub width: u32,
pub height: u32,
pub bytes_per_sample: usize,
pub planes: [Vec<u8>; PLANE_COUNT],
}
pub fn split(mosaic: &[u8], width: u32, height: u32, bytes_per_sample: usize) -> Result<Planes> {
if !width.is_multiple_of(2) || !height.is_multiple_of(2) {
return Err(Error::OddDimensions { width, height });
}
let bps = bytes_per_sample;
let expected = width as usize * height as usize * bps;
if mosaic.len() != expected {
return Err(Error::WrongLength {
expected,
actual: mosaic.len(),
});
}
let (w, h) = (width as usize, height as usize);
let (hw, hh) = (w / 2, h / 2);
let mut planes: [Vec<u8>; PLANE_COUNT] =
std::array::from_fn(|_| Vec::with_capacity(hw * hh * bps));
for y in 0..h {
let row = &mosaic[y * w * bps..(y + 1) * w * bps];
let pair = (y % 2) * 2;
for x in 0..w {
planes[pair + (x % 2)].extend_from_slice(&row[x * bps..(x + 1) * bps]);
}
}
Ok(Planes {
width: hw as u32,
height: hh as u32,
bytes_per_sample: bps,
planes,
})
}
pub fn merge(planes: &Planes) -> Vec<u8> {
let bps = planes.bytes_per_sample;
let (hw, hh) = (planes.width as usize, planes.height as usize);
let (w, h) = (hw * 2, hh * 2);
let mut out = vec![0u8; w * h * bps];
for y in 0..h {
let plane_row = (y / 2) * hw * bps;
let pair = (y % 2) * 2;
for x in 0..w {
let src = &planes.planes[pair + (x % 2)];
let s = plane_row + (x / 2) * bps;
let d = (y * w + x) * bps;
out[d..d + bps].copy_from_slice(&src[s..s + bps]);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn noise(n: usize, seed: u64) -> Vec<u8> {
let mut s = seed | 1;
(0..n)
.map(|_| {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
(s >> 24) as u8
})
.collect()
}
#[test]
fn round_trip_is_byte_exact() {
for (w, h) in [(8u32, 6u32), (64, 32), (2, 2), (1024, 8)] {
let src = noise(w as usize * h as usize * 2, u64::from(w * h));
let p = split(&src, w, h, 2).unwrap();
assert_eq!(src, merge(&p), "round trip failed for {w}×{h}");
}
}
#[test]
fn planes_are_quarter_size_and_partition_the_mosaic() {
let (w, h) = (8u32, 6u32);
let src = noise(w as usize * h as usize * 2, 7);
let p = split(&src, w, h, 2).unwrap();
assert_eq!((p.width, p.height), (4, 3));
let total: usize = p.planes.iter().map(|v| v.len()).sum();
assert_eq!(total, (w * h) as usize * 2);
}
#[test]
fn split_routes_positions_correctly() {
let src: Vec<u8> = [1u16, 2, 3, 4]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
let p = split(&src, 2, 2, 2).unwrap();
assert_eq!(p.planes[0], vec![1, 0]); assert_eq!(p.planes[1], vec![2, 0]); assert_eq!(p.planes[2], vec![3, 0]); assert_eq!(p.planes[3], vec![4, 0]); }
#[test]
fn byte_order_is_irrelevant() {
let src = noise(16 * 16 * 2, 3);
let a = split(&src, 16, 16, 2).unwrap();
let mut swapped = src.clone();
for c in swapped.chunks_exact_mut(2) {
c.swap(0, 1);
}
let b = split(&swapped, 16, 16, 2).unwrap();
for i in 0..PLANE_COUNT {
let unswapped: Vec<u8> = b.planes[i]
.chunks_exact(2)
.flat_map(|c| [c[1], c[0]])
.collect();
assert_eq!(a.planes[i], unswapped);
}
}
#[test]
fn odd_dimensions_rejected() {
let src = vec![0u8; 3 * 4 * 2];
assert_eq!(
split(&src, 3, 4, 2),
Err(Error::OddDimensions {
width: 3,
height: 4
})
);
}
#[test]
fn wrong_length_rejected() {
assert!(matches!(
split(&[0u8; 10], 4, 4, 2),
Err(Error::WrongLength { .. })
));
}
}