1use cranpose_ui_graphics::Rect;
2
3#[derive(Clone, Debug, PartialEq, Eq)]
4pub struct PixelDifference {
5 pub x: u32,
6 pub y: u32,
7 pub lhs: [u8; 4],
8 pub rhs: [u8; 4],
9 pub difference: u32,
10}
11
12#[derive(Clone, Debug, Default, PartialEq, Eq)]
13pub struct ImageDifferenceStats {
14 pub differing_pixels: u32,
15 pub max_difference: u32,
16 pub first_difference: Option<PixelDifference>,
17}
18
19pub fn sample_pixel(pixels: &[u8], width: u32, x: u32, y: u32) -> [u8; 4] {
20 let idx = ((y * width + x) * 4) as usize;
21 [
22 pixels[idx],
23 pixels[idx + 1],
24 pixels[idx + 2],
25 pixels[idx + 3],
26 ]
27}
28
29pub fn pixel_difference(lhs: [u8; 4], rhs: [u8; 4]) -> u32 {
30 lhs.into_iter()
31 .zip(rhs)
32 .map(|(left, right)| left.abs_diff(right) as u32)
33 .sum()
34}
35
36pub fn normalize_rgba_region(
37 pixels: &[u8],
38 width: u32,
39 height: u32,
40 rect: Rect,
41 output_width: u32,
42 output_height: u32,
43) -> Vec<u8> {
44 assert!(output_width > 0, "normalized output_width must be positive");
45 assert!(
46 output_height > 0,
47 "normalized output_height must be positive"
48 );
49
50 let mut out = Vec::with_capacity((output_width * output_height * 4) as usize);
51 for y in 0..output_height {
52 for x in 0..output_width {
53 let sample_x = rect.x + ((x as f32 + 0.5) * rect.width / output_width as f32);
54 let sample_y = rect.y + ((y as f32 + 0.5) * rect.height / output_height as f32);
55 out.extend_from_slice(&sample_pixel_bilinear(
56 pixels, width, height, sample_x, sample_y,
57 ));
58 }
59 }
60 out
61}
62
63pub fn image_difference_stats(
64 lhs: &[u8],
65 rhs: &[u8],
66 width: u32,
67 height: u32,
68 tolerance: u32,
69) -> ImageDifferenceStats {
70 assert_eq!(
71 lhs.len(),
72 rhs.len(),
73 "image_difference_stats requires equal buffer lengths"
74 );
75
76 let mut differing_pixels = 0;
77 let mut max_difference = 0;
78 let mut first_difference = None;
79 for y in 0..height {
80 for x in 0..width {
81 let index = ((y * width + x) * 4) as usize;
82 let lhs_pixel = [lhs[index], lhs[index + 1], lhs[index + 2], lhs[index + 3]];
83 let rhs_pixel = [rhs[index], rhs[index + 1], rhs[index + 2], rhs[index + 3]];
84 let difference = pixel_difference(lhs_pixel, rhs_pixel);
85 if difference > tolerance {
86 differing_pixels += 1;
87 max_difference = max_difference.max(difference);
88 if first_difference.is_none() {
89 first_difference = Some(PixelDifference {
90 x,
91 y,
92 lhs: lhs_pixel,
93 rhs: rhs_pixel,
94 difference,
95 });
96 }
97 }
98 }
99 }
100
101 ImageDifferenceStats {
102 differing_pixels,
103 max_difference,
104 first_difference,
105 }
106}
107
108fn sample_pixel_bilinear(pixels: &[u8], width: u32, height: u32, x: f32, y: f32) -> [u8; 4] {
109 let max_x = width.saturating_sub(1) as f32;
110 let max_y = height.saturating_sub(1) as f32;
111 let source_x = (x - 0.5).clamp(0.0, max_x);
112 let source_y = (y - 0.5).clamp(0.0, max_y);
113 let x0 = source_x.floor() as u32;
114 let y0 = source_y.floor() as u32;
115 let x1 = (x0 + 1).min(width.saturating_sub(1));
116 let y1 = (y0 + 1).min(height.saturating_sub(1));
117 let tx = source_x - x0 as f32;
118 let ty = source_y - y0 as f32;
119 let top_left = sample_pixel(pixels, width, x0, y0);
120 let top_right = sample_pixel(pixels, width, x1, y0);
121 let bottom_left = sample_pixel(pixels, width, x0, y1);
122 let bottom_right = sample_pixel(pixels, width, x1, y1);
123
124 let lerp_channel = |index: usize| {
125 let top = top_left[index] as f32 * (1.0 - tx) + top_right[index] as f32 * tx;
126 let bottom = bottom_left[index] as f32 * (1.0 - tx) + bottom_right[index] as f32 * tx;
127 (top * (1.0 - ty) + bottom * ty).round() as u8
128 };
129
130 [
131 lerp_channel(0),
132 lerp_channel(1),
133 lerp_channel(2),
134 lerp_channel(3),
135 ]
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn normalize_rgba_region_preserves_pixel_grid_at_native_size() {
144 let pixels = vec![
145 10, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 100, 110, 120, 255,
146 ];
147 let normalized = normalize_rgba_region(
148 &pixels,
149 2,
150 2,
151 Rect {
152 x: 0.0,
153 y: 0.0,
154 width: 2.0,
155 height: 2.0,
156 },
157 2,
158 2,
159 );
160 assert_eq!(normalized, pixels);
161 }
162
163 #[test]
164 fn image_difference_stats_reports_first_difference() {
165 let lhs = vec![0, 0, 0, 255, 8, 8, 8, 255];
166 let rhs = vec![0, 0, 0, 255, 18, 12, 10, 255];
167 let stats = image_difference_stats(&lhs, &rhs, 2, 1, 3);
168
169 assert_eq!(stats.differing_pixels, 1);
170 assert_eq!(stats.max_difference, 16);
171 assert_eq!(
172 stats.first_difference,
173 Some(PixelDifference {
174 x: 1,
175 y: 0,
176 lhs: [8, 8, 8, 255],
177 rhs: [18, 12, 10, 255],
178 difference: 16,
179 })
180 );
181 }
182}