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