1use image::{
4 open, DynamicImage, GenericImage, GenericImageView, GrayImage, Luma, Pixel, Rgb, RgbImage,
5};
6
7use itertools::Itertools;
8use std::cmp::{max, min};
9use std::collections::HashSet;
10use std::fmt;
11use std::fmt::Write;
12use std::path::Path;
13
14#[macro_export]
83macro_rules! gray_image {
84 () => {
86 gray_image!(type: u8)
87 };
88 (type: $channel_type:ty) => {
90 {
91 use image::{ImageBuffer, Luma};
92 ImageBuffer::<Luma<$channel_type>, Vec<$channel_type>>::new(0, 0)
93 }
94 };
95 ($( $( $x: expr ),*);*) => {
97 gray_image!(type: u8, $( $( $x ),*);*)
98 };
99 (type: $channel_type:ty, $( $( $x: expr ),*);*) => {
101 {
102 use image::{ImageBuffer, Luma};
103
104 let nested_array = [ $( [ $($x),* ] ),* ];
105 let height = nested_array.len() as u32;
106 let width = nested_array[0].len() as u32;
107
108 let flat_array: Vec<_> = nested_array.iter()
109 .flat_map(|row| row.into_iter())
110 .cloned()
111 .collect();
112
113 ImageBuffer::<Luma<$channel_type>, Vec<$channel_type>>::from_raw(width, height, flat_array)
114 .unwrap()
115 }
116 }
117}
118
119#[macro_export]
185macro_rules! rgb_image {
186 () => {
188 rgb_image!(type: u8)
189 };
190 (type: $channel_type:ty) => {
192 {
193 use image::{ImageBuffer, Rgb};
194 ImageBuffer::<Rgb<$channel_type>, Vec<$channel_type>>::new(0, 0)
195 }
196 };
197 ($( $( [$r: expr, $g: expr, $b: expr]),*);*) => {
199 rgb_image!(type: u8, $( $( [$r, $g, $b]),*);*)
200 };
201 (type: $channel_type:ty, $( $( [$r: expr, $g: expr, $b: expr]),*);*) => {
203 {
204 use image::{ImageBuffer, Rgb};
205 let nested_array = [$( [ $([$r, $g, $b]),*]),*];
206 let height = nested_array.len() as u32;
207 let width = nested_array[0].len() as u32;
208
209 let flat_array: Vec<_> = nested_array.iter()
210 .flat_map(|row| row.into_iter().flat_map(|p| p.into_iter()))
211 .cloned()
212 .collect();
213
214 ImageBuffer::<Rgb<$channel_type>, Vec<$channel_type>>::from_raw(width, height, flat_array)
215 .unwrap()
216 }
217 }
218}
219
220#[macro_export]
286macro_rules! rgba_image {
287 () => {
289 rgba_image!(type: u8)
290 };
291 (type: $channel_type:ty) => {
293 {
294 use image::{ImageBuffer, Rgba};
295 ImageBuffer::<Rgba<$channel_type>, Vec<$channel_type>>::new(0, 0)
296 }
297 };
298 ($( $( [$r: expr, $g: expr, $b: expr, $a:expr]),*);*) => {
300 rgba_image!(type: u8, $( $( [$r, $g, $b, $a]),*);*)
301 };
302 (type: $channel_type:ty, $( $( [$r: expr, $g: expr, $b: expr, $a: expr]),*);*) => {
304 {
305 use image::{ImageBuffer, Rgba};
306 let nested_array = [$( [ $([$r, $g, $b, $a]),*]),*];
307 let height = nested_array.len() as u32;
308 let width = nested_array[0].len() as u32;
309
310 let flat_array: Vec<_> = nested_array.iter()
311 .flat_map(|row| row.into_iter().flat_map(|p| p.into_iter()))
312 .cloned()
313 .collect();
314
315 ImageBuffer::<Rgba<$channel_type>, Vec<$channel_type>>::from_raw(width, height, flat_array)
316 .unwrap()
317 }
318 }
319}
320
321pub fn pixel_diff_summary<I, J, P>(actual: &I, expected: &J) -> Option<String>
324where
325 P: Pixel + PartialEq,
326 P::Subpixel: fmt::Debug,
327 I: GenericImage<Pixel = P>,
328 J: GenericImage<Pixel = P>,
329{
330 significant_pixel_diff_summary(actual, expected, |p, q| p != q)
331}
332
333pub fn significant_pixel_diff_summary<I, J, F, P>(
337 actual: &I,
338 expected: &J,
339 is_significant_diff: F,
340) -> Option<String>
341where
342 P: Pixel,
343 P::Subpixel: fmt::Debug,
344 I: GenericImage<Pixel = P>,
345 J: GenericImage<Pixel = P>,
346 F: Fn((u32, u32, I::Pixel), (u32, u32, J::Pixel)) -> bool,
347{
348 if actual.dimensions() != expected.dimensions() {
349 return Some(format!(
350 "dimensions do not match. \
351 actual: {:?}, expected: {:?}",
352 actual.dimensions(),
353 expected.dimensions()
354 ));
355 }
356 let diffs = pixel_diffs(actual, expected, is_significant_diff);
357 if diffs.is_empty() {
358 return None;
359 }
360 Some(describe_pixel_diffs(actual, expected, &diffs))
361}
362
363#[macro_export]
365macro_rules! assert_pixels_eq {
366 ($actual:expr, $expected:expr) => {{
367 $crate::assert_dimensions_match!($actual, $expected);
368 match $crate::utils::pixel_diff_summary(&$actual, &$expected) {
369 None => {}
370 Some(err) => panic!("{}", err),
371 };
372 }};
373}
374
375#[macro_export]
378macro_rules! assert_pixels_eq_within {
379 ($actual:expr, $expected:expr, $channel_tolerance:expr) => {{
380 $crate::assert_dimensions_match!($actual, $expected);
381 let diffs = $crate::utils::pixel_diffs(&$actual, &$expected, |p, q| {
382 use image::Pixel;
383 let cp = p.2.channels();
384 let cq = q.2.channels();
385 if cp.len() != cq.len() {
386 panic!(
387 "pixels have different channel counts. \
388 actual: {:?}, expected: {:?}",
389 cp.len(),
390 cq.len()
391 )
392 }
393
394 let mut large_diff = false;
395 for i in 0..cp.len() {
396 let sp = cp[i];
397 let sq = cq[i];
398 let diff = if sp > sq { sp - sq } else { sq - sp };
400 if diff > $channel_tolerance {
401 large_diff = true;
402 break;
403 }
404 }
405
406 large_diff
407 });
408 if !diffs.is_empty() {
409 panic!(
410 "{}",
411 $crate::utils::describe_pixel_diffs(&$actual, &$expected, &diffs,)
412 )
413 }
414 }};
415}
416
417#[macro_export]
419macro_rules! assert_dimensions_match {
420 ($actual:expr, $expected:expr) => {{
421 let actual_dim = $actual.dimensions();
422 let expected_dim = $expected.dimensions();
423
424 if actual_dim != expected_dim {
425 panic!(
426 "dimensions do not match. \
427 actual: {:?}, expected: {:?}",
428 actual_dim, expected_dim
429 )
430 }
431 }};
432}
433
434pub fn pixel_diffs<I, J, F, P>(actual: &I, expected: &J, is_diff: F) -> Vec<Diff<I::Pixel>>
436where
437 P: Pixel,
438 I: GenericImage<Pixel = P>,
439 J: GenericImage<Pixel = P>,
440 F: Fn((u32, u32, I::Pixel), (u32, u32, J::Pixel)) -> bool,
441{
442 if is_empty(actual) || is_empty(expected) {
443 return vec![];
444 }
445
446 GenericImageView::pixels(actual)
450 .zip(GenericImageView::pixels(expected))
451 .filter(|&(p, q)| is_diff(p, q))
452 .map(|(p, q)| {
453 assert!(p.0 == q.0 && p.1 == q.1, "Pixel locations do not match");
454 Diff {
455 x: p.0,
456 y: p.1,
457 actual: p.2,
458 expected: q.2,
459 }
460 })
461 .collect::<Vec<_>>()
462}
463
464fn is_empty<I: GenericImage>(image: &I) -> bool {
465 image.width() == 0 || image.height() == 0
466}
467
468pub struct Diff<P> {
470 pub x: u32,
472 pub y: u32,
474 pub expected: P,
476 pub actual: P,
478}
479
480pub fn describe_pixel_diffs<I, J, P>(actual: &I, expected: &J, diffs: &[Diff<P>]) -> String
482where
483 P: Pixel,
484 P::Subpixel: fmt::Debug,
485 I: GenericImage<Pixel = P>,
486 J: GenericImage<Pixel = P>,
487{
488 let mut err = "pixels do not match.\n".to_owned();
489
490 let top_left = diffs.iter().fold((u32::MAX, u32::MAX), |acc, d| {
492 (acc.0.min(d.x), acc.1.min(d.y))
493 });
494 let bottom_right = diffs
495 .iter()
496 .fold((0, 0), |acc, d| (acc.0.max(d.x), acc.1.max(d.y)));
497
498 if max(bottom_right.0 - top_left.0, bottom_right.1 - top_left.1) < 6 {
501 let left = max(0, top_left.0 as i32 - 2) as u32;
502 let top = max(0, top_left.1 as i32 - 2) as u32;
503 let right = min(actual.width() as i32 - 1, bottom_right.0 as i32 + 2) as u32;
504 let bottom = min(actual.height() as i32 - 1, bottom_right.1 as i32 + 2) as u32;
505
506 let diff_locations = diffs.iter().map(|d| (d.x, d.y)).collect::<HashSet<_>>();
507
508 err.push_str(&colored("Actual:", Color::Red));
509 let actual_rendered = render_image_region(actual, left, top, right, bottom, |x, y| {
510 if diff_locations.contains(&(x, y)) {
511 Color::Red
512 } else {
513 Color::Cyan
514 }
515 });
516 err.push_str(&actual_rendered);
517
518 err.push_str(&colored("Expected:", Color::Green));
519 let expected_rendered = render_image_region(expected, left, top, right, bottom, |x, y| {
520 if diff_locations.contains(&(x, y)) {
521 Color::Green
522 } else {
523 Color::Cyan
524 }
525 });
526 err.push_str(&expected_rendered);
527
528 return err;
529 }
530
531 err.push_str(
533 &(diffs
534 .iter()
535 .take(5)
536 .map(|d| {
537 format!(
538 "\nlocation: {}, actual: {}, expected: {} ",
539 colored(&format!("{:?}", (d.x, d.y)), Color::Yellow),
540 colored(&render_pixel(d.actual), Color::Red),
541 colored(&render_pixel(d.expected), Color::Green)
542 )
543 })
544 .collect::<Vec<_>>()
545 .join("")),
546 );
547 err
548}
549
550enum Color {
551 Red,
552 Green,
553 Cyan,
554 Yellow,
555}
556
557fn render_image_region<I, P, C>(
558 image: &I,
559 left: u32,
560 top: u32,
561 right: u32,
562 bottom: u32,
563 color: C,
564) -> String
565where
566 P: Pixel,
567 P::Subpixel: fmt::Debug,
568 I: GenericImage<Pixel = P>,
569 C: Fn(u32, u32) -> Color,
570{
571 let mut rendered = String::new();
572
573 let mut rendered_pixels = vec![];
575 for y in top..bottom + 1 {
576 for x in left..right + 1 {
577 let p = image.get_pixel(x, y);
578 rendered_pixels.push(render_pixel(p));
579 }
580 }
581
582 let pixel_column_width = rendered_pixels.iter().map(|p| p.len()).max().unwrap() + 1;
584 let max_digits = (max(1, max(right, bottom)) as f64).log10().ceil() as usize;
586 let pixel_column_width = pixel_column_width.max(max_digits + 1);
588 let num_columns = (right - left + 1) as usize;
589
590 write!(rendered, "\n{}", " ".repeat(max_digits + 4)).unwrap();
592 for x in left..right + 1 {
593 write!(rendered, "{x:>w$} ", x = x, w = pixel_column_width).unwrap();
594 }
595
596 write!(
598 rendered,
599 "\n {}+{}",
600 " ".repeat(max_digits),
601 "-".repeat((pixel_column_width + 1) * num_columns + 1)
602 )
603 .unwrap();
604 write!(rendered, "\n {y:>w$}| ", y = " ", w = max_digits).unwrap();
606
607 let mut count = 0;
608 for y in top..bottom + 1 {
609 write!(rendered, "\n {y:>w$}| ", y = y, w = max_digits).unwrap();
611
612 for x in left..right + 1 {
613 let padded = format!(
615 "{c:>w$}",
616 c = rendered_pixels[count],
617 w = pixel_column_width
618 );
619 write!(rendered, "{} ", &colored(&padded, color(x, y))).unwrap();
620 count += 1;
621 }
622 write!(rendered, "\n {y:>w$}| ", y = " ", w = max_digits).unwrap();
624 }
625 rendered.push('\n');
626 rendered
627}
628
629fn render_pixel<P>(p: P) -> String
630where
631 P: Pixel,
632 P::Subpixel: fmt::Debug,
633{
634 let cs = p.channels();
635 match cs.len() {
636 1 => format!("{:?}", cs[0]),
637 _ => format!("[{}]", cs.iter().map(|c| format!("{:?}", c)).join(", ")),
638 }
639}
640
641fn colored(s: &str, c: Color) -> String {
642 let escape_sequence = match c {
643 Color::Red => "\x1b[31m",
644 Color::Green => "\x1b[32m",
645 Color::Cyan => "\x1b[36m",
646 Color::Yellow => "\x1b[33m",
647 };
648 format!("{}{}\x1b[0m", escape_sequence, s)
649}
650
651pub fn load_image_or_panic<P: AsRef<Path> + fmt::Debug>(path: P) -> DynamicImage {
653 open(path.as_ref()).expect(&format!("Could not load image at {:?}", path.as_ref()))
654}
655
656pub fn gray_bench_image(width: u32, height: u32) -> GrayImage {
660 let mut image = GrayImage::new(width, height);
661 for y in 0..image.height() {
662 for x in 0..image.width() {
663 let intensity = (x % 7 + y % 6) as u8;
664 image.put_pixel(x, y, Luma([intensity]));
665 }
666 }
667 image
668}
669
670pub fn rgb_bench_image(width: u32, height: u32) -> RgbImage {
672 use std::cmp;
673 let mut image = RgbImage::new(width, height);
674 for y in 0..image.height() {
675 for x in 0..image.width() {
676 let r = (x % 7 + y % 6) as u8;
677 let g = 255u8 - r;
678 let b = cmp::min(r, g);
679 image.put_pixel(x, y, Rgb([r, g, b]));
680 }
681 }
682 image
683}
684
685#[cfg(test)]
686mod tests {
687 use super::*;
688
689 #[test]
690 fn test_assert_pixels_eq_passes() {
691 let image = gray_image!(
692 00, 01, 02;
693 10, 11, 12);
694
695 assert_pixels_eq!(image, image);
696 }
697
698 #[test]
699 #[should_panic]
700 fn test_assert_pixels_eq_fails() {
701 let image = gray_image!(
702 00, 01, 02;
703 10, 11, 12);
704
705 let diff = gray_image!(
706 00, 11, 02;
707 10, 11, 12);
708
709 assert_pixels_eq!(diff, image);
710 }
711
712 #[test]
713 fn test_assert_pixels_eq_within_passes() {
714 let image = gray_image!(
715 00, 01, 02;
716 10, 11, 12);
717
718 let diff = gray_image!(
719 00, 02, 02;
720 10, 11, 12);
721
722 assert_pixels_eq_within!(diff, image, 1);
723 }
724
725 #[test]
726 #[should_panic]
727 fn test_assert_pixels_eq_within_fails() {
728 let image = gray_image!(
729 00, 01, 02;
730 10, 11, 12);
731
732 let diff = gray_image!(
733 00, 03, 02;
734 10, 11, 12);
735
736 assert_pixels_eq_within!(diff, image, 1);
737 }
738
739 #[test]
740 fn test_pixel_diff_summary_handles_1x1_image() {
741 let summary = pixel_diff_summary(&gray_image!(1), &gray_image!(0));
742 assert_eq!(&summary.unwrap()[0..19], "pixels do not match");
743 }
744}