1 /*
2  * Copyright 2011 Google Inc.
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 #include "include/core/SkBitmap.h"
8 #include "include/core/SkData.h"
9 #include "include/core/SkImageEncoder.h"
10 #include "include/core/SkPixelRef.h"
11 #include "include/core/SkStream.h"
12 #include "include/private/SkTDArray.h"
13 #include "src/core/SkOSFile.h"
14 #include "src/core/SkTSearch.h"
15 #include "src/utils/SkOSPath.h"
16 #include "tools/skdiff/skdiff.h"
17 #include "tools/skdiff/skdiff_html.h"
18 #include "tools/skdiff/skdiff_utils.h"
19 
20 #include <stdlib.h>
21 
22 /**
23  * skdiff
24  *
25  * Given three directory names, expects to find identically-named files in
26  * each of the first two; the first are treated as a set of baseline,
27  * the second a set of variant images, and a diff image is written into the
28  * third directory for each pair.
29  * Creates an index.html in the current third directory to compare each
30  * pair that does not match exactly.
31  * Recursively descends directories, unless run with --norecurse.
32  *
33  * Returns zero exit code if all images match across baseDir and comparisonDir.
34  */
35 
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_back(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_back(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_back(
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_back(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, 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,bool ignoreColorSpace,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                                 bool ignoreColorSpace,
379                                 RecordArray* differences,
380                                 const SkString& baseDir,
381                                 const SkString& comparisonDir,
382                                 const SkString& outputDir,
383                                 const StringArray& matchSubstrings,
384                                 const StringArray& nomatchSubstrings,
385                                 bool recurseIntoSubdirs,
386                                 bool getBounds,
387                                 bool verbose,
388                                 DiffSummary* summary) {
389     SkASSERT(!baseDir.isEmpty());
390     SkASSERT(!comparisonDir.isEmpty());
391 
392     FileArray baseFiles;
393     FileArray comparisonFiles;
394 
395     get_file_list(baseDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, &baseFiles);
396     get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
397                   &comparisonFiles);
398 
399     if (!baseFiles.isEmpty()) {
400         qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
401               SkCastForQSort(compare_file_name_metrics));
402     }
403     if (!comparisonFiles.isEmpty()) {
404         qsort(comparisonFiles.begin(), comparisonFiles.count(),
405               sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
406     }
407 
408     if (!outputDir.isEmpty()) {
409         sk_mkdir(outputDir.c_str());
410     }
411 
412     int i = 0;
413     int j = 0;
414 
415     while (i < baseFiles.count() &&
416            j < comparisonFiles.count()) {
417 
418         SkString basePath(baseDir);
419         SkString comparisonPath(comparisonDir);
420 
421         DiffRecord *drp = new DiffRecord;
422         int v = strcmp(baseFiles[i]->c_str(), comparisonFiles[j]->c_str());
423 
424         if (v < 0) {
425             // in baseDir, but not in comparisonDir
426             drp->fResult = DiffRecord::kCouldNotCompare_Result;
427 
428             basePath.append(*baseFiles[i]);
429             comparisonPath.append(*baseFiles[i]);
430 
431             drp->fBase.fFilename = *baseFiles[i];
432             drp->fBase.fFullPath = basePath;
433             drp->fBase.fStatus = DiffResource::kExists_Status;
434 
435             drp->fComparison.fFilename = *baseFiles[i];
436             drp->fComparison.fFullPath = comparisonPath;
437             drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
438 
439             VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, baseFiles[i]);
440 
441             ++i;
442         } else if (v > 0) {
443             // in comparisonDir, but not in baseDir
444             drp->fResult = DiffRecord::kCouldNotCompare_Result;
445 
446             basePath.append(*comparisonFiles[j]);
447             comparisonPath.append(*comparisonFiles[j]);
448 
449             drp->fBase.fFilename = *comparisonFiles[j];
450             drp->fBase.fFullPath = basePath;
451             drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
452 
453             drp->fComparison.fFilename = *comparisonFiles[j];
454             drp->fComparison.fFullPath = comparisonPath;
455             drp->fComparison.fStatus = DiffResource::kExists_Status;
456 
457             VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, comparisonFiles[j]);
458 
459             ++j;
460         } else {
461             // Found the same filename in both baseDir and comparisonDir.
462             SkASSERT(DiffRecord::kUnknown_Result == drp->fResult);
463 
464             basePath.append(*baseFiles[i]);
465             comparisonPath.append(*comparisonFiles[j]);
466 
467             drp->fBase.fFilename = *baseFiles[i];
468             drp->fBase.fFullPath = basePath;
469             drp->fBase.fStatus = DiffResource::kExists_Status;
470 
471             drp->fComparison.fFilename = *comparisonFiles[j];
472             drp->fComparison.fFullPath = comparisonPath;
473             drp->fComparison.fStatus = DiffResource::kExists_Status;
474 
475             sk_sp<SkData> baseFileBits(read_file(drp->fBase.fFullPath.c_str()));
476             if (baseFileBits) {
477                 drp->fBase.fStatus = DiffResource::kRead_Status;
478             }
479             sk_sp<SkData> comparisonFileBits(read_file(drp->fComparison.fFullPath.c_str()));
480             if (comparisonFileBits) {
481                 drp->fComparison.fStatus = DiffResource::kRead_Status;
482             }
483             if (nullptr == baseFileBits || nullptr == comparisonFileBits) {
484                 if (nullptr == baseFileBits) {
485                     drp->fBase.fStatus = DiffResource::kCouldNotRead_Status;
486                     VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, baseFiles[i]);
487                 }
488                 if (nullptr == comparisonFileBits) {
489                     drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status;
490                     VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, comparisonFiles[j]);
491                 }
492                 drp->fResult = DiffRecord::kCouldNotCompare_Result;
493 
494             } else if (are_buffers_equal(baseFileBits.get(), comparisonFileBits.get())) {
495                 drp->fResult = DiffRecord::kEqualBits_Result;
496                 VERBOSE_STATUS("MATCH", ANSI_COLOR_GREEN, baseFiles[i]);
497             } else {
498                 AutoReleasePixels arp(drp);
499                 get_bitmap(baseFileBits, drp->fBase, false, ignoreColorSpace);
500                 get_bitmap(comparisonFileBits, drp->fComparison, false, ignoreColorSpace);
501                 VERBOSE_STATUS("DIFFERENT", ANSI_COLOR_RED, baseFiles[i]);
502                 if (DiffResource::kDecoded_Status == drp->fBase.fStatus &&
503                     DiffResource::kDecoded_Status == drp->fComparison.fStatus) {
504                     create_and_write_diff_image(drp, dmp, colorThreshold,
505                                                 outputDir, drp->fBase.fFilename);
506                 } else {
507                     drp->fResult = DiffRecord::kCouldNotCompare_Result;
508                 }
509             }
510 
511             ++i;
512             ++j;
513         }
514 
515         if (getBounds) {
516             get_bounds(*drp);
517         }
518         SkASSERT(DiffRecord::kUnknown_Result != drp->fResult);
519         differences->push_back(drp);
520         summary->add(drp);
521     }
522 
523     for (; i < baseFiles.count(); ++i) {
524         // files only in baseDir
525         DiffRecord *drp = new DiffRecord();
526         drp->fBase.fFilename = *baseFiles[i];
527         drp->fBase.fFullPath = baseDir;
528         drp->fBase.fFullPath.append(drp->fBase.fFilename);
529         drp->fBase.fStatus = DiffResource::kExists_Status;
530 
531         drp->fComparison.fFilename = *baseFiles[i];
532         drp->fComparison.fFullPath = comparisonDir;
533         drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
534         drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
535 
536         drp->fResult = DiffRecord::kCouldNotCompare_Result;
537         if (getBounds) {
538             get_bounds(*drp);
539         }
540         differences->push_back(drp);
541         summary->add(drp);
542     }
543 
544     for (; j < comparisonFiles.count(); ++j) {
545         // files only in comparisonDir
546         DiffRecord *drp = new DiffRecord();
547         drp->fBase.fFilename = *comparisonFiles[j];
548         drp->fBase.fFullPath = baseDir;
549         drp->fBase.fFullPath.append(drp->fBase.fFilename);
550         drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
551 
552         drp->fComparison.fFilename = *comparisonFiles[j];
553         drp->fComparison.fFullPath = comparisonDir;
554         drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
555         drp->fComparison.fStatus = DiffResource::kExists_Status;
556 
557         drp->fResult = DiffRecord::kCouldNotCompare_Result;
558         if (getBounds) {
559             get_bounds(*drp);
560         }
561         differences->push_back(drp);
562         summary->add(drp);
563     }
564 
565     release_file_list(&baseFiles);
566     release_file_list(&comparisonFiles);
567 }
568 
usage(char * argv0)569 static void usage (char * argv0) {
570     SkDebugf("Skia baseline image diff tool\n");
571     SkDebugf("\n"
572 "Usage: \n"
573 "    %s <baseDir> <comparisonDir> [outputDir] \n", argv0);
574     SkDebugf(
575 "\nArguments:"
576 "\n    --failonresult <result>: After comparing all file pairs, exit with nonzero"
577 "\n                             return code (number of file pairs yielding this"
578 "\n                             result) if any file pairs yielded this result."
579 "\n                             This flag may be repeated, in which case the"
580 "\n                             return code will be the number of fail pairs"
581 "\n                             yielding ANY of these results."
582 "\n    --failonstatus <baseStatus> <comparisonStatus>: exit with nonzero return"
583 "\n                             code if any file pairs yielded this status."
584 "\n    --help: display this info"
585 "\n    --listfilenames: list all filenames for each result type in stdout"
586 "\n    --match <substring>: compare files whose filenames contain this substring;"
587 "\n                         if unspecified, compare ALL files."
588 "\n                         this flag may be repeated."
589 "\n    --nocolorspace: Ignore color space of images."
590 "\n    --nodiffs: don't write out image diffs or index.html, just generate"
591 "\n               report on stdout"
592 "\n    --nomatch <substring>: regardless of --match, DO NOT compare files whose"
593 "\n                           filenames contain this substring."
594 "\n                           this flag may be repeated."
595 "\n    --noprintdirs: do not print the directories used."
596 "\n    --norecurse: do not recurse into subdirectories."
597 "\n    --sortbymaxmismatch: sort by worst color channel mismatch;"
598 "\n                         break ties with -sortbymismatch"
599 "\n    --sortbymismatch: sort by average color channel mismatch"
600 "\n    --threshold <n>: only report differences > n (per color channel) [default 0]"
601 "\n    --weighted: sort by # pixels different weighted by color difference"
602 "\n"
603 "\n    baseDir: directory to read baseline images from."
604 "\n    comparisonDir: directory to read comparison images from"
605 "\n    outputDir: directory to write difference images and index.html to;"
606 "\n               defaults to comparisonDir"
607 "\n"
608 "\nIf no sort is specified, it will sort by fraction of pixels mismatching."
609 "\n");
610 }
611 
612 const int kNoError = 0;
613 const int kGenericError = -1;
614 
main(int argc,char ** argv)615 int main(int argc, char** argv) {
616     DiffMetricProc diffProc = compute_diff_pmcolor;
617     int (*sortProc)(const void*, const void*) = compare<CompareDiffMetrics>;
618 
619     // Maximum error tolerated in any one color channel in any one pixel before
620     // a difference is reported.
621     int colorThreshold = 0;
622     SkString baseDir;
623     SkString comparisonDir;
624     SkString outputDir;
625 
626     StringArray matchSubstrings;
627     StringArray nomatchSubstrings;
628 
629     bool generateDiffs = true;
630     bool listFilenames = false;
631     bool printDirNames = true;
632     bool recurseIntoSubdirs = true;
633     bool verbose = false;
634     bool listFailingBase = false;
635     bool ignoreColorSpace = false;
636 
637     RecordArray differences;
638     DiffSummary summary;
639 
640     bool failOnResultType[DiffRecord::kResultCount];
641     for (int i = 0; i < DiffRecord::kResultCount; i++) {
642         failOnResultType[i] = false;
643     }
644 
645     bool failOnStatusType[DiffResource::kStatusCount][DiffResource::kStatusCount];
646     for (int base = 0; base < DiffResource::kStatusCount; ++base) {
647         for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
648             failOnStatusType[base][comparison] = false;
649         }
650     }
651 
652     int i;
653     int numUnflaggedArguments = 0;
654     for (i = 1; i < argc; i++) {
655         if (!strcmp(argv[i], "--failonresult")) {
656             if (argc == ++i) {
657                 SkDebugf("failonresult expects one argument.\n");
658                 continue;
659             }
660             DiffRecord::Result type = DiffRecord::getResultByName(argv[i]);
661             if (type != DiffRecord::kResultCount) {
662                 failOnResultType[type] = true;
663             } else {
664                 SkDebugf("ignoring unrecognized result <%s>\n", argv[i]);
665             }
666             continue;
667         }
668         if (!strcmp(argv[i], "--failonstatus")) {
669             if (argc == ++i) {
670                 SkDebugf("failonstatus missing base status.\n");
671                 continue;
672             }
673             bool baseStatuses[DiffResource::kStatusCount];
674             if (!DiffResource::getMatchingStatuses(argv[i], baseStatuses)) {
675                 SkDebugf("unrecognized base status <%s>\n", argv[i]);
676             }
677 
678             if (argc == ++i) {
679                 SkDebugf("failonstatus missing comparison status.\n");
680                 continue;
681             }
682             bool comparisonStatuses[DiffResource::kStatusCount];
683             if (!DiffResource::getMatchingStatuses(argv[i], comparisonStatuses)) {
684                 SkDebugf("unrecognized comarison status <%s>\n", argv[i]);
685             }
686 
687             for (int base = 0; base < DiffResource::kStatusCount; ++base) {
688                 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
689                     failOnStatusType[base][comparison] |=
690                         baseStatuses[base] && comparisonStatuses[comparison];
691                 }
692             }
693             continue;
694         }
695         if (!strcmp(argv[i], "--help")) {
696             usage(argv[0]);
697             return kNoError;
698         }
699         if (!strcmp(argv[i], "--listfilenames")) {
700             listFilenames = true;
701             continue;
702         }
703         if (!strcmp(argv[i], "--verbose")) {
704             verbose = true;
705             continue;
706         }
707         if (!strcmp(argv[i], "--match")) {
708             matchSubstrings.push_back(new SkString(argv[++i]));
709             continue;
710         }
711         if (!strcmp(argv[i], "--nocolorspace")) {
712             ignoreColorSpace = true;
713             continue;
714         }
715         if (!strcmp(argv[i], "--nodiffs")) {
716             generateDiffs = false;
717             continue;
718         }
719         if (!strcmp(argv[i], "--nomatch")) {
720             nomatchSubstrings.push_back(new SkString(argv[++i]));
721             continue;
722         }
723         if (!strcmp(argv[i], "--noprintdirs")) {
724             printDirNames = false;
725             continue;
726         }
727         if (!strcmp(argv[i], "--norecurse")) {
728             recurseIntoSubdirs = false;
729             continue;
730         }
731         if (!strcmp(argv[i], "--sortbymaxmismatch")) {
732             sortProc = compare<CompareDiffMaxMismatches>;
733             continue;
734         }
735         if (!strcmp(argv[i], "--sortbymismatch")) {
736             sortProc = compare<CompareDiffMeanMismatches>;
737             continue;
738         }
739         if (!strcmp(argv[i], "--threshold")) {
740             colorThreshold = atoi(argv[++i]);
741             continue;
742         }
743         if (!strcmp(argv[i], "--weighted")) {
744             sortProc = compare<CompareDiffWeighted>;
745             continue;
746         }
747         if (argv[i][0] != '-') {
748             switch (numUnflaggedArguments++) {
749                 case 0:
750                     baseDir.set(argv[i]);
751                     continue;
752                 case 1:
753                     comparisonDir.set(argv[i]);
754                     continue;
755                 case 2:
756                     outputDir.set(argv[i]);
757                     continue;
758                 default:
759                     SkDebugf("extra unflagged argument <%s>\n", argv[i]);
760                     usage(argv[0]);
761                     return kGenericError;
762             }
763         }
764         if (!strcmp(argv[i], "--listFailingBase")) {
765             listFailingBase = true;
766             continue;
767         }
768 
769         SkDebugf("Unrecognized argument <%s>\n", argv[i]);
770         usage(argv[0]);
771         return kGenericError;
772     }
773 
774     if (numUnflaggedArguments == 2) {
775         outputDir = comparisonDir;
776     } else if (numUnflaggedArguments != 3) {
777         usage(argv[0]);
778         return kGenericError;
779     }
780 
781     if (!baseDir.endsWith(PATH_DIV_STR)) {
782         baseDir.append(PATH_DIV_STR);
783     }
784     if (printDirNames) {
785         printf("baseDir is [%s]\n", baseDir.c_str());
786     }
787 
788     if (!comparisonDir.endsWith(PATH_DIV_STR)) {
789         comparisonDir.append(PATH_DIV_STR);
790     }
791     if (printDirNames) {
792         printf("comparisonDir is [%s]\n", comparisonDir.c_str());
793     }
794 
795     if (!outputDir.endsWith(PATH_DIV_STR)) {
796         outputDir.append(PATH_DIV_STR);
797     }
798     if (generateDiffs) {
799         if (printDirNames) {
800             printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
801         }
802     } else {
803         if (printDirNames) {
804             printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
805         }
806         outputDir.set("");
807     }
808 
809     // If no matchSubstrings were specified, match ALL strings
810     // (except for whatever nomatchSubstrings were specified, if any).
811     if (matchSubstrings.isEmpty()) {
812         matchSubstrings.push_back(new SkString(""));
813     }
814 
815     create_diff_images(diffProc, colorThreshold, ignoreColorSpace, &differences,
816                        baseDir, comparisonDir, outputDir,
817                        matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, generateDiffs,
818                        verbose, &summary);
819     summary.print(listFilenames, failOnResultType, failOnStatusType);
820 
821     if (listFailingBase) {
822         summary.printfFailingBaseNames("\n");
823     }
824 
825     if (differences.count()) {
826         qsort(differences.begin(), differences.count(),
827               sizeof(DiffRecord*), sortProc);
828     }
829 
830     if (generateDiffs) {
831         print_diff_page(summary.fNumMatches, colorThreshold, differences,
832                         baseDir, comparisonDir, outputDir);
833     }
834 
835     for (i = 0; i < differences.count(); i++) {
836         delete differences[i];
837     }
838     matchSubstrings.deleteAll();
839     nomatchSubstrings.deleteAll();
840 
841     int num_failing_results = 0;
842     for (int i = 0; i < DiffRecord::kResultCount; i++) {
843         if (failOnResultType[i]) {
844             num_failing_results += summary.fResultsOfType[i].count();
845         }
846     }
847     if (!failOnResultType[DiffRecord::kCouldNotCompare_Result]) {
848         for (int base = 0; base < DiffResource::kStatusCount; ++base) {
849             for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
850                 if (failOnStatusType[base][comparison]) {
851                     num_failing_results += summary.fStatusOfType[base][comparison].count();
852                 }
853             }
854         }
855     }
856 
857     // On Linux (and maybe other platforms too), any results outside of the
858     // range [0...255] are wrapped (mod 256).  Do the conversion ourselves, to
859     // make sure that we only return 0 when there were no failures.
860     return (num_failing_results > 255) ? 255 : num_failing_results;
861 }
862