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