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