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