// Copyright 2015 Google Inc. All Rights Reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the COPYING file in the root of the source // tree. An additional intellectual property rights grant can be found // in the file PATENTS. All contributing project authors may // be found in the AUTHORS file in the root of the source tree. // ----------------------------------------------------------------------------- // // Checks if given pair of animated GIF/WebP images are identical: // That is: their reconstructed canvases match pixel-by-pixel and their other // animation properties (loop count etc) also match. // // example: anim_diff foo.gif bar.webp #include #include #include #include // for 'strtod'. #include // for 'strcmp'. #include "./anim_util.h" #if defined(_MSC_VER) && _MSC_VER < 1900 #define snprintf _snprintf #endif // Returns true if 'a + b' will overflow. static int AdditionWillOverflow(int a, int b) { return (b > 0) && (a > INT_MAX - b); } static int FramesAreEqual(const uint8_t* const rgba1, const uint8_t* const rgba2, int width, int height) { const int stride = width * 4; // Always true for 'DecodedFrame.rgba'. return !memcmp(rgba1, rgba2, stride * height); } static WEBP_INLINE int PixelsAreSimilar(uint32_t src, uint32_t dst, int max_allowed_diff) { const int src_a = (src >> 24) & 0xff; const int src_r = (src >> 16) & 0xff; const int src_g = (src >> 8) & 0xff; const int src_b = (src >> 0) & 0xff; const int dst_a = (dst >> 24) & 0xff; const int dst_r = (dst >> 16) & 0xff; const int dst_g = (dst >> 8) & 0xff; const int dst_b = (dst >> 0) & 0xff; return (abs(src_r * src_a - dst_r * dst_a) <= (max_allowed_diff * 255)) && (abs(src_g * src_a - dst_g * dst_a) <= (max_allowed_diff * 255)) && (abs(src_b * src_a - dst_b * dst_a) <= (max_allowed_diff * 255)) && (abs(src_a - dst_a) <= max_allowed_diff); } static int FramesAreSimilar(const uint8_t* const rgba1, const uint8_t* const rgba2, int width, int height, int max_allowed_diff) { int i, j; assert(max_allowed_diff > 0); for (j = 0; j < height; ++j) { for (i = 0; i < width; ++i) { const int stride = width * 4; const size_t offset = j * stride + i; if (!PixelsAreSimilar(rgba1[offset], rgba2[offset], max_allowed_diff)) { return 0; } } } return 1; } // Minimize number of frames by combining successive frames that have at max // 'max_diff' difference per channel between corresponding pixels. static void MinimizeAnimationFrames(AnimatedImage* const img, int max_diff) { uint32_t i; for (i = 1; i < img->num_frames; ++i) { DecodedFrame* const frame1 = &img->frames[i - 1]; DecodedFrame* const frame2 = &img->frames[i]; const uint8_t* const rgba1 = frame1->rgba; const uint8_t* const rgba2 = frame2->rgba; int should_merge_frames = 0; // If merging frames will result in integer overflow for 'duration', // skip merging. if (AdditionWillOverflow(frame1->duration, frame2->duration)) continue; if (max_diff > 0) { should_merge_frames = FramesAreSimilar(rgba1, rgba2, img->canvas_width, img->canvas_height, max_diff); } else { should_merge_frames = FramesAreEqual(rgba1, rgba2, img->canvas_width, img->canvas_height); } if (should_merge_frames) { // Merge 'i+1'th frame into 'i'th frame. frame1->duration += frame2->duration; if (i + 1 < img->num_frames) { memmove(&img->frames[i], &img->frames[i + 1], (img->num_frames - i - 1) * sizeof(*img->frames)); } --img->num_frames; --i; } } } static int CompareValues(uint32_t a, uint32_t b, const char* output_str) { if (a != b) { fprintf(stderr, "%s: %d vs %d\n", output_str, a, b); return 0; } return 1; } // Note: As long as frame durations and reconstructed frames are identical, it // is OK for other aspects like offsets, dispose/blend method to vary. static int CompareAnimatedImagePair(const AnimatedImage* const img1, const AnimatedImage* const img2, int premultiply, double min_psnr) { int ok = 1; const int is_multi_frame_image = (img1->num_frames > 1); uint32_t i; ok = CompareValues(img1->canvas_width, img2->canvas_width, "Canvas width mismatch") && ok; ok = CompareValues(img1->canvas_height, img2->canvas_height, "Canvas height mismatch") && ok; ok = CompareValues(img1->num_frames, img2->num_frames, "Frame count mismatch") && ok; if (!ok) return 0; // These are fatal failures, can't proceed. if (is_multi_frame_image) { // Checks relevant for multi-frame images only. ok = CompareValues(img1->loop_count, img2->loop_count, "Loop count mismatch") && ok; ok = CompareValues(img1->bgcolor, img2->bgcolor, "Background color mismatch") && ok; } for (i = 0; i < img1->num_frames; ++i) { // Pixel-by-pixel comparison. const uint8_t* const rgba1 = img1->frames[i].rgba; const uint8_t* const rgba2 = img2->frames[i].rgba; int max_diff; double psnr; if (is_multi_frame_image) { // Check relevant for multi-frame images only. const char format[] = "Frame #%d, duration mismatch"; char tmp[sizeof(format) + 8]; ok = ok && (snprintf(tmp, sizeof(tmp), format, i) >= 0); ok = ok && CompareValues(img1->frames[i].duration, img2->frames[i].duration, tmp); } GetDiffAndPSNR(rgba1, rgba2, img1->canvas_width, img1->canvas_height, premultiply, &max_diff, &psnr); if (min_psnr > 0.) { if (psnr < min_psnr) { fprintf(stderr, "Frame #%d, psnr = %.2lf (min_psnr = %f)\n", i, psnr, min_psnr); ok = 0; } } else { if (max_diff != 0) { fprintf(stderr, "Frame #%d, max pixel diff: %d\n", i, max_diff); ok = 0; } } } return ok; } static void Help(void) { printf("Usage: anim_diff [options]\n"); printf("\nOptions:\n"); printf(" -dump_frames dump decoded frames in PAM format\n"); printf(" -min_psnr ... minimum per-frame PSNR\n"); printf(" -raw_comparison ..... if this flag is not used, RGB is\n"); printf(" premultiplied before comparison\n"); #ifdef WEBP_EXPERIMENTAL_FEATURES printf(" -max_diff ..... maximum allowed difference per channel " " between corresponding pixels in subsequent" " frames\n"); #endif } int main(int argc, const char* argv[]) { int return_code = -1; int dump_frames = 0; const char* dump_folder = NULL; double min_psnr = 0.; int got_input1 = 0; int got_input2 = 0; int premultiply = 1; int max_diff = 0; int i, c; const char* files[2] = { NULL, NULL }; AnimatedImage images[2]; if (argc < 3) { Help(); return -1; } for (c = 1; c < argc; ++c) { int parse_error = 0; if (!strcmp(argv[c], "-dump_frames")) { if (c < argc - 1) { dump_frames = 1; dump_folder = argv[++c]; } else { parse_error = 1; } } else if (!strcmp(argv[c], "-min_psnr")) { if (c < argc - 1) { const char* const v = argv[++c]; char* end = NULL; const double d = strtod(v, &end); if (end == v) { parse_error = 1; fprintf(stderr, "Error! '%s' is not a floating point number.\n", v); } min_psnr = d; } else { parse_error = 1; } } else if (!strcmp(argv[c], "-raw_comparison")) { premultiply = 0; #ifdef WEBP_EXPERIMENTAL_FEATURES } else if (!strcmp(argv[c], "-max_diff")) { if (c < argc - 1) { const char* const v = argv[++c]; char* end = NULL; const int n = (int)strtol(v, &end, 10); if (end == v) { parse_error = 1; fprintf(stderr, "Error! '%s' is not an integer.\n", v); } max_diff = n; } else { parse_error = 1; } #endif } else { if (!got_input1) { files[0] = argv[c]; got_input1 = 1; } else if (!got_input2) { files[1] = argv[c]; got_input2 = 1; } else { parse_error = 1; } } if (parse_error) { Help(); return -1; } } if (!got_input2) { Help(); return -1; } if (dump_frames) { printf("Dumping decoded frames in: %s\n", dump_folder); } memset(images, 0, sizeof(images)); for (i = 0; i < 2; ++i) { printf("Decoding file: %s\n", files[i]); if (!ReadAnimatedImage(files[i], &images[i], dump_frames, dump_folder)) { fprintf(stderr, "Error decoding file: %s\n Aborting.\n", files[i]); return_code = -2; goto End; } else { MinimizeAnimationFrames(&images[i], max_diff); } } if (!CompareAnimatedImagePair(&images[0], &images[1], premultiply, min_psnr)) { fprintf(stderr, "\nFiles %s and %s differ.\n", files[0], files[1]); return_code = -3; } else { printf("\nFiles %s and %s are identical.\n", files[0], files[1]); return_code = 0; } End: ClearAnimatedImage(&images[0]); ClearAnimatedImage(&images[1]); return return_code; }