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