1use std::fmt::Debug;
7use std::fmt::Display;
8
9use thiserror::Error;
10use tiny_skia::Pixmap;
11use tytanic_utils::fmt::Term;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct Size {
16 pub width: u32,
18
19 pub height: u32,
21}
22
23impl Display for Size {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 write!(f, "{}x{}", self.width, self.height)
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq)]
31pub enum Strategy {
32 Simple {
35 max_delta: u8,
39
40 max_deviation: usize,
43 },
44}
45
46impl Default for Strategy {
47 fn default() -> Self {
48 Self::Simple {
49 max_delta: 0,
50 max_deviation: 0,
51 }
52 }
53}
54
55pub fn page(output: &Pixmap, reference: &Pixmap, strategy: Strategy) -> Result<(), PageError> {
57 match strategy {
58 Strategy::Simple {
59 max_delta,
60 max_deviation,
61 } => page_simple(output, reference, max_delta, max_deviation),
62 }
63}
64
65fn page_simple(
67 output: &Pixmap,
68 reference: &Pixmap,
69 max_delta: u8,
70 max_deviation: usize,
71) -> Result<(), PageError> {
72 if output.width() != reference.width() || output.height() != reference.height() {
73 return Err(PageError::Dimensions {
74 output: Size {
75 width: output.width(),
76 height: output.height(),
77 },
78 reference: Size {
79 width: reference.width(),
80 height: reference.height(),
81 },
82 });
83 }
84
85 let deviations = Iterator::zip(output.pixels().iter(), reference.pixels().iter())
86 .filter(|(a, b)| {
87 u8::abs_diff(a.red(), b.red()) > max_delta
88 || u8::abs_diff(a.green(), b.green()) > max_delta
89 || u8::abs_diff(a.blue(), b.blue()) > max_delta
90 || u8::abs_diff(a.alpha(), b.alpha()) > max_delta
91 })
92 .count();
93
94 if deviations > max_deviation {
95 return Err(PageError::SimpleDeviations { deviations });
96 }
97
98 Ok(())
99}
100
101#[derive(Debug, Clone, Error)]
103pub struct Error {
104 pub output: usize,
106
107 pub reference: usize,
109
110 pub pages: Vec<(usize, PageError)>,
112}
113
114impl Display for Error {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 if self.output != self.reference {
117 write!(
118 f,
119 "page count differed (out {} != ref {})",
120 self.output, self.reference,
121 )?;
122 }
123
124 if self.output != self.reference && self.pages.is_empty() {
125 write!(f, " and ")?;
126 }
127
128 if self.pages.is_empty() {
129 write!(
130 f,
131 "{} {} differed at indices: {:?}",
132 self.pages.len(),
133 Term::simple("page").with(self.pages.len()),
134 self.pages.iter().map(|(n, _)| n).collect::<Vec<_>>()
135 )?;
136 }
137
138 Ok(())
139 }
140}
141
142#[derive(Debug, Clone, Error)]
144pub enum PageError {
145 #[error("dimensions differed: out {output} != ref {reference}")]
147 Dimensions {
148 output: Size,
150
151 reference: Size,
153 },
154
155 #[error(
157 "content differed in at least {} {}",
158 deviations,
159 Term::simple("pixel").with(*deviations)
160 )]
161 SimpleDeviations {
162 deviations: usize,
165 },
166}
167
168#[cfg(test)]
169mod tests {
170 use tiny_skia::PremultipliedColorU8;
171
172 use super::*;
173
174 fn images() -> [Pixmap; 2] {
175 let a = Pixmap::new(10, 1).unwrap();
176 let mut b = Pixmap::new(10, 1).unwrap();
177
178 let red = PremultipliedColorU8::from_rgba(128, 0, 0, 128).unwrap();
179 b.pixels_mut()[0] = red;
180 b.pixels_mut()[1] = red;
181 b.pixels_mut()[2] = red;
182 b.pixels_mut()[3] = red;
183
184 [a, b]
185 }
186
187 #[test]
188 fn test_page_simple_below_max_delta() {
189 let [a, b] = images();
190 assert!(
191 page(
192 &a,
193 &b,
194 Strategy::Simple {
195 max_delta: 128,
196 max_deviation: 0,
197 },
198 )
199 .is_ok()
200 )
201 }
202
203 #[test]
204 fn test_page_simple_below_max_devitation() {
205 let [a, b] = images();
206 assert!(
207 page(
208 &a,
209 &b,
210 Strategy::Simple {
211 max_delta: 0,
212 max_deviation: 5,
213 },
214 )
215 .is_ok()
216 );
217 }
218
219 #[test]
220 fn test_page_simple_above_max_devitation() {
221 let [a, b] = images();
222 assert!(matches!(
223 page(
224 &a,
225 &b,
226 Strategy::Simple {
227 max_delta: 0,
228 max_deviation: 0,
229 },
230 ),
231 Err(PageError::SimpleDeviations { deviations: 4 })
232 ))
233 }
234}