#[derive(Debug, Clone)]
pub struct Frame {
pub rgba: Vec<u8>,
pub w: u32,
pub h: u32,
}
impl Frame {
#[must_use]
pub fn new(rgba: Vec<u8>, w: u32, h: u32) -> Option<Self> {
let expected = (w as usize).checked_mul(h as usize)?.checked_mul(4)?;
if rgba.len() == expected && w > 0 && h > 0 {
Some(Self { rgba, w, h })
} else {
None
}
}
}
#[must_use]
pub fn compose(
frames: &[Frame],
cols: usize,
gap: u32,
bg: [u8; 4],
) -> Option<(Vec<u8>, u32, u32)> {
if frames.is_empty() {
return None;
}
let n = frames.len();
let cols = cols.max(1).min(n);
let rows = n.div_ceil(cols);
let gap = gap as usize;
let cell_w = frames.iter().map(|f| f.w as usize).max()?;
let cell_h = frames.iter().map(|f| f.h as usize).max()?;
let out_w = cols
.checked_mul(cell_w)?
.checked_add(cols.checked_add(1)?.checked_mul(gap)?)?;
let out_h = rows
.checked_mul(cell_h)?
.checked_add(rows.checked_add(1)?.checked_mul(gap)?)?;
let total = out_w.checked_mul(out_h)?.checked_mul(4)?;
if total > 512 * 1024 * 1024 {
return None;
}
let mut out = vec![0u8; total];
for px in out.chunks_exact_mut(4) {
px.copy_from_slice(&bg);
}
let out_row_bytes = out_w * 4;
for (i, frame) in frames.iter().enumerate() {
let col = i % cols;
let row = i / cols;
let x0 = gap + col * (cell_w + gap);
let y0 = gap + row * (cell_h + gap);
let fw = frame.w as usize;
let fh = frame.h as usize;
let frame_row_bytes = fw * 4;
for y in 0..fh {
let dst_start = (y0 + y) * out_row_bytes + x0 * 4;
let src_start = y * frame_row_bytes;
let dst_end = dst_start + frame_row_bytes;
let src_end = src_start + frame_row_bytes;
if dst_end <= out.len() && src_end <= frame.rgba.len() {
out[dst_start..dst_end].copy_from_slice(&frame.rgba[src_start..src_end]);
}
}
}
Some((out, out_w as u32, out_h as u32))
}
#[must_use]
pub fn default_cols(n: usize) -> usize {
if n == 0 {
return 1;
}
#[allow(
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
let c = (n as f64).sqrt().ceil() as usize;
c.clamp(1, 8)
}
#[cfg(test)]
mod tests {
use super::*;
fn solid(w: u32, h: u32, color: [u8; 4]) -> Frame {
let mut rgba = Vec::with_capacity((w * h * 4) as usize);
for _ in 0..(w * h) {
rgba.extend_from_slice(&color);
}
Frame::new(rgba, w, h).unwrap()
}
#[test]
fn frame_new_validates_size() {
assert!(Frame::new(vec![0; 16], 2, 2).is_some());
assert!(Frame::new(vec![0; 15], 2, 2).is_none());
assert!(Frame::new(vec![], 0, 0).is_none());
}
#[test]
fn empty_returns_none() {
assert!(compose(&[], 4, 2, [0, 0, 0, 0]).is_none());
}
#[test]
fn single_frame_no_gap() {
let f = solid(3, 2, [10, 20, 30, 255]);
let (rgba, w, h) = compose(std::slice::from_ref(&f), 4, 0, [0, 0, 0, 0]).unwrap();
assert_eq!((w, h), (3, 2));
assert_eq!(rgba.len(), 3 * 2 * 4);
assert_eq!(&rgba[0..4], &[10, 20, 30, 255]);
}
#[test]
fn grid_dims_with_gap() {
let frames: Vec<Frame> = (0..3).map(|_| solid(4, 2, [1, 2, 3, 255])).collect();
let (_, w, h) = compose(&frames, 2, 1, [0, 0, 0, 255]).unwrap();
assert_eq!((w, h), (11, 7));
}
#[test]
fn ragged_sizes_clamp_to_max_cell() {
let a = solid(4, 2, [255, 0, 0, 255]);
let b = solid(2, 4, [0, 255, 0, 255]);
let (_, w, h) = compose(&[a, b], 2, 0, [0, 0, 0, 0]).unwrap();
assert_eq!((w, h), (8, 4));
}
#[test]
fn background_fills_gaps() {
let f = solid(2, 2, [255, 255, 255, 255]);
let bg = [9, 8, 7, 255];
let (rgba, w, _h) = compose(std::slice::from_ref(&f), 1, 1, bg).unwrap();
assert_eq!(&rgba[0..4], &bg);
let off = (w as usize + 1) * 4;
assert_eq!(&rgba[off..off + 4], &[255, 255, 255, 255]);
}
#[test]
fn default_cols_is_roughly_square() {
assert_eq!(default_cols(0), 1);
assert_eq!(default_cols(1), 1);
assert_eq!(default_cols(4), 2);
assert_eq!(default_cols(9), 3);
assert_eq!(default_cols(20), 5);
assert_eq!(default_cols(1000), 8); }
}