// 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 // 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); } // Minimize number of frames by combining successive frames that have exact same // ARGB data into a single longer duration frame. static void MinimizeAnimationFrames(AnimatedImage* const img) { 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; // If merging frames will result in integer overflow for 'duration', // skip merging. if (AdditionWillOverflow(frame1->duration, frame2->duration)) continue; if (!memcmp(rgba1, rgba2, img->canvas_width * 4 * img->canvas_height)) { // 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("\nUsage: anim_diff [-dump_frames ] " "[-min_psnr ][-raw_comparison]\n"); } 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 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; } 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]); } } 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; }