1#[derive(Debug, Clone, Copy, PartialEq, Default)]
35pub enum ImageMaskShape {
36 #[default]
38 None,
39 Circle,
42 RoundedSquare(f32),
45}
46
47#[derive(Debug, Clone, Copy)]
51enum MaskShape {
52 Circle,
53 RoundedSquare(f32),
54 Square,
55}
56
57const SAMPLES_PER_AXIS: u32 = 4;
58
59pub fn center_crop_square(pixels: &[u8], width: u32, height: u32) -> (Vec<u8>, u32) {
63 let side = width.min(height);
64 if width == side && height == side {
65 return (pixels.to_vec(), side);
66 }
67 debug_assert_eq!(
68 pixels.len(),
69 (width * height * 4) as usize,
70 "pixel buffer length must be width * height * 4"
71 );
72 let x_off = ((width - side) / 2) as usize;
73 let y_off = ((height - side) / 2) as usize;
74 let stride = (width * 4) as usize;
75 let row_bytes = (side * 4) as usize;
76 let mut out = Vec::with_capacity((side as usize) * row_bytes);
77 for j in 0..side as usize {
78 let row_start = (y_off + j) * stride + x_off * 4;
79 out.extend_from_slice(&pixels[row_start..row_start + row_bytes]);
80 }
81 (out, side)
82}
83
84pub fn apply_alpha_mask(pixels: &mut [u8], width: u32, height: u32, shape: ImageMaskShape) {
93 debug_assert_eq!(pixels.len(), (width * height * 4) as usize);
94 let internal = match shape {
95 ImageMaskShape::None => return,
96 ImageMaskShape::Circle => MaskShape::Circle,
97 ImageMaskShape::RoundedSquare(ratio) => {
98 let r = ratio.clamp(0.0, 0.5) * (width.min(height) as f32);
99 if r <= 0.0 {
100 MaskShape::Square
101 } else {
102 MaskShape::RoundedSquare(r)
103 }
104 }
105 };
106 let radius = match internal {
107 MaskShape::Square => return,
108 MaskShape::Circle => (width.min(height) as f32) / 2.0,
109 MaskShape::RoundedSquare(r) => r,
110 };
111 apply_rounded(pixels, width, height, radius);
112}
113
114fn apply_rounded(pixels: &mut [u8], width: u32, height: u32, radius: f32) {
115 if width == 0 || height == 0 {
116 return;
117 }
118 let w = width as f32;
119 let h = height as f32;
120 let r = radius.clamp(0.0, (w.min(h)) / 2.0);
121 if r <= 0.0 {
122 return;
124 }
125
126 for j in 0..height {
127 for i in 0..width {
128 let coverage = pixel_coverage(i as f32, j as f32, w, h, r);
129 let idx = ((j * width + i) * 4 + 3) as usize;
130 let original = pixels[idx] as f32;
131 let masked = (original * coverage + 0.5).clamp(0.0, 255.0) as u8;
134 pixels[idx] = masked;
135 }
136 }
137}
138
139fn pixel_coverage(px: f32, py: f32, w: f32, h: f32, r: f32) -> f32 {
148 let mut hits: u32 = 0;
149 let total = SAMPLES_PER_AXIS * SAMPLES_PER_AXIS;
150 for sy in 0..SAMPLES_PER_AXIS {
151 for sx in 0..SAMPLES_PER_AXIS {
152 let sub_x = px + (sx as f32 + 0.5) / SAMPLES_PER_AXIS as f32;
153 let sub_y = py + (sy as f32 + 0.5) / SAMPLES_PER_AXIS as f32;
154 if inside_rounded_rect(sub_x, sub_y, w, h, r) {
155 hits += 1;
156 }
157 }
158 }
159 hits as f32 / total as f32
160}
161
162#[inline]
163fn inside_rounded_rect(x: f32, y: f32, w: f32, h: f32, r: f32) -> bool {
164 if x < 0.0 || y < 0.0 || x > w || y > h {
165 return false;
166 }
167 let cx = x.clamp(r, w - r);
169 let cy = y.clamp(r, h - r);
170 let dx = x - cx;
171 let dy = y - cy;
172 dx * dx + dy * dy <= r * r
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 fn solid(width: u32, height: u32) -> Vec<u8> {
180 let mut v = Vec::with_capacity((width * height * 4) as usize);
182 for _ in 0..(width * height) {
183 v.extend_from_slice(&[10, 20, 30, 200]);
184 }
185 v
186 }
187
188 fn alpha_at(pixels: &[u8], width: u32, x: u32, y: u32) -> u8 {
189 pixels[((y * width + x) * 4 + 3) as usize]
190 }
191
192 #[test]
193 fn mask_circle_zeros_corners() {
194 let mut pixels = solid(32, 32);
195 apply_alpha_mask(&mut pixels, 32, 32, ImageMaskShape::Circle);
196 assert_eq!(alpha_at(&pixels, 32, 0, 0), 0);
198 assert_eq!(alpha_at(&pixels, 32, 31, 0), 0);
199 assert_eq!(alpha_at(&pixels, 32, 0, 31), 0);
200 assert_eq!(alpha_at(&pixels, 32, 31, 31), 0);
201 }
202
203 #[test]
204 fn mask_circle_full_center() {
205 let mut pixels = solid(32, 32);
206 apply_alpha_mask(&mut pixels, 32, 32, ImageMaskShape::Circle);
207 assert_eq!(alpha_at(&pixels, 32, 16, 16), 200);
210 }
211
212 #[test]
213 fn mask_circle_aa_at_boundary() {
214 let mut pixels = solid(32, 32);
220 apply_alpha_mask(&mut pixels, 32, 32, ImageMaskShape::Circle);
221 let edge = alpha_at(&pixels, 32, 5, 4);
222 assert!(
223 edge > 0 && edge < 200,
224 "expected partial coverage at the curve boundary, got {edge}"
225 );
226 }
227
228 #[test]
229 fn mask_rounded_square_radius_zero_is_passthrough() {
230 let mut pixels = solid(16, 16);
231 apply_alpha_mask(&mut pixels, 16, 16, ImageMaskShape::RoundedSquare(0.0));
232 for j in 0..16 {
234 for i in 0..16 {
235 assert_eq!(alpha_at(&pixels, 16, i, j), 200);
236 }
237 }
238 }
239
240 #[test]
241 fn mask_rounded_square_full_radius_equals_circle() {
242 let mut a = solid(24, 24);
243 let mut b = solid(24, 24);
244 apply_alpha_mask(&mut a, 24, 24, ImageMaskShape::Circle);
245 apply_alpha_mask(&mut b, 24, 24, ImageMaskShape::RoundedSquare(0.5));
247 for (av, bv) in a.iter().zip(b.iter()) {
250 assert!(
251 av.abs_diff(*bv) <= 1,
252 "circle and full-radius rounded-square should match within 1 alpha LSB"
253 );
254 }
255 }
256
257 #[test]
258 fn mask_preserves_rgb() {
259 let mut pixels = solid(16, 16);
260 apply_alpha_mask(&mut pixels, 16, 16, ImageMaskShape::Circle);
261 for i in (0..pixels.len()).step_by(4) {
262 assert_eq!(pixels[i], 10);
263 assert_eq!(pixels[i + 1], 20);
264 assert_eq!(pixels[i + 2], 30);
265 }
266 }
267
268 #[test]
269 fn mask_none_is_noop() {
270 let mut pixels = solid(8, 8);
271 apply_alpha_mask(&mut pixels, 8, 8, ImageMaskShape::None);
272 for j in 0..8 {
273 for i in 0..8 {
274 assert_eq!(alpha_at(&pixels, 8, i, j), 200);
275 }
276 }
277 }
278
279 #[test]
280 fn mask_handles_size_one_image() {
281 let mut pixels = vec![10, 20, 30, 200];
286 apply_alpha_mask(&mut pixels, 1, 1, ImageMaskShape::Circle);
287 assert!(pixels[3] > 0, "1×1 alpha must remain non-zero");
288 assert!(pixels[3] <= 200, "1×1 alpha cannot exceed source");
289 assert_eq!(&pixels[..3], &[10, 20, 30]);
291 }
292
293 #[test]
294 fn mask_circle_alpha_multiplied_with_source() {
295 let mut pixels = Vec::with_capacity(32 * 32 * 4);
298 for _ in 0..(32 * 32) {
299 pixels.extend_from_slice(&[10, 20, 30, 100]);
300 }
301 apply_alpha_mask(&mut pixels, 32, 32, ImageMaskShape::Circle);
302 assert_eq!(alpha_at(&pixels, 32, 16, 16), 100);
303 assert_eq!(alpha_at(&pixels, 32, 0, 0), 0);
305 }
306
307 #[test]
308 fn center_crop_square_is_identity_when_already_square() {
309 let p = solid(16, 16);
310 let (out, side) = center_crop_square(&p, 16, 16);
311 assert_eq!(side, 16);
312 assert_eq!(out, p);
313 }
314
315 #[test]
316 fn center_crop_square_landscape() {
317 let mut pixels = Vec::new();
319 for y in 0..4 {
320 for x in 0..8 {
321 pixels.extend_from_slice(&[x as u8, y as u8, 0, 255]);
322 }
323 }
324 let (out, side) = center_crop_square(&pixels, 8, 4);
325 assert_eq!(side, 4);
326 assert_eq!(out.len(), 4 * 4 * 4);
327 assert_eq!(out[0], 2);
329 let last = out.len() - 4;
331 assert_eq!(out[last], 5);
332 }
333
334 #[test]
335 fn center_crop_square_portrait() {
336 let mut pixels = Vec::new();
338 for y in 0..8 {
339 for x in 0..4 {
340 pixels.extend_from_slice(&[x as u8, y as u8, 0, 255]);
341 }
342 }
343 let (out, side) = center_crop_square(&pixels, 4, 8);
344 assert_eq!(side, 4);
345 assert_eq!(out[1], 2); }
347}