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 * Does *not* recursively descend directories.
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.
44 kEqualBits, // both files in the pair contain exactly the same bits
45 kEqualPixels, // not bitwise equal, but their pixels are exactly the same
46 kDifferentSizes, // both are images we can parse, but of different sizes
47 kDifferentPixels,// both are images we can parse, but with different pixels
48 kDifferentOther, // files have different bits but are not parsable images
49 kBaseMissing, // missing from baseDir
50 kComparisonMissing,// missing from comparisonDir
51 kUnknown, // not compared yet
53 kNumResultTypes // NOT A VALID VALUE--used to set up arrays. Must be last.
57 DiffRecord (const SkString filename,
58 const SkString basePath,
59 const SkString comparisonPath,
60 const Result result = kUnknown)
61 : fFilename (filename)
62 , fBasePath (basePath)
63 , fComparisonPath (comparisonPath)
64 , fBaseBitmap (new SkBitmap ())
65 , fComparisonBitmap (new SkBitmap ())
66 , fDifferenceBitmap (new SkBitmap ())
67 , fWhiteBitmap (new SkBitmap ())
70 , fFractionDifference (0)
71 , fWeightedFraction (0)
72 , fAverageMismatchR (0)
73 , fAverageMismatchG (0)
74 , fAverageMismatchB (0)
83 SkString fComparisonPath;
85 SkBitmap* fBaseBitmap;
86 SkBitmap* fComparisonBitmap;
87 SkBitmap* fDifferenceBitmap;
88 SkBitmap* fWhiteBitmap;
93 /// Arbitrary floating-point metric to be used to sort images from most
94 /// to least different from baseline; values of 0 will be omitted from the
96 float fFractionDifference;
97 float fWeightedFraction;
99 float fAverageMismatchR;
100 float fAverageMismatchG;
101 float fAverageMismatchB;
103 uint32_t fMaxMismatchR;
104 uint32_t fMaxMismatchG;
105 uint32_t fMaxMismatchB;
107 /// Which category of diff result.
111 #define MAX2(a,b) (((b) < (a)) ? (a) : (b))
112 #define MAX3(a,b,c) (((b) < (a)) ? MAX2((a), (c)) : MAX2((b), (c)))
114 const SkPMColor PMCOLOR_WHITE = SkPreMultiplyColor(SK_ColorWHITE);
115 const SkPMColor PMCOLOR_BLACK = SkPreMultiplyColor(SK_ColorBLACK);
117 typedef SkTDArray<SkString*> StringArray;
118 typedef StringArray FileArray;
125 , fMaxMismatchPercent (0) { };
128 for (int i = 0; i < kNumResultTypes; i++) {
129 fResultsOfType[i].deleteAll();
133 uint32_t fNumMatches;
134 uint32_t fNumMismatches;
135 uint32_t fMaxMismatchV;
136 float fMaxMismatchPercent;
138 FileArray fResultsOfType[kNumResultTypes];
140 // Print the contents of this FileArray, if any, to stdout.
141 // (If the FileArray is empty, print nothing.)
142 void printContents(const FileArray& fileArray, const char* headerText) {
143 int n = fileArray.count();
145 printf("%s:\n", headerText);
146 for (int i = 0; i < n; ++i) {
147 printf("\t%s\n", fileArray[i]->c_str());
153 printContents(fResultsOfType[kBaseMissing],
154 "Missing in baseDir");
155 printContents(fResultsOfType[kComparisonMissing],
156 "Missing in comparisonDir");
157 printf("%d of %d images matched.\n", fNumMatches,
158 fNumMatches + fNumMismatches);
159 if (fNumMismatches > 0) {
160 printf("Maximum pixel intensity mismatch %d\n", fMaxMismatchV);
161 printf("Largest area mismatch was %.2f%% of pixels\n",
162 fMaxMismatchPercent);
167 void add (DiffRecord* drp) {
168 uint32_t mismatchValue;
170 fResultsOfType[drp->fResult].push(new SkString(drp->fFilename));
171 switch (drp->fResult) {
178 case kDifferentSizes:
180 drp->fFractionDifference = 2;// sort as if 200% of pixels differed
182 case kDifferentPixels:
184 if (drp->fFractionDifference * 100 > fMaxMismatchPercent) {
185 fMaxMismatchPercent = drp->fFractionDifference * 100;
187 mismatchValue = MAX3(drp->fMaxMismatchR, drp->fMaxMismatchG,
189 if (mismatchValue > fMaxMismatchV) {
190 fMaxMismatchV = mismatchValue;
193 case kDifferentOther:
195 drp->fFractionDifference = 3;// sort as if 300% of pixels differed
200 case kComparisonMissing:
204 SkDEBUGFAIL("adding uncategorized DiffRecord");
207 SkDEBUGFAIL("adding DiffRecord with unhandled fResult value");
213 typedef SkTDArray<DiffRecord*> RecordArray;
215 /// Comparison routine for qsort; sorts by fFractionDifference
216 /// from largest to smallest.
217 static int compare_diff_metrics (DiffRecord** lhs, DiffRecord** rhs) {
218 if ((*lhs)->fFractionDifference < (*rhs)->fFractionDifference) {
221 if ((*rhs)->fFractionDifference < (*lhs)->fFractionDifference) {
227 static int compare_diff_weighted (DiffRecord** lhs, DiffRecord** rhs) {
228 if ((*lhs)->fWeightedFraction < (*rhs)->fWeightedFraction) {
231 if ((*lhs)->fWeightedFraction > (*rhs)->fWeightedFraction) {
237 /// Comparison routine for qsort; sorts by max(fAverageMismatch{RGB})
238 /// from largest to smallest.
239 static int compare_diff_mean_mismatches (DiffRecord** lhs, DiffRecord** rhs) {
240 float leftValue = MAX3((*lhs)->fAverageMismatchR,
241 (*lhs)->fAverageMismatchG,
242 (*lhs)->fAverageMismatchB);
243 float rightValue = MAX3((*rhs)->fAverageMismatchR,
244 (*rhs)->fAverageMismatchG,
245 (*rhs)->fAverageMismatchB);
246 if (leftValue < rightValue) {
249 if (rightValue < leftValue) {
255 /// Comparison routine for qsort; sorts by max(fMaxMismatch{RGB})
256 /// from largest to smallest.
257 static int compare_diff_max_mismatches (DiffRecord** lhs, DiffRecord** rhs) {
258 uint32_t leftValue = MAX3((*lhs)->fMaxMismatchR,
259 (*lhs)->fMaxMismatchG,
260 (*lhs)->fMaxMismatchB);
261 uint32_t rightValue = MAX3((*rhs)->fMaxMismatchR,
262 (*rhs)->fMaxMismatchG,
263 (*rhs)->fMaxMismatchB);
264 if (leftValue < rightValue) {
267 if (rightValue < leftValue) {
270 return compare_diff_mean_mismatches(lhs, rhs);
275 /// Parameterized routine to compute the color of a pixel in a difference image.
276 typedef SkPMColor (*DiffMetricProc)(SkPMColor, SkPMColor);
279 static void expand_and_copy (int width, int height, SkBitmap** dest) {
280 SkBitmap* temp = new SkBitmap ();
282 temp->setConfig((*dest)->config(), width, height);
284 (*dest)->copyPixelsTo(temp->getPixels(), temp->getSize(),
290 /// Returns true if the two buffers passed in are both non-NULL, and include
291 /// exactly the same byte values (and identical lengths).
292 static bool are_buffers_equal(SkData* skdata1, SkData* skdata2) {
293 if ((NULL == skdata1) || (NULL == skdata2)) {
296 if (skdata1->size() != skdata2->size()) {
299 return (0 == memcmp(skdata1->data(), skdata2->data(), skdata1->size()));
302 /// Reads the file at the given path and returns its complete contents as an
303 /// SkData object (or returns NULL on error).
304 static SkData* read_file(const char* file_path) {
305 SkFILEStream fileStream(file_path);
306 if (!fileStream.isValid()) {
307 SkDebugf("WARNING: could not open file <%s> for reading\n", file_path);
310 size_t bytesInFile = fileStream.getLength();
311 size_t bytesLeftToRead = bytesInFile;
313 void* bufferStart = sk_malloc_throw(bytesInFile);
314 char* bufferPointer = (char*)bufferStart;
315 while (bytesLeftToRead > 0) {
316 size_t bytesReadThisTime = fileStream.read(
317 bufferPointer, bytesLeftToRead);
318 if (0 == bytesReadThisTime) {
319 SkDebugf("WARNING: error reading from <%s>\n", file_path);
320 sk_free(bufferStart);
323 bytesLeftToRead -= bytesReadThisTime;
324 bufferPointer += bytesReadThisTime;
326 return SkData::NewFromMalloc(bufferStart, bytesInFile);
329 /// Decodes binary contents of baseFile and comparisonFile into
330 /// diffRecord->fBaseBitmap and diffRecord->fComparisonBitmap.
331 /// Returns true if that succeeds.
332 static bool get_bitmaps (SkData* baseFileContents,
333 SkData* comparisonFileContents,
334 DiffRecord* diffRecord) {
335 SkMemoryStream compareStream(comparisonFileContents->data(),
336 comparisonFileContents->size());
337 SkMemoryStream baseStream(baseFileContents->data(),
338 baseFileContents->size());
340 SkImageDecoder* codec = SkImageDecoder::Factory(&baseStream);
342 SkDebugf("ERROR: no codec found for basePath <%s>\n",
343 diffRecord->fBasePath.c_str());
347 // In debug, the DLL will automatically be unloaded when this is deleted,
348 // but that shouldn't be a problem in release mode.
349 SkAutoTDelete<SkImageDecoder> ad(codec);
352 if (!codec->decode(&baseStream, diffRecord->fBaseBitmap,
353 SkBitmap::kARGB_8888_Config,
354 SkImageDecoder::kDecodePixels_Mode)) {
355 SkDebugf("ERROR: codec failed for basePath <%s>\n",
356 diffRecord->fBasePath.c_str());
360 diffRecord->fBaseWidth = diffRecord->fBaseBitmap->width();
361 diffRecord->fBaseHeight = diffRecord->fBaseBitmap->height();
363 if (!codec->decode(&compareStream, diffRecord->fComparisonBitmap,
364 SkBitmap::kARGB_8888_Config,
365 SkImageDecoder::kDecodePixels_Mode)) {
366 SkDebugf("ERROR: codec failed for comparisonPath <%s>\n",
367 diffRecord->fComparisonPath.c_str());
374 static bool get_bitmap_height_width(const SkString& path,
375 int *height, int *width) {
376 SkFILEStream stream(path.c_str());
377 if (!stream.isValid()) {
378 SkDebugf("ERROR: couldn't open file <%s>\n",
383 SkImageDecoder* codec = SkImageDecoder::Factory(&stream);
385 SkDebugf("ERROR: no codec found for <%s>\n",
390 SkAutoTDelete<SkImageDecoder> ad(codec);
394 if (!codec->decode(&stream, &bm,
395 SkBitmap::kARGB_8888_Config,
396 SkImageDecoder::kDecodePixels_Mode)) {
397 SkDebugf("ERROR: codec failed for <%s>\n",
402 *height = bm.height();
408 // from gm - thanks to PNG, we need to force all pixels 100% opaque
409 static void force_all_opaque(const SkBitmap& bitmap) {
410 SkAutoLockPixels lock(bitmap);
411 for (int y = 0; y < bitmap.height(); y++) {
412 for (int x = 0; x < bitmap.width(); x++) {
413 *bitmap.getAddr32(x, y) |= (SK_A32_MASK << SK_A32_SHIFT);
419 static bool write_bitmap(const SkString& path, const SkBitmap* bitmap) {
421 bitmap->copyTo(©, SkBitmap::kARGB_8888_Config);
422 force_all_opaque(copy);
423 return SkImageEncoder::EncodeFile(path.c_str(), copy,
424 SkImageEncoder::kPNG_Type, 100);
428 static inline SkPMColor compute_diff_pmcolor(SkPMColor c0, SkPMColor c1) {
429 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
430 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
431 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
433 return SkPackARGB32(0xFF, SkAbs32(dr), SkAbs32(dg), SkAbs32(db));
436 static inline bool colors_match_thresholded(SkPMColor c0, SkPMColor c1,
437 const int threshold) {
438 int da = SkGetPackedA32(c0) - SkGetPackedA32(c1);
439 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
440 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
441 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
443 return ((SkAbs32(da) <= threshold) &&
444 (SkAbs32(dr) <= threshold) &&
445 (SkAbs32(dg) <= threshold) &&
446 (SkAbs32(db) <= threshold));
450 // Postcondition: when we exit this method, dr->fResult should have some value
451 // other than kUnknown.
452 static void compute_diff(DiffRecord* dr,
453 DiffMetricProc diffFunction,
454 const int colorThreshold) {
455 SkAutoLockPixels alpDiff(*dr->fDifferenceBitmap);
456 SkAutoLockPixels alpWhite(*dr->fWhiteBitmap);
458 const int w = dr->fComparisonBitmap->width();
459 const int h = dr->fComparisonBitmap->height();
460 int mismatchedPixels = 0;
461 int totalMismatchR = 0;
462 int totalMismatchG = 0;
463 int totalMismatchB = 0;
465 if (w != dr->fBaseWidth || h != dr->fBaseHeight) {
466 dr->fResult = kDifferentSizes;
469 // Accumulate fractionally different pixels, then divide out
470 // # of pixels at the end.
471 dr->fWeightedFraction = 0;
472 for (int y = 0; y < h; y++) {
473 for (int x = 0; x < w; x++) {
474 SkPMColor c0 = *dr->fBaseBitmap->getAddr32(x, y);
475 SkPMColor c1 = *dr->fComparisonBitmap->getAddr32(x, y);
476 SkPMColor trueDifference = compute_diff_pmcolor(c0, c1);
477 SkPMColor outputDifference = diffFunction(c0, c1);
478 uint32_t thisR = SkGetPackedR32(trueDifference);
479 uint32_t thisG = SkGetPackedG32(trueDifference);
480 uint32_t thisB = SkGetPackedB32(trueDifference);
481 totalMismatchR += thisR;
482 totalMismatchG += thisG;
483 totalMismatchB += thisB;
484 // In HSV, value is defined as max RGB component.
485 int value = MAX3(thisR, thisG, thisB);
486 dr->fWeightedFraction += ((float) value) / 255;
487 if (thisR > dr->fMaxMismatchR) {
488 dr->fMaxMismatchR = thisR;
490 if (thisG > dr->fMaxMismatchG) {
491 dr->fMaxMismatchG = thisG;
493 if (thisB > dr->fMaxMismatchB) {
494 dr->fMaxMismatchB = thisB;
496 if (!colors_match_thresholded(c0, c1, colorThreshold)) {
498 *dr->fDifferenceBitmap->getAddr32(x, y) = outputDifference;
499 *dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_WHITE;
501 *dr->fDifferenceBitmap->getAddr32(x, y) = 0;
502 *dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_BLACK;
506 if (0 == mismatchedPixels) {
507 dr->fResult = kEqualPixels;
510 dr->fResult = kDifferentPixels;
511 int pixelCount = w * h;
512 dr->fFractionDifference = ((float) mismatchedPixels) / pixelCount;
513 dr->fWeightedFraction /= pixelCount;
514 dr->fAverageMismatchR = ((float) totalMismatchR) / pixelCount;
515 dr->fAverageMismatchG = ((float) totalMismatchG) / pixelCount;
516 dr->fAverageMismatchB = ((float) totalMismatchB) / pixelCount;
519 static SkString filename_to_derived_filename (const SkString& filename,
520 const char *suffix) {
521 SkString diffName (filename);
522 const char* cstring = diffName.c_str();
523 int dotOffset = strrchr(cstring, '.') - cstring;
524 diffName.remove(dotOffset, diffName.size() - dotOffset);
525 diffName.append(suffix);
529 /// Given a image filename, returns the name of the file containing the
530 /// associated difference image.
531 static SkString filename_to_diff_filename (const SkString& filename) {
532 return filename_to_derived_filename(filename, "-diff.png");
535 /// Given a image filename, returns the name of the file containing the
536 /// "white" difference image.
537 static SkString filename_to_white_filename (const SkString& filename) {
538 return filename_to_derived_filename(filename, "-white.png");
541 static void release_bitmaps(DiffRecord* drp) {
542 delete drp->fBaseBitmap;
543 drp->fBaseBitmap = NULL;
544 delete drp->fComparisonBitmap;
545 drp->fComparisonBitmap = NULL;
546 delete drp->fDifferenceBitmap;
547 drp->fDifferenceBitmap = NULL;
548 delete drp->fWhiteBitmap;
549 drp->fWhiteBitmap = NULL;
553 /// If outputDir.isEmpty(), don't write out diff files.
554 static void create_and_write_diff_image(DiffRecord* drp,
556 const int colorThreshold,
557 const SkString& outputDir,
558 const SkString& filename) {
559 const int w = drp->fBaseWidth;
560 const int h = drp->fBaseHeight;
561 drp->fDifferenceBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h);
562 drp->fDifferenceBitmap->allocPixels();
563 drp->fWhiteBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h);
564 drp->fWhiteBitmap->allocPixels();
566 SkASSERT(kUnknown == drp->fResult);
567 compute_diff(drp, dmp, colorThreshold);
568 SkASSERT(kUnknown != drp->fResult);
570 if ((kDifferentPixels == drp->fResult) && !outputDir.isEmpty()) {
571 SkString differencePath (outputDir);
572 differencePath.append(filename_to_diff_filename(filename));
573 write_bitmap(differencePath, drp->fDifferenceBitmap);
574 SkString whitePath (outputDir);
575 whitePath.append(filename_to_white_filename(filename));
576 write_bitmap(whitePath, drp->fWhiteBitmap);
579 release_bitmaps(drp);
582 /// Returns true if string contains any of these substrings.
583 static bool string_contains_any_of(const SkString& string,
584 const StringArray& substrings) {
585 for (int i = 0; i < substrings.count(); i++) {
586 if (string.contains(substrings[i]->c_str())) {
593 /// Iterate over dir and get all files that:
594 /// - match any of the substrings in matchSubstrings, but...
595 /// - DO NOT match any of the substrings in nomatchSubstrings
596 /// Returns the list of files in *files.
597 static void get_file_list(const SkString& dir,
598 const StringArray& matchSubstrings,
599 const StringArray& nomatchSubstrings,
601 SkOSFile::Iter it(dir.c_str());
603 while (it.next(&filename)) {
604 if (string_contains_any_of(filename, matchSubstrings) &&
605 !string_contains_any_of(filename, nomatchSubstrings)) {
606 files->push(new SkString(filename));
611 static void release_file_list(FileArray *files) {
615 /// Comparison routines for qsort, sort by file names.
616 static int compare_file_name_metrics(SkString **lhs, SkString **rhs) {
617 return strcmp((*lhs)->c_str(), (*rhs)->c_str());
620 /// Creates difference images, returns the number that have a 0 metric.
621 /// If outputDir.isEmpty(), don't write out diff files.
622 static void create_diff_images (DiffMetricProc dmp,
623 const int colorThreshold,
624 RecordArray* differences,
625 const SkString& baseDir,
626 const SkString& comparisonDir,
627 const SkString& outputDir,
628 const StringArray& matchSubstrings,
629 const StringArray& nomatchSubstrings,
630 DiffSummary* summary) {
631 SkASSERT(!baseDir.isEmpty());
632 SkASSERT(!comparisonDir.isEmpty());
635 FileArray comparisonFiles;
637 get_file_list(baseDir, matchSubstrings, nomatchSubstrings, &baseFiles);
638 get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings,
641 if (!baseFiles.isEmpty()) {
642 qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
643 SkCastForQSort(compare_file_name_metrics));
645 if (!comparisonFiles.isEmpty()) {
646 qsort(comparisonFiles.begin(), comparisonFiles.count(),
647 sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
653 while (i < baseFiles.count() &&
654 j < comparisonFiles.count()) {
656 SkString basePath (baseDir);
657 basePath.append(*baseFiles[i]);
658 SkString comparisonPath (comparisonDir);
659 comparisonPath.append(*comparisonFiles[j]);
661 DiffRecord *drp = NULL;
662 int v = strcmp(baseFiles[i]->c_str(),
663 comparisonFiles[j]->c_str());
666 // in baseDir, but not in comparisonDir
667 drp = new DiffRecord(*baseFiles[i], basePath, comparisonPath,
671 // in comparisonDir, but not in baseDir
672 drp = new DiffRecord(*comparisonFiles[j], basePath, comparisonPath,
676 // Found the same filename in both baseDir and comparisonDir.
677 drp = new DiffRecord(*baseFiles[i], basePath, comparisonPath);
678 SkASSERT(kUnknown == drp->fResult);
680 SkData* baseFileBits;
681 SkData* comparisonFileBits;
682 if (NULL == (baseFileBits = read_file(basePath.c_str()))) {
683 SkDebugf("WARNING: couldn't read base file <%s>\n",
685 drp->fResult = kBaseMissing;
686 } else if (NULL == (comparisonFileBits = read_file(
687 comparisonPath.c_str()))) {
688 SkDebugf("WARNING: couldn't read comparison file <%s>\n",
689 comparisonPath.c_str());
690 drp->fResult = kComparisonMissing;
692 if (are_buffers_equal(baseFileBits, comparisonFileBits)) {
693 drp->fResult = kEqualBits;
694 } else if (get_bitmaps(baseFileBits, comparisonFileBits, drp)) {
695 create_and_write_diff_image(drp, dmp, colorThreshold,
696 outputDir, *baseFiles[i]);
698 drp->fResult = kDifferentOther;
702 baseFileBits->unref();
704 if (comparisonFileBits) {
705 comparisonFileBits->unref();
710 SkASSERT(kUnknown != drp->fResult);
711 differences->push(drp);
715 for (; i < baseFiles.count(); ++i) {
716 // files only in baseDir
717 SkString basePath (baseDir);
718 basePath.append(*baseFiles[i]);
719 SkString comparisonPath;
720 DiffRecord *drp = new DiffRecord(*baseFiles[i], basePath,
721 comparisonPath, kComparisonMissing);
722 differences->push(drp);
726 for (; j < comparisonFiles.count(); ++j) {
727 // files only in comparisonDir
729 SkString comparisonPath(comparisonDir);
730 comparisonPath.append(*comparisonFiles[j]);
731 DiffRecord *drp = new DiffRecord(*comparisonFiles[j], basePath,
732 comparisonPath, kBaseMissing);
733 differences->push(drp);
737 release_file_list(&baseFiles);
738 release_file_list(&comparisonFiles);
741 /// Make layout more consistent by scaling image to 240 height, 360 width,
742 /// or natural size, whichever is smallest.
743 static int compute_image_height (int height, int width) {
745 if (height < retval) {
748 float scale = (float) retval / height;
749 if (width * scale > 360) {
750 scale = (float) 360 / width;
751 retval = static_cast<int>(height * scale);
756 static void print_table_header (SkFILEWStream* stream,
757 const int matchCount,
758 const int colorThreshold,
759 const RecordArray& differences,
760 const SkString &baseDir,
761 const SkString &comparisonDir,
762 bool doOutputDate=false) {
763 stream->writeText("<table>\n");
764 stream->writeText("<tr><th>");
767 SkTime::GetDateTime(&dt);
768 stream->writeText("SkDiff run at ");
769 stream->writeDecAsText(dt.fHour);
770 stream->writeText(":");
771 if (dt.fMinute < 10) {
772 stream->writeText("0");
774 stream->writeDecAsText(dt.fMinute);
775 stream->writeText(":");
776 if (dt.fSecond < 10) {
777 stream->writeText("0");
779 stream->writeDecAsText(dt.fSecond);
780 stream->writeText("<br>");
782 stream->writeDecAsText(matchCount);
783 stream->writeText(" of ");
784 stream->writeDecAsText(differences.count());
785 stream->writeText(" images matched ");
786 if (colorThreshold == 0) {
787 stream->writeText("exactly");
789 stream->writeText("within ");
790 stream->writeDecAsText(colorThreshold);
791 stream->writeText(" color units per component");
793 stream->writeText(".<br>");
794 stream->writeText("</th>\n<th>");
795 stream->writeText("every different pixel shown in white");
796 stream->writeText("</th>\n<th>");
797 stream->writeText("color difference at each pixel");
798 stream->writeText("</th>\n<th>");
799 stream->writeText(baseDir.c_str());
800 stream->writeText("</th>\n<th>");
801 stream->writeText(comparisonDir.c_str());
802 stream->writeText("</th>\n");
803 stream->writeText("</tr>\n");
806 static void print_pixel_count (SkFILEWStream* stream,
807 const DiffRecord& diff) {
808 stream->writeText("<br>(");
809 stream->writeDecAsText(static_cast<int>(diff.fFractionDifference *
812 stream->writeText(" pixels)");
814 stream->writeDecAsText(diff.fWeightedFraction *
817 stream->writeText(" weighted pixels)");
821 static void print_label_cell (SkFILEWStream* stream,
822 const DiffRecord& diff) {
825 stream->writeText("<td><b>");
826 stream->writeText(diff.fFilename.c_str());
827 stream->writeText("</b><br>");
828 switch (diff.fResult) {
830 SkDEBUGFAIL("should not encounter DiffRecord with kEqualBits here");
833 SkDEBUGFAIL("should not encounter DiffRecord with kEqualPixels here");
835 case kDifferentSizes:
836 stream->writeText("Image sizes differ</td>");
838 case kDifferentPixels:
839 sprintf(metricBuf, "%12.4f%%", 100 * diff.fFractionDifference);
840 stream->writeText(metricBuf);
841 stream->writeText(" of pixels differ");
842 stream->writeText("\n (");
843 sprintf(metricBuf, "%12.4f%%", 100 * diff.fWeightedFraction);
844 stream->writeText(metricBuf);
845 stream->writeText(" weighted)");
846 // Write the actual number of pixels that differ if it's < 1%
847 if (diff.fFractionDifference < 0.01) {
848 print_pixel_count(stream, diff);
850 stream->writeText("<br>Average color mismatch ");
851 stream->writeDecAsText(static_cast<int>(MAX3(diff.fAverageMismatchR,
852 diff.fAverageMismatchG,
853 diff.fAverageMismatchB)));
854 stream->writeText("<br>Max color mismatch ");
855 stream->writeDecAsText(MAX3(diff.fMaxMismatchR,
857 diff.fMaxMismatchB));
858 stream->writeText("</td>");
860 case kDifferentOther:
861 stream->writeText("Files differ; unable to parse one or both files</td>");
864 stream->writeText("Missing from baseDir</td>");
866 case kComparisonMissing:
867 stream->writeText("Missing from comparisonDir</td>");
870 SkDEBUGFAIL("encountered DiffRecord with unknown result type");
875 static void print_image_cell (SkFILEWStream* stream,
876 const SkString& path,
878 stream->writeText("<td><a href=\"");
879 stream->writeText(path.c_str());
880 stream->writeText("\"><img src=\"");
881 stream->writeText(path.c_str());
882 stream->writeText("\" height=\"");
883 stream->writeDecAsText(height);
884 stream->writeText("px\"></a></td>");
888 static void print_text_cell (SkFILEWStream* stream, const char* text) {
889 stream->writeText("<td align=center>");
891 stream->writeText(text);
893 stream->writeText("</td>");
897 static void print_diff_with_missing_file(SkFILEWStream* stream,
899 const SkString& relativePath) {
900 stream->writeText("<tr>\n");
901 print_label_cell(stream, diff);
902 stream->writeText("<td>N/A</td>");
903 stream->writeText("<td>N/A</td>");
904 if (kBaseMissing != diff.fResult) {
906 if (!get_bitmap_height_width(diff.fBasePath, &h, &w)) {
907 stream->writeText("<td>N/A</td>");
909 int height = compute_image_height(h, w);
910 if (!diff.fBasePath.startsWith(PATH_DIV_STR)) {
911 diff.fBasePath.prepend(relativePath);
913 print_image_cell(stream, diff.fBasePath, height);
916 stream->writeText("<td>N/A</td>");
918 if (kComparisonMissing != diff.fResult) {
920 if (!get_bitmap_height_width(diff.fComparisonPath, &h, &w)) {
921 stream->writeText("<td>N/A</td>");
923 int height = compute_image_height(h, w);
924 if (!diff.fComparisonPath.startsWith(PATH_DIV_STR)) {
925 diff.fComparisonPath.prepend(relativePath);
927 print_image_cell(stream, diff.fComparisonPath, height);
930 stream->writeText("<td>N/A</td>");
932 stream->writeText("</tr>\n");
936 static void print_diff_page (const int matchCount,
937 const int colorThreshold,
938 const RecordArray& differences,
939 const SkString& baseDir,
940 const SkString& comparisonDir,
941 const SkString& outputDir) {
943 SkASSERT(!baseDir.isEmpty());
944 SkASSERT(!comparisonDir.isEmpty());
945 SkASSERT(!outputDir.isEmpty());
947 SkString outputPath (outputDir);
948 outputPath.append("index.html");
949 //SkFILEWStream outputStream ("index.html");
950 SkFILEWStream outputStream (outputPath.c_str());
952 // Need to convert paths from relative-to-cwd to relative-to-outputDir
953 // FIXME this doesn't work if there are '..' inside the outputDir
955 SkString relativePath;
956 for (ui = 0; ui < outputDir.size(); ui++) {
957 if (outputDir[ui] == PATH_DIV_CHAR) {
958 relativePath.append(".." PATH_DIV_STR);
962 outputStream.writeText("<html>\n<body>\n");
963 print_table_header(&outputStream, matchCount, colorThreshold, differences,
964 baseDir, comparisonDir);
966 for (i = 0; i < differences.count(); i++) {
967 DiffRecord* diff = differences[i];
969 switch (diff->fResult) {
970 // Cases in which there is no diff to report.
974 // Cases in which we want a detailed pixel diff.
975 case kDifferentPixels:
977 // Cases in which the files differed, but we can't display the diff.
978 case kDifferentSizes:
979 case kDifferentOther:
981 case kComparisonMissing:
982 print_diff_with_missing_file(&outputStream, *diff, relativePath);
985 SkDEBUGFAIL("encountered DiffRecord with unknown result type");
989 if (!diff->fBasePath.startsWith(PATH_DIV_STR)) {
990 diff->fBasePath.prepend(relativePath);
992 if (!diff->fComparisonPath.startsWith(PATH_DIV_STR)) {
993 diff->fComparisonPath.prepend(relativePath);
996 int height = compute_image_height(diff->fBaseHeight, diff->fBaseWidth);
997 outputStream.writeText("<tr>\n");
998 print_label_cell(&outputStream, *diff);
999 print_image_cell(&outputStream,
1000 filename_to_white_filename(diff->fFilename), height);
1001 print_image_cell(&outputStream,
1002 filename_to_diff_filename(diff->fFilename), height);
1003 print_image_cell(&outputStream, diff->fBasePath, height);
1004 print_image_cell(&outputStream, diff->fComparisonPath, height);
1005 outputStream.writeText("</tr>\n");
1006 outputStream.flush();
1008 outputStream.writeText("</table>\n");
1009 outputStream.writeText("</body>\n</html>\n");
1010 outputStream.flush();
1013 static void usage (char * argv0) {
1014 SkDebugf("Skia baseline image diff tool\n");
1017 " %s <baseDir> <comparisonDir> [outputDir] \n"
1021 " --nodiffs: don't write out image diffs or index.html, just generate \n"
1022 " report on stdout \n"
1023 " --threshold <n>: only report differences > n (per color channel) [default 0]\n"
1024 " --match: compare files whose filenames contain this substring; if \n"
1025 " unspecified, compare ALL files. \n"
1026 " this flag may be repeated to add more matching substrings. \n"
1027 " --nomatch: regardless of --match, DO NOT compare files whose filenames \n"
1028 " contain this substring. \n"
1029 " this flag may be repeated to add more forbidden substrings. \n"
1030 " --sortbymismatch: sort by average color channel mismatch\n");
1032 " --sortbymaxmismatch: sort by worst color channel mismatch;\n"
1033 " break ties with -sortbymismatch\n"
1034 " [default sort is by fraction of pixels mismatching]\n");
1036 " --weighted: sort by # pixels different weighted by color difference\n");
1038 " baseDir: directory to read baseline images from.\n");
1040 " comparisonDir: directory to read comparison images from\n");
1042 " outputDir: directory to write difference images and index.html to; \n"
1043 " defaults to comparisonDir \n");
1046 int main (int argc, char ** argv) {
1047 DiffMetricProc diffProc = compute_diff_pmcolor;
1048 int (*sortProc)(const void*, const void*) = SkCastForQSort(compare_diff_metrics);
1050 // Maximum error tolerated in any one color channel in any one pixel before
1051 // a difference is reported.
1052 int colorThreshold = 0;
1054 SkString comparisonDir;
1056 StringArray matchSubstrings;
1057 StringArray nomatchSubstrings;
1059 bool generateDiffs = true;
1061 RecordArray differences;
1062 DiffSummary summary;
1065 int numUnflaggedArguments = 0;
1066 for (i = 1; i < argc; i++) {
1067 if (!strcmp(argv[i], "--help")) {
1071 if (!strcmp(argv[i], "--nodiffs")) {
1072 generateDiffs = false;
1075 if (!strcmp(argv[i], "--threshold")) {
1076 colorThreshold = atoi(argv[++i]);
1079 if (!strcmp(argv[i], "--match")) {
1080 matchSubstrings.push(new SkString(argv[++i]));
1083 if (!strcmp(argv[i], "--nomatch")) {
1084 nomatchSubstrings.push(new SkString(argv[++i]));
1087 if (!strcmp(argv[i], "--sortbymismatch")) {
1088 sortProc = SkCastForQSort(compare_diff_mean_mismatches);
1091 if (!strcmp(argv[i], "--sortbymaxmismatch")) {
1092 sortProc = SkCastForQSort(compare_diff_max_mismatches);
1095 if (!strcmp(argv[i], "--weighted")) {
1096 sortProc = SkCastForQSort(compare_diff_weighted);
1099 if (argv[i][0] != '-') {
1100 switch (numUnflaggedArguments++) {
1102 baseDir.set(argv[i]);
1105 comparisonDir.set(argv[i]);
1108 outputDir.set(argv[i]);
1111 SkDebugf("extra unflagged argument <%s>\n", argv[i]);
1117 SkDebugf("Unrecognized argument <%s>\n", argv[i]);
1122 if (numUnflaggedArguments == 2) {
1123 outputDir = comparisonDir;
1124 } else if (numUnflaggedArguments != 3) {
1129 if (!baseDir.endsWith(PATH_DIV_STR)) {
1130 baseDir.append(PATH_DIV_STR);
1132 printf("baseDir is [%s]\n", baseDir.c_str());
1134 if (!comparisonDir.endsWith(PATH_DIV_STR)) {
1135 comparisonDir.append(PATH_DIV_STR);
1137 printf("comparisonDir is [%s]\n", comparisonDir.c_str());
1139 if (!outputDir.endsWith(PATH_DIV_STR)) {
1140 outputDir.append(PATH_DIV_STR);
1142 if (generateDiffs) {
1143 printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
1145 printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
1149 // If no matchSubstrings were specified, match ALL strings
1150 // (except for whatever nomatchSubstrings were specified, if any).
1151 if (matchSubstrings.isEmpty()) {
1152 matchSubstrings.push(new SkString(""));
1155 create_diff_images(diffProc, colorThreshold, &differences,
1156 baseDir, comparisonDir, outputDir,
1157 matchSubstrings, nomatchSubstrings, &summary);
1160 if (differences.count()) {
1161 qsort(differences.begin(), differences.count(),
1162 sizeof(DiffRecord*), sortProc);
1165 if (generateDiffs) {
1166 print_diff_page(summary.fNumMatches, colorThreshold, differences,
1167 baseDir, comparisonDir, outputDir);
1170 for (i = 0; i < differences.count(); i++) {
1171 delete differences[i];
1173 matchSubstrings.deleteAll();
1174 nomatchSubstrings.deleteAll();
1176 return summary.fNumMismatches;