1#[derive(Debug, Clone)]
12pub struct Frame {
13 pub rgba: Vec<u8>,
15 pub w: u32,
17 pub h: u32,
19}
20
21impl Frame {
22 #[must_use]
24 pub fn new(rgba: Vec<u8>, w: u32, h: u32) -> Option<Self> {
25 let expected = (w as usize).checked_mul(h as usize)?.checked_mul(4)?;
26 if rgba.len() == expected && w > 0 && h > 0 {
27 Some(Self { rgba, w, h })
28 } else {
29 None
30 }
31 }
32}
33
34#[must_use]
40pub fn compose(
41 frames: &[Frame],
42 cols: usize,
43 gap: u32,
44 bg: [u8; 4],
45) -> Option<(Vec<u8>, u32, u32)> {
46 if frames.is_empty() {
47 return None;
48 }
49 let n = frames.len();
50 let cols = cols.max(1).min(n);
51 let rows = n.div_ceil(cols);
52 let gap = gap as usize;
53
54 let cell_w = frames.iter().map(|f| f.w as usize).max()?;
55 let cell_h = frames.iter().map(|f| f.h as usize).max()?;
56
57 let out_w = cols
59 .checked_mul(cell_w)?
60 .checked_add(cols.checked_add(1)?.checked_mul(gap)?)?;
61 let out_h = rows
62 .checked_mul(cell_h)?
63 .checked_add(rows.checked_add(1)?.checked_mul(gap)?)?;
64 let total = out_w.checked_mul(out_h)?.checked_mul(4)?;
65 if total > 512 * 1024 * 1024 {
67 return None;
68 }
69
70 let mut out = vec![0u8; total];
72 for px in out.chunks_exact_mut(4) {
73 px.copy_from_slice(&bg);
74 }
75
76 let out_row_bytes = out_w * 4;
77 for (i, frame) in frames.iter().enumerate() {
78 let col = i % cols;
79 let row = i / cols;
80 let x0 = gap + col * (cell_w + gap);
81 let y0 = gap + row * (cell_h + gap);
82 let fw = frame.w as usize;
83 let fh = frame.h as usize;
84 let frame_row_bytes = fw * 4;
85 for y in 0..fh {
86 let dst_start = (y0 + y) * out_row_bytes + x0 * 4;
87 let src_start = y * frame_row_bytes;
88 let dst_end = dst_start + frame_row_bytes;
91 let src_end = src_start + frame_row_bytes;
92 if dst_end <= out.len() && src_end <= frame.rgba.len() {
93 out[dst_start..dst_end].copy_from_slice(&frame.rgba[src_start..src_end]);
94 }
95 }
96 }
97
98 Some((out, out_w as u32, out_h as u32))
99}
100
101#[must_use]
104pub fn default_cols(n: usize) -> usize {
105 if n == 0 {
106 return 1;
107 }
108 #[allow(
109 clippy::cast_precision_loss,
110 clippy::cast_sign_loss,
111 clippy::cast_possible_truncation
112 )]
113 let c = (n as f64).sqrt().ceil() as usize;
114 c.clamp(1, 8)
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 fn solid(w: u32, h: u32, color: [u8; 4]) -> Frame {
122 let mut rgba = Vec::with_capacity((w * h * 4) as usize);
123 for _ in 0..(w * h) {
124 rgba.extend_from_slice(&color);
125 }
126 Frame::new(rgba, w, h).unwrap()
127 }
128
129 #[test]
130 fn frame_new_validates_size() {
131 assert!(Frame::new(vec![0; 16], 2, 2).is_some());
132 assert!(Frame::new(vec![0; 15], 2, 2).is_none());
133 assert!(Frame::new(vec![], 0, 0).is_none());
134 }
135
136 #[test]
137 fn empty_returns_none() {
138 assert!(compose(&[], 4, 2, [0, 0, 0, 0]).is_none());
139 }
140
141 #[test]
142 fn single_frame_no_gap() {
143 let f = solid(3, 2, [10, 20, 30, 255]);
144 let (rgba, w, h) = compose(std::slice::from_ref(&f), 4, 0, [0, 0, 0, 0]).unwrap();
145 assert_eq!((w, h), (3, 2));
146 assert_eq!(rgba.len(), 3 * 2 * 4);
147 assert_eq!(&rgba[0..4], &[10, 20, 30, 255]);
149 }
150
151 #[test]
152 fn grid_dims_with_gap() {
153 let frames: Vec<Frame> = (0..3).map(|_| solid(4, 2, [1, 2, 3, 255])).collect();
155 let (_, w, h) = compose(&frames, 2, 1, [0, 0, 0, 255]).unwrap();
156 assert_eq!((w, h), (11, 7));
158 }
159
160 #[test]
161 fn ragged_sizes_clamp_to_max_cell() {
162 let a = solid(4, 2, [255, 0, 0, 255]);
163 let b = solid(2, 4, [0, 255, 0, 255]);
164 let (_, w, h) = compose(&[a, b], 2, 0, [0, 0, 0, 0]).unwrap();
165 assert_eq!((w, h), (8, 4));
167 }
168
169 #[test]
170 fn background_fills_gaps() {
171 let f = solid(2, 2, [255, 255, 255, 255]);
172 let bg = [9, 8, 7, 255];
173 let (rgba, w, _h) = compose(std::slice::from_ref(&f), 1, 1, bg).unwrap();
174 assert_eq!(&rgba[0..4], &bg);
176 let off = (w as usize + 1) * 4;
178 assert_eq!(&rgba[off..off + 4], &[255, 255, 255, 255]);
179 }
180
181 #[test]
182 fn default_cols_is_roughly_square() {
183 assert_eq!(default_cols(0), 1);
184 assert_eq!(default_cols(1), 1);
185 assert_eq!(default_cols(4), 2);
186 assert_eq!(default_cols(9), 3);
187 assert_eq!(default_cols(20), 5);
188 assert_eq!(default_cols(1000), 8); }
190}