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