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 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 { 65 DiffSummary () 66 : fNumMatches(0) 67 , fNumMismatches(0) 68 , fMaxMismatchV(0) 69 , fMaxMismatchPercent(0) { }; 70 71 ~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 92 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 107 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. 133 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 145 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 170 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 183 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. 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. 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. 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 318 static void release_file_list(FileArray *files) { 319 files->deleteAll(); 320 } 321 322 /// Comparison routines for qsort, sort by file names. 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: 329 AutoReleasePixels(DiffRecord* drp) 330 : fDrp(drp) { 331 SkASSERT(drp != NULL); 332 } 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 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 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. 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 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); 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 856 int main(int argc, char * const argv[]) { 857 return tool_main(argc, (char**) argv); 858 } 859 #endif 860