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