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