• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2011 Google Inc.
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 #include "skdiff.h"
8 #include "skdiff_html.h"
9 #include "skdiff_utils.h"
10 #include "SkBitmap.h"
11 #include "SkData.h"
12 #include "SkImageEncoder.h"
13 #include "SkOSFile.h"
14 #include "SkOSPath.h"
15 #include "SkStream.h"
16 #include "SkPixelRef.h"
17 #include "../private/SkTDArray.h"
18 #include "../private/SkTSearch.h"
19 
20 #include <stdlib.h>
21 
22 /**
23  * skdiff
24  *
25  * Given three directory names, expects to find identically-named files in
26  * each of the first two; the first are treated as a set of baseline,
27  * the second a set of variant images, and a diff image is written into the
28  * third directory for each pair.
29  * Creates an index.html in the current third directory to compare each
30  * pair that does not match exactly.
31  * Recursively descends directories, unless run with --norecurse.
32  *
33  * Returns zero exit code if all images match across baseDir and comparisonDir.
34  */
35 
36 typedef SkTDArray<SkString*> StringArray;
37 typedef StringArray FileArray;
38 
add_unique_basename(StringArray * array,const SkString & filename)39 static void add_unique_basename(StringArray* array, const SkString& filename) {
40     // trim off dirs
41     const char* src = filename.c_str();
42     const char* trimmed = strrchr(src, SkOSPath::SEPARATOR);
43     if (trimmed) {
44         trimmed += 1;   // skip the separator
45     } else {
46         trimmed = src;
47     }
48     const char* end = strrchr(trimmed, '.');
49     if (!end) {
50         end = trimmed + strlen(trimmed);
51     }
52     SkString result(trimmed, end - trimmed);
53 
54     // only add unique entries
55     for (int i = 0; i < array->count(); ++i) {
56         if (*array->getAt(i) == result) {
57             return;
58         }
59     }
60     *array->append() = new SkString(result);
61 }
62 
63 struct DiffSummary {
DiffSummaryDiffSummary64     DiffSummary ()
65         : fNumMatches(0)
66         , fNumMismatches(0)
67         , fMaxMismatchV(0)
68         , fMaxMismatchPercent(0) { }
69 
~DiffSummaryDiffSummary70     ~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 
printContentsDiffSummary91     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 
printStatusDiffSummary106     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.
printContentsDiffSummary132     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 
printDiffSummary144     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 
printfFailingBaseNamesDiffSummary169     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 
addDiffSummary182     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.
string_contains_any_of(const SkString & string,const StringArray & substrings)241 static bool string_contains_any_of(const SkString& string,
242                                    const StringArray& substrings) {
243     for (int i = 0; i < substrings.count(); i++) {
244         if (string.contains(substrings[i]->c_str())) {
245             return true;
246         }
247     }
248     return false;
249 }
250 
251 /// Internal (potentially recursive) implementation of get_file_list.
get_file_list_subdir(const SkString & rootDir,const SkString & subDir,const StringArray & matchSubstrings,const StringArray & nomatchSubstrings,bool recurseIntoSubdirs,FileArray * files)252 static void get_file_list_subdir(const SkString& rootDir, const SkString& subDir,
253                                  const StringArray& matchSubstrings,
254                                  const StringArray& nomatchSubstrings,
255                                  bool recurseIntoSubdirs, FileArray *files) {
256     bool isSubDirEmpty = subDir.isEmpty();
257     SkString dir(rootDir);
258     if (!isSubDirEmpty) {
259         dir.append(PATH_DIV_STR);
260         dir.append(subDir);
261     }
262 
263     // Iterate over files (not directories) within dir.
264     SkOSFile::Iter fileIterator(dir.c_str());
265     SkString fileName;
266     while (fileIterator.next(&fileName, false)) {
267         if (fileName.startsWith(".")) {
268             continue;
269         }
270         SkString pathRelativeToRootDir(subDir);
271         if (!isSubDirEmpty) {
272             pathRelativeToRootDir.append(PATH_DIV_STR);
273         }
274         pathRelativeToRootDir.append(fileName);
275         if (string_contains_any_of(pathRelativeToRootDir, matchSubstrings) &&
276             !string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) {
277             files->push(new SkString(pathRelativeToRootDir));
278         }
279     }
280 
281     // Recurse into any non-ignored subdirectories.
282     if (recurseIntoSubdirs) {
283         SkOSFile::Iter dirIterator(dir.c_str());
284         SkString dirName;
285         while (dirIterator.next(&dirName, true)) {
286             if (dirName.startsWith(".")) {
287                 continue;
288             }
289             SkString pathRelativeToRootDir(subDir);
290             if (!isSubDirEmpty) {
291                 pathRelativeToRootDir.append(PATH_DIV_STR);
292             }
293             pathRelativeToRootDir.append(dirName);
294             if (!string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) {
295                 get_file_list_subdir(rootDir, pathRelativeToRootDir,
296                                      matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
297                                      files);
298             }
299         }
300     }
301 }
302 
303 /// Iterate over dir and get all files whose filename:
304 ///  - matches any of the substrings in matchSubstrings, but...
305 ///  - DOES NOT match any of the substrings in nomatchSubstrings
306 ///  - DOES NOT start with a dot (.)
307 /// Adds the matching files to the list in *files.
get_file_list(const SkString & dir,const StringArray & matchSubstrings,const StringArray & nomatchSubstrings,bool recurseIntoSubdirs,FileArray * files)308 static void get_file_list(const SkString& dir,
309                           const StringArray& matchSubstrings,
310                           const StringArray& nomatchSubstrings,
311                           bool recurseIntoSubdirs, FileArray *files) {
312     get_file_list_subdir(dir, SkString(""),
313                          matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
314                          files);
315 }
316 
release_file_list(FileArray * files)317 static void release_file_list(FileArray *files) {
318     files->deleteAll();
319 }
320 
321 /// Comparison routines for qsort, sort by file names.
compare_file_name_metrics(SkString ** lhs,SkString ** rhs)322 static int compare_file_name_metrics(SkString **lhs, SkString **rhs) {
323     return strcmp((*lhs)->c_str(), (*rhs)->c_str());
324 }
325 
326 class AutoReleasePixels {
327 public:
AutoReleasePixels(DiffRecord * drp)328     AutoReleasePixels(DiffRecord* drp)
329     : fDrp(drp) {
330         SkASSERT(drp != nullptr);
331     }
~AutoReleasePixels()332     ~AutoReleasePixels() {
333         fDrp->fBase.fBitmap.setPixelRef(nullptr, 0, 0);
334         fDrp->fComparison.fBitmap.setPixelRef(nullptr, 0, 0);
335         fDrp->fDifference.fBitmap.setPixelRef(nullptr, 0, 0);
336         fDrp->fWhite.fBitmap.setPixelRef(nullptr, 0, 0);
337     }
338 
339 private:
340     DiffRecord* fDrp;
341 };
342 
get_bounds(DiffResource & resource,const char * name)343 static void get_bounds(DiffResource& resource, const char* name) {
344     if (resource.fBitmap.empty() && !DiffResource::isStatusFailed(resource.fStatus)) {
345         sk_sp<SkData> fileBits(read_file(resource.fFullPath.c_str()));
346         if (fileBits) {
347             get_bitmap(fileBits, resource, true);
348         } else {
349             SkDebugf("WARNING: couldn't read %s file <%s>\n", name, resource.fFullPath.c_str());
350             resource.fStatus = DiffResource::kCouldNotRead_Status;
351         }
352     }
353 }
354 
get_bounds(DiffRecord & drp)355 static void get_bounds(DiffRecord& drp) {
356     get_bounds(drp.fBase, "base");
357     get_bounds(drp.fComparison, "comparison");
358 }
359 
360 #ifdef SK_OS_WIN
361 #define ANSI_COLOR_RED     ""
362 #define ANSI_COLOR_GREEN   ""
363 #define ANSI_COLOR_YELLOW  ""
364 #define ANSI_COLOR_RESET   ""
365 #else
366 #define ANSI_COLOR_RED     "\x1b[31m"
367 #define ANSI_COLOR_GREEN   "\x1b[32m"
368 #define ANSI_COLOR_YELLOW  "\x1b[33m"
369 #define ANSI_COLOR_RESET   "\x1b[0m"
370 #endif
371 
372 #define VERBOSE_STATUS(status,color,filename) if (verbose) printf( "[ " color " %10s " ANSI_COLOR_RESET " ] %s\n", status, filename->c_str())
373 
374 /// Creates difference images, returns the number that have a 0 metric.
375 /// If outputDir.isEmpty(), don't write out diff files.
create_diff_images(DiffMetricProc dmp,const int colorThreshold,RecordArray * differences,const SkString & baseDir,const SkString & comparisonDir,const SkString & outputDir,const StringArray & matchSubstrings,const StringArray & nomatchSubstrings,bool recurseIntoSubdirs,bool getBounds,bool verbose,DiffSummary * summary)376 static void create_diff_images (DiffMetricProc dmp,
377                                 const int colorThreshold,
378                                 RecordArray* differences,
379                                 const SkString& baseDir,
380                                 const SkString& comparisonDir,
381                                 const SkString& outputDir,
382                                 const StringArray& matchSubstrings,
383                                 const StringArray& nomatchSubstrings,
384                                 bool recurseIntoSubdirs,
385                                 bool getBounds,
386                                 bool verbose,
387                                 DiffSummary* summary) {
388     SkASSERT(!baseDir.isEmpty());
389     SkASSERT(!comparisonDir.isEmpty());
390 
391     FileArray baseFiles;
392     FileArray comparisonFiles;
393 
394     get_file_list(baseDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, &baseFiles);
395     get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
396                   &comparisonFiles);
397 
398     if (!baseFiles.isEmpty()) {
399         qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
400               SkCastForQSort(compare_file_name_metrics));
401     }
402     if (!comparisonFiles.isEmpty()) {
403         qsort(comparisonFiles.begin(), comparisonFiles.count(),
404               sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
405     }
406 
407     if (!outputDir.isEmpty()) {
408         sk_mkdir(outputDir.c_str());
409     }
410 
411     int i = 0;
412     int j = 0;
413 
414     while (i < baseFiles.count() &&
415            j < comparisonFiles.count()) {
416 
417         SkString basePath(baseDir);
418         SkString comparisonPath(comparisonDir);
419 
420         DiffRecord *drp = new DiffRecord;
421         int v = strcmp(baseFiles[i]->c_str(), comparisonFiles[j]->c_str());
422 
423         if (v < 0) {
424             // in baseDir, but not in comparisonDir
425             drp->fResult = DiffRecord::kCouldNotCompare_Result;
426 
427             basePath.append(*baseFiles[i]);
428             comparisonPath.append(*baseFiles[i]);
429 
430             drp->fBase.fFilename = *baseFiles[i];
431             drp->fBase.fFullPath = basePath;
432             drp->fBase.fStatus = DiffResource::kExists_Status;
433 
434             drp->fComparison.fFilename = *baseFiles[i];
435             drp->fComparison.fFullPath = comparisonPath;
436             drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
437 
438             VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, baseFiles[i]);
439 
440             ++i;
441         } else if (v > 0) {
442             // in comparisonDir, but not in baseDir
443             drp->fResult = DiffRecord::kCouldNotCompare_Result;
444 
445             basePath.append(*comparisonFiles[j]);
446             comparisonPath.append(*comparisonFiles[j]);
447 
448             drp->fBase.fFilename = *comparisonFiles[j];
449             drp->fBase.fFullPath = basePath;
450             drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
451 
452             drp->fComparison.fFilename = *comparisonFiles[j];
453             drp->fComparison.fFullPath = comparisonPath;
454             drp->fComparison.fStatus = DiffResource::kExists_Status;
455 
456             VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, comparisonFiles[j]);
457 
458             ++j;
459         } else {
460             // Found the same filename in both baseDir and comparisonDir.
461             SkASSERT(DiffRecord::kUnknown_Result == drp->fResult);
462 
463             basePath.append(*baseFiles[i]);
464             comparisonPath.append(*comparisonFiles[j]);
465 
466             drp->fBase.fFilename = *baseFiles[i];
467             drp->fBase.fFullPath = basePath;
468             drp->fBase.fStatus = DiffResource::kExists_Status;
469 
470             drp->fComparison.fFilename = *comparisonFiles[j];
471             drp->fComparison.fFullPath = comparisonPath;
472             drp->fComparison.fStatus = DiffResource::kExists_Status;
473 
474             sk_sp<SkData> baseFileBits(read_file(drp->fBase.fFullPath.c_str()));
475             if (baseFileBits) {
476                 drp->fBase.fStatus = DiffResource::kRead_Status;
477             }
478             sk_sp<SkData> comparisonFileBits(read_file(drp->fComparison.fFullPath.c_str()));
479             if (comparisonFileBits) {
480                 drp->fComparison.fStatus = DiffResource::kRead_Status;
481             }
482             if (nullptr == baseFileBits || nullptr == comparisonFileBits) {
483                 if (nullptr == baseFileBits) {
484                     drp->fBase.fStatus = DiffResource::kCouldNotRead_Status;
485                     VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, baseFiles[i]);
486                 }
487                 if (nullptr == comparisonFileBits) {
488                     drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status;
489                     VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, comparisonFiles[j]);
490                 }
491                 drp->fResult = DiffRecord::kCouldNotCompare_Result;
492 
493             } else if (are_buffers_equal(baseFileBits.get(), comparisonFileBits.get())) {
494                 drp->fResult = DiffRecord::kEqualBits_Result;
495                 VERBOSE_STATUS("MATCH", ANSI_COLOR_GREEN, baseFiles[i]);
496             } else {
497                 AutoReleasePixels arp(drp);
498                 get_bitmap(baseFileBits, drp->fBase, false);
499                 get_bitmap(comparisonFileBits, drp->fComparison, false);
500                 VERBOSE_STATUS("DIFFERENT", ANSI_COLOR_RED, baseFiles[i]);
501                 if (DiffResource::kDecoded_Status == drp->fBase.fStatus &&
502                     DiffResource::kDecoded_Status == drp->fComparison.fStatus) {
503                     create_and_write_diff_image(drp, dmp, colorThreshold,
504                                                 outputDir, drp->fBase.fFilename);
505                 } else {
506                     drp->fResult = DiffRecord::kCouldNotCompare_Result;
507                 }
508             }
509 
510             ++i;
511             ++j;
512         }
513 
514         if (getBounds) {
515             get_bounds(*drp);
516         }
517         SkASSERT(DiffRecord::kUnknown_Result != drp->fResult);
518         differences->push(drp);
519         summary->add(drp);
520     }
521 
522     for (; i < baseFiles.count(); ++i) {
523         // files only in baseDir
524         DiffRecord *drp = new DiffRecord();
525         drp->fBase.fFilename = *baseFiles[i];
526         drp->fBase.fFullPath = baseDir;
527         drp->fBase.fFullPath.append(drp->fBase.fFilename);
528         drp->fBase.fStatus = DiffResource::kExists_Status;
529 
530         drp->fComparison.fFilename = *baseFiles[i];
531         drp->fComparison.fFullPath = comparisonDir;
532         drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
533         drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
534 
535         drp->fResult = DiffRecord::kCouldNotCompare_Result;
536         if (getBounds) {
537             get_bounds(*drp);
538         }
539         differences->push(drp);
540         summary->add(drp);
541     }
542 
543     for (; j < comparisonFiles.count(); ++j) {
544         // files only in comparisonDir
545         DiffRecord *drp = new DiffRecord();
546         drp->fBase.fFilename = *comparisonFiles[j];
547         drp->fBase.fFullPath = baseDir;
548         drp->fBase.fFullPath.append(drp->fBase.fFilename);
549         drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
550 
551         drp->fComparison.fFilename = *comparisonFiles[j];
552         drp->fComparison.fFullPath = comparisonDir;
553         drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
554         drp->fComparison.fStatus = DiffResource::kExists_Status;
555 
556         drp->fResult = DiffRecord::kCouldNotCompare_Result;
557         if (getBounds) {
558             get_bounds(*drp);
559         }
560         differences->push(drp);
561         summary->add(drp);
562     }
563 
564     release_file_list(&baseFiles);
565     release_file_list(&comparisonFiles);
566 }
567 
usage(char * argv0)568 static void usage (char * argv0) {
569     SkDebugf("Skia baseline image diff tool\n");
570     SkDebugf("\n"
571 "Usage: \n"
572 "    %s <baseDir> <comparisonDir> [outputDir] \n", argv0);
573     SkDebugf(
574 "\nArguments:"
575 "\n    --failonresult <result>: After comparing all file pairs, exit with nonzero"
576 "\n                             return code (number of file pairs yielding this"
577 "\n                             result) if any file pairs yielded this result."
578 "\n                             This flag may be repeated, in which case the"
579 "\n                             return code will be the number of fail pairs"
580 "\n                             yielding ANY of these results."
581 "\n    --failonstatus <baseStatus> <comparisonStatus>: exit with nonzero return"
582 "\n                             code if any file pairs yielded this status."
583 "\n    --help: display this info"
584 "\n    --listfilenames: list all filenames for each result type in stdout"
585 "\n    --match <substring>: compare files whose filenames contain this substring;"
586 "\n                         if unspecified, compare ALL files."
587 "\n                         this flag may be repeated."
588 "\n    --nodiffs: don't write out image diffs or index.html, just generate"
589 "\n               report on stdout"
590 "\n    --nomatch <substring>: regardless of --match, DO NOT compare files whose"
591 "\n                           filenames contain this substring."
592 "\n                           this flag may be repeated."
593 "\n    --noprintdirs: do not print the directories used."
594 "\n    --norecurse: do not recurse into subdirectories."
595 "\n    --sortbymaxmismatch: sort by worst color channel mismatch;"
596 "\n                         break ties with -sortbymismatch"
597 "\n    --sortbymismatch: sort by average color channel mismatch"
598 "\n    --threshold <n>: only report differences > n (per color channel) [default 0]"
599 "\n    --weighted: sort by # pixels different weighted by color difference"
600 "\n"
601 "\n    baseDir: directory to read baseline images from."
602 "\n    comparisonDir: directory to read comparison images from"
603 "\n    outputDir: directory to write difference images and index.html to;"
604 "\n               defaults to comparisonDir"
605 "\n"
606 "\nIf no sort is specified, it will sort by fraction of pixels mismatching."
607 "\n");
608 }
609 
610 const int kNoError = 0;
611 const int kGenericError = -1;
612 
main(int argc,char ** argv)613 int main(int argc, char** argv) {
614     DiffMetricProc diffProc = compute_diff_pmcolor;
615     int (*sortProc)(const void*, const void*) = compare<CompareDiffMetrics>;
616 
617     // Maximum error tolerated in any one color channel in any one pixel before
618     // a difference is reported.
619     int colorThreshold = 0;
620     SkString baseDir;
621     SkString comparisonDir;
622     SkString outputDir;
623 
624     StringArray matchSubstrings;
625     StringArray nomatchSubstrings;
626 
627     bool generateDiffs = true;
628     bool listFilenames = false;
629     bool printDirNames = true;
630     bool recurseIntoSubdirs = true;
631     bool verbose = false;
632     bool listFailingBase = false;
633 
634     RecordArray differences;
635     DiffSummary summary;
636 
637     bool failOnResultType[DiffRecord::kResultCount];
638     for (int i = 0; i < DiffRecord::kResultCount; i++) {
639         failOnResultType[i] = false;
640     }
641 
642     bool failOnStatusType[DiffResource::kStatusCount][DiffResource::kStatusCount];
643     for (int base = 0; base < DiffResource::kStatusCount; ++base) {
644         for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
645             failOnStatusType[base][comparison] = false;
646         }
647     }
648 
649     int i;
650     int numUnflaggedArguments = 0;
651     for (i = 1; i < argc; i++) {
652         if (!strcmp(argv[i], "--failonresult")) {
653             if (argc == ++i) {
654                 SkDebugf("failonresult expects one argument.\n");
655                 continue;
656             }
657             DiffRecord::Result type = DiffRecord::getResultByName(argv[i]);
658             if (type != DiffRecord::kResultCount) {
659                 failOnResultType[type] = true;
660             } else {
661                 SkDebugf("ignoring unrecognized result <%s>\n", argv[i]);
662             }
663             continue;
664         }
665         if (!strcmp(argv[i], "--failonstatus")) {
666             if (argc == ++i) {
667                 SkDebugf("failonstatus missing base status.\n");
668                 continue;
669             }
670             bool baseStatuses[DiffResource::kStatusCount];
671             if (!DiffResource::getMatchingStatuses(argv[i], baseStatuses)) {
672                 SkDebugf("unrecognized base status <%s>\n", argv[i]);
673             }
674 
675             if (argc == ++i) {
676                 SkDebugf("failonstatus missing comparison status.\n");
677                 continue;
678             }
679             bool comparisonStatuses[DiffResource::kStatusCount];
680             if (!DiffResource::getMatchingStatuses(argv[i], comparisonStatuses)) {
681                 SkDebugf("unrecognized comarison status <%s>\n", argv[i]);
682             }
683 
684             for (int base = 0; base < DiffResource::kStatusCount; ++base) {
685                 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
686                     failOnStatusType[base][comparison] |=
687                         baseStatuses[base] && comparisonStatuses[comparison];
688                 }
689             }
690             continue;
691         }
692         if (!strcmp(argv[i], "--help")) {
693             usage(argv[0]);
694             return kNoError;
695         }
696         if (!strcmp(argv[i], "--listfilenames")) {
697             listFilenames = true;
698             continue;
699         }
700         if (!strcmp(argv[i], "--verbose")) {
701             verbose = true;
702             continue;
703         }
704         if (!strcmp(argv[i], "--match")) {
705             matchSubstrings.push(new SkString(argv[++i]));
706             continue;
707         }
708         if (!strcmp(argv[i], "--nodiffs")) {
709             generateDiffs = false;
710             continue;
711         }
712         if (!strcmp(argv[i], "--nomatch")) {
713             nomatchSubstrings.push(new SkString(argv[++i]));
714             continue;
715         }
716         if (!strcmp(argv[i], "--noprintdirs")) {
717             printDirNames = false;
718             continue;
719         }
720         if (!strcmp(argv[i], "--norecurse")) {
721             recurseIntoSubdirs = false;
722             continue;
723         }
724         if (!strcmp(argv[i], "--sortbymaxmismatch")) {
725             sortProc = compare<CompareDiffMaxMismatches>;
726             continue;
727         }
728         if (!strcmp(argv[i], "--sortbymismatch")) {
729             sortProc = compare<CompareDiffMeanMismatches>;
730             continue;
731         }
732         if (!strcmp(argv[i], "--threshold")) {
733             colorThreshold = atoi(argv[++i]);
734             continue;
735         }
736         if (!strcmp(argv[i], "--weighted")) {
737             sortProc = compare<CompareDiffWeighted>;
738             continue;
739         }
740         if (argv[i][0] != '-') {
741             switch (numUnflaggedArguments++) {
742                 case 0:
743                     baseDir.set(argv[i]);
744                     continue;
745                 case 1:
746                     comparisonDir.set(argv[i]);
747                     continue;
748                 case 2:
749                     outputDir.set(argv[i]);
750                     continue;
751                 default:
752                     SkDebugf("extra unflagged argument <%s>\n", argv[i]);
753                     usage(argv[0]);
754                     return kGenericError;
755             }
756         }
757         if (!strcmp(argv[i], "--listFailingBase")) {
758             listFailingBase = true;
759             continue;
760         }
761 
762         SkDebugf("Unrecognized argument <%s>\n", argv[i]);
763         usage(argv[0]);
764         return kGenericError;
765     }
766 
767     if (numUnflaggedArguments == 2) {
768         outputDir = comparisonDir;
769     } else if (numUnflaggedArguments != 3) {
770         usage(argv[0]);
771         return kGenericError;
772     }
773 
774     if (!baseDir.endsWith(PATH_DIV_STR)) {
775         baseDir.append(PATH_DIV_STR);
776     }
777     if (printDirNames) {
778         printf("baseDir is [%s]\n", baseDir.c_str());
779     }
780 
781     if (!comparisonDir.endsWith(PATH_DIV_STR)) {
782         comparisonDir.append(PATH_DIV_STR);
783     }
784     if (printDirNames) {
785         printf("comparisonDir is [%s]\n", comparisonDir.c_str());
786     }
787 
788     if (!outputDir.endsWith(PATH_DIV_STR)) {
789         outputDir.append(PATH_DIV_STR);
790     }
791     if (generateDiffs) {
792         if (printDirNames) {
793             printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
794         }
795     } else {
796         if (printDirNames) {
797             printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
798         }
799         outputDir.set("");
800     }
801 
802     // If no matchSubstrings were specified, match ALL strings
803     // (except for whatever nomatchSubstrings were specified, if any).
804     if (matchSubstrings.isEmpty()) {
805         matchSubstrings.push(new SkString(""));
806     }
807 
808     create_diff_images(diffProc, colorThreshold, &differences,
809                        baseDir, comparisonDir, outputDir,
810                        matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, generateDiffs,
811                        verbose, &summary);
812     summary.print(listFilenames, failOnResultType, failOnStatusType);
813 
814     if (listFailingBase) {
815         summary.printfFailingBaseNames("\n");
816     }
817 
818     if (differences.count()) {
819         qsort(differences.begin(), differences.count(),
820               sizeof(DiffRecord*), sortProc);
821     }
822 
823     if (generateDiffs) {
824         print_diff_page(summary.fNumMatches, colorThreshold, differences,
825                         baseDir, comparisonDir, outputDir);
826     }
827 
828     for (i = 0; i < differences.count(); i++) {
829         delete differences[i];
830     }
831     matchSubstrings.deleteAll();
832     nomatchSubstrings.deleteAll();
833 
834     int num_failing_results = 0;
835     for (int i = 0; i < DiffRecord::kResultCount; i++) {
836         if (failOnResultType[i]) {
837             num_failing_results += summary.fResultsOfType[i].count();
838         }
839     }
840     if (!failOnResultType[DiffRecord::kCouldNotCompare_Result]) {
841         for (int base = 0; base < DiffResource::kStatusCount; ++base) {
842             for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
843                 if (failOnStatusType[base][comparison]) {
844                     num_failing_results += summary.fStatusOfType[base][comparison].count();
845                 }
846             }
847         }
848     }
849 
850     // On Linux (and maybe other platforms too), any results outside of the
851     // range [0...255] are wrapped (mod 256).  Do the conversion ourselves, to
852     // make sure that we only return 0 when there were no failures.
853     return (num_failing_results > 255) ? 255 : num_failing_results;
854 }
855