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