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