3 * Copyright 2011 Google Inc.
5 * Use of this source code is governed by a BSD-style license that can be
6 * found in the LICENSE file.
8 #include "SkColorPriv.h"
10 #include "SkImageDecoder.h"
11 #include "SkImageEncoder.h"
14 #include "SkTDArray.h"
15 #include "SkTemplates.h"
17 #include "SkTSearch.h"
23 * Given three directory names, expects to find identically-named files in
24 * each of the first two; the first are treated as a set of baseline,
25 * the second a set of variant images, and a diff image is written into the
26 * third directory for each pair.
27 * Creates an index.html in the current third directory to compare each
28 * pair that does not match exactly.
29 * Recursively descends directories, unless run with --norecurse.
31 * Returns zero exit code if all images match across baseDir and comparisonDir.
34 #if SK_BUILD_FOR_WIN32
35 #define PATH_DIV_STR "\\"
36 #define PATH_DIV_CHAR '\\'
38 #define PATH_DIV_STR "/"
39 #define PATH_DIV_CHAR '/'
42 // Result of comparison for each pair of files.
43 // Listed from "better" to "worse", for sorting of results.
54 kNumResultTypes // NOT A VALID VALUE--used to set up arrays. Must be last.
57 // Returns the Result with this name.
58 // If there is no Result with this name, returns kNumResultTypes.
59 // TODO: Is there a better return value for the fall-through case?
60 static Result getResultByName(const char *name) {
61 if (0 == strcmp("EqualBits", name)) {
64 if (0 == strcmp("EqualPixels", name)) {
67 if (0 == strcmp("DifferentPixels", name)) {
68 return kDifferentPixels;
70 if (0 == strcmp("DifferentSizes", name)) {
71 return kDifferentSizes;
73 if (0 == strcmp("DifferentOther", name)) {
74 return kDifferentOther;
76 if (0 == strcmp("ComparisonMissing", name)) {
77 return kComparisonMissing;
79 if (0 == strcmp("BaseMissing", name)) {
82 if (0 == strcmp("Unknown", name)) {
85 return kNumResultTypes;
88 // Returns a text description of the given Result type.
89 static const char *getResultDescription(Result result) {
92 return "contain exactly the same bits";
94 return "contain the same pixel values, but not the same bits";
95 case kDifferentPixels:
96 return "have identical dimensions but some differing pixels";
98 return "have differing dimensions";
100 return "contain different bits and are not parsable images";
102 return "missing from baseDir";
103 case kComparisonMissing:
104 return "missing from comparisonDir";
106 return "not compared yet";
113 DiffRecord (const SkString filename,
114 const SkString basePath,
115 const SkString comparisonPath,
116 const Result result = kUnknown)
117 : fFilename (filename)
118 , fBasePath (basePath)
119 , fComparisonPath (comparisonPath)
120 , fBaseBitmap (new SkBitmap ())
121 , fComparisonBitmap (new SkBitmap ())
122 , fDifferenceBitmap (new SkBitmap ())
123 , fWhiteBitmap (new SkBitmap ())
126 , fFractionDifference (0)
127 , fWeightedFraction (0)
128 , fAverageMismatchR (0)
129 , fAverageMismatchG (0)
130 , fAverageMismatchB (0)
139 SkString fComparisonPath;
141 SkBitmap* fBaseBitmap;
142 SkBitmap* fComparisonBitmap;
143 SkBitmap* fDifferenceBitmap;
144 SkBitmap* fWhiteBitmap;
149 /// Arbitrary floating-point metric to be used to sort images from most
150 /// to least different from baseline; values of 0 will be omitted from the
152 float fFractionDifference;
153 float fWeightedFraction;
155 float fAverageMismatchR;
156 float fAverageMismatchG;
157 float fAverageMismatchB;
159 uint32_t fMaxMismatchR;
160 uint32_t fMaxMismatchG;
161 uint32_t fMaxMismatchB;
163 /// Which category of diff result.
167 #define MAX2(a,b) (((b) < (a)) ? (a) : (b))
168 #define MAX3(a,b,c) (((b) < (a)) ? MAX2((a), (c)) : MAX2((b), (c)))
170 const SkPMColor PMCOLOR_WHITE = SkPreMultiplyColor(SK_ColorWHITE);
171 const SkPMColor PMCOLOR_BLACK = SkPreMultiplyColor(SK_ColorBLACK);
173 typedef SkTDArray<SkString*> StringArray;
174 typedef StringArray FileArray;
181 , fMaxMismatchPercent (0) { };
184 for (int i = 0; i < kNumResultTypes; i++) {
185 fResultsOfType[i].deleteAll();
189 uint32_t fNumMatches;
190 uint32_t fNumMismatches;
191 uint32_t fMaxMismatchV;
192 float fMaxMismatchPercent;
194 FileArray fResultsOfType[kNumResultTypes];
196 // Print a line about the contents of this FileArray to stdout.
197 void printContents(const FileArray& fileArray, const char* headerText, bool listFilenames) {
198 int n = fileArray.count();
199 printf("%d file pairs %s", n, headerText);
202 for (int i = 0; i < n; ++i) {
203 printf("%s ", fileArray[i]->c_str());
209 void print(bool listFilenames, bool failOnResultType[kNumResultTypes]) {
210 printf("\ncompared %d file pairs:\n", fNumMatches + fNumMismatches);
211 for (int resultInt = 0; resultInt < kNumResultTypes; resultInt++) {
212 Result result = static_cast<Result>(resultInt);
213 if (failOnResultType[result]) {
218 printContents(fResultsOfType[result], getResultDescription(result), listFilenames);
220 printf("(results marked with [*] will cause nonzero return value)\n");
221 printf("\nnumber of mismatching file pairs: %d\n", fNumMismatches);
222 if (fNumMismatches > 0) {
223 printf("Maximum pixel intensity mismatch %d\n", fMaxMismatchV);
224 printf("Largest area mismatch was %.2f%% of pixels\n",fMaxMismatchPercent);
228 void add (DiffRecord* drp) {
229 uint32_t mismatchValue;
231 fResultsOfType[drp->fResult].push(new SkString(drp->fFilename));
232 switch (drp->fResult) {
239 case kDifferentSizes:
242 case kDifferentPixels:
244 if (drp->fFractionDifference * 100 > fMaxMismatchPercent) {
245 fMaxMismatchPercent = drp->fFractionDifference * 100;
247 mismatchValue = MAX3(drp->fMaxMismatchR, drp->fMaxMismatchG,
249 if (mismatchValue > fMaxMismatchV) {
250 fMaxMismatchV = mismatchValue;
253 case kDifferentOther:
259 case kComparisonMissing:
263 SkDEBUGFAIL("adding uncategorized DiffRecord");
266 SkDEBUGFAIL("adding DiffRecord with unhandled fResult value");
272 typedef SkTDArray<DiffRecord*> RecordArray;
274 /// A wrapper for any sortProc (comparison routine) which applies a first-order
275 /// sort beforehand, and a tiebreaker if the sortProc returns 0.
277 static int compare(const void* untyped_lhs, const void* untyped_rhs) {
278 const DiffRecord* lhs = *reinterpret_cast<DiffRecord* const*>(untyped_lhs);
279 const DiffRecord* rhs = *reinterpret_cast<DiffRecord* const*>(untyped_rhs);
281 // First-order sort... these comparisons should be applied before comparing
282 // pixel values, no matter what.
283 if (lhs->fResult != rhs->fResult) {
284 return (lhs->fResult < rhs->fResult) ? 1 : -1;
287 // Passed first-order sort, so call the pixel comparison routine.
288 int result = T::comparePixels(lhs, rhs);
293 // Tiebreaker... if we got to this point, we don't really care
294 // which order they are sorted in, but let's at least be consistent.
295 return strcmp(lhs->fFilename.c_str(), rhs->fFilename.c_str());
298 /// Comparison routine for qsort; sorts by fFractionDifference
299 /// from largest to smallest.
300 class CompareDiffMetrics {
302 static int comparePixels(const DiffRecord* lhs, const DiffRecord* rhs) {
303 if (lhs->fFractionDifference < rhs->fFractionDifference) {
306 if (rhs->fFractionDifference < lhs->fFractionDifference) {
313 class CompareDiffWeighted {
315 static int comparePixels(const DiffRecord* lhs, const DiffRecord* rhs) {
316 if (lhs->fWeightedFraction < rhs->fWeightedFraction) {
319 if (lhs->fWeightedFraction > rhs->fWeightedFraction) {
326 /// Comparison routine for qsort; sorts by max(fAverageMismatch{RGB})
327 /// from largest to smallest.
328 class CompareDiffMeanMismatches {
330 static int comparePixels(const DiffRecord* lhs, const DiffRecord* rhs) {
331 float leftValue = MAX3(lhs->fAverageMismatchR,
332 lhs->fAverageMismatchG,
333 lhs->fAverageMismatchB);
334 float rightValue = MAX3(rhs->fAverageMismatchR,
335 rhs->fAverageMismatchG,
336 rhs->fAverageMismatchB);
337 if (leftValue < rightValue) {
340 if (rightValue < leftValue) {
347 /// Comparison routine for qsort; sorts by max(fMaxMismatch{RGB})
348 /// from largest to smallest.
349 class CompareDiffMaxMismatches {
351 static int comparePixels(const DiffRecord* lhs, const DiffRecord* rhs) {
352 uint32_t leftValue = MAX3(lhs->fMaxMismatchR,
355 uint32_t rightValue = MAX3(rhs->fMaxMismatchR,
358 if (leftValue < rightValue) {
361 if (rightValue < leftValue) {
365 return CompareDiffMeanMismatches::comparePixels(lhs, rhs);
371 /// Parameterized routine to compute the color of a pixel in a difference image.
372 typedef SkPMColor (*DiffMetricProc)(SkPMColor, SkPMColor);
375 static void expand_and_copy (int width, int height, SkBitmap** dest) {
376 SkBitmap* temp = new SkBitmap ();
378 temp->setConfig((*dest)->config(), width, height);
380 (*dest)->copyPixelsTo(temp->getPixels(), temp->getSize(),
386 /// Returns true if the two buffers passed in are both non-NULL, and include
387 /// exactly the same byte values (and identical lengths).
388 static bool are_buffers_equal(SkData* skdata1, SkData* skdata2) {
389 if ((NULL == skdata1) || (NULL == skdata2)) {
392 if (skdata1->size() != skdata2->size()) {
395 return (0 == memcmp(skdata1->data(), skdata2->data(), skdata1->size()));
398 /// Reads the file at the given path and returns its complete contents as an
399 /// SkData object (or returns NULL on error).
400 static SkData* read_file(const char* file_path) {
401 SkFILEStream fileStream(file_path);
402 if (!fileStream.isValid()) {
403 SkDebugf("WARNING: could not open file <%s> for reading\n", file_path);
406 size_t bytesInFile = fileStream.getLength();
407 size_t bytesLeftToRead = bytesInFile;
409 void* bufferStart = sk_malloc_throw(bytesInFile);
410 char* bufferPointer = (char*)bufferStart;
411 while (bytesLeftToRead > 0) {
412 size_t bytesReadThisTime = fileStream.read(
413 bufferPointer, bytesLeftToRead);
414 if (0 == bytesReadThisTime) {
415 SkDebugf("WARNING: error reading from <%s>\n", file_path);
416 sk_free(bufferStart);
419 bytesLeftToRead -= bytesReadThisTime;
420 bufferPointer += bytesReadThisTime;
422 return SkData::NewFromMalloc(bufferStart, bytesInFile);
425 /// Decodes binary contents of baseFile and comparisonFile into
426 /// diffRecord->fBaseBitmap and diffRecord->fComparisonBitmap.
427 /// Returns true if that succeeds.
428 static bool get_bitmaps (SkData* baseFileContents,
429 SkData* comparisonFileContents,
430 DiffRecord* diffRecord) {
431 SkMemoryStream compareStream(comparisonFileContents->data(),
432 comparisonFileContents->size());
433 SkMemoryStream baseStream(baseFileContents->data(),
434 baseFileContents->size());
436 SkImageDecoder* codec = SkImageDecoder::Factory(&baseStream);
438 SkDebugf("ERROR: no codec found for basePath <%s>\n",
439 diffRecord->fBasePath.c_str());
443 // In debug, the DLL will automatically be unloaded when this is deleted,
444 // but that shouldn't be a problem in release mode.
445 SkAutoTDelete<SkImageDecoder> ad(codec);
448 if (!codec->decode(&baseStream, diffRecord->fBaseBitmap,
449 SkBitmap::kARGB_8888_Config,
450 SkImageDecoder::kDecodePixels_Mode)) {
451 SkDebugf("ERROR: codec failed for basePath <%s>\n",
452 diffRecord->fBasePath.c_str());
456 diffRecord->fBaseWidth = diffRecord->fBaseBitmap->width();
457 diffRecord->fBaseHeight = diffRecord->fBaseBitmap->height();
459 if (!codec->decode(&compareStream, diffRecord->fComparisonBitmap,
460 SkBitmap::kARGB_8888_Config,
461 SkImageDecoder::kDecodePixels_Mode)) {
462 SkDebugf("ERROR: codec failed for comparisonPath <%s>\n",
463 diffRecord->fComparisonPath.c_str());
470 static bool get_bitmap_height_width(const SkString& path,
471 int *height, int *width) {
472 SkFILEStream stream(path.c_str());
473 if (!stream.isValid()) {
474 SkDebugf("ERROR: couldn't open file <%s>\n",
479 SkImageDecoder* codec = SkImageDecoder::Factory(&stream);
481 SkDebugf("ERROR: no codec found for <%s>\n",
486 SkAutoTDelete<SkImageDecoder> ad(codec);
490 if (!codec->decode(&stream, &bm,
491 SkBitmap::kARGB_8888_Config,
492 SkImageDecoder::kDecodePixels_Mode)) {
493 SkDebugf("ERROR: codec failed for <%s>\n",
498 *height = bm.height();
504 // from gm - thanks to PNG, we need to force all pixels 100% opaque
505 static void force_all_opaque(const SkBitmap& bitmap) {
506 SkAutoLockPixels lock(bitmap);
507 for (int y = 0; y < bitmap.height(); y++) {
508 for (int x = 0; x < bitmap.width(); x++) {
509 *bitmap.getAddr32(x, y) |= (SK_A32_MASK << SK_A32_SHIFT);
515 static bool write_bitmap(const SkString& path, const SkBitmap* bitmap) {
517 bitmap->copyTo(©, SkBitmap::kARGB_8888_Config);
518 force_all_opaque(copy);
519 return SkImageEncoder::EncodeFile(path.c_str(), copy,
520 SkImageEncoder::kPNG_Type, 100);
524 static inline SkPMColor compute_diff_pmcolor(SkPMColor c0, SkPMColor c1) {
525 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
526 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
527 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
529 return SkPackARGB32(0xFF, SkAbs32(dr), SkAbs32(dg), SkAbs32(db));
532 static inline bool colors_match_thresholded(SkPMColor c0, SkPMColor c1,
533 const int threshold) {
534 int da = SkGetPackedA32(c0) - SkGetPackedA32(c1);
535 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
536 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
537 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
539 return ((SkAbs32(da) <= threshold) &&
540 (SkAbs32(dr) <= threshold) &&
541 (SkAbs32(dg) <= threshold) &&
542 (SkAbs32(db) <= threshold));
546 // Postcondition: when we exit this method, dr->fResult should have some value
547 // other than kUnknown.
548 static void compute_diff(DiffRecord* dr,
549 DiffMetricProc diffFunction,
550 const int colorThreshold) {
551 SkAutoLockPixels alpDiff(*dr->fDifferenceBitmap);
552 SkAutoLockPixels alpWhite(*dr->fWhiteBitmap);
554 const int w = dr->fComparisonBitmap->width();
555 const int h = dr->fComparisonBitmap->height();
556 int mismatchedPixels = 0;
557 int totalMismatchR = 0;
558 int totalMismatchG = 0;
559 int totalMismatchB = 0;
561 if (w != dr->fBaseWidth || h != dr->fBaseHeight) {
562 dr->fResult = kDifferentSizes;
565 // Accumulate fractionally different pixels, then divide out
566 // # of pixels at the end.
567 dr->fWeightedFraction = 0;
568 for (int y = 0; y < h; y++) {
569 for (int x = 0; x < w; x++) {
570 SkPMColor c0 = *dr->fBaseBitmap->getAddr32(x, y);
571 SkPMColor c1 = *dr->fComparisonBitmap->getAddr32(x, y);
572 SkPMColor trueDifference = compute_diff_pmcolor(c0, c1);
573 SkPMColor outputDifference = diffFunction(c0, c1);
574 uint32_t thisR = SkGetPackedR32(trueDifference);
575 uint32_t thisG = SkGetPackedG32(trueDifference);
576 uint32_t thisB = SkGetPackedB32(trueDifference);
577 totalMismatchR += thisR;
578 totalMismatchG += thisG;
579 totalMismatchB += thisB;
580 // In HSV, value is defined as max RGB component.
581 int value = MAX3(thisR, thisG, thisB);
582 dr->fWeightedFraction += ((float) value) / 255;
583 if (thisR > dr->fMaxMismatchR) {
584 dr->fMaxMismatchR = thisR;
586 if (thisG > dr->fMaxMismatchG) {
587 dr->fMaxMismatchG = thisG;
589 if (thisB > dr->fMaxMismatchB) {
590 dr->fMaxMismatchB = thisB;
592 if (!colors_match_thresholded(c0, c1, colorThreshold)) {
594 *dr->fDifferenceBitmap->getAddr32(x, y) = outputDifference;
595 *dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_WHITE;
597 *dr->fDifferenceBitmap->getAddr32(x, y) = 0;
598 *dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_BLACK;
602 if (0 == mismatchedPixels) {
603 dr->fResult = kEqualPixels;
606 dr->fResult = kDifferentPixels;
607 int pixelCount = w * h;
608 dr->fFractionDifference = ((float) mismatchedPixels) / pixelCount;
609 dr->fWeightedFraction /= pixelCount;
610 dr->fAverageMismatchR = ((float) totalMismatchR) / pixelCount;
611 dr->fAverageMismatchG = ((float) totalMismatchG) / pixelCount;
612 dr->fAverageMismatchB = ((float) totalMismatchB) / pixelCount;
615 /// Return a copy of the "input" string, within which we have replaced all instances
616 /// of oldSubstring with newSubstring.
618 /// TODO: If we like this, we should move it into the core SkString implementation,
619 /// adding more checks and ample test cases, and paying more attention to efficiency.
620 static SkString replace_all(const SkString &input,
621 const char oldSubstring[], const char newSubstring[]) {
623 const char *input_cstr = input.c_str();
624 const char *first_char = input_cstr;
625 const char *match_char;
626 int oldSubstringLen = strlen(oldSubstring);
627 while (NULL != (match_char = strstr(first_char, oldSubstring))) {
628 output.append(first_char, (match_char - first_char));
629 output.append(newSubstring);
630 first_char = match_char + oldSubstringLen;
632 output.append(first_char);
636 static SkString filename_to_derived_filename (const SkString& filename,
637 const char *suffix) {
638 SkString diffName (filename);
639 const char* cstring = diffName.c_str();
640 int dotOffset = strrchr(cstring, '.') - cstring;
641 diffName.remove(dotOffset, diffName.size() - dotOffset);
642 diffName.append(suffix);
644 // In case we recursed into subdirectories, replace slashes with something else
645 // so the diffs will all be written into a single flat directory.
646 diffName = replace_all(diffName, PATH_DIV_STR, "_");
650 /// Given a image filename, returns the name of the file containing the
651 /// associated difference image.
652 static SkString filename_to_diff_filename (const SkString& filename) {
653 return filename_to_derived_filename(filename, "-diff.png");
656 /// Given a image filename, returns the name of the file containing the
657 /// "white" difference image.
658 static SkString filename_to_white_filename (const SkString& filename) {
659 return filename_to_derived_filename(filename, "-white.png");
662 static void release_bitmaps(DiffRecord* drp) {
663 delete drp->fBaseBitmap;
664 drp->fBaseBitmap = NULL;
665 delete drp->fComparisonBitmap;
666 drp->fComparisonBitmap = NULL;
667 delete drp->fDifferenceBitmap;
668 drp->fDifferenceBitmap = NULL;
669 delete drp->fWhiteBitmap;
670 drp->fWhiteBitmap = NULL;
674 /// If outputDir.isEmpty(), don't write out diff files.
675 static void create_and_write_diff_image(DiffRecord* drp,
677 const int colorThreshold,
678 const SkString& outputDir,
679 const SkString& filename) {
680 const int w = drp->fBaseWidth;
681 const int h = drp->fBaseHeight;
682 drp->fDifferenceBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h);
683 drp->fDifferenceBitmap->allocPixels();
684 drp->fWhiteBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h);
685 drp->fWhiteBitmap->allocPixels();
687 SkASSERT(kUnknown == drp->fResult);
688 compute_diff(drp, dmp, colorThreshold);
689 SkASSERT(kUnknown != drp->fResult);
691 if ((kDifferentPixels == drp->fResult) && !outputDir.isEmpty()) {
692 SkString differencePath (outputDir);
693 differencePath.append(filename_to_diff_filename(filename));
694 write_bitmap(differencePath, drp->fDifferenceBitmap);
695 SkString whitePath (outputDir);
696 whitePath.append(filename_to_white_filename(filename));
697 write_bitmap(whitePath, drp->fWhiteBitmap);
700 release_bitmaps(drp);
703 /// Returns true if string contains any of these substrings.
704 static bool string_contains_any_of(const SkString& string,
705 const StringArray& substrings) {
706 for (int i = 0; i < substrings.count(); i++) {
707 if (string.contains(substrings[i]->c_str())) {
714 /// Internal (potentially recursive) implementation of get_file_list.
715 static void get_file_list_subdir(const SkString& rootDir, const SkString& subDir,
716 const StringArray& matchSubstrings,
717 const StringArray& nomatchSubstrings,
718 bool recurseIntoSubdirs, FileArray *files) {
719 bool isSubDirEmpty = subDir.isEmpty();
720 SkString dir(rootDir);
721 if (!isSubDirEmpty) {
722 dir.append(PATH_DIV_STR);
726 // Iterate over files (not directories) within dir.
727 SkOSFile::Iter fileIterator(dir.c_str());
729 while (fileIterator.next(&fileName, false)) {
730 if (fileName.startsWith(".")) {
733 SkString pathRelativeToRootDir(subDir);
734 if (!isSubDirEmpty) {
735 pathRelativeToRootDir.append(PATH_DIV_STR);
737 pathRelativeToRootDir.append(fileName);
738 if (string_contains_any_of(pathRelativeToRootDir, matchSubstrings) &&
739 !string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) {
740 files->push(new SkString(pathRelativeToRootDir));
744 // Recurse into any non-ignored subdirectories.
745 if (recurseIntoSubdirs) {
746 SkOSFile::Iter dirIterator(dir.c_str());
748 while (dirIterator.next(&dirName, true)) {
749 if (dirName.startsWith(".")) {
752 SkString pathRelativeToRootDir(subDir);
753 if (!isSubDirEmpty) {
754 pathRelativeToRootDir.append(PATH_DIV_STR);
756 pathRelativeToRootDir.append(dirName);
757 if (!string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) {
758 get_file_list_subdir(rootDir, pathRelativeToRootDir,
759 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
766 /// Iterate over dir and get all files whose filename:
767 /// - matches any of the substrings in matchSubstrings, but...
768 /// - DOES NOT match any of the substrings in nomatchSubstrings
769 /// - DOES NOT start with a dot (.)
770 /// Adds the matching files to the list in *files.
771 static void get_file_list(const SkString& dir,
772 const StringArray& matchSubstrings,
773 const StringArray& nomatchSubstrings,
774 bool recurseIntoSubdirs, FileArray *files) {
775 get_file_list_subdir(dir, SkString(""),
776 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
780 static void release_file_list(FileArray *files) {
784 /// Comparison routines for qsort, sort by file names.
785 static int compare_file_name_metrics(SkString **lhs, SkString **rhs) {
786 return strcmp((*lhs)->c_str(), (*rhs)->c_str());
789 /// Creates difference images, returns the number that have a 0 metric.
790 /// If outputDir.isEmpty(), don't write out diff files.
791 static void create_diff_images (DiffMetricProc dmp,
792 const int colorThreshold,
793 RecordArray* differences,
794 const SkString& baseDir,
795 const SkString& comparisonDir,
796 const SkString& outputDir,
797 const StringArray& matchSubstrings,
798 const StringArray& nomatchSubstrings,
799 bool recurseIntoSubdirs,
800 DiffSummary* summary) {
801 SkASSERT(!baseDir.isEmpty());
802 SkASSERT(!comparisonDir.isEmpty());
805 FileArray comparisonFiles;
807 get_file_list(baseDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, &baseFiles);
808 get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
811 if (!baseFiles.isEmpty()) {
812 qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
813 SkCastForQSort(compare_file_name_metrics));
815 if (!comparisonFiles.isEmpty()) {
816 qsort(comparisonFiles.begin(), comparisonFiles.count(),
817 sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
823 while (i < baseFiles.count() &&
824 j < comparisonFiles.count()) {
826 SkString basePath (baseDir);
827 basePath.append(*baseFiles[i]);
828 SkString comparisonPath (comparisonDir);
829 comparisonPath.append(*comparisonFiles[j]);
831 DiffRecord *drp = NULL;
832 int v = strcmp(baseFiles[i]->c_str(),
833 comparisonFiles[j]->c_str());
836 // in baseDir, but not in comparisonDir
837 drp = new DiffRecord(*baseFiles[i], basePath, comparisonPath,
841 // in comparisonDir, but not in baseDir
842 drp = new DiffRecord(*comparisonFiles[j], basePath, comparisonPath,
846 // Found the same filename in both baseDir and comparisonDir.
847 drp = new DiffRecord(*baseFiles[i], basePath, comparisonPath);
848 SkASSERT(kUnknown == drp->fResult);
850 SkData* baseFileBits = NULL;
851 SkData* comparisonFileBits = NULL;
852 if (NULL == (baseFileBits = read_file(basePath.c_str()))) {
853 SkDebugf("WARNING: couldn't read base file <%s>\n",
855 drp->fResult = kBaseMissing;
856 } else if (NULL == (comparisonFileBits = read_file(comparisonPath.c_str()))) {
857 SkDebugf("WARNING: couldn't read comparison file <%s>\n",
858 comparisonPath.c_str());
859 drp->fResult = kComparisonMissing;
861 if (are_buffers_equal(baseFileBits, comparisonFileBits)) {
862 drp->fResult = kEqualBits;
863 } else if (get_bitmaps(baseFileBits, comparisonFileBits, drp)) {
864 create_and_write_diff_image(drp, dmp, colorThreshold,
865 outputDir, *baseFiles[i]);
867 drp->fResult = kDifferentOther;
871 baseFileBits->unref();
873 if (comparisonFileBits) {
874 comparisonFileBits->unref();
879 SkASSERT(kUnknown != drp->fResult);
880 differences->push(drp);
884 for (; i < baseFiles.count(); ++i) {
885 // files only in baseDir
886 SkString basePath (baseDir);
887 basePath.append(*baseFiles[i]);
888 SkString comparisonPath;
889 DiffRecord *drp = new DiffRecord(*baseFiles[i], basePath,
890 comparisonPath, kComparisonMissing);
891 differences->push(drp);
895 for (; j < comparisonFiles.count(); ++j) {
896 // files only in comparisonDir
898 SkString comparisonPath(comparisonDir);
899 comparisonPath.append(*comparisonFiles[j]);
900 DiffRecord *drp = new DiffRecord(*comparisonFiles[j], basePath,
901 comparisonPath, kBaseMissing);
902 differences->push(drp);
906 release_file_list(&baseFiles);
907 release_file_list(&comparisonFiles);
910 /// Make layout more consistent by scaling image to 240 height, 360 width,
911 /// or natural size, whichever is smallest.
912 static int compute_image_height (int height, int width) {
914 if (height < retval) {
917 float scale = (float) retval / height;
918 if (width * scale > 360) {
919 scale = (float) 360 / width;
920 retval = static_cast<int>(height * scale);
925 static void print_table_header (SkFILEWStream* stream,
926 const int matchCount,
927 const int colorThreshold,
928 const RecordArray& differences,
929 const SkString &baseDir,
930 const SkString &comparisonDir,
931 bool doOutputDate=false) {
932 stream->writeText("<table>\n");
933 stream->writeText("<tr><th>");
934 stream->writeText("select image</th>\n<th>");
937 SkTime::GetDateTime(&dt);
938 stream->writeText("SkDiff run at ");
939 stream->writeDecAsText(dt.fHour);
940 stream->writeText(":");
941 if (dt.fMinute < 10) {
942 stream->writeText("0");
944 stream->writeDecAsText(dt.fMinute);
945 stream->writeText(":");
946 if (dt.fSecond < 10) {
947 stream->writeText("0");
949 stream->writeDecAsText(dt.fSecond);
950 stream->writeText("<br>");
952 stream->writeDecAsText(matchCount);
953 stream->writeText(" of ");
954 stream->writeDecAsText(differences.count());
955 stream->writeText(" images matched ");
956 if (colorThreshold == 0) {
957 stream->writeText("exactly");
959 stream->writeText("within ");
960 stream->writeDecAsText(colorThreshold);
961 stream->writeText(" color units per component");
963 stream->writeText(".<br>");
964 stream->writeText("</th>\n<th>");
965 stream->writeText("every different pixel shown in white");
966 stream->writeText("</th>\n<th>");
967 stream->writeText("color difference at each pixel");
968 stream->writeText("</th>\n<th>baseDir: ");
969 stream->writeText(baseDir.c_str());
970 stream->writeText("</th>\n<th>comparisonDir: ");
971 stream->writeText(comparisonDir.c_str());
972 stream->writeText("</th>\n");
973 stream->writeText("</tr>\n");
976 static void print_pixel_count (SkFILEWStream* stream,
977 const DiffRecord& diff) {
978 stream->writeText("<br>(");
979 stream->writeDecAsText(static_cast<int>(diff.fFractionDifference *
982 stream->writeText(" pixels)");
984 stream->writeDecAsText(diff.fWeightedFraction *
987 stream->writeText(" weighted pixels)");
991 static void print_checkbox_cell (SkFILEWStream* stream,
992 const DiffRecord& diff) {
993 stream->writeText("<td><input type=\"checkbox\" name=\"");
994 stream->writeText(diff.fFilename.c_str());
995 stream->writeText("\" checked=\"yes\"></td>");
998 static void print_label_cell (SkFILEWStream* stream,
999 const DiffRecord& diff) {
1000 char metricBuf [20];
1002 stream->writeText("<td><b>");
1003 stream->writeText(diff.fFilename.c_str());
1004 stream->writeText("</b><br>");
1005 switch (diff.fResult) {
1007 SkDEBUGFAIL("should not encounter DiffRecord with kEqualBits here");
1010 SkDEBUGFAIL("should not encounter DiffRecord with kEqualPixels here");
1012 case kDifferentSizes:
1013 stream->writeText("Image sizes differ</td>");
1015 case kDifferentPixels:
1016 sprintf(metricBuf, "%12.4f%%", 100 * diff.fFractionDifference);
1017 stream->writeText(metricBuf);
1018 stream->writeText(" of pixels differ");
1019 stream->writeText("\n (");
1020 sprintf(metricBuf, "%12.4f%%", 100 * diff.fWeightedFraction);
1021 stream->writeText(metricBuf);
1022 stream->writeText(" weighted)");
1023 // Write the actual number of pixels that differ if it's < 1%
1024 if (diff.fFractionDifference < 0.01) {
1025 print_pixel_count(stream, diff);
1027 stream->writeText("<br>Average color mismatch ");
1028 stream->writeDecAsText(static_cast<int>(MAX3(diff.fAverageMismatchR,
1029 diff.fAverageMismatchG,
1030 diff.fAverageMismatchB)));
1031 stream->writeText("<br>Max color mismatch ");
1032 stream->writeDecAsText(MAX3(diff.fMaxMismatchR,
1034 diff.fMaxMismatchB));
1035 stream->writeText("</td>");
1037 case kDifferentOther:
1038 stream->writeText("Files differ; unable to parse one or both files</td>");
1041 stream->writeText("Missing from baseDir</td>");
1043 case kComparisonMissing:
1044 stream->writeText("Missing from comparisonDir</td>");
1047 SkDEBUGFAIL("encountered DiffRecord with unknown result type");
1052 static void print_image_cell (SkFILEWStream* stream,
1053 const SkString& path,
1055 stream->writeText("<td><a href=\"");
1056 stream->writeText(path.c_str());
1057 stream->writeText("\"><img src=\"");
1058 stream->writeText(path.c_str());
1059 stream->writeText("\" height=\"");
1060 stream->writeDecAsText(height);
1061 stream->writeText("px\"></a></td>");
1065 static void print_text_cell (SkFILEWStream* stream, const char* text) {
1066 stream->writeText("<td align=center>");
1068 stream->writeText(text);
1070 stream->writeText("</td>");
1074 static void print_diff_with_missing_file(SkFILEWStream* stream,
1076 const SkString& relativePath) {
1077 stream->writeText("<tr>\n");
1078 print_checkbox_cell(stream, diff);
1079 print_label_cell(stream, diff);
1080 stream->writeText("<td>N/A</td>");
1081 stream->writeText("<td>N/A</td>");
1082 if (kBaseMissing != diff.fResult) {
1084 if (!get_bitmap_height_width(diff.fBasePath, &h, &w)) {
1085 stream->writeText("<td>N/A</td>");
1087 int height = compute_image_height(h, w);
1088 if (!diff.fBasePath.startsWith(PATH_DIV_STR)) {
1089 diff.fBasePath.prepend(relativePath);
1091 print_image_cell(stream, diff.fBasePath, height);
1094 stream->writeText("<td>N/A</td>");
1096 if (kComparisonMissing != diff.fResult) {
1098 if (!get_bitmap_height_width(diff.fComparisonPath, &h, &w)) {
1099 stream->writeText("<td>N/A</td>");
1101 int height = compute_image_height(h, w);
1102 if (!diff.fComparisonPath.startsWith(PATH_DIV_STR)) {
1103 diff.fComparisonPath.prepend(relativePath);
1105 print_image_cell(stream, diff.fComparisonPath, height);
1108 stream->writeText("<td>N/A</td>");
1110 stream->writeText("</tr>\n");
1114 static void print_diff_page (const int matchCount,
1115 const int colorThreshold,
1116 const RecordArray& differences,
1117 const SkString& baseDir,
1118 const SkString& comparisonDir,
1119 const SkString& outputDir) {
1121 SkASSERT(!baseDir.isEmpty());
1122 SkASSERT(!comparisonDir.isEmpty());
1123 SkASSERT(!outputDir.isEmpty());
1125 SkString outputPath (outputDir);
1126 outputPath.append("index.html");
1127 //SkFILEWStream outputStream ("index.html");
1128 SkFILEWStream outputStream (outputPath.c_str());
1130 // Need to convert paths from relative-to-cwd to relative-to-outputDir
1131 // FIXME this doesn't work if there are '..' inside the outputDir
1133 bool isPathAbsolute = false;
1134 // On Windows or Linux, a path starting with PATH_DIV_CHAR is absolute.
1135 if (outputDir.size() > 0 && PATH_DIV_CHAR == outputDir[0]) {
1136 isPathAbsolute = true;
1138 #ifdef SK_BUILD_FOR_WIN32
1139 // On Windows, absolute paths can also start with "x:", where x is any
1141 if (outputDir.size() > 1 && ':' == outputDir[1]) {
1142 isPathAbsolute = true;
1146 SkString relativePath;
1147 if (!isPathAbsolute) {
1149 for (ui = 0; ui < outputDir.size(); ui++) {
1150 if (outputDir[ui] == PATH_DIV_CHAR) {
1151 relativePath.append(".." PATH_DIV_STR);
1156 outputStream.writeText(
1158 "<script src=\"https://ajax.googleapis.com/ajax/"
1159 "libs/jquery/1.7.2/jquery.min.js\"></script>\n"
1160 "<script type=\"text/javascript\">\n"
1161 "function generateCheckedList() {\n"
1162 "var boxes = $(\":checkbox:checked\");\n"
1163 "var fileCmdLineString = '';\n"
1164 "var fileMultiLineString = '';\n"
1165 "for (var i = 0; i < boxes.length; i++) {\n"
1166 "fileMultiLineString += boxes[i].name + '<br>';\n"
1167 "fileCmdLineString += boxes[i].name + ' ';\n"
1169 "$(\"#checkedList\").html(fileCmdLineString + "
1170 "'<br><br>' + fileMultiLineString);\n"
1172 "</script>\n</head>\n<body>\n");
1173 print_table_header(&outputStream, matchCount, colorThreshold, differences,
1174 baseDir, comparisonDir);
1176 for (i = 0; i < differences.count(); i++) {
1177 DiffRecord* diff = differences[i];
1179 switch (diff->fResult) {
1180 // Cases in which there is no diff to report.
1184 // Cases in which we want a detailed pixel diff.
1185 case kDifferentPixels:
1187 // Cases in which the files differed, but we can't display the diff.
1188 case kDifferentSizes:
1189 case kDifferentOther:
1191 case kComparisonMissing:
1192 print_diff_with_missing_file(&outputStream, *diff, relativePath);
1195 SkDEBUGFAIL("encountered DiffRecord with unknown result type");
1199 if (!diff->fBasePath.startsWith(PATH_DIV_STR)) {
1200 diff->fBasePath.prepend(relativePath);
1202 if (!diff->fComparisonPath.startsWith(PATH_DIV_STR)) {
1203 diff->fComparisonPath.prepend(relativePath);
1206 int height = compute_image_height(diff->fBaseHeight, diff->fBaseWidth);
1207 outputStream.writeText("<tr>\n");
1208 print_checkbox_cell(&outputStream, *diff);
1209 print_label_cell(&outputStream, *diff);
1210 print_image_cell(&outputStream,
1211 filename_to_white_filename(diff->fFilename), height);
1212 print_image_cell(&outputStream,
1213 filename_to_diff_filename(diff->fFilename), height);
1214 print_image_cell(&outputStream, diff->fBasePath, height);
1215 print_image_cell(&outputStream, diff->fComparisonPath, height);
1216 outputStream.writeText("</tr>\n");
1217 outputStream.flush();
1219 outputStream.writeText(
1221 "<input type=\"button\" "
1222 "onclick=\"generateCheckedList()\" "
1223 "value=\"Create Rebaseline List\">\n"
1224 "<div id=\"checkedList\"></div>\n"
1225 "</body>\n</html>\n");
1226 outputStream.flush();
1229 static void usage (char * argv0) {
1230 SkDebugf("Skia baseline image diff tool\n");
1233 " %s <baseDir> <comparisonDir> [outputDir] \n"
1237 "\n --failonresult <result>: After comparing all file pairs, exit with nonzero"
1238 "\n return code (number of file pairs yielding this"
1239 "\n result) if any file pairs yielded this result."
1240 "\n This flag may be repeated, in which case the"
1241 "\n return code will be the number of fail pairs"
1242 "\n yielding ANY of these results."
1243 "\n --help: display this info"
1244 "\n --listfilenames: list all filenames for each result type in stdout"
1245 "\n --match <substring>: compare files whose filenames contain this substring;"
1246 "\n if unspecified, compare ALL files."
1247 "\n this flag may be repeated."
1248 "\n --nodiffs: don't write out image diffs or index.html, just generate"
1249 "\n report on stdout"
1250 "\n --nomatch <substring>: regardless of --match, DO NOT compare files whose"
1251 "\n filenames contain this substring."
1252 "\n this flag may be repeated."
1253 "\n --noprintdirs: do not print the directories used."
1254 "\n --norecurse: do not recurse into subdirectories."
1255 "\n --sortbymaxmismatch: sort by worst color channel mismatch;"
1256 "\n break ties with -sortbymismatch"
1257 "\n --sortbymismatch: sort by average color channel mismatch"
1258 "\n --threshold <n>: only report differences > n (per color channel) [default 0]"
1259 "\n --weighted: sort by # pixels different weighted by color difference"
1261 "\n baseDir: directory to read baseline images from."
1262 "\n comparisonDir: directory to read comparison images from"
1263 "\n outputDir: directory to write difference images and index.html to;"
1264 "\n defaults to comparisonDir"
1266 "\nIf no sort is specified, it will sort by fraction of pixels mismatching."
1270 const int kNoError = 0;
1271 const int kGenericError = -1;
1273 int main (int argc, char ** argv) {
1274 DiffMetricProc diffProc = compute_diff_pmcolor;
1275 int (*sortProc)(const void*, const void*) = compare<CompareDiffMetrics>;
1277 // Maximum error tolerated in any one color channel in any one pixel before
1278 // a difference is reported.
1279 int colorThreshold = 0;
1281 SkString comparisonDir;
1284 StringArray matchSubstrings;
1285 StringArray nomatchSubstrings;
1287 bool generateDiffs = true;
1288 bool listFilenames = false;
1289 bool printDirNames = true;
1290 bool recurseIntoSubdirs = true;
1292 RecordArray differences;
1293 DiffSummary summary;
1295 bool failOnResultType[kNumResultTypes];
1296 for (int i = 0; i < kNumResultTypes; i++) {
1297 failOnResultType[i] = false;
1301 int numUnflaggedArguments = 0;
1302 for (i = 1; i < argc; i++) {
1303 if (!strcmp(argv[i], "--failonresult")) {
1304 Result type = getResultByName(argv[++i]);
1305 failOnResultType[type] = true;
1308 if (!strcmp(argv[i], "--help")) {
1312 if (!strcmp(argv[i], "--listfilenames")) {
1313 listFilenames = true;
1316 if (!strcmp(argv[i], "--match")) {
1317 matchSubstrings.push(new SkString(argv[++i]));
1320 if (!strcmp(argv[i], "--nodiffs")) {
1321 generateDiffs = false;
1324 if (!strcmp(argv[i], "--nomatch")) {
1325 nomatchSubstrings.push(new SkString(argv[++i]));
1328 if (!strcmp(argv[i], "--noprintdirs")) {
1329 printDirNames = false;
1332 if (!strcmp(argv[i], "--norecurse")) {
1333 recurseIntoSubdirs = false;
1336 if (!strcmp(argv[i], "--sortbymaxmismatch")) {
1337 sortProc = compare<CompareDiffMaxMismatches>;
1340 if (!strcmp(argv[i], "--sortbymismatch")) {
1341 sortProc = compare<CompareDiffMeanMismatches>;
1344 if (!strcmp(argv[i], "--threshold")) {
1345 colorThreshold = atoi(argv[++i]);
1348 if (!strcmp(argv[i], "--weighted")) {
1349 sortProc = compare<CompareDiffWeighted>;
1352 if (argv[i][0] != '-') {
1353 switch (numUnflaggedArguments++) {
1355 baseDir.set(argv[i]);
1358 comparisonDir.set(argv[i]);
1361 outputDir.set(argv[i]);
1364 SkDebugf("extra unflagged argument <%s>\n", argv[i]);
1366 return kGenericError;
1370 SkDebugf("Unrecognized argument <%s>\n", argv[i]);
1372 return kGenericError;
1375 if (numUnflaggedArguments == 2) {
1376 outputDir = comparisonDir;
1377 } else if (numUnflaggedArguments != 3) {
1379 return kGenericError;
1382 if (!baseDir.endsWith(PATH_DIV_STR)) {
1383 baseDir.append(PATH_DIV_STR);
1385 if (printDirNames) {
1386 printf("baseDir is [%s]\n", baseDir.c_str());
1389 if (!comparisonDir.endsWith(PATH_DIV_STR)) {
1390 comparisonDir.append(PATH_DIV_STR);
1392 if (printDirNames) {
1393 printf("comparisonDir is [%s]\n", comparisonDir.c_str());
1396 if (!outputDir.endsWith(PATH_DIV_STR)) {
1397 outputDir.append(PATH_DIV_STR);
1399 if (generateDiffs) {
1400 if (printDirNames) {
1401 printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
1404 if (printDirNames) {
1405 printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
1410 // If no matchSubstrings were specified, match ALL strings
1411 // (except for whatever nomatchSubstrings were specified, if any).
1412 if (matchSubstrings.isEmpty()) {
1413 matchSubstrings.push(new SkString(""));
1416 create_diff_images(diffProc, colorThreshold, &differences,
1417 baseDir, comparisonDir, outputDir,
1418 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, &summary);
1419 summary.print(listFilenames, failOnResultType);
1421 if (differences.count()) {
1422 qsort(differences.begin(), differences.count(),
1423 sizeof(DiffRecord*), sortProc);
1426 if (generateDiffs) {
1427 print_diff_page(summary.fNumMatches, colorThreshold, differences,
1428 baseDir, comparisonDir, outputDir);
1431 for (i = 0; i < differences.count(); i++) {
1432 delete differences[i];
1434 matchSubstrings.deleteAll();
1435 nomatchSubstrings.deleteAll();
1437 int num_failing_results = 0;
1438 for (int i = 0; i < kNumResultTypes; i++) {
1439 if (failOnResultType[i]) {
1440 num_failing_results += summary.fResultsOfType[i].count();
1444 // On Linux (and maybe other platforms too), any results outside of the
1445 // range [0...255] are wrapped (mod 256). Do the conversion ourselves, to
1446 // make sure that we only return 0 when there were no failures.
1447 return (num_failing_results > 255) ? 255 : num_failing_results;