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