1use presentar_core::draw::DrawCommand;
6use presentar_core::Color;
7use std::collections::hash_map::DefaultHasher;
8use std::hash::{Hash, Hasher};
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, Clone)]
13pub struct Image {
14 pub width: u32,
16 pub height: u32,
18 pub data: Vec<u8>,
20}
21
22impl Image {
23 #[must_use]
25 pub fn new(width: u32, height: u32) -> Self {
26 let size = (width as usize) * (height as usize) * 4;
27 Self {
28 width,
29 height,
30 data: vec![0; size],
31 }
32 }
33
34 #[must_use]
36 pub fn filled(width: u32, height: u32, r: u8, g: u8, b: u8, a: u8) -> Self {
37 let size = (width as usize) * (height as usize) * 4;
38 let mut data = Vec::with_capacity(size);
39 for _ in 0..(width * height) {
40 data.extend_from_slice(&[r, g, b, a]);
41 }
42 Self {
43 width,
44 height,
45 data,
46 }
47 }
48
49 #[must_use]
51 pub fn as_bytes(&self) -> &[u8] {
52 &self.data
53 }
54
55 #[must_use]
57 pub fn get_pixel(&self, x: u32, y: u32) -> Option<[u8; 4]> {
58 if x >= self.width || y >= self.height {
59 return None;
60 }
61 let idx = ((y * self.width + x) * 4) as usize;
62 Some([
63 self.data[idx],
64 self.data[idx + 1],
65 self.data[idx + 2],
66 self.data[idx + 3],
67 ])
68 }
69
70 pub fn set_pixel(&mut self, x: u32, y: u32, rgba: [u8; 4]) {
72 if x < self.width && y < self.height {
73 let idx = ((y * self.width + x) * 4) as usize;
74 self.data[idx..idx + 4].copy_from_slice(&rgba);
75 }
76 }
77
78 #[allow(clippy::cast_possible_wrap)]
80 pub fn fill_rect(&mut self, x: i32, y: i32, width: u32, height: u32, color: &Color) {
81 let rgba = [
82 (color.r * 255.0) as u8,
83 (color.g * 255.0) as u8,
84 (color.b * 255.0) as u8,
85 (color.a * 255.0) as u8,
86 ];
87
88 let x_start = x.max(0) as u32;
89 let y_start = y.max(0) as u32;
90 let x_end = ((x + width as i32) as u32).min(self.width);
91 let y_end = ((y + height as i32) as u32).min(self.height);
92
93 for py in y_start..y_end {
94 for px in x_start..x_end {
95 self.blend_pixel(px, py, rgba);
96 }
97 }
98 }
99
100 #[allow(clippy::cast_lossless, clippy::needless_range_loop)]
102 fn blend_pixel(&mut self, x: u32, y: u32, src: [u8; 4]) {
103 if x >= self.width || y >= self.height {
104 return;
105 }
106 let idx = ((y * self.width + x) * 4) as usize;
107
108 let src_a = src[3] as f32 / 255.0;
109 if src_a >= 0.999 {
110 self.data[idx..idx + 4].copy_from_slice(&src);
111 return;
112 }
113
114 let dst_a = self.data[idx + 3] as f32 / 255.0;
115 let out_a = src_a + dst_a * (1.0 - src_a);
116
117 if out_a > 0.0 {
118 for i in 0..3 {
119 let src_c = src[i] as f32 / 255.0;
120 let dst_c = self.data[idx + i] as f32 / 255.0;
121 let out_c = (src_c * src_a + dst_c * dst_a * (1.0 - src_a)) / out_a;
122 self.data[idx + i] = (out_c * 255.0) as u8;
123 }
124 self.data[idx + 3] = (out_a * 255.0) as u8;
125 }
126 }
127
128 #[allow(clippy::cast_possible_wrap)]
130 pub fn fill_circle(&mut self, cx: i32, cy: i32, radius: u32, color: &Color) {
131 let rgba = [
132 (color.r * 255.0) as u8,
133 (color.g * 255.0) as u8,
134 (color.b * 255.0) as u8,
135 (color.a * 255.0) as u8,
136 ];
137
138 let r = radius as i32;
139 let r_sq = (r * r) as f32;
140
141 for dy in -r..=r {
142 for dx in -r..=r {
143 let dist_sq = (dx * dx + dy * dy) as f32;
144 if dist_sq <= r_sq {
145 let px = cx + dx;
146 let py = cy + dy;
147 if px >= 0 && py >= 0 {
148 self.blend_pixel(px as u32, py as u32, rgba);
149 }
150 }
151 }
152 }
153 }
154
155 pub fn render(&mut self, commands: &[DrawCommand]) {
157 for cmd in commands {
158 self.render_command(cmd);
159 }
160 }
161
162 fn render_command(&mut self, cmd: &DrawCommand) {
163 match cmd {
164 DrawCommand::Rect { bounds, style, .. } => {
165 if let Some(fill) = style.fill {
166 self.fill_rect(
167 bounds.x as i32,
168 bounds.y as i32,
169 bounds.width as u32,
170 bounds.height as u32,
171 &fill,
172 );
173 }
174 }
175 DrawCommand::Circle {
176 center,
177 radius,
178 style,
179 } => {
180 if let Some(fill) = style.fill {
181 self.fill_circle(center.x as i32, center.y as i32, *radius as u32, &fill);
182 }
183 }
184 DrawCommand::Group { children, .. } => {
185 self.render(children);
186 }
187 _ => {}
188 }
189 }
190
191 #[must_use]
193 pub fn hash(&self) -> u64 {
194 let mut hasher = DefaultHasher::new();
195 self.width.hash(&mut hasher);
196 self.height.hash(&mut hasher);
197 self.data.hash(&mut hasher);
198 hasher.finish()
199 }
200
201 #[must_use]
203 pub fn region(&self, x: u32, y: u32, width: u32, height: u32) -> Image {
204 let mut result = Image::new(width, height);
205
206 for dy in 0..height {
207 for dx in 0..width {
208 if let Some(pixel) = self.get_pixel(x + dx, y + dy) {
209 result.set_pixel(dx, dy, pixel);
210 }
211 }
212 }
213
214 result
215 }
216
217 #[must_use]
219 pub fn scale(&self, new_width: u32, new_height: u32) -> Image {
220 let mut result = Image::new(new_width, new_height);
221
222 if self.width == 0 || self.height == 0 {
223 return result;
224 }
225
226 for y in 0..new_height {
227 for x in 0..new_width {
228 let src_x = (x as f32 * self.width as f32 / new_width as f32) as u32;
229 let src_y = (y as f32 * self.height as f32 / new_height as f32) as u32;
230 if let Some(pixel) = self.get_pixel(src_x, src_y) {
231 result.set_pixel(x, y, pixel);
232 }
233 }
234 }
235
236 result
237 }
238
239 #[must_use]
241 pub fn count_color(&self, target: [u8; 4], tolerance: u8) -> usize {
242 let mut count = 0;
243
244 for y in 0..self.height {
245 for x in 0..self.width {
246 if let Some(pixel) = self.get_pixel(x, y) {
247 let matches = (0..4).all(|i| {
248 let diff = (pixel[i] as i32 - target[i] as i32).unsigned_abs() as u8;
249 diff <= tolerance
250 });
251 if matches {
252 count += 1;
253 }
254 }
255 }
256 }
257
258 count
259 }
260
261 #[must_use]
263 pub fn histogram(&self) -> [[u32; 256]; 4] {
264 let mut hist = [[0u32; 256]; 4];
265
266 for chunk in self.data.chunks_exact(4) {
267 for (i, &val) in chunk.iter().enumerate() {
268 hist[i][val as usize] += 1;
269 }
270 }
271
272 hist
273 }
274
275 #[must_use]
277 pub fn mean_color(&self) -> [f32; 4] {
278 let pixel_count = (self.width * self.height) as f64;
279 if pixel_count == 0.0 {
280 return [0.0; 4];
281 }
282
283 let mut sums = [0.0f64; 4];
284
285 for chunk in self.data.chunks_exact(4) {
286 for (i, &val) in chunk.iter().enumerate() {
287 sums[i] += f64::from(val);
288 }
289 }
290
291 [
292 (sums[0] / pixel_count) as f32,
293 (sums[1] / pixel_count) as f32,
294 (sums[2] / pixel_count) as f32,
295 (sums[3] / pixel_count) as f32,
296 ]
297 }
298
299 #[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
301 pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: &Color) {
302 let rgba = [
303 (color.r * 255.0) as u8,
304 (color.g * 255.0) as u8,
305 (color.b * 255.0) as u8,
306 (color.a * 255.0) as u8,
307 ];
308
309 let dx = (x1 - x0).abs();
310 let dy = -(y1 - y0).abs();
311 let sx = if x0 < x1 { 1 } else { -1 };
312 let sy = if y0 < y1 { 1 } else { -1 };
313 let mut err = dx + dy;
314
315 let mut x = x0;
316 let mut y = y0;
317
318 loop {
319 if x >= 0 && y >= 0 {
320 self.blend_pixel(x as u32, y as u32, rgba);
321 }
322
323 if x == x1 && y == y1 {
324 break;
325 }
326
327 let e2 = 2 * err;
328 if e2 >= dy {
329 if x == x1 {
330 break;
331 }
332 err += dy;
333 x += sx;
334 }
335 if e2 <= dx {
336 if y == y1 {
337 break;
338 }
339 err += dx;
340 y += sy;
341 }
342 }
343 }
344
345 #[allow(clippy::cast_possible_wrap)]
347 pub fn stroke_rect(&mut self, x: i32, y: i32, width: u32, height: u32, color: &Color) {
348 let x2 = x + width as i32 - 1;
349 let y2 = y + height as i32 - 1;
350
351 self.draw_line(x, y, x2, y, color);
352 self.draw_line(x2, y, x2, y2, color);
353 self.draw_line(x2, y2, x, y2, color);
354 self.draw_line(x, y2, x, y, color);
355 }
356}
357
358#[derive(Debug, Clone)]
360pub struct ComparisonResult {
361 pub byte_diff: f64,
363 pub perceptual_diff: f64,
365 pub ssim: f64,
367 pub same_dimensions: bool,
369 pub changed_pixels: u64,
371 pub total_pixels: u64,
373}
374
375impl ComparisonResult {
376 #[must_use]
378 pub fn is_match(&self, threshold: f64) -> bool {
379 self.same_dimensions && self.byte_diff <= threshold
380 }
381
382 #[must_use]
384 pub fn changed_percentage(&self) -> f64 {
385 if self.total_pixels == 0 {
386 return 0.0;
387 }
388 self.changed_pixels as f64 / self.total_pixels as f64 * 100.0
389 }
390}
391
392pub struct Snapshot;
394
395impl Snapshot {
396 pub fn assert_match(name: &str, actual: &Image, threshold: f64) {
402 let baseline_path = Self::baseline_path(name);
403
404 if let Some(baseline) = Self::load_baseline(&baseline_path) {
405 let diff_ratio = Self::diff(&baseline, actual);
406
407 if diff_ratio > threshold {
408 let actual_path = Self::actual_path(name);
410 let diff_path = Self::diff_path(name);
411
412 Self::save_image(&actual_path, actual);
413 Self::save_diff(&diff_path, &baseline, actual);
414
415 panic!(
416 "Visual regression '{}': {:.2}% diff (threshold: {:.2}%)\n\
417 Baseline: {}\n\
418 Actual: {}\n\
419 Diff: {}",
420 name,
421 diff_ratio * 100.0,
422 threshold * 100.0,
423 baseline_path.display(),
424 actual_path.display(),
425 diff_path.display()
426 );
427 }
428 } else if std::env::var("SNAPSHOT_UPDATE").is_ok() {
429 Self::save_image(&baseline_path, actual);
431 println!("Created new baseline: {}", baseline_path.display());
432 } else {
433 panic!(
434 "No baseline found for '{}'. Run with SNAPSHOT_UPDATE=1 to create.\n\
435 Expected path: {}",
436 name,
437 baseline_path.display()
438 );
439 }
440 }
441
442 #[must_use]
444 pub fn diff(a: &Image, b: &Image) -> f64 {
445 if a.width != b.width || a.height != b.height {
446 return 1.0; }
448
449 let mut diff_count = 0u64;
450 let total = a.data.len() as u64;
451
452 for (a_byte, b_byte) in a.data.iter().zip(b.data.iter()) {
453 if a_byte != b_byte {
454 diff_count += 1;
455 }
456 }
457
458 diff_count as f64 / total as f64
459 }
460
461 #[must_use]
464 pub fn perceptual_diff(a: &Image, b: &Image) -> f64 {
465 if a.width != b.width || a.height != b.height {
466 return 1.0;
467 }
468
469 let pixel_count = f64::from(a.width * a.height);
470 if pixel_count == 0.0 {
471 return 0.0;
472 }
473
474 let mut total_error = 0.0;
475
476 for i in 0..(a.data.len() / 4) {
477 let idx = i * 4;
478 for j in 0..3 {
480 let diff = f64::from(a.data[idx + j]) - f64::from(b.data[idx + j]);
481 total_error += diff * diff;
482 }
483 }
484
485 let max_error = 255.0 * 255.0 * 3.0 * pixel_count;
487 total_error / max_error
488 }
489
490 #[must_use]
492 pub fn generate_diff_image(a: &Image, b: &Image) -> Image {
493 let width = a.width.max(b.width);
494 let height = a.height.max(b.height);
495 let mut diff = Image::new(width, height);
496
497 for y in 0..height {
498 for x in 0..width {
499 let pixel_a = a.get_pixel(x, y).unwrap_or([0, 0, 0, 0]);
500 let pixel_b = b.get_pixel(x, y).unwrap_or([0, 0, 0, 0]);
501
502 let color_diff: i32 = (0..3)
503 .map(|i| (i32::from(pixel_a[i]) - i32::from(pixel_b[i])).abs())
504 .sum();
505
506 if color_diff == 0 {
507 diff.set_pixel(x, y, [pixel_a[0] / 4, pixel_a[1] / 4, pixel_a[2] / 4, 255]);
509 } else {
510 let intensity = (color_diff.min(255 * 3) / 3) as u8;
512 diff.set_pixel(x, y, [255, intensity, intensity, 255]);
513 }
514 }
515 }
516
517 diff
518 }
519
520 #[must_use]
522 pub fn compare(a: &Image, b: &Image) -> ComparisonResult {
523 let same_dimensions = a.width == b.width && a.height == b.height;
524 let total_pixels = u64::from(a.width) * u64::from(a.height);
525
526 if !same_dimensions {
527 return ComparisonResult {
528 byte_diff: 1.0,
529 perceptual_diff: 1.0,
530 ssim: 0.0,
531 same_dimensions: false,
532 changed_pixels: total_pixels,
533 total_pixels,
534 };
535 }
536
537 let byte_diff = Self::diff(a, b);
538 let perceptual_diff = Self::perceptual_diff(a, b);
539 let ssim = Self::ssim(a, b);
540 let changed_pixels = Self::count_changed_pixels(a, b);
541
542 ComparisonResult {
543 byte_diff,
544 perceptual_diff,
545 ssim,
546 same_dimensions: true,
547 changed_pixels,
548 total_pixels,
549 }
550 }
551
552 #[must_use]
554 pub fn count_changed_pixels(a: &Image, b: &Image) -> u64 {
555 if a.width != b.width || a.height != b.height {
556 return u64::from(a.width.max(b.width)) * u64::from(a.height.max(b.height));
557 }
558
559 let mut count = 0u64;
560 for i in 0..(a.data.len() / 4) {
561 let idx = i * 4;
562 let diff = (0..4).any(|j| a.data[idx + j] != b.data[idx + j]);
563 if diff {
564 count += 1;
565 }
566 }
567 count
568 }
569
570 #[must_use]
575 pub fn ssim(a: &Image, b: &Image) -> f64 {
576 if a.width != b.width || a.height != b.height {
577 return 0.0;
578 }
579
580 let pixel_count = (a.width * a.height) as usize;
581 if pixel_count == 0 {
582 return 1.0;
583 }
584
585 let mut lum_a = Vec::with_capacity(pixel_count);
587 let mut lum_b = Vec::with_capacity(pixel_count);
588
589 for i in 0..pixel_count {
590 let idx = i * 4;
591 let la = 0.299 * f64::from(a.data[idx])
593 + 0.587 * f64::from(a.data[idx + 1])
594 + 0.114 * f64::from(a.data[idx + 2]);
595 let lb = 0.299 * f64::from(b.data[idx])
596 + 0.587 * f64::from(b.data[idx + 1])
597 + 0.114 * f64::from(b.data[idx + 2]);
598 lum_a.push(la);
599 lum_b.push(lb);
600 }
601
602 let mean_a: f64 = lum_a.iter().sum::<f64>() / pixel_count as f64;
604 let mean_b: f64 = lum_b.iter().sum::<f64>() / pixel_count as f64;
605
606 let mut var_a = 0.0;
608 let mut var_b = 0.0;
609 let mut covar = 0.0;
610
611 for i in 0..pixel_count {
612 let da = lum_a[i] - mean_a;
613 let db = lum_b[i] - mean_b;
614 var_a += da * da;
615 var_b += db * db;
616 covar += da * db;
617 }
618
619 var_a /= pixel_count as f64;
620 var_b /= pixel_count as f64;
621 covar /= pixel_count as f64;
622
623 const C1: f64 = 6.5025; const C2: f64 = 58.5225; let numerator = (2.0 * mean_a * mean_b + C1) * (2.0 * covar + C2);
629 let denominator = (mean_a * mean_a + mean_b * mean_b + C1) * (var_a + var_b + C2);
630
631 numerator / denominator
632 }
633
634 #[must_use]
636 pub fn compare_region(
637 a: &Image,
638 b: &Image,
639 x: u32,
640 y: u32,
641 width: u32,
642 height: u32,
643 ) -> ComparisonResult {
644 let region_a = a.region(x, y, width, height);
645 let region_b = b.region(x, y, width, height);
646 Self::compare(®ion_a, ®ion_b)
647 }
648
649 pub fn assert_region_match(
655 name: &str,
656 actual: &Image,
657 baseline: &Image,
658 x: u32,
659 y: u32,
660 width: u32,
661 height: u32,
662 threshold: f64,
663 ) {
664 let result = Self::compare_region(actual, baseline, x, y, width, height);
665 if !result.is_match(threshold) {
666 panic!(
667 "Region mismatch in '{}' at ({}, {}) {}x{}: {:.2}% diff (threshold: {:.2}%)",
668 name,
669 x,
670 y,
671 width,
672 height,
673 result.byte_diff * 100.0,
674 threshold * 100.0
675 );
676 }
677 }
678
679 fn baseline_path(name: &str) -> PathBuf {
680 PathBuf::from(format!("tests/snapshots/{name}.png"))
681 }
682
683 fn actual_path(name: &str) -> PathBuf {
684 PathBuf::from(format!("tests/snapshots/{name}.actual.png"))
685 }
686
687 fn diff_path(name: &str) -> PathBuf {
688 PathBuf::from(format!("tests/snapshots/{name}.diff.png"))
689 }
690
691 fn load_baseline(path: &Path) -> Option<Image> {
692 if path.exists() {
694 Some(Image::new(100, 100))
696 } else {
697 None
698 }
699 }
700
701 const fn save_image(_path: &Path, _image: &Image) {
702 }
705
706 const fn save_diff(_path: &Path, _baseline: &Image, _actual: &Image) {
707 }
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715
716 #[test]
717 fn test_image_new() {
718 let img = Image::new(100, 100);
719 assert_eq!(img.width, 100);
720 assert_eq!(img.height, 100);
721 assert_eq!(img.data.len(), 100 * 100 * 4);
722 }
723
724 #[test]
725 fn test_image_filled() {
726 let img = Image::filled(10, 10, 255, 0, 0, 255);
727 assert_eq!(img.get_pixel(0, 0), Some([255, 0, 0, 255]));
728 assert_eq!(img.get_pixel(5, 5), Some([255, 0, 0, 255]));
729 }
730
731 #[test]
732 fn test_image_get_set_pixel() {
733 let mut img = Image::new(10, 10);
734 img.set_pixel(5, 5, [255, 128, 64, 255]);
735 assert_eq!(img.get_pixel(5, 5), Some([255, 128, 64, 255]));
736 }
737
738 #[test]
739 fn test_image_get_pixel_out_of_bounds() {
740 let img = Image::new(10, 10);
741 assert_eq!(img.get_pixel(100, 100), None);
742 }
743
744 #[test]
745 fn test_diff_identical() {
746 let a = Image::filled(10, 10, 255, 0, 0, 255);
747 let b = Image::filled(10, 10, 255, 0, 0, 255);
748 assert_eq!(Snapshot::diff(&a, &b), 0.0);
749 }
750
751 #[test]
752 fn test_diff_completely_different() {
753 let a = Image::filled(10, 10, 255, 0, 0, 255);
754 let b = Image::filled(10, 10, 0, 255, 0, 255);
755 let diff = Snapshot::diff(&a, &b);
756 assert!(diff > 0.0);
757 }
758
759 #[test]
760 fn test_diff_different_sizes() {
761 let a = Image::new(10, 10);
762 let b = Image::new(20, 20);
763 assert_eq!(Snapshot::diff(&a, &b), 1.0);
764 }
765
766 #[test]
767 fn test_diff_partial() {
768 let a = Image::filled(10, 10, 255, 0, 0, 255);
769 let mut b = Image::filled(10, 10, 255, 0, 0, 255);
770
771 b.set_pixel(0, 0, [0, 0, 0, 255]);
774
775 let diff = Snapshot::diff(&a, &b);
776 assert!((diff - 0.0025).abs() < 0.001);
778 }
779
780 #[test]
781 fn test_fill_rect() {
782 let mut img = Image::new(100, 100);
783 img.fill_rect(10, 10, 20, 20, &Color::RED);
784
785 assert_eq!(img.get_pixel(15, 15), Some([255, 0, 0, 255]));
787 assert_eq!(img.get_pixel(0, 0), Some([0, 0, 0, 0]));
789 }
790
791 #[test]
792 fn test_fill_rect_clipping() {
793 let mut img = Image::new(50, 50);
794 img.fill_rect(40, 40, 20, 20, &Color::BLUE);
796
797 assert_eq!(img.get_pixel(45, 45), Some([0, 0, 255, 255]));
799 assert_eq!(img.get_pixel(49, 49), Some([0, 0, 255, 255]));
801 }
802
803 #[test]
804 fn test_fill_circle() {
805 let mut img = Image::new(100, 100);
806 img.fill_circle(50, 50, 10, &Color::GREEN);
807
808 assert_eq!(img.get_pixel(50, 50), Some([0, 255, 0, 255]));
810 assert_eq!(img.get_pixel(50, 65), Some([0, 0, 0, 0]));
812 }
813
814 #[test]
815 fn test_render_rect_command() {
816 use presentar_core::draw::DrawCommand;
817 use presentar_core::Rect;
818
819 let mut img = Image::new(100, 100);
820 let commands = vec![DrawCommand::filled_rect(
821 Rect::new(10.0, 10.0, 30.0, 30.0),
822 Color::RED,
823 )];
824 img.render(&commands);
825
826 assert_eq!(img.get_pixel(20, 20), Some([255, 0, 0, 255]));
827 }
828
829 #[test]
830 fn test_render_circle_command() {
831 use presentar_core::draw::DrawCommand;
832 use presentar_core::Point;
833
834 let mut img = Image::new(100, 100);
835 let commands = vec![DrawCommand::filled_circle(
836 Point::new(50.0, 50.0),
837 15.0,
838 Color::BLUE,
839 )];
840 img.render(&commands);
841
842 assert_eq!(img.get_pixel(50, 50), Some([0, 0, 255, 255]));
843 }
844
845 #[test]
846 fn test_perceptual_diff_identical() {
847 let a = Image::filled(10, 10, 128, 64, 32, 255);
848 let b = Image::filled(10, 10, 128, 64, 32, 255);
849 assert_eq!(Snapshot::perceptual_diff(&a, &b), 0.0);
850 }
851
852 #[test]
853 fn test_perceptual_diff_different() {
854 let a = Image::filled(10, 10, 255, 255, 255, 255);
855 let b = Image::filled(10, 10, 0, 0, 0, 255);
856 let diff = Snapshot::perceptual_diff(&a, &b);
857 assert!((diff - 1.0).abs() < 0.001);
859 }
860
861 #[test]
862 fn test_perceptual_diff_partial() {
863 let a = Image::filled(10, 10, 100, 100, 100, 255);
864 let b = Image::filled(10, 10, 110, 100, 100, 255);
865 let diff = Snapshot::perceptual_diff(&a, &b);
866 assert!(diff > 0.0);
868 assert!(diff < 0.01);
869 }
870
871 #[test]
872 fn test_generate_diff_image() {
873 let a = Image::filled(10, 10, 255, 0, 0, 255);
874 let mut b = Image::filled(10, 10, 255, 0, 0, 255);
875 b.set_pixel(5, 5, [0, 255, 0, 255]);
876
877 let diff = Snapshot::generate_diff_image(&a, &b);
878
879 let pixel = diff.get_pixel(5, 5).expect("pixel exists");
881 assert_eq!(pixel[0], 255); let unchanged = diff.get_pixel(0, 0).expect("pixel exists");
885 assert!(unchanged[0] < 100); }
887
888 #[test]
889 fn test_alpha_blending() {
890 let mut img = Image::filled(10, 10, 255, 0, 0, 255); img.fill_rect(0, 0, 10, 10, &Color::new(0.0, 0.0, 1.0, 0.5)); let pixel = img.get_pixel(5, 5).expect("pixel exists");
894 assert!(pixel[0] > 100); assert!(pixel[2] > 100); }
898
899 #[test]
902 fn test_image_hash() {
903 let a = Image::filled(10, 10, 255, 0, 0, 255);
904 let b = Image::filled(10, 10, 255, 0, 0, 255);
905 let c = Image::filled(10, 10, 0, 255, 0, 255);
906
907 assert_eq!(a.hash(), b.hash());
908 assert_ne!(a.hash(), c.hash());
909 }
910
911 #[test]
912 fn test_image_region() {
913 let mut img = Image::new(100, 100);
914 img.fill_rect(10, 10, 20, 20, &Color::RED);
915
916 let region = img.region(10, 10, 20, 20);
917 assert_eq!(region.width, 20);
918 assert_eq!(region.height, 20);
919 assert_eq!(region.get_pixel(5, 5), Some([255, 0, 0, 255]));
920 }
921
922 #[test]
923 fn test_image_region_out_of_bounds() {
924 let img = Image::filled(10, 10, 255, 0, 0, 255);
925 let region = img.region(8, 8, 5, 5);
926
927 assert_eq!(region.width, 5);
929 assert_eq!(region.height, 5);
930 assert_eq!(region.get_pixel(0, 0), Some([255, 0, 0, 255]));
932 assert_eq!(region.get_pixel(3, 3), Some([0, 0, 0, 0]));
934 }
935
936 #[test]
937 fn test_image_scale() {
938 let img = Image::filled(10, 10, 255, 0, 0, 255);
939 let scaled = img.scale(20, 20);
940
941 assert_eq!(scaled.width, 20);
942 assert_eq!(scaled.height, 20);
943 assert_eq!(scaled.get_pixel(10, 10), Some([255, 0, 0, 255]));
944 }
945
946 #[test]
947 fn test_image_scale_down() {
948 let img = Image::filled(20, 20, 255, 0, 0, 255);
949 let scaled = img.scale(10, 10);
950
951 assert_eq!(scaled.width, 10);
952 assert_eq!(scaled.height, 10);
953 assert_eq!(scaled.get_pixel(5, 5), Some([255, 0, 0, 255]));
954 }
955
956 #[test]
957 fn test_image_count_color() {
958 let img = Image::filled(10, 10, 255, 0, 0, 255);
959 let count = img.count_color([255, 0, 0, 255], 0);
960 assert_eq!(count, 100);
961
962 let count = img.count_color([255, 5, 0, 255], 10);
963 assert_eq!(count, 100);
964
965 let count = img.count_color([0, 255, 0, 255], 0);
966 assert_eq!(count, 0);
967 }
968
969 #[test]
970 fn test_image_histogram() {
971 let img = Image::filled(10, 10, 255, 128, 0, 255);
972 let hist = img.histogram();
973
974 assert_eq!(hist[0][255], 100); assert_eq!(hist[1][128], 100); assert_eq!(hist[2][0], 100); assert_eq!(hist[3][255], 100); }
979
980 #[test]
981 fn test_image_mean_color() {
982 let img = Image::filled(10, 10, 100, 100, 100, 255);
983 let mean = img.mean_color();
984
985 assert!((mean[0] - 100.0).abs() < 0.01);
986 assert!((mean[1] - 100.0).abs() < 0.01);
987 assert!((mean[2] - 100.0).abs() < 0.01);
988 assert!((mean[3] - 255.0).abs() < 0.01);
989 }
990
991 #[test]
992 fn test_image_draw_line() {
993 let mut img = Image::new(100, 100);
994 img.draw_line(0, 0, 99, 99, &Color::WHITE);
995
996 assert_eq!(img.get_pixel(0, 0), Some([255, 255, 255, 255]));
998 assert_eq!(img.get_pixel(99, 99), Some([255, 255, 255, 255]));
999 assert_eq!(img.get_pixel(50, 50), Some([255, 255, 255, 255]));
1001 }
1002
1003 #[test]
1004 fn test_image_stroke_rect() {
1005 let mut img = Image::new(100, 100);
1006 img.stroke_rect(10, 10, 20, 20, &Color::WHITE);
1007
1008 assert_eq!(img.get_pixel(10, 10), Some([255, 255, 255, 255]));
1010 assert_eq!(img.get_pixel(29, 29), Some([255, 255, 255, 255]));
1011 assert_eq!(img.get_pixel(15, 15), Some([0, 0, 0, 0]));
1013 }
1014
1015 #[test]
1016 fn test_comparison_result_is_match() {
1017 let result = ComparisonResult {
1018 byte_diff: 0.01,
1019 perceptual_diff: 0.01,
1020 ssim: 0.99,
1021 same_dimensions: true,
1022 changed_pixels: 1,
1023 total_pixels: 100,
1024 };
1025
1026 assert!(result.is_match(0.05));
1027 assert!(!result.is_match(0.005));
1028 }
1029
1030 #[test]
1031 fn test_comparison_result_changed_percentage() {
1032 let result = ComparisonResult {
1033 byte_diff: 0.0,
1034 perceptual_diff: 0.0,
1035 ssim: 1.0,
1036 same_dimensions: true,
1037 changed_pixels: 10,
1038 total_pixels: 100,
1039 };
1040
1041 assert!((result.changed_percentage() - 10.0).abs() < 0.01);
1042 }
1043
1044 #[test]
1045 fn test_snapshot_compare_identical() {
1046 let a = Image::filled(10, 10, 255, 0, 0, 255);
1047 let b = Image::filled(10, 10, 255, 0, 0, 255);
1048
1049 let result = Snapshot::compare(&a, &b);
1050
1051 assert!(result.is_match(0.0));
1052 assert_eq!(result.byte_diff, 0.0);
1053 assert_eq!(result.changed_pixels, 0);
1054 assert!((result.ssim - 1.0).abs() < 0.01);
1055 }
1056
1057 #[test]
1058 fn test_snapshot_compare_different_dimensions() {
1059 let a = Image::new(10, 10);
1060 let b = Image::new(20, 20);
1061
1062 let result = Snapshot::compare(&a, &b);
1063
1064 assert!(!result.same_dimensions);
1065 assert_eq!(result.byte_diff, 1.0);
1066 assert_eq!(result.ssim, 0.0);
1067 }
1068
1069 #[test]
1070 fn test_snapshot_count_changed_pixels() {
1071 let a = Image::filled(10, 10, 255, 0, 0, 255);
1072 let mut b = Image::filled(10, 10, 255, 0, 0, 255);
1073 b.set_pixel(0, 0, [0, 255, 0, 255]);
1074 b.set_pixel(1, 0, [0, 255, 0, 255]);
1075
1076 let count = Snapshot::count_changed_pixels(&a, &b);
1077 assert_eq!(count, 2);
1078 }
1079
1080 #[test]
1081 fn test_snapshot_ssim_identical() {
1082 let a = Image::filled(10, 10, 128, 128, 128, 255);
1083 let b = Image::filled(10, 10, 128, 128, 128, 255);
1084
1085 let ssim = Snapshot::ssim(&a, &b);
1086 assert!((ssim - 1.0).abs() < 0.01);
1087 }
1088
1089 #[test]
1090 fn test_snapshot_ssim_different() {
1091 let a = Image::filled(10, 10, 255, 255, 255, 255);
1092 let b = Image::filled(10, 10, 0, 0, 0, 255);
1093
1094 let ssim = Snapshot::ssim(&a, &b);
1095 assert!(ssim < 0.5); }
1097
1098 #[test]
1099 fn test_snapshot_ssim_different_dimensions() {
1100 let a = Image::new(10, 10);
1101 let b = Image::new(20, 20);
1102
1103 let ssim = Snapshot::ssim(&a, &b);
1104 assert_eq!(ssim, 0.0);
1105 }
1106
1107 #[test]
1108 fn test_snapshot_compare_region() {
1109 let mut a = Image::new(100, 100);
1110 a.fill_rect(10, 10, 20, 20, &Color::RED);
1111 let mut b = Image::new(100, 100);
1112 b.fill_rect(10, 10, 20, 20, &Color::RED);
1113
1114 let result = Snapshot::compare_region(&a, &b, 10, 10, 20, 20);
1115 assert!(result.is_match(0.0));
1116 }
1117
1118 #[test]
1119 fn test_snapshot_compare_region_different() {
1120 let mut a = Image::new(100, 100);
1121 a.fill_rect(10, 10, 20, 20, &Color::RED);
1122 let mut b = Image::new(100, 100);
1123 b.fill_rect(10, 10, 20, 20, &Color::BLUE);
1124
1125 let result = Snapshot::compare_region(&a, &b, 10, 10, 20, 20);
1126 assert!(!result.is_match(0.0));
1127 assert!(result.byte_diff > 0.0);
1128 }
1129}