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
36typedef SkTDArray<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->getAt(i) == result) {
57            return;
58        }
59    }
60    *array->append() = new SkString(result);
61}
62
63struct 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.
241static 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.
252static 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.
308static 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
317static void release_file_list(FileArray *files) {
318    files->deleteAll();
319}
320
321/// Comparison routines for qsort, sort by file names.
322static int compare_file_name_metrics(SkString **lhs, SkString **rhs) {
323    return strcmp((*lhs)->c_str(), (*rhs)->c_str());
324}
325
326class AutoReleasePixels {
327public:
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
339private:
340    DiffRecord* fDrp;
341};
342
343static 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
355static 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.
376static 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
568static 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
610const int kNoError = 0;
611const int kGenericError = -1;
612
613int 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