1 /* gifdiff.c - Gifdiff compares GIF images for identical appearance.
2 Copyright (C) 1998-2021 Eddie Kohler, ekohler@gmail.com
3 This file is part of gifdiff, in the gifsicle package.
4
5 Gifdiff is free software. It is distributed under the GNU Public License,
6 version 2; you can copy, distribute, or alter it at will, as long
7 as this notice is kept intact and this source code is made available. There
8 is no warranty, express or implied. */
9
10 #include <config.h>
11 #include <lcdfgif/gif.h>
12 #include <lcdf/clp.h>
13 #include <stdarg.h>
14 #include <stdio.h>
15 #include <string.h>
16 #include <errno.h>
17 #if HAVE_UNISTD_H
18 # include <unistd.h>
19 #endif
20
21 #define QUIET_OPT 300
22 #define HELP_OPT 301
23 #define VERSION_OPT 302
24 #define IGNORE_REDUNDANCY_OPT 303
25 #define REDUNDANCY_OPT 304
26 #define IGNORE_BACKGROUND_OPT 305
27 #define BACKGROUND_OPT 306
28
29 const Clp_Option options[] = {
30 { "help", 'h', HELP_OPT, 0, 0 },
31 { "brief", 'q', QUIET_OPT, 0, Clp_Negate },
32 { "redudancy", 0, REDUNDANCY_OPT, 0, Clp_Negate },
33 { "ignore-redundancy", 'w', IGNORE_REDUNDANCY_OPT, 0, Clp_Negate },
34 { "bg", 0, BACKGROUND_OPT, 0, Clp_Negate },
35 { "ignore-bg", 0, IGNORE_BACKGROUND_OPT, 0, Clp_Negate },
36 { "background", 0, BACKGROUND_OPT, 0, Clp_Negate },
37 { "ignore-background", 'B', IGNORE_BACKGROUND_OPT, 0, Clp_Negate },
38 { "version", 'v', VERSION_OPT, 0, 0 }
39 };
40
41 const char *program_name;
42
43 static const char *filename1;
44 static const char *filename2;
45
46 static unsigned screen_width, screen_height;
47 #define TRANSP (0)
48
49 static uint16_t *gdata[2];
50 static uint16_t *glast[2];
51 static uint16_t *scratch;
52 static uint16_t *line;
53
54 static int brief;
55 static int ignore_redundancy;
56 static int ignore_background;
57
58 static Clp_Parser* clp;
59
60
61 static void
combine_colormaps(Gif_Colormap * gfcm,Gif_Colormap * newcm)62 combine_colormaps(Gif_Colormap *gfcm, Gif_Colormap *newcm)
63 {
64 int i, gfcm_ncol = gfcm ? gfcm->ncol : 0;
65 for (i = 0; i < gfcm_ncol; i++) {
66 Gif_Color *c = &gfcm->col[i];
67 c->pixel = Gif_AddColor(newcm, c, 1);
68 }
69 }
70
71 static void
fill_area(uint16_t * data,int l,int t,int w,int h,uint16_t val)72 fill_area(uint16_t *data, int l, int t, int w, int h, uint16_t val)
73 {
74 int x;
75 data += screen_width * t + l;
76 for (; h > 0; --h) {
77 for (x = w; x > 0; --x)
78 *data++ = val;
79 data += screen_width - w;
80 }
81 }
82
83 static void
copy_area(uint16_t * dst,const uint16_t * src,int l,int t,int w,int h)84 copy_area(uint16_t *dst, const uint16_t *src, int l, int t, int w, int h)
85 {
86 dst += screen_width * t + l;
87 src += screen_width * t + l;
88 for (; h > 0; --h, dst += screen_width, src += screen_width)
89 memcpy(dst, src, sizeof(uint16_t) * w);
90 }
91
92 static void
expand_bounds(int * lf,int * tp,int * rt,int * bt,const Gif_Image * gfi)93 expand_bounds(int *lf, int *tp, int *rt, int *bt, const Gif_Image *gfi)
94 {
95 int empty = (*lf >= *rt || *tp >= *bt);
96 if (empty || gfi->left < *lf)
97 *lf = gfi->left;
98 if (empty || gfi->top < *tp)
99 *tp = gfi->top;
100 if (empty || gfi->left + gfi->width > *rt)
101 *rt = gfi->left + gfi->width;
102 if (empty || gfi->top + gfi->height > *bt)
103 *bt = gfi->top + gfi->height;
104 }
105
106
107 static int
apply_image(int is_second,Gif_Stream * gfs,int imageno,uint16_t background)108 apply_image(int is_second, Gif_Stream *gfs, int imageno, uint16_t background)
109 {
110 int i, x, y, any_change;
111 Gif_Image *gfi = gfs->images[imageno];
112 Gif_Image *pgfi = imageno ? gfs->images[imageno - 1] : 0;
113 int width = gfi->width;
114 uint16_t map[256];
115 uint16_t *data = gdata[is_second];
116 uint16_t *last = glast[is_second];
117 Gif_Colormap *gfcm = gfi->local ? gfi->local : gfs->global;
118 int gfcm_ncol = gfcm ? gfcm->ncol : 0;
119
120 /* set up colormap */
121 for (i = 0; i < gfcm_ncol; ++i)
122 map[i] = gfcm->col[i].pixel;
123 for (i = gfcm_ncol; i < 256; ++i)
124 map[i] = 1;
125 if (gfi->transparent >= 0 && gfi->transparent < 256)
126 map[gfi->transparent] = TRANSP;
127
128 /* if this image's disposal is 'previous', save the post-disposal version in
129 'scratch' */
130 if (gfi->disposal == GIF_DISPOSAL_PREVIOUS) {
131 copy_area(scratch, data, gfi->left, gfi->top, gfi->width, gfi->height);
132 if (pgfi && pgfi->disposal == GIF_DISPOSAL_PREVIOUS)
133 copy_area(scratch, last, pgfi->left, pgfi->top, pgfi->width, pgfi->height);
134 else if (pgfi && pgfi->disposal == GIF_DISPOSAL_BACKGROUND)
135 fill_area(scratch, pgfi->left, pgfi->top, pgfi->width, pgfi->height, background);
136 }
137
138 /* uncompress and clip */
139 Gif_UncompressImage(gfs, gfi);
140 Gif_ClipImage(gfi, 0, 0, screen_width, screen_height);
141
142 any_change = imageno == 0;
143 {
144 int lf = 0, tp = 0, rt = 0, bt = 0;
145 expand_bounds(&lf, &tp, &rt, &bt, gfi);
146 if (pgfi && pgfi->disposal == GIF_DISPOSAL_PREVIOUS)
147 expand_bounds(&lf, &tp, &rt, &bt, pgfi);
148 else if (pgfi && pgfi->disposal == GIF_DISPOSAL_BACKGROUND) {
149 expand_bounds(&lf, &tp, &rt, &bt, pgfi);
150 fill_area(last, pgfi->left, pgfi->top, pgfi->width, pgfi->height, background);
151 } else
152 pgfi = 0;
153 for (y = tp; y < bt; ++y) {
154 uint16_t *outd = data + screen_width * y + lf;
155 if (!any_change)
156 memcpy(line, outd, (rt - lf) * sizeof(uint16_t));
157 if (pgfi && y >= pgfi->top && y < pgfi->top + pgfi->height)
158 memcpy(outd + pgfi->left - lf,
159 last + screen_width * y + pgfi->left,
160 pgfi->width * sizeof(uint16_t));
161 if (y >= gfi->top && y < gfi->top + gfi->height) {
162 uint16_t *xoutd = outd + gfi->left - lf;
163 const uint8_t *ind = gfi->img[y - gfi->top];
164 for (x = 0; x < width; ++x, ++ind, ++xoutd)
165 if (map[*ind] != TRANSP)
166 *xoutd = map[*ind];
167 }
168 if (!any_change && memcmp(line, outd, (rt - lf) * sizeof(uint16_t)) != 0)
169 any_change = 1;
170 }
171 }
172
173 Gif_ReleaseUncompressedImage(gfi);
174 Gif_ReleaseCompressedImage(gfi);
175
176 /* switch 'glast' with 'scratch' if necessary */
177 if (gfi->disposal == GIF_DISPOSAL_PREVIOUS) {
178 uint16_t *x = scratch;
179 scratch = glast[is_second];
180 glast[is_second] = x;
181 }
182
183 return any_change;
184 }
185
186
187 #define SAME 0
188 #define DIFFERENT 1
189 static int was_different;
190
191 static void
different(const char * format,...)192 different(const char *format, ...)
193 {
194 va_list val;
195 va_start(val, format);
196 if (!brief) {
197 vfprintf(stdout, format, val);
198 fputc('\n', stdout);
199 }
200 va_end(val);
201 was_different = 1;
202 }
203
204
205 static void
name_loopcount(int loopcount,char * buf)206 name_loopcount(int loopcount, char *buf)
207 {
208 if (loopcount < 0)
209 strcpy(buf, "none");
210 else if (loopcount == 0)
211 strcpy(buf, "forever");
212 else
213 sprintf(buf, "%d", loopcount);
214 }
215
216 static void
name_delay(int delay,char * buf)217 name_delay(int delay, char *buf)
218 {
219 if (delay == 0)
220 strcpy(buf, "none");
221 else
222 sprintf(buf, "%d.%02ds", delay / 100, delay % 100);
223 }
224
225 static void
name_color(int color,Gif_Colormap * gfcm,char * buf)226 name_color(int color, Gif_Colormap *gfcm, char *buf)
227 {
228 if (color == TRANSP)
229 strcpy(buf, "transparent");
230 else {
231 Gif_Color *c = &gfcm->col[color];
232 sprintf(buf, "#%02X%02X%02X", c->gfc_red, c->gfc_green, c->gfc_blue);
233 }
234 }
235
236
237 int
compare(Gif_Stream * s1,Gif_Stream * s2)238 compare(Gif_Stream *s1, Gif_Stream *s2)
239 {
240 Gif_Colormap *newcm;
241 int imageno1, imageno2, background1, background2;
242 char buf1[256], buf2[256], fbuf[256];
243
244 was_different = 0;
245
246 /* Compare image counts and screen sizes. If either of these differs, quit
247 early. */
248 Gif_CalculateScreenSize(s1, 0);
249 Gif_CalculateScreenSize(s2, 0);
250
251 if (s1->screen_width != s2->screen_width
252 || s1->screen_height != s2->screen_height) {
253 different("screen sizes differ: <%dx%d >%dx%d", s1->screen_width,
254 s1->screen_height, s2->screen_width, s2->screen_height);
255 return DIFFERENT;
256 }
257
258 if (s1->screen_width == 0 || s1->screen_height == 0
259 || s2->screen_width == 0 || s2->screen_height == 0) {
260 /* paranoia -- don't think this can happen */
261 different("zero screen sizes");
262 return DIFFERENT;
263 }
264
265 if (s1->nimages == 0 || s2->nimages == 0) {
266 if (s1->nimages != s2->nimages) {
267 different("frame counts differ: <#%d >#%d", s1->nimages, s2->nimages);
268 return DIFFERENT;
269 } else
270 return SAME;
271 }
272
273 /* Create arrays for the image data */
274 screen_width = s1->screen_width;
275 screen_height = s1->screen_height;
276
277 gdata[0] = Gif_NewArray(uint16_t, screen_width * screen_height);
278 gdata[1] = Gif_NewArray(uint16_t, screen_width * screen_height);
279 glast[0] = Gif_NewArray(uint16_t, screen_width * screen_height);
280 glast[1] = Gif_NewArray(uint16_t, screen_width * screen_height);
281 scratch = Gif_NewArray(uint16_t, screen_width * screen_height);
282 line = Gif_NewArray(uint16_t, screen_width);
283
284 /* Merge all distinct colors from the two images into one colormap, setting
285 the 'pixel' slots in the images' colormaps to the corresponding values
286 in the merged colormap. Don't forget transparency */
287 newcm = Gif_NewFullColormap(1, 256);
288 combine_colormaps(s1->global, newcm);
289 combine_colormaps(s2->global, newcm);
290 for (imageno1 = 0; imageno1 < s1->nimages; ++imageno1)
291 combine_colormaps(s1->images[imageno1]->local, newcm);
292 for (imageno2 = 0; imageno2 < s2->nimages; ++imageno2)
293 combine_colormaps(s2->images[imageno2]->local, newcm);
294
295 /* Choose the background values */
296 background1 = background2 = TRANSP;
297 if ((s1->nimages == 0 || s1->images[0]->transparent < 0)
298 && s1->global && s1->background < s1->global->ncol)
299 background1 = s1->global->col[ s1->background ].pixel;
300 if ((s2->nimages == 0 || s2->images[0]->transparent < 0)
301 && s2->global && s2->background < s2->global->ncol)
302 background2 = s2->global->col[ s2->background ].pixel;
303
304 /* Clear screens */
305 fill_area(gdata[0], 0, 0, screen_width, screen_height, TRANSP);
306 fill_area(gdata[1], 0, 0, screen_width, screen_height, TRANSP);
307
308 /* Loopcounts differ? */
309 if (s1->loopcount != s2->loopcount) {
310 name_loopcount(s1->loopcount, buf1);
311 name_loopcount(s2->loopcount, buf2);
312 different("loop counts differ: <%s >%s", buf1, buf2);
313 }
314
315 /* Loop over frames, comparing image data and delays */
316 apply_image(0, s1, 0, background1);
317 apply_image(1, s2, 0, background2);
318 imageno1 = imageno2 = 0;
319 while (imageno1 != s1->nimages && imageno2 != s2->nimages) {
320 int fi1 = imageno1, fi2 = imageno2,
321 delay1 = s1->images[fi1]->delay, delay2 = s2->images[fi2]->delay;
322
323 /* get message right */
324 if (imageno1 == imageno2)
325 sprintf(fbuf, "#%d", imageno1);
326 else
327 sprintf(fbuf, "<#%d >#%d", imageno1, imageno2);
328
329 /* compare pixels */
330 if (memcmp(gdata[0], gdata[1],
331 screen_width * screen_height * sizeof(uint16_t)) != 0) {
332 unsigned d, c = screen_width * screen_height;
333 uint16_t *d1 = gdata[0], *d2 = gdata[1];
334 for (d = 0; d < c; d++, d1++, d2++)
335 if (*d1 != *d2) {
336 name_color(*d1, newcm, buf1);
337 name_color(*d2, newcm, buf2);
338 different("frame %s pixels differ: %d,%d <%s >%s",
339 fbuf, d % screen_width, d / screen_width, buf1, buf2);
340 break;
341 }
342 }
343
344 /* compare background */
345 if (!ignore_background && background1 != background2
346 && (imageno1 == 0 || s1->images[imageno1 - 1]->disposal == GIF_DISPOSAL_BACKGROUND)
347 && (imageno2 == 0 || s2->images[imageno2 - 1]->disposal == GIF_DISPOSAL_BACKGROUND)) {
348 unsigned d, c = screen_width * screen_height;
349 uint16_t *d1 = gdata[0], *d2 = gdata[1];
350 for (d = 0; d < c; ++d, ++d1, ++d2)
351 if (*d1 == TRANSP || *d2 == TRANSP) {
352 name_color(background1, newcm, buf1);
353 name_color(background2, newcm, buf2);
354 different("frame %s background pixels differ: %d,%d <%s >%s",
355 fbuf, d % screen_width, d / screen_width, buf1, buf2);
356 background1 = background2 = TRANSP;
357 break;
358 }
359 }
360
361 /* move to next images, skipping redundancy */
362 for (++imageno1;
363 imageno1 < s1->nimages && !apply_image(0, s1, imageno1, background1);
364 ++imageno1)
365 delay1 += s1->images[imageno1]->delay;
366 for (++imageno2;
367 imageno2 < s2->nimages && !apply_image(1, s2, imageno2, background2);
368 ++imageno2)
369 delay2 += s2->images[imageno2]->delay;
370
371 if (!ignore_redundancy) {
372 fi1 = (imageno1 - fi1) - (imageno2 - fi2);
373 for (; fi1 > 0; --fi1)
374 different("extra redundant frame: <#%d", imageno1 - fi1);
375 for (; fi1 < 0; ++fi1)
376 different("extra redundant frame: >#%d", imageno2 + fi1);
377 }
378
379 if (delay1 != delay2) {
380 name_delay(delay1, buf1);
381 name_delay(delay2, buf2);
382 different("frame %s delays differ: <%s >%s", fbuf, buf1, buf2);
383 }
384 }
385
386 if (imageno1 != s1->nimages || imageno2 != s2->nimages)
387 different("frame counts differ: <#%d >#%d", s1->nimages, s2->nimages);
388
389 /* That's it! */
390 Gif_DeleteColormap(newcm);
391 Gif_DeleteArray(gdata[0]);
392 Gif_DeleteArray(gdata[1]);
393 Gif_DeleteArray(glast[0]);
394 Gif_DeleteArray(glast[1]);
395 Gif_DeleteArray(scratch);
396 Gif_DeleteArray(line);
397
398 return was_different ? DIFFERENT : SAME;
399 }
400
401
short_usage(void)402 void short_usage(void) {
403 Clp_fprintf(clp, stderr, "Usage: %s [OPTION]... FILE1 FILE2\n\
404 Try %<%s --help%> for more information.\n",
405 program_name, program_name);
406 }
407
usage(void)408 void usage(void) {
409 Clp_fprintf(clp, stdout, "\
410 %<Gifdiff%> compares two GIF files (either images or animations) for identical\n\
411 visual appearance. An animation and an optimized version of the same animation\n\
412 should compare as the same. Gifdiff exits with status 0 if the images are\n\
413 the same, 1 if they%,re different, and 2 if there was some error.\n\
414 \n\
415 Usage: %s [OPTION]... FILE1 FILE2\n\n", program_name);
416 Clp_fprintf(clp, stdout, "\
417 Options:\n\
418 -q, --brief Don%,t report detailed differences.\n\
419 -w, --ignore-redundancy Ignore differences in redundant frames.\n\
420 -B, --ignore-background Ignore differences in background colors.\n\
421 -h, --help Print this message and exit.\n\
422 -v, --version Print version number and exit.\n\
423 \n\
424 Report bugs to <ekohler@gmail.com>.\n");
425 }
426
427
fatal_error(const char * format,...)428 void fatal_error(const char* format, ...) {
429 char buf[BUFSIZ];
430 int n = snprintf(buf, BUFSIZ, "%s: ", program_name);
431 va_list val;
432 va_start(val, format);
433 Clp_vsnprintf(clp, buf + n, BUFSIZ - n, format, val);
434 va_end(val);
435 fputs(buf, stderr);
436 exit(2); /* exit(2) for trouble */
437 }
438
error(const char * format,...)439 void error(const char* format, ...) {
440 char buf[BUFSIZ];
441 int n = snprintf(buf, BUFSIZ, "%s: ", program_name);
442 va_list val;
443 va_start(val, format);
444 Clp_vsnprintf(clp, buf + n, BUFSIZ - n, format, val);
445 va_end(val);
446 fputs(buf, stderr);
447 }
448
449 static int gifread_error_count;
450
451 static void
gifread_error(Gif_Stream * gfs,Gif_Image * gfi,int is_error,const char * message)452 gifread_error(Gif_Stream* gfs, Gif_Image* gfi,
453 int is_error, const char *message)
454 {
455 static int last_is_error = 0;
456 static int last_which_image = 0;
457 static char last_message[256];
458 static int different_error_count = 0;
459 static int same_error_count = 0;
460 int which_image = Gif_ImageNumber(gfs, gfi);
461 const char *filename = gfs->landmark;
462 if (which_image < 0)
463 which_image = gfs->nimages;
464
465 if (gifread_error_count == 0) {
466 last_which_image = -1;
467 last_message[0] = 0;
468 different_error_count = 0;
469 }
470
471 gifread_error_count++;
472 if (last_message[0] && different_error_count <= 10
473 && (last_which_image != which_image || message == 0
474 || strcmp(message, last_message) != 0)) {
475 const char *etype = last_is_error ? "error" : "warning";
476 error("While reading %<%s%> frame #%d:\n", filename, last_which_image);
477 if (same_error_count == 1)
478 error(" %s: %s\n", etype, last_message);
479 else if (same_error_count > 0)
480 error(" %s: %s (%d times)\n", etype, last_message, same_error_count);
481 same_error_count = 0;
482 last_message[0] = 0;
483 }
484
485 if (message) {
486 if (last_message[0] == 0)
487 different_error_count++;
488 same_error_count++;
489 strcpy(last_message, message);
490 last_which_image = which_image;
491 last_is_error = is_error;
492 } else
493 last_message[0] = 0;
494
495 if (different_error_count == 11 && message) {
496 error("(more errors while reading %<%s%>)\n", filename);
497 different_error_count++;
498 }
499 }
500
501 static Gif_Stream *
read_stream(const char ** filename)502 read_stream(const char **filename)
503 {
504 FILE *f;
505 Gif_Stream *gfs;
506 if (*filename == 0) {
507 #if 0
508 /* Since gifdiff always takes explicit filename arguments,
509 allow explicit reads from terminal. */
510 #ifndef OUTPUT_GIF_TO_TERMINAL
511 if (isatty(fileno(stdin))) {
512 fatal_error("<stdin>: is a terminal\n");
513 return NULL;
514 }
515 #endif
516 #endif
517 f = stdin;
518 #if defined(_MSDOS) || defined(_WIN32)
519 _setmode(_fileno(stdin), _O_BINARY);
520 #elif defined(__DJGPP__)
521 setmode(fileno(stdin), O_BINARY);
522 #elif defined(__EMX__)
523 _fsetmode(stdin, "b");
524 #endif
525 *filename = "<stdin>";
526 } else {
527 f = fopen(*filename, "rb");
528 if (!f)
529 fatal_error("%s: %s\n", *filename, strerror(errno));
530 }
531 gifread_error_count = 0;
532 gfs = Gif_FullReadFile(f, GIF_READ_COMPRESSED, *filename, gifread_error);
533 if (!gfs)
534 fatal_error("%s: file not in GIF format\n", *filename);
535 return gfs;
536 }
537
538 int
main(int argc,char * argv[])539 main(int argc, char *argv[])
540 {
541 int how_many_inputs = 0;
542 int status;
543 const char **inputp;
544 Gif_Stream *gfs1, *gfs2;
545
546 clp = Clp_NewParser(argc, (const char * const *)argv,
547 sizeof(options) / sizeof(options[0]), options);
548
549 program_name = Clp_ProgramName(clp);
550 brief = 0;
551
552 while (1) {
553 int opt = Clp_Next(clp);
554 switch (opt) {
555
556 case HELP_OPT:
557 usage();
558 exit(0);
559 break;
560
561 case VERSION_OPT:
562 printf("gifdiff (LCDF Gifsicle) %s\n", VERSION);
563 printf("Copyright (C) 1998-2021 Eddie Kohler\n\
564 This is free software; see the source for copying conditions.\n\
565 There is NO warranty, not even for merchantability or fitness for a\n\
566 particular purpose.\n");
567 exit(0);
568 break;
569
570 case QUIET_OPT:
571 brief = !clp->negated;
572 break;
573
574 case IGNORE_REDUNDANCY_OPT:
575 ignore_redundancy = !clp->negated;
576 break;
577
578 case REDUNDANCY_OPT:
579 ignore_redundancy = !!clp->negated;
580 break;
581
582 case IGNORE_BACKGROUND_OPT:
583 ignore_background = !clp->negated;
584 break;
585
586 case BACKGROUND_OPT:
587 ignore_background = !!clp->negated;
588 break;
589
590 case Clp_NotOption:
591 if (how_many_inputs == 2) {
592 error("too many file arguments\n");
593 goto bad_option;
594 }
595 inputp = (how_many_inputs == 0 ? &filename1 : &filename2);
596 how_many_inputs++;
597 if (strcmp(clp->vstr, "-") == 0)
598 *inputp = 0;
599 else
600 *inputp = clp->vstr;
601 break;
602
603 bad_option:
604 case Clp_BadOption:
605 short_usage();
606 exit(1);
607 break;
608
609 case Clp_Done:
610 goto done;
611
612 }
613 }
614
615 done:
616
617 if (how_many_inputs < 2)
618 fatal_error("need exactly 2 file arguments\n");
619 if (filename1 == 0 && filename2 == 0)
620 fatal_error("can%,t read both files from stdin\n");
621
622 gfs1 = read_stream(&filename1);
623 gfs2 = read_stream(&filename2);
624
625 status = (compare(gfs1, gfs2) == DIFFERENT);
626 if (status == 1 && brief)
627 printf("GIF files %s and %s differ\n", filename1, filename2);
628
629 Gif_DeleteStream(gfs1);
630 Gif_DeleteStream(gfs2);
631 return status;
632 }
633