Home | History | Annotate | Download | only in skdiff
      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 "SkImageEncoder.h"
     13 #include "SkOSFile.h"
     14 #include "SkOSPath.h"
     15 #include "SkStream.h"
     16 #include "SkPixelRef.h"
     17 #include "../private/SkTDArray.h"
     18 #include "../private/SkTSearch.h"
     19 
     20 #include <stdlib.h>
     21 
     22 /**
     23  * skdiff
     24  *
     25  * Given three directory names, expects to find identically-named files in
     26  * each of the first two; the first are treated as a set of baseline,
     27  * the second a set of variant images, and a diff image is written into the
     28  * third directory for each pair.
     29  * Creates an index.html in the current third directory to compare each
     30  * pair that does not match exactly.
     31  * Recursively descends directories, unless run with --norecurse.
     32  *
     33  * Returns zero exit code if all images match across baseDir and comparisonDir.
     34  */
     35 
     36 typedef SkTDArray<SkString*> StringArray;
     37 typedef StringArray FileArray;
     38 
     39 static void add_unique_basename(StringArray* array, const SkString& filename) {
     40     // trim off dirs
     41     const char* src = filename.c_str();
     42     const char* trimmed = strrchr(src, SkOSPath::SEPARATOR);
     43     if (trimmed) {
     44         trimmed += 1;   // skip the separator
     45     } else {
     46         trimmed = src;
     47     }
     48     const char* end = strrchr(trimmed, '.');
     49     if (!end) {
     50         end = trimmed + strlen(trimmed);
     51     }
     52     SkString result(trimmed, end - trimmed);
     53 
     54     // only add unique entries
     55     for (int i = 0; i < array->count(); ++i) {
     56         if (*array->getAt(i) == result) {
     57             return;
     58         }
     59     }
     60     *array->append() = new SkString(result);
     61 }
     62 
     63 struct DiffSummary {
     64     DiffSummary ()
     65         : fNumMatches(0)
     66         , fNumMismatches(0)
     67         , fMaxMismatchV(0)
     68         , fMaxMismatchPercent(0) { }
     69 
     70     ~DiffSummary() {
     71         for (int i = 0; i < DiffRecord::kResultCount; ++i) {
     72             fResultsOfType[i].deleteAll();
     73         }
     74         for (int base = 0; base < DiffResource::kStatusCount; ++base) {
     75             for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
     76                 fStatusOfType[base][comparison].deleteAll();
     77             }
     78         }
     79     }
     80 
     81     uint32_t fNumMatches;
     82     uint32_t fNumMismatches;
     83     uint32_t fMaxMismatchV;
     84     float fMaxMismatchPercent;
     85 
     86     FileArray fResultsOfType[DiffRecord::kResultCount];
     87     FileArray fStatusOfType[DiffResource::kStatusCount][DiffResource::kStatusCount];
     88 
     89     StringArray fFailedBaseNames[DiffRecord::kResultCount];
     90 
     91     void printContents(const FileArray& fileArray,
     92                        const char* baseStatus, const char* comparisonStatus,
     93                        bool listFilenames) {
     94         int n = fileArray.count();
     95         printf("%d file pairs %s in baseDir and %s in comparisonDir",
     96                 n,            baseStatus,       comparisonStatus);
     97         if (listFilenames) {
     98             printf(": ");
     99             for (int i = 0; i < n; ++i) {
    100                 printf("%s ", fileArray[i]->c_str());
    101             }
    102         }
    103         printf("\n");
    104     }
    105 
    106     void printStatus(bool listFilenames,
    107                      bool failOnStatusType[DiffResource::kStatusCount]
    108                                           [DiffResource::kStatusCount]) {
    109         typedef DiffResource::Status Status;
    110 
    111         for (int base = 0; base < DiffResource::kStatusCount; ++base) {
    112             Status baseStatus = static_cast<Status>(base);
    113             for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
    114                 Status comparisonStatus = static_cast<Status>(comparison);
    115                 const FileArray& fileArray = fStatusOfType[base][comparison];
    116                 if (fileArray.count() > 0) {
    117                     if (failOnStatusType[base][comparison]) {
    118                         printf("   [*] ");
    119                     } else {
    120                         printf("   [_] ");
    121                     }
    122                     printContents(fileArray,
    123                                   DiffResource::getStatusDescription(baseStatus),
    124                                   DiffResource::getStatusDescription(comparisonStatus),
    125                                   listFilenames);
    126                 }
    127             }
    128         }
    129     }
    130 
    131     // Print a line about the contents of this FileArray to stdout.
    132     void printContents(const FileArray& fileArray, const char* headerText, bool listFilenames) {
    133         int n = fileArray.count();
    134         printf("%d file pairs %s", n, headerText);
    135         if (listFilenames) {
    136             printf(": ");
    137             for (int i = 0; i < n; ++i) {
    138                 printf("%s ", fileArray[i]->c_str());
    139             }
    140         }
    141         printf("\n");
    142     }
    143 
    144     void print(bool listFilenames, bool failOnResultType[DiffRecord::kResultCount],
    145                bool failOnStatusType[DiffResource::kStatusCount]
    146                                     [DiffResource::kStatusCount]) {
    147         printf("\ncompared %d file pairs:\n", fNumMatches + fNumMismatches);
    148         for (int resultInt = 0; resultInt < DiffRecord::kResultCount; ++resultInt) {
    149             DiffRecord::Result result = static_cast<DiffRecord::Result>(resultInt);
    150             if (failOnResultType[result]) {
    151                 printf("[*] ");
    152             } else {
    153                 printf("[_] ");
    154             }
    155             printContents(fResultsOfType[result], DiffRecord::getResultDescription(result),
    156                           listFilenames);
    157             if (DiffRecord::kCouldNotCompare_Result == result) {
    158                 printStatus(listFilenames, failOnStatusType);
    159             }
    160         }
    161         printf("(results marked with [*] will cause nonzero return value)\n");
    162         printf("\nnumber of mismatching file pairs: %d\n", fNumMismatches);
    163         if (fNumMismatches > 0) {
    164             printf("Maximum pixel intensity mismatch %d\n", fMaxMismatchV);
    165             printf("Largest area mismatch was %.2f%% of pixels\n",fMaxMismatchPercent);
    166         }
    167     }
    168 
    169     void printfFailingBaseNames(const char separator[]) {
    170         for (int resultInt = 0; resultInt < DiffRecord::kResultCount; ++resultInt) {
    171             const StringArray& array = fFailedBaseNames[resultInt];
    172             if (array.count()) {
    173                 printf("%s [%d]%s", DiffRecord::ResultNames[resultInt], array.count(), separator);
    174                 for (int j = 0; j < array.count(); ++j) {
    175                     printf("%s%s", array[j]->c_str(), separator);
    176                 }
    177                 printf("\n");
    178             }
    179         }
    180     }
    181 
    182     void add (DiffRecord* drp) {
    183         uint32_t mismatchValue;
    184 
    185         if (drp->fBase.fFilename.equals(drp->fComparison.fFilename)) {
    186             fResultsOfType[drp->fResult].push(new SkString(drp->fBase.fFilename));
    187         } else {
    188             SkString* blame = new SkString("(");
    189             blame->append(drp->fBase.fFilename);
    190             blame->append(", ");
    191             blame->append(drp->fComparison.fFilename);
    192             blame->append(")");
    193             fResultsOfType[drp->fResult].push(blame);
    194         }
    195         switch (drp->fResult) {
    196           case DiffRecord::kEqualBits_Result:
    197             fNumMatches++;
    198             break;
    199           case DiffRecord::kEqualPixels_Result:
    200             fNumMatches++;
    201             break;
    202           case DiffRecord::kDifferentSizes_Result:
    203             fNumMismatches++;
    204             break;
    205           case DiffRecord::kDifferentPixels_Result:
    206             fNumMismatches++;
    207             if (drp->fFractionDifference * 100 > fMaxMismatchPercent) {
    208                 fMaxMismatchPercent = drp->fFractionDifference * 100;
    209             }
    210             mismatchValue = MAX3(drp->fMaxMismatchR, drp->fMaxMismatchG,
    211                                  drp->fMaxMismatchB);
    212             if (mismatchValue > fMaxMismatchV) {
    213                 fMaxMismatchV = mismatchValue;
    214             }
    215             break;
    216           case DiffRecord::kCouldNotCompare_Result:
    217             fNumMismatches++;
    218             fStatusOfType[drp->fBase.fStatus][drp->fComparison.fStatus].push(
    219                     new SkString(drp->fBase.fFilename));
    220             break;
    221           case DiffRecord::kUnknown_Result:
    222             SkDEBUGFAIL("adding uncategorized DiffRecord");
    223             break;
    224           default:
    225             SkDEBUGFAIL("adding DiffRecord with unhandled fResult value");
    226             break;
    227         }
    228 
    229         switch (drp->fResult) {
    230             case DiffRecord::kEqualBits_Result:
    231             case DiffRecord::kEqualPixels_Result:
    232                 break;
    233             default:
    234                 add_unique_basename(&fFailedBaseNames[drp->fResult], drp->fBase.fFilename);
    235                 break;
    236         }
    237     }
    238 };
    239 
    240 /// Returns true if string contains any of these substrings.
    241 static bool string_contains_any_of(const SkString& string,
    242                                    const StringArray& substrings) {
    243     for (int i = 0; i < substrings.count(); i++) {
    244         if (string.contains(substrings[i]->c_str())) {
    245             return true;
    246         }
    247     }
    248     return false;
    249 }
    250 
    251 /// Internal (potentially recursive) implementation of get_file_list.
    252 static void get_file_list_subdir(const SkString& rootDir, const SkString& subDir,
    253                                  const StringArray& matchSubstrings,
    254                                  const StringArray& nomatchSubstrings,
    255                                  bool recurseIntoSubdirs, FileArray *files) {
    256     bool isSubDirEmpty = subDir.isEmpty();
    257     SkString dir(rootDir);
    258     if (!isSubDirEmpty) {
    259         dir.append(PATH_DIV_STR);
    260         dir.append(subDir);
    261     }
    262 
    263     // Iterate over files (not directories) within dir.
    264     SkOSFile::Iter fileIterator(dir.c_str());
    265     SkString fileName;
    266     while (fileIterator.next(&fileName, false)) {
    267         if (fileName.startsWith(".")) {
    268             continue;
    269         }
    270         SkString pathRelativeToRootDir(subDir);
    271         if (!isSubDirEmpty) {
    272             pathRelativeToRootDir.append(PATH_DIV_STR);
    273         }
    274         pathRelativeToRootDir.append(fileName);
    275         if (string_contains_any_of(pathRelativeToRootDir, matchSubstrings) &&
    276             !string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) {
    277             files->push(new SkString(pathRelativeToRootDir));
    278         }
    279     }
    280 
    281     // Recurse into any non-ignored subdirectories.
    282     if (recurseIntoSubdirs) {
    283         SkOSFile::Iter dirIterator(dir.c_str());
    284         SkString dirName;
    285         while (dirIterator.next(&dirName, true)) {
    286             if (dirName.startsWith(".")) {
    287                 continue;
    288             }
    289             SkString pathRelativeToRootDir(subDir);
    290             if (!isSubDirEmpty) {
    291                 pathRelativeToRootDir.append(PATH_DIV_STR);
    292             }
    293             pathRelativeToRootDir.append(dirName);
    294             if (!string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) {
    295                 get_file_list_subdir(rootDir, pathRelativeToRootDir,
    296                                      matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
    297                                      files);
    298             }
    299         }
    300     }
    301 }
    302 
    303 /// Iterate over dir and get all files whose filename:
    304 ///  - matches any of the substrings in matchSubstrings, but...
    305 ///  - DOES NOT match any of the substrings in nomatchSubstrings
    306 ///  - DOES NOT start with a dot (.)
    307 /// Adds the matching files to the list in *files.
    308 static void get_file_list(const SkString& dir,
    309                           const StringArray& matchSubstrings,
    310                           const StringArray& nomatchSubstrings,
    311                           bool recurseIntoSubdirs, FileArray *files) {
    312     get_file_list_subdir(dir, SkString(""),
    313                          matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
    314                          files);
    315 }
    316 
    317 static void release_file_list(FileArray *files) {
    318     files->deleteAll();
    319 }
    320 
    321 /// Comparison routines for qsort, sort by file names.
    322 static int compare_file_name_metrics(SkString **lhs, SkString **rhs) {
    323     return strcmp((*lhs)->c_str(), (*rhs)->c_str());
    324 }
    325 
    326 class AutoReleasePixels {
    327 public:
    328     AutoReleasePixels(DiffRecord* drp)
    329     : fDrp(drp) {
    330         SkASSERT(drp != nullptr);
    331     }
    332     ~AutoReleasePixels() {
    333         fDrp->fBase.fBitmap.setPixelRef(nullptr, 0, 0);
    334         fDrp->fComparison.fBitmap.setPixelRef(nullptr, 0, 0);
    335         fDrp->fDifference.fBitmap.setPixelRef(nullptr, 0, 0);
    336         fDrp->fWhite.fBitmap.setPixelRef(nullptr, 0, 0);
    337     }
    338 
    339 private:
    340     DiffRecord* fDrp;
    341 };
    342 
    343 static void get_bounds(DiffResource& resource, const char* name) {
    344     if (resource.fBitmap.empty() && !DiffResource::isStatusFailed(resource.fStatus)) {
    345         sk_sp<SkData> fileBits(read_file(resource.fFullPath.c_str()));
    346         if (fileBits) {
    347             get_bitmap(fileBits, resource, true);
    348         } else {
    349             SkDebugf("WARNING: couldn't read %s file <%s>\n", name, resource.fFullPath.c_str());
    350             resource.fStatus = DiffResource::kCouldNotRead_Status;
    351         }
    352     }
    353 }
    354 
    355 static void get_bounds(DiffRecord& drp) {
    356     get_bounds(drp.fBase, "base");
    357     get_bounds(drp.fComparison, "comparison");
    358 }
    359 
    360 #ifdef SK_OS_WIN
    361 #define ANSI_COLOR_RED     ""
    362 #define ANSI_COLOR_GREEN   ""
    363 #define ANSI_COLOR_YELLOW  ""
    364 #define ANSI_COLOR_RESET   ""
    365 #else
    366 #define ANSI_COLOR_RED     "\x1b[31m"
    367 #define ANSI_COLOR_GREEN   "\x1b[32m"
    368 #define ANSI_COLOR_YELLOW  "\x1b[33m"
    369 #define ANSI_COLOR_RESET   "\x1b[0m"
    370 #endif
    371 
    372 #define VERBOSE_STATUS(status,color,filename) if (verbose) printf( "[ " color " %10s " ANSI_COLOR_RESET " ] %s\n", status, filename->c_str())
    373 
    374 /// Creates difference images, returns the number that have a 0 metric.
    375 /// If outputDir.isEmpty(), don't write out diff files.
    376 static void create_diff_images (DiffMetricProc dmp,
    377                                 const int colorThreshold,
    378                                 RecordArray* differences,
    379                                 const SkString& baseDir,
    380                                 const SkString& comparisonDir,
    381                                 const SkString& outputDir,
    382                                 const StringArray& matchSubstrings,
    383                                 const StringArray& nomatchSubstrings,
    384                                 bool recurseIntoSubdirs,
    385                                 bool getBounds,
    386                                 bool verbose,
    387                                 DiffSummary* summary) {
    388     SkASSERT(!baseDir.isEmpty());
    389     SkASSERT(!comparisonDir.isEmpty());
    390 
    391     FileArray baseFiles;
    392     FileArray comparisonFiles;
    393 
    394     get_file_list(baseDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, &baseFiles);
    395     get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
    396                   &comparisonFiles);
    397 
    398     if (!baseFiles.isEmpty()) {
    399         qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
    400               SkCastForQSort(compare_file_name_metrics));
    401     }
    402     if (!comparisonFiles.isEmpty()) {
    403         qsort(comparisonFiles.begin(), comparisonFiles.count(),
    404               sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
    405     }
    406 
    407     if (!outputDir.isEmpty()) {
    408         sk_mkdir(outputDir.c_str());
    409     }
    410 
    411     int i = 0;
    412     int j = 0;
    413 
    414     while (i < baseFiles.count() &&
    415            j < comparisonFiles.count()) {
    416 
    417         SkString basePath(baseDir);
    418         SkString comparisonPath(comparisonDir);
    419 
    420         DiffRecord *drp = new DiffRecord;
    421         int v = strcmp(baseFiles[i]->c_str(), comparisonFiles[j]->c_str());
    422 
    423         if (v < 0) {
    424             // in baseDir, but not in comparisonDir
    425             drp->fResult = DiffRecord::kCouldNotCompare_Result;
    426 
    427             basePath.append(*baseFiles[i]);
    428             comparisonPath.append(*baseFiles[i]);
    429 
    430             drp->fBase.fFilename = *baseFiles[i];
    431             drp->fBase.fFullPath = basePath;
    432             drp->fBase.fStatus = DiffResource::kExists_Status;
    433 
    434             drp->fComparison.fFilename = *baseFiles[i];
    435             drp->fComparison.fFullPath = comparisonPath;
    436             drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
    437 
    438             VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, baseFiles[i]);
    439 
    440             ++i;
    441         } else if (v > 0) {
    442             // in comparisonDir, but not in baseDir
    443             drp->fResult = DiffRecord::kCouldNotCompare_Result;
    444 
    445             basePath.append(*comparisonFiles[j]);
    446             comparisonPath.append(*comparisonFiles[j]);
    447 
    448             drp->fBase.fFilename = *comparisonFiles[j];
    449             drp->fBase.fFullPath = basePath;
    450             drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
    451 
    452             drp->fComparison.fFilename = *comparisonFiles[j];
    453             drp->fComparison.fFullPath = comparisonPath;
    454             drp->fComparison.fStatus = DiffResource::kExists_Status;
    455 
    456             VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, comparisonFiles[j]);
    457 
    458             ++j;
    459         } else {
    460             // Found the same filename in both baseDir and comparisonDir.
    461             SkASSERT(DiffRecord::kUnknown_Result == drp->fResult);
    462 
    463             basePath.append(*baseFiles[i]);
    464             comparisonPath.append(*comparisonFiles[j]);
    465 
    466             drp->fBase.fFilename = *baseFiles[i];
    467             drp->fBase.fFullPath = basePath;
    468             drp->fBase.fStatus = DiffResource::kExists_Status;
    469 
    470             drp->fComparison.fFilename = *comparisonFiles[j];
    471             drp->fComparison.fFullPath = comparisonPath;
    472             drp->fComparison.fStatus = DiffResource::kExists_Status;
    473 
    474             sk_sp<SkData> baseFileBits(read_file(drp->fBase.fFullPath.c_str()));
    475             if (baseFileBits) {
    476                 drp->fBase.fStatus = DiffResource::kRead_Status;
    477             }
    478             sk_sp<SkData> comparisonFileBits(read_file(drp->fComparison.fFullPath.c_str()));
    479             if (comparisonFileBits) {
    480                 drp->fComparison.fStatus = DiffResource::kRead_Status;
    481             }
    482             if (nullptr == baseFileBits || nullptr == comparisonFileBits) {
    483                 if (nullptr == baseFileBits) {
    484                     drp->fBase.fStatus = DiffResource::kCouldNotRead_Status;
    485                     VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, baseFiles[i]);
    486                 }
    487                 if (nullptr == comparisonFileBits) {
    488                     drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status;
    489                     VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, comparisonFiles[j]);
    490                 }
    491                 drp->fResult = DiffRecord::kCouldNotCompare_Result;
    492 
    493             } else if (are_buffers_equal(baseFileBits.get(), comparisonFileBits.get())) {
    494                 drp->fResult = DiffRecord::kEqualBits_Result;
    495                 VERBOSE_STATUS("MATCH", ANSI_COLOR_GREEN, baseFiles[i]);
    496             } else {
    497                 AutoReleasePixels arp(drp);
    498                 get_bitmap(baseFileBits, drp->fBase, false);
    499                 get_bitmap(comparisonFileBits, drp->fComparison, false);
    500                 VERBOSE_STATUS("DIFFERENT", ANSI_COLOR_RED, baseFiles[i]);
    501                 if (DiffResource::kDecoded_Status == drp->fBase.fStatus &&
    502                     DiffResource::kDecoded_Status == drp->fComparison.fStatus) {
    503                     create_and_write_diff_image(drp, dmp, colorThreshold,
    504                                                 outputDir, drp->fBase.fFilename);
    505                 } else {
    506                     drp->fResult = DiffRecord::kCouldNotCompare_Result;
    507                 }
    508             }
    509 
    510             ++i;
    511             ++j;
    512         }
    513 
    514         if (getBounds) {
    515             get_bounds(*drp);
    516         }
    517         SkASSERT(DiffRecord::kUnknown_Result != drp->fResult);
    518         differences->push(drp);
    519         summary->add(drp);
    520     }
    521 
    522     for (; i < baseFiles.count(); ++i) {
    523         // files only in baseDir
    524         DiffRecord *drp = new DiffRecord();
    525         drp->fBase.fFilename = *baseFiles[i];
    526         drp->fBase.fFullPath = baseDir;
    527         drp->fBase.fFullPath.append(drp->fBase.fFilename);
    528         drp->fBase.fStatus = DiffResource::kExists_Status;
    529 
    530         drp->fComparison.fFilename = *baseFiles[i];
    531         drp->fComparison.fFullPath = comparisonDir;
    532         drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
    533         drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
    534 
    535         drp->fResult = DiffRecord::kCouldNotCompare_Result;
    536         if (getBounds) {
    537             get_bounds(*drp);
    538         }
    539         differences->push(drp);
    540         summary->add(drp);
    541     }
    542 
    543     for (; j < comparisonFiles.count(); ++j) {
    544         // files only in comparisonDir
    545         DiffRecord *drp = new DiffRecord();
    546         drp->fBase.fFilename = *comparisonFiles[j];
    547         drp->fBase.fFullPath = baseDir;
    548         drp->fBase.fFullPath.append(drp->fBase.fFilename);
    549         drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
    550 
    551         drp->fComparison.fFilename = *comparisonFiles[j];
    552         drp->fComparison.fFullPath = comparisonDir;
    553         drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
    554         drp->fComparison.fStatus = DiffResource::kExists_Status;
    555 
    556         drp->fResult = DiffRecord::kCouldNotCompare_Result;
    557         if (getBounds) {
    558             get_bounds(*drp);
    559         }
    560         differences->push(drp);
    561         summary->add(drp);
    562     }
    563 
    564     release_file_list(&baseFiles);
    565     release_file_list(&comparisonFiles);
    566 }
    567 
    568 static void usage (char * argv0) {
    569     SkDebugf("Skia baseline image diff tool\n");
    570     SkDebugf("\n"
    571 "Usage: \n"
    572 "    %s <baseDir> <comparisonDir> [outputDir] \n", argv0);
    573     SkDebugf(
    574 "\nArguments:"
    575 "\n    --failonresult <result>: After comparing all file pairs, exit with nonzero"
    576 "\n                             return code (number of file pairs yielding this"
    577 "\n                             result) if any file pairs yielded this result."
    578 "\n                             This flag may be repeated, in which case the"
    579 "\n                             return code will be the number of fail pairs"
    580 "\n                             yielding ANY of these results."
    581 "\n    --failonstatus <baseStatus> <comparisonStatus>: exit with nonzero return"
    582 "\n                             code if any file pairs yielded this status."
    583 "\n    --help: display this info"
    584 "\n    --listfilenames: list all filenames for each result type in stdout"
    585 "\n    --match <substring>: compare files whose filenames contain this substring;"
    586 "\n                         if unspecified, compare ALL files."
    587 "\n                         this flag may be repeated."
    588 "\n    --nodiffs: don't write out image diffs or index.html, just generate"
    589 "\n               report on stdout"
    590 "\n    --nomatch <substring>: regardless of --match, DO NOT compare files whose"
    591 "\n                           filenames contain this substring."
    592 "\n                           this flag may be repeated."
    593 "\n    --noprintdirs: do not print the directories used."
    594 "\n    --norecurse: do not recurse into subdirectories."
    595 "\n    --sortbymaxmismatch: sort by worst color channel mismatch;"
    596 "\n                         break ties with -sortbymismatch"
    597 "\n    --sortbymismatch: sort by average color channel mismatch"
    598 "\n    --threshold <n>: only report differences > n (per color channel) [default 0]"
    599 "\n    --weighted: sort by # pixels different weighted by color difference"
    600 "\n"
    601 "\n    baseDir: directory to read baseline images from."
    602 "\n    comparisonDir: directory to read comparison images from"
    603 "\n    outputDir: directory to write difference images and index.html to;"
    604 "\n               defaults to comparisonDir"
    605 "\n"
    606 "\nIf no sort is specified, it will sort by fraction of pixels mismatching."
    607 "\n");
    608 }
    609 
    610 const int kNoError = 0;
    611 const int kGenericError = -1;
    612 
    613 int 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