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