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