skia2/tools/skdiff_main.cpp
epoger@google.com be6188d647 skdiff: return nonzero exit value if there are any mismatches
This will allow us to replace "gm -r" in our buildbots with "skdiff".  This will speed things up, and work around http://code.google.com/p/skia/issues/detail?id=473 ('PDF gradtext gm image results are nondeterministic')
Review URL: https://codereview.appspot.com/6242071

git-svn-id: http://skia.googlecode.com/svn/trunk@4097 2bbb7eff-a529-9590-31e7-b0007b416f81
2012-05-31 15:13:45 +00:00

1178 lines
41 KiB
C++

/*
* Copyright 2011 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "SkColorPriv.h"
#include "SkData.h"
#include "SkImageDecoder.h"
#include "SkImageEncoder.h"
#include "SkOSFile.h"
#include "SkStream.h"
#include "SkTDArray.h"
#include "SkTemplates.h"
#include "SkTime.h"
#include "SkTSearch.h"
#include "SkTypes.h"
/**
* skdiff
*
* Given three directory names, expects to find identically-named files in
* each of the first two; the first are treated as a set of baseline,
* the second a set of variant images, and a diff image is written into the
* third directory for each pair.
* Creates an index.html in the current third directory to compare each
* pair that does not match exactly.
* Does *not* recursively descend directories.
*
* Returns zero exit code if all images match across baseDir and comparisonDir.
*/
#if SK_BUILD_FOR_WIN32
#define PATH_DIV_STR "\\"
#define PATH_DIV_CHAR '\\'
#else
#define PATH_DIV_STR "/"
#define PATH_DIV_CHAR '/'
#endif
// Result of comparison for each pair of files.
enum Result {
kEqualBits, // both files in the pair contain exactly the same bits
kEqualPixels, // not bitwise equal, but their pixels are exactly the same
kDifferentSizes, // both are images we can parse, but of different sizes
kDifferentPixels,// both are images we can parse, but with different pixels
kDifferentOther, // files have different bits but are not parsable images
kBaseMissing, // missing from baseDir
kComparisonMissing,// missing from comparisonDir
kUnknown, // not compared yet
//
kNumResultTypes // NOT A VALID VALUE--used to set up arrays. Must be last.
};
struct DiffRecord {
DiffRecord (const SkString filename,
const SkString basePath,
const SkString comparisonPath,
const Result result = kUnknown)
: fFilename (filename)
, fBasePath (basePath)
, fComparisonPath (comparisonPath)
, fBaseBitmap (new SkBitmap ())
, fComparisonBitmap (new SkBitmap ())
, fDifferenceBitmap (new SkBitmap ())
, fWhiteBitmap (new SkBitmap ())
, fBaseHeight (0)
, fBaseWidth (0)
, fFractionDifference (0)
, fWeightedFraction (0)
, fAverageMismatchR (0)
, fAverageMismatchG (0)
, fAverageMismatchB (0)
, fMaxMismatchR (0)
, fMaxMismatchG (0)
, fMaxMismatchB (0)
, fResult(result) {
};
SkString fFilename;
SkString fBasePath;
SkString fComparisonPath;
SkBitmap* fBaseBitmap;
SkBitmap* fComparisonBitmap;
SkBitmap* fDifferenceBitmap;
SkBitmap* fWhiteBitmap;
int fBaseHeight;
int fBaseWidth;
/// Arbitrary floating-point metric to be used to sort images from most
/// to least different from baseline; values of 0 will be omitted from the
/// summary webpage.
float fFractionDifference;
float fWeightedFraction;
float fAverageMismatchR;
float fAverageMismatchG;
float fAverageMismatchB;
uint32_t fMaxMismatchR;
uint32_t fMaxMismatchG;
uint32_t fMaxMismatchB;
/// Which category of diff result.
Result fResult;
};
#define MAX2(a,b) (((b) < (a)) ? (a) : (b))
#define MAX3(a,b,c) (((b) < (a)) ? MAX2((a), (c)) : MAX2((b), (c)))
const SkPMColor PMCOLOR_WHITE = SkPreMultiplyColor(SK_ColorWHITE);
const SkPMColor PMCOLOR_BLACK = SkPreMultiplyColor(SK_ColorBLACK);
typedef SkTDArray<SkString*> StringArray;
typedef StringArray FileArray;
struct DiffSummary {
DiffSummary ()
: fNumMatches (0)
, fNumMismatches (0)
, fMaxMismatchV (0)
, fMaxMismatchPercent (0) { };
~DiffSummary() {
for (int i = 0; i < kNumResultTypes; i++) {
fResultsOfType[i].deleteAll();
}
}
uint32_t fNumMatches;
uint32_t fNumMismatches;
uint32_t fMaxMismatchV;
float fMaxMismatchPercent;
FileArray fResultsOfType[kNumResultTypes];
// Print the contents of this FileArray, if any, to stdout.
// (If the FileArray is empty, print nothing.)
void printContents(const FileArray& fileArray, const char* headerText) {
int n = fileArray.count();
if (n > 0) {
printf("%s:\n", headerText);
for (int i = 0; i < n; ++i) {
printf("\t%s\n", fileArray[i]->c_str());
}
}
}
void print () {
printContents(fResultsOfType[kBaseMissing],
"Missing in baseDir");
printContents(fResultsOfType[kComparisonMissing],
"Missing in comparisonDir");
printf("%d of %d images matched.\n", fNumMatches,
fNumMatches + fNumMismatches);
if (fNumMismatches > 0) {
printf("Maximum pixel intensity mismatch %d\n", fMaxMismatchV);
printf("Largest area mismatch was %.2f%% of pixels\n",
fMaxMismatchPercent);
}
}
void add (DiffRecord* drp) {
uint32_t mismatchValue;
fResultsOfType[drp->fResult].push(new SkString(drp->fFilename));
switch (drp->fResult) {
case kEqualBits:
fNumMatches++;
break;
case kEqualPixels:
fNumMatches++;
break;
case kDifferentSizes:
fNumMismatches++;
drp->fFractionDifference = 2.0;// sort as if 200% of pixels differed
break;
case kDifferentPixels:
fNumMismatches++;
if (drp->fFractionDifference * 100 > fMaxMismatchPercent) {
fMaxMismatchPercent = drp->fFractionDifference * 100;
}
mismatchValue = MAX3(drp->fMaxMismatchR, drp->fMaxMismatchG,
drp->fMaxMismatchB);
if (mismatchValue > fMaxMismatchV) {
fMaxMismatchV = mismatchValue;
}
break;
case kDifferentOther:
fNumMismatches++;
drp->fFractionDifference = 3.0;// sort as if 300% of pixels differed
break;
case kBaseMissing:
fNumMismatches++;
break;
case kComparisonMissing:
fNumMismatches++;
break;
case kUnknown:
SkDEBUGFAIL("adding uncategorized DiffRecord");
break;
default:
SkDEBUGFAIL("adding DiffRecord with unhandled fResult value");
break;
}
}
};
typedef SkTDArray<DiffRecord*> RecordArray;
/// Comparison routine for qsort; sorts by fFractionDifference
/// from largest to smallest.
static int compare_diff_metrics (DiffRecord** lhs, DiffRecord** rhs) {
if ((*lhs)->fFractionDifference < (*rhs)->fFractionDifference) {
return 1;
}
if ((*rhs)->fFractionDifference < (*lhs)->fFractionDifference) {
return -1;
}
return 0;
}
static int compare_diff_weighted (DiffRecord** lhs, DiffRecord** rhs) {
if ((*lhs)->fWeightedFraction < (*rhs)->fWeightedFraction) {
return 1;
}
if ((*lhs)->fWeightedFraction > (*rhs)->fWeightedFraction) {
return -1;
}
return 0;
}
/// Comparison routine for qsort; sorts by max(fAverageMismatch{RGB})
/// from largest to smallest.
static int compare_diff_mean_mismatches (DiffRecord** lhs, DiffRecord** rhs) {
float leftValue = MAX3((*lhs)->fAverageMismatchR,
(*lhs)->fAverageMismatchG,
(*lhs)->fAverageMismatchB);
float rightValue = MAX3((*rhs)->fAverageMismatchR,
(*rhs)->fAverageMismatchG,
(*rhs)->fAverageMismatchB);
if (leftValue < rightValue) {
return 1;
}
if (rightValue < leftValue) {
return -1;
}
return 0;
}
/// Comparison routine for qsort; sorts by max(fMaxMismatch{RGB})
/// from largest to smallest.
static int compare_diff_max_mismatches (DiffRecord** lhs, DiffRecord** rhs) {
uint32_t leftValue = MAX3((*lhs)->fMaxMismatchR,
(*lhs)->fMaxMismatchG,
(*lhs)->fMaxMismatchB);
uint32_t rightValue = MAX3((*rhs)->fMaxMismatchR,
(*rhs)->fMaxMismatchG,
(*rhs)->fMaxMismatchB);
if (leftValue < rightValue) {
return 1;
}
if (rightValue < leftValue) {
return -1;
}
return compare_diff_mean_mismatches(lhs, rhs);
}
/// Parameterized routine to compute the color of a pixel in a difference image.
typedef SkPMColor (*DiffMetricProc)(SkPMColor, SkPMColor);
static void expand_and_copy (int width, int height, SkBitmap** dest) {
SkBitmap* temp = new SkBitmap ();
temp->reset();
temp->setConfig((*dest)->config(), width, height);
temp->allocPixels();
(*dest)->copyPixelsTo(temp->getPixels(), temp->getSize(),
temp->rowBytes());
*dest = temp;
}
/// Returns true if the two buffers passed in are both non-NULL, and include
/// exactly the same byte values (and identical lengths).
static bool are_buffers_equal(SkData* skdata1, SkData* skdata2) {
if ((NULL == skdata1) || (NULL == skdata2)) {
return false;
}
if (skdata1->size() != skdata2->size()) {
return false;
}
return (0 == memcmp(skdata1->data(), skdata2->data(), skdata1->size()));
}
/// Reads the file at the given path and returns its complete contents as an
/// SkData object (or returns NULL on error).
static SkData* read_file(const char* file_path) {
SkFILEStream fileStream(file_path);
if (!fileStream.isValid()) {
SkDebugf("WARNING: could not open file <%s> for reading\n", file_path);
return NULL;
}
size_t bytesInFile = fileStream.getLength();
size_t bytesLeftToRead = bytesInFile;
void* bufferStart = sk_malloc_throw(bytesInFile);
char* bufferPointer = (char*)bufferStart;
while (bytesLeftToRead > 0) {
size_t bytesReadThisTime = fileStream.read(
bufferPointer, bytesLeftToRead);
if (0 == bytesReadThisTime) {
SkDebugf("WARNING: error reading from <%s>\n", file_path);
sk_free(bufferStart);
return NULL;
}
bytesLeftToRead -= bytesReadThisTime;
bufferPointer += bytesReadThisTime;
}
return SkData::NewFromMalloc(bufferStart, bytesInFile);
}
/// Decodes binary contents of baseFile and comparisonFile into
/// diffRecord->fBaseBitmap and diffRecord->fComparisonBitmap.
/// Returns true if that succeeds.
static bool get_bitmaps (SkData* baseFileContents,
SkData* comparisonFileContents,
DiffRecord* diffRecord) {
SkMemoryStream compareStream(comparisonFileContents->data(),
comparisonFileContents->size());
SkMemoryStream baseStream(baseFileContents->data(),
baseFileContents->size());
SkImageDecoder* codec = SkImageDecoder::Factory(&baseStream);
if (NULL == codec) {
SkDebugf("ERROR: no codec found for basePath <%s>\n",
diffRecord->fBasePath.c_str());
return false;
}
// In debug, the DLL will automatically be unloaded when this is deleted,
// but that shouldn't be a problem in release mode.
SkAutoTDelete<SkImageDecoder> ad(codec);
baseStream.rewind();
if (!codec->decode(&baseStream, diffRecord->fBaseBitmap,
SkBitmap::kARGB_8888_Config,
SkImageDecoder::kDecodePixels_Mode)) {
SkDebugf("ERROR: codec failed for basePath <%s>\n",
diffRecord->fBasePath.c_str());
return false;
}
diffRecord->fBaseWidth = diffRecord->fBaseBitmap->width();
diffRecord->fBaseHeight = diffRecord->fBaseBitmap->height();
if (!codec->decode(&compareStream, diffRecord->fComparisonBitmap,
SkBitmap::kARGB_8888_Config,
SkImageDecoder::kDecodePixels_Mode)) {
SkDebugf("ERROR: codec failed for comparisonPath <%s>\n",
diffRecord->fComparisonPath.c_str());
return false;
}
return true;
}
static bool get_bitmap_height_width(const SkString& path,
int *height, int *width) {
SkFILEStream stream(path.c_str());
if (!stream.isValid()) {
SkDebugf("ERROR: couldn't open file <%s>\n",
path.c_str());
return false;
}
SkImageDecoder* codec = SkImageDecoder::Factory(&stream);
if (NULL == codec) {
SkDebugf("ERROR: no codec found for <%s>\n",
path.c_str());
return false;
}
SkAutoTDelete<SkImageDecoder> ad(codec);
SkBitmap bm;
stream.rewind();
if (!codec->decode(&stream, &bm,
SkBitmap::kARGB_8888_Config,
SkImageDecoder::kDecodePixels_Mode)) {
SkDebugf("ERROR: codec failed for <%s>\n",
path.c_str());
return false;
}
*height = bm.height();
*width = bm.width();
return true;
}
// from gm - thanks to PNG, we need to force all pixels 100% opaque
static void force_all_opaque(const SkBitmap& bitmap) {
SkAutoLockPixels lock(bitmap);
for (int y = 0; y < bitmap.height(); y++) {
for (int x = 0; x < bitmap.width(); x++) {
*bitmap.getAddr32(x, y) |= (SK_A32_MASK << SK_A32_SHIFT);
}
}
}
// from gm
static bool write_bitmap(const SkString& path, const SkBitmap* bitmap) {
SkBitmap copy;
bitmap->copyTo(&copy, SkBitmap::kARGB_8888_Config);
force_all_opaque(copy);
return SkImageEncoder::EncodeFile(path.c_str(), copy,
SkImageEncoder::kPNG_Type, 100);
}
// from gm
static inline SkPMColor compute_diff_pmcolor(SkPMColor c0, SkPMColor c1) {
int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
return SkPackARGB32(0xFF, SkAbs32(dr), SkAbs32(dg), SkAbs32(db));
}
static inline bool colors_match_thresholded(SkPMColor c0, SkPMColor c1,
const int threshold) {
int da = SkGetPackedA32(c0) - SkGetPackedA32(c1);
int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
return ((SkAbs32(da) <= threshold) &&
(SkAbs32(dr) <= threshold) &&
(SkAbs32(dg) <= threshold) &&
(SkAbs32(db) <= threshold));
}
// based on gm
// Postcondition: when we exit this method, dr->fResult should have some value
// other than kUnknown.
static void compute_diff(DiffRecord* dr,
DiffMetricProc diffFunction,
const int colorThreshold) {
SkAutoLockPixels alpDiff(*dr->fDifferenceBitmap);
SkAutoLockPixels alpWhite(*dr->fWhiteBitmap);
const int w = dr->fComparisonBitmap->width();
const int h = dr->fComparisonBitmap->height();
int mismatchedPixels = 0;
int totalMismatchR = 0;
int totalMismatchG = 0;
int totalMismatchB = 0;
if (w != dr->fBaseWidth || h != dr->fBaseHeight) {
dr->fResult = kDifferentSizes;
return;
}
// Accumulate fractionally different pixels, then divide out
// # of pixels at the end.
dr->fWeightedFraction = 0;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
SkPMColor c0 = *dr->fBaseBitmap->getAddr32(x, y);
SkPMColor c1 = *dr->fComparisonBitmap->getAddr32(x, y);
SkPMColor trueDifference = compute_diff_pmcolor(c0, c1);
SkPMColor outputDifference = diffFunction(c0, c1);
uint32_t thisR = SkGetPackedR32(trueDifference);
uint32_t thisG = SkGetPackedG32(trueDifference);
uint32_t thisB = SkGetPackedB32(trueDifference);
totalMismatchR += thisR;
totalMismatchG += thisG;
totalMismatchB += thisB;
// In HSV, value is defined as max RGB component.
int value = MAX3(thisR, thisG, thisB);
dr->fWeightedFraction += ((float) value) / 255;
if (thisR > dr->fMaxMismatchR) {
dr->fMaxMismatchR = thisR;
}
if (thisG > dr->fMaxMismatchG) {
dr->fMaxMismatchG = thisG;
}
if (thisB > dr->fMaxMismatchB) {
dr->fMaxMismatchB = thisB;
}
if (!colors_match_thresholded(c0, c1, colorThreshold)) {
mismatchedPixels++;
*dr->fDifferenceBitmap->getAddr32(x, y) = outputDifference;
*dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_WHITE;
} else {
*dr->fDifferenceBitmap->getAddr32(x, y) = 0;
*dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_BLACK;
}
}
}
if (0 == mismatchedPixels) {
dr->fResult = kEqualPixels;
return;
}
dr->fResult = kDifferentPixels;
int pixelCount = w * h;
dr->fFractionDifference = ((float) mismatchedPixels) / pixelCount;
dr->fWeightedFraction /= pixelCount;
dr->fAverageMismatchR = ((float) totalMismatchR) / pixelCount;
dr->fAverageMismatchG = ((float) totalMismatchG) / pixelCount;
dr->fAverageMismatchB = ((float) totalMismatchB) / pixelCount;
}
static SkString filename_to_derived_filename (const SkString& filename,
const char *suffix) {
SkString diffName (filename);
const char* cstring = diffName.c_str();
int dotOffset = strrchr(cstring, '.') - cstring;
diffName.remove(dotOffset, diffName.size() - dotOffset);
diffName.append(suffix);
return diffName;
}
/// Given a image filename, returns the name of the file containing the
/// associated difference image.
static SkString filename_to_diff_filename (const SkString& filename) {
return filename_to_derived_filename(filename, "-diff.png");
}
/// Given a image filename, returns the name of the file containing the
/// "white" difference image.
static SkString filename_to_white_filename (const SkString& filename) {
return filename_to_derived_filename(filename, "-white.png");
}
static void release_bitmaps(DiffRecord* drp) {
delete drp->fBaseBitmap;
drp->fBaseBitmap = NULL;
delete drp->fComparisonBitmap;
drp->fComparisonBitmap = NULL;
delete drp->fDifferenceBitmap;
drp->fDifferenceBitmap = NULL;
delete drp->fWhiteBitmap;
drp->fWhiteBitmap = NULL;
}
/// If outputDir.isEmpty(), don't write out diff files.
static void create_and_write_diff_image(DiffRecord* drp,
DiffMetricProc dmp,
const int colorThreshold,
const SkString& outputDir,
const SkString& filename) {
const int w = drp->fBaseWidth;
const int h = drp->fBaseHeight;
drp->fDifferenceBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h);
drp->fDifferenceBitmap->allocPixels();
drp->fWhiteBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h);
drp->fWhiteBitmap->allocPixels();
SkASSERT(kUnknown == drp->fResult);
compute_diff(drp, dmp, colorThreshold);
SkASSERT(kUnknown != drp->fResult);
if ((kDifferentPixels == drp->fResult) && !outputDir.isEmpty()) {
SkString differencePath (outputDir);
differencePath.append(filename_to_diff_filename(filename));
write_bitmap(differencePath, drp->fDifferenceBitmap);
SkString whitePath (outputDir);
whitePath.append(filename_to_white_filename(filename));
write_bitmap(whitePath, drp->fWhiteBitmap);
}
release_bitmaps(drp);
}
/// Returns true if string contains any of these substrings.
static bool string_contains_any_of(const SkString& string,
const StringArray& substrings) {
for (int i = 0; i < substrings.count(); i++) {
if (string.contains(substrings[i]->c_str())) {
return true;
}
}
return false;
}
/// Iterate over dir and get all files that:
/// - match any of the substrings in matchSubstrings, but...
/// - DO NOT match any of the substrings in nomatchSubstrings
/// Returns the list of files in *files.
static void get_file_list(const SkString& dir,
const StringArray& matchSubstrings,
const StringArray& nomatchSubstrings,
FileArray *files) {
SkOSFile::Iter it(dir.c_str());
SkString filename;
while (it.next(&filename)) {
if (string_contains_any_of(filename, matchSubstrings) &&
!string_contains_any_of(filename, nomatchSubstrings)) {
files->push(new SkString(filename));
}
}
}
static void release_file_list(FileArray *files) {
files->deleteAll();
}
/// Comparison routines for qsort, sort by file names.
static int compare_file_name_metrics(SkString **lhs, SkString **rhs) {
return strcmp((*lhs)->c_str(), (*rhs)->c_str());
}
/// Creates difference images, returns the number that have a 0 metric.
/// If outputDir.isEmpty(), don't write out diff files.
static void create_diff_images (DiffMetricProc dmp,
const int colorThreshold,
RecordArray* differences,
const SkString& baseDir,
const SkString& comparisonDir,
const SkString& outputDir,
const StringArray& matchSubstrings,
const StringArray& nomatchSubstrings,
DiffSummary* summary) {
SkASSERT(!baseDir.isEmpty());
SkASSERT(!comparisonDir.isEmpty());
FileArray baseFiles;
FileArray comparisonFiles;
get_file_list(baseDir, matchSubstrings, nomatchSubstrings, &baseFiles);
get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings,
&comparisonFiles);
if (!baseFiles.isEmpty()) {
qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
SkCastForQSort(compare_file_name_metrics));
}
if (!comparisonFiles.isEmpty()) {
qsort(comparisonFiles.begin(), comparisonFiles.count(),
sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
}
int i = 0;
int j = 0;
while (i < baseFiles.count() &&
j < comparisonFiles.count()) {
SkString basePath (baseDir);
basePath.append(*baseFiles[i]);
SkString comparisonPath (comparisonDir);
comparisonPath.append(*comparisonFiles[j]);
DiffRecord *drp = NULL;
int v = strcmp(baseFiles[i]->c_str(),
comparisonFiles[j]->c_str());
if (v < 0) {
// in baseDir, but not in comparisonDir
drp = new DiffRecord(*baseFiles[i], basePath, comparisonPath,
kComparisonMissing);
++i;
} else if (v > 0) {
// in comparisonDir, but not in baseDir
drp = new DiffRecord(*comparisonFiles[j], basePath, comparisonPath,
kBaseMissing);
++j;
} else {
// Found the same filename in both baseDir and comparisonDir.
drp = new DiffRecord(*baseFiles[i], basePath, comparisonPath);
SkASSERT(kUnknown == drp->fResult);
SkData* baseFileBits;
SkData* comparisonFileBits;
if (NULL == (baseFileBits = read_file(basePath.c_str()))) {
SkDebugf("WARNING: couldn't read base file <%s>\n",
basePath.c_str());
drp->fResult = kBaseMissing;
} else if (NULL == (comparisonFileBits = read_file(
comparisonPath.c_str()))) {
SkDebugf("WARNING: couldn't read comparison file <%s>\n",
comparisonPath.c_str());
drp->fResult = kComparisonMissing;
} else {
if (are_buffers_equal(baseFileBits, comparisonFileBits)) {
drp->fResult = kEqualBits;
} else if (get_bitmaps(baseFileBits, comparisonFileBits, drp)) {
create_and_write_diff_image(drp, dmp, colorThreshold,
outputDir, *baseFiles[i]);
} else {
drp->fResult = kDifferentOther;
}
}
if (baseFileBits) {
baseFileBits->unref();
}
if (comparisonFileBits) {
comparisonFileBits->unref();
}
++i;
++j;
}
SkASSERT(kUnknown != drp->fResult);
differences->push(drp);
summary->add(drp);
}
for (; i < baseFiles.count(); ++i) {
// files only in baseDir
SkString basePath (baseDir);
basePath.append(*baseFiles[i]);
SkString comparisonPath;
DiffRecord *drp = new DiffRecord(*baseFiles[i], basePath,
comparisonPath, kComparisonMissing);
differences->push(drp);
summary->add(drp);
}
for (; j < comparisonFiles.count(); ++j) {
// files only in comparisonDir
SkString basePath;
SkString comparisonPath(comparisonDir);
comparisonPath.append(*comparisonFiles[j]);
DiffRecord *drp = new DiffRecord(*comparisonFiles[j], basePath,
comparisonPath, kBaseMissing);
differences->push(drp);
summary->add(drp);
}
release_file_list(&baseFiles);
release_file_list(&comparisonFiles);
}
/// Make layout more consistent by scaling image to 240 height, 360 width,
/// or natural size, whichever is smallest.
static int compute_image_height (int height, int width) {
int retval = 240;
if (height < retval) {
retval = height;
}
float scale = (float) retval / height;
if (width * scale > 360) {
scale = (float) 360 / width;
retval = static_cast<int>(height * scale);
}
return retval;
}
static void print_table_header (SkFILEWStream* stream,
const int matchCount,
const int colorThreshold,
const RecordArray& differences,
const SkString &baseDir,
const SkString &comparisonDir,
bool doOutputDate=false) {
stream->writeText("<table>\n");
stream->writeText("<tr><th>");
if (doOutputDate) {
SkTime::DateTime dt;
SkTime::GetDateTime(&dt);
stream->writeText("SkDiff run at ");
stream->writeDecAsText(dt.fHour);
stream->writeText(":");
if (dt.fMinute < 10) {
stream->writeText("0");
}
stream->writeDecAsText(dt.fMinute);
stream->writeText(":");
if (dt.fSecond < 10) {
stream->writeText("0");
}
stream->writeDecAsText(dt.fSecond);
stream->writeText("<br>");
}
stream->writeDecAsText(matchCount);
stream->writeText(" of ");
stream->writeDecAsText(differences.count());
stream->writeText(" images matched ");
if (colorThreshold == 0) {
stream->writeText("exactly");
} else {
stream->writeText("within ");
stream->writeDecAsText(colorThreshold);
stream->writeText(" color units per component");
}
stream->writeText(".<br>");
stream->writeText("</th>\n<th>");
stream->writeText("every different pixel shown in white");
stream->writeText("</th>\n<th>");
stream->writeText("color difference at each pixel");
stream->writeText("</th>\n<th>");
stream->writeText(baseDir.c_str());
stream->writeText("</th>\n<th>");
stream->writeText(comparisonDir.c_str());
stream->writeText("</th>\n");
stream->writeText("</tr>\n");
}
static void print_pixel_count (SkFILEWStream* stream,
const DiffRecord& diff) {
stream->writeText("<br>(");
stream->writeDecAsText(static_cast<int>(diff.fFractionDifference *
diff.fBaseWidth *
diff.fBaseHeight));
stream->writeText(" pixels)");
/*
stream->writeDecAsText(diff.fWeightedFraction *
diff.fBaseWidth *
diff.fBaseHeight);
stream->writeText(" weighted pixels)");
*/
}
static void print_label_cell (SkFILEWStream* stream,
const DiffRecord& diff) {
char metricBuf [20];
stream->writeText("<td><b>");
stream->writeText(diff.fFilename.c_str());
stream->writeText("</b><br>");
switch (diff.fResult) {
case kEqualBits:
SkDEBUGFAIL("should not encounter DiffRecord with kEqualBits here");
return;
case kEqualPixels:
SkDEBUGFAIL("should not encounter DiffRecord with kEqualPixels here");
return;
case kDifferentSizes:
stream->writeText("Image sizes differ</td>");
return;
case kDifferentPixels:
sprintf(metricBuf, "%12.4f%%", 100 * diff.fFractionDifference);
stream->writeText(metricBuf);
stream->writeText(" of pixels differ");
stream->writeText("\n (");
sprintf(metricBuf, "%12.4f%%", 100 * diff.fWeightedFraction);
stream->writeText(metricBuf);
stream->writeText(" weighted)");
// Write the actual number of pixels that differ if it's < 1%
if (diff.fFractionDifference < 0.01) {
print_pixel_count(stream, diff);
}
stream->writeText("<br>Average color mismatch ");
stream->writeDecAsText(static_cast<int>(MAX3(diff.fAverageMismatchR,
diff.fAverageMismatchG,
diff.fAverageMismatchB)));
stream->writeText("<br>Max color mismatch ");
stream->writeDecAsText(MAX3(diff.fMaxMismatchR,
diff.fMaxMismatchG,
diff.fMaxMismatchB));
stream->writeText("</td>");
break;
case kDifferentOther:
stream->writeText("Files differ; unable to parse one or both files</td>");
return;
case kBaseMissing:
stream->writeText("Missing from baseDir</td>");
return;
case kComparisonMissing:
stream->writeText("Missing from comparisonDir</td>");
return;
default:
SkDEBUGFAIL("encountered DiffRecord with unknown result type");
return;
}
}
static void print_image_cell (SkFILEWStream* stream,
const SkString& path,
int height) {
stream->writeText("<td><a href=\"");
stream->writeText(path.c_str());
stream->writeText("\"><img src=\"");
stream->writeText(path.c_str());
stream->writeText("\" height=\"");
stream->writeDecAsText(height);
stream->writeText("px\"></a></td>");
}
static void print_text_cell (SkFILEWStream* stream, const char* text) {
stream->writeText("<td align=center>");
if (NULL != text) {
stream->writeText(text);
}
stream->writeText("</td>");
}
static void print_diff_with_missing_file(SkFILEWStream* stream,
DiffRecord& diff,
const SkString& relativePath) {
stream->writeText("<tr>\n");
print_label_cell(stream, diff);
stream->writeText("<td>N/A</td>");
stream->writeText("<td>N/A</td>");
if (kBaseMissing != diff.fResult) {
int h, w;
if (!get_bitmap_height_width(diff.fBasePath, &h, &w)) {
stream->writeText("<td>N/A</td>");
} else {
int height = compute_image_height(h, w);
if (!diff.fBasePath.startsWith(PATH_DIV_STR)) {
diff.fBasePath.prepend(relativePath);
}
print_image_cell(stream, diff.fBasePath, height);
}
} else {
stream->writeText("<td>N/A</td>");
}
if (kComparisonMissing != diff.fResult) {
int h, w;
if (!get_bitmap_height_width(diff.fComparisonPath, &h, &w)) {
stream->writeText("<td>N/A</td>");
} else {
int height = compute_image_height(h, w);
if (!diff.fComparisonPath.startsWith(PATH_DIV_STR)) {
diff.fComparisonPath.prepend(relativePath);
}
print_image_cell(stream, diff.fComparisonPath, height);
}
} else {
stream->writeText("<td>N/A</td>");
}
stream->writeText("</tr>\n");
stream->flush();
}
static void print_diff_page (const int matchCount,
const int colorThreshold,
const RecordArray& differences,
const SkString& baseDir,
const SkString& comparisonDir,
const SkString& outputDir) {
SkASSERT(!baseDir.isEmpty());
SkASSERT(!comparisonDir.isEmpty());
SkASSERT(!outputDir.isEmpty());
SkString outputPath (outputDir);
outputPath.append("index.html");
//SkFILEWStream outputStream ("index.html");
SkFILEWStream outputStream (outputPath.c_str());
// Need to convert paths from relative-to-cwd to relative-to-outputDir
// FIXME this doesn't work if there are '..' inside the outputDir
unsigned int ui;
SkString relativePath;
for (ui = 0; ui < outputDir.size(); ui++) {
if (outputDir[ui] == PATH_DIV_CHAR) {
relativePath.append(".." PATH_DIV_STR);
}
}
outputStream.writeText("<html>\n<body>\n");
print_table_header(&outputStream, matchCount, colorThreshold, differences,
baseDir, comparisonDir);
int i;
for (i = 0; i < differences.count(); i++) {
DiffRecord* diff = differences[i];
switch (diff->fResult) {
// Cases in which there is no diff to report.
case kEqualBits:
case kEqualPixels:
continue;
// Cases in which we want a detailed pixel diff.
case kDifferentPixels:
break;
// Cases in which the files differed, but we can't display the diff.
case kDifferentSizes:
case kDifferentOther:
case kBaseMissing:
case kComparisonMissing:
print_diff_with_missing_file(&outputStream, *diff, relativePath);
continue;
default:
SkDEBUGFAIL("encountered DiffRecord with unknown result type");
continue;
}
if (!diff->fBasePath.startsWith(PATH_DIV_STR)) {
diff->fBasePath.prepend(relativePath);
}
if (!diff->fComparisonPath.startsWith(PATH_DIV_STR)) {
diff->fComparisonPath.prepend(relativePath);
}
int height = compute_image_height(diff->fBaseHeight, diff->fBaseWidth);
outputStream.writeText("<tr>\n");
print_label_cell(&outputStream, *diff);
print_image_cell(&outputStream,
filename_to_white_filename(diff->fFilename), height);
print_image_cell(&outputStream,
filename_to_diff_filename(diff->fFilename), height);
print_image_cell(&outputStream, diff->fBasePath, height);
print_image_cell(&outputStream, diff->fComparisonPath, height);
outputStream.writeText("</tr>\n");
outputStream.flush();
}
outputStream.writeText("</table>\n");
outputStream.writeText("</body>\n</html>\n");
outputStream.flush();
}
static void usage (char * argv0) {
SkDebugf("Skia baseline image diff tool\n");
SkDebugf("\n"
"Usage: \n"
" %s <baseDir> <comparisonDir> [outputDir] \n"
, argv0, argv0);
SkDebugf("\n"
"Arguments: \n"
" --nodiffs: don't write out image diffs or index.html, just generate \n"
" report on stdout \n"
" --threshold <n>: only report differences > n (per color channel) [default 0]\n"
" --match: compare files whose filenames contain this substring; if \n"
" unspecified, compare ALL files. \n"
" this flag may be repeated to add more matching substrings. \n"
" --nomatch: regardless of --match, DO NOT compare files whose filenames \n"
" contain this substring. \n"
" this flag may be repeated to add more forbidden substrings. \n"
" --sortbymismatch: sort by average color channel mismatch\n");
SkDebugf(
" --sortbymaxmismatch: sort by worst color channel mismatch;\n"
" break ties with -sortbymismatch\n"
" [default sort is by fraction of pixels mismatching]\n");
SkDebugf(
" --weighted: sort by # pixels different weighted by color difference\n");
SkDebugf(
" baseDir: directory to read baseline images from.\n");
SkDebugf(
" comparisonDir: directory to read comparison images from\n");
SkDebugf(
" outputDir: directory to write difference images and index.html to; \n"
" defaults to comparisonDir \n");
}
int main (int argc, char ** argv) {
DiffMetricProc diffProc = compute_diff_pmcolor;
int (*sortProc)(const void*, const void*) = SkCastForQSort(compare_diff_metrics);
// Maximum error tolerated in any one color channel in any one pixel before
// a difference is reported.
int colorThreshold = 0;
SkString baseDir;
SkString comparisonDir;
SkString outputDir;
StringArray matchSubstrings;
StringArray nomatchSubstrings;
bool generateDiffs = true;
RecordArray differences;
DiffSummary summary;
int i;
int numUnflaggedArguments = 0;
for (i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--help")) {
usage(argv[0]);
return 0;
}
if (!strcmp(argv[i], "--nodiffs")) {
generateDiffs = false;
continue;
}
if (!strcmp(argv[i], "--threshold")) {
colorThreshold = atoi(argv[++i]);
continue;
}
if (!strcmp(argv[i], "--match")) {
matchSubstrings.push(new SkString(argv[++i]));
continue;
}
if (!strcmp(argv[i], "--nomatch")) {
nomatchSubstrings.push(new SkString(argv[++i]));
continue;
}
if (!strcmp(argv[i], "--sortbymismatch")) {
sortProc = SkCastForQSort(compare_diff_mean_mismatches);
continue;
}
if (!strcmp(argv[i], "--sortbymaxmismatch")) {
sortProc = SkCastForQSort(compare_diff_max_mismatches);
continue;
}
if (!strcmp(argv[i], "--weighted")) {
sortProc = SkCastForQSort(compare_diff_weighted);
continue;
}
if (argv[i][0] != '-') {
switch (numUnflaggedArguments++) {
case 0:
baseDir.set(argv[i]);
continue;
case 1:
comparisonDir.set(argv[i]);
continue;
case 2:
outputDir.set(argv[i]);
continue;
default:
SkDebugf("extra unflagged argument <%s>\n", argv[i]);
usage(argv[0]);
return 0;
}
}
SkDebugf("Unrecognized argument <%s>\n", argv[i]);
usage(argv[0]);
return 0;
}
if (numUnflaggedArguments == 2) {
outputDir = comparisonDir;
} else if (numUnflaggedArguments != 3) {
usage(argv[0]);
return 0;
}
if (!baseDir.endsWith(PATH_DIV_STR)) {
baseDir.append(PATH_DIV_STR);
}
printf("baseDir is [%s]\n", baseDir.c_str());
if (!comparisonDir.endsWith(PATH_DIV_STR)) {
comparisonDir.append(PATH_DIV_STR);
}
printf("comparisonDir is [%s]\n", comparisonDir.c_str());
if (!outputDir.endsWith(PATH_DIV_STR)) {
outputDir.append(PATH_DIV_STR);
}
if (generateDiffs) {
printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
} else {
printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
outputDir.set("");
}
// Default substring matching:
// - No matter what, don't match any PDF files.
// We may want to change this later, but for now this maintains the filter
// that get_file_list() used to always apply.
// - If no matchSubstrings were specified, match ALL strings.
nomatchSubstrings.push(new SkString(".pdf"));
if (matchSubstrings.isEmpty()) {
matchSubstrings.push(new SkString(""));
}
create_diff_images(diffProc, colorThreshold, &differences,
baseDir, comparisonDir, outputDir,
matchSubstrings, nomatchSubstrings, &summary);
summary.print();
if (differences.count()) {
qsort(differences.begin(), differences.count(),
sizeof(DiffRecord*), sortProc);
}
if (generateDiffs) {
print_diff_page(summary.fNumMatches, colorThreshold, differences,
baseDir, comparisonDir, outputDir);
}
for (i = 0; i < differences.count(); i++) {
delete differences[i];
}
matchSubstrings.deleteAll();
nomatchSubstrings.deleteAll();
return summary.fNumMismatches;
}