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 #ifdef SK_OS_WIN
    315 #define ANSI_COLOR_RED     ""
    316 #define ANSI_COLOR_GREEN   ""
    317 #define ANSI_COLOR_YELLOW  ""
    318 #define ANSI_COLOR_RESET   ""
    319 #else
    320 #define ANSI_COLOR_RED     "\x1b[31m"
    321 #define ANSI_COLOR_GREEN   "\x1b[32m"
    322 #define ANSI_COLOR_YELLOW  "\x1b[33m"
    323 #define ANSI_COLOR_RESET   "\x1b[0m"
    324 #endif
    325 
    326 #define VERBOSE_STATUS(status,color,filename) if (verbose) printf( "[ " color " %10s " ANSI_COLOR_RESET " ] %s\n", status, filename->c_str())
    327 
    328 /// Creates difference images, returns the number that have a 0 metric.
    329 /// If outputDir.isEmpty(), don't write out diff files.
    330 static void create_diff_images (DiffMetricProc dmp,
    331                                 const int colorThreshold,
    332                                 RecordArray* differences,
    333                                 const SkString& baseDir,
    334                                 const SkString& comparisonDir,
    335                                 const SkString& outputDir,
    336                                 const StringArray& matchSubstrings,
    337                                 const StringArray& nomatchSubstrings,
    338                                 bool recurseIntoSubdirs,
    339                                 bool getBounds,
    340                                 bool verbose,
    341                                 DiffSummary* summary) {
    342     SkASSERT(!baseDir.isEmpty());
    343     SkASSERT(!comparisonDir.isEmpty());
    344 
    345     FileArray baseFiles;
    346     FileArray comparisonFiles;
    347 
    348     get_file_list(baseDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, &baseFiles);
    349     get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
    350                   &comparisonFiles);
    351 
    352     if (!baseFiles.isEmpty()) {
    353         qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
    354               SkCastForQSort(compare_file_name_metrics));
    355     }
    356     if (!comparisonFiles.isEmpty()) {
    357         qsort(comparisonFiles.begin(), comparisonFiles.count(),
    358               sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
    359     }
    360 
    361     int i = 0;
    362     int j = 0;
    363 
    364     while (i < baseFiles.count() &&
    365            j < comparisonFiles.count()) {
    366 
    367         SkString basePath(baseDir);
    368         SkString comparisonPath(comparisonDir);
    369 
    370         DiffRecord *drp = new DiffRecord;
    371         int v = strcmp(baseFiles[i]->c_str(), comparisonFiles[j]->c_str());
    372 
    373         if (v < 0) {
    374             // in baseDir, but not in comparisonDir
    375             drp->fResult = DiffRecord::kCouldNotCompare_Result;
    376 
    377             basePath.append(*baseFiles[i]);
    378             comparisonPath.append(*baseFiles[i]);
    379 
    380             drp->fBase.fFilename = *baseFiles[i];
    381             drp->fBase.fFullPath = basePath;
    382             drp->fBase.fStatus = DiffResource::kExists_Status;
    383 
    384             drp->fComparison.fFilename = *baseFiles[i];
    385             drp->fComparison.fFullPath = comparisonPath;
    386             drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
    387 
    388             VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, baseFiles[i]);
    389 
    390             ++i;
    391         } else if (v > 0) {
    392             // in comparisonDir, but not in baseDir
    393             drp->fResult = DiffRecord::kCouldNotCompare_Result;
    394 
    395             basePath.append(*comparisonFiles[j]);
    396             comparisonPath.append(*comparisonFiles[j]);
    397 
    398             drp->fBase.fFilename = *comparisonFiles[j];
    399             drp->fBase.fFullPath = basePath;
    400             drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
    401 
    402             drp->fComparison.fFilename = *comparisonFiles[j];
    403             drp->fComparison.fFullPath = comparisonPath;
    404             drp->fComparison.fStatus = DiffResource::kExists_Status;
    405 
    406             VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, comparisonFiles[j]);
    407 
    408             ++j;
    409         } else {
    410             // Found the same filename in both baseDir and comparisonDir.
    411             SkASSERT(DiffRecord::kUnknown_Result == drp->fResult);
    412 
    413             basePath.append(*baseFiles[i]);
    414             comparisonPath.append(*comparisonFiles[j]);
    415 
    416             drp->fBase.fFilename = *baseFiles[i];
    417             drp->fBase.fFullPath = basePath;
    418             drp->fBase.fStatus = DiffResource::kExists_Status;
    419 
    420             drp->fComparison.fFilename = *comparisonFiles[j];
    421             drp->fComparison.fFullPath = comparisonPath;
    422             drp->fComparison.fStatus = DiffResource::kExists_Status;
    423 
    424             SkAutoDataUnref baseFileBits(read_file(drp->fBase.fFullPath.c_str()));
    425             if (NULL != baseFileBits) {
    426                 drp->fBase.fStatus = DiffResource::kRead_Status;
    427             }
    428             SkAutoDataUnref comparisonFileBits(read_file(drp->fComparison.fFullPath.c_str()));
    429             if (NULL != comparisonFileBits) {
    430                 drp->fComparison.fStatus = DiffResource::kRead_Status;
    431             }
    432             if (NULL == baseFileBits || NULL == comparisonFileBits) {
    433                 if (NULL == baseFileBits) {
    434                     drp->fBase.fStatus = DiffResource::kCouldNotRead_Status;
    435                     VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, baseFiles[i]);
    436                 }
    437                 if (NULL == comparisonFileBits) {
    438                     drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status;
    439                     VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, comparisonFiles[j]);
    440                 }
    441                 drp->fResult = DiffRecord::kCouldNotCompare_Result;
    442 
    443             } else if (are_buffers_equal(baseFileBits, comparisonFileBits)) {
    444                 drp->fResult = DiffRecord::kEqualBits_Result;
    445                 VERBOSE_STATUS("MATCH", ANSI_COLOR_GREEN, baseFiles[i]);
    446             } else {
    447                 AutoReleasePixels arp(drp);
    448                 get_bitmap(baseFileBits, drp->fBase, SkImageDecoder::kDecodePixels_Mode);
    449                 get_bitmap(comparisonFileBits, drp->fComparison,
    450                            SkImageDecoder::kDecodePixels_Mode);
    451                 VERBOSE_STATUS("DIFFERENT", ANSI_COLOR_RED, baseFiles[i]);
    452                 if (DiffResource::kDecoded_Status == drp->fBase.fStatus &&
    453                     DiffResource::kDecoded_Status == drp->fComparison.fStatus) {
    454                     create_and_write_diff_image(drp, dmp, colorThreshold,
    455                                                 outputDir, drp->fBase.fFilename);
    456                 } else {
    457                     drp->fResult = DiffRecord::kCouldNotCompare_Result;
    458                 }
    459             }
    460 
    461             ++i;
    462             ++j;
    463         }
    464 
    465         if (getBounds) {
    466             get_bounds(*drp);
    467         }
    468         SkASSERT(DiffRecord::kUnknown_Result != drp->fResult);
    469         differences->push(drp);
    470         summary->add(drp);
    471     }
    472 
    473     for (; i < baseFiles.count(); ++i) {
    474         // files only in baseDir
    475         DiffRecord *drp = new DiffRecord();
    476         drp->fBase.fFilename = *baseFiles[i];
    477         drp->fBase.fFullPath = baseDir;
    478         drp->fBase.fFullPath.append(drp->fBase.fFilename);
    479         drp->fBase.fStatus = DiffResource::kExists_Status;
    480 
    481         drp->fComparison.fFilename = *baseFiles[i];
    482         drp->fComparison.fFullPath = comparisonDir;
    483         drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
    484         drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
    485 
    486         drp->fResult = DiffRecord::kCouldNotCompare_Result;
    487         if (getBounds) {
    488             get_bounds(*drp);
    489         }
    490         differences->push(drp);
    491         summary->add(drp);
    492     }
    493 
    494     for (; j < comparisonFiles.count(); ++j) {
    495         // files only in comparisonDir
    496         DiffRecord *drp = new DiffRecord();
    497         drp->fBase.fFilename = *comparisonFiles[j];
    498         drp->fBase.fFullPath = baseDir;
    499         drp->fBase.fFullPath.append(drp->fBase.fFilename);
    500         drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
    501 
    502         drp->fComparison.fFilename = *comparisonFiles[j];
    503         drp->fComparison.fFullPath = comparisonDir;
    504         drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
    505         drp->fComparison.fStatus = DiffResource::kExists_Status;
    506 
    507         drp->fResult = DiffRecord::kCouldNotCompare_Result;
    508         if (getBounds) {
    509             get_bounds(*drp);
    510         }
    511         differences->push(drp);
    512         summary->add(drp);
    513     }
    514 
    515     release_file_list(&baseFiles);
    516     release_file_list(&comparisonFiles);
    517 }
    518 
    519 static void usage (char * argv0) {
    520     SkDebugf("Skia baseline image diff tool\n");
    521     SkDebugf("\n"
    522 "Usage: \n"
    523 "    %s <baseDir> <comparisonDir> [outputDir] \n", argv0);
    524     SkDebugf(
    525 "\nArguments:"
    526 "\n    --failonresult <result>: After comparing all file pairs, exit with nonzero"
    527 "\n                             return code (number of file pairs yielding this"
    528 "\n                             result) if any file pairs yielded this result."
    529 "\n                             This flag may be repeated, in which case the"
    530 "\n                             return code will be the number of fail pairs"
    531 "\n                             yielding ANY of these results."
    532 "\n    --failonstatus <baseStatus> <comparisonStatus>: exit with nonzero return"
    533 "\n                             code if any file pairs yielded this status."
    534 "\n    --help: display this info"
    535 "\n    --listfilenames: list all filenames for each result type in stdout"
    536 "\n    --match <substring>: compare files whose filenames contain this substring;"
    537 "\n                         if unspecified, compare ALL files."
    538 "\n                         this flag may be repeated."
    539 "\n    --nodiffs: don't write out image diffs or index.html, just generate"
    540 "\n               report on stdout"
    541 "\n    --nomatch <substring>: regardless of --match, DO NOT compare files whose"
    542 "\n                           filenames contain this substring."
    543 "\n                           this flag may be repeated."
    544 "\n    --noprintdirs: do not print the directories used."
    545 "\n    --norecurse: do not recurse into subdirectories."
    546 "\n    --sortbymaxmismatch: sort by worst color channel mismatch;"
    547 "\n                         break ties with -sortbymismatch"
    548 "\n    --sortbymismatch: sort by average color channel mismatch"
    549 "\n    --threshold <n>: only report differences > n (per color channel) [default 0]"
    550 "\n    --weighted: sort by # pixels different weighted by color difference"
    551 "\n"
    552 "\n    baseDir: directory to read baseline images from."
    553 "\n    comparisonDir: directory to read comparison images from"
    554 "\n    outputDir: directory to write difference images and index.html to;"
    555 "\n               defaults to comparisonDir"
    556 "\n"
    557 "\nIf no sort is specified, it will sort by fraction of pixels mismatching."
    558 "\n");
    559 }
    560 
    561 const int kNoError = 0;
    562 const int kGenericError = -1;
    563 
    564 int tool_main(int argc, char** argv);
    565 int tool_main(int argc, char** argv) {
    566     DiffMetricProc diffProc = compute_diff_pmcolor;
    567     int (*sortProc)(const void*, const void*) = compare<CompareDiffMetrics>;
    568 
    569     // Maximum error tolerated in any one color channel in any one pixel before
    570     // a difference is reported.
    571     int colorThreshold = 0;
    572     SkString baseDir;
    573     SkString comparisonDir;
    574     SkString outputDir;
    575 
    576     StringArray matchSubstrings;
    577     StringArray nomatchSubstrings;
    578 
    579     bool generateDiffs = true;
    580     bool listFilenames = false;
    581     bool printDirNames = true;
    582     bool recurseIntoSubdirs = true;
    583     bool verbose = false;
    584 
    585     RecordArray differences;
    586     DiffSummary summary;
    587 
    588     bool failOnResultType[DiffRecord::kResultCount];
    589     for (int i = 0; i < DiffRecord::kResultCount; i++) {
    590         failOnResultType[i] = false;
    591     }
    592 
    593     bool failOnStatusType[DiffResource::kStatusCount][DiffResource::kStatusCount];
    594     for (int base = 0; base < DiffResource::kStatusCount; ++base) {
    595         for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
    596             failOnStatusType[base][comparison] = false;
    597         }
    598     }
    599 
    600     int i;
    601     int numUnflaggedArguments = 0;
    602     for (i = 1; i < argc; i++) {
    603         if (!strcmp(argv[i], "--failonresult")) {
    604             if (argc == ++i) {
    605                 SkDebugf("failonresult expects one argument.\n");
    606                 continue;
    607             }
    608             DiffRecord::Result type = DiffRecord::getResultByName(argv[i]);
    609             if (type != DiffRecord::kResultCount) {
    610                 failOnResultType[type] = true;
    611             } else {
    612                 SkDebugf("ignoring unrecognized result <%s>\n", argv[i]);
    613             }
    614             continue;
    615         }
    616         if (!strcmp(argv[i], "--failonstatus")) {
    617             if (argc == ++i) {
    618                 SkDebugf("failonstatus missing base status.\n");
    619                 continue;
    620             }
    621             bool baseStatuses[DiffResource::kStatusCount];
    622             if (!DiffResource::getMatchingStatuses(argv[i], baseStatuses)) {
    623                 SkDebugf("unrecognized base status <%s>\n", argv[i]);
    624             }
    625 
    626             if (argc == ++i) {
    627                 SkDebugf("failonstatus missing comparison status.\n");
    628                 continue;
    629             }
    630             bool comparisonStatuses[DiffResource::kStatusCount];
    631             if (!DiffResource::getMatchingStatuses(argv[i], comparisonStatuses)) {
    632                 SkDebugf("unrecognized comarison status <%s>\n", argv[i]);
    633             }
    634 
    635             for (int base = 0; base < DiffResource::kStatusCount; ++base) {
    636                 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
    637                     failOnStatusType[base][comparison] |=
    638                         baseStatuses[base] && comparisonStatuses[comparison];
    639                 }
    640             }
    641             continue;
    642         }
    643         if (!strcmp(argv[i], "--help")) {
    644             usage(argv[0]);
    645             return kNoError;
    646         }
    647         if (!strcmp(argv[i], "--listfilenames")) {
    648             listFilenames = true;
    649             continue;
    650         }
    651         if (!strcmp(argv[i], "--verbose")) {
    652             verbose = true;
    653             continue;
    654         }
    655         if (!strcmp(argv[i], "--match")) {
    656             matchSubstrings.push(new SkString(argv[++i]));
    657             continue;
    658         }
    659         if (!strcmp(argv[i], "--nodiffs")) {
    660             generateDiffs = false;
    661             continue;
    662         }
    663         if (!strcmp(argv[i], "--nomatch")) {
    664             nomatchSubstrings.push(new SkString(argv[++i]));
    665             continue;
    666         }
    667         if (!strcmp(argv[i], "--noprintdirs")) {
    668             printDirNames = false;
    669             continue;
    670         }
    671         if (!strcmp(argv[i], "--norecurse")) {
    672             recurseIntoSubdirs = false;
    673             continue;
    674         }
    675         if (!strcmp(argv[i], "--sortbymaxmismatch")) {
    676             sortProc = compare<CompareDiffMaxMismatches>;
    677             continue;
    678         }
    679         if (!strcmp(argv[i], "--sortbymismatch")) {
    680             sortProc = compare<CompareDiffMeanMismatches>;
    681             continue;
    682         }
    683         if (!strcmp(argv[i], "--threshold")) {
    684             colorThreshold = atoi(argv[++i]);
    685             continue;
    686         }
    687         if (!strcmp(argv[i], "--weighted")) {
    688             sortProc = compare<CompareDiffWeighted>;
    689             continue;
    690         }
    691         if (argv[i][0] != '-') {
    692             switch (numUnflaggedArguments++) {
    693                 case 0:
    694                     baseDir.set(argv[i]);
    695                     continue;
    696                 case 1:
    697                     comparisonDir.set(argv[i]);
    698                     continue;
    699                 case 2:
    700                     outputDir.set(argv[i]);
    701                     continue;
    702                 default:
    703                     SkDebugf("extra unflagged argument <%s>\n", argv[i]);
    704                     usage(argv[0]);
    705                     return kGenericError;
    706             }
    707         }
    708 
    709         SkDebugf("Unrecognized argument <%s>\n", argv[i]);
    710         usage(argv[0]);
    711         return kGenericError;
    712     }
    713 
    714     if (numUnflaggedArguments == 2) {
    715         outputDir = comparisonDir;
    716     } else if (numUnflaggedArguments != 3) {
    717         usage(argv[0]);
    718         return kGenericError;
    719     }
    720 
    721     if (!baseDir.endsWith(PATH_DIV_STR)) {
    722         baseDir.append(PATH_DIV_STR);
    723     }
    724     if (printDirNames) {
    725         printf("baseDir is [%s]\n", baseDir.c_str());
    726     }
    727 
    728     if (!comparisonDir.endsWith(PATH_DIV_STR)) {
    729         comparisonDir.append(PATH_DIV_STR);
    730     }
    731     if (printDirNames) {
    732         printf("comparisonDir is [%s]\n", comparisonDir.c_str());
    733     }
    734 
    735     if (!outputDir.endsWith(PATH_DIV_STR)) {
    736         outputDir.append(PATH_DIV_STR);
    737     }
    738     if (generateDiffs) {
    739         if (printDirNames) {
    740             printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
    741         }
    742     } else {
    743         if (printDirNames) {
    744             printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
    745         }
    746         outputDir.set("");
    747     }
    748 
    749     // If no matchSubstrings were specified, match ALL strings
    750     // (except for whatever nomatchSubstrings were specified, if any).
    751     if (matchSubstrings.isEmpty()) {
    752         matchSubstrings.push(new SkString(""));
    753     }
    754 
    755     create_diff_images(diffProc, colorThreshold, &differences,
    756                        baseDir, comparisonDir, outputDir,
    757                        matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, generateDiffs,
    758                        verbose, &summary);
    759     summary.print(listFilenames, failOnResultType, failOnStatusType);
    760 
    761     if (differences.count()) {
    762         qsort(differences.begin(), differences.count(),
    763               sizeof(DiffRecord*), sortProc);
    764     }
    765 
    766     if (generateDiffs) {
    767         print_diff_page(summary.fNumMatches, colorThreshold, differences,
    768                         baseDir, comparisonDir, outputDir);
    769     }
    770 
    771     for (i = 0; i < differences.count(); i++) {
    772         delete differences[i];
    773     }
    774     matchSubstrings.deleteAll();
    775     nomatchSubstrings.deleteAll();
    776 
    777     int num_failing_results = 0;
    778     for (int i = 0; i < DiffRecord::kResultCount; i++) {
    779         if (failOnResultType[i]) {
    780             num_failing_results += summary.fResultsOfType[i].count();
    781         }
    782     }
    783     if (!failOnResultType[DiffRecord::kCouldNotCompare_Result]) {
    784         for (int base = 0; base < DiffResource::kStatusCount; ++base) {
    785             for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
    786                 if (failOnStatusType[base][comparison]) {
    787                     num_failing_results += summary.fStatusOfType[base][comparison].count();
    788                 }
    789             }
    790         }
    791     }
    792 
    793     // On Linux (and maybe other platforms too), any results outside of the
    794     // range [0...255] are wrapped (mod 256).  Do the conversion ourselves, to
    795     // make sure that we only return 0 when there were no failures.
    796     return (num_failing_results > 255) ? 255 : num_failing_results;
    797 }
    798 
    799 #if !defined SK_BUILD_FOR_IOS
    800 int main(int argc, char * const argv[]) {
    801     return tool_main(argc, (char**) argv);
    802 }
    803 #endif
    804