1 /*
2  * Copyright 2011 Google Inc.
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 #include "skdiff.h"
8 #include "skdiff_html.h"
9 #include "skdiff_utils.h"
10 #include "SkBitmap.h"
11 #include "SkData.h"
12 #include "SkForceLinking.h"
13 #include "SkImageDecoder.h"
14 #include "SkImageEncoder.h"
15 #include "SkOSFile.h"
16 #include "SkStream.h"
17 #include "SkTDArray.h"
18 #include "SkTemplates.h"
19 #include "SkTSearch.h"
20 
21 __SK_FORCE_IMAGE_DECODER_LINKING;
22 
23 /**
24  * skdiff
25  *
26  * Given three directory names, expects to find identically-named files in
27  * each of the first two; the first are treated as a set of baseline,
28  * the second a set of variant images, and a diff image is written into the
29  * third directory for each pair.
30  * Creates an index.html in the current third directory to compare each
31  * pair that does not match exactly.
32  * Recursively descends directories, unless run with --norecurse.
33  *
34  * Returns zero exit code if all images match across baseDir and comparisonDir.
35  */
36 
37 typedef SkTDArray<SkString*> StringArray;
38 typedef StringArray FileArray;
39 
add_unique_basename(StringArray * array,const SkString & filename)40 static void add_unique_basename(StringArray* array, const SkString& filename) {
41     // trim off dirs
42     const char* src = filename.c_str();
43     const char* trimmed = strrchr(src, SkPATH_SEPARATOR);
44     if (trimmed) {
45         trimmed += 1;   // skip the separator
46     } else {
47         trimmed = src;
48     }
49     const char* end = strrchr(trimmed, '.');
50     if (!end) {
51         end = trimmed + strlen(trimmed);
52     }
53     SkString result(trimmed, end - trimmed);
54 
55     // only add unique entries
56     for (int i = 0; i < array->count(); ++i) {
57         if (*array->getAt(i) == result) {
58             return;
59         }
60     }
61     *array->append() = new SkString(result);
62 }
63 
64 struct DiffSummary {
DiffSummaryDiffSummary65     DiffSummary ()
66         : fNumMatches(0)
67         , fNumMismatches(0)
68         , fMaxMismatchV(0)
69         , fMaxMismatchPercent(0) { };
70 
~DiffSummaryDiffSummary71     ~DiffSummary() {
72         for (int i = 0; i < DiffRecord::kResultCount; ++i) {
73             fResultsOfType[i].deleteAll();
74         }
75         for (int base = 0; base < DiffResource::kStatusCount; ++base) {
76             for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
77                 fStatusOfType[base][comparison].deleteAll();
78             }
79         }
80     }
81 
82     uint32_t fNumMatches;
83     uint32_t fNumMismatches;
84     uint32_t fMaxMismatchV;
85     float fMaxMismatchPercent;
86 
87     FileArray fResultsOfType[DiffRecord::kResultCount];
88     FileArray fStatusOfType[DiffResource::kStatusCount][DiffResource::kStatusCount];
89 
90     StringArray fFailedBaseNames[DiffRecord::kResultCount];
91 
printContentsDiffSummary92     void printContents(const FileArray& fileArray,
93                        const char* baseStatus, const char* comparisonStatus,
94                        bool listFilenames) {
95         int n = fileArray.count();
96         printf("%d file pairs %s in baseDir and %s in comparisonDir",
97                 n,            baseStatus,       comparisonStatus);
98         if (listFilenames) {
99             printf(": ");
100             for (int i = 0; i < n; ++i) {
101                 printf("%s ", fileArray[i]->c_str());
102             }
103         }
104         printf("\n");
105     }
106 
printStatusDiffSummary107     void printStatus(bool listFilenames,
108                      bool failOnStatusType[DiffResource::kStatusCount]
109                                           [DiffResource::kStatusCount]) {
110         typedef DiffResource::Status Status;
111 
112         for (int base = 0; base < DiffResource::kStatusCount; ++base) {
113             Status baseStatus = static_cast<Status>(base);
114             for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
115                 Status comparisonStatus = static_cast<Status>(comparison);
116                 const FileArray& fileArray = fStatusOfType[base][comparison];
117                 if (fileArray.count() > 0) {
118                     if (failOnStatusType[base][comparison]) {
119                         printf("   [*] ");
120                     } else {
121                         printf("   [_] ");
122                     }
123                     printContents(fileArray,
124                                   DiffResource::getStatusDescription(baseStatus),
125                                   DiffResource::getStatusDescription(comparisonStatus),
126                                   listFilenames);
127                 }
128             }
129         }
130     }
131 
132     // Print a line about the contents of this FileArray to stdout.
printContentsDiffSummary133     void printContents(const FileArray& fileArray, const char* headerText, bool listFilenames) {
134         int n = fileArray.count();
135         printf("%d file pairs %s", n, headerText);
136         if (listFilenames) {
137             printf(": ");
138             for (int i = 0; i < n; ++i) {
139                 printf("%s ", fileArray[i]->c_str());
140             }
141         }
142         printf("\n");
143     }
144 
printDiffSummary145     void print(bool listFilenames, bool failOnResultType[DiffRecord::kResultCount],
146                bool failOnStatusType[DiffResource::kStatusCount]
147                                     [DiffResource::kStatusCount]) {
148         printf("\ncompared %d file pairs:\n", fNumMatches + fNumMismatches);
149         for (int resultInt = 0; resultInt < DiffRecord::kResultCount; ++resultInt) {
150             DiffRecord::Result result = static_cast<DiffRecord::Result>(resultInt);
151             if (failOnResultType[result]) {
152                 printf("[*] ");
153             } else {
154                 printf("[_] ");
155             }
156             printContents(fResultsOfType[result], DiffRecord::getResultDescription(result),
157                           listFilenames);
158             if (DiffRecord::kCouldNotCompare_Result == result) {
159                 printStatus(listFilenames, failOnStatusType);
160             }
161         }
162         printf("(results marked with [*] will cause nonzero return value)\n");
163         printf("\nnumber of mismatching file pairs: %d\n", fNumMismatches);
164         if (fNumMismatches > 0) {
165             printf("Maximum pixel intensity mismatch %d\n", fMaxMismatchV);
166             printf("Largest area mismatch was %.2f%% of pixels\n",fMaxMismatchPercent);
167         }
168     }
169 
printfFailingBaseNamesDiffSummary170     void printfFailingBaseNames(const char separator[]) {
171         for (int resultInt = 0; resultInt < DiffRecord::kResultCount; ++resultInt) {
172             const StringArray& array = fFailedBaseNames[resultInt];
173             if (array.count()) {
174                 printf("%s [%d]%s", DiffRecord::ResultNames[resultInt], array.count(), separator);
175                 for (int j = 0; j < array.count(); ++j) {
176                     printf("%s%s", array[j]->c_str(), separator);
177                 }
178                 printf("\n");
179             }
180         }
181     }
182 
addDiffSummary183     void add (DiffRecord* drp) {
184         uint32_t mismatchValue;
185 
186         if (drp->fBase.fFilename.equals(drp->fComparison.fFilename)) {
187             fResultsOfType[drp->fResult].push(new SkString(drp->fBase.fFilename));
188         } else {
189             SkString* blame = new SkString("(");
190             blame->append(drp->fBase.fFilename);
191             blame->append(", ");
192             blame->append(drp->fComparison.fFilename);
193             blame->append(")");
194             fResultsOfType[drp->fResult].push(blame);
195         }
196         switch (drp->fResult) {
197           case DiffRecord::kEqualBits_Result:
198             fNumMatches++;
199             break;
200           case DiffRecord::kEqualPixels_Result:
201             fNumMatches++;
202             break;
203           case DiffRecord::kDifferentSizes_Result:
204             fNumMismatches++;
205             break;
206           case DiffRecord::kDifferentPixels_Result:
207             fNumMismatches++;
208             if (drp->fFractionDifference * 100 > fMaxMismatchPercent) {
209                 fMaxMismatchPercent = drp->fFractionDifference * 100;
210             }
211             mismatchValue = MAX3(drp->fMaxMismatchR, drp->fMaxMismatchG,
212                                  drp->fMaxMismatchB);
213             if (mismatchValue > fMaxMismatchV) {
214                 fMaxMismatchV = mismatchValue;
215             }
216             break;
217           case DiffRecord::kCouldNotCompare_Result:
218             fNumMismatches++;
219             fStatusOfType[drp->fBase.fStatus][drp->fComparison.fStatus].push(
220                     new SkString(drp->fBase.fFilename));
221             break;
222           case DiffRecord::kUnknown_Result:
223             SkDEBUGFAIL("adding uncategorized DiffRecord");
224             break;
225           default:
226             SkDEBUGFAIL("adding DiffRecord with unhandled fResult value");
227             break;
228         }
229 
230         switch (drp->fResult) {
231             case DiffRecord::kEqualBits_Result:
232             case DiffRecord::kEqualPixels_Result:
233                 break;
234             default:
235                 add_unique_basename(&fFailedBaseNames[drp->fResult], drp->fBase.fFilename);
236                 break;
237         }
238     }
239 };
240 
241 /// Returns true if string contains any of these substrings.
string_contains_any_of(const SkString & string,const StringArray & substrings)242 static bool string_contains_any_of(const SkString& string,
243                                    const StringArray& substrings) {
244     for (int i = 0; i < substrings.count(); i++) {
245         if (string.contains(substrings[i]->c_str())) {
246             return true;
247         }
248     }
249     return false;
250 }
251 
252 /// Internal (potentially recursive) implementation of get_file_list.
get_file_list_subdir(const SkString & rootDir,const SkString & subDir,const StringArray & matchSubstrings,const StringArray & nomatchSubstrings,bool recurseIntoSubdirs,FileArray * files)253 static void get_file_list_subdir(const SkString& rootDir, const SkString& subDir,
254                                  const StringArray& matchSubstrings,
255                                  const StringArray& nomatchSubstrings,
256                                  bool recurseIntoSubdirs, FileArray *files) {
257     bool isSubDirEmpty = subDir.isEmpty();
258     SkString dir(rootDir);
259     if (!isSubDirEmpty) {
260         dir.append(PATH_DIV_STR);
261         dir.append(subDir);
262     }
263 
264     // Iterate over files (not directories) within dir.
265     SkOSFile::Iter fileIterator(dir.c_str());
266     SkString fileName;
267     while (fileIterator.next(&fileName, false)) {
268         if (fileName.startsWith(".")) {
269             continue;
270         }
271         SkString pathRelativeToRootDir(subDir);
272         if (!isSubDirEmpty) {
273             pathRelativeToRootDir.append(PATH_DIV_STR);
274         }
275         pathRelativeToRootDir.append(fileName);
276         if (string_contains_any_of(pathRelativeToRootDir, matchSubstrings) &&
277             !string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) {
278             files->push(new SkString(pathRelativeToRootDir));
279         }
280     }
281 
282     // Recurse into any non-ignored subdirectories.
283     if (recurseIntoSubdirs) {
284         SkOSFile::Iter dirIterator(dir.c_str());
285         SkString dirName;
286         while (dirIterator.next(&dirName, true)) {
287             if (dirName.startsWith(".")) {
288                 continue;
289             }
290             SkString pathRelativeToRootDir(subDir);
291             if (!isSubDirEmpty) {
292                 pathRelativeToRootDir.append(PATH_DIV_STR);
293             }
294             pathRelativeToRootDir.append(dirName);
295             if (!string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) {
296                 get_file_list_subdir(rootDir, pathRelativeToRootDir,
297                                      matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
298                                      files);
299             }
300         }
301     }
302 }
303 
304 /// Iterate over dir and get all files whose filename:
305 ///  - matches any of the substrings in matchSubstrings, but...
306 ///  - DOES NOT match any of the substrings in nomatchSubstrings
307 ///  - DOES NOT start with a dot (.)
308 /// Adds the matching files to the list in *files.
get_file_list(const SkString & dir,const StringArray & matchSubstrings,const StringArray & nomatchSubstrings,bool recurseIntoSubdirs,FileArray * files)309 static void get_file_list(const SkString& dir,
310                           const StringArray& matchSubstrings,
311                           const StringArray& nomatchSubstrings,
312                           bool recurseIntoSubdirs, FileArray *files) {
313     get_file_list_subdir(dir, SkString(""),
314                          matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
315                          files);
316 }
317 
release_file_list(FileArray * files)318 static void release_file_list(FileArray *files) {
319     files->deleteAll();
320 }
321 
322 /// Comparison routines for qsort, sort by file names.
compare_file_name_metrics(SkString ** lhs,SkString ** rhs)323 static int compare_file_name_metrics(SkString **lhs, SkString **rhs) {
324     return strcmp((*lhs)->c_str(), (*rhs)->c_str());
325 }
326 
327 class AutoReleasePixels {
328 public:
AutoReleasePixels(DiffRecord * drp)329     AutoReleasePixels(DiffRecord* drp)
330     : fDrp(drp) {
331         SkASSERT(drp != NULL);
332     }
~AutoReleasePixels()333     ~AutoReleasePixels() {
334         fDrp->fBase.fBitmap.setPixelRef(NULL);
335         fDrp->fComparison.fBitmap.setPixelRef(NULL);
336         fDrp->fDifference.fBitmap.setPixelRef(NULL);
337         fDrp->fWhite.fBitmap.setPixelRef(NULL);
338     }
339 
340 private:
341     DiffRecord* fDrp;
342 };
343 
get_bounds(DiffResource & resource,const char * name)344 static void get_bounds(DiffResource& resource, const char* name) {
345     if (resource.fBitmap.empty() && !DiffResource::isStatusFailed(resource.fStatus)) {
346         SkAutoDataUnref fileBits(read_file(resource.fFullPath.c_str()));
347         if (NULL == fileBits) {
348             SkDebugf("WARNING: couldn't read %s file <%s>\n", name, resource.fFullPath.c_str());
349             resource.fStatus = DiffResource::kCouldNotRead_Status;
350         } else {
351             get_bitmap(fileBits, resource, SkImageDecoder::kDecodeBounds_Mode);
352         }
353     }
354 }
355 
get_bounds(DiffRecord & drp)356 static void get_bounds(DiffRecord& drp) {
357     get_bounds(drp.fBase, "base");
358     get_bounds(drp.fComparison, "comparison");
359 }
360 
361 #ifdef SK_OS_WIN
362 #define ANSI_COLOR_RED     ""
363 #define ANSI_COLOR_GREEN   ""
364 #define ANSI_COLOR_YELLOW  ""
365 #define ANSI_COLOR_RESET   ""
366 #else
367 #define ANSI_COLOR_RED     "\x1b[31m"
368 #define ANSI_COLOR_GREEN   "\x1b[32m"
369 #define ANSI_COLOR_YELLOW  "\x1b[33m"
370 #define ANSI_COLOR_RESET   "\x1b[0m"
371 #endif
372 
373 #define VERBOSE_STATUS(status,color,filename) if (verbose) printf( "[ " color " %10s " ANSI_COLOR_RESET " ] %s\n", status, filename->c_str())
374 
375 /// Creates difference images, returns the number that have a 0 metric.
376 /// If outputDir.isEmpty(), don't write out diff files.
create_diff_images(DiffMetricProc dmp,const int colorThreshold,RecordArray * differences,const SkString & baseDir,const SkString & comparisonDir,const SkString & outputDir,const StringArray & matchSubstrings,const StringArray & nomatchSubstrings,bool recurseIntoSubdirs,bool getBounds,bool verbose,DiffSummary * summary)377 static void create_diff_images (DiffMetricProc dmp,
378                                 const int colorThreshold,
379                                 RecordArray* differences,
380                                 const SkString& baseDir,
381                                 const SkString& comparisonDir,
382                                 const SkString& outputDir,
383                                 const StringArray& matchSubstrings,
384                                 const StringArray& nomatchSubstrings,
385                                 bool recurseIntoSubdirs,
386                                 bool getBounds,
387                                 bool verbose,
388                                 DiffSummary* summary) {
389     SkASSERT(!baseDir.isEmpty());
390     SkASSERT(!comparisonDir.isEmpty());
391 
392     FileArray baseFiles;
393     FileArray comparisonFiles;
394 
395     get_file_list(baseDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, &baseFiles);
396     get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
397                   &comparisonFiles);
398 
399     if (!baseFiles.isEmpty()) {
400         qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
401               SkCastForQSort(compare_file_name_metrics));
402     }
403     if (!comparisonFiles.isEmpty()) {
404         qsort(comparisonFiles.begin(), comparisonFiles.count(),
405               sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
406     }
407 
408     int i = 0;
409     int j = 0;
410 
411     while (i < baseFiles.count() &&
412            j < comparisonFiles.count()) {
413 
414         SkString basePath(baseDir);
415         SkString comparisonPath(comparisonDir);
416 
417         DiffRecord *drp = new DiffRecord;
418         int v = strcmp(baseFiles[i]->c_str(), comparisonFiles[j]->c_str());
419 
420         if (v < 0) {
421             // in baseDir, but not in comparisonDir
422             drp->fResult = DiffRecord::kCouldNotCompare_Result;
423 
424             basePath.append(*baseFiles[i]);
425             comparisonPath.append(*baseFiles[i]);
426 
427             drp->fBase.fFilename = *baseFiles[i];
428             drp->fBase.fFullPath = basePath;
429             drp->fBase.fStatus = DiffResource::kExists_Status;
430 
431             drp->fComparison.fFilename = *baseFiles[i];
432             drp->fComparison.fFullPath = comparisonPath;
433             drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
434 
435             VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, baseFiles[i]);
436 
437             ++i;
438         } else if (v > 0) {
439             // in comparisonDir, but not in baseDir
440             drp->fResult = DiffRecord::kCouldNotCompare_Result;
441 
442             basePath.append(*comparisonFiles[j]);
443             comparisonPath.append(*comparisonFiles[j]);
444 
445             drp->fBase.fFilename = *comparisonFiles[j];
446             drp->fBase.fFullPath = basePath;
447             drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
448 
449             drp->fComparison.fFilename = *comparisonFiles[j];
450             drp->fComparison.fFullPath = comparisonPath;
451             drp->fComparison.fStatus = DiffResource::kExists_Status;
452 
453             VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, comparisonFiles[j]);
454 
455             ++j;
456         } else {
457             // Found the same filename in both baseDir and comparisonDir.
458             SkASSERT(DiffRecord::kUnknown_Result == drp->fResult);
459 
460             basePath.append(*baseFiles[i]);
461             comparisonPath.append(*comparisonFiles[j]);
462 
463             drp->fBase.fFilename = *baseFiles[i];
464             drp->fBase.fFullPath = basePath;
465             drp->fBase.fStatus = DiffResource::kExists_Status;
466 
467             drp->fComparison.fFilename = *comparisonFiles[j];
468             drp->fComparison.fFullPath = comparisonPath;
469             drp->fComparison.fStatus = DiffResource::kExists_Status;
470 
471             SkAutoDataUnref baseFileBits(read_file(drp->fBase.fFullPath.c_str()));
472             if (baseFileBits) {
473                 drp->fBase.fStatus = DiffResource::kRead_Status;
474             }
475             SkAutoDataUnref comparisonFileBits(read_file(drp->fComparison.fFullPath.c_str()));
476             if (comparisonFileBits) {
477                 drp->fComparison.fStatus = DiffResource::kRead_Status;
478             }
479             if (NULL == baseFileBits || NULL == comparisonFileBits) {
480                 if (NULL == baseFileBits) {
481                     drp->fBase.fStatus = DiffResource::kCouldNotRead_Status;
482                     VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, baseFiles[i]);
483                 }
484                 if (NULL == comparisonFileBits) {
485                     drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status;
486                     VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, comparisonFiles[j]);
487                 }
488                 drp->fResult = DiffRecord::kCouldNotCompare_Result;
489 
490             } else if (are_buffers_equal(baseFileBits, comparisonFileBits)) {
491                 drp->fResult = DiffRecord::kEqualBits_Result;
492                 VERBOSE_STATUS("MATCH", ANSI_COLOR_GREEN, baseFiles[i]);
493             } else {
494                 AutoReleasePixels arp(drp);
495                 get_bitmap(baseFileBits, drp->fBase, SkImageDecoder::kDecodePixels_Mode);
496                 get_bitmap(comparisonFileBits, drp->fComparison,
497                            SkImageDecoder::kDecodePixels_Mode);
498                 VERBOSE_STATUS("DIFFERENT", ANSI_COLOR_RED, baseFiles[i]);
499                 if (DiffResource::kDecoded_Status == drp->fBase.fStatus &&
500                     DiffResource::kDecoded_Status == drp->fComparison.fStatus) {
501                     create_and_write_diff_image(drp, dmp, colorThreshold,
502                                                 outputDir, drp->fBase.fFilename);
503                 } else {
504                     drp->fResult = DiffRecord::kCouldNotCompare_Result;
505                 }
506             }
507 
508             ++i;
509             ++j;
510         }
511 
512         if (getBounds) {
513             get_bounds(*drp);
514         }
515         SkASSERT(DiffRecord::kUnknown_Result != drp->fResult);
516         differences->push(drp);
517         summary->add(drp);
518     }
519 
520     for (; i < baseFiles.count(); ++i) {
521         // files only in baseDir
522         DiffRecord *drp = new DiffRecord();
523         drp->fBase.fFilename = *baseFiles[i];
524         drp->fBase.fFullPath = baseDir;
525         drp->fBase.fFullPath.append(drp->fBase.fFilename);
526         drp->fBase.fStatus = DiffResource::kExists_Status;
527 
528         drp->fComparison.fFilename = *baseFiles[i];
529         drp->fComparison.fFullPath = comparisonDir;
530         drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
531         drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
532 
533         drp->fResult = DiffRecord::kCouldNotCompare_Result;
534         if (getBounds) {
535             get_bounds(*drp);
536         }
537         differences->push(drp);
538         summary->add(drp);
539     }
540 
541     for (; j < comparisonFiles.count(); ++j) {
542         // files only in comparisonDir
543         DiffRecord *drp = new DiffRecord();
544         drp->fBase.fFilename = *comparisonFiles[j];
545         drp->fBase.fFullPath = baseDir;
546         drp->fBase.fFullPath.append(drp->fBase.fFilename);
547         drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
548 
549         drp->fComparison.fFilename = *comparisonFiles[j];
550         drp->fComparison.fFullPath = comparisonDir;
551         drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
552         drp->fComparison.fStatus = DiffResource::kExists_Status;
553 
554         drp->fResult = DiffRecord::kCouldNotCompare_Result;
555         if (getBounds) {
556             get_bounds(*drp);
557         }
558         differences->push(drp);
559         summary->add(drp);
560     }
561 
562     release_file_list(&baseFiles);
563     release_file_list(&comparisonFiles);
564 }
565 
usage(char * argv0)566 static void usage (char * argv0) {
567     SkDebugf("Skia baseline image diff tool\n");
568     SkDebugf("\n"
569 "Usage: \n"
570 "    %s <baseDir> <comparisonDir> [outputDir] \n", argv0);
571     SkDebugf(
572 "\nArguments:"
573 "\n    --failonresult <result>: After comparing all file pairs, exit with nonzero"
574 "\n                             return code (number of file pairs yielding this"
575 "\n                             result) if any file pairs yielded this result."
576 "\n                             This flag may be repeated, in which case the"
577 "\n                             return code will be the number of fail pairs"
578 "\n                             yielding ANY of these results."
579 "\n    --failonstatus <baseStatus> <comparisonStatus>: exit with nonzero return"
580 "\n                             code if any file pairs yielded this status."
581 "\n    --help: display this info"
582 "\n    --listfilenames: list all filenames for each result type in stdout"
583 "\n    --match <substring>: compare files whose filenames contain this substring;"
584 "\n                         if unspecified, compare ALL files."
585 "\n                         this flag may be repeated."
586 "\n    --nodiffs: don't write out image diffs or index.html, just generate"
587 "\n               report on stdout"
588 "\n    --nomatch <substring>: regardless of --match, DO NOT compare files whose"
589 "\n                           filenames contain this substring."
590 "\n                           this flag may be repeated."
591 "\n    --noprintdirs: do not print the directories used."
592 "\n    --norecurse: do not recurse into subdirectories."
593 "\n    --sortbymaxmismatch: sort by worst color channel mismatch;"
594 "\n                         break ties with -sortbymismatch"
595 "\n    --sortbymismatch: sort by average color channel mismatch"
596 "\n    --threshold <n>: only report differences > n (per color channel) [default 0]"
597 "\n    --weighted: sort by # pixels different weighted by color difference"
598 "\n"
599 "\n    baseDir: directory to read baseline images from."
600 "\n    comparisonDir: directory to read comparison images from"
601 "\n    outputDir: directory to write difference images and index.html to;"
602 "\n               defaults to comparisonDir"
603 "\n"
604 "\nIf no sort is specified, it will sort by fraction of pixels mismatching."
605 "\n");
606 }
607 
608 const int kNoError = 0;
609 const int kGenericError = -1;
610 
611 int tool_main(int argc, char** argv);
tool_main(int argc,char ** argv)612 int tool_main(int argc, char** argv) {
613     DiffMetricProc diffProc = compute_diff_pmcolor;
614     int (*sortProc)(const void*, const void*) = compare<CompareDiffMetrics>;
615 
616     // Maximum error tolerated in any one color channel in any one pixel before
617     // a difference is reported.
618     int colorThreshold = 0;
619     SkString baseDir;
620     SkString comparisonDir;
621     SkString outputDir;
622 
623     StringArray matchSubstrings;
624     StringArray nomatchSubstrings;
625 
626     bool generateDiffs = true;
627     bool listFilenames = false;
628     bool printDirNames = true;
629     bool recurseIntoSubdirs = true;
630     bool verbose = false;
631     bool listFailingBase = false;
632 
633     RecordArray differences;
634     DiffSummary summary;
635 
636     bool failOnResultType[DiffRecord::kResultCount];
637     for (int i = 0; i < DiffRecord::kResultCount; i++) {
638         failOnResultType[i] = false;
639     }
640 
641     bool failOnStatusType[DiffResource::kStatusCount][DiffResource::kStatusCount];
642     for (int base = 0; base < DiffResource::kStatusCount; ++base) {
643         for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
644             failOnStatusType[base][comparison] = false;
645         }
646     }
647 
648     int i;
649     int numUnflaggedArguments = 0;
650     for (i = 1; i < argc; i++) {
651         if (!strcmp(argv[i], "--failonresult")) {
652             if (argc == ++i) {
653                 SkDebugf("failonresult expects one argument.\n");
654                 continue;
655             }
656             DiffRecord::Result type = DiffRecord::getResultByName(argv[i]);
657             if (type != DiffRecord::kResultCount) {
658                 failOnResultType[type] = true;
659             } else {
660                 SkDebugf("ignoring unrecognized result <%s>\n", argv[i]);
661             }
662             continue;
663         }
664         if (!strcmp(argv[i], "--failonstatus")) {
665             if (argc == ++i) {
666                 SkDebugf("failonstatus missing base status.\n");
667                 continue;
668             }
669             bool baseStatuses[DiffResource::kStatusCount];
670             if (!DiffResource::getMatchingStatuses(argv[i], baseStatuses)) {
671                 SkDebugf("unrecognized base status <%s>\n", argv[i]);
672             }
673 
674             if (argc == ++i) {
675                 SkDebugf("failonstatus missing comparison status.\n");
676                 continue;
677             }
678             bool comparisonStatuses[DiffResource::kStatusCount];
679             if (!DiffResource::getMatchingStatuses(argv[i], comparisonStatuses)) {
680                 SkDebugf("unrecognized comarison status <%s>\n", argv[i]);
681             }
682 
683             for (int base = 0; base < DiffResource::kStatusCount; ++base) {
684                 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
685                     failOnStatusType[base][comparison] |=
686                         baseStatuses[base] && comparisonStatuses[comparison];
687                 }
688             }
689             continue;
690         }
691         if (!strcmp(argv[i], "--help")) {
692             usage(argv[0]);
693             return kNoError;
694         }
695         if (!strcmp(argv[i], "--listfilenames")) {
696             listFilenames = true;
697             continue;
698         }
699         if (!strcmp(argv[i], "--verbose")) {
700             verbose = true;
701             continue;
702         }
703         if (!strcmp(argv[i], "--match")) {
704             matchSubstrings.push(new SkString(argv[++i]));
705             continue;
706         }
707         if (!strcmp(argv[i], "--nodiffs")) {
708             generateDiffs = false;
709             continue;
710         }
711         if (!strcmp(argv[i], "--nomatch")) {
712             nomatchSubstrings.push(new SkString(argv[++i]));
713             continue;
714         }
715         if (!strcmp(argv[i], "--noprintdirs")) {
716             printDirNames = false;
717             continue;
718         }
719         if (!strcmp(argv[i], "--norecurse")) {
720             recurseIntoSubdirs = false;
721             continue;
722         }
723         if (!strcmp(argv[i], "--sortbymaxmismatch")) {
724             sortProc = compare<CompareDiffMaxMismatches>;
725             continue;
726         }
727         if (!strcmp(argv[i], "--sortbymismatch")) {
728             sortProc = compare<CompareDiffMeanMismatches>;
729             continue;
730         }
731         if (!strcmp(argv[i], "--threshold")) {
732             colorThreshold = atoi(argv[++i]);
733             continue;
734         }
735         if (!strcmp(argv[i], "--weighted")) {
736             sortProc = compare<CompareDiffWeighted>;
737             continue;
738         }
739         if (argv[i][0] != '-') {
740             switch (numUnflaggedArguments++) {
741                 case 0:
742                     baseDir.set(argv[i]);
743                     continue;
744                 case 1:
745                     comparisonDir.set(argv[i]);
746                     continue;
747                 case 2:
748                     outputDir.set(argv[i]);
749                     continue;
750                 default:
751                     SkDebugf("extra unflagged argument <%s>\n", argv[i]);
752                     usage(argv[0]);
753                     return kGenericError;
754             }
755         }
756         if (!strcmp(argv[i], "--listFailingBase")) {
757             listFailingBase = true;
758             continue;
759         }
760 
761         SkDebugf("Unrecognized argument <%s>\n", argv[i]);
762         usage(argv[0]);
763         return kGenericError;
764     }
765 
766     if (numUnflaggedArguments == 2) {
767         outputDir = comparisonDir;
768     } else if (numUnflaggedArguments != 3) {
769         usage(argv[0]);
770         return kGenericError;
771     }
772 
773     if (!baseDir.endsWith(PATH_DIV_STR)) {
774         baseDir.append(PATH_DIV_STR);
775     }
776     if (printDirNames) {
777         printf("baseDir is [%s]\n", baseDir.c_str());
778     }
779 
780     if (!comparisonDir.endsWith(PATH_DIV_STR)) {
781         comparisonDir.append(PATH_DIV_STR);
782     }
783     if (printDirNames) {
784         printf("comparisonDir is [%s]\n", comparisonDir.c_str());
785     }
786 
787     if (!outputDir.endsWith(PATH_DIV_STR)) {
788         outputDir.append(PATH_DIV_STR);
789     }
790     if (generateDiffs) {
791         if (printDirNames) {
792             printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
793         }
794     } else {
795         if (printDirNames) {
796             printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
797         }
798         outputDir.set("");
799     }
800 
801     // If no matchSubstrings were specified, match ALL strings
802     // (except for whatever nomatchSubstrings were specified, if any).
803     if (matchSubstrings.isEmpty()) {
804         matchSubstrings.push(new SkString(""));
805     }
806 
807     create_diff_images(diffProc, colorThreshold, &differences,
808                        baseDir, comparisonDir, outputDir,
809                        matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, generateDiffs,
810                        verbose, &summary);
811     summary.print(listFilenames, failOnResultType, failOnStatusType);
812 
813     if (listFailingBase) {
814         summary.printfFailingBaseNames("\n");
815     }
816 
817     if (differences.count()) {
818         qsort(differences.begin(), differences.count(),
819               sizeof(DiffRecord*), sortProc);
820     }
821 
822     if (generateDiffs) {
823         print_diff_page(summary.fNumMatches, colorThreshold, differences,
824                         baseDir, comparisonDir, outputDir);
825     }
826 
827     for (i = 0; i < differences.count(); i++) {
828         delete differences[i];
829     }
830     matchSubstrings.deleteAll();
831     nomatchSubstrings.deleteAll();
832 
833     int num_failing_results = 0;
834     for (int i = 0; i < DiffRecord::kResultCount; i++) {
835         if (failOnResultType[i]) {
836             num_failing_results += summary.fResultsOfType[i].count();
837         }
838     }
839     if (!failOnResultType[DiffRecord::kCouldNotCompare_Result]) {
840         for (int base = 0; base < DiffResource::kStatusCount; ++base) {
841             for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
842                 if (failOnStatusType[base][comparison]) {
843                     num_failing_results += summary.fStatusOfType[base][comparison].count();
844                 }
845             }
846         }
847     }
848 
849     // On Linux (and maybe other platforms too), any results outside of the
850     // range [0...255] are wrapped (mod 256).  Do the conversion ourselves, to
851     // make sure that we only return 0 when there were no failures.
852     return (num_failing_results > 255) ? 255 : num_failing_results;
853 }
854 
855 #if !defined SK_BUILD_FOR_IOS
main(int argc,char * const argv[])856 int main(int argc, char * const argv[]) {
857     return tool_main(argc, (char**) argv);
858 }
859 #endif
860