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"
9 #include "SkImageDecoder.h"
10 #include "SkImageEncoder.h"
13 #include "SkTDArray.h"
14 #include "SkTemplates.h"
16 #include "SkTSearch.h"
22 * Given three directory names, expects to find identically-named files in
23 * each of the first two; the first are treated as a set of baseline,
24 * the second a set of variant images, and a diff image is written into the
25 * third directory for each pair.
26 * Creates an index.html in the current third directory to compare each
27 * pair that does not match exactly.
28 * Does *not* recursively descend directories.
30 * With the --chromium flag, *does* recursively descend the first directory
31 * named, comparing *-expected.png with *-actual.png and writing diff
32 * images into the second directory, also writing index.html there.
35 #if SK_BUILD_FOR_WIN32
36 #define PATH_DIV_STR "\\"
37 #define PATH_DIV_CHAR '\\'
39 #define PATH_DIV_STR "/"
40 #define PATH_DIV_CHAR '/'
44 DiffRecord (const SkString filename,
45 const SkString basePath,
46 const SkString comparisonPath,
47 bool baseMissing = false,
48 bool comparisonMissing = false)
49 : fFilename (filename)
50 , fBasePath (basePath)
51 , fComparisonPath (comparisonPath)
52 , fBaseBitmap (new SkBitmap ())
53 , fComparisonBitmap (new SkBitmap ())
54 , fDifferenceBitmap (new SkBitmap ())
55 , fWhiteBitmap (new SkBitmap ())
58 , fFractionDifference (0)
59 , fWeightedFraction (0)
60 , fAverageMismatchR (0)
61 , fAverageMismatchG (0)
62 , fAverageMismatchB (0)
66 , fDoImageSizesMismatch (false)
67 , fBaseMissing(baseMissing)
68 , fComparisonMissing(comparisonMissing) {
69 // These asserts are valid for GM, but not for --chromium
70 //SkASSERT(basePath.endsWith(filename.c_str()));
71 //SkASSERT(comparisonPath.endsWith(filename.c_str()));
76 SkString fComparisonPath;
78 SkBitmap* fBaseBitmap;
79 SkBitmap* fComparisonBitmap;
80 SkBitmap* fDifferenceBitmap;
81 SkBitmap* fWhiteBitmap;
86 /// Arbitrary floating-point metric to be used to sort images from most
87 /// to least different from baseline; values of 0 will be omitted from the
89 float fFractionDifference;
90 float fWeightedFraction;
92 float fAverageMismatchR;
93 float fAverageMismatchG;
94 float fAverageMismatchB;
96 uint32_t fMaxMismatchR;
97 uint32_t fMaxMismatchG;
98 uint32_t fMaxMismatchB;
100 /// By the time we need to report image size mismatch, we've already
101 /// released the bitmaps, so we need to remember it when we detect it.
102 bool fDoImageSizesMismatch;
105 bool fComparisonMissing;
108 #define MAX2(a,b) (((b) < (a)) ? (a) : (b))
109 #define MAX3(a,b,c) (((b) < (a)) ? MAX2((a), (c)) : MAX2((b), (c)))
111 const SkPMColor PMCOLOR_WHITE = SkPreMultiplyColor(SK_ColorWHITE);
112 const SkPMColor PMCOLOR_BLACK = SkPreMultiplyColor(SK_ColorBLACK);
114 typedef SkTDArray<SkString*> StringArray;
115 typedef StringArray FileArray;
122 , fMaxMismatchPercent (0) { };
125 fBaseMissing.deleteAll();
126 fComparisonMissing.deleteAll();
129 uint32_t fNumMatches;
130 uint32_t fNumMismatches;
131 uint32_t fMaxMismatchV;
132 float fMaxMismatchPercent;
134 FileArray fBaseMissing;
135 FileArray fComparisonMissing;
138 int n = fBaseMissing.count();
140 printf("Missing in baseDir:\n");
141 for (int i = 0; i < n; ++i) {
142 printf("\t%s\n", fBaseMissing[i]->c_str());
145 n = fComparisonMissing.count();
147 printf("Missing in comparisonDir:\n");
148 for (int i = 0; i < n; ++i) {
149 printf("\t%s\n", fComparisonMissing[i]->c_str());
152 printf("%d of %d images matched.\n", fNumMatches,
153 fNumMatches + fNumMismatches);
154 if (fNumMismatches > 0) {
155 printf("Maximum pixel intensity mismatch %d\n", fMaxMismatchV);
156 printf("Largest area mismatch was %.2f%% of pixels\n",
157 fMaxMismatchPercent);
162 void add (DiffRecord* drp) {
163 if (drp->fBaseMissing) {
164 fBaseMissing.push(new SkString(drp->fFilename));
166 } else if (drp->fComparisonMissing) {
167 fComparisonMissing.push(new SkString(drp->fFilename));
169 } else if (0 == drp->fFractionDifference) {
173 if (drp->fFractionDifference * 100 > fMaxMismatchPercent) {
174 fMaxMismatchPercent = drp->fFractionDifference * 100;
176 uint32_t value = MAX3(drp->fMaxMismatchR, drp->fMaxMismatchG,
178 if (value > fMaxMismatchV) {
179 fMaxMismatchV = value;
185 typedef SkTDArray<DiffRecord*> RecordArray;
187 /// Comparison routine for qsort; sorts by fFractionDifference
188 /// from largest to smallest.
189 static int compare_diff_metrics (DiffRecord** lhs, DiffRecord** rhs) {
190 if ((*lhs)->fFractionDifference < (*rhs)->fFractionDifference) {
193 if ((*rhs)->fFractionDifference < (*lhs)->fFractionDifference) {
199 static int compare_diff_weighted (DiffRecord** lhs, DiffRecord** rhs) {
200 if ((*lhs)->fWeightedFraction < (*rhs)->fWeightedFraction) {
203 if ((*lhs)->fWeightedFraction > (*rhs)->fWeightedFraction) {
209 /// Comparison routine for qsort; sorts by max(fAverageMismatch{RGB})
210 /// from largest to smallest.
211 static int compare_diff_mean_mismatches (DiffRecord** lhs, DiffRecord** rhs) {
212 float leftValue = MAX3((*lhs)->fAverageMismatchR,
213 (*lhs)->fAverageMismatchG,
214 (*lhs)->fAverageMismatchB);
215 float rightValue = MAX3((*rhs)->fAverageMismatchR,
216 (*rhs)->fAverageMismatchG,
217 (*rhs)->fAverageMismatchB);
218 if (leftValue < rightValue) {
221 if (rightValue < leftValue) {
227 /// Comparison routine for qsort; sorts by max(fMaxMismatch{RGB})
228 /// from largest to smallest.
229 static int compare_diff_max_mismatches (DiffRecord** lhs, DiffRecord** rhs) {
230 uint32_t leftValue = MAX3((*lhs)->fMaxMismatchR,
231 (*lhs)->fMaxMismatchG,
232 (*lhs)->fMaxMismatchB);
233 uint32_t rightValue = MAX3((*rhs)->fMaxMismatchR,
234 (*rhs)->fMaxMismatchG,
235 (*rhs)->fMaxMismatchB);
236 if (leftValue < rightValue) {
239 if (rightValue < leftValue) {
242 return compare_diff_mean_mismatches(lhs, rhs);
247 /// Parameterized routine to compute the color of a pixel in a difference image.
248 typedef SkPMColor (*DiffMetricProc)(SkPMColor, SkPMColor);
250 static void expand_and_copy (int width, int height, SkBitmap** dest) {
251 SkBitmap* temp = new SkBitmap ();
253 temp->setConfig((*dest)->config(), width, height);
255 (*dest)->copyPixelsTo(temp->getPixels(), temp->getSize(),
260 static bool get_bitmaps (DiffRecord* diffRecord) {
261 SkFILEStream compareStream(diffRecord->fComparisonPath.c_str());
262 if (!compareStream.isValid()) {
263 SkDebugf("WARNING: couldn't open comparison file <%s>\n",
264 diffRecord->fComparisonPath.c_str());
268 SkFILEStream baseStream(diffRecord->fBasePath.c_str());
269 if (!baseStream.isValid()) {
270 SkDebugf("ERROR: couldn't open base file <%s>\n",
271 diffRecord->fBasePath.c_str());
275 SkImageDecoder* codec = SkImageDecoder::Factory(&baseStream);
277 SkDebugf("ERROR: no codec found for <%s>\n",
278 diffRecord->fBasePath.c_str());
282 // In debug, the DLL will automatically be unloaded when this is deleted,
283 // but that shouldn't be a problem in release mode.
284 SkAutoTDelete<SkImageDecoder> ad(codec);
287 if (!codec->decode(&baseStream, diffRecord->fBaseBitmap,
288 SkBitmap::kARGB_8888_Config,
289 SkImageDecoder::kDecodePixels_Mode)) {
290 SkDebugf("ERROR: codec failed for <%s>\n",
291 diffRecord->fBasePath.c_str());
295 diffRecord->fBaseWidth = diffRecord->fBaseBitmap->width();
296 diffRecord->fBaseHeight = diffRecord->fBaseBitmap->height();
298 if (!codec->decode(&compareStream, diffRecord->fComparisonBitmap,
299 SkBitmap::kARGB_8888_Config,
300 SkImageDecoder::kDecodePixels_Mode)) {
301 SkDebugf("ERROR: codec failed for <%s>\n",
302 diffRecord->fComparisonPath.c_str());
309 static bool get_bitmap_height_width(const SkString& path,
310 int *height, int *width) {
311 SkFILEStream stream(path.c_str());
312 if (!stream.isValid()) {
313 SkDebugf("ERROR: couldn't open file <%s>\n",
318 SkImageDecoder* codec = SkImageDecoder::Factory(&stream);
320 SkDebugf("ERROR: no codec found for <%s>\n",
325 SkAutoTDelete<SkImageDecoder> ad(codec);
329 if (!codec->decode(&stream, &bm,
330 SkBitmap::kARGB_8888_Config,
331 SkImageDecoder::kDecodePixels_Mode)) {
332 SkDebugf("ERROR: codec failed for <%s>\n",
337 *height = bm.height();
343 // from gm - thanks to PNG, we need to force all pixels 100% opaque
344 static void force_all_opaque(const SkBitmap& bitmap) {
345 SkAutoLockPixels lock(bitmap);
346 for (int y = 0; y < bitmap.height(); y++) {
347 for (int x = 0; x < bitmap.width(); x++) {
348 *bitmap.getAddr32(x, y) |= (SK_A32_MASK << SK_A32_SHIFT);
354 static bool write_bitmap(const SkString& path, const SkBitmap* bitmap) {
356 bitmap->copyTo(©, SkBitmap::kARGB_8888_Config);
357 force_all_opaque(copy);
358 return SkImageEncoder::EncodeFile(path.c_str(), copy,
359 SkImageEncoder::kPNG_Type, 100);
363 static inline SkPMColor compute_diff_pmcolor(SkPMColor c0, SkPMColor c1) {
364 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
365 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
366 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
368 return SkPackARGB32(0xFF, SkAbs32(dr), SkAbs32(dg), SkAbs32(db));
371 static inline bool colors_match_thresholded(SkPMColor c0, SkPMColor c1,
372 const int threshold) {
373 int da = SkGetPackedA32(c0) - SkGetPackedA32(c1);
374 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
375 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
376 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
378 return ((SkAbs32(da) <= threshold) &&
379 (SkAbs32(dr) <= threshold) &&
380 (SkAbs32(dg) <= threshold) &&
381 (SkAbs32(db) <= threshold));
385 static void compute_diff(DiffRecord* dr,
386 DiffMetricProc diffFunction,
387 const int colorThreshold) {
388 SkAutoLockPixels alpDiff(*dr->fDifferenceBitmap);
389 SkAutoLockPixels alpWhite(*dr->fWhiteBitmap);
391 const int w = dr->fComparisonBitmap->width();
392 const int h = dr->fComparisonBitmap->height();
393 int mismatchedPixels = 0;
394 int totalMismatchR = 0;
395 int totalMismatchG = 0;
396 int totalMismatchB = 0;
398 if (w != dr->fBaseWidth || h != dr->fBaseHeight) {
399 dr->fDoImageSizesMismatch = true;
400 dr->fFractionDifference = 1;
403 // Accumulate fractionally different pixels, then divide out
404 // # of pixels at the end.
405 dr->fWeightedFraction = 0;
406 for (int y = 0; y < h; y++) {
407 for (int x = 0; x < w; x++) {
408 SkPMColor c0 = *dr->fBaseBitmap->getAddr32(x, y);
409 SkPMColor c1 = *dr->fComparisonBitmap->getAddr32(x, y);
410 SkPMColor trueDifference = compute_diff_pmcolor(c0, c1);
411 SkPMColor outputDifference = diffFunction(c0, c1);
412 uint32_t thisR = SkGetPackedR32(trueDifference);
413 uint32_t thisG = SkGetPackedG32(trueDifference);
414 uint32_t thisB = SkGetPackedB32(trueDifference);
415 totalMismatchR += thisR;
416 totalMismatchG += thisG;
417 totalMismatchB += thisB;
418 // In HSV, value is defined as max RGB component.
419 int value = MAX3(thisR, thisG, thisB);
420 dr->fWeightedFraction += ((float) value) / 255;
421 if (thisR > dr->fMaxMismatchR) {
422 dr->fMaxMismatchR = thisR;
424 if (thisG > dr->fMaxMismatchG) {
425 dr->fMaxMismatchG = thisG;
427 if (thisB > dr->fMaxMismatchB) {
428 dr->fMaxMismatchB = thisB;
430 if (!colors_match_thresholded(c0, c1, colorThreshold)) {
432 *dr->fDifferenceBitmap->getAddr32(x, y) = outputDifference;
433 *dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_WHITE;
435 *dr->fDifferenceBitmap->getAddr32(x, y) = 0;
436 *dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_BLACK;
440 int pixelCount = w * h;
441 dr->fFractionDifference = ((float) mismatchedPixels) / pixelCount;
442 dr->fWeightedFraction /= pixelCount;
443 dr->fAverageMismatchR = ((float) totalMismatchR) / pixelCount;
444 dr->fAverageMismatchG = ((float) totalMismatchG) / pixelCount;
445 dr->fAverageMismatchB = ((float) totalMismatchB) / pixelCount;
448 static SkString filename_to_derived_filename (const SkString& filename,
449 const char *suffix) {
450 SkString diffName (filename);
451 const char* cstring = diffName.c_str();
452 int dotOffset = strrchr(cstring, '.') - cstring;
453 diffName.remove(dotOffset, diffName.size() - dotOffset);
454 diffName.append(suffix);
458 /// Given a image filename, returns the name of the file containing the
459 /// associated difference image.
460 static SkString filename_to_diff_filename (const SkString& filename) {
461 return filename_to_derived_filename(filename, "-diff.png");
464 /// Given a image filename, returns the name of the file containing the
465 /// "white" difference image.
466 static SkString filename_to_white_filename (const SkString& filename) {
467 return filename_to_derived_filename(filename, "-white.png");
470 /// Convert a chromium/WebKit LayoutTest "foo-expected.png" to "foo-actual.png"
471 static SkString chrome_expected_path_to_actual (const SkString& expected) {
472 SkString actualPath (expected);
473 actualPath.remove(actualPath.size() - 13, 13);
474 actualPath.append("-actual.png");
478 /// Convert a chromium/WebKit LayoutTest "foo-expected.png" to "foo.png"
479 static SkString chrome_expected_name_to_short (const SkString& expected) {
480 SkString shortName (expected);
481 shortName.remove(shortName.size() - 13, 13);
482 shortName.append(".png");
486 static void release_bitmaps(DiffRecord* drp) {
487 delete drp->fBaseBitmap;
488 drp->fBaseBitmap = NULL;
489 delete drp->fComparisonBitmap;
490 drp->fComparisonBitmap = NULL;
491 delete drp->fDifferenceBitmap;
492 drp->fDifferenceBitmap = NULL;
493 delete drp->fWhiteBitmap;
494 drp->fWhiteBitmap = NULL;
498 /// If outputDir.isEmpty(), don't write out diff files.
499 static void create_and_write_diff_image(DiffRecord* drp,
501 const int colorThreshold,
502 const SkString& outputDir,
503 const SkString& filename) {
504 const int w = drp->fBaseWidth;
505 const int h = drp->fBaseHeight;
506 drp->fDifferenceBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h);
507 drp->fDifferenceBitmap->allocPixels();
508 drp->fWhiteBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h);
509 drp->fWhiteBitmap->allocPixels();
510 compute_diff(drp, dmp, colorThreshold);
512 if (!outputDir.isEmpty()) {
513 SkString differencePath (outputDir);
514 differencePath.append(filename_to_diff_filename(filename));
515 write_bitmap(differencePath, drp->fDifferenceBitmap);
516 SkString whitePath (outputDir);
517 whitePath.append(filename_to_white_filename(filename));
518 write_bitmap(whitePath, drp->fWhiteBitmap);
521 release_bitmaps(drp);
524 /// Returns true if string contains any of these substrings.
525 static bool string_contains_any_of(const SkString& string,
526 const StringArray& substrings) {
527 for (int i = 0; i < substrings.count(); i++) {
528 if (string.contains(substrings[i]->c_str())) {
535 /// Iterate over dir and get all files that:
536 /// - match any of the substrings in matchSubstrings, but...
537 /// - DO NOT match any of the substrings in nomatchSubstrings
538 /// Returns the list of files in *files.
539 static void get_file_list(const SkString& dir,
540 const StringArray& matchSubstrings,
541 const StringArray& nomatchSubstrings,
543 SkOSFile::Iter it(dir.c_str());
545 while (it.next(&filename)) {
546 if (string_contains_any_of(filename, matchSubstrings) &&
547 !string_contains_any_of(filename, nomatchSubstrings)) {
548 files->push(new SkString(filename));
553 static void release_file_list(FileArray *files) {
557 /// Comparison routines for qsort, sort by file names.
558 static int compare_file_name_metrics(SkString **lhs, SkString **rhs) {
559 return strcmp((*lhs)->c_str(), (*rhs)->c_str());
562 /// Creates difference images, returns the number that have a 0 metric.
563 /// If outputDir.isEmpty(), don't write out diff files.
564 static void create_diff_images (DiffMetricProc dmp,
565 const int colorThreshold,
566 RecordArray* differences,
567 const SkString& baseDir,
568 const SkString& comparisonDir,
569 const SkString& outputDir,
570 const StringArray& matchSubstrings,
571 const StringArray& nomatchSubstrings,
572 DiffSummary* summary) {
573 SkASSERT(!baseDir.isEmpty());
574 SkASSERT(!comparisonDir.isEmpty());
577 FileArray comparisonFiles;
579 get_file_list(baseDir, matchSubstrings, nomatchSubstrings, &baseFiles);
580 get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings,
583 if (!baseFiles.isEmpty()) {
584 qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
585 SkCastForQSort(compare_file_name_metrics));
587 if (!comparisonFiles.isEmpty()) {
588 qsort(comparisonFiles.begin(), comparisonFiles.count(),
589 sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
595 while (i < baseFiles.count() &&
596 j < comparisonFiles.count()) {
598 SkString basePath (baseDir);
599 basePath.append(*baseFiles[i]);
600 SkString comparisonPath (comparisonDir);
601 comparisonPath.append(*comparisonFiles[j]);
603 DiffRecord *drp = NULL;
604 int v = strcmp(baseFiles[i]->c_str(),
605 comparisonFiles[j]->c_str());
608 // in baseDir, but not in comparisonDir
609 drp = new DiffRecord(*baseFiles[i],
610 basePath, comparisonPath, false, true);
613 // in comparisonDir, but not in baseDir
614 drp = new DiffRecord(*comparisonFiles[j],
615 basePath, comparisonPath, true, false);
619 drp = new DiffRecord(*baseFiles[i], basePath, comparisonPath);
621 if (get_bitmaps(drp)) {
622 create_and_write_diff_image(drp, dmp, colorThreshold,
623 outputDir, *baseFiles[i]);
630 differences->push(drp);
634 for (; i < baseFiles.count(); ++i) {
635 // files only in baseDir
636 SkString basePath (baseDir);
637 basePath.append(*baseFiles[i]);
638 SkString comparisonPath;
639 DiffRecord *drp = new DiffRecord(*baseFiles[i], basePath,
640 comparisonPath, false, true);
641 differences->push(drp);
645 for (; j < comparisonFiles.count(); ++j) {
646 // files only in comparisonDir
648 SkString comparisonPath(comparisonDir);
649 comparisonPath.append(*comparisonFiles[j]);
650 DiffRecord *drp = new DiffRecord(*comparisonFiles[j], basePath,
651 comparisonPath, true, false);
652 differences->push(drp);
656 release_file_list(&baseFiles);
657 release_file_list(&comparisonFiles);
660 static void create_diff_images_chromium (DiffMetricProc dmp,
661 const int colorThreshold,
662 RecordArray* differences,
663 const SkString& dirname,
664 const SkString& outputDir,
665 DiffSummary* summary) {
666 SkASSERT(!outputDir.isEmpty());
668 SkOSFile::Iter baseIterator (dirname.c_str());
670 while (baseIterator.next(&filename)) {
671 if (filename.endsWith(".pdf")) {
674 if (filename.endsWith("-expected.png")) {
675 SkString expectedPath (dirname);
676 expectedPath.append(filename);
677 SkString shortName (chrome_expected_name_to_short(filename));
678 SkString actualPath (chrome_expected_path_to_actual(expectedPath));
680 new DiffRecord (shortName, expectedPath, actualPath);
681 if (!get_bitmaps(drp)) {
684 create_and_write_diff_image(drp, dmp, colorThreshold,
685 outputDir, shortName);
687 differences->push(drp);
693 static void analyze_chromium(DiffMetricProc dmp,
694 const int colorThreshold,
695 RecordArray* differences,
696 const SkString& dirname,
697 const SkString& outputDir,
698 DiffSummary* summary) {
699 SkASSERT(!outputDir.isEmpty());
700 create_diff_images_chromium(dmp, colorThreshold, differences,
701 dirname, outputDir, summary);
702 SkOSFile::Iter dirIterator(dirname.c_str());
704 while (dirIterator.next(&newdirname, true)) {
705 if (newdirname.startsWith(".")) {
708 SkString fullname (dirname);
709 fullname.append(newdirname);
710 if (!fullname.endsWith(PATH_DIV_STR)) {
711 fullname.append(PATH_DIV_STR);
713 analyze_chromium(dmp, colorThreshold, differences,
714 fullname, outputDir, summary);
718 /// Make layout more consistent by scaling image to 240 height, 360 width,
719 /// or natural size, whichever is smallest.
720 static int compute_image_height (int height, int width) {
722 if (height < retval) {
725 float scale = (float) retval / height;
726 if (width * scale > 360) {
727 scale = (float) 360 / width;
728 retval = static_cast<int>(height * scale);
733 static void print_table_header (SkFILEWStream* stream,
734 const int matchCount,
735 const int colorThreshold,
736 const RecordArray& differences,
737 const SkString &baseDir,
738 const SkString &comparisonDir,
739 bool doOutputDate=false) {
740 stream->writeText("<table>\n");
741 stream->writeText("<tr><th>");
744 SkTime::GetDateTime(&dt);
745 stream->writeText("SkDiff run at ");
746 stream->writeDecAsText(dt.fHour);
747 stream->writeText(":");
748 if (dt.fMinute < 10) {
749 stream->writeText("0");
751 stream->writeDecAsText(dt.fMinute);
752 stream->writeText(":");
753 if (dt.fSecond < 10) {
754 stream->writeText("0");
756 stream->writeDecAsText(dt.fSecond);
757 stream->writeText("<br>");
759 stream->writeDecAsText(matchCount);
760 stream->writeText(" of ");
761 stream->writeDecAsText(differences.count());
762 stream->writeText(" images matched ");
763 if (colorThreshold == 0) {
764 stream->writeText("exactly");
766 stream->writeText("within ");
767 stream->writeDecAsText(colorThreshold);
768 stream->writeText(" color units per component");
770 stream->writeText(".<br>");
771 stream->writeText("</th>\n<th>");
772 stream->writeText("every different pixel shown in white");
773 stream->writeText("</th>\n<th>");
774 stream->writeText("color difference at each pixel");
775 stream->writeText("</th>\n<th>");
776 stream->writeText(baseDir.c_str());
777 stream->writeText("</th>\n<th>");
778 stream->writeText(comparisonDir.c_str());
779 stream->writeText("</th>\n");
780 stream->writeText("</tr>\n");
783 static void print_pixel_count (SkFILEWStream* stream,
784 const DiffRecord& diff) {
785 stream->writeText("<br>(");
786 stream->writeDecAsText(static_cast<int>(diff.fFractionDifference *
789 stream->writeText(" pixels)");
791 stream->writeDecAsText(diff.fWeightedFraction *
794 stream->writeText(" weighted pixels)");
798 static void print_label_cell (SkFILEWStream* stream,
799 const DiffRecord& diff) {
800 stream->writeText("<td>");
801 stream->writeText(diff.fFilename.c_str());
802 stream->writeText("<br>");
803 if (diff.fBaseMissing || diff.fComparisonMissing) {
804 stream->writeText("</td>");
807 if (diff.fDoImageSizesMismatch) {
808 stream->writeText("Image sizes differ");
809 stream->writeText("</td>");
813 sprintf(metricBuf, "%12.4f%%", 100 * diff.fFractionDifference);
814 stream->writeText(metricBuf);
815 stream->writeText(" of pixels differ");
816 stream->writeText("\n (");
817 sprintf(metricBuf, "%12.4f%%", 100 * diff.fWeightedFraction);
818 stream->writeText(metricBuf);
819 stream->writeText(" weighted)");
820 // Write the actual number of pixels that differ if it's < 1%
821 if (diff.fFractionDifference < 0.01) {
822 print_pixel_count(stream, diff);
824 stream->writeText("<br>Average color mismatch ");
825 stream->writeDecAsText(static_cast<int>(MAX3(diff.fAverageMismatchR,
826 diff.fAverageMismatchG,
827 diff.fAverageMismatchB)));
828 stream->writeText("<br>Max color mismatch ");
829 stream->writeDecAsText(MAX3(diff.fMaxMismatchR,
831 diff.fMaxMismatchB));
832 stream->writeText("</td>");
835 static void print_image_cell (SkFILEWStream* stream,
836 const SkString& path,
838 stream->writeText("<td><a href=\"");
839 stream->writeText(path.c_str());
840 stream->writeText("\"><img src=\"");
841 stream->writeText(path.c_str());
842 stream->writeText("\" height=\"");
843 stream->writeDecAsText(height);
844 stream->writeText("px\"></a></td>");
847 static void print_text_cell (SkFILEWStream* stream, const char* text) {
848 stream->writeText("<td align=center>");
850 stream->writeText(text);
852 stream->writeText("</td>");
855 static void print_diff_with_missing_file(SkFILEWStream* stream,
857 const SkString& relativePath) {
858 stream->writeText("<tr>\n");
859 print_label_cell(stream, diff);
860 stream->writeText("<td>N/A</td>");
861 stream->writeText("<td>N/A</td>");
862 if (!diff.fBaseMissing) {
864 if (!get_bitmap_height_width(diff.fBasePath, &h, &w)) {
865 stream->writeText("<td>N/A</td>");
867 int height = compute_image_height(h, w);
868 if (!diff.fBasePath.startsWith(PATH_DIV_STR)) {
869 diff.fBasePath.prepend(relativePath);
871 print_image_cell(stream, diff.fBasePath, height);
874 stream->writeText("<td>N/A</td>");
876 if (!diff.fComparisonMissing) {
878 if (!get_bitmap_height_width(diff.fComparisonPath, &h, &w)) {
879 stream->writeText("<td>N/A</td>");
881 int height = compute_image_height(h, w);
882 if (!diff.fComparisonPath.startsWith(PATH_DIV_STR)) {
883 diff.fComparisonPath.prepend(relativePath);
885 print_image_cell(stream, diff.fComparisonPath, height);
888 stream->writeText("<td>N/A</td>");
890 stream->writeText("</tr>\n");
894 static void print_diff_page (const int matchCount,
895 const int colorThreshold,
896 const RecordArray& differences,
897 const SkString& baseDir,
898 const SkString& comparisonDir,
899 const SkString& outputDir) {
901 SkASSERT(!baseDir.isEmpty());
902 SkASSERT(!comparisonDir.isEmpty());
903 SkASSERT(!outputDir.isEmpty());
905 SkString outputPath (outputDir);
906 outputPath.append("index.html");
907 //SkFILEWStream outputStream ("index.html");
908 SkFILEWStream outputStream (outputPath.c_str());
910 // Need to convert paths from relative-to-cwd to relative-to-outputDir
911 // FIXME this doesn't work if there are '..' inside the outputDir
913 SkString relativePath;
914 for (ui = 0; ui < outputDir.size(); ui++) {
915 if (outputDir[ui] == PATH_DIV_CHAR) {
916 relativePath.append(".." PATH_DIV_STR);
920 outputStream.writeText("<html>\n<body>\n");
921 print_table_header(&outputStream, matchCount, colorThreshold, differences,
922 baseDir, comparisonDir);
924 for (i = 0; i < differences.count(); i++) {
925 DiffRecord* diff = differences[i];
927 if (diff->fBaseMissing || diff->fComparisonMissing) {
928 print_diff_with_missing_file(&outputStream, *diff, relativePath);
930 } else if (0 == diff->fFractionDifference) {
934 if (!diff->fBasePath.startsWith(PATH_DIV_STR)) {
935 diff->fBasePath.prepend(relativePath);
937 if (!diff->fComparisonPath.startsWith(PATH_DIV_STR)) {
938 diff->fComparisonPath.prepend(relativePath);
941 int height = compute_image_height(diff->fBaseHeight, diff->fBaseWidth);
942 outputStream.writeText("<tr>\n");
943 print_label_cell(&outputStream, *diff);
944 if (diff->fDoImageSizesMismatch) {
945 print_text_cell(&outputStream,
946 "[image size mismatch, so no diff to display]");
947 print_text_cell(&outputStream,
948 "[image size mismatch, so no diff to display]");
950 print_image_cell(&outputStream,
951 filename_to_white_filename(diff->fFilename), height);
952 print_image_cell(&outputStream,
953 filename_to_diff_filename(diff->fFilename), height);
955 print_image_cell(&outputStream, diff->fBasePath, height);
956 print_image_cell(&outputStream, diff->fComparisonPath, height);
957 outputStream.writeText("</tr>\n");
958 outputStream.flush();
960 outputStream.writeText("</table>\n");
961 outputStream.writeText("</body>\n</html>\n");
962 outputStream.flush();
965 static void usage (char * argv0) {
966 SkDebugf("Skia baseline image diff tool\n");
969 " %s <baseDir> <comparisonDir> [outputDir] \n"
971 " %s --chromium-release|--chromium-debug <baseDir> <outputDir> \n",
975 " --nodiffs: don't write out image diffs or index.html, just generate \n"
976 " report on stdout \n"
977 " --threshold <n>: only report differences > n (per color channel) [default 0]\n"
978 " --match: compare files whose filenames contain this substring; if \n"
979 " unspecified, compare ALL files. \n"
980 " this flag may be repeated to add more matching substrings. \n"
981 " --nomatch: regardless of --match, DO NOT compare files whose filenames \n"
982 " contain this substring. \n"
983 " this flag may be repeated to add more forbidden substrings. \n"
984 " --sortbymismatch: sort by average color channel mismatch\n");
986 " --sortbymaxmismatch: sort by worst color channel mismatch;\n"
987 " break ties with -sortbymismatch\n"
988 " [default sort is by fraction of pixels mismatching]\n");
990 " --weighted: sort by # pixels different weighted by color difference\n");
992 " --chromium-release: process Webkit LayoutTests results instead of gm\n"
993 " --chromium-debug: process Webkit LayoutTests results instead of gm\n");
995 " baseDir: directory to read baseline images from,\n"
996 " or chromium/src directory for --chromium.\n");
998 " comparisonDir: directory to read comparison images from\n");
1000 " outputDir: directory to write difference images and index.html to; \n"
1001 " defaults to comparisonDir when not running --chromium \n");
1004 int main (int argc, char ** argv) {
1005 DiffMetricProc diffProc = compute_diff_pmcolor;
1006 int (*sortProc)(const void*, const void*) = SkCastForQSort(compare_diff_metrics);
1008 // Maximum error tolerated in any one color channel in any one pixel before
1009 // a difference is reported.
1010 int colorThreshold = 0;
1012 SkString comparisonDir;
1014 StringArray matchSubstrings;
1015 StringArray nomatchSubstrings;
1017 bool analyzeChromium = false;
1018 bool chromiumDebug = false;
1019 bool chromiumRelease = false;
1020 bool generateDiffs = true;
1022 RecordArray differences;
1023 DiffSummary summary;
1026 int numUnflaggedArguments = 0;
1027 for (i = 1; i < argc; i++) {
1028 if (!strcmp(argv[i], "--help")) {
1032 if (!strcmp(argv[i], "--nodiffs")) {
1033 generateDiffs = false;
1036 if (!strcmp(argv[i], "--threshold")) {
1037 colorThreshold = atoi(argv[++i]);
1040 if (!strcmp(argv[i], "--match")) {
1041 matchSubstrings.push(new SkString(argv[++i]));
1044 if (!strcmp(argv[i], "--nomatch")) {
1045 nomatchSubstrings.push(new SkString(argv[++i]));
1048 if (!strcmp(argv[i], "--sortbymismatch")) {
1049 sortProc = SkCastForQSort(compare_diff_mean_mismatches);
1052 if (!strcmp(argv[i], "--sortbymaxmismatch")) {
1053 sortProc = SkCastForQSort(compare_diff_max_mismatches);
1056 if (!strcmp(argv[i], "--weighted")) {
1057 sortProc = SkCastForQSort(compare_diff_weighted);
1060 if (!strcmp(argv[i], "--chromium-release")) {
1061 analyzeChromium = true;
1062 chromiumRelease = true;
1065 if (!strcmp(argv[i], "--chromium-debug")) {
1066 analyzeChromium = true;
1067 chromiumDebug = true;
1070 if (argv[i][0] != '-') {
1071 switch (numUnflaggedArguments++) {
1073 baseDir.set(argv[i]);
1076 comparisonDir.set(argv[i]);
1079 outputDir.set(argv[i]);
1082 SkDebugf("extra unflagged argument <%s>\n", argv[i]);
1088 SkDebugf("Unrecognized argument <%s>\n", argv[i]);
1093 if (analyzeChromium) {
1094 if (numUnflaggedArguments != 2) {
1098 outputDir = comparisonDir;
1099 if (chromiumRelease && chromiumDebug) {
1101 "--chromium must be either -release or -debug, not both!\n");
1106 if (numUnflaggedArguments == 2) {
1107 outputDir = comparisonDir;
1108 } else if (numUnflaggedArguments != 3) {
1113 if (!baseDir.endsWith(PATH_DIV_STR)) {
1114 baseDir.append(PATH_DIV_STR);
1116 printf("baseDir is [%s]\n", baseDir.c_str());
1118 if (!comparisonDir.endsWith(PATH_DIV_STR)) {
1119 comparisonDir.append(PATH_DIV_STR);
1121 printf("comparisonDir is [%s]\n", comparisonDir.c_str());
1123 if (!outputDir.endsWith(PATH_DIV_STR)) {
1124 outputDir.append(PATH_DIV_STR);
1126 if (generateDiffs) {
1127 printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
1129 printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
1133 // Default substring matching:
1134 // - No matter what, don't match any PDF files.
1135 // We may want to change this later, but for now this maintains the filter
1136 // that get_file_list() used to always apply.
1137 // - If no matchSubstrings were specified, match ALL strings.
1138 nomatchSubstrings.push(new SkString(".pdf"));
1139 if (matchSubstrings.isEmpty()) {
1140 matchSubstrings.push(new SkString(""));
1143 if (analyzeChromium) {
1144 baseDir.append("webkit" PATH_DIV_STR);
1145 if (chromiumRelease) {
1146 baseDir.append("Release" PATH_DIV_STR);
1148 if (chromiumDebug) {
1149 baseDir.append("Debug" PATH_DIV_STR);
1151 baseDir.append("layout-test-results" PATH_DIV_STR);
1152 analyze_chromium(diffProc, colorThreshold, &differences,
1153 baseDir, outputDir, &summary);
1155 create_diff_images(diffProc, colorThreshold, &differences,
1156 baseDir, comparisonDir, outputDir,
1157 matchSubstrings, nomatchSubstrings, &summary);
1161 if (differences.count()) {
1162 qsort(differences.begin(), differences.count(),
1163 sizeof(DiffRecord*), sortProc);
1166 if (generateDiffs) {
1167 print_diff_page(summary.fNumMatches, colorThreshold, differences,
1168 baseDir, comparisonDir, outputDir);
1171 for (i = 0; i < differences.count(); i++) {
1172 delete differences[i];
1174 matchSubstrings.deleteAll();
1175 nomatchSubstrings.deleteAll();