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 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 { 64 DiffSummary () 65 : fNumMatches(0) 66 , fNumMismatches(0) 67 , fMaxMismatchV(0) 68 , fMaxMismatchPercent(0) { } 69 70 ~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 91 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 106 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. 132 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 144 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 169 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 182 void add (DiffRecord* drp) { 183 uint32_t mismatchValue; 184 185 if (drp->fBase.fFilename.equals(drp->fComparison.fFilename)) { 186 fResultsOfType[drp->fResult].push_back(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_back(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_back( 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. 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. 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_back(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. 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 317 static void release_file_list(FileArray *files) { 318 files->deleteAll(); 319 } 320 321 /// Comparison routines for qsort, sort by file names. 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: 328 AutoReleasePixels(DiffRecord* drp) 329 : fDrp(drp) { 330 SkASSERT(drp != nullptr); 331 } 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 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, 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 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. 376 static void create_diff_images (DiffMetricProc dmp, 377 const int colorThreshold, 378 bool ignoreColorSpace, 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 if (!outputDir.isEmpty()) { 409 sk_mkdir(outputDir.c_str()); 410 } 411 412 int i = 0; 413 int j = 0; 414 415 while (i < baseFiles.count() && 416 j < comparisonFiles.count()) { 417 418 SkString basePath(baseDir); 419 SkString comparisonPath(comparisonDir); 420 421 DiffRecord *drp = new DiffRecord; 422 int v = strcmp(baseFiles[i]->c_str(), comparisonFiles[j]->c_str()); 423 424 if (v < 0) { 425 // in baseDir, but not in comparisonDir 426 drp->fResult = DiffRecord::kCouldNotCompare_Result; 427 428 basePath.append(*baseFiles[i]); 429 comparisonPath.append(*baseFiles[i]); 430 431 drp->fBase.fFilename = *baseFiles[i]; 432 drp->fBase.fFullPath = basePath; 433 drp->fBase.fStatus = DiffResource::kExists_Status; 434 435 drp->fComparison.fFilename = *baseFiles[i]; 436 drp->fComparison.fFullPath = comparisonPath; 437 drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status; 438 439 VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, baseFiles[i]); 440 441 ++i; 442 } else if (v > 0) { 443 // in comparisonDir, but not in baseDir 444 drp->fResult = DiffRecord::kCouldNotCompare_Result; 445 446 basePath.append(*comparisonFiles[j]); 447 comparisonPath.append(*comparisonFiles[j]); 448 449 drp->fBase.fFilename = *comparisonFiles[j]; 450 drp->fBase.fFullPath = basePath; 451 drp->fBase.fStatus = DiffResource::kDoesNotExist_Status; 452 453 drp->fComparison.fFilename = *comparisonFiles[j]; 454 drp->fComparison.fFullPath = comparisonPath; 455 drp->fComparison.fStatus = DiffResource::kExists_Status; 456 457 VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, comparisonFiles[j]); 458 459 ++j; 460 } else { 461 // Found the same filename in both baseDir and comparisonDir. 462 SkASSERT(DiffRecord::kUnknown_Result == drp->fResult); 463 464 basePath.append(*baseFiles[i]); 465 comparisonPath.append(*comparisonFiles[j]); 466 467 drp->fBase.fFilename = *baseFiles[i]; 468 drp->fBase.fFullPath = basePath; 469 drp->fBase.fStatus = DiffResource::kExists_Status; 470 471 drp->fComparison.fFilename = *comparisonFiles[j]; 472 drp->fComparison.fFullPath = comparisonPath; 473 drp->fComparison.fStatus = DiffResource::kExists_Status; 474 475 sk_sp<SkData> baseFileBits(read_file(drp->fBase.fFullPath.c_str())); 476 if (baseFileBits) { 477 drp->fBase.fStatus = DiffResource::kRead_Status; 478 } 479 sk_sp<SkData> comparisonFileBits(read_file(drp->fComparison.fFullPath.c_str())); 480 if (comparisonFileBits) { 481 drp->fComparison.fStatus = DiffResource::kRead_Status; 482 } 483 if (nullptr == baseFileBits || nullptr == comparisonFileBits) { 484 if (nullptr == baseFileBits) { 485 drp->fBase.fStatus = DiffResource::kCouldNotRead_Status; 486 VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, baseFiles[i]); 487 } 488 if (nullptr == comparisonFileBits) { 489 drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status; 490 VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, comparisonFiles[j]); 491 } 492 drp->fResult = DiffRecord::kCouldNotCompare_Result; 493 494 } else if (are_buffers_equal(baseFileBits.get(), comparisonFileBits.get())) { 495 drp->fResult = DiffRecord::kEqualBits_Result; 496 VERBOSE_STATUS("MATCH", ANSI_COLOR_GREEN, baseFiles[i]); 497 } else { 498 AutoReleasePixels arp(drp); 499 get_bitmap(baseFileBits, drp->fBase, false, ignoreColorSpace); 500 get_bitmap(comparisonFileBits, drp->fComparison, false, ignoreColorSpace); 501 VERBOSE_STATUS("DIFFERENT", ANSI_COLOR_RED, baseFiles[i]); 502 if (DiffResource::kDecoded_Status == drp->fBase.fStatus && 503 DiffResource::kDecoded_Status == drp->fComparison.fStatus) { 504 create_and_write_diff_image(drp, dmp, colorThreshold, 505 outputDir, drp->fBase.fFilename); 506 } else { 507 drp->fResult = DiffRecord::kCouldNotCompare_Result; 508 } 509 } 510 511 ++i; 512 ++j; 513 } 514 515 if (getBounds) { 516 get_bounds(*drp); 517 } 518 SkASSERT(DiffRecord::kUnknown_Result != drp->fResult); 519 differences->push_back(drp); 520 summary->add(drp); 521 } 522 523 for (; i < baseFiles.count(); ++i) { 524 // files only in baseDir 525 DiffRecord *drp = new DiffRecord(); 526 drp->fBase.fFilename = *baseFiles[i]; 527 drp->fBase.fFullPath = baseDir; 528 drp->fBase.fFullPath.append(drp->fBase.fFilename); 529 drp->fBase.fStatus = DiffResource::kExists_Status; 530 531 drp->fComparison.fFilename = *baseFiles[i]; 532 drp->fComparison.fFullPath = comparisonDir; 533 drp->fComparison.fFullPath.append(drp->fComparison.fFilename); 534 drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status; 535 536 drp->fResult = DiffRecord::kCouldNotCompare_Result; 537 if (getBounds) { 538 get_bounds(*drp); 539 } 540 differences->push_back(drp); 541 summary->add(drp); 542 } 543 544 for (; j < comparisonFiles.count(); ++j) { 545 // files only in comparisonDir 546 DiffRecord *drp = new DiffRecord(); 547 drp->fBase.fFilename = *comparisonFiles[j]; 548 drp->fBase.fFullPath = baseDir; 549 drp->fBase.fFullPath.append(drp->fBase.fFilename); 550 drp->fBase.fStatus = DiffResource::kDoesNotExist_Status; 551 552 drp->fComparison.fFilename = *comparisonFiles[j]; 553 drp->fComparison.fFullPath = comparisonDir; 554 drp->fComparison.fFullPath.append(drp->fComparison.fFilename); 555 drp->fComparison.fStatus = DiffResource::kExists_Status; 556 557 drp->fResult = DiffRecord::kCouldNotCompare_Result; 558 if (getBounds) { 559 get_bounds(*drp); 560 } 561 differences->push_back(drp); 562 summary->add(drp); 563 } 564 565 release_file_list(&baseFiles); 566 release_file_list(&comparisonFiles); 567 } 568 569 static void usage (char * argv0) { 570 SkDebugf("Skia baseline image diff tool\n"); 571 SkDebugf("\n" 572 "Usage: \n" 573 " %s <baseDir> <comparisonDir> [outputDir] \n", argv0); 574 SkDebugf( 575 "\nArguments:" 576 "\n --failonresult <result>: After comparing all file pairs, exit with nonzero" 577 "\n return code (number of file pairs yielding this" 578 "\n result) if any file pairs yielded this result." 579 "\n This flag may be repeated, in which case the" 580 "\n return code will be the number of fail pairs" 581 "\n yielding ANY of these results." 582 "\n --failonstatus <baseStatus> <comparisonStatus>: exit with nonzero return" 583 "\n code if any file pairs yielded this status." 584 "\n --help: display this info" 585 "\n --listfilenames: list all filenames for each result type in stdout" 586 "\n --match <substring>: compare files whose filenames contain this substring;" 587 "\n if unspecified, compare ALL files." 588 "\n this flag may be repeated." 589 "\n --nocolorspace: Ignore color space of images." 590 "\n --nodiffs: don't write out image diffs or index.html, just generate" 591 "\n report on stdout" 592 "\n --nomatch <substring>: regardless of --match, DO NOT compare files whose" 593 "\n filenames contain this substring." 594 "\n this flag may be repeated." 595 "\n --noprintdirs: do not print the directories used." 596 "\n --norecurse: do not recurse into subdirectories." 597 "\n --sortbymaxmismatch: sort by worst color channel mismatch;" 598 "\n break ties with -sortbymismatch" 599 "\n --sortbymismatch: sort by average color channel mismatch" 600 "\n --threshold <n>: only report differences > n (per color channel) [default 0]" 601 "\n --weighted: sort by # pixels different weighted by color difference" 602 "\n" 603 "\n baseDir: directory to read baseline images from." 604 "\n comparisonDir: directory to read comparison images from" 605 "\n outputDir: directory to write difference images and index.html to;" 606 "\n defaults to comparisonDir" 607 "\n" 608 "\nIf no sort is specified, it will sort by fraction of pixels mismatching." 609 "\n"); 610 } 611 612 const int kNoError = 0; 613 const int kGenericError = -1; 614 615 int main(int argc, char** argv) { 616 DiffMetricProc diffProc = compute_diff_pmcolor; 617 int (*sortProc)(const void*, const void*) = compare<CompareDiffMetrics>; 618 619 // Maximum error tolerated in any one color channel in any one pixel before 620 // a difference is reported. 621 int colorThreshold = 0; 622 SkString baseDir; 623 SkString comparisonDir; 624 SkString outputDir; 625 626 StringArray matchSubstrings; 627 StringArray nomatchSubstrings; 628 629 bool generateDiffs = true; 630 bool listFilenames = false; 631 bool printDirNames = true; 632 bool recurseIntoSubdirs = true; 633 bool verbose = false; 634 bool listFailingBase = false; 635 bool ignoreColorSpace = false; 636 637 RecordArray differences; 638 DiffSummary summary; 639 640 bool failOnResultType[DiffRecord::kResultCount]; 641 for (int i = 0; i < DiffRecord::kResultCount; i++) { 642 failOnResultType[i] = false; 643 } 644 645 bool failOnStatusType[DiffResource::kStatusCount][DiffResource::kStatusCount]; 646 for (int base = 0; base < DiffResource::kStatusCount; ++base) { 647 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) { 648 failOnStatusType[base][comparison] = false; 649 } 650 } 651 652 int i; 653 int numUnflaggedArguments = 0; 654 for (i = 1; i < argc; i++) { 655 if (!strcmp(argv[i], "--failonresult")) { 656 if (argc == ++i) { 657 SkDebugf("failonresult expects one argument.\n"); 658 continue; 659 } 660 DiffRecord::Result type = DiffRecord::getResultByName(argv[i]); 661 if (type != DiffRecord::kResultCount) { 662 failOnResultType[type] = true; 663 } else { 664 SkDebugf("ignoring unrecognized result <%s>\n", argv[i]); 665 } 666 continue; 667 } 668 if (!strcmp(argv[i], "--failonstatus")) { 669 if (argc == ++i) { 670 SkDebugf("failonstatus missing base status.\n"); 671 continue; 672 } 673 bool baseStatuses[DiffResource::kStatusCount]; 674 if (!DiffResource::getMatchingStatuses(argv[i], baseStatuses)) { 675 SkDebugf("unrecognized base status <%s>\n", argv[i]); 676 } 677 678 if (argc == ++i) { 679 SkDebugf("failonstatus missing comparison status.\n"); 680 continue; 681 } 682 bool comparisonStatuses[DiffResource::kStatusCount]; 683 if (!DiffResource::getMatchingStatuses(argv[i], comparisonStatuses)) { 684 SkDebugf("unrecognized comarison status <%s>\n", argv[i]); 685 } 686 687 for (int base = 0; base < DiffResource::kStatusCount; ++base) { 688 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) { 689 failOnStatusType[base][comparison] |= 690 baseStatuses[base] && comparisonStatuses[comparison]; 691 } 692 } 693 continue; 694 } 695 if (!strcmp(argv[i], "--help")) { 696 usage(argv[0]); 697 return kNoError; 698 } 699 if (!strcmp(argv[i], "--listfilenames")) { 700 listFilenames = true; 701 continue; 702 } 703 if (!strcmp(argv[i], "--verbose")) { 704 verbose = true; 705 continue; 706 } 707 if (!strcmp(argv[i], "--match")) { 708 matchSubstrings.push_back(new SkString(argv[++i])); 709 continue; 710 } 711 if (!strcmp(argv[i], "--nocolorspace")) { 712 ignoreColorSpace = true; 713 continue; 714 } 715 if (!strcmp(argv[i], "--nodiffs")) { 716 generateDiffs = false; 717 continue; 718 } 719 if (!strcmp(argv[i], "--nomatch")) { 720 nomatchSubstrings.push_back(new SkString(argv[++i])); 721 continue; 722 } 723 if (!strcmp(argv[i], "--noprintdirs")) { 724 printDirNames = false; 725 continue; 726 } 727 if (!strcmp(argv[i], "--norecurse")) { 728 recurseIntoSubdirs = false; 729 continue; 730 } 731 if (!strcmp(argv[i], "--sortbymaxmismatch")) { 732 sortProc = compare<CompareDiffMaxMismatches>; 733 continue; 734 } 735 if (!strcmp(argv[i], "--sortbymismatch")) { 736 sortProc = compare<CompareDiffMeanMismatches>; 737 continue; 738 } 739 if (!strcmp(argv[i], "--threshold")) { 740 colorThreshold = atoi(argv[++i]); 741 continue; 742 } 743 if (!strcmp(argv[i], "--weighted")) { 744 sortProc = compare<CompareDiffWeighted>; 745 continue; 746 } 747 if (argv[i][0] != '-') { 748 switch (numUnflaggedArguments++) { 749 case 0: 750 baseDir.set(argv[i]); 751 continue; 752 case 1: 753 comparisonDir.set(argv[i]); 754 continue; 755 case 2: 756 outputDir.set(argv[i]); 757 continue; 758 default: 759 SkDebugf("extra unflagged argument <%s>\n", argv[i]); 760 usage(argv[0]); 761 return kGenericError; 762 } 763 } 764 if (!strcmp(argv[i], "--listFailingBase")) { 765 listFailingBase = true; 766 continue; 767 } 768 769 SkDebugf("Unrecognized argument <%s>\n", argv[i]); 770 usage(argv[0]); 771 return kGenericError; 772 } 773 774 if (numUnflaggedArguments == 2) { 775 outputDir = comparisonDir; 776 } else if (numUnflaggedArguments != 3) { 777 usage(argv[0]); 778 return kGenericError; 779 } 780 781 if (!baseDir.endsWith(PATH_DIV_STR)) { 782 baseDir.append(PATH_DIV_STR); 783 } 784 if (printDirNames) { 785 printf("baseDir is [%s]\n", baseDir.c_str()); 786 } 787 788 if (!comparisonDir.endsWith(PATH_DIV_STR)) { 789 comparisonDir.append(PATH_DIV_STR); 790 } 791 if (printDirNames) { 792 printf("comparisonDir is [%s]\n", comparisonDir.c_str()); 793 } 794 795 if (!outputDir.endsWith(PATH_DIV_STR)) { 796 outputDir.append(PATH_DIV_STR); 797 } 798 if (generateDiffs) { 799 if (printDirNames) { 800 printf("writing diffs to outputDir is [%s]\n", outputDir.c_str()); 801 } 802 } else { 803 if (printDirNames) { 804 printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str()); 805 } 806 outputDir.set(""); 807 } 808 809 // If no matchSubstrings were specified, match ALL strings 810 // (except for whatever nomatchSubstrings were specified, if any). 811 if (matchSubstrings.isEmpty()) { 812 matchSubstrings.push_back(new SkString("")); 813 } 814 815 create_diff_images(diffProc, colorThreshold, ignoreColorSpace, &differences, 816 baseDir, comparisonDir, outputDir, 817 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, generateDiffs, 818 verbose, &summary); 819 summary.print(listFilenames, failOnResultType, failOnStatusType); 820 821 if (listFailingBase) { 822 summary.printfFailingBaseNames("\n"); 823 } 824 825 if (differences.count()) { 826 qsort(differences.begin(), differences.count(), 827 sizeof(DiffRecord*), sortProc); 828 } 829 830 if (generateDiffs) { 831 print_diff_page(summary.fNumMatches, colorThreshold, differences, 832 baseDir, comparisonDir, outputDir); 833 } 834 835 for (i = 0; i < differences.count(); i++) { 836 delete differences[i]; 837 } 838 matchSubstrings.deleteAll(); 839 nomatchSubstrings.deleteAll(); 840 841 int num_failing_results = 0; 842 for (int i = 0; i < DiffRecord::kResultCount; i++) { 843 if (failOnResultType[i]) { 844 num_failing_results += summary.fResultsOfType[i].count(); 845 } 846 } 847 if (!failOnResultType[DiffRecord::kCouldNotCompare_Result]) { 848 for (int base = 0; base < DiffResource::kStatusCount; ++base) { 849 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) { 850 if (failOnStatusType[base][comparison]) { 851 num_failing_results += summary.fStatusOfType[base][comparison].count(); 852 } 853 } 854 } 855 } 856 857 // On Linux (and maybe other platforms too), any results outside of the 858 // range [0...255] are wrapped (mod 256). Do the conversion ourselves, to 859 // make sure that we only return 0 when there were no failures. 860 return (num_failing_results > 255) ? 255 : num_failing_results; 861 } 862